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

1"""Entity resolver route — spec §2.6.6 (v0.8). 

2 

3GET /v1/entities/resolve?uri=<raw>&top_k=<int>&threshold=<float> 

4 

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) 

9 

10Returns the best resolved URI and scored candidates. 

11""" 

12 

13from __future__ import annotations 

14 

15from typing import Annotated, Any 

16 

17from fastapi import APIRouter, Depends, HTTPException, Query 

18 

19from ..auth import Identity, resolve_identity 

20from ..db import db 

21from ..recall.entity_resolver import FUZZY_SCORE_THRESHOLD, resolve_entity 

22 

23router = APIRouter(prefix="/v1/entities", tags=["entities"]) 

24 

25 

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). 

39 

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. 

43 

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") 

48 

49 with db() as conn: 

50 result = resolve_entity(uri, conn, top_k=top_k, threshold=threshold) 

51 

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 }