Spaces:
Sleeping
Sleeping
| from fastapi import APIRouter, HTTPException | |
| from pydantic import BaseModel, ConfigDict, Field | |
| from app.services.citation_graph import CitationGraph, CitationGraphService | |
| from app.services.citation_exploration import ( | |
| CitationExplorationDelta, | |
| CitationExplorationResponse, | |
| CitationExplorationService, | |
| ) | |
| from app.services.citation_graph import CitationGraphNode | |
| from app.services.paper_acquisition import PaperAcquisitionResult, PaperAcquisitionService | |
| from app.services.project_activity import ProjectActivityService | |
| router = APIRouter(prefix="/api/citation-graph", tags=["citation-graph"]) | |
| _activity = ProjectActivityService() | |
| class RefreshCitationGraphRequest(BaseModel): | |
| model_config = ConfigDict(extra="forbid", strict=True) | |
| project_id: str = Field(min_length=1, max_length=128) | |
| class AddReferenceToProjectRequest(BaseModel): | |
| model_config = ConfigDict(extra="forbid", strict=True) | |
| project_id: str = Field(min_length=1, max_length=128) | |
| source_node_id: str = Field(min_length=1, max_length=200) | |
| candidate_id: str = Field(min_length=64, max_length=64, pattern=r"^[0-9a-f]{64}$") | |
| class ExploreCitationRequest(BaseModel): | |
| model_config = ConfigDict(extra="forbid", strict=True) | |
| project_id: str = Field(min_length=1, max_length=128) | |
| candidate_id: str = Field(min_length=64, max_length=64, pattern=r"^[0-9a-f]{64}$") | |
| class EnrichCitationEdgesRequest(BaseModel): | |
| model_config = ConfigDict(extra="forbid", strict=True) | |
| project_id: str = Field(min_length=1, max_length=128) | |
| neighborhood_key: str = Field(min_length=1, max_length=256) | |
| class ExploreAuthorRequest(ExploreCitationRequest): | |
| author_index: int = Field(ge=0, le=200) | |
| def _require_valid_project_id(project_id: str) -> None: | |
| if not CitationGraphService.is_valid_project_id(project_id): | |
| raise HTTPException(400, "Invalid project id") | |
| async def get_citation_graph(project_id: str): | |
| _require_valid_project_id(project_id) | |
| service = CitationGraphService() | |
| graph = service.load(project_id) | |
| project = service.project_service.load(project_id) | |
| if project is not None and project.files and ( | |
| not graph.nodes | |
| or graph.file_fingerprint != service.file_fingerprint(project_id) | |
| or not service.metadata_is_fresh(graph) | |
| ): | |
| return await service.refresh(project_id) | |
| return graph | |
| async def refresh_citation_graph(req: RefreshCitationGraphRequest): | |
| _require_valid_project_id(req.project_id) | |
| _activity.record(req.project_id, "citation_graph_refresh", "Citation map refreshing", status="running") | |
| graph = await CitationGraphService().refresh(req.project_id) | |
| _activity.record( | |
| req.project_id, | |
| "citation_graph_refresh", | |
| "Citation map refreshed", | |
| metadata={"nodes": len(graph.nodes), "edges": len(graph.edges)}, | |
| ) | |
| return graph | |
| async def add_reference_to_project(req: AddReferenceToProjectRequest): | |
| """Acquire only a candidate already persisted in this project's citation graph.""" | |
| _require_valid_project_id(req.project_id) | |
| graph = CitationGraphService().load(req.project_id) | |
| source = graph.nodes_by_id.get(req.source_node_id) | |
| if source is None or source.status != "owned": | |
| raise HTTPException(404, "Source paper not found") | |
| references = source.metadata.get("verified_unmatched_references") or [] | |
| candidate = next( | |
| (item for item in references if isinstance(item, dict) and item.get("candidate_id") == req.candidate_id), | |
| None, | |
| ) | |
| if candidate is None: | |
| raise HTTPException(404, "Verified reference not found") | |
| return await PaperAcquisitionService().acquire(req.project_id, candidate) | |
| async def explore_citation(req: ExploreCitationRequest): | |
| """Open a transient paper-centred graph from a server-verified candidate.""" | |
| _require_valid_project_id(req.project_id) | |
| try: | |
| return await CitationExplorationService().explore(req.project_id, req.candidate_id) | |
| except LookupError as exc: | |
| raise HTTPException(404, str(exc)) from exc | |
| except RuntimeError as exc: | |
| raise HTTPException(502, str(exc)) from exc | |
| async def enrich_explored_edges(req: EnrichCitationEdgesRequest): | |
| """Lazily add verified visible-paper citation edges to a transient graph.""" | |
| _require_valid_project_id(req.project_id) | |
| try: | |
| return await CitationExplorationService().enrich_edges(req.project_id, req.neighborhood_key) | |
| except LookupError as exc: | |
| raise HTTPException(404, str(exc)) from exc | |
| async def explored_paper_details(req: ExploreCitationRequest): | |
| """Resolve and cache rich metadata for one server-known paper.""" | |
| _require_valid_project_id(req.project_id) | |
| try: | |
| return await CitationExplorationService().details(req.project_id, req.candidate_id) | |
| except LookupError as exc: | |
| raise HTTPException(404, str(exc)) from exc | |
| except RuntimeError as exc: | |
| raise HTTPException(502, str(exc)) from exc | |
| async def explore_author(req: ExploreAuthorRequest): | |
| """Build the selected paper author's cached top-ten citation graph.""" | |
| _require_valid_project_id(req.project_id) | |
| try: | |
| return await CitationExplorationService().explore_author( | |
| req.project_id, req.candidate_id, req.author_index | |
| ) | |
| except LookupError as exc: | |
| raise HTTPException(404, str(exc)) from exc | |
| except RuntimeError as exc: | |
| raise HTTPException(502, str(exc)) from exc | |
| async def download_explored_paper(req: ExploreCitationRequest): | |
| """Download a candidate from the trusted exploration cache into the project.""" | |
| _require_valid_project_id(req.project_id) | |
| try: | |
| return await CitationExplorationService().download(req.project_id, req.candidate_id) | |
| except LookupError as exc: | |
| raise HTTPException(404, str(exc)) from exc | |