EmpathRAG / src /pipeline /service_graph.py
MukulRay's picture
Response quality: greeting/goodbye/meta handlers, slang routing, lexical variety, last-verified badge, markdown norm, crisis variants
aedf4f8
Raw
History Blame
3.56 kB
"""Resource registry loader and matcher for EmpathRAG Core support routing.
The filename remains `service_graph.py` for compatibility, but the paper/demo
concept is a resource registry of service objects, not a graph algorithm.
"""
from __future__ import annotations
from dataclasses import dataclass
import json
from pathlib import Path
DEFAULT_SERVICE_GRAPH = Path("data/curated/service_graph.jsonl")
@dataclass(frozen=True)
class ServiceNode:
service_id: str
resource_name: str
description: str
urgency_level: str
safety_tiers: list[str]
route_types: list[str]
audience: list[str]
issue_types: list[str]
confidentiality_status: str
hours: str
contact_mode: list[str]
contact: str
location: str
source_url: str
source_authority: str
last_verified: str
usage_modes: list[str]
do_not_use_for: list[str]
notes: str
# Optional list of authoritative documents the user is encouraged to read
# directly (CPT form, OPT guide, RCL/Academic Difficulty policy, etc.).
# Each entry: {title, url, document_type, embeddable, requires_login}.
# Defaults empty so existing rows without the field still load.
documents: list[dict] = ()
@classmethod
def from_dict(cls, row: dict) -> "ServiceNode":
# Drop unknown keys so a future schema addition doesn't break older
# callers, and supply default for `documents` if missing.
allowed = {f.name for f in cls.__dataclass_fields__.values()}
kwargs = {k: v for k, v in row.items() if k in allowed}
kwargs.setdefault("documents", [])
return cls(**kwargs)
def as_source(self, why: str = "resource registry match") -> dict:
usage_mode = self.usage_modes[0] if self.usage_modes else "retrieval"
return {
"text": self.description,
"title": self.resource_name,
"source_name": self.resource_name,
"url": self.source_url,
"topic": ",".join(self.route_types),
"risk_level": self.urgency_level,
"usage_mode": usage_mode,
"source_type": self.source_authority,
"why_retrieved": why,
"documents": list(self.documents) if self.documents else [],
"last_verified": self.last_verified,
}
def load_service_graph(path: Path | str = DEFAULT_SERVICE_GRAPH) -> list[ServiceNode]:
graph_path = Path(path)
if not graph_path.exists():
return []
nodes: list[ServiceNode] = []
with graph_path.open("r", encoding="utf-8") as handle:
for line in handle:
if not line.strip():
continue
nodes.append(ServiceNode.from_dict(json.loads(line)))
return nodes
def match_services(
route: str,
safety_tier: str,
audience_mode: str = "student",
limit: int = 5,
path: Path | str = DEFAULT_SERVICE_GRAPH,
) -> list[ServiceNode]:
nodes = load_service_graph(path)
scored: list[tuple[int, ServiceNode]] = []
for node in nodes:
score = 0
if route in node.route_types:
score += 8
if safety_tier in node.safety_tiers:
score += 5
if audience_mode in node.audience:
score += 3
if audience_mode == "helping_friend" and "friend_peer" in node.audience:
score += 4
if score > 0 and route not in node.do_not_use_for:
scored.append((score, node))
scored.sort(key=lambda item: item[0], reverse=True)
return [node for _, node in scored[:limit]]