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,87 @@
|
|
|
1
|
+
"""ADR-0117 DD-1b: the compliance domain's store extension.
|
|
2
|
+
|
|
3
|
+
Composes a generic `Store` (`kg_write`/`kg_read`) to persist + read the Requirement KG, so the engine store imports
|
|
4
|
+
no compliance contract. A new domain writes its own typed records the same way -- the pattern, not the schema, is
|
|
5
|
+
what the engine provides. The `Requirement` vertex + its property storage types are schema-declared (ADR-0067 P5b),
|
|
6
|
+
so `kg_write` encodes each field (incl. `pages` as a native array, `bbox`/`applicability_json` as JSON strings)."""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Iterable, Optional
|
|
10
|
+
|
|
11
|
+
from rag_wright.capabilities.highlight_serve import _decode_bbox
|
|
12
|
+
from rag_wright.contracts.compliance import Requirement, RequirementLocation
|
|
13
|
+
from rag_wright.store.arcadedb import REQUIREMENT_TYPE
|
|
14
|
+
from rag_wright.store.seam import KgNode
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ComplianceStore:
|
|
18
|
+
"""Requirement-KG writes + the compliance READ facade over a generic `Store` (EP-REF-1b). The reads shape
|
|
19
|
+
the stored `Requirement` rows into the domain answers a product serves (what EP-SEAM-3 lifts), so the engine
|
|
20
|
+
store stays generic and a new domain supplies its own facade the same way."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, store) -> None:
|
|
23
|
+
self._store = store
|
|
24
|
+
|
|
25
|
+
def write_requirements(self, requirements: Iterable[Requirement]) -> int:
|
|
26
|
+
"""Upsert `Requirement` nodes by `requirement_id` (idempotent -- a re-ingest is a no-op on unchanged rules),
|
|
27
|
+
in one transaction. Domain-native values in; the store encodes per the declared storage type. Returns count."""
|
|
28
|
+
nodes = [
|
|
29
|
+
KgNode(REQUIREMENT_TYPE, "requirement_id", {
|
|
30
|
+
"requirement_id": req.requirement_id,
|
|
31
|
+
"source": req.source,
|
|
32
|
+
"citation": req.citation,
|
|
33
|
+
"deontic_type": req.deontic_type.value,
|
|
34
|
+
"actor": req.actor,
|
|
35
|
+
"requirement_text": req.requirement_text,
|
|
36
|
+
"evidence_standard": req.evidence_standard or "",
|
|
37
|
+
"severity": req.severity.value if req.severity else "",
|
|
38
|
+
"applicability_json": [[c.dimension, c.value] for c in req.applicability_scope],
|
|
39
|
+
"confidence": req.confidence.value,
|
|
40
|
+
"pages": [int(p) for p in req.pages],
|
|
41
|
+
"bbox": list(req.bbox) if req.bbox is not None else None,
|
|
42
|
+
})
|
|
43
|
+
for req in requirements
|
|
44
|
+
]
|
|
45
|
+
self._store.kg_write(nodes)
|
|
46
|
+
return len(nodes)
|
|
47
|
+
|
|
48
|
+
# --- EP-REF-1b: the compliance READ facade (the reference versions the product seam lifts) ---------
|
|
49
|
+
|
|
50
|
+
def requirements_for(self, source: str) -> list[dict]:
|
|
51
|
+
"""The curated requirement rows filed under one policy `source` -- the proof an ingest landed. DB-scoped
|
|
52
|
+
(`all_requirements(sources=[source])`), so a multi-policy store never fetches the rows outside scope."""
|
|
53
|
+
return self._store.all_requirements(sources=[source])
|
|
54
|
+
|
|
55
|
+
def requirement_locations(self, source: str) -> list[RequirementLocation]:
|
|
56
|
+
"""Where each requirement of one policy sits in its document (pages + optional bbox), for a citation
|
|
57
|
+
preview. Reads defensively: a requirement curated before provenance landed (ADR-0107) has no pages/bbox,
|
|
58
|
+
so `pages` -> [] and a malformed/absent `bbox` -> None (a page is still openable without a rectangle)."""
|
|
59
|
+
return [
|
|
60
|
+
RequirementLocation(
|
|
61
|
+
requirement_id=str(r.get("requirement_id") or ""),
|
|
62
|
+
citation=str(r.get("citation") or ""),
|
|
63
|
+
pages=[int(p) for p in (r.get("pages") or [])],
|
|
64
|
+
bbox=_decode_bbox(r.get("bbox")),
|
|
65
|
+
text=str(r.get("requirement_text") or ""),
|
|
66
|
+
)
|
|
67
|
+
for r in self._store.all_requirements(sources=[source])
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
def curated_requirement_count(self, sources: Optional[list[str]] = None) -> int:
|
|
71
|
+
"""How many requirements are IN SCOPE -- the denominator of an honest coverage statement (consulted N of
|
|
72
|
+
curated M). `sources=None` counts store-wide; a list scopes it (DB-side), `[]` is zero."""
|
|
73
|
+
return len(self._store.all_requirements(sources=sources))
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def policy_of_requirement(requirement_id: str) -> str:
|
|
77
|
+
"""The policy a requirement belongs to, from its id `<source>:<section>:<hash>`. Split on the FIRST colon
|
|
78
|
+
-- a source name may legitimately contain dots/hyphens ("ISO.27001-2022"), which a last-colon split eats.
|
|
79
|
+
Empty id -> ''. This prefix is the only link a `ComplianceFinding` carries back to its policy."""
|
|
80
|
+
return requirement_id.split(":", 1)[0] if requirement_id else ""
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def gated_pairs(report) -> list[dict]:
|
|
84
|
+
"""The (assertion|document, rule) pairs the symbolic ACTOR gate skipped before any judge call -- honest
|
|
85
|
+
coverage (ADR-0068): a shallow copy of `ComplianceReport.gated_pairs`. The ACTOR gate ONLY; top-k semantic
|
|
86
|
+
narrowing also drops pairs and is NOT reported here, so `[]` does not mean every pair was evaluated."""
|
|
87
|
+
return [dict(entry) for entry in (getattr(report, "gated_pairs", None) or [])]
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""KG-4 (FR-Q, ADR-0033): Leg A -- intra-contract scoped-query serving over the typed contract KG.
|
|
2
|
+
|
|
3
|
+
This is the structured/relational/cited backend the classification-only front door (`highlight_serve`)
|
|
4
|
+
couldn't give. Scoped to one `contract_id` (a clause_id is `<contract_id>:<index>:<hash>`, so a contract's
|
|
5
|
+
clauses are one key range), it answers questions over the typed KG that a clause-type filter can't:
|
|
6
|
+
|
|
7
|
+
- **disambiguate** same-type clauses by a property ("the *mutual* cap clause"; "the indemnity that covers
|
|
8
|
+
*fraud*") -- `disambiguate`;
|
|
9
|
+
- **aggregate** across the contract ("every clause that waives *consequential* damages"; "all *governed_by*
|
|
10
|
+
facts") -- `aggregate_by_property`, `clauses_of_function`;
|
|
11
|
+
- serve the whole **per-contract KG** (each clause + its typed properties) -- `contract_clause_index`.
|
|
12
|
+
|
|
13
|
+
Every answer is CITED: each clause carries its `clause_id` and each property its edge provenance
|
|
14
|
+
(`confidence` EXTRACTED/INFERRED/AMBIGUOUS + `span_id`, whose doc offsets live on the span) -- no claim
|
|
15
|
+
without a citation (FR-Q.6). The store is duck-typed (the three `*_contract*` / `clauses_with_property`
|
|
16
|
+
readbacks) so this layer is unit-tested with no live ArcadeDB.
|
|
17
|
+
|
|
18
|
+
Out of scope here (need layers not in the clause KG): party<->clause ROLE questions (a per-clause party-role layer, Leg C -- not built; the PartyTo edge was retired,
|
|
19
|
+
issue 0028) and cross-clause REFERENCES (not extracted). Noted for a later pass.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import Protocol, runtime_checkable
|
|
25
|
+
|
|
26
|
+
from pydantic import BaseModel
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CitedProperty(BaseModel):
|
|
30
|
+
"""One typed property of a clause, with its edge provenance (FR-Q.6)."""
|
|
31
|
+
|
|
32
|
+
dimension: str
|
|
33
|
+
value: str
|
|
34
|
+
edge_type: str
|
|
35
|
+
predicate_iri: str = ""
|
|
36
|
+
confidence: str = ""
|
|
37
|
+
span_id: str = ""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CitedClause(BaseModel):
|
|
41
|
+
"""A clause in a contract with its typed properties, each cited."""
|
|
42
|
+
|
|
43
|
+
contract_id: str
|
|
44
|
+
clause_id: str
|
|
45
|
+
function: str
|
|
46
|
+
folio_iri: str = ""
|
|
47
|
+
span_id: str = "" # the clause's operative span (1:1); rehydrate a property-less clause from THIS, reliably
|
|
48
|
+
exception_of: str = "" # ADR-0044: if set, this clause is an INFERRED carve-out/exception to that cap clause_id
|
|
49
|
+
properties: list[CitedProperty] = []
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@runtime_checkable
|
|
53
|
+
class _KGStore(Protocol):
|
|
54
|
+
def clauses_in_contract(self, contract_id: str) -> list[dict]: ...
|
|
55
|
+
def contract_clause_kg(self, contract_id: str) -> list[dict]: ...
|
|
56
|
+
def clauses_with_property(self, contract_id: str, dimension: str, value: str) -> list[dict]: ...
|
|
57
|
+
def all_spans_by_contract(self, contract_id: str) -> list[dict]: ... # 0006-D: untyped-span fallback
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _contract_of(clause_id: str) -> str:
|
|
61
|
+
return clause_id.split(":", 1)[0]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _prop(row: dict) -> CitedProperty:
|
|
65
|
+
return CitedProperty(
|
|
66
|
+
dimension=row.get("dimension", ""), value=row.get("value", ""),
|
|
67
|
+
edge_type=row.get("edge_type", ""), predicate_iri=row.get("predicate_iri") or "",
|
|
68
|
+
confidence=row.get("confidence") or "", span_id=row.get("span_id") or "",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def contract_clause_index(
|
|
73
|
+
store: _KGStore, contract_id: str, *, include_untyped: bool = False
|
|
74
|
+
) -> list[CitedClause]:
|
|
75
|
+
"""The full per-contract KG: every clause (incl. property-less ones) with its typed properties, cited.
|
|
76
|
+
|
|
77
|
+
`include_untyped` (issue 0006-D): also serve the spans the classifier left UNTYPED (`function == NONE`),
|
|
78
|
+
which produce no Clause-KG node. Without this, a clause the classifier misses (e.g. a renewal clause it can't
|
|
79
|
+
map to the taxonomy) is ABSENT from what Leg-A serves -> a silent recall hole ("not_found" on a clause that
|
|
80
|
+
plainly exists). With it, an untyped span is served as a bare `CitedClause` (no properties), rehydrated from
|
|
81
|
+
its own `span_id` -- so recall is decoupled from classification (the ADR-0047 whole-index posture)."""
|
|
82
|
+
by_clause: dict[str, list[CitedProperty]] = {}
|
|
83
|
+
for row in store.contract_clause_kg(contract_id):
|
|
84
|
+
if not row.get("dimension"): # a non-property clause edge (e.g. IsExceptionTo, ADR-0044) -> not a fact
|
|
85
|
+
continue
|
|
86
|
+
by_clause.setdefault(row["clause_id"], []).append(_prop(row))
|
|
87
|
+
index = []
|
|
88
|
+
for c in store.clauses_in_contract(contract_id):
|
|
89
|
+
cid = c["clause_id"]
|
|
90
|
+
index.append(CitedClause(
|
|
91
|
+
contract_id=_contract_of(cid), clause_id=cid, function=c.get("function", ""),
|
|
92
|
+
folio_iri=c.get("folio_iri") or "", span_id=c.get("span_id") or "",
|
|
93
|
+
properties=by_clause.get(cid, []),
|
|
94
|
+
))
|
|
95
|
+
if include_untyped:
|
|
96
|
+
covered = {c.span_id for c in index if c.span_id} # spans a typed clause already carries
|
|
97
|
+
for row in store.all_spans_by_contract(contract_id):
|
|
98
|
+
sid = row.get("span_id")
|
|
99
|
+
if sid and sid not in covered and (row.get("text") or "").strip():
|
|
100
|
+
index.append(CitedClause(
|
|
101
|
+
contract_id=contract_id, clause_id=sid, function=row.get("function") or "NONE",
|
|
102
|
+
span_id=sid, properties=[])) # bare; rehydrate_clause_texts fetches its text by function+span
|
|
103
|
+
return index
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def clauses_of_function(store: _KGStore, contract_id: str, function: str) -> list[CitedClause]:
|
|
107
|
+
"""Aggregation by clause type: all clauses of `function` in the contract, cited with their properties."""
|
|
108
|
+
return [c for c in contract_clause_index(store, contract_id) if c.function == function]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def disambiguate(
|
|
112
|
+
store: _KGStore, contract_id: str, function: str, dimension: str, value: str
|
|
113
|
+
) -> list[CitedClause]:
|
|
114
|
+
"""The same-type clauses in the contract that assert (dimension, value) -- e.g. among several
|
|
115
|
+
Cap-on-Liability clauses, the *mutual* one. The scoped-query answer classification can't give."""
|
|
116
|
+
matched = {r["clause_id"] for r in store.clauses_with_property(contract_id, dimension, value)}
|
|
117
|
+
return [c for c in clauses_of_function(store, contract_id, function) if c.clause_id in matched]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def aggregate_by_property(
|
|
121
|
+
store: _KGStore, contract_id: str, dimension: str, value: str
|
|
122
|
+
) -> list[CitedClause]:
|
|
123
|
+
"""Every clause in the contract asserting (dimension, value), regardless of type -- e.g. all clauses
|
|
124
|
+
that waive consequential damages, or every mutual obligation."""
|
|
125
|
+
matched = {r["clause_id"] for r in store.clauses_with_property(contract_id, dimension, value)}
|
|
126
|
+
by_id = {c.clause_id: c for c in contract_clause_index(store, contract_id)}
|
|
127
|
+
return [by_id[cid] for cid in matched if cid in by_id]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def grounded_only(clauses: list[CitedClause]) -> list[CitedClause]:
|
|
131
|
+
"""Drop the AMBIGUOUS (grounding-judge-flagged) properties -- the high-precision view for a citation UI.
|
|
132
|
+
Returns copies keeping only EXTRACTED/INFERRED properties."""
|
|
133
|
+
return [
|
|
134
|
+
c.model_copy(update={"properties": [p for p in c.properties if p.confidence != "AMBIGUOUS"]})
|
|
135
|
+
for c in clauses
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def register_intra_document_scoped_query(registry) -> None:
|
|
140
|
+
"""CAP-REG-2: register `intra_document_scoped_query` (function; intra-contract scoped KG serving)."""
|
|
141
|
+
registry.register(
|
|
142
|
+
"intra_document_scoped_query",
|
|
143
|
+
contract=CitedClause,
|
|
144
|
+
kind="function",
|
|
145
|
+
display_name="Intra-document scoped query",
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def register_clause_disambiguation(registry) -> None:
|
|
150
|
+
"""CAP-REG-2: register `clause_disambiguation` (function; disambiguation by typed property)."""
|
|
151
|
+
registry.register(
|
|
152
|
+
"clause_disambiguation",
|
|
153
|
+
contract=CitedClause,
|
|
154
|
+
kind="function",
|
|
155
|
+
display_name="Clause disambiguation",
|
|
156
|
+
)
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""ADR-0117 DD-1b: the contract domain's store extension.
|
|
2
|
+
|
|
3
|
+
Composes a generic `Store` (`kg_write`/`kg_read`) to write the typed clause KG (KG-3), the legacy flat property
|
|
4
|
+
graph, and contract metadata -- so the engine store imports no contract contract. The clause/value/contract vertex
|
|
5
|
+
types + the dimension->edge map + predicate IRIs are pack-authoritative (`contract_bridge.ttl`, ADR-0067 P5a/P5b);
|
|
6
|
+
`kg_write` encodes each field per its declared storage type (e.g. `functions` as a JSON string). Domain-native
|
|
7
|
+
values in; the store owns all wire encoding."""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from rag_wright.capabilities.highlight_serve import _decode_bbox
|
|
13
|
+
from rag_wright.contracts.contract_meta import ContractRecord
|
|
14
|
+
from rag_wright.contracts.highlight import SpanLocation
|
|
15
|
+
from rag_wright.contracts.property import FOLIO_SUBJECT_IRI, ClausePropertyRecord
|
|
16
|
+
from rag_wright.store.arcadedb import (
|
|
17
|
+
CLAUSE_TYPE,
|
|
18
|
+
CONTRACT_TYPE,
|
|
19
|
+
IS_EXCEPTION_TO_EDGE_TYPE,
|
|
20
|
+
PROPERTY_EDGE_TYPE,
|
|
21
|
+
PROPVALUE_TYPE,
|
|
22
|
+
TYPED_PROPERTY_EDGE_TYPES,
|
|
23
|
+
_DIM_EDGE_STR,
|
|
24
|
+
_edge_predicate_iri,
|
|
25
|
+
_property_value_key,
|
|
26
|
+
)
|
|
27
|
+
from rag_wright.ontology.contract_taxonomy import AFFILIATE_OF, CONTRACTS_WITH # DD-5 contract edge names
|
|
28
|
+
from rag_wright.store.seam import NOT_NULL, KgEdge, KgNode
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ContractKGStore:
|
|
32
|
+
"""Typed clause-KG + contract-metadata writes over a generic `Store`."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, store) -> None:
|
|
35
|
+
self._store = store
|
|
36
|
+
|
|
37
|
+
def _already_written(self, clause_id: str) -> bool:
|
|
38
|
+
"""Content-hash gate (FR-S.2): a committed `clause_id` embeds the content hash, so identical content ->
|
|
39
|
+
identical assertions -- skip (and the plain edge creates therefore run at most once per clause)."""
|
|
40
|
+
return bool(self._store.kg_read(CLAUSE_TYPE, fields=["clause_id"], where={"clause_id": clause_id}, limit=1))
|
|
41
|
+
|
|
42
|
+
def write_clause_kg(self, record: ClausePropertyRecord) -> None:
|
|
43
|
+
"""KG-3 (ADR-0033): the Clause node + one shared PropertyValue node per (dimension,value) + one TYPED edge
|
|
44
|
+
per assertion (grounded with a predicate IRI + provenance). Idempotent by the content-hash gate."""
|
|
45
|
+
if self._already_written(record.clause_id):
|
|
46
|
+
return
|
|
47
|
+
functions = [{"function": f.function, "confidence": f.confidence.value} for f in record.functions]
|
|
48
|
+
nodes = [KgNode(CLAUSE_TYPE, "clause_id", {
|
|
49
|
+
"clause_id": record.clause_id, "function": record.function, "folio_iri": record.folio_iri,
|
|
50
|
+
"span_id": record.span_id, "functions": functions})]
|
|
51
|
+
edges = []
|
|
52
|
+
for a in record.assertions:
|
|
53
|
+
edge_type = _DIM_EDGE_STR[a.dimension.value]
|
|
54
|
+
key = _property_value_key(a.dimension.value, a.value)
|
|
55
|
+
nodes.append(KgNode(PROPVALUE_TYPE, "value_key", {
|
|
56
|
+
"value_key": key, "dimension": a.dimension.value, "value": a.value,
|
|
57
|
+
"folio_iri": FOLIO_SUBJECT_IRI.get(a.value, "")}))
|
|
58
|
+
edges.append(KgEdge(edge_type, CLAUSE_TYPE, "clause_id", record.clause_id, PROPVALUE_TYPE, "value_key",
|
|
59
|
+
key, {
|
|
60
|
+
"dimension": a.dimension.value, "predicate_iri": _edge_predicate_iri(edge_type),
|
|
61
|
+
"confidence": a.confidence.value, "span_id": a.span_id,
|
|
62
|
+
"chunk_id": str(a.provenance.chunk_id),
|
|
63
|
+
"source_doc_id": a.provenance.source_doc_id}))
|
|
64
|
+
self._store.kg_write(nodes, edges)
|
|
65
|
+
|
|
66
|
+
def write_property_graph(self, record: ClausePropertyRecord) -> None:
|
|
67
|
+
"""Legacy flat property graph (ADR-0025/0026; superseded by `write_clause_kg`): the Clause node + shared
|
|
68
|
+
value nodes + one generic `HasProperty` edge per assertion. Same content-hash gate."""
|
|
69
|
+
if self._already_written(record.clause_id):
|
|
70
|
+
return
|
|
71
|
+
nodes = [KgNode(CLAUSE_TYPE, "clause_id", {
|
|
72
|
+
"clause_id": record.clause_id, "function": record.function, "folio_iri": record.folio_iri})]
|
|
73
|
+
edges = []
|
|
74
|
+
for a in record.assertions:
|
|
75
|
+
key = _property_value_key(a.dimension.value, a.value)
|
|
76
|
+
nodes.append(KgNode(PROPVALUE_TYPE, "value_key", {
|
|
77
|
+
"value_key": key, "dimension": a.dimension.value, "value": a.value,
|
|
78
|
+
"folio_iri": FOLIO_SUBJECT_IRI.get(a.value, "")}))
|
|
79
|
+
edges.append(KgEdge(PROPERTY_EDGE_TYPE, CLAUSE_TYPE, "clause_id", record.clause_id, PROPVALUE_TYPE,
|
|
80
|
+
"value_key", key, {
|
|
81
|
+
"confidence": a.confidence.value, "span_id": a.span_id,
|
|
82
|
+
"chunk_id": str(a.provenance.chunk_id),
|
|
83
|
+
"source_doc_id": a.provenance.source_doc_id}))
|
|
84
|
+
self._store.kg_write(nodes, edges)
|
|
85
|
+
|
|
86
|
+
def patch_canonical_jurisdictions(self) -> dict[str, int]:
|
|
87
|
+
"""KG-5a (DD-1c): additively canonicalize the `jurisdiction` value nodes -- write a `canonical_value` on
|
|
88
|
+
each (surface `value` untouched, kept for citation), deterministically (no LLM, no re-extraction). Fixes the
|
|
89
|
+
retrieval-match loss from surface variants (England / England and Wales / English law). Idempotent; the
|
|
90
|
+
`canonical_value` property is pack-declared. Returns {seen, canonicalized}."""
|
|
91
|
+
from rag_wright.contracts.jurisdiction import canonicalize_jurisdiction
|
|
92
|
+
|
|
93
|
+
rows = self._store.kg_read(PROPVALUE_TYPE, fields=["value_key", "value"],
|
|
94
|
+
where={"dimension": "jurisdiction"})
|
|
95
|
+
nodes = []
|
|
96
|
+
for r in rows:
|
|
97
|
+
canon = canonicalize_jurisdiction(r["value"])
|
|
98
|
+
if canon:
|
|
99
|
+
nodes.append(KgNode(PROPVALUE_TYPE, "value_key",
|
|
100
|
+
{"value_key": r["value_key"], "canonical_value": canon}))
|
|
101
|
+
if nodes:
|
|
102
|
+
self._store.kg_write(nodes)
|
|
103
|
+
return {"seen": len(rows), "canonicalized": len(nodes)}
|
|
104
|
+
|
|
105
|
+
def upsert_contract(self, record: ContractRecord) -> None:
|
|
106
|
+
"""CU-B3: upsert a contract's metadata by `contract_id`. `parties` -> native list (the store json-encodes
|
|
107
|
+
the `parties_json` STRING column); `page_count` may be None (-> null)."""
|
|
108
|
+
self._store.kg_write([KgNode(CONTRACT_TYPE, "contract_id", {
|
|
109
|
+
"contract_id": record.contract_id, "name": record.name, "agreement_type": record.agreement_type,
|
|
110
|
+
"parties_json": list(record.parties), "agreement_date": record.agreement_date,
|
|
111
|
+
"effective_date": record.effective_date, "source_doc_id": record.source_doc_id,
|
|
112
|
+
"content_hash": record.content_hash, "page_count": record.page_count})])
|
|
113
|
+
|
|
114
|
+
# --- EP-REF-1a-ii: the typed clause-KG edge-traversal READS, over the generic `kg_edges` primitive. These
|
|
115
|
+
# moved OFF the engine store (which must hold no domain traversal, ADR-0117); the store keeps only the
|
|
116
|
+
# generic kg_edges/kg_read. Shapes are byte-for-byte the old store methods' (acceptance: identical reads).
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def _bounds(contract_id: str) -> tuple[str, str]:
|
|
120
|
+
return contract_id + ":", contract_id + ";" # clause_ids are `<contract>:<idx>:<hash>`
|
|
121
|
+
|
|
122
|
+
def clause_typed_edges(self, clause_id: str) -> list[dict]:
|
|
123
|
+
"""The clause's typed property edges: edge type, dimension, value, predicate IRI, and provenance."""
|
|
124
|
+
return self._store.kg_edges(
|
|
125
|
+
CLAUSE_TYPE, where={"clause_id": clause_id}, direction="out",
|
|
126
|
+
select={"edge_type": "e.@type", "predicate_iri": "e.predicate_iri", "dimension": "v.dimension",
|
|
127
|
+
"value": "v.value", "folio_iri": "v.folio_iri", "confidence": "e.confidence",
|
|
128
|
+
"span_id": "e.span_id"})
|
|
129
|
+
|
|
130
|
+
def contract_clause_kg(self, contract_id: str) -> list[dict]:
|
|
131
|
+
"""The per-contract typed subgraph: one row per typed (clause -> value) edge, with the clause function,
|
|
132
|
+
edge type/dimension/value/predicate IRI, and provenance. Only PROPERTY edges (which carry a `dimension`);
|
|
133
|
+
the `dimension IS NOT NULL` guard excludes clause->clause edges (IsExceptionTo, ADR-0044)."""
|
|
134
|
+
lo, hi = self._bounds(contract_id)
|
|
135
|
+
return self._store.kg_edges(
|
|
136
|
+
CLAUSE_TYPE, key_range=("clause_id", lo, hi), direction="out", edge_where={"dimension": NOT_NULL},
|
|
137
|
+
select={"clause_id": "c.clause_id", "function": "c.function", "edge_type": "e.@type",
|
|
138
|
+
"dimension": "e.dimension", "value": "v.value", "folio_iri": "v.folio_iri",
|
|
139
|
+
"predicate_iri": "e.predicate_iri", "confidence": "e.confidence", "span_id": "e.span_id"})
|
|
140
|
+
|
|
141
|
+
def clauses_with_property(self, contract_id: str, dimension: str, value: str) -> list[dict]:
|
|
142
|
+
"""The clauses in one contract that assert (dimension, value) -- clause_id + function + edge provenance."""
|
|
143
|
+
lo, hi = self._bounds(contract_id)
|
|
144
|
+
return self._store.kg_edges(
|
|
145
|
+
CLAUSE_TYPE, key_range=("clause_id", lo, hi), direction="out",
|
|
146
|
+
edge_where={"dimension": dimension}, target_where={"value": value},
|
|
147
|
+
select={"clause_id": "c.clause_id", "function": "c.function", "edge_type": "e.@type",
|
|
148
|
+
"confidence": "e.confidence", "span_id": "e.span_id"})
|
|
149
|
+
|
|
150
|
+
def exceptions_of_clause(self, cap_clause_id: str) -> list[dict]:
|
|
151
|
+
"""The exception/carve-out clauses linked to a Cap clause (`IsExceptionTo` in-edges, ADR-0044): the SOURCE
|
|
152
|
+
clauses reached across the incoming edge. Rows: {clause_id, function, span_id}."""
|
|
153
|
+
return self._store.kg_edges(
|
|
154
|
+
CLAUSE_TYPE, where={"clause_id": cap_clause_id}, direction="in",
|
|
155
|
+
edge_type=IS_EXCEPTION_TO_EDGE_TYPE,
|
|
156
|
+
select={"clause_id": "v.clause_id", "function": "v.function", "span_id": "v.span_id"})
|
|
157
|
+
|
|
158
|
+
def span_properties(self, span_ids: list[str]) -> dict[str, set[tuple[str, str]]]:
|
|
159
|
+
"""The typed (dimension, value) assertions on each span, joined via the ADR-0025 `edge.span_id`. Scans each
|
|
160
|
+
typed edge type once (IN-filtered), so round-trips are bounded by the edge-type count, not the pool size.
|
|
161
|
+
Returns {span_id: {(dimension, value)}} (every input span present, empty if none)."""
|
|
162
|
+
out: dict[str, set[tuple[str, str]]] = {s: set() for s in span_ids}
|
|
163
|
+
if not span_ids:
|
|
164
|
+
return out
|
|
165
|
+
for edge_type in TYPED_PROPERTY_EDGE_TYPES:
|
|
166
|
+
for r in self._store.kg_edges(
|
|
167
|
+
edge_type=edge_type, edge_where={"span_id": span_ids},
|
|
168
|
+
select={"span_id": "span_id", "dimension": "dimension", "value": "inV().value"}):
|
|
169
|
+
sid, dim, val = r.get("span_id"), r.get("dimension"), r.get("value")
|
|
170
|
+
if sid in out and dim and val is not None:
|
|
171
|
+
out[sid].add((str(dim), str(val)))
|
|
172
|
+
return out
|
|
173
|
+
|
|
174
|
+
def clauses_in_contract(self, contract_id: str) -> list[dict]:
|
|
175
|
+
"""Every clause in one contract (a NODE read, delegated to the generic store -- it is a `kg_read`-
|
|
176
|
+
relocatable follow-up, not an edge traversal). Kept here so this extension is the complete `_KGStore`
|
|
177
|
+
reader the Leg-A serving (`contract_kg_serve`) needs."""
|
|
178
|
+
return self._store.clauses_in_contract(contract_id)
|
|
179
|
+
|
|
180
|
+
def all_spans_by_contract(self, contract_id: str) -> list[dict]:
|
|
181
|
+
"""Every span in one contract (delegated to the generic store). The Leg-A serving
|
|
182
|
+
(`contract_clause_index(..., include_untyped=True)`) reads this so a span the classifier left untyped is
|
|
183
|
+
still a candidate -- without it, serve raised AttributeError and intra_document_qa abstained on EVERY doc."""
|
|
184
|
+
return self._store.all_spans_by_contract(contract_id)
|
|
185
|
+
|
|
186
|
+
# --- EP-REF-1b-ii: contract traversal + vocab (the reference reads EP-SEAM-3 lifts), over the entity-graph
|
|
187
|
+
# traversal primitive + the clause index. The CONTRACTS_WITH / AFFILIATE_OF naming is the DD-5 contract
|
|
188
|
+
# vocabulary over the generic (domain-free) graph_query.
|
|
189
|
+
|
|
190
|
+
def party_counterparties(self, entity_id: str, *, max_hops: int = 1,
|
|
191
|
+
documents: Optional[list[str]] = None) -> list:
|
|
192
|
+
"""The parties this one has a CONTRACTS_WITH edge to, each citing the contract it came from. ONE hop by
|
|
193
|
+
default -- two hops would return the counterparties OF the counterparties (parties this one has no
|
|
194
|
+
agreement with), overstating exposure. `documents` scopes the traversal (every edge on a path must belong)."""
|
|
195
|
+
from rag_wright.capabilities.graph_query import graph_query
|
|
196
|
+
return list(graph_query(entity_id, store=self._store, relationship_type=CONTRACTS_WITH,
|
|
197
|
+
max_hops=max_hops, documents=documents).evidence)
|
|
198
|
+
|
|
199
|
+
def party_affiliates(self, entity_id: str, *, documents: Optional[list[str]] = None) -> list:
|
|
200
|
+
"""The parties this one has an AFFILIATE_OF edge to (same corporate group). A SEPARATE traversal from
|
|
201
|
+
counterparties on purpose: a parent and its subsidiary are two legal persons whose obligations must not be
|
|
202
|
+
pooled, and the engine does not merge the entities -- the affiliate is its own node."""
|
|
203
|
+
from rag_wright.capabilities.graph_query import graph_query
|
|
204
|
+
return list(graph_query(entity_id, store=self._store, relationship_type=AFFILIATE_OF,
|
|
205
|
+
documents=documents).evidence)
|
|
206
|
+
|
|
207
|
+
def contract_terms(self, contract_id: str) -> list:
|
|
208
|
+
"""The typed clauses of one contract -- the terms that create the exposure. The FULL view, NOT
|
|
209
|
+
grounded-only: the high-precision view drops AMBIGUOUS properties, including the verbatim out-of-vocab
|
|
210
|
+
values ADR-0102 kept, so an exposure/citation surface must read what the graph holds, not less (a
|
|
211
|
+
precision filter is a per-caller UI choice, never baked into the facade)."""
|
|
212
|
+
from rag_wright.capabilities.contract_kg_serve import contract_clause_index
|
|
213
|
+
return list(contract_clause_index(self, contract_id))
|
|
214
|
+
|
|
215
|
+
# --- EP-REF-1b-iii: clause-type taxonomy (pure, from contracts/function.py) + span location assembly ---
|
|
216
|
+
|
|
217
|
+
@staticmethod
|
|
218
|
+
def canonical_clause_type(label: str) -> Optional[str]:
|
|
219
|
+
"""Map a user's clause label onto the engine's clause-type taxonomy, or `None` if it maps to nothing.
|
|
220
|
+
Case-insensitive + alias-resolving ("Limitation of Liability" -> "Cap On Liability"), so a caller's label
|
|
221
|
+
(e.g. a correction) is VALIDATED rather than taken on trust -- an unmappable label returns None, not a
|
|
222
|
+
silent empty scope."""
|
|
223
|
+
from rag_wright.contracts.function import canonical_function
|
|
224
|
+
return canonical_function(label)
|
|
225
|
+
|
|
226
|
+
@staticmethod
|
|
227
|
+
def clause_type_vocabulary() -> tuple[str, ...]:
|
|
228
|
+
"""Every clause type a sweep/correction UI can be scoped to (the engine's function-label vocabulary)."""
|
|
229
|
+
from rag_wright.contracts.function import FUNCTION_LABELS
|
|
230
|
+
return tuple(FUNCTION_LABELS)
|
|
231
|
+
|
|
232
|
+
def span_locations(self, contract_id: str) -> list[SpanLocation]:
|
|
233
|
+
"""Every span of one document with where it sits in the original (pages/bbox/offsets) + the clause ids
|
|
234
|
+
extracted from it -- for a citation PREVIEW (a span-id-in-hand lookup, no query/model call). Each clause
|
|
235
|
+
carries the `span_id` it came from (the only bridge between the clause-id and span-id spaces); a clause
|
|
236
|
+
whose span is not in this document is dropped rather than inventing a location."""
|
|
237
|
+
spans = self._store.all_spans_by_contract(contract_id)
|
|
238
|
+
present = {r["span_id"] for r in spans}
|
|
239
|
+
clauses: dict[str, list[str]] = {}
|
|
240
|
+
for c in self.clauses_in_contract(contract_id):
|
|
241
|
+
sid = c.get("span_id") or ""
|
|
242
|
+
if sid in present:
|
|
243
|
+
clauses.setdefault(sid, []).append(str(c.get("clause_id") or ""))
|
|
244
|
+
return [
|
|
245
|
+
SpanLocation(
|
|
246
|
+
span_id=r["span_id"], clause_ids=clauses.get(r["span_id"], []),
|
|
247
|
+
pages=[int(p) for p in (r.get("pages") or [])], bbox=_decode_bbox(r.get("bbox")),
|
|
248
|
+
doc_start=r.get("doc_start"), doc_end=r.get("doc_end"), text=r.get("text") or "",
|
|
249
|
+
)
|
|
250
|
+
for r in spans
|
|
251
|
+
]
|