Files
library-desk/src/routers/graph.py
T
jpmschweitzerandClaude Opus 4.5 02d728ac5b refactor: remove dead code and unused imports
- Remove unused get_default_user() from dependencies.py
- Remove unused imports from routers:
  - wiki.py: HTTPAuthorizationCredentials, Security
  - graph.py: Neo4jClient, WikiJSClient
  - hybrid_rag.py: VectorService, GraphService (duplicates)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-23 17:20:27 +01:00

252 lines
8.5 KiB
Python

"""
Graph router for Library Desk API.
Endpoints for Neo4j knowledge graph operations.
"""
from fastapi import APIRouter, HTTPException, Depends, Query
from typing import Optional, List
import logging
from src.models.graph import (
CypherQueryRequest, CypherQueryResponse,
UpdateFromPageRequest, GraphUpdateSummary,
NodeListResponse, GraphNodeDetail,
MindMapResponse
)
from src.services.graph_service import GraphService
from src.core.dependencies import Neo4jDep, WikiJSDep, verify_api_key
from src.core.multi_tenancy import DEFAULT_USER
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/graph", tags=["Graph"])
# Dependency to get graph service
def get_graph_service(
neo4j_client: Neo4jDep,
wiki_client: WikiJSDep
) -> GraphService:
"""Get graph service instance."""
return GraphService(neo4j_client, wiki_client)
@router.post("/query", response_model=CypherQueryResponse)
async def execute_cypher_query(
request: CypherQueryRequest,
graph_service: GraphService = Depends(get_graph_service),
api_key: str = Depends(verify_api_key)
):
"""
Execute a user-scoped Cypher query.
The query is automatically scoped to the user's data for security.
This prevents users from accessing other users' graph data.
**Example Request:**
```json
{
"query": "MATCH (d:Document) RETURN d LIMIT 10",
"parameters": {},
"user": "jpmschweitzer"
}
```
**Security:** Query is automatically scoped with user label.
"""
try:
return await graph_service.execute_query(
query=request.query,
parameters=request.parameters,
user=request.user
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Cypher query failed: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Query execution failed")
@router.get("/nodes", response_model=NodeListResponse)
async def list_nodes(
user: str = Query(default=DEFAULT_USER, description="User identifier"),
node_type: Optional[str] = Query(default=None, description="Node type filter"),
limit: int = Query(default=100, ge=1, le=500, description="Maximum nodes"),
graph_service: GraphService = Depends(get_graph_service),
api_key: str = Depends(verify_api_key)
):
"""
List graph nodes for a user.
Optionally filter by node type (Document, Person, Project, Concept, etc.).
**Example:** `/graph/nodes?user=jpmschweitzer&node_type=Document&limit=50`
"""
try:
return await graph_service.list_nodes(
user=user,
node_type=node_type,
limit=limit
)
except Exception as e:
logger.error(f"Failed to list nodes: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Failed to list nodes")
@router.get("/nodes/{node_id}", response_model=GraphNodeDetail)
async def get_node(
node_id: str,
user: str = Query(default=DEFAULT_USER, description="User identifier"),
graph_service: GraphService = Depends(get_graph_service),
api_key: str = Depends(verify_api_key)
):
"""
Get detailed information about a graph node.
Returns the node, its relationships, and connected nodes.
**Example:** `/graph/nodes/4:abc123def:0?user=jpmschweitzer`
"""
try:
node = await graph_service.get_node(node_id, user)
if not node:
raise HTTPException(status_code=404, detail=f"Node {node_id} not found")
return node
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to get node {node_id}: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Failed to get node")
@router.post("/update-from-page/{page_id}", response_model=GraphUpdateSummary)
async def update_graph_from_page(
page_id: int,
user: str = Query(default=DEFAULT_USER, description="User identifier"),
force_refresh: bool = Query(default=False, description="Force re-extraction"),
graph_service: GraphService = Depends(get_graph_service),
api_key: str = Depends(verify_api_key)
):
"""
Update knowledge graph from a wiki page.
This endpoint:
1. Fetches the page from Wiki.js
2. Extracts entities (people, projects, concepts, technologies)
3. Creates/updates Document node
4. Creates entity nodes and MENTIONS relationships
5. Returns summary of what was updated
**Use Cases:**
- Called automatically after page creation/update (via BackgroundTasks)
- Called manually by user/Librarian to refresh graph
- Called by Scheduler for batch processing
**Example:** `POST /graph/update-from-page/4?user=jpmschweitzer`
**Returns:** Summary with nodes/relationships created and entities extracted
"""
try:
summary = await graph_service.update_from_page(
page_id=page_id,
user=user,
force_refresh=force_refresh
)
if not summary.success:
raise HTTPException(
status_code=500,
detail=f"Graph update failed: {summary.error_message}"
)
return summary
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to update graph from page {page_id}: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Graph update failed")
@router.post("/mindmap", response_model=MindMapResponse)
async def generate_mindmap(
center_node_id: str = Query(..., description="Central node ID"),
user: str = Query(default=DEFAULT_USER, description="User identifier"),
depth: int = Query(default=2, ge=1, le=5, description="Traversal depth"),
graph_service: GraphService = Depends(get_graph_service),
api_key: str = Depends(verify_api_key)
):
"""
Generate mind map data for visualization.
Traverses the graph from a center node and returns nodes/links
in a format suitable for D3.js or similar visualization libraries.
**Parameters:**
- `center_node_id`: The node to center the mind map on
- `depth`: How many hops away from center to include (1-5)
- `user`: User identifier for scoping
**Example:** `POST /graph/mindmap?center_node_id=4:abc:0&depth=2`
**Returns:** Nodes and links for visualization
"""
try:
return await graph_service.generate_mindmap(
center_node_id=center_node_id,
user=user,
depth=depth
)
except Exception as e:
logger.error(f"Failed to generate mindmap: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Mindmap generation failed")
@router.post("/generate-entity-pages")
async def generate_entity_pages(
user: str = Query(default=DEFAULT_USER, description="User identifier"),
min_mentions: int = Query(default=5, ge=1, le=100, description="Minimum mentions threshold"),
entity_types: Optional[List[str]] = Query(default=None, description="Entity types to process"),
graph_service: GraphService = Depends(get_graph_service),
api_key: str = Depends(verify_api_key)
):
"""
Generate wiki stub pages for graph entities.
Creates pages in `/entities/{type}/{name}` namespace for entities
that have been mentioned in multiple documents. This creates a
bidirectional knowledge graph ↔ wiki synchronization.
**Threshold:** Entities must be mentioned in at least `min_mentions` documents (default: 5)
**Auto-stub flow:**
1. Find entities with >= min_mentions
2. Check if entity already has a wiki page
3. If not, create stub page with:
- List of mentioning documents
- Related entities (co-occurring)
- Auto-generated tag to prevent feedback loops
**Feedback loop protection:** Pages tagged with `entity-stub` skip entity extraction
**Parameters:**
- `min_mentions`: Minimum number of document mentions required (default: 5)
- `entity_types`: List of types to process (default: Person, Technology, Concept, Project)
- `user`: User identifier for scoping
**Example:** `POST /graph/generate-entity-pages?min_mentions=3&user=jpmschweitzer`
**Returns:** Summary with list of pages created and skipped
"""
try:
result = await graph_service.generate_entity_stubs(
user=user,
min_mentions=min_mentions,
entity_types=entity_types
)
return result
except Exception as e:
logger.error(f"Failed to generate entity pages: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Entity page generation failed")