- Introduced `card_language` attribute in ProjectRecord and Workspace classes to handle multiple languages for project cards. - Updated project creation and renaming methods to accept and store the card language. - Enhanced ZoteroReader to read and return attachment metadata, including language-specific summaries and claims. - Modified LLM client to generate card content based on the specified language, supporting both English and Chinese. - Updated tests to cover new functionality, ensuring correct handling of multilingual card generation and retrieval. - Adjusted UI tests to verify the presence of language selection options and proper rendering of multilingual content.
230 lines
8.9 KiB
Python
230 lines
8.9 KiB
Python
import json
|
|
from pathlib import Path
|
|
|
|
from zotero_kb.cards import CardBuilder
|
|
from zotero_kb.config import AppConfig
|
|
from zotero_kb.projects import ProjectService
|
|
from zotero_kb.workspace import Workspace
|
|
from zotero_kb.zotero_reader import ZoteroItemRecord
|
|
|
|
|
|
class FakeLlmClient:
|
|
def generate_card(self, source_bundle: dict[str, object]) -> dict[str, object]:
|
|
title = str(source_bundle["title"])
|
|
language = str(source_bundle.get("card_language", "en"))
|
|
if "Scoped" in title:
|
|
summary = "项目范围检索能改善写作。" if language == "zh" else "Project-scoped retrieval improves drafting."
|
|
claims = ["项目范围限定能提升引文精度。"] if language == "zh" else ["Project scoping improves citation precision."]
|
|
else:
|
|
summary = "无关检索基线。" if language == "zh" else "Unrelated retrieval baseline."
|
|
claims = ["基线检索范围较宽。"] if language == "zh" else ["Baseline retrieval is broad."]
|
|
return {
|
|
"summary": summary,
|
|
"core_claims": claims,
|
|
"methods": ["Method details."],
|
|
"evidence": ["Evidence details."],
|
|
"quotable_passages": claims,
|
|
"writing_hints": [summary],
|
|
"keywords": ["retrieval"],
|
|
}
|
|
|
|
|
|
def _build_item(item_key: str, title: str) -> ZoteroItemRecord:
|
|
return ZoteroItemRecord(
|
|
item_key=item_key,
|
|
title=title,
|
|
creators=["Alice Smith"],
|
|
year="2024",
|
|
item_type="journalArticle",
|
|
abstract=title,
|
|
tags=["retrieval"],
|
|
collection_paths=[["Theory", "Drafting"]],
|
|
notes=[title],
|
|
attachments=[],
|
|
attachment_texts=[title],
|
|
)
|
|
|
|
|
|
def test_add_item_to_project_updates_selected_items_and_project_index(tmp_path: Path) -> None:
|
|
config = AppConfig(workspace_dir=tmp_path / "workspace", zotero_data_dir=tmp_path / "zotero")
|
|
workspace = Workspace(config)
|
|
workspace.create_project("thesis-ch2", "Thesis Chapter 2", "openai", "gpt-5-mini", "en")
|
|
builder = CardBuilder(config.workspace_dir, FakeLlmClient())
|
|
builder.build_or_update(_build_item("PAPER0001", "Scoped Retrieval for Drafting"))
|
|
|
|
service = ProjectService(config.workspace_dir)
|
|
payload = service.add_items("thesis-ch2", ["PAPER0001"])
|
|
|
|
assert payload["selected_items"] == ["PAPER0001"]
|
|
assert payload["cards"][0]["item_key"] == "PAPER0001"
|
|
|
|
selected_items = json.loads(
|
|
(config.workspace_dir / "projects" / "thesis-ch2" / "selected-items.json").read_text(encoding="utf-8")
|
|
)
|
|
assert selected_items == ["PAPER0001"]
|
|
|
|
|
|
def test_remove_item_from_project_updates_selected_items(tmp_path: Path) -> None:
|
|
config = AppConfig(workspace_dir=tmp_path / "workspace", zotero_data_dir=tmp_path / "zotero")
|
|
workspace = Workspace(config)
|
|
workspace.create_project("thesis-ch2", "Thesis Chapter 2", "openai", "gpt-5-mini", "en")
|
|
builder = CardBuilder(config.workspace_dir, FakeLlmClient())
|
|
builder.build_or_update(_build_item("PAPER0001", "Scoped Retrieval for Drafting"))
|
|
|
|
service = ProjectService(config.workspace_dir)
|
|
service.add_items("thesis-ch2", ["PAPER0001"])
|
|
payload = service.remove_item("thesis-ch2", "PAPER0001")
|
|
|
|
assert payload["selected_items"] == []
|
|
|
|
|
|
def test_add_item_to_project_returns_pending_item_when_card_not_generated(tmp_path: Path) -> None:
|
|
config = AppConfig(workspace_dir=tmp_path / "workspace", zotero_data_dir=tmp_path / "zotero")
|
|
workspace = Workspace(config)
|
|
workspace.create_project("thesis-ch2", "Thesis Chapter 2", "openai", "gpt-5-mini", "zh")
|
|
|
|
items_index_path = config.workspace_dir / "library" / "index" / "items.json"
|
|
items_index_path.parent.mkdir(parents=True, exist_ok=True)
|
|
items_index_path.write_text(
|
|
json.dumps(
|
|
{
|
|
"PAPER0001": {
|
|
"item_key": "PAPER0001",
|
|
"title": "Pending Item",
|
|
"creators": ["Alice Smith"],
|
|
"year": "2024",
|
|
"item_type": "journalArticle",
|
|
}
|
|
},
|
|
ensure_ascii=False,
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
service = ProjectService(config.workspace_dir)
|
|
payload = service.add_items("thesis-ch2", ["PAPER0001"])
|
|
|
|
assert payload["selected_items"] == ["PAPER0001"]
|
|
assert len(payload["items"]) == 1
|
|
assert payload["items"][0]["item_key"] == "PAPER0001"
|
|
assert payload["items"][0]["card_status"] == "pending"
|
|
|
|
|
|
def test_project_view_treats_legacy_cards_as_english_only(tmp_path: Path) -> None:
|
|
config = AppConfig(workspace_dir=tmp_path / "workspace", zotero_data_dir=tmp_path / "zotero")
|
|
workspace = Workspace(config)
|
|
workspace.create_project("thesis-ch2", "Thesis Chapter 2", "openai", "gpt-5-mini", "zh")
|
|
|
|
cards_index_path = config.workspace_dir / "library" / "index" / "cards.json"
|
|
cards_index_path.parent.mkdir(parents=True, exist_ok=True)
|
|
cards_index_path.write_text(
|
|
json.dumps(
|
|
{
|
|
"PAPER0001": {
|
|
"item_key": "PAPER0001",
|
|
"title": "Legacy Card",
|
|
"summary": "English legacy summary",
|
|
"claims": ["Legacy claim"],
|
|
"citations": [],
|
|
"quotable_spans": [],
|
|
"writing_hints": [],
|
|
"attachments": [],
|
|
}
|
|
},
|
|
ensure_ascii=False,
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
items_index_path = config.workspace_dir / "library" / "index" / "items.json"
|
|
items_index_path.write_text(
|
|
json.dumps(
|
|
{
|
|
"PAPER0001": {
|
|
"item_key": "PAPER0001",
|
|
"title": "Legacy Card",
|
|
"creators": ["Alice Smith"],
|
|
"year": "2024",
|
|
"item_type": "journalArticle",
|
|
}
|
|
},
|
|
ensure_ascii=False,
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
service = ProjectService(config.workspace_dir)
|
|
payload = service.add_items("thesis-ch2", ["PAPER0001"])
|
|
|
|
assert payload["items"][0]["card_status"] == "pending"
|
|
workspace.rename_project("thesis-ch2", "Thesis Chapter 2", "en")
|
|
english_view = service.get_project_view("thesis-ch2")
|
|
assert english_view["items"][0]["card_status"] == "done"
|
|
assert english_view["items"][0]["summary"] == "English legacy summary"
|
|
|
|
|
|
def test_project_view_reads_only_current_language_card_variant(tmp_path: Path) -> None:
|
|
config = AppConfig(workspace_dir=tmp_path / "workspace", zotero_data_dir=tmp_path / "zotero")
|
|
workspace = Workspace(config)
|
|
workspace.create_project("thesis-ch2", "Thesis Chapter 2", "openai", "gpt-5-mini", "zh")
|
|
|
|
cards_index_path = config.workspace_dir / "library" / "index" / "cards.json"
|
|
cards_index_path.parent.mkdir(parents=True, exist_ok=True)
|
|
cards_index_path.write_text(
|
|
json.dumps(
|
|
{
|
|
"PAPER0001": {
|
|
"zh": {
|
|
"item_key": "PAPER0001",
|
|
"language": "zh",
|
|
"title": "Bilingual Card",
|
|
"summary": "中文摘要",
|
|
"claims": ["中文 claim"],
|
|
"citations": [],
|
|
"quotable_spans": [],
|
|
"writing_hints": [],
|
|
"attachments": [],
|
|
},
|
|
"en": {
|
|
"item_key": "PAPER0001",
|
|
"language": "en",
|
|
"title": "Bilingual Card",
|
|
"summary": "English summary",
|
|
"claims": ["English claim"],
|
|
"citations": [],
|
|
"quotable_spans": [],
|
|
"writing_hints": [],
|
|
"attachments": [],
|
|
},
|
|
}
|
|
},
|
|
ensure_ascii=False,
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
items_index_path = config.workspace_dir / "library" / "index" / "items.json"
|
|
items_index_path.write_text(
|
|
json.dumps(
|
|
{
|
|
"PAPER0001": {
|
|
"item_key": "PAPER0001",
|
|
"title": "Bilingual Card",
|
|
"creators": ["Alice Smith"],
|
|
"year": "2024",
|
|
"item_type": "journalArticle",
|
|
}
|
|
},
|
|
ensure_ascii=False,
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
service = ProjectService(config.workspace_dir)
|
|
zh_view = service.add_items("thesis-ch2", ["PAPER0001"])
|
|
assert zh_view["items"][0]["summary"] == "中文摘要"
|
|
assert zh_view["cards"][0]["language"] == "zh"
|
|
|
|
workspace.rename_project("thesis-ch2", "Thesis Chapter 2", "en")
|
|
en_view = service.get_project_view("thesis-ch2")
|
|
assert en_view["items"][0]["summary"] == "English summary"
|
|
assert en_view["cards"][0]["language"] == "en"
|