File size: 8,027 Bytes
4554903 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 | """Pharos pack loading + confidence-gated routing.
Ported from the Pharos router on MTH
(~/lab/projects/pharos/router.py): embedding similarity when
sentence-transformers is installed, with a deterministic keyword-overlap
fallback so routing still works on a bare deployment.
Encoding policy follows the Pharos encoding-comparison results:
triples-only is the safe default; richer encodings only above confidence
thresholds; walk-only never (it hurt reasoning in the benchmark).
"""
import json
import re
from dataclasses import dataclass, field
from pathlib import Path
DEFAULT_PACKS_DIR = Path(__file__).parent.parent.parent / "packs"
@dataclass
class Pack:
name: str
description: str
triples: list
path: Path
def triples_text(self, max_triples: int = 200) -> str:
lines = [f"Knowledge domain: {self.description}\n", "Key relationships:"]
for t in self.triples[:max_triples]:
s = t.get("subject", t.get("s", ""))
p = t.get("predicate", t.get("p", ""))
o = t.get("object", t.get("o", ""))
lines.append(f"- {s} [{p}] {o}")
return "\n".join(lines)
def descriptor(self) -> str:
from collections import Counter
counter: Counter = Counter()
for t in self.triples[:120]:
for k in ("subject", "s", "predicate", "p", "object", "o"):
counter.update(re.findall(
r"[a-z]{3,}",
str(t.get(k, "")).lower().replace("_", " "),
))
common = [w for w, _ in counter.most_common(100)]
return (f"{self.name.replace('_', ' ')}. {self.description}. "
+ " ".join(common))
@dataclass
class RoutedKnowledge:
knowledge_text: str = ""
pack_names: list = field(default_factory=list)
confidence: float = 0.0
encoding_policy: str = "none"
explanation: str = ""
class PackLibrary:
def __init__(self, packs_dir: Path | str = DEFAULT_PACKS_DIR):
self.packs_dir = Path(packs_dir)
self.packs: list[Pack] = []
self._load()
def _load(self) -> None:
if not self.packs_dir.exists():
return
# Flat .json packs (Multiverse demo format) ...
for f in sorted(self.packs_dir.glob("*.json")):
try:
data = json.loads(f.read_text())
except json.JSONDecodeError:
continue
if isinstance(data, dict) and "triples" in data:
self.packs.append(Pack(
name=data.get("pack_name", f.stem),
description=data.get("description", f.stem),
triples=data["triples"], path=f,
))
# ... and directory packs (triples.json inside, Pharos pack format)
for d in sorted(p for p in self.packs_dir.iterdir() if p.is_dir()):
tf = d / "triples.json"
if not tf.exists():
continue
try:
data = json.loads(tf.read_text())
except json.JSONDecodeError:
continue
if isinstance(data, list):
triples, desc = data, d.name
else:
triples = data.get("triples", [])
desc = data.get("description", d.name)
self.packs.append(Pack(name=d.name, description=desc,
triples=triples, path=tf))
def get(self, name: str) -> Pack | None:
for p in self.packs:
if p.name == name:
return p
return None
class PharosRouter:
"""Confidence-gated pack selection (see module docstring)."""
def __init__(self, library: PackLibrary, match_threshold: float = 0.30,
source_threshold: float = 0.55, max_packs: int = 2,
embedding_model: str = "all-MiniLM-L6-v2"):
self.library = library
self.match_threshold = match_threshold
self.source_threshold = source_threshold
self.max_packs = max_packs
self._st_model = None
self._embeddings = None
self._try_embeddings(embedding_model)
def _try_embeddings(self, model_name: str) -> None:
try:
from sentence_transformers import SentenceTransformer
except ImportError:
return
if not self.library.packs:
return
self._st_model = SentenceTransformer(model_name)
descriptors = [p.descriptor() for p in self.library.packs]
self._embeddings = self._st_model.encode(
descriptors, convert_to_numpy=True,
)
def route(self, query: str) -> RoutedKnowledge:
if not self.library.packs:
return RoutedKnowledge(explanation="no packs loaded")
scored = (self._route_embedding(query) if self._st_model is not None
else self._route_keyword(query))
matches = [(p, s) for p, s in scored if s >= self.match_threshold]
matches = matches[: self.max_packs]
if not matches:
return RoutedKnowledge(
explanation=f"no pack above threshold {self.match_threshold}",
)
top_conf = matches[0][1]
policy = "triples_source" if top_conf >= self.source_threshold else "triples"
knowledge = "\n\n".join(p.triples_text() for p, _ in matches)
return RoutedKnowledge(
knowledge_text=knowledge,
pack_names=[p.name for p, _ in matches],
confidence=round(top_conf, 3),
encoding_policy=policy,
explanation=(f"top={matches[0][0].name} ({top_conf:.3f}), "
f"method={'embedding' if self._st_model else 'keyword'}"),
)
def _route_embedding(self, query: str) -> list:
import numpy as np
q = self._st_model.encode(query, convert_to_numpy=True)
sims = self._embeddings @ q / (
np.linalg.norm(self._embeddings, axis=1) * (np.linalg.norm(q) or 1)
)
ranked = sorted(zip(self.library.packs, sims.tolist()),
key=lambda x: -x[1])
return ranked
def _route_keyword(self, query: str) -> list:
"""Fraction of (content-bearing) query words found in the pack
descriptor, with 4-char prefix matching so migration/migrations
and share/shared/shares count as hits."""
stop = {"the", "and", "for", "with", "how", "what", "why", "does",
"should", "would", "can", "you", "this", "that", "when"}
q_words = [w for w in re.findall(r"[a-z]{3,}", query.lower())
if w not in stop]
scored = []
for pack in self.library.packs:
d_words = set(re.findall(r"[a-z]{3,}", pack.descriptor().lower()))
if not q_words or not d_words:
scored.append((pack, 0.0))
continue
def matched(qw: str) -> bool:
if qw in d_words:
return True
if len(qw) >= 4:
prefix = qw[:4]
return any(dw.startswith(prefix) or qw.startswith(dw[:4])
for dw in d_words if len(dw) >= 4)
return False
hits = sum(1 for qw in q_words if matched(qw))
scored.append((pack, hits / len(q_words)))
return sorted(scored, key=lambda x: -x[1])
def load_router(packs_dir: Path | str = DEFAULT_PACKS_DIR,
**kwargs) -> PharosRouter:
return PharosRouter(PackLibrary(packs_dir), **kwargs)
if __name__ == "__main__":
import sys
router = load_router(sys.argv[1] if len(sys.argv) > 1 else DEFAULT_PACKS_DIR)
print(f"packs: {[p.name for p in router.library.packs]}")
for q in ("How do I add a migration for a new table?",
"Why does the webhook skip signature verification?"):
r = router.route(q)
print(f"\nQ: {q}\n -> {r.pack_names} conf={r.confidence} "
f"policy={r.encoding_policy} ({r.explanation})")
|