Spaces:
Sleeping
Sleeping
File size: 6,402 Bytes
2e818da | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | 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")
@router.get("", response_model=CitationGraph)
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
@router.post("/refresh", response_model=CitationGraph)
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
@router.post("/references/add-to-project", response_model=PaperAcquisitionResult)
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)
@router.post("/explore", response_model=CitationExplorationResponse)
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
@router.post("/explore/enrich-edges", response_model=CitationExplorationDelta)
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
@router.post("/explore/details", response_model=CitationGraphNode)
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
@router.post("/explore/authors", response_model=CitationExplorationResponse)
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
@router.post("/explore/download", response_model=PaperAcquisitionResult)
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
|