rag-wright 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rag_wright/__init__.py +13 -0
- rag_wright/api/__init__.py +33 -0
- rag_wright/api/config.py +59 -0
- rag_wright/api/discover.py +70 -0
- rag_wright/api/documents.py +39 -0
- rag_wright/api/ids.py +31 -0
- rag_wright/api/invoke.py +99 -0
- rag_wright/api/kg.py +61 -0
- rag_wright/api/mcp.py +94 -0
- rag_wright/api/usage.py +30 -0
- rag_wright/api/workspace.py +85 -0
- rag_wright/capabilities/__init__.py +8 -0
- rag_wright/capabilities/answer_generator.py +427 -0
- rag_wright/capabilities/ard.py +286 -0
- rag_wright/capabilities/assertion_extraction.py +79 -0
- rag_wright/capabilities/chunk_read.py +58 -0
- rag_wright/capabilities/chunk_write.py +163 -0
- rag_wright/capabilities/claim_extraction.py +153 -0
- rag_wright/capabilities/clause_exception_linking.py +117 -0
- rag_wright/capabilities/compliance_judgment.py +322 -0
- rag_wright/capabilities/compliance_store.py +87 -0
- rag_wright/capabilities/contract_kg_serve.py +156 -0
- rag_wright/capabilities/contract_kg_store.py +251 -0
- rag_wright/capabilities/dg_extraction.py +585 -0
- rag_wright/capabilities/disambiguation.py +163 -0
- rag_wright/capabilities/document_parse.py +87 -0
- rag_wright/capabilities/document_scope.py +49 -0
- rag_wright/capabilities/embedding.py +164 -0
- rag_wright/capabilities/embedding_profiles.py +43 -0
- rag_wright/capabilities/entity_resolution.py +154 -0
- rag_wright/capabilities/fusion.py +64 -0
- rag_wright/capabilities/graph_extraction.py +243 -0
- rag_wright/capabilities/graph_query.py +73 -0
- rag_wright/capabilities/graph_storage.py +111 -0
- rag_wright/capabilities/highlight_serve.py +142 -0
- rag_wright/capabilities/hybrid_search.py +65 -0
- rag_wright/capabilities/invoke.py +31 -0
- rag_wright/capabilities/jev_decision.py +38 -0
- rag_wright/capabilities/manifests.py +872 -0
- rag_wright/capabilities/okf_navigate.py +456 -0
- rag_wright/capabilities/parsing.py +286 -0
- rag_wright/capabilities/property_boosted_retrieval.py +125 -0
- rag_wright/capabilities/query_function_classifier.py +94 -0
- rag_wright/capabilities/query_understanding.py +109 -0
- rag_wright/capabilities/registry.py +262 -0
- rag_wright/capabilities/remote_encoders.py +94 -0
- rag_wright/capabilities/requirement_extraction.py +247 -0
- rag_wright/capabilities/reranking.py +123 -0
- rag_wright/capabilities/retrieval_core.py +126 -0
- rag_wright/capabilities/rlm_chunking.py +808 -0
- rag_wright/capabilities/rlm_synthesis.py +316 -0
- rag_wright/capabilities/scan_quality.py +136 -0
- rag_wright/capabilities/span_relevance_judgment.py +191 -0
- rag_wright/capabilities/vision_to_text.py +85 -0
- rag_wright/capabilities/vlm_ocr.py +85 -0
- rag_wright/contracts/__init__.py +6 -0
- rag_wright/contracts/chunk.py +79 -0
- rag_wright/contracts/compliance.py +303 -0
- rag_wright/contracts/contract_meta.py +27 -0
- rag_wright/contracts/extraction.py +130 -0
- rag_wright/contracts/function.py +167 -0
- rag_wright/contracts/function_routing.py +91 -0
- rag_wright/contracts/highlight.py +74 -0
- rag_wright/contracts/identifiers.py +153 -0
- rag_wright/contracts/jurisdiction.py +96 -0
- rag_wright/contracts/ontology.py +142 -0
- rag_wright/contracts/property.py +201 -0
- rag_wright/contracts/provenance.py +78 -0
- rag_wright/contracts/query_intent.py +53 -0
- rag_wright/contracts/span.py +76 -0
- rag_wright/contracts/value_match.py +84 -0
- rag_wright/corpus/__init__.py +0 -0
- rag_wright/corpus/canonicalize.py +116 -0
- rag_wright/corpus/cuad.py +153 -0
- rag_wright/corpus/cuad_ingestion.py +72 -0
- rag_wright/corpus/document_parser.py +299 -0
- rag_wright/corpus/edgar.py +231 -0
- rag_wright/corpus/gcs_ingestion.py +120 -0
- rag_wright/corpus/http.py +110 -0
- rag_wright/corpus/selection.py +152 -0
- rag_wright/mcp/__init__.py +11 -0
- rag_wright/mcp/compliance_server.py +299 -0
- rag_wright/mcp/intra_document_qa_server.py +170 -0
- rag_wright/mcp/relational_qa_server.py +171 -0
- rag_wright/mcp/session_store.py +64 -0
- rag_wright/mcp/typed_property_retrieval_server.py +191 -0
- rag_wright/models/__init__.py +8 -0
- rag_wright/models/profiles.py +331 -0
- rag_wright/models/seam.py +497 -0
- rag_wright/models/tag_structured.py +285 -0
- rag_wright/models/tracing.py +179 -0
- rag_wright/models/usage.py +102 -0
- rag_wright/okf/__init__.py +11 -0
- rag_wright/okf/compile.py +292 -0
- rag_wright/okf/document.py +47 -0
- rag_wright/okf/enrich.py +176 -0
- rag_wright/okf/links.py +190 -0
- rag_wright/okf/lint.py +105 -0
- rag_wright/ontology/__init__.py +6 -0
- rag_wright/ontology/_generated_template_meta.py +60 -0
- rag_wright/ontology/_generated_vocab.py +52 -0
- rag_wright/ontology/clause_template.py +964 -0
- rag_wright/ontology/codegen.py +84 -0
- rag_wright/ontology/compliance_bridge.ttl +186 -0
- rag_wright/ontology/contract_bridge.ttl +2685 -0
- rag_wright/ontology/contract_taxonomy.py +24 -0
- rag_wright/ontology/derive.py +58 -0
- rag_wright/ontology/loader.py +435 -0
- rag_wright/ontology/packs/ftc_16cfr255.ttl +29 -0
- rag_wright/ontology/registry.py +87 -0
- rag_wright/ontology/template_introspect.py +100 -0
- rag_wright/py.typed +0 -0
- rag_wright/reference/__init__.py +2 -0
- rag_wright/reference/compliance.py +41 -0
- rag_wright/reference/contract_seam.py +123 -0
- rag_wright/skills/__init__.py +7 -0
- rag_wright/skills/claim_extraction/SKILL.md +47 -0
- rag_wright/skills/claim_extraction/__init__.py +1 -0
- rag_wright/skills/claim_extraction/template.py +50 -0
- rag_wright/skills/compliance_judgment/SKILL.md +59 -0
- rag_wright/skills/corpus_ingest/SKILL.md +106 -0
- rag_wright/skills/extraction_semantic_judge/SKILL.md +51 -0
- rag_wright/skills/extraction_semantic_judge/__init__.py +1 -0
- rag_wright/skills/generation/SKILL.md +64 -0
- rag_wright/skills/generation/__init__.py +1 -0
- rag_wright/skills/generic_compliance_judgment/SKILL.md +58 -0
- rag_wright/skills/okf_navigate/SKILL.md +137 -0
- rag_wright/skills/requirement_extraction/SKILL.md +47 -0
- rag_wright/skills/requirement_extraction/__init__.py +1 -0
- rag_wright/skills/requirement_extraction/template.py +50 -0
- rag_wright/skills/rlm/SKILL.md +186 -0
- rag_wright/skills/rlm/__init__.py +31 -0
- rag_wright/skills/rlm/agent.py +292 -0
- rag_wright/skills/span_relevance_judgment/SKILL.md +67 -0
- rag_wright/skills/vision_to_text/SKILL.md +36 -0
- rag_wright/skills/vision_to_text/__init__.py +1 -0
- rag_wright/spans/__init__.py +1 -0
- rag_wright/spans/boundary.py +78 -0
- rag_wright/spans/clause_function_classifier.py +490 -0
- rag_wright/spans/clause_kg_extractor.py +337 -0
- rag_wright/spans/cuad_labels.py +81 -0
- rag_wright/spans/dim_classifier.py +158 -0
- rag_wright/spans/dim_fleet.json +411 -0
- rag_wright/spans/function_classifier.py +77 -0
- rag_wright/spans/function_families.py +62 -0
- rag_wright/spans/hybrid_classifier.py +103 -0
- rag_wright/spans/legalbert_classifier.py +83 -0
- rag_wright/spans/model_capabilities.py +107 -0
- rag_wright/spans/new_function_labels.py +111 -0
- rag_wright/spans/page_map.py +68 -0
- rag_wright/spans/property_extractor.py +365 -0
- rag_wright/spans/property_grounding.py +182 -0
- rag_wright/spans/reclassify.py +77 -0
- rag_wright/spans/scarce_function_labels.py +105 -0
- rag_wright/spans/segment.py +341 -0
- rag_wright/spans/semantic_judge.py +197 -0
- rag_wright/spans/symbolic_validation.py +131 -0
- rag_wright/spans/tag_clause_extractor.py +182 -0
- rag_wright/store/__init__.py +6 -0
- rag_wright/store/arcadedb.py +1135 -0
- rag_wright/store/chunk_text.py +66 -0
- rag_wright/store/seam.py +213 -0
- rag_wright/subgraphs/__init__.py +0 -0
- rag_wright/subgraphs/async_ingestion.py +204 -0
- rag_wright/subgraphs/compliance_check.py +1042 -0
- rag_wright/subgraphs/compliance_ingestion.py +306 -0
- rag_wright/subgraphs/contract_ingestion_pipeline.py +999 -0
- rag_wright/subgraphs/graph_extraction.py +102 -0
- rag_wright/subgraphs/intra_document_qa.py +328 -0
- rag_wright/subgraphs/observability.py +140 -0
- rag_wright/subgraphs/query_constraint_extraction.py +73 -0
- rag_wright/subgraphs/relational_qa.py +165 -0
- rag_wright/subgraphs/requirement_extraction.py +137 -0
- rag_wright/subgraphs/scaffold.py +65 -0
- rag_wright/subgraphs/semantic_chunking.py +183 -0
- rag_wright/subgraphs/typed_clause_extraction.py +172 -0
- rag_wright/subgraphs/typed_property_retrieval.py +278 -0
- rag_wright/util/__init__.py +1 -0
- rag_wright/util/concurrent.py +153 -0
- rag_wright/util/spacy_model.py +45 -0
- rag_wright-0.1.0.dist-info/METADATA +168 -0
- rag_wright-0.1.0.dist-info/RECORD +184 -0
- rag_wright-0.1.0.dist-info/WHEEL +4 -0
- rag_wright-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
"""OKF navigation (FR-K.5/K.6, T50): the `okf_navigate` traversal capability.
|
|
2
|
+
|
|
3
|
+
Realizes the reachability ceiling (T47+T49 proved it is 0.845 for ACORD, model-free): a model navigates the
|
|
4
|
+
OKF bundle by progressive disclosure and returns a shortlist of concept ids, computing NO query-to-chunk
|
|
5
|
+
similarity. It is GENERIC over any OKF bundle -- it reasons over generic signposts (index entries, frontmatter,
|
|
6
|
+
descriptions, links), never over "clause" or "category" -- so the same capability navigates a legal-clause
|
|
7
|
+
bundle or a BigQuery-table bundle unchanged.
|
|
8
|
+
|
|
9
|
+
Structurally the RLM dynamic-sub-agent machinery (T15/T28) applied to a bundle instead of a flat working set:
|
|
10
|
+
one interpreter session (ADR-0020, KI-1) runs an authored navigation workflow that (a) uses PTC navigation
|
|
11
|
+
primitives -- deterministic bundle reads exposed as `tools.<name>()` -- to sift frontmatter and index entries
|
|
12
|
+
WITHOUT a model call, then (b) makes the two model decisions: a `okf_selector` sub-agent chooses which
|
|
13
|
+
signposts to expand, and the `judgeBodies` PTC tool judges concept bodies against the query -- a Python asyncio
|
|
14
|
+
fan-out (the embed_chunks pattern) through the profile seam, not a sub-agent, because the interpreter cannot
|
|
15
|
+
overlap `task()` dispatches (one JS engine per process). The query is threaded into the selector and the judge
|
|
16
|
+
by construction, not orchestrator choice (the T42 lesson). Bodies are read only after the frontmatter/index
|
|
17
|
+
sift, so bodies-read is a small fraction of candidates-considered.
|
|
18
|
+
|
|
19
|
+
The interpreter navigation is behind a `Navigator` seam: hermetic tests inject a stub navigator to exercise the
|
|
20
|
+
capability plumbing (trace, dedup, bounds, telemetry) without a model; the live `SeamNavigator` is opt-in.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import asyncio
|
|
26
|
+
import json
|
|
27
|
+
import posixpath
|
|
28
|
+
import re
|
|
29
|
+
import time
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Optional, Protocol, runtime_checkable
|
|
32
|
+
|
|
33
|
+
from langchain_core.language_models.chat_models import BaseChatModel
|
|
34
|
+
from langchain_core.messages import HumanMessage, SystemMessage, ToolMessage
|
|
35
|
+
from langchain_core.tools import tool
|
|
36
|
+
from pydantic import BaseModel
|
|
37
|
+
|
|
38
|
+
from deepagents import create_deep_agent
|
|
39
|
+
from deepagents.middleware.subagents import SubAgent
|
|
40
|
+
from rag_wright.capabilities.registry import CapabilityRegistry
|
|
41
|
+
from rag_wright.models.profiles import ModelRole, model_for
|
|
42
|
+
from rag_wright.models.seam import build_model
|
|
43
|
+
from rag_wright.models.tag_structured import build_tag_structured # ADR-0045: LLM-agnostic client-side output
|
|
44
|
+
from rag_wright.okf.document import parse_okf
|
|
45
|
+
from rag_wright.skills.rlm.agent import _resolve_model, rlm_interpreter_session
|
|
46
|
+
|
|
47
|
+
OKF_SELECTOR = "okf_selector"
|
|
48
|
+
# The only sub-agent is the selector. Body relevance is judged by the `judgeBodies` PTC tool (Python asyncio
|
|
49
|
+
# fan-out), not a sub-agent: the interpreter cannot overlap `task()` dispatches (one JS engine per process,
|
|
50
|
+
# ADR-0020 / KI-1), so reader parallelism must live in Python, the embed_chunks pattern.
|
|
51
|
+
GRANTED_SUBAGENTS: tuple[str, ...] = (OKF_SELECTOR,)
|
|
52
|
+
|
|
53
|
+
_READER_CONCURRENCY = 8 # in-flight reader judgments per judgeBodies batch (bound against provider rate limits)
|
|
54
|
+
|
|
55
|
+
DEFAULT_MAX_DEPTH = 3
|
|
56
|
+
DEFAULT_FRONTIER_BUDGET = 50 # max concept bodies a traversal may read (the recall@50 evidence cap)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Bounds(BaseModel):
|
|
60
|
+
"""Traversal run parameters, recorded with every result (RAC-50)."""
|
|
61
|
+
|
|
62
|
+
max_depth: int = DEFAULT_MAX_DEPTH
|
|
63
|
+
frontier_budget: int = DEFAULT_FRONTIER_BUDGET
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Telemetry(BaseModel):
|
|
67
|
+
"""Per-query cost telemetry. Serial round count is the primary latency proxy (fan-out within a round is
|
|
68
|
+
parallel); bodies_read vs candidates_considered evidences the sift-before-read invariant."""
|
|
69
|
+
|
|
70
|
+
serial_rounds: int = 0
|
|
71
|
+
dispatches: int = 0
|
|
72
|
+
candidates_considered: int = 0
|
|
73
|
+
bodies_read: int = 0
|
|
74
|
+
peak_frontier: int = 0
|
|
75
|
+
max_depth_reached: int = 0
|
|
76
|
+
wall_clock_s: float = 0.0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class TraceStep(BaseModel):
|
|
80
|
+
"""One navigation step, for the trace joined against gold in T51 (why a subtree was or was not expanded)."""
|
|
81
|
+
|
|
82
|
+
depth: int
|
|
83
|
+
action: str # "read_index" | "select" | "read_body" | "expand_links" | "stop"
|
|
84
|
+
path: str = ""
|
|
85
|
+
detail: str = ""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class NavigationResult(BaseModel):
|
|
89
|
+
"""The `okf_navigate` output and capability contract: a concept-id shortlist plus the full trace."""
|
|
90
|
+
|
|
91
|
+
query: str
|
|
92
|
+
shortlist: list[str] # concept ids (frontmatter chunk_id where present, else concept path)
|
|
93
|
+
bounds: Bounds
|
|
94
|
+
telemetry: Telemetry
|
|
95
|
+
trace: list[TraceStep]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# --- generic OKF navigation primitives (FR-K.5): deterministic reads, no model call ---------------
|
|
99
|
+
|
|
100
|
+
_INDEX = "index.md"
|
|
101
|
+
_LINK = re.compile(r"\*\s*\[[^\]]*\]\(([^)]+)\)(?:\s*-\s*(.*))?") # an index-entry line
|
|
102
|
+
_MD_LINK = re.compile(r"\[[^\]]*\]\(([^)]+)\)") # any markdown link (for generic cross-link extraction)
|
|
103
|
+
_RELATED_HEADING = "## Related clauses" # our compiler's cross-link section; read_body strips it as non-content
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _resolve_bundle_link(from_rel_path: str, link: str) -> Optional[str]:
|
|
107
|
+
"""Resolve a markdown link to a root-relative bundle concept path, or None if it is not one.
|
|
108
|
+
|
|
109
|
+
Handles absolute-from-root (`/a/b.md`) and relative (`b.md`, `./b.md`, `../c/b.md`) links per OKF §5;
|
|
110
|
+
drops external URLs, `mailto:`, anchor-only, and non-`.md` targets. Domain-agnostic — works on any bundle.
|
|
111
|
+
"""
|
|
112
|
+
target = link.split("#", 1)[0].strip()
|
|
113
|
+
if not target or "://" in target or target.startswith("mailto:") or not target.endswith(".md"):
|
|
114
|
+
return None
|
|
115
|
+
if target.startswith("/"):
|
|
116
|
+
return posixpath.normpath(target.lstrip("/"))
|
|
117
|
+
return posixpath.normpath(posixpath.join(posixpath.dirname(from_rel_path), target))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class Signpost(BaseModel):
|
|
121
|
+
"""One index entry as a navigable signpost, with its path PRE-RESOLVED in Python.
|
|
122
|
+
|
|
123
|
+
`path` is the root-relative target the workflow passes straight to the next `read_index` (a subdirectory)
|
|
124
|
+
or `read_body` (a concept) -- so the emitted JS never does path arithmetic, which the model gets wrong.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
path: str # root-relative: the subdir to descend (is_dir) or the concept file to read
|
|
128
|
+
description: str
|
|
129
|
+
is_dir: bool
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class OkfBundleReader:
|
|
133
|
+
"""Deterministic, model-free reads over any OKF bundle. The PTC surface the navigation workflow sifts with.
|
|
134
|
+
|
|
135
|
+
Paths are bundle-root-relative (``""`` is the root). Nothing here assumes a domain: it reads index entries,
|
|
136
|
+
frontmatter, bodies and links as the OKF spec defines them, so it works on any conformant bundle.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
def __init__(self, bundle_root: Path) -> None:
|
|
140
|
+
self._root = Path(bundle_root)
|
|
141
|
+
|
|
142
|
+
def read_index(self, rel_dir: str = "") -> list[Signpost]:
|
|
143
|
+
"""The signposts listed in ``<rel_dir>/index.md`` (progressive disclosure); [] if none."""
|
|
144
|
+
index = self._root / rel_dir / _INDEX
|
|
145
|
+
if not index.exists():
|
|
146
|
+
return []
|
|
147
|
+
_, body = parse_okf(index.read_text(encoding="utf-8"))
|
|
148
|
+
out: list[Signpost] = []
|
|
149
|
+
for line in body.splitlines():
|
|
150
|
+
m = _LINK.match(line.strip())
|
|
151
|
+
if not m:
|
|
152
|
+
continue
|
|
153
|
+
link = m.group(1)
|
|
154
|
+
is_dir = link.endswith("/") or link.endswith("/" + _INDEX)
|
|
155
|
+
# resolve the (directory-relative or absolute) index link to a root-relative path, in Python
|
|
156
|
+
resolved = link.lstrip("/") if link.startswith("/") else (f"{rel_dir}/{link}" if rel_dir else link)
|
|
157
|
+
if is_dir:
|
|
158
|
+
resolved = resolved[: -len("/" + _INDEX)] if resolved.endswith("/" + _INDEX) else resolved.rstrip("/")
|
|
159
|
+
out.append(Signpost(path=resolved, description=(m.group(2) or "").strip(), is_dir=is_dir))
|
|
160
|
+
return out
|
|
161
|
+
|
|
162
|
+
def read_frontmatter(self, rel_path: str) -> dict:
|
|
163
|
+
"""The concept's frontmatter (the cheap sift performed before any body is read); {} if absent."""
|
|
164
|
+
path = self._concept_path(rel_path)
|
|
165
|
+
if path is None or not path.exists():
|
|
166
|
+
return {}
|
|
167
|
+
fm, _ = parse_okf(path.read_text(encoding="utf-8"))
|
|
168
|
+
return fm
|
|
169
|
+
|
|
170
|
+
def read_body(self, rel_path: str) -> str:
|
|
171
|
+
"""The concept's clause/body text (the expensive read the workflow threads to a reader sub-agent)."""
|
|
172
|
+
path = self._concept_path(rel_path)
|
|
173
|
+
if path is None or not path.exists():
|
|
174
|
+
return ""
|
|
175
|
+
_, body = parse_okf(path.read_text(encoding="utf-8"))
|
|
176
|
+
idx = body.find(_RELATED_HEADING)
|
|
177
|
+
return (body[:idx] if idx != -1 else body).strip()
|
|
178
|
+
|
|
179
|
+
def related(self, rel_path: str) -> list[str]:
|
|
180
|
+
"""Outbound cross-links to other concepts: ANY bundle-internal markdown link in the body (OKF §5).
|
|
181
|
+
|
|
182
|
+
Generic over any OKF bundle -- it does not depend on a ``## Related`` section (our compiler's
|
|
183
|
+
convention); it resolves every markdown link (absolute-from-root or relative), keeps the ones that
|
|
184
|
+
point at a concept file (`.md`) inside the bundle, deduplicates, and drops external/anchor/self links.
|
|
185
|
+
Returns root-relative paths (the frontier the traversal expands along)."""
|
|
186
|
+
path = self._concept_path(rel_path)
|
|
187
|
+
if path is None or not path.exists():
|
|
188
|
+
return []
|
|
189
|
+
_, body = parse_okf(path.read_text(encoding="utf-8"))
|
|
190
|
+
src = rel_path.lstrip("/")
|
|
191
|
+
out: list[str] = []
|
|
192
|
+
seen = {src}
|
|
193
|
+
for m in _MD_LINK.finditer(body):
|
|
194
|
+
target = _resolve_bundle_link(src, m.group(1))
|
|
195
|
+
if target and target not in seen:
|
|
196
|
+
seen.add(target)
|
|
197
|
+
out.append(target)
|
|
198
|
+
return out
|
|
199
|
+
|
|
200
|
+
def concept_id(self, rel_path: str) -> str:
|
|
201
|
+
"""The concept's returned identifier: frontmatter ``chunk_id`` if present, else its bundle path."""
|
|
202
|
+
fm = self.read_frontmatter(rel_path)
|
|
203
|
+
return str(fm.get("chunk_id") or rel_path)
|
|
204
|
+
|
|
205
|
+
def _concept_path(self, rel_path: str) -> Optional[Path]:
|
|
206
|
+
target = rel_path.lstrip("/")
|
|
207
|
+
if not target or not target.endswith(".md"):
|
|
208
|
+
return None
|
|
209
|
+
return self._root / target
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# --- the Navigator seam ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@runtime_checkable
|
|
216
|
+
class Navigator(Protocol):
|
|
217
|
+
"""Navigate the bundle for a query -> (concept-id shortlist, trace, telemetry). Stubbed in hermetic tests."""
|
|
218
|
+
|
|
219
|
+
def navigate(self, query: str, reader: OkfBundleReader, bounds: Bounds) -> tuple[list[str], list[TraceStep], Telemetry]: ...
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def okf_navigate(
|
|
223
|
+
query: str,
|
|
224
|
+
bundle_root: Path,
|
|
225
|
+
*,
|
|
226
|
+
navigator: Optional[Navigator] = None,
|
|
227
|
+
bounds: Optional[Bounds] = None,
|
|
228
|
+
) -> NavigationResult:
|
|
229
|
+
"""Navigate `bundle_root` for `query`, returning a concept-id shortlist plus the trace and telemetry.
|
|
230
|
+
|
|
231
|
+
`navigator` defaults to the live interpreter-driven `SeamNavigator`; a stub is injected for hermetic tests.
|
|
232
|
+
No code path computes a query-to-chunk embedding similarity.
|
|
233
|
+
"""
|
|
234
|
+
bounds = bounds or Bounds()
|
|
235
|
+
reader = OkfBundleReader(bundle_root)
|
|
236
|
+
navigator = navigator if navigator is not None else SeamNavigator()
|
|
237
|
+
shortlist, trace, telemetry = navigator.navigate(query, reader, bounds)
|
|
238
|
+
# dedup preserving order (RAC-50): a concept reached via several paths appears once
|
|
239
|
+
seen: set[str] = set()
|
|
240
|
+
deduped = [cid for cid in shortlist if not (cid in seen or seen.add(cid))]
|
|
241
|
+
return NavigationResult(query=query, shortlist=deduped, bounds=bounds, telemetry=telemetry, trace=trace)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# --- the live interpreter-driven navigator (the model WRITES the workflow, taught by the Skill) --------
|
|
245
|
+
|
|
246
|
+
_SKILL_PATH = Path(__file__).parents[1] / "skills" / "okf_navigate" / "SKILL.md"
|
|
247
|
+
|
|
248
|
+
_SELECTOR_PROMPT = (
|
|
249
|
+
"You navigate a knowledge bundle to answer a QUESTION. You are given a numbered list of signposts "
|
|
250
|
+
"(each a name, a one-line description, and whether it is a subdirectory). Choose which are worth "
|
|
251
|
+
"exploring to answer the question; prefer precision, do not select everything. You never see the "
|
|
252
|
+
"underlying documents, only the signposts. Reply with ONLY a JSON object of the integer indices to "
|
|
253
|
+
'explore, e.g. {"keep": [0, 3, 4]}, and nothing else.'
|
|
254
|
+
)
|
|
255
|
+
_READER_JUDGE_PROMPT = (
|
|
256
|
+
"You judge one document against a QUESTION: is it relevant evidence for answering it? You see one "
|
|
257
|
+
"document at a time, never the whole bundle. Decide relevant / not relevant."
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class _Relevance(BaseModel):
|
|
262
|
+
"""The reader judgment schema, emitted via client-side tag-parse (ADR-0045, build_tag_structured) -- one
|
|
263
|
+
`<relevant>true|false</relevant>` tag parsed on our side, so it works on any model/provider and never hits
|
|
264
|
+
the reasoning-mode tool_choice rejection that a forced schema (`task(responseSchema)`) did."""
|
|
265
|
+
|
|
266
|
+
relevant: bool
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def navigate_method() -> str:
|
|
270
|
+
"""The OKF-navigation method (the Skill body, YAML frontmatter stripped) as the orchestrator's system
|
|
271
|
+
prompt. It teaches the progressive-disclosure method and the canonical workflow the model writes into
|
|
272
|
+
`eval`; the model authors the program, it is not hardcoded here (the dynamic-subagents paradigm)."""
|
|
273
|
+
text = _SKILL_PATH.read_text(encoding="utf-8")
|
|
274
|
+
if text.startswith("---"):
|
|
275
|
+
marker = text.find("\n---", 3)
|
|
276
|
+
if marker != -1:
|
|
277
|
+
text = text[marker + 4 :]
|
|
278
|
+
return text.strip()
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _with_question(prompt: str, query: str) -> str:
|
|
282
|
+
"""Bake the question into a sub-agent's system prompt, so query-relevance is enforced, not left to the
|
|
283
|
+
orchestrator to thread into each dispatch (the FR-Q.5 / T42 lesson)."""
|
|
284
|
+
return f"{prompt}\n\nThe QUESTION to answer (judge relevance to THIS, verbatim):\n{query}"
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _selector_config(query: str, model) -> SubAgent:
|
|
288
|
+
return {"name": OKF_SELECTOR, "description": "Chooses which OKF signposts to explore for a question.",
|
|
289
|
+
"system_prompt": _with_question(_SELECTOR_PROMPT, query),
|
|
290
|
+
"model": _resolve_model(model, ModelRole.STRUCTURED_REASONING)}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _navigation_ptc(reader: OkfBundleReader, bounds: Bounds, query: str, reader_model_id: str):
|
|
294
|
+
"""The PTC navigation primitives. The interpreter renders their signatures, so the model writes correct
|
|
295
|
+
object-argument calls (e.g. tools.readIndex({rel_dir})). Selection lives in the selector sub-agent, but
|
|
296
|
+
body relevance is judged HERE, by `judge_bodies`, so it can fan out in Python (the interpreter cannot
|
|
297
|
+
overlap sub-agent dispatches). The query and reader model are bound in by construction, not passed by JS."""
|
|
298
|
+
|
|
299
|
+
@tool
|
|
300
|
+
def frontier_budget() -> int:
|
|
301
|
+
"""The maximum number of concept bodies the traversal may read."""
|
|
302
|
+
return bounds.frontier_budget
|
|
303
|
+
|
|
304
|
+
@tool
|
|
305
|
+
def read_index(rel_dir: str = "") -> list:
|
|
306
|
+
"""List the index signposts under a bundle directory (each {path, description, is_dir}); no body read."""
|
|
307
|
+
return [s.model_dump() for s in reader.read_index(rel_dir)]
|
|
308
|
+
|
|
309
|
+
@tool
|
|
310
|
+
def read_body(rel_path: str) -> str:
|
|
311
|
+
"""Read one concept's body text (the expensive read, performed only after the signpost sift)."""
|
|
312
|
+
return reader.read_body(rel_path)
|
|
313
|
+
|
|
314
|
+
@tool
|
|
315
|
+
def related(rel_path: str) -> list:
|
|
316
|
+
"""Outbound related links from a concept (lateral frontier expansion)."""
|
|
317
|
+
return reader.related(rel_path)
|
|
318
|
+
|
|
319
|
+
@tool
|
|
320
|
+
def concept_id(rel_path: str) -> str:
|
|
321
|
+
"""The concept's returned identifier (frontmatter chunk_id where present)."""
|
|
322
|
+
return reader.concept_id(rel_path)
|
|
323
|
+
|
|
324
|
+
@tool
|
|
325
|
+
async def judge_bodies(rel_paths: list[str]) -> list[bool]:
|
|
326
|
+
"""Read and judge many concept bodies against the question CONCURRENTLY, returning a parallel list of
|
|
327
|
+
booleans (one per input path, order preserved). This is where reader parallelism lives: the fan-out is
|
|
328
|
+
Python asyncio (Semaphore + gather + to_thread, the embed_chunks pattern), so it is NOT bottlenecked by
|
|
329
|
+
the single-JS-engine limit that serializes sub-agent dispatches. Each judgment goes through the profile
|
|
330
|
+
seam, never a hardcoded provider flag (ADR-0045: client-side tag-parse, LLM-agnostic)."""
|
|
331
|
+
judge = build_tag_structured(reader_model_id, _Relevance)
|
|
332
|
+
system = _with_question(_READER_JUDGE_PROMPT, query)
|
|
333
|
+
semaphore = asyncio.Semaphore(_READER_CONCURRENCY)
|
|
334
|
+
|
|
335
|
+
async def _one(rel_path: str) -> bool:
|
|
336
|
+
body = reader.read_body(rel_path)
|
|
337
|
+
if not body:
|
|
338
|
+
return False
|
|
339
|
+
async with semaphore:
|
|
340
|
+
verdict = await asyncio.to_thread(
|
|
341
|
+
judge.invoke,
|
|
342
|
+
[SystemMessage(content=system), HumanMessage(content=f"Document:\n{body}")],
|
|
343
|
+
)
|
|
344
|
+
return bool(getattr(verdict, "relevant", False))
|
|
345
|
+
|
|
346
|
+
return list(await asyncio.gather(*(_one(p) for p in rel_paths)))
|
|
347
|
+
|
|
348
|
+
return [frontier_budget, read_index, read_body, related, concept_id, judge_bodies]
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
_NAVIGATE_REQUEST = (
|
|
352
|
+
"Run this as a workflow: emit the OKF navigation workflow from your instructions to the `eval` tool now, "
|
|
353
|
+
"in one call. It reads the bundle via tools.readIndex / tools.readBody, dispatches the okf_selector "
|
|
354
|
+
"sub-agent, and judges bodies with tools.judgeBodies. The bundle is NOT on any filesystem, so do NOT use "
|
|
355
|
+
"ls, glob, or read_file. Do NOT "
|
|
356
|
+
'answer from your own knowledge. Return ONLY the eval result (`{"shortlist": [...]}`).'
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _extract_shortlist(messages) -> list[str]:
|
|
361
|
+
"""The result is an eval tool return (deterministic), not the chat message (GraphWright interpreter_output).
|
|
362
|
+
|
|
363
|
+
The model may run a *diagnostic* eval after the workflow (e.g. re-reading an index), so scan eval results
|
|
364
|
+
newest-first and take the first one that carries a `shortlist` key -- not merely the last eval.
|
|
365
|
+
"""
|
|
366
|
+
for message in reversed(messages):
|
|
367
|
+
if isinstance(message, ToolMessage) and message.name == "eval":
|
|
368
|
+
content = str(message.content)
|
|
369
|
+
if '"shortlist"' in content:
|
|
370
|
+
return _parse_shortlist(content)
|
|
371
|
+
return []
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
class SeamNavigator:
|
|
375
|
+
"""The live navigator: one interpreter session over which the model, taught by the okf_navigate Skill,
|
|
376
|
+
WRITES the navigation workflow (it is not hardcoded), dispatches the okf_selector sub-agent, and judges
|
|
377
|
+
bodies via the judgeBodies PTC tool. Per-role models resolve through the profile seam (STRUCTURED_REASONING
|
|
378
|
+
/ DeepSeek V4 Pro) or are injected (tests). Gemma is enrichment-only (ADR-0023); the navigation judgments
|
|
379
|
+
run on the strong model."""
|
|
380
|
+
|
|
381
|
+
def __init__(
|
|
382
|
+
self, model: object = None, *, selector_model: object = None, reader_model: object = None,
|
|
383
|
+
config: Optional[dict] = None,
|
|
384
|
+
) -> None:
|
|
385
|
+
self._model = model
|
|
386
|
+
self._selector_model = selector_model
|
|
387
|
+
self._reader_model = reader_model
|
|
388
|
+
self._config = config or {} # langchain invoke config (Langfuse callbacks, recursion_limit backstop)
|
|
389
|
+
|
|
390
|
+
def navigate(self, query: str, reader: OkfBundleReader, bounds: Bounds):
|
|
391
|
+
# Attach any Langfuse callback to the MODELS (not just the invoke config): the interpreter runs eval
|
|
392
|
+
# on a worker thread (run_coroutine_threadsafe), which drops the callback/OTEL context, so sub-agent
|
|
393
|
+
# calls are only traced if the callback is bound at model construction.
|
|
394
|
+
callbacks = self._config.get("callbacks") or None
|
|
395
|
+
|
|
396
|
+
def resolve(arg: object) -> BaseChatModel:
|
|
397
|
+
if isinstance(arg, BaseChatModel):
|
|
398
|
+
return arg
|
|
399
|
+
return build_model(arg or model_for(ModelRole.STRUCTURED_REASONING), callbacks=callbacks)
|
|
400
|
+
|
|
401
|
+
orchestrator = resolve(self._model)
|
|
402
|
+
selector = resolve(self._selector_model if self._selector_model is not None else self._model)
|
|
403
|
+
# The reader is a PTC tool (build_tag_structured), which needs a model-id string, not a built model.
|
|
404
|
+
# Prefer an explicit id; fall back to the STRUCTURED_REASONING default. (A BaseChatModel injected as the
|
|
405
|
+
# reader cannot drive the tag-parse path, so only the live id/None path is supported for judging.)
|
|
406
|
+
reader_model_id = next(
|
|
407
|
+
(m for m in (self._reader_model, self._model) if isinstance(m, str)),
|
|
408
|
+
model_for(ModelRole.STRUCTURED_REASONING),
|
|
409
|
+
)
|
|
410
|
+
start = time.perf_counter()
|
|
411
|
+
# raise the eval-result cap: the shortlist + decision log at a high frontier budget exceeds the
|
|
412
|
+
# interpreter's 4,000-char default, which would truncate the JSON mid-string and lose the shortlist.
|
|
413
|
+
ptc = _navigation_ptc(reader, bounds, query, reader_model_id)
|
|
414
|
+
with rlm_interpreter_session(ptc=ptc, max_result_chars=200_000) as interpreter:
|
|
415
|
+
agent = create_deep_agent(
|
|
416
|
+
model=orchestrator,
|
|
417
|
+
tools=[],
|
|
418
|
+
system_prompt=navigate_method(), # the Skill method (the model writes the workflow from it)
|
|
419
|
+
subagents=[_selector_config(query, selector)],
|
|
420
|
+
middleware=[interpreter],
|
|
421
|
+
)
|
|
422
|
+
messages = agent.invoke({"messages": [HumanMessage(content=_NAVIGATE_REQUEST)]}, config=self._config)["messages"]
|
|
423
|
+
elapsed = time.perf_counter() - start
|
|
424
|
+
shortlist = _extract_shortlist(messages)
|
|
425
|
+
return shortlist, [], Telemetry(wall_clock_s=elapsed, bodies_read=len(shortlist))
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _parse_shortlist(text: str) -> list[str]:
|
|
429
|
+
"""Extract the shortlist from the workflow's JSON result. Never raises (returns [])."""
|
|
430
|
+
decoder = json.JSONDecoder()
|
|
431
|
+
for i, ch in enumerate(text):
|
|
432
|
+
if ch != "{":
|
|
433
|
+
continue
|
|
434
|
+
try:
|
|
435
|
+
value, _ = decoder.raw_decode(text, i)
|
|
436
|
+
except json.JSONDecodeError:
|
|
437
|
+
continue
|
|
438
|
+
if isinstance(value, dict) and "shortlist" in value and isinstance(value["shortlist"], list):
|
|
439
|
+
return [str(c) for c in value["shortlist"]]
|
|
440
|
+
# truncation-robust fallback: pull the `"shortlist": [ ... ]` array even if the outer object was cut off
|
|
441
|
+
m = re.search(r'"shortlist"\s*:\s*\[(.*?)(?:\]|$)', text, re.DOTALL)
|
|
442
|
+
if m:
|
|
443
|
+
ids = re.findall(r'"([^"]+)"', m.group(1))
|
|
444
|
+
if ids:
|
|
445
|
+
return ids
|
|
446
|
+
return []
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def register_okf_navigate(registry: CapabilityRegistry) -> None:
|
|
450
|
+
"""Register `okf_navigate` (FR-K.6): a query-discovered `agent_skill` (category 1; an ARD manifest follows)."""
|
|
451
|
+
registry.register(
|
|
452
|
+
"okf_navigate",
|
|
453
|
+
contract=NavigationResult,
|
|
454
|
+
kind="agent_skill",
|
|
455
|
+
display_name="OKF navigation (embedding-free progressive-disclosure traversal)",
|
|
456
|
+
)
|