feat: add POST /api/projects/{id}/cards/generate endpoint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Saberlve 2026-04-15 20:08:14 +08:00
parent 9ba1668a66
commit 4350f9b3a1
2 changed files with 107 additions and 0 deletions

View File

@ -35,6 +35,10 @@ class ImportItemKeysRequest(BaseModel):
item_keys: list[str] = Field(min_length=1)
class GenerateCardsRequest(BaseModel):
item_keys: list[str] = Field(min_length=1)
def create_app(
config: AppConfig,
*,
@ -201,6 +205,70 @@ def create_app(
)
return {"project_id": project_id, "items": items, "pending_count": pending_count, "done_count": done_count}
@app.post("/api/projects/{project_id}/cards/generate")
def generate_cards(project_id: str, payload: GenerateCardsRequest | dict[str, object]) -> dict[str, object]:
project_file = config.workspace_dir / "projects" / project_id / "project.json"
if not project_file.exists():
raise HTTPException(status_code=404, detail="Project not found")
if isinstance(payload, dict):
item_keys = [str(value) for value in payload.get("item_keys", [])]
else:
item_keys = [str(value) for value in payload.item_keys]
project_payload = _read_json(project_file)
llm_payload = project_payload.get("llm", {})
try:
resolved_client = llm_client or create_card_generation_client(
str(llm_payload.get("provider", "deterministic")),
str(llm_payload.get("model", "deterministic")),
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
builder = CardBuilder(config.workspace_dir, resolved_client)
items = reader.read_items(item_keys)
items_index_path = config.workspace_dir / "library" / "index" / "items.json"
items_index = project_service._read_json(items_index_path)
cards_index = project_service._read_json(config.workspace_dir / "library" / "index" / "cards.json")
generated: list[str] = []
failed: list[str] = []
response_items: list[dict[str, object]] = []
for item in items:
item_key = item.item_key
item_data = items_index.get(item_key, {})
try:
builder.build_or_update(item)
generated.append(item_key)
card_status = "done"
except Exception:
failed.append(item_key)
card_status = "failed"
response_items.append(
{
"item_key": item_key,
"title": item_data.get("title", item.title if hasattr(item, 'title') else "Untitled"),
"creators": item_data.get("creators", item.creators if hasattr(item, 'creators') else []),
"year": item_data.get("year", item.year if hasattr(item, 'year') else None),
"item_type": item_data.get("item_type", item.item_type if hasattr(item, 'item_type') else "unknown"),
"card_status": card_status,
}
)
if generated:
project_service.add_items(project_id, generated)
return {
"project_id": project_id,
"generated": generated,
"failed": failed,
"items": response_items,
}
@app.delete("/api/projects/{project_id}/items/{item_key}")
def remove_project_item(project_id: str, item_key: str) -> dict[str, object]:
return project_service.remove_item(project_id, item_key)

View File

@ -3,6 +3,7 @@ from pathlib import Path
from tests.fixtures.build_zotero_fixture import build_fixture_zotero_dir
from zotero_kb.api import CreateProjectRequest, WritingPromptRequest, create_app
from zotero_kb.api import GenerateCardsRequest
from zotero_kb.config import AppConfig
@ -261,3 +262,41 @@ def test_import_item_keys_does_not_generate_cards(tmp_path: Path) -> None:
else:
# File not existing means no cards were generated, which is expected
pass
def test_cards_generate_creates_cards_for_pending_items(tmp_path: Path) -> None:
"""cards/generate endpoint should generate cards for items that have pending status."""
config = make_test_config(tmp_path)
config.bridge_file.unlink()
app = create_app(config, llm_client=FakeLlmClient())
create_project = _route(app, "/api/projects", "POST")
import_keys = _route(app, "/api/projects/{project_id}/imports/item-keys", "POST")
import_state = _route(app, "/api/projects/{project_id}/import-state", "GET")
generate_cards = _route(app, "/api/projects/{project_id}/cards/generate", "POST")
create_project(
CreateProjectRequest(
project_id="thesis-ch2",
name="Thesis Chapter 2",
llm_provider="openai",
llm_model="gpt-5-mini",
)
)
# Import item keys first (writes to items.json but doesn't generate cards)
import_keys("thesis-ch2", {"item_keys": ["PAPER0001"]})
# Verify card_status is pending before generation
state_before = import_state("thesis-ch2")
assert state_before["items"][0]["card_status"] == "pending"
# Call generate endpoint
response = generate_cards("thesis-ch2", GenerateCardsRequest(item_keys=["PAPER0001"]))
# Verify response structure
assert response["project_id"] == "thesis-ch2"
assert response["generated"] == ["PAPER0001"]
assert response["failed"] == []
assert len(response["items"]) == 1
assert response["items"][0]["item_key"] == "PAPER0001"
assert response["items"][0]["card_status"] == "done"