Coverage for node / src / stigmem_node / routes / resolver.py: 88%
14 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-25 01:49 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-25 01:49 +0000
1"""Entity resolver route — spec §2.6.6 (v0.8).
3GET /v1/entities/resolve?uri=<raw>&top_k=<int>&threshold=<float>
5Three-layer entity resolution:
6 Layer 1 — canonical normalisation (deterministic)
7 Layer 2 — alias table lookup (entity_aliases)
8 Layer 3 — token-fuzzy scoring over live fact graph (same-type prefix)
10Returns the best resolved URI and scored candidates.
11"""
13from __future__ import annotations
15from typing import Annotated, Any
17from fastapi import APIRouter, Depends, HTTPException, Query
19from ..auth import Identity, resolve_identity
20from ..db import db
21from ..recall.entity_resolver import FUZZY_SCORE_THRESHOLD, resolve_entity
23router = APIRouter(prefix="/v1/entities", tags=["entities"])
26@router.get("/resolve")
27def resolve_entity_uri(
28 identity: Annotated[Identity, Depends(resolve_identity)],
29 uri: str = Query(..., description="Raw entity URI to resolve"),
30 top_k: int = Query(5, ge=1, le=20, description="Max Layer 3 fuzzy candidates to return"),
31 threshold: float = Query(
32 FUZZY_SCORE_THRESHOLD,
33 ge=0.0,
34 le=1.0,
35 description="Minimum fuzzy score threshold for Layer 3 candidates",
36 ),
37) -> dict[str, Any]:
38 """Resolve a raw entity URI using 3-layer fuzzy resolution (Spec-01-Fact-Model).
40 Layer 1: canonical normalisation (case/whitespace collapse).
41 Layer 2: alias table lookup (explicit pre-registered mappings).
42 Layer 3: token-fuzzy scoring over entities of the same type prefix in the fact graph.
44 Returns the best resolved URI and scored candidates with match details.
45 """
46 if not identity.can_read(): 46 ↛ 47line 46 didn't jump to line 47 because the condition on line 46 was never true
47 raise HTTPException(status_code=403, detail="read permission required")
49 with db() as conn:
50 result = resolve_entity(uri, conn, top_k=top_k, threshold=threshold)
52 return {
53 "query": result.query,
54 "canonical": result.canonical,
55 "best": result.best,
56 "resolution_layer": (
57 1
58 if result.layer1_match
59 else 2
60 if result.layer2_match
61 else 3
62 if result.layer3_candidates
63 else None
64 ),
65 "layer1_match": result.layer1_match,
66 "layer2_match": result.layer2_match,
67 "layer3_candidates": [
68 {
69 "uri": c.uri,
70 "score": round(c.score, 4),
71 "match_note": c.match_note,
72 }
73 for c in result.layer3_candidates
74 ],
75 }