"""Phase 5 projection and GraphRAG search routes.""" from __future__ import annotations from typing import Annotated, Any from fastapi import APIRouter, Body, Depends, HTTPException, Query from pydantic import BaseModel, Field from sqlalchemy.orm import Session from ont_platform.api.db_deps import get_db from ont_platform.config import load_settings from ont_platform.core.graph.cypher_guard import ReadOnlyCypherGuard, UnsafeCypherError from ont_platform.core.graph.search import CandidateGraphSearchService from ont_platform.core.projection.rdf_to_neo4j import RDFToNeo4jProjector router = APIRouter(prefix="/api/v1/graph", tags=["graph"]) class ProjectionPreviewRequest(BaseModel): project_id: str = "default" triples: list[tuple[str, str, str]] provenance: dict[str, Any] = Field(default_factory=dict) class ReadOnlyCypherRequest(BaseModel): query: str limit: int | None = None @router.post("/projection/preview") async def preview_projection(request: Annotated[ProjectionPreviewRequest, Body()]) -> dict: projector = RDFToNeo4jProjector(project_id=request.project_id) result = await projector.preview_projection( request.triples, provenance=request.provenance, ) return {"status": "success", "projection": result.to_dict()} @router.post("/cypher/read") def sanitize_read_only_cypher(request: Annotated[ReadOnlyCypherRequest, Body()]) -> dict: settings = load_settings() guard = ReadOnlyCypherGuard(max_limit=settings.text2cypher_result_limit) try: sanitized = guard.sanitize(request.query, limit=request.limit) except UnsafeCypherError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc return { "status": "success", "read_only": True, "query": sanitized.query, "limit": sanitized.limit, } @router.get("/search") def search_graph( db: Annotated[Session, Depends(get_db)], q: Annotated[str, Query(min_length=1)], project_id: Annotated[str, Query()] = "default", limit: Annotated[int, Query(ge=1, le=100)] = 20, ) -> dict: settings = load_settings() effective_limit = min(limit, settings.graph_search_result_limit) results = CandidateGraphSearchService(db).search( project_id=project_id, query=q, limit=effective_limit, ) return { "status": "success", "query": q, "result_count": len(results), "results": [result.to_dict() for result in results], } __all__ = ["router"]