contexttrace 1.2.0__tar.gz → 1.3.0__tar.gz
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.
- {contexttrace-1.2.0 → contexttrace-1.3.0}/PKG-INFO +59 -2
- {contexttrace-1.2.0 → contexttrace-1.3.0}/README.md +57 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/__init__.py +13 -2
- contexttrace-1.3.0/contexttrace/_version.py +1 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/cli.py +37 -3
- contexttrace-1.3.0/contexttrace/evidence_integrity.py +257 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/integrations/__init__.py +10 -2
- contexttrace-1.3.0/contexttrace/integrations/_lineage.py +34 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/integrations/langchain.py +46 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/integrations/llamaindex.py +50 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/repair.py +77 -6
- contexttrace-1.3.0/contexttrace/verify/atomic_coverage.py +312 -0
- contexttrace-1.3.0/contexttrace/verify/local_quality.py +884 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/trace_inspect.py +3 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace.egg-info/SOURCES.txt +4 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/pyproject.toml +2 -2
- contexttrace-1.2.0/contexttrace/_version.py +0 -1
- {contexttrace-1.2.0 → contexttrace-1.3.0}/LICENSE +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/MANIFEST.in +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/capture.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/capture_endpoint.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/client.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/config.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/contracts.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/demo.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/demo_data.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/diagnose.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/diagnose_report.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/endpoint_eval.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/errors.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/evaluator.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/integrations/fastapi.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/integrations/langgraph.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/integrations/opentelemetry.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/local.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/privacy.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/py.typed +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/regression.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/reliability.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/report.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/schemas/__init__.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/schemas/claim-verification-hybrid-v2.schema.json +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/schemas/claim-verification-v1.schema.json +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/schemas/claim-verification-v2.schema.json +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/schemas/diagnosis-v1.schema.json +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/schemas/regression-case-v1.schema.json +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/schemas/repair-plan-v1.schema.json +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/schemas/trace-v1.schema.json +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/storage/__init__.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/storage/sqlite_store.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/thresholds.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/transport.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/__init__.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/abstention.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/audit.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/audit_benchmark.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/audit_benchmark_cases.json +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/audit_report.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/benchmark.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/calibration.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/citations.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/claims.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/compare.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/compare_report.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/demos.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/evidence.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/external_benchmark_cases.json +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/facts.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/hybrid_v2/__init__.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/hybrid_v2/constants.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/hybrid_v2/development_manifest.json +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/hybrid_v2/relations.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/hybrid_v2/runner.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/hybrid_v2/schema.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/judges.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/local_ml.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/local_nli.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/nli_calibration.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/public_holdout_cases.json +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/qa.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/qa_report.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/real_benchmark_cases.json +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/report.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/root_cause.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/rulepacks/generic_v1.yaml +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/rulepacks/legacy_ragtruth_calibrated.yaml +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/rulepacks/policy.yaml +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/rulepacks/temporal.yaml +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/runner.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/schema.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/semantic_core/__init__.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/semantic_core_v2/__init__.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/semantic_core_v2/cascade.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/semantic_core_v2/citations.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/semantic_core_v2/claims.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/semantic_core_v2/constants.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/semantic_core_v2/diagnosis.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/semantic_core_v2/limits.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/semantic_core_v2/nli.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/semantic_core_v2/profile.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/semantic_core_v2/rulepacks/generic_v2.json +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/semantic_core_v2/rulepacks/source_condition_v2.json +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/semantic_core_v2/rulepacks.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/semantic_core_v2/runner.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/semantic_core_v2/schema.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/semantic_core_v2/source.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/semantic_core_v2_1/__init__.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/semantic_core_v2_1/artifacts/__init__.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/semantic_core_v2_1/artifacts/support-risk-v1.json +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/semantic_core_v2_1/attribution.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/semantic_core_v2_1/bounded.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/semantic_core_v2_1/checker.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/semantic_core_v2_1/claims.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/semantic_core_v2_1/grouping.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/semantic_core_v2_1/profile.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/semantic_core_v2_1/risk_model.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/semantic_core_v2_1/runner.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/semantic_core_v2_1/source.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/semantic_normalization.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/source_trust.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/spans.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/statuses.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/suite.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/suite_report.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/triage.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/verify/verdicts.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/contexttrace/viewer.py +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/setup.cfg +0 -0
- {contexttrace-1.2.0 → contexttrace-1.3.0}/setup.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: contexttrace
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Local-first evidence-chain debugger for RAG and AI agent claim grounding, citation checks, root-cause diagnosis, and regression tests.
|
|
5
|
-
Author:
|
|
5
|
+
Author: Samarth Vinayaka
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/samarth1412/Context-Trace
|
|
8
8
|
Project-URL: Documentation, https://github.com/samarth1412/Context-Trace/tree/main/docs
|
|
@@ -149,6 +149,63 @@ ContextTrace classifies each claim as `supported`, `partially_supported`, `unsup
|
|
|
149
149
|
|
|
150
150
|
Important: `supported` means grounded by the selected evidence span. It does not mean independently true, current, or authoritative.
|
|
151
151
|
|
|
152
|
+
### Audit evidence transformations
|
|
153
|
+
|
|
154
|
+
Instrument a selector or chunker with captured source lineage, then check
|
|
155
|
+
whether it dropped a linked answer, declared qualifier, condition, or value:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from contexttrace import build_evidence_lineage, capture_rag_trace
|
|
159
|
+
|
|
160
|
+
source = "Question: When? Answer: After approval."
|
|
161
|
+
lineage = build_evidence_lineage(
|
|
162
|
+
source_unit_id="refund_qa",
|
|
163
|
+
source_text=source,
|
|
164
|
+
linked_parts=[
|
|
165
|
+
{"id": "question", "role": "question", "text": "Question: When?"},
|
|
166
|
+
{"id": "answer", "role": "answer", "text": "Answer: After approval."},
|
|
167
|
+
],
|
|
168
|
+
)
|
|
169
|
+
trace = capture_rag_trace(
|
|
170
|
+
query="When?",
|
|
171
|
+
answer="After approval.",
|
|
172
|
+
contexts=[{"id": "selected", "text": "Question: When?", "metadata": lineage}],
|
|
173
|
+
)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
At a framework selection boundary, clone and bind the selected object directly:
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from contexttrace import (
|
|
180
|
+
bind_langchain_evidence_lineage,
|
|
181
|
+
bind_llamaindex_evidence_lineage,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
selected_document = bind_langchain_evidence_lineage(
|
|
185
|
+
selected_document,
|
|
186
|
+
source_document=parent_document,
|
|
187
|
+
material_spans=declared_material_spans,
|
|
188
|
+
)
|
|
189
|
+
selected_node = bind_llamaindex_evidence_lineage(
|
|
190
|
+
selected_node,
|
|
191
|
+
source_node=parent_node,
|
|
192
|
+
material_spans=declared_material_spans,
|
|
193
|
+
)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Both helpers preserve the original object and existing callback paths carry the
|
|
197
|
+
lineage metadata into ContextTrace.
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
contexttrace inspect trace.json --fail-on evidence_integrity
|
|
201
|
+
contexttrace repair trace.json --out repair.md
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The audit runs locally with no model or network calls. Missing lineage stays
|
|
205
|
+
unknown, and the stable claim verifier remains unchanged. See the
|
|
206
|
+
[evidence-integrity contract](../../docs/evidence-integrity-v1.3.md) and
|
|
207
|
+
[offline examples](../../examples/evidence_integrity/README.md).
|
|
208
|
+
|
|
152
209
|
## Diagnose An Agent Trace
|
|
153
210
|
|
|
154
211
|
`diagnose` also accepts agent step traces and localizes tool/final-answer
|
|
@@ -74,6 +74,63 @@ ContextTrace classifies each claim as `supported`, `partially_supported`, `unsup
|
|
|
74
74
|
|
|
75
75
|
Important: `supported` means grounded by the selected evidence span. It does not mean independently true, current, or authoritative.
|
|
76
76
|
|
|
77
|
+
### Audit evidence transformations
|
|
78
|
+
|
|
79
|
+
Instrument a selector or chunker with captured source lineage, then check
|
|
80
|
+
whether it dropped a linked answer, declared qualifier, condition, or value:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from contexttrace import build_evidence_lineage, capture_rag_trace
|
|
84
|
+
|
|
85
|
+
source = "Question: When? Answer: After approval."
|
|
86
|
+
lineage = build_evidence_lineage(
|
|
87
|
+
source_unit_id="refund_qa",
|
|
88
|
+
source_text=source,
|
|
89
|
+
linked_parts=[
|
|
90
|
+
{"id": "question", "role": "question", "text": "Question: When?"},
|
|
91
|
+
{"id": "answer", "role": "answer", "text": "Answer: After approval."},
|
|
92
|
+
],
|
|
93
|
+
)
|
|
94
|
+
trace = capture_rag_trace(
|
|
95
|
+
query="When?",
|
|
96
|
+
answer="After approval.",
|
|
97
|
+
contexts=[{"id": "selected", "text": "Question: When?", "metadata": lineage}],
|
|
98
|
+
)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
At a framework selection boundary, clone and bind the selected object directly:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from contexttrace import (
|
|
105
|
+
bind_langchain_evidence_lineage,
|
|
106
|
+
bind_llamaindex_evidence_lineage,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
selected_document = bind_langchain_evidence_lineage(
|
|
110
|
+
selected_document,
|
|
111
|
+
source_document=parent_document,
|
|
112
|
+
material_spans=declared_material_spans,
|
|
113
|
+
)
|
|
114
|
+
selected_node = bind_llamaindex_evidence_lineage(
|
|
115
|
+
selected_node,
|
|
116
|
+
source_node=parent_node,
|
|
117
|
+
material_spans=declared_material_spans,
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Both helpers preserve the original object and existing callback paths carry the
|
|
122
|
+
lineage metadata into ContextTrace.
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
contexttrace inspect trace.json --fail-on evidence_integrity
|
|
126
|
+
contexttrace repair trace.json --out repair.md
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The audit runs locally with no model or network calls. Missing lineage stays
|
|
130
|
+
unknown, and the stable claim verifier remains unchanged. See the
|
|
131
|
+
[evidence-integrity contract](../../docs/evidence-integrity-v1.3.md) and
|
|
132
|
+
[offline examples](../../examples/evidence_integrity/README.md).
|
|
133
|
+
|
|
77
134
|
## Diagnose An Agent Trace
|
|
78
135
|
|
|
79
136
|
`diagnose` also accepts agent step traces and localizes tool/final-answer
|
|
@@ -12,10 +12,17 @@ from contexttrace.errors import (
|
|
|
12
12
|
ContextTraceHTTPError,
|
|
13
13
|
ContextTraceLocalError,
|
|
14
14
|
)
|
|
15
|
+
from contexttrace.evidence_integrity import audit_evidence_integrity, build_evidence_lineage
|
|
15
16
|
from contexttrace.integrations.fastapi import ContextTraceFastAPIMiddleware
|
|
16
|
-
from contexttrace.integrations.langchain import
|
|
17
|
+
from contexttrace.integrations.langchain import (
|
|
18
|
+
ContextTraceCallbackHandler,
|
|
19
|
+
bind_langchain_evidence_lineage,
|
|
20
|
+
)
|
|
17
21
|
from contexttrace.integrations.langgraph import ContextTraceLangGraphTracer
|
|
18
|
-
from contexttrace.integrations.llamaindex import
|
|
22
|
+
from contexttrace.integrations.llamaindex import (
|
|
23
|
+
ContextTraceLlamaIndexCallbackHandler,
|
|
24
|
+
bind_llamaindex_evidence_lineage,
|
|
25
|
+
)
|
|
19
26
|
from contexttrace.integrations.opentelemetry import OpenTelemetryExporter, export_contexttrace_trace
|
|
20
27
|
from contexttrace.privacy import PrivacyPolicy, TextCipher
|
|
21
28
|
from contexttrace.reliability import ReliabilityScore, ReliabilityScorer
|
|
@@ -47,7 +54,11 @@ __all__ = [
|
|
|
47
54
|
"capture_response_trace",
|
|
48
55
|
"build_repair_plan",
|
|
49
56
|
"build_regression_case",
|
|
57
|
+
"build_evidence_lineage",
|
|
58
|
+
"bind_langchain_evidence_lineage",
|
|
59
|
+
"bind_llamaindex_evidence_lineage",
|
|
50
60
|
"diagnose_payload",
|
|
61
|
+
"audit_evidence_integrity",
|
|
51
62
|
"diagnose_trace_file",
|
|
52
63
|
"write_diagnosis_regression_test",
|
|
53
64
|
"export_contexttrace_trace",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.3.0"
|
|
@@ -405,7 +405,13 @@ def verify_v2_command(
|
|
|
405
405
|
@cli.command("inspect")
|
|
406
406
|
@click.argument("trace_json")
|
|
407
407
|
@click.option("--json", "json_output", is_flag=True, help="Print trace inspection as JSON.")
|
|
408
|
-
|
|
408
|
+
@click.option(
|
|
409
|
+
"--fail-on",
|
|
410
|
+
multiple=True,
|
|
411
|
+
type=click.Choice(["warning", "evidence_integrity", "unknown_integrity"]),
|
|
412
|
+
help="Exit nonzero on trace warnings, observed evidence-integrity issues, or uncaptured lineage.",
|
|
413
|
+
)
|
|
414
|
+
def inspect_command(trace_json: str, json_output: bool, fail_on: tuple[str, ...]) -> int:
|
|
409
415
|
"""Inspect portable RAG trace shape before verification."""
|
|
410
416
|
|
|
411
417
|
try:
|
|
@@ -416,7 +422,7 @@ def inspect_command(trace_json: str, json_output: bool) -> int:
|
|
|
416
422
|
result = inspect_trace(trace, trace_path=trace_json)
|
|
417
423
|
if json_output:
|
|
418
424
|
click.echo(json.dumps(result, indent=2))
|
|
419
|
-
return
|
|
425
|
+
return _inspect_exit_code(result, fail_on)
|
|
420
426
|
|
|
421
427
|
click.echo("Trace: %s" % trace_json)
|
|
422
428
|
click.echo("Query: %s" % result["query"])
|
|
@@ -440,6 +446,21 @@ def inspect_command(trace_json: str, json_output: bool) -> int:
|
|
|
440
446
|
if result["metadata_keys"]:
|
|
441
447
|
click.echo("Metadata keys: %s" % ", ".join(result["metadata_keys"]))
|
|
442
448
|
|
|
449
|
+
integrity = result["evidence_integrity"]
|
|
450
|
+
integrity_summary = integrity["summary"]
|
|
451
|
+
click.echo(
|
|
452
|
+
"Evidence integrity: %s (%s issues, %s assessed, %s unknown)"
|
|
453
|
+
% (
|
|
454
|
+
integrity["status"],
|
|
455
|
+
integrity_summary["issue_count"],
|
|
456
|
+
integrity_summary["assessed_contexts"],
|
|
457
|
+
integrity_summary["unknown_contexts"],
|
|
458
|
+
)
|
|
459
|
+
)
|
|
460
|
+
for issue in integrity["issues"]:
|
|
461
|
+
detail = issue.get("item_role") or issue.get("observed")
|
|
462
|
+
click.echo("- %s [%s]: %s" % (issue["type"], issue["selected_context_id"], detail))
|
|
463
|
+
|
|
443
464
|
warnings = result["warnings"]
|
|
444
465
|
if warnings:
|
|
445
466
|
click.echo("Warnings:")
|
|
@@ -449,7 +470,20 @@ def inspect_command(trace_json: str, json_output: bool) -> int:
|
|
|
449
470
|
click.echo("Suggested next commands:")
|
|
450
471
|
for command in result["suggested_next_commands"]:
|
|
451
472
|
click.echo("- %s" % command)
|
|
452
|
-
return
|
|
473
|
+
return _inspect_exit_code(result, fail_on)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _inspect_exit_code(result: dict[str, object], fail_on: tuple[str, ...]) -> int:
|
|
477
|
+
integrity = result.get("evidence_integrity") or {}
|
|
478
|
+
summary = (integrity.get("summary") or {}) if isinstance(integrity, dict) else {}
|
|
479
|
+
issue_count = int(summary.get("issue_count") or 0) if isinstance(summary, dict) else 0
|
|
480
|
+
unknown_count = int(summary.get("unknown_contexts") or 0) if isinstance(summary, dict) else 0
|
|
481
|
+
should_fail = (
|
|
482
|
+
"warning" in fail_on and bool(result.get("warnings"))
|
|
483
|
+
or "evidence_integrity" in fail_on and issue_count > 0
|
|
484
|
+
or "unknown_integrity" in fail_on and unknown_count > 0
|
|
485
|
+
)
|
|
486
|
+
return 1 if should_fail else 0
|
|
453
487
|
|
|
454
488
|
|
|
455
489
|
@cli.command("diagnose")
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import unicodedata
|
|
5
|
+
from typing import Any, Iterable
|
|
6
|
+
|
|
7
|
+
from contexttrace.verify.schema import RAGTrace, TraceContext
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
EVIDENCE_METADATA_KEY = "contexttrace_evidence"
|
|
11
|
+
|
|
12
|
+
LINKED_PART_DROPPED = "linked_part_dropped"
|
|
13
|
+
MATERIAL_SPAN_DROPPED = "material_span_dropped"
|
|
14
|
+
SELECTED_TEXT_NOT_IN_SOURCE = "selected_text_not_in_source"
|
|
15
|
+
INVALID_LINEAGE = "invalid_lineage"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build_evidence_lineage(
|
|
19
|
+
*,
|
|
20
|
+
source_unit_id: str,
|
|
21
|
+
source_text: str,
|
|
22
|
+
linked_parts: Iterable[dict[str, Any]] | None = None,
|
|
23
|
+
material_spans: Iterable[dict[str, Any]] | None = None,
|
|
24
|
+
transformation: str | None = None,
|
|
25
|
+
) -> dict[str, Any]:
|
|
26
|
+
"""Build namespaced metadata for deterministic source-to-selection checks."""
|
|
27
|
+
|
|
28
|
+
unit_id = _required_text(source_unit_id, "source_unit_id")
|
|
29
|
+
text = _required_text(source_text, "source_text")
|
|
30
|
+
lineage: dict[str, Any] = {
|
|
31
|
+
"schema_version": "1.0",
|
|
32
|
+
"source_unit_id": unit_id,
|
|
33
|
+
"source_text": text,
|
|
34
|
+
}
|
|
35
|
+
if linked_parts is not None:
|
|
36
|
+
lineage["linked_parts"] = _build_parts(linked_parts, text, "linked_parts")
|
|
37
|
+
if material_spans is not None:
|
|
38
|
+
lineage["material_spans"] = _build_parts(material_spans, text, "material_spans")
|
|
39
|
+
if transformation is not None:
|
|
40
|
+
lineage["transformation"] = _required_text(transformation, "transformation")
|
|
41
|
+
return {EVIDENCE_METADATA_KEY: lineage}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def audit_evidence_integrity(trace: RAGTrace) -> dict[str, Any]:
|
|
45
|
+
"""Report observed evidence loss from explicitly captured lineage metadata."""
|
|
46
|
+
|
|
47
|
+
assessments = [_assess_context(context) for context in trace.contexts]
|
|
48
|
+
captured = [item for item in assessments if item["capture_status"] == "captured"]
|
|
49
|
+
unknown = [item for item in assessments if item["capture_status"] != "captured"]
|
|
50
|
+
issues = [issue for item in captured for issue in item["issues"]]
|
|
51
|
+
if not captured:
|
|
52
|
+
status = "not_captured"
|
|
53
|
+
elif issues:
|
|
54
|
+
status = "issues_found"
|
|
55
|
+
elif unknown:
|
|
56
|
+
status = "partial"
|
|
57
|
+
else:
|
|
58
|
+
status = "complete"
|
|
59
|
+
counts: dict[str, int] = {}
|
|
60
|
+
for issue in issues:
|
|
61
|
+
issue_type = str(issue["type"])
|
|
62
|
+
counts[issue_type] = counts.get(issue_type, 0) + 1
|
|
63
|
+
return {
|
|
64
|
+
"schema_version": "1.0",
|
|
65
|
+
"status": status,
|
|
66
|
+
"summary": {
|
|
67
|
+
"total_contexts": len(trace.contexts),
|
|
68
|
+
"assessed_contexts": len(captured),
|
|
69
|
+
"unknown_contexts": len(unknown),
|
|
70
|
+
"issue_count": len(issues),
|
|
71
|
+
"issue_types": counts,
|
|
72
|
+
},
|
|
73
|
+
"issues": issues,
|
|
74
|
+
"contexts": assessments,
|
|
75
|
+
"network_calls": 0,
|
|
76
|
+
"model_calls": 0,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _assess_context(context: TraceContext) -> dict[str, Any]:
|
|
81
|
+
raw = context.metadata.get(EVIDENCE_METADATA_KEY)
|
|
82
|
+
if raw is None:
|
|
83
|
+
return {
|
|
84
|
+
"selected_context_id": context.id,
|
|
85
|
+
"capture_status": "not_captured",
|
|
86
|
+
"reason": "No contexttrace_evidence lineage metadata was captured.",
|
|
87
|
+
"issues": [],
|
|
88
|
+
}
|
|
89
|
+
if not isinstance(raw, dict):
|
|
90
|
+
return _invalid_context(context.id, "contexttrace_evidence must be an object.")
|
|
91
|
+
source_unit_id = _clean(raw.get("source_unit_id"))
|
|
92
|
+
source_text = _clean(raw.get("source_text"))
|
|
93
|
+
if not source_unit_id or not source_text:
|
|
94
|
+
return _invalid_context(
|
|
95
|
+
context.id,
|
|
96
|
+
"Captured lineage must include non-empty source_unit_id and source_text.",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
selected = _normalize(context.text)
|
|
100
|
+
source = _normalize(source_text)
|
|
101
|
+
linked_parts, invalid_parts = _parts(raw.get("linked_parts"), source_text, "linked_parts")
|
|
102
|
+
material_spans, invalid_spans = _parts(
|
|
103
|
+
raw.get("material_spans"), source_text, "material_spans"
|
|
104
|
+
)
|
|
105
|
+
if invalid_parts or invalid_spans:
|
|
106
|
+
return _invalid_context(context.id, "; ".join(invalid_parts + invalid_spans))
|
|
107
|
+
|
|
108
|
+
issues: list[dict[str, Any]] = []
|
|
109
|
+
if selected not in source:
|
|
110
|
+
issues.append(
|
|
111
|
+
_issue(
|
|
112
|
+
issue_type=SELECTED_TEXT_NOT_IN_SOURCE,
|
|
113
|
+
severity="high",
|
|
114
|
+
context_id=context.id,
|
|
115
|
+
source_unit_id=source_unit_id,
|
|
116
|
+
observed="Selected text is not a normalized verbatim span of the captured source unit.",
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
else:
|
|
120
|
+
present_parts = [
|
|
121
|
+
part
|
|
122
|
+
for part in linked_parts
|
|
123
|
+
if part["required"] and _normalize(part["text"]) in selected
|
|
124
|
+
]
|
|
125
|
+
for part in linked_parts:
|
|
126
|
+
if not part["required"] or _normalize(part["text"]) in selected:
|
|
127
|
+
continue
|
|
128
|
+
if present_parts:
|
|
129
|
+
issues.append(
|
|
130
|
+
_issue(
|
|
131
|
+
issue_type=LINKED_PART_DROPPED,
|
|
132
|
+
severity="high",
|
|
133
|
+
context_id=context.id,
|
|
134
|
+
source_unit_id=source_unit_id,
|
|
135
|
+
observed="A required linked part is present in the source unit but absent from the selected text.",
|
|
136
|
+
item=part,
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
for span in material_spans:
|
|
140
|
+
if span["required"] and _normalize(span["text"]) not in selected:
|
|
141
|
+
issues.append(
|
|
142
|
+
_issue(
|
|
143
|
+
issue_type=MATERIAL_SPAN_DROPPED,
|
|
144
|
+
severity="high",
|
|
145
|
+
context_id=context.id,
|
|
146
|
+
source_unit_id=source_unit_id,
|
|
147
|
+
observed="A declared material span is present in the source unit but absent from the selected text.",
|
|
148
|
+
item=span,
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
"selected_context_id": context.id,
|
|
154
|
+
"source_unit_id": source_unit_id,
|
|
155
|
+
"transformation": _clean(raw.get("transformation")) or None,
|
|
156
|
+
"capture_status": "captured",
|
|
157
|
+
"issues": issues,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _parts(value: Any, source_text: str, field: str) -> tuple[list[dict[str, Any]], list[str]]:
|
|
162
|
+
if value is None:
|
|
163
|
+
return [], []
|
|
164
|
+
if not isinstance(value, list):
|
|
165
|
+
return [], ["%s must be a list." % field]
|
|
166
|
+
parts: list[dict[str, Any]] = []
|
|
167
|
+
invalid: list[str] = []
|
|
168
|
+
for index, item in enumerate(value):
|
|
169
|
+
try:
|
|
170
|
+
part = _part(item, index, field)
|
|
171
|
+
except ValueError as exc:
|
|
172
|
+
invalid.append(str(exc))
|
|
173
|
+
continue
|
|
174
|
+
if _normalize(part["text"]) not in _normalize(source_text):
|
|
175
|
+
invalid.append("%s[%s].text is not present in source_text." % (field, index))
|
|
176
|
+
parts.append(part)
|
|
177
|
+
return parts, invalid
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _build_parts(
|
|
181
|
+
values: Iterable[dict[str, Any]], source_text: str, field: str
|
|
182
|
+
) -> list[dict[str, Any]]:
|
|
183
|
+
parts = [_part(value, index, field) for index, value in enumerate(values)]
|
|
184
|
+
for index, part in enumerate(parts):
|
|
185
|
+
if _normalize(part["text"]) not in _normalize(source_text):
|
|
186
|
+
raise ValueError("%s[%s].text must be present in source_text." % (field, index))
|
|
187
|
+
return parts
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _part(value: Any, index: int, field: str) -> dict[str, Any]:
|
|
191
|
+
if not isinstance(value, dict):
|
|
192
|
+
raise ValueError("%s[%s] must be an object." % (field, index))
|
|
193
|
+
required = value.get("required", True)
|
|
194
|
+
if not isinstance(required, bool):
|
|
195
|
+
raise ValueError("%s[%s].required must be a boolean." % (field, index))
|
|
196
|
+
return {
|
|
197
|
+
"id": _required_text(value.get("id"), "%s[%s].id" % (field, index)),
|
|
198
|
+
"role": _required_text(value.get("role"), "%s[%s].role" % (field, index)),
|
|
199
|
+
"text": _required_text(value.get("text"), "%s[%s].text" % (field, index)),
|
|
200
|
+
"required": required,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _issue(
|
|
205
|
+
*,
|
|
206
|
+
issue_type: str,
|
|
207
|
+
severity: str,
|
|
208
|
+
context_id: str,
|
|
209
|
+
source_unit_id: str,
|
|
210
|
+
observed: str,
|
|
211
|
+
item: dict[str, Any] | None = None,
|
|
212
|
+
) -> dict[str, Any]:
|
|
213
|
+
result: dict[str, Any] = {
|
|
214
|
+
"type": issue_type,
|
|
215
|
+
"status": "observed",
|
|
216
|
+
"severity": severity,
|
|
217
|
+
"selected_context_id": context_id,
|
|
218
|
+
"source_unit_id": source_unit_id,
|
|
219
|
+
"observed": observed,
|
|
220
|
+
}
|
|
221
|
+
if item is not None:
|
|
222
|
+
result.update(
|
|
223
|
+
{
|
|
224
|
+
"item_id": item["id"],
|
|
225
|
+
"item_role": item["role"],
|
|
226
|
+
"missing_text": item["text"],
|
|
227
|
+
}
|
|
228
|
+
)
|
|
229
|
+
return result
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _invalid_context(context_id: str, reason: str) -> dict[str, Any]:
|
|
233
|
+
return {
|
|
234
|
+
"selected_context_id": context_id,
|
|
235
|
+
"capture_status": INVALID_LINEAGE,
|
|
236
|
+
"reason": reason,
|
|
237
|
+
"issues": [],
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _required_text(value: Any, field: str) -> str:
|
|
242
|
+
if not isinstance(value, str):
|
|
243
|
+
raise ValueError("%s must be a non-empty string." % field)
|
|
244
|
+
text = value.strip()
|
|
245
|
+
if not text:
|
|
246
|
+
raise ValueError("%s must be a non-empty string." % field)
|
|
247
|
+
return text
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _clean(value: Any) -> str:
|
|
251
|
+
return "" if value is None else str(value).strip()
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _normalize(value: str) -> str:
|
|
255
|
+
text = unicodedata.normalize("NFKC", value)
|
|
256
|
+
text = text.translate(str.maketrans("“”‘’", "\"\"''"))
|
|
257
|
+
return re.sub(r"\s+", " ", text).strip().casefold()
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
from contexttrace.integrations.fastapi import ContextTraceFastAPIMiddleware
|
|
2
|
-
from contexttrace.integrations.langchain import
|
|
2
|
+
from contexttrace.integrations.langchain import (
|
|
3
|
+
ContextTraceCallbackHandler,
|
|
4
|
+
bind_langchain_evidence_lineage,
|
|
5
|
+
)
|
|
3
6
|
from contexttrace.integrations.langgraph import ContextTraceLangGraphTracer
|
|
4
|
-
from contexttrace.integrations.llamaindex import
|
|
7
|
+
from contexttrace.integrations.llamaindex import (
|
|
8
|
+
ContextTraceLlamaIndexCallbackHandler,
|
|
9
|
+
bind_llamaindex_evidence_lineage,
|
|
10
|
+
)
|
|
5
11
|
from contexttrace.integrations.opentelemetry import OpenTelemetryExporter, export_contexttrace_trace
|
|
6
12
|
|
|
7
13
|
__all__ = [
|
|
@@ -9,6 +15,8 @@ __all__ = [
|
|
|
9
15
|
"ContextTraceFastAPIMiddleware",
|
|
10
16
|
"ContextTraceLangGraphTracer",
|
|
11
17
|
"ContextTraceLlamaIndexCallbackHandler",
|
|
18
|
+
"bind_langchain_evidence_lineage",
|
|
19
|
+
"bind_llamaindex_evidence_lineage",
|
|
12
20
|
"OpenTelemetryExporter",
|
|
13
21
|
"export_contexttrace_trace",
|
|
14
22
|
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from copy import copy
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def clone_with_updates(value: Any, **updates: Any) -> Any:
|
|
8
|
+
"""Clone a framework value without mutating the application-owned object."""
|
|
9
|
+
|
|
10
|
+
if isinstance(value, dict):
|
|
11
|
+
cloned = dict(value)
|
|
12
|
+
cloned.update(updates)
|
|
13
|
+
return cloned
|
|
14
|
+
|
|
15
|
+
model_copy = getattr(value, "model_copy", None)
|
|
16
|
+
if callable(model_copy):
|
|
17
|
+
return model_copy(update=updates)
|
|
18
|
+
|
|
19
|
+
legacy_copy = getattr(value, "copy", None)
|
|
20
|
+
if callable(legacy_copy):
|
|
21
|
+
try:
|
|
22
|
+
return legacy_copy(update=updates)
|
|
23
|
+
except TypeError:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
cloned = copy(value)
|
|
28
|
+
for key, item in updates.items():
|
|
29
|
+
setattr(cloned, key, item)
|
|
30
|
+
except (AttributeError, TypeError) as exc:
|
|
31
|
+
raise TypeError(
|
|
32
|
+
"Could not clone %s with evidence-lineage metadata." % type(value).__name__
|
|
33
|
+
) from exc
|
|
34
|
+
return cloned
|
|
@@ -7,6 +7,8 @@ from dataclasses import dataclass, field
|
|
|
7
7
|
from typing import Any, Callable, Dict, Iterable, Optional
|
|
8
8
|
|
|
9
9
|
from contexttrace.client import ContextTrace
|
|
10
|
+
from contexttrace.evidence_integrity import build_evidence_lineage
|
|
11
|
+
from contexttrace.integrations._lineage import clone_with_updates
|
|
10
12
|
|
|
11
13
|
try:
|
|
12
14
|
from langchain_core.callbacks import BaseCallbackHandler
|
|
@@ -436,6 +438,50 @@ def langchain_document_to_chunk(document: Any, index: int = 0) -> dict[str, Any]
|
|
|
436
438
|
}
|
|
437
439
|
|
|
438
440
|
|
|
441
|
+
def bind_langchain_evidence_lineage(
|
|
442
|
+
selected_document: Any,
|
|
443
|
+
*,
|
|
444
|
+
source_document: Any,
|
|
445
|
+
source_unit_id: str | None = None,
|
|
446
|
+
linked_parts: Iterable[dict[str, Any]] | None = None,
|
|
447
|
+
material_spans: Iterable[dict[str, Any]] | None = None,
|
|
448
|
+
transformation: str = "langchain_document_selection",
|
|
449
|
+
) -> Any:
|
|
450
|
+
"""Clone a selected LangChain document with its captured source lineage."""
|
|
451
|
+
|
|
452
|
+
source_chunk = langchain_document_to_chunk(source_document)
|
|
453
|
+
selected_chunk = langchain_document_to_chunk(selected_document)
|
|
454
|
+
resolved_source_id = source_unit_id or _langchain_document_id(source_document)
|
|
455
|
+
if not resolved_source_id:
|
|
456
|
+
raise ValueError(
|
|
457
|
+
"source_unit_id is required when the source document has no chunk_id, id, or doc_id."
|
|
458
|
+
)
|
|
459
|
+
lineage = build_evidence_lineage(
|
|
460
|
+
source_unit_id=str(resolved_source_id),
|
|
461
|
+
source_text=str(source_chunk["content"]),
|
|
462
|
+
linked_parts=linked_parts,
|
|
463
|
+
material_spans=material_spans,
|
|
464
|
+
transformation=transformation,
|
|
465
|
+
)
|
|
466
|
+
metadata = dict(selected_chunk["metadata"])
|
|
467
|
+
metadata.update(lineage)
|
|
468
|
+
return clone_with_updates(selected_document, metadata=metadata)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _langchain_document_id(document: Any) -> Any:
|
|
472
|
+
metadata = getattr(document, "metadata", None) or {}
|
|
473
|
+
if isinstance(document, dict):
|
|
474
|
+
metadata = document.get("metadata") or metadata
|
|
475
|
+
if not isinstance(metadata, dict):
|
|
476
|
+
metadata = {}
|
|
477
|
+
return (
|
|
478
|
+
metadata.get("chunk_id")
|
|
479
|
+
or metadata.get("id")
|
|
480
|
+
or metadata.get("doc_id")
|
|
481
|
+
or getattr(document, "id", None)
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
|
|
439
485
|
def _extract_query(inputs: Any) -> Optional[str]:
|
|
440
486
|
if isinstance(inputs, str):
|
|
441
487
|
return inputs
|
|
@@ -6,6 +6,8 @@ from dataclasses import dataclass, field
|
|
|
6
6
|
from typing import Any, Callable, Dict, Iterable, Optional
|
|
7
7
|
|
|
8
8
|
from contexttrace.client import ContextTrace
|
|
9
|
+
from contexttrace.evidence_integrity import build_evidence_lineage
|
|
10
|
+
from contexttrace.integrations._lineage import clone_with_updates
|
|
9
11
|
|
|
10
12
|
try:
|
|
11
13
|
from llama_index.core.callbacks.base_handler import BaseCallbackHandler
|
|
@@ -365,6 +367,54 @@ def llamaindex_node_to_chunk(node_or_node_with_score: Any, index: int = 0) -> di
|
|
|
365
367
|
}
|
|
366
368
|
|
|
367
369
|
|
|
370
|
+
def bind_llamaindex_evidence_lineage(
|
|
371
|
+
selected_node: Any,
|
|
372
|
+
*,
|
|
373
|
+
source_node: Any,
|
|
374
|
+
source_unit_id: str | None = None,
|
|
375
|
+
linked_parts: Iterable[dict[str, Any]] | None = None,
|
|
376
|
+
material_spans: Iterable[dict[str, Any]] | None = None,
|
|
377
|
+
transformation: str = "llamaindex_node_postprocessor",
|
|
378
|
+
) -> Any:
|
|
379
|
+
"""Clone a selected LlamaIndex node with its captured source lineage."""
|
|
380
|
+
|
|
381
|
+
source_chunk = llamaindex_node_to_chunk(source_node)
|
|
382
|
+
selected_chunk = llamaindex_node_to_chunk(selected_node)
|
|
383
|
+
resolved_source_id = source_unit_id or _llamaindex_node_id(source_node)
|
|
384
|
+
if not resolved_source_id:
|
|
385
|
+
raise ValueError(
|
|
386
|
+
"source_unit_id is required when the source node has no chunk_id, id, doc_id, or node_id."
|
|
387
|
+
)
|
|
388
|
+
lineage = build_evidence_lineage(
|
|
389
|
+
source_unit_id=str(resolved_source_id),
|
|
390
|
+
source_text=str(source_chunk["content"]),
|
|
391
|
+
linked_parts=linked_parts,
|
|
392
|
+
material_spans=material_spans,
|
|
393
|
+
transformation=transformation,
|
|
394
|
+
)
|
|
395
|
+
metadata = dict(selected_chunk["metadata"])
|
|
396
|
+
metadata.update(lineage)
|
|
397
|
+
|
|
398
|
+
selected_value = getattr(selected_node, "node", selected_node)
|
|
399
|
+
cloned_node = clone_with_updates(selected_value, metadata=metadata)
|
|
400
|
+
if selected_value is selected_node:
|
|
401
|
+
return cloned_node
|
|
402
|
+
return clone_with_updates(selected_node, node=cloned_node)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _llamaindex_node_id(node_or_node_with_score: Any) -> Any:
|
|
406
|
+
node = getattr(node_or_node_with_score, "node", node_or_node_with_score)
|
|
407
|
+
metadata = _node_metadata(node)
|
|
408
|
+
return (
|
|
409
|
+
metadata.get("chunk_id")
|
|
410
|
+
or metadata.get("id")
|
|
411
|
+
or metadata.get("doc_id")
|
|
412
|
+
or getattr(node, "node_id", None)
|
|
413
|
+
or getattr(node, "id_", None)
|
|
414
|
+
or getattr(node, "id", None)
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
|
|
368
418
|
def _node_metadata(node: Any) -> dict[str, Any]:
|
|
369
419
|
metadata = getattr(node, "metadata", None)
|
|
370
420
|
if metadata is None:
|