80 lines
2.5 KiB
Python
80 lines
2.5 KiB
Python
|
|
"""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"]
|