105 findings to zero. Most were mechanical — 67 unused imports, and assorted
f-strings without placeholders. Three groups needed a decision.
The 15 F821 "undefined name" were forward references, not runtime errors. Each
annotation is quoted — `-> "WikiService"`, `Optional["IngestionService"]` — with
the real import inside the function body to break an import cycle. A quoted
annotation is never evaluated, so the code ran; the names were simply
unresolvable to any checker. They now have a TYPE_CHECKING block, which costs
nothing at import time and keeps the cycle broken.
The 6 E402 split two ways. `import secrets`, `Security`, `Request` and
`HTTPBearer` in dependencies.py had drifted below several hundred lines of
factory functions for no reason — stdlib and fastapi, no cycle to avoid — and
moved up. The other three are deliberate and now say so: the VectorService and
GraphService aliases import back into dependencies.py, and main.py's routers
expect a configured app, so both must stay put.
Bare `except:` narrowed to `except Exception:` in three places, which stops them
swallowing KeyboardInterrupt and SystemExit.
The 5 unused locals were all genuinely dead. One is worth naming rather than
fixing: qdrant_client.delete()'s return value was bound and never read, so a
failed delete is indistinguishable from a successful one — the assignment is
gone, but nothing checks the status either way and that has not changed here.
`timing = {}` in _retrieve_parallel looked like it might mean the reported
per-leg timings were always zero; traced, and they come from output["timing"],
so the local was only vestigial.
426 passed, 29 skipped, unchanged. The app imports and the service aliases still
resolve, which is the check that mattered after moving imports in
dependencies.py.
The gate still prints "not gated here yet: test (T-56)" — lint is green, tests
remain unwired, and that is left visible rather than silently absent.
Co-Authored-By: Claude <noreply@anthropic.com>
132 lines
5.1 KiB
Python
132 lines
5.1 KiB
Python
"""
|
|
Graph models for Library Desk Neo4j operations.
|
|
|
|
Provides models for knowledge graph nodes, relationships, and queries.
|
|
"""
|
|
|
|
from pydantic import BaseModel, Field
|
|
from typing import List, Dict, Any, Optional
|
|
|
|
from src.core.multi_tenancy import RequiredUser
|
|
|
|
|
|
class GraphNode(BaseModel):
|
|
"""Graph node representation."""
|
|
id: str = Field(..., description="Node ID")
|
|
labels: List[str] = Field(..., description="Node labels")
|
|
properties: Dict[str, Any] = Field(default_factory=dict, description="Node properties")
|
|
|
|
|
|
class GraphRelationship(BaseModel):
|
|
"""Graph relationship representation."""
|
|
id: str = Field(..., description="Relationship ID")
|
|
type: str = Field(..., description="Relationship type")
|
|
start_node: str = Field(..., description="Start node ID")
|
|
end_node: str = Field(..., description="End node ID")
|
|
properties: Dict[str, Any] = Field(default_factory=dict, description="Relationship properties")
|
|
|
|
|
|
class GraphNodeDetail(BaseModel):
|
|
"""Detailed node with relationships."""
|
|
node: GraphNode = Field(..., description="Node data")
|
|
relationships: List[GraphRelationship] = Field(
|
|
default_factory=list,
|
|
description="Connected relationships"
|
|
)
|
|
related_nodes: List[GraphNode] = Field(
|
|
default_factory=list,
|
|
description="Connected nodes"
|
|
)
|
|
|
|
|
|
class CypherQueryRequest(BaseModel):
|
|
"""Request to execute a Cypher query."""
|
|
query: str = Field(..., description="Cypher query to execute")
|
|
parameters: Dict[str, Any] = Field(
|
|
default_factory=dict,
|
|
description="Query parameters"
|
|
)
|
|
user: RequiredUser = Field(
|
|
...,
|
|
description=(
|
|
"User identifier (tenant). Required. NOTE: raw Cypher queries are NOT "
|
|
"automatically scoped to this tenant — the endpoint is read-only and "
|
|
"intended for admin/debug use. Results may span all tenants."
|
|
)
|
|
)
|
|
|
|
|
|
class CypherQueryResponse(BaseModel):
|
|
"""Response from Cypher query execution."""
|
|
results: List[Dict[str, Any]] = Field(..., description="Query results")
|
|
count: int = Field(..., description="Number of results")
|
|
query_time_ms: float = Field(..., description="Query execution time in milliseconds")
|
|
|
|
|
|
class UpdateFromPageRequest(BaseModel):
|
|
"""Request to update graph from a wiki page."""
|
|
page_id: int = Field(..., description="Wiki page ID to process")
|
|
user: RequiredUser = Field(
|
|
...,
|
|
description="User identifier (tenant). Required — graph writes are scoped to this tenant's labels."
|
|
)
|
|
force_refresh: bool = Field(
|
|
default=False,
|
|
description="Force re-extraction even if page hasn't changed"
|
|
)
|
|
|
|
|
|
class EntityMention(BaseModel):
|
|
"""Extracted entity mention."""
|
|
text: str = Field(..., description="Entity text")
|
|
type: str = Field(..., description="Entity type (Person, Project, Concept, etc.)")
|
|
confidence: float = Field(default=1.0, description="Extraction confidence (0-1)")
|
|
|
|
|
|
class GraphUpdateSummary(BaseModel):
|
|
"""Summary of graph update operation."""
|
|
page_id: int = Field(..., description="Page ID processed")
|
|
page_title: str = Field(..., description="Page title")
|
|
nodes_created: int = Field(default=0, description="New nodes created")
|
|
nodes_updated: int = Field(default=0, description="Existing nodes updated")
|
|
relationships_created: int = Field(default=0, description="New relationships created")
|
|
entities_extracted: List[EntityMention] = Field(
|
|
default_factory=list,
|
|
description="Entities extracted from page"
|
|
)
|
|
processing_time_ms: float = Field(..., description="Processing time in milliseconds")
|
|
success: bool = Field(default=True, description="Whether update succeeded")
|
|
error_message: Optional[str] = Field(default=None, description="Error message if failed")
|
|
|
|
|
|
class NodeListResponse(BaseModel):
|
|
"""Response for node listing."""
|
|
nodes: List[GraphNode] = Field(..., description="List of nodes")
|
|
total: int = Field(..., description="Total number of nodes")
|
|
user: str = Field(..., description="User filter applied")
|
|
|
|
|
|
class MindMapNode(BaseModel):
|
|
"""Mind map node for visualization."""
|
|
id: str = Field(..., description="Node ID")
|
|
label: str = Field(..., description="Node label/name")
|
|
type: str = Field(..., description="Node type")
|
|
size: int = Field(default=10, description="Visual size")
|
|
color: Optional[str] = Field(default=None, description="Node color")
|
|
|
|
|
|
class MindMapLink(BaseModel):
|
|
"""Mind map link for visualization."""
|
|
source: str = Field(..., description="Source node ID")
|
|
target: str = Field(..., description="Target node ID")
|
|
type: str = Field(..., description="Relationship type")
|
|
strength: float = Field(default=1.0, description="Link strength")
|
|
|
|
|
|
class MindMapResponse(BaseModel):
|
|
"""Mind map data for D3.js or similar visualization."""
|
|
nodes: List[MindMapNode] = Field(..., description="Graph nodes")
|
|
links: List[MindMapLink] = Field(..., description="Graph edges")
|
|
center_node: str = Field(..., description="Central node ID")
|
|
depth: int = Field(..., description="Traversal depth")
|