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,110 @@
|
|
|
1
|
+
"""Throttled, disk-cached HTTP fetching for corpus acquisition (T7, guardrail 1).
|
|
2
|
+
|
|
3
|
+
EDGAR rejects requests without a User-Agent and rate-limits; an IP block would stall the task. So
|
|
4
|
+
every fetch goes through a fetcher that (1) enforces a real aggregate rate limit over a rolling
|
|
5
|
+
one-second window (not a per-call sleep, so a burst never exceeds the cap) and (2) reads through a
|
|
6
|
+
durable on-disk cache, so a URL fetched in one run is never re-fetched in a later run.
|
|
7
|
+
|
|
8
|
+
The transport (the actual GET) is injected, so the throttle and cache logic are testable without
|
|
9
|
+
network. The CLI (`scripts/acquire_edgar.py`) supplies a real `requests`-backed transport.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import hashlib
|
|
15
|
+
import time
|
|
16
|
+
from collections import deque
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
Transport = Callable[[str, dict[str, str]], bytes]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RateLimiter:
|
|
25
|
+
"""An aggregate rate limiter: at most `max_per_sec` acquisitions in any rolling 1-second window.
|
|
26
|
+
|
|
27
|
+
Tracks the timestamps of recent acquisitions; when the window is full, it sleeps exactly until
|
|
28
|
+
the oldest one leaves the window. This bounds the aggregate rate, unlike a fixed per-call sleep.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
max_per_sec: float,
|
|
34
|
+
*,
|
|
35
|
+
clock: Callable[[], float] = time.monotonic,
|
|
36
|
+
sleep: Callable[[float], None] = time.sleep,
|
|
37
|
+
window: float = 1.0,
|
|
38
|
+
) -> None:
|
|
39
|
+
if max_per_sec <= 0:
|
|
40
|
+
raise ValueError("max_per_sec must be positive")
|
|
41
|
+
self._max = max_per_sec
|
|
42
|
+
self._window = window
|
|
43
|
+
self._clock = clock
|
|
44
|
+
self._sleep = sleep
|
|
45
|
+
self._events: deque[float] = deque()
|
|
46
|
+
|
|
47
|
+
def _evict(self, now: float) -> None:
|
|
48
|
+
while self._events and now - self._events[0] >= self._window:
|
|
49
|
+
self._events.popleft()
|
|
50
|
+
|
|
51
|
+
def acquire(self) -> None:
|
|
52
|
+
now = self._clock()
|
|
53
|
+
self._evict(now)
|
|
54
|
+
if len(self._events) >= self._max:
|
|
55
|
+
wait = self._window - (now - self._events[0])
|
|
56
|
+
if wait > 0:
|
|
57
|
+
self._sleep(wait)
|
|
58
|
+
now = self._clock()
|
|
59
|
+
self._evict(now)
|
|
60
|
+
self._events.append(now)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class DiskCache:
|
|
64
|
+
"""A durable key->bytes cache backed by files under `root` (survives across runs)."""
|
|
65
|
+
|
|
66
|
+
def __init__(self, root: Path | str) -> None:
|
|
67
|
+
self._root = Path(root)
|
|
68
|
+
self._root.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
|
|
70
|
+
def _path(self, key: str) -> Path:
|
|
71
|
+
return self._root / (hashlib.sha256(key.encode("utf-8")).hexdigest() + ".cache")
|
|
72
|
+
|
|
73
|
+
def get(self, key: str) -> Optional[bytes]:
|
|
74
|
+
path = self._path(key)
|
|
75
|
+
return path.read_bytes() if path.exists() else None
|
|
76
|
+
|
|
77
|
+
def put(self, key: str, value: bytes) -> None:
|
|
78
|
+
self._path(key).write_bytes(value)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ThrottledCachingFetcher:
|
|
82
|
+
"""Fetch a URL through the durable cache and the aggregate rate limiter.
|
|
83
|
+
|
|
84
|
+
A cache hit short-circuits both the network and the limiter; only a miss consumes rate budget
|
|
85
|
+
and calls the transport, then caches the result.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
*,
|
|
91
|
+
user_agent: str,
|
|
92
|
+
cache: DiskCache,
|
|
93
|
+
limiter: RateLimiter,
|
|
94
|
+
transport: Transport,
|
|
95
|
+
) -> None:
|
|
96
|
+
if not user_agent.strip():
|
|
97
|
+
raise ValueError("a non-empty User-Agent is required (EDGAR rejects requests without one)")
|
|
98
|
+
self._ua = user_agent
|
|
99
|
+
self._cache = cache
|
|
100
|
+
self._limiter = limiter
|
|
101
|
+
self._transport = transport
|
|
102
|
+
|
|
103
|
+
def get(self, url: str) -> bytes:
|
|
104
|
+
cached = self._cache.get(url)
|
|
105
|
+
if cached is not None:
|
|
106
|
+
return cached
|
|
107
|
+
self._limiter.acquire()
|
|
108
|
+
data = self._transport(url, {"User-Agent": self._ua})
|
|
109
|
+
self._cache.put(url, data)
|
|
110
|
+
return data
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""CUAD subset selection (T7, docs/archive/plans/Corpus_Acquisition.md).
|
|
2
|
+
|
|
3
|
+
The subset is a **deliberate, recorded filter over the full CUAD pull, chosen for archetype
|
|
4
|
+
coverage, never a first-N slice**. Given per-contract metadata, `select_subset` picks ~100-150
|
|
5
|
+
contracts that: include some scanned PDFs (so the Docling OCR / vision-to-text path is exercised);
|
|
6
|
+
spread across the agreement types rather than clustering one; and deliberately include multi-party
|
|
7
|
+
contracts and contracts that share a party (the raw material the relational and multi-hop eval
|
|
8
|
+
questions are built from, T10). The `SubsetManifest` records the criteria and coverage stats so the
|
|
9
|
+
subset is reproducible.
|
|
10
|
+
|
|
11
|
+
This is pure logic (no network, no PDF parsing). The full pull, scanned detection (via the system
|
|
12
|
+
`pdftotext`), and the manifest write live in the CLI (`scripts/acquire_cuad.py`).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from collections import Counter, defaultdict
|
|
18
|
+
|
|
19
|
+
from pydantic import BaseModel, Field
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ContractMeta(BaseModel):
|
|
23
|
+
"""Metadata for one pulled CUAD contract, the input to selection."""
|
|
24
|
+
|
|
25
|
+
contract_id: str
|
|
26
|
+
agreement_type: str
|
|
27
|
+
parties: list[str] # normalized party surface forms
|
|
28
|
+
is_scanned: bool
|
|
29
|
+
size_bytes: int
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SelectionCriteria(BaseModel):
|
|
33
|
+
"""The recorded selection criteria (part of the manifest, so the subset is reproducible)."""
|
|
34
|
+
|
|
35
|
+
target_min: int = 100
|
|
36
|
+
target_max: int = 150
|
|
37
|
+
min_scanned: int = 10 # some scanned PDFs for the vision-to-text path
|
|
38
|
+
min_shared_party_contracts: int = 20 # raw material for multi-hop questions (T10)
|
|
39
|
+
min_multi_party: int = 20 # contracts with >= 2 parties
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SubsetManifest(BaseModel):
|
|
43
|
+
"""The selected subset plus the criteria and coverage stats. Reproducible from the same input.
|
|
44
|
+
|
|
45
|
+
`source_snapshot` pins the exact CUAD source (a Zenodo record id or a GitHub release/commit) so
|
|
46
|
+
"reproducible from the manifest" is real: the same source + the same criteria reproduce the same
|
|
47
|
+
subset.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
source_snapshot: str
|
|
51
|
+
criteria: SelectionCriteria
|
|
52
|
+
selected_ids: list[str]
|
|
53
|
+
agreement_type_counts: dict[str, int]
|
|
54
|
+
scanned_count: int
|
|
55
|
+
multi_party_count: int
|
|
56
|
+
shared_party_groups: list[list[str]] = Field(default_factory=list)
|
|
57
|
+
total_size_bytes: int
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _shared_party_index(contracts: list[ContractMeta]) -> dict[str, list[str]]:
|
|
61
|
+
"""party -> sorted contract_ids that name it, for parties shared by >= 2 contracts."""
|
|
62
|
+
by_party: dict[str, list[str]] = defaultdict(list)
|
|
63
|
+
for contract in contracts:
|
|
64
|
+
for party in contract.parties:
|
|
65
|
+
by_party[party].append(contract.contract_id)
|
|
66
|
+
return {
|
|
67
|
+
party: sorted(ids) for party, ids in by_party.items() if len(set(ids)) >= 2
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def select_subset(
|
|
72
|
+
contracts: list[ContractMeta], criteria: SelectionCriteria, *, source_snapshot: str
|
|
73
|
+
) -> SubsetManifest:
|
|
74
|
+
"""Select a coverage-driven subset. Deterministic: same input yields the same manifest.
|
|
75
|
+
|
|
76
|
+
`source_snapshot` is the pinned CUAD source (Zenodo record id / GitHub release) recorded in the
|
|
77
|
+
manifest so the subset is reproducible.
|
|
78
|
+
"""
|
|
79
|
+
by_id = {c.contract_id: c for c in contracts}
|
|
80
|
+
ordered = sorted(contracts, key=lambda c: c.contract_id) # deterministic traversal
|
|
81
|
+
shared = _shared_party_index(contracts)
|
|
82
|
+
selected: dict[str, None] = {} # insertion-ordered set
|
|
83
|
+
|
|
84
|
+
def take(contract_id: str) -> None:
|
|
85
|
+
if contract_id in by_id and contract_id not in selected:
|
|
86
|
+
if len(selected) < criteria.target_max:
|
|
87
|
+
selected[contract_id] = None
|
|
88
|
+
|
|
89
|
+
# 1. Scanned coverage (take all available if fewer than requested).
|
|
90
|
+
for c in ordered:
|
|
91
|
+
if len([i for i in selected if by_id[i].is_scanned]) >= criteria.min_scanned:
|
|
92
|
+
break
|
|
93
|
+
if c.is_scanned:
|
|
94
|
+
take(c.contract_id)
|
|
95
|
+
|
|
96
|
+
# 2. Shared-party coverage: pull whole groups so a shared party links >= 2 selected contracts.
|
|
97
|
+
for _party, ids in sorted(shared.items()):
|
|
98
|
+
if sum(1 for i in selected if len(shared_groups_of(by_id[i], shared)) > 0) >= (
|
|
99
|
+
criteria.min_shared_party_contracts
|
|
100
|
+
):
|
|
101
|
+
break
|
|
102
|
+
for contract_id in ids:
|
|
103
|
+
take(contract_id)
|
|
104
|
+
|
|
105
|
+
# 3. Multi-party coverage.
|
|
106
|
+
for c in ordered:
|
|
107
|
+
if sum(1 for i in selected if len(by_id[i].parties) >= 2) >= criteria.min_multi_party:
|
|
108
|
+
break
|
|
109
|
+
if len(c.parties) >= 2:
|
|
110
|
+
take(c.contract_id)
|
|
111
|
+
|
|
112
|
+
# 4. Agreement-type spread: round-robin across types (sorted) until the target is reached.
|
|
113
|
+
by_type: dict[str, list[str]] = defaultdict(list)
|
|
114
|
+
for c in ordered:
|
|
115
|
+
by_type[c.agreement_type].append(c.contract_id)
|
|
116
|
+
made_progress = True
|
|
117
|
+
while len(selected) < criteria.target_max and made_progress:
|
|
118
|
+
made_progress = False
|
|
119
|
+
for _type, ids in sorted(by_type.items()):
|
|
120
|
+
for contract_id in ids:
|
|
121
|
+
if contract_id not in selected:
|
|
122
|
+
before = len(selected)
|
|
123
|
+
take(contract_id)
|
|
124
|
+
if len(selected) > before:
|
|
125
|
+
made_progress = True
|
|
126
|
+
break # one per type per round -> spread, not cluster
|
|
127
|
+
|
|
128
|
+
selected_ids = list(selected)
|
|
129
|
+
chosen = [by_id[i] for i in selected_ids]
|
|
130
|
+
selected_set = set(selected_ids)
|
|
131
|
+
shared_groups = [
|
|
132
|
+
sorted(i for i in ids if i in selected_set)
|
|
133
|
+
for ids in shared.values()
|
|
134
|
+
]
|
|
135
|
+
shared_groups = [g for g in shared_groups if len(g) >= 2]
|
|
136
|
+
# dedupe identical groups deterministically
|
|
137
|
+
unique_groups = sorted({tuple(g) for g in shared_groups})
|
|
138
|
+
return SubsetManifest(
|
|
139
|
+
source_snapshot=source_snapshot,
|
|
140
|
+
criteria=criteria,
|
|
141
|
+
selected_ids=selected_ids,
|
|
142
|
+
agreement_type_counts=dict(Counter(c.agreement_type for c in chosen)),
|
|
143
|
+
scanned_count=sum(1 for c in chosen if c.is_scanned),
|
|
144
|
+
multi_party_count=sum(1 for c in chosen if len(c.parties) >= 2),
|
|
145
|
+
shared_party_groups=[list(g) for g in unique_groups],
|
|
146
|
+
total_size_bytes=sum(c.size_bytes for c in chosen),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def shared_groups_of(contract: ContractMeta, shared: dict[str, list[str]]) -> list[str]:
|
|
151
|
+
"""The shared parties this contract participates in (non-empty means it links other contracts)."""
|
|
152
|
+
return [party for party in contract.parties if party in shared]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""MCP-tool surfaces over the registered capabilities (MCP-PROTO).
|
|
2
|
+
|
|
3
|
+
A capability that is an in-process `subgraph` (LG-3) can ALSO be exposed as an ARD `mcp_tool`: a thin FastMCP
|
|
4
|
+
server binds the store + models server-side and exposes the subgraph's `run_*` entrypoint as one tool
|
|
5
|
+
(question -> typed contract). An external agent then discovers it via ARD search and calls it as a single tool
|
|
6
|
+
-- saving context tokens and inter-agent coordination vs. embedding the subgraph. Same capability code, two
|
|
7
|
+
surfaces (subgraph for compilation, mcp_tool for cross-agent calls).
|
|
8
|
+
|
|
9
|
+
FastMCP is the server framework (grounded via the cloned-repo AST graph `graphify-out/fastmcp/graph.json` +
|
|
10
|
+
the installed 3.4.6 signatures, per the library-grounding rule). `compliance_server` is the reference wrapper.
|
|
11
|
+
"""
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""MCP-PROTO: the reference `compliance_check` MCP-tool server (FastMCP).
|
|
2
|
+
|
|
3
|
+
Wraps the `compliance_check` SUBGRAPH (CC-6) as a single MCP tool `check_ad_compliance(ad_text, source_doc)`
|
|
4
|
+
-> a cited `ComplianceReport` (as JSON). The store (the FTC 16 CFR 255 Requirement KG) + the models (Granite
|
|
5
|
+
judge/extraction via the seam, BGE narrowing via the A100 adapter) bind SERVER-SIDE from env, so the tool call
|
|
6
|
+
is just `{ad_text}` -- the token/coordination win. An external agent discovers this via ARD search and calls it.
|
|
7
|
+
|
|
8
|
+
Grounded (library rule): FastMCP `server.py:L278` (`FastMCP(name, instructions, version=...)`), `@mcp.tool`,
|
|
9
|
+
`run(transport=...)` -- confirmed against the cloned-repo AST graph (`graphify-out/fastmcp/graph.json`) + the
|
|
10
|
+
installed 3.4.6 signatures.
|
|
11
|
+
|
|
12
|
+
`build_compliance_mcp(check_fn)` injects the checker so the server is hermetically testable (a stub `check_fn`,
|
|
13
|
+
no ArcadeDB/LLM). `main()` picks the production checker (real subgraph, env-wired) or a deterministic demo
|
|
14
|
+
checker (`RAG_MCP_DEMO=1`, no infra) and serves over stdio (so a Deep Agent can spawn it).
|
|
15
|
+
|
|
16
|
+
# real (needs the compliance KG + a model backend via env, like scripts/eval_compliance_gold.py):
|
|
17
|
+
uv run --no-sync python -m rag_wright.mcp.compliance_server
|
|
18
|
+
# demo (no infra -- deterministic report; for the Deep-Agent prototype):
|
|
19
|
+
RAG_MCP_DEMO=1 uv run --no-sync python -m rag_wright.mcp.compliance_server
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
from typing import Any, Optional, Protocol
|
|
26
|
+
|
|
27
|
+
from fastmcp import FastMCP
|
|
28
|
+
|
|
29
|
+
from rag_wright.contracts.compliance import Claim, ComplianceFinding, ComplianceReport, Verdict
|
|
30
|
+
from rag_wright.mcp.session_store import StoreResolver, resolve_request_store
|
|
31
|
+
from rag_wright.store.seam import Store
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# The injected checker: (text, source_doc, optional policy `sources` scope) -> ComplianceReport. Injected so the
|
|
35
|
+
# server is testable without infra. ASYNC-C1 (ADR-0057): async -- the tool handler awaits it, and it awaits the
|
|
36
|
+
# async compliance_check subgraph. `sources` (issue 0007): scope the check to named policies (None = whole store).
|
|
37
|
+
class CheckFn(Protocol):
|
|
38
|
+
async def __call__(self, store: Optional[Store], text: str, source_doc: str,
|
|
39
|
+
sources: Optional[list[str]] = None) -> ComplianceReport: ...
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# The injected DOCUMENT checker (issue 0008): (doc_name, raw bytes, optional policy `sources`) -> report. Parses
|
|
43
|
+
# and segments the uploaded subject document, then checks each section. Injected so the server stays testable.
|
|
44
|
+
class DocumentCheckFn(Protocol):
|
|
45
|
+
async def __call__(self, store: Optional[Store], doc_name: str, data: bytes,
|
|
46
|
+
sources: Optional[list[str]] = None) -> ComplianceReport: ...
|
|
47
|
+
|
|
48
|
+
_TOOL_DESCRIPTION = (
|
|
49
|
+
"Check an advertisement's claims against the FTC endorsement & testimonial rules (16 CFR Part 255). "
|
|
50
|
+
"Extracts the ad's objective claims, matches each to the applicable regulatory requirements, and judges "
|
|
51
|
+
"them from the AD TEXT ALONE, returning a cited compliance report: an ad-level verdict (violation / "
|
|
52
|
+
"needs_review / compliant), per-claim findings each with a rationale and both-sided citation (the claim "
|
|
53
|
+
"span and the regulation clause), a verdict summary, and a per-requirement gap matrix. A claim that cannot "
|
|
54
|
+
"be verified from the text is escalated to needs_review (human-gated), never silently cleared or flagged."
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _report_to_dict(report: ComplianceReport) -> dict[str, Any]:
|
|
59
|
+
"""The tool's JSON payload: the report plus its computed ad-level `verdict` (a @property, so not in
|
|
60
|
+
model_dump). This is the structured content the calling agent receives."""
|
|
61
|
+
return {"verdict": report.verdict.value, **report.model_dump(mode="json")}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
_GENERIC_TOOL_DESCRIPTION = (
|
|
65
|
+
"Check ANY subject (a practice, document, or scenario) against the ingested regulation knowledge graph and "
|
|
66
|
+
"return a cited LLM compliance verdict -- WITHOUT needing a domain-specific applicability ontology. It "
|
|
67
|
+
"semantically retrieves the most relevant requirements and judges the subject against each from the text, "
|
|
68
|
+
"returning a cited report (verdict / needs_review / compliant, per-requirement findings with both-sided "
|
|
69
|
+
"citations, summary, gap matrix) plus a note suggesting domain applicability enrichment for more precise "
|
|
70
|
+
"routing. Use this for any regulatory domain; use check_ad_compliance for the advertising-tuned path."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def build_compliance_mcp(check_fn: CheckFn, *, name: str = "rag-wright-compliance",
|
|
75
|
+
generic_check_fn: CheckFn | None = None,
|
|
76
|
+
document_check_fn: "DocumentCheckFn | None" = None,
|
|
77
|
+
store_resolver: Optional[StoreResolver] = None, env_store: Any = None) -> FastMCP:
|
|
78
|
+
"""Build the FastMCP server exposing the compliance tools. `check_fn` = the advertising `check_ad_compliance`
|
|
79
|
+
(injected: real subgraph in production, a stub in tests). `generic_check_fn` (optional) adds the
|
|
80
|
+
domain-agnostic `check_compliance` tool (COMP-VERDICT-GENERIC). `document_check_fn` (optional, issue 0008)
|
|
81
|
+
adds the `check_compliance_document` tool for uploaded subject documents. All testable with no ArcadeDB / LLM."""
|
|
82
|
+
mcp: FastMCP = FastMCP(
|
|
83
|
+
name=name,
|
|
84
|
+
instructions=(
|
|
85
|
+
"Compliance tools over a deontic Requirement knowledge graph. `check_ad_compliance` is the "
|
|
86
|
+
"advertising-tuned path (FTC 16 CFR 255, structured claim-type routing). `check_compliance` is the "
|
|
87
|
+
"DOMAIN-AGNOSTIC path: it gives a cited LLM verdict for ANY subject against ANY ingested regulation, "
|
|
88
|
+
"even one whose domain has no applicability ontology yet."
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
@mcp.tool(name="check_ad_compliance", description=_TOOL_DESCRIPTION)
|
|
93
|
+
async def check_ad_compliance(ad_text: str, source_doc: str = "ad",
|
|
94
|
+
sources: Optional[list[str]] = None) -> dict[str, Any]:
|
|
95
|
+
"""Screen one advertisement for FTC endorsement-rule compliance.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
ad_text: The full advertisement copy to screen.
|
|
99
|
+
source_doc: A short identifier for the ad (used in citations). Defaults to "ad".
|
|
100
|
+
sources: Optional list of policy `source` names to scope the check to. Omit (null) to check against
|
|
101
|
+
every curated policy in the store; name one or more to check ONLY against those.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
A cited compliance report: {verdict, source_doc, summary, findings[], gap_matrix[]}.
|
|
105
|
+
"""
|
|
106
|
+
store = await resolve_request_store(store_resolver, env_store) # per-request, out-of-band (issue 0035)
|
|
107
|
+
return _report_to_dict(await check_fn(store, ad_text, source_doc, sources=sources))
|
|
108
|
+
|
|
109
|
+
if generic_check_fn is not None: # COMP-VERDICT-GENERIC: the domain-agnostic verdict tool (any domain)
|
|
110
|
+
@mcp.tool(name="check_compliance", description=_GENERIC_TOOL_DESCRIPTION)
|
|
111
|
+
async def check_compliance(subject_text: str, source_doc: str = "subject",
|
|
112
|
+
sources: Optional[list[str]] = None) -> dict[str, Any]:
|
|
113
|
+
"""Check any subject against the ingested regulation KG -> a cited LLM verdict, WITHOUT needing a
|
|
114
|
+
domain-specific applicability ontology (semantic-retrieve relevant requirements -> LLM-judge).
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
subject_text: The practice / document / scenario to check for compliance.
|
|
118
|
+
source_doc: A short identifier for the subject (used in citations). Defaults to "subject".
|
|
119
|
+
sources: Optional list of policy `source` names to scope the check to. Omit (null) to check
|
|
120
|
+
against every curated policy in the store; name one or more to check ONLY against those
|
|
121
|
+
(the bring-your-own-policy / named-standard case).
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
A cited compliance report {verdict, source_doc, summary, findings[], gap_matrix[]}, plus a
|
|
125
|
+
`note` suggesting domain applicability enrichment for more precise claim<->requirement routing.
|
|
126
|
+
"""
|
|
127
|
+
store = await resolve_request_store(store_resolver, env_store) # per-request (issue 0035)
|
|
128
|
+
out = _report_to_dict(await generic_check_fn(store, subject_text, source_doc, sources=sources))
|
|
129
|
+
out["note"] = ("Generic domain-agnostic verdict (semantic retrieval + LLM judge). For more precise "
|
|
130
|
+
"claim<->requirement routing in this domain, enrich its applicability dimensions.")
|
|
131
|
+
return out
|
|
132
|
+
|
|
133
|
+
if document_check_fn is not None: # issue 0008: check an uploaded subject DOCUMENT (parsed + segmented)
|
|
134
|
+
@mcp.tool(name="check_compliance_document", description=_GENERIC_TOOL_DESCRIPTION)
|
|
135
|
+
async def check_compliance_document(doc_name: str, data_base64: str,
|
|
136
|
+
sources: Optional[list[str]] = None) -> dict[str, Any]:
|
|
137
|
+
"""Check an uploaded subject DOCUMENT (PDF/DOCX/MD/TXT) against the ingested regulation KG. The
|
|
138
|
+
document is parsed, split at its headings, and EACH section is judged -> per-section cited findings.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
doc_name: The document's file name, WITH extension (e.g. "policy_subject.pdf") -- the extension
|
|
142
|
+
selects the parser backend.
|
|
143
|
+
data_base64: The raw document bytes, base64-encoded (JSON cannot carry binary directly).
|
|
144
|
+
sources: Optional list of policy `source` names to scope the check to (issue 0007). Omit (null)
|
|
145
|
+
to check against every curated policy; name one or more to check ONLY against those.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
A cited compliance report {verdict, source_doc, summary, findings[], gap_matrix[]}.
|
|
149
|
+
"""
|
|
150
|
+
import base64
|
|
151
|
+
|
|
152
|
+
data = base64.b64decode(data_base64)
|
|
153
|
+
store = await resolve_request_store(store_resolver, env_store) # per-request (issue 0035)
|
|
154
|
+
out = _report_to_dict(await document_check_fn(store, doc_name, data, sources=sources))
|
|
155
|
+
out["note"] = ("Per-section verdict over the uploaded document (parsed + heading-split). For more "
|
|
156
|
+
"precise claim<->requirement routing in this domain, enrich its applicability dimensions.")
|
|
157
|
+
return out
|
|
158
|
+
|
|
159
|
+
return mcp
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# --- production checker: the real compliance_check subgraph, env-wired (like scripts/eval_compliance_gold.py) -
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def production_check_fn(*, k: int = 5) -> CheckFn:
|
|
166
|
+
"""Wire the real `compliance_check` over the env-selected store + models: ArcadeDB compliance KG
|
|
167
|
+
(`ARCADEDB_*`), Granite judge/extraction (`RAG_SERVING`), BGE narrowing (`STACK_URL` -> A100, else local).
|
|
168
|
+
Heavy imports are lazy so `RAG_MCP_DEMO` never pays for them."""
|
|
169
|
+
from dotenv import load_dotenv
|
|
170
|
+
|
|
171
|
+
load_dotenv()
|
|
172
|
+
from rag_wright.capabilities.dg_extraction import default_extraction_model
|
|
173
|
+
from rag_wright.capabilities.remote_encoders import query_embedder
|
|
174
|
+
from rag_wright.models.profiles import ModelRole, model_for
|
|
175
|
+
from rag_wright.subgraphs.compliance_check import run_ad_compliance_check
|
|
176
|
+
|
|
177
|
+
extract_model = default_extraction_model("claim-extract") # tenant-indep. (RAG_MODEL_ALL-aware default)
|
|
178
|
+
judge_model_id = model_for(ModelRole.STRUCTURED_REASONING)
|
|
179
|
+
embedder = query_embedder()
|
|
180
|
+
|
|
181
|
+
async def _check(store, ad_text: str, source_doc: str, sources: Optional[list[str]] = None) -> ComplianceReport:
|
|
182
|
+
return await run_ad_compliance_check( # per-request store (issue 0035)
|
|
183
|
+
ad_text, source_doc, store=store, extract_model=extract_model,
|
|
184
|
+
judge_model_id=judge_model_id, embedder=embedder, k=k, sources=sources)
|
|
185
|
+
|
|
186
|
+
return _check
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def production_generic_check_fn(*, k: int = 8) -> CheckFn:
|
|
190
|
+
"""COMP-VERDICT-GENERIC: wire the DOMAIN-AGNOSTIC verdict over the env-selected store + models (no claim
|
|
191
|
+
extraction; semantic-retrieve + generic judge). Same infra as `production_check_fn`."""
|
|
192
|
+
from dotenv import load_dotenv
|
|
193
|
+
|
|
194
|
+
load_dotenv()
|
|
195
|
+
from rag_wright.capabilities.remote_encoders import query_embedder
|
|
196
|
+
from rag_wright.models.profiles import ModelRole, model_for
|
|
197
|
+
from rag_wright.subgraphs.compliance_check import run_generic_compliance_verdict
|
|
198
|
+
|
|
199
|
+
judge_model_id = model_for(ModelRole.STRUCTURED_REASONING) # tenant-independent
|
|
200
|
+
embedder = query_embedder()
|
|
201
|
+
|
|
202
|
+
async def _check(store, subject_text: str, source_doc: str, sources: Optional[list[str]] = None) -> ComplianceReport:
|
|
203
|
+
return await run_generic_compliance_verdict( # per-request store (issue 0035)
|
|
204
|
+
subject_text, source_doc, store=store, judge_model_id=judge_model_id, embedder=embedder, k=k,
|
|
205
|
+
sources=sources)
|
|
206
|
+
|
|
207
|
+
return _check
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def production_document_check_fn(*, k: int = 8) -> "DocumentCheckFn":
|
|
211
|
+
"""Issue 0008: wire the DOMAIN-AGNOSTIC subject-DOCUMENT verdict over the env-selected store + models -- parse
|
|
212
|
+
the uploaded bytes, heading-split, judge each section. Same infra as `production_generic_check_fn`."""
|
|
213
|
+
from dotenv import load_dotenv
|
|
214
|
+
|
|
215
|
+
load_dotenv()
|
|
216
|
+
from rag_wright.capabilities.remote_encoders import query_embedder
|
|
217
|
+
from rag_wright.models.profiles import ModelRole, model_for
|
|
218
|
+
from rag_wright.subgraphs.compliance_check import run_compliance_document_verdict
|
|
219
|
+
|
|
220
|
+
judge_model_id = model_for(ModelRole.STRUCTURED_REASONING) # tenant-independent
|
|
221
|
+
embedder = query_embedder()
|
|
222
|
+
|
|
223
|
+
async def _check(store, doc_name: str, data: bytes, sources: Optional[list[str]] = None) -> ComplianceReport:
|
|
224
|
+
return await run_compliance_document_verdict( # per-request store (issue 0035)
|
|
225
|
+
doc_name, data, store=store, judge_model_id=judge_model_id, embedder=embedder, k=k, sources=sources)
|
|
226
|
+
|
|
227
|
+
return _check
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# --- demo checker: a deterministic, real-shaped report (no ArcadeDB / LLM) for the Deep-Agent prototype -------
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def demo_check_fn() -> CheckFn:
|
|
234
|
+
"""A deterministic stub with the REAL contract shape -- flags two unsubstantiated proof-overclaims (so the
|
|
235
|
+
ad-level rollup is VIOLATION, >= the threshold of 2). Lets the Deep-Agent prototype (and the hermetic test)
|
|
236
|
+
exercise the full MCP path with no ArcadeDB / LLM."""
|
|
237
|
+
_overclaims = [
|
|
238
|
+
("clinically proven to erase deep wrinkles", "'clinically proven' with no cited study in the ad text"),
|
|
239
|
+
("guaranteed to reverse aging in 7 days", "'guaranteed' result claim with no substantiation shown"),
|
|
240
|
+
]
|
|
241
|
+
|
|
242
|
+
async def _check(store, ad_text: str, source_doc: str, sources: Optional[list[str]] = None) -> ComplianceReport:
|
|
243
|
+
findings = [ # store + `sources` accepted for the CheckFn contract; the deterministic demo ignores them
|
|
244
|
+
ComplianceFinding(
|
|
245
|
+
claim_id=Claim.make_id(source_doc, i, assertion),
|
|
246
|
+
requirement_id="req-255.2-substantiation", verdict=Verdict.VIOLATION,
|
|
247
|
+
rationale=f"Overclaims proof: {why}.",
|
|
248
|
+
citation_claim=f"{source_doc}: {assertion}",
|
|
249
|
+
citation_requirement="§ 255.2 (req-255.2-substantiation): objective claims must be substantiated",
|
|
250
|
+
confidence=0.95)
|
|
251
|
+
for i, (assertion, why) in enumerate(_overclaims)
|
|
252
|
+
]
|
|
253
|
+
return ComplianceReport(
|
|
254
|
+
source_doc=source_doc, findings=findings, summary={"violation": len(findings)},
|
|
255
|
+
gap_matrix=[{"requirement_id": "req-255.2-substantiation", "citation": "§ 255.2",
|
|
256
|
+
"verdict": "violation", "claims_checked": len(findings)}])
|
|
257
|
+
|
|
258
|
+
return _check
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def production_env_store() -> Store:
|
|
262
|
+
"""The single-tenant fallback store from `COMPLIANCE_DB` (demo / eval / single-tenant). Built lazily so
|
|
263
|
+
`RAG_MCP_DEMO` never touches ArcadeDB; a multi-tenant caller passes a `store_resolver` (issue 0035)."""
|
|
264
|
+
from rag_wright.store.arcadedb import ArcadeDBStore
|
|
265
|
+
|
|
266
|
+
return ArcadeDBStore.from_env(database=os.environ.get("COMPLIANCE_DB", "ragwright_compliance"))
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def register_compliance_check_mcp(registry) -> None:
|
|
270
|
+
"""Register `compliance_check_mcp` (MCP-PROTO): the ARD `mcp_tool` surface of the `compliance_check`
|
|
271
|
+
subgraph -- the same capability exposed as a discoverable, cross-agent MCP tool (`check_ad_compliance`,
|
|
272
|
+
served by `rag_wright.mcp.compliance_server`) so an agent can call it as ONE tool via ARD search instead of
|
|
273
|
+
embedding the subgraph. Distinct ARD identity from the in-process `compliance_check` subgraph; same output
|
|
274
|
+
contract `ComplianceReport`."""
|
|
275
|
+
registry.register(
|
|
276
|
+
"compliance_check_mcp",
|
|
277
|
+
contract=ComplianceReport,
|
|
278
|
+
kind="mcp_tool",
|
|
279
|
+
display_name="Ad compliance check (MCP tool)",
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def main() -> None:
|
|
284
|
+
"""Serve the compliance MCP tools over stdio. `RAG_MCP_DEMO=1` uses the no-infra demo checker (advertising
|
|
285
|
+
tool only); otherwise all three real tools are exposed: `check_ad_compliance`, `check_compliance` (generic
|
|
286
|
+
text), and `check_compliance_document` (issue 0008, uploaded subject document)."""
|
|
287
|
+
if os.environ.get("RAG_MCP_DEMO") == "1":
|
|
288
|
+
build_compliance_mcp(demo_check_fn()).run(transport="stdio")
|
|
289
|
+
return
|
|
290
|
+
build_compliance_mcp( # single-tenant CLI: env-store fallback (issue 0035; multi-tenant passes a resolver)
|
|
291
|
+
production_check_fn(),
|
|
292
|
+
generic_check_fn=production_generic_check_fn(),
|
|
293
|
+
document_check_fn=production_document_check_fn(),
|
|
294
|
+
env_store=production_env_store,
|
|
295
|
+
).run(transport="stdio")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
if __name__ == "__main__":
|
|
299
|
+
main()
|