crprotocol 2.0.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.
Files changed (153) hide show
  1. crp/__init__.py +126 -0
  2. crp/__main__.py +8 -0
  3. crp/_typing.py +27 -0
  4. crp/_version.py +5 -0
  5. crp/adapters.py +31 -0
  6. crp/advanced/__init__.py +40 -0
  7. crp/advanced/auto_ingest.py +400 -0
  8. crp/advanced/cqs.py +235 -0
  9. crp/advanced/cross_window.py +477 -0
  10. crp/advanced/curator.py +265 -0
  11. crp/advanced/feedback.py +146 -0
  12. crp/advanced/hierarchical.py +211 -0
  13. crp/advanced/meta_learning.py +401 -0
  14. crp/advanced/parallel.py +98 -0
  15. crp/advanced/review_cycle.py +329 -0
  16. crp/advanced/scale_mode.py +129 -0
  17. crp/advanced/source_grounding.py +207 -0
  18. crp/ckf/__init__.py +35 -0
  19. crp/ckf/community.py +377 -0
  20. crp/ckf/fabric.py +445 -0
  21. crp/ckf/gc.py +175 -0
  22. crp/ckf/graph_walk.py +87 -0
  23. crp/ckf/merge.py +133 -0
  24. crp/ckf/pattern_query.py +122 -0
  25. crp/ckf/pubsub.py +128 -0
  26. crp/ckf/semantic.py +207 -0
  27. crp/cli/__init__.py +7 -0
  28. crp/cli/main.py +329 -0
  29. crp/cli/sidecar.py +929 -0
  30. crp/cli/startup.py +272 -0
  31. crp/continuation/__init__.py +103 -0
  32. crp/continuation/completion.py +348 -0
  33. crp/continuation/degradation.py +157 -0
  34. crp/continuation/document_map.py +160 -0
  35. crp/continuation/flow.py +109 -0
  36. crp/continuation/gap.py +419 -0
  37. crp/continuation/manager.py +484 -0
  38. crp/continuation/quality_monitor.py +179 -0
  39. crp/continuation/stitch.py +419 -0
  40. crp/continuation/trigger.py +142 -0
  41. crp/continuation/voice.py +157 -0
  42. crp/core/__init__.py +69 -0
  43. crp/core/batch.py +77 -0
  44. crp/core/circuit_breaker.py +116 -0
  45. crp/core/config.py +377 -0
  46. crp/core/context_tools.py +540 -0
  47. crp/core/dispatch_router.py +3977 -0
  48. crp/core/errors.py +128 -0
  49. crp/core/extraction_facade.py +384 -0
  50. crp/core/facilitator.py +713 -0
  51. crp/core/idempotency.py +215 -0
  52. crp/core/orchestrator.py +1435 -0
  53. crp/core/relay_strategies.py +613 -0
  54. crp/core/security_manager.py +140 -0
  55. crp/core/session.py +134 -0
  56. crp/core/task_intent.py +36 -0
  57. crp/core/window.py +363 -0
  58. crp/envelope/__init__.py +30 -0
  59. crp/envelope/builder.py +288 -0
  60. crp/envelope/decomposer.py +236 -0
  61. crp/envelope/formatter.py +168 -0
  62. crp/envelope/packer.py +211 -0
  63. crp/envelope/reranker.py +209 -0
  64. crp/envelope/scoring.py +310 -0
  65. crp/extraction/__init__.py +45 -0
  66. crp/extraction/complexity.py +96 -0
  67. crp/extraction/contradiction.py +132 -0
  68. crp/extraction/pipeline.py +360 -0
  69. crp/extraction/quality_gate.py +237 -0
  70. crp/extraction/stage1_regex.py +173 -0
  71. crp/extraction/stage2_statistical.py +244 -0
  72. crp/extraction/stage3_gliner.py +210 -0
  73. crp/extraction/stage4_uie.py +183 -0
  74. crp/extraction/stage5_discourse.py +175 -0
  75. crp/extraction/stage6_llm.py +178 -0
  76. crp/extraction/structured_output.py +219 -0
  77. crp/extraction/types.py +299 -0
  78. crp/license_guard.py +722 -0
  79. crp/observability/__init__.py +30 -0
  80. crp/observability/audit.py +118 -0
  81. crp/observability/events.py +233 -0
  82. crp/observability/metrics.py +264 -0
  83. crp/observability/quality.py +135 -0
  84. crp/observability/structured_logging.py +81 -0
  85. crp/observability/telemetry.py +117 -0
  86. crp/provenance/__init__.py +314 -0
  87. crp/provenance/_embeddings.py +97 -0
  88. crp/provenance/_types.py +378 -0
  89. crp/provenance/attribution_scorer.py +252 -0
  90. crp/provenance/claim_detector.py +229 -0
  91. crp/provenance/contradiction_detector.py +243 -0
  92. crp/provenance/distortion_detector.py +397 -0
  93. crp/provenance/entailment_verifier.py +358 -0
  94. crp/provenance/fabrication_detector.py +203 -0
  95. crp/provenance/hallucination_scorer.py +320 -0
  96. crp/provenance/omission_analyzer.py +106 -0
  97. crp/provenance/provenance_chain.py +205 -0
  98. crp/provenance/report_generator.py +440 -0
  99. crp/providers/__init__.py +43 -0
  100. crp/providers/anthropic.py +270 -0
  101. crp/providers/base.py +135 -0
  102. crp/providers/custom.py +63 -0
  103. crp/providers/diagnostic.py +251 -0
  104. crp/providers/llamacpp.py +224 -0
  105. crp/providers/manager.py +139 -0
  106. crp/providers/ollama.py +243 -0
  107. crp/providers/openai.py +628 -0
  108. crp/providers/tokenizers.py +48 -0
  109. crp/py.typed +0 -0
  110. crp/resources/__init__.py +53 -0
  111. crp/resources/adaptive_allocator.py +525 -0
  112. crp/resources/cost_model.py +388 -0
  113. crp/resources/overhead_manager.py +217 -0
  114. crp/resources/resource_manager.py +262 -0
  115. crp/schemas/__init__.py +20 -0
  116. crp/schemas/cost-estimate.json +33 -0
  117. crp/schemas/crp-error.json +43 -0
  118. crp/schemas/envelope-preview.json +40 -0
  119. crp/schemas/persisted-state-header.json +27 -0
  120. crp/schemas/quality-report.json +94 -0
  121. crp/schemas/session-handle.json +33 -0
  122. crp/schemas/session-status.json +57 -0
  123. crp/schemas/stream-event.json +18 -0
  124. crp/schemas/task-intent.json +42 -0
  125. crp/security/__init__.py +93 -0
  126. crp/security/audit_trail.py +392 -0
  127. crp/security/binding.py +192 -0
  128. crp/security/compliance.py +813 -0
  129. crp/security/consent.py +593 -0
  130. crp/security/embedding_defense.py +161 -0
  131. crp/security/encryption.py +202 -0
  132. crp/security/injection.py +335 -0
  133. crp/security/integrity.py +267 -0
  134. crp/security/privacy.py +662 -0
  135. crp/security/quarantine.py +249 -0
  136. crp/security/rbac.py +221 -0
  137. crp/security/validation.py +164 -0
  138. crp/state/__init__.py +31 -0
  139. crp/state/cold_storage.py +258 -0
  140. crp/state/compaction.py +263 -0
  141. crp/state/critical_state.py +104 -0
  142. crp/state/event_log.py +313 -0
  143. crp/state/fact.py +189 -0
  144. crp/state/serialization.py +189 -0
  145. crp/state/session_cleanup.py +77 -0
  146. crp/state/snapshot.py +290 -0
  147. crp/state/warm_store.py +346 -0
  148. crprotocol-2.0.0.dist-info/METADATA +1295 -0
  149. crprotocol-2.0.0.dist-info/RECORD +153 -0
  150. crprotocol-2.0.0.dist-info/WHEEL +4 -0
  151. crprotocol-2.0.0.dist-info/entry_points.txt +2 -0
  152. crprotocol-2.0.0.dist-info/licenses/LICENSE.md +170 -0
  153. crprotocol-2.0.0.dist-info/licenses/NOTICE +18 -0
@@ -0,0 +1,440 @@
1
+ # Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
2
+ # Licensed under Elastic License 2.0 — see LICENSE.md for details.
3
+ """Report Generator — human-readable provenance reports (§7.14.3).
4
+
5
+ Produces regulator-auditable decision provenance reports in markdown and
6
+ JSON formats. Reports answer: "For each claim in the AI output, what
7
+ evidence supported it and how confident are we in that attribution?"
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import time
13
+ from typing import Any
14
+
15
+ from ._types import (
16
+ AttributionType,
17
+ ClaimType,
18
+ EntailmentLabel,
19
+ HallucinationRisk,
20
+ OmissionSeverity,
21
+ ProvenanceReport,
22
+ )
23
+
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Markdown report
27
+ # ---------------------------------------------------------------------------
28
+
29
+
30
+ def generate_markdown_report(report: ProvenanceReport) -> str:
31
+ """Generate a human-readable markdown provenance report.
32
+
33
+ Suitable for regulatory audit, compliance review, or integration into
34
+ documentation. Format follows EU AI Act Article 12 logging requirements.
35
+
36
+ Args:
37
+ report: Complete provenance report from the DPE pipeline.
38
+
39
+ Returns:
40
+ Markdown-formatted report string.
41
+ """
42
+ lines: list[str] = []
43
+
44
+ # Header
45
+ lines.append("## Decision Provenance Report")
46
+ session_short = report.session_id[:12] + "..." if len(report.session_id) > 12 else report.session_id
47
+ window_short = report.window_id[:12] + "..." if len(report.window_id) > 12 else report.window_id
48
+ ts = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(report.timestamp))
49
+ lines.append(f"### Session: {session_short} | Window: {window_short} | Generated: {ts}")
50
+ lines.append("")
51
+
52
+ # Output summary
53
+ lines.append("### Output Summary")
54
+ lines.append(f"The model generated output containing **{report.total_claims}** identifiable claims.")
55
+ lines.append(f"- {report.factual_claims} factual claims (require attribution)")
56
+ lines.append(f"- {report.opinion_claims} opinions")
57
+ lines.append(f"- {report.procedural_claims} procedural instructions")
58
+ lines.append(f"- {report.hedge_claims} hedged/qualified statements")
59
+ lines.append(f"- {report.connective_claims} connective/structural")
60
+ lines.append("")
61
+
62
+ # Attribution summary table
63
+ lines.append("### Attribution Summary")
64
+ grounding_pct = f"{report.grounding_ratio * 100:.1f}%" if report.factual_claims > 0 else "N/A"
65
+
66
+ lines.append("")
67
+ lines.append("| # | Claim (preview) | Type | Source | Score | Confidence |")
68
+ lines.append("|---|-----------------|------|--------|-------|------------|")
69
+
70
+ for i, attr in enumerate(report.attributions, 1):
71
+ preview = attr.claim_text[:60] + "..." if len(attr.claim_text) > 60 else attr.claim_text
72
+ preview = preview.replace("|", "\\|")
73
+ source = attr.attribution_type.value if attr.attribution_type != AttributionType.UNCERTAIN else "—"
74
+ score = f"{attr.top_score:.2f}" if attr.top_score > 0 else "—"
75
+ conf = f"{attr.confidence:.2f}" if attr.confidence > 0 else "—"
76
+ lines.append(f"| {i} | {preview} | {attr.claim_type.value} | {source} | {score} | {conf} |")
77
+
78
+ lines.append("")
79
+
80
+ # Grounding statistics
81
+ lines.append("### Grounding Statistics")
82
+ lines.append(f"- **Context-grounded claims:** {report.context_grounded_count}")
83
+ lines.append(f"- **Parametric claims (training data):** {report.parametric_count}")
84
+ lines.append(f"- **Mixed attribution:** {report.mixed_count}")
85
+ lines.append(f"- **Uncertain attribution:** {report.uncertain_count}")
86
+ lines.append(f"- **Grounding ratio:** {grounding_pct}")
87
+ lines.append("")
88
+
89
+ # Provenance chains
90
+ if report.chains:
91
+ lines.append("### Provenance Chains (Top Factual Claims)")
92
+ for chain in report.chains:
93
+ if chain.attribution_type in (AttributionType.CONTEXT_GROUNDED, AttributionType.MIXED):
94
+ claim_preview = chain.claim_text[:80]
95
+ lines.append(f"\n**Claim #{chain.claim_index}:** \"{claim_preview}...\"")
96
+ for link in chain.links:
97
+ indent = " " if link.level != "claim" else ""
98
+ lines.append(f"{indent}← **{link.level.upper()}:** {link.label}")
99
+ lines.append("")
100
+
101
+ # Integrity
102
+ lines.append("### Provenance Chain Integrity")
103
+ status = "✅" if report.chain_verified else "⚠️ Not verified"
104
+ lines.append(f"- Audit chain integrity: {status}")
105
+ lines.append(f"- Envelope facts available: {report.envelope_facts_count}")
106
+ lines.append("")
107
+
108
+ # Regulatory notes
109
+ lines.append("### Regulatory Compliance Notes")
110
+ lines.append("- **EU AI Act Art. 12:** Activity logging ✅")
111
+ if report.factual_claims > 0:
112
+ lines.append(
113
+ f"- {report.context_grounded_count}/{report.factual_claims} factual claims "
114
+ f"grounded in provided context ({grounding_pct})"
115
+ )
116
+ if report.parametric_count > 0:
117
+ lines.append(
118
+ f"- {report.parametric_count} claim(s) flagged as potentially from "
119
+ f"parametric knowledge — may need manual verification"
120
+ )
121
+ if report.uncertain_count > 0:
122
+ lines.append(
123
+ f"- {report.uncertain_count} claim(s) with uncertain attribution "
124
+ f"— manual review recommended"
125
+ )
126
+ lines.append("")
127
+
128
+ # Fidelity verification section
129
+ fid = report.fidelity
130
+ if fid is not None:
131
+ lines.append("### Fidelity Verification")
132
+ lines.append(f"**Fidelity score:** {fid.fidelity_score:.2f} / 1.00")
133
+ lines.append("")
134
+
135
+ if fid.distortions:
136
+ lines.append("#### Distortions Detected")
137
+ lines.append("| # | Claim (preview) | Type | Severity | Detail |")
138
+ lines.append("|---|-----------------|------|----------|--------|")
139
+ for i, d in enumerate(fid.distortions, 1):
140
+ preview = d.claim_text[:50].replace("|", "\\|")
141
+ lines.append(
142
+ f"| {i} | {preview} | {d.distortion_type.value} "
143
+ f"| {d.severity:.2f} | {d.detail[:80]} |"
144
+ )
145
+ lines.append("")
146
+
147
+ if fid.fabrications:
148
+ lines.append("#### Fabrications Detected")
149
+ lines.append("| # | Claim (preview) | Entity | Type | Severity |")
150
+ lines.append("|---|-----------------|--------|------|----------|")
151
+ for i, f in enumerate(fid.fabrications, 1):
152
+ preview = f.claim_text[:50].replace("|", "\\|")
153
+ lines.append(
154
+ f"| {i} | {preview} | `{f.fabricated_entity}` "
155
+ f"| {f.entity_type.value} | {f.severity:.2f} |"
156
+ )
157
+ lines.append("")
158
+
159
+ critical_omissions = [
160
+ o for o in fid.omissions
161
+ if o.severity in (OmissionSeverity.CRITICAL, OmissionSeverity.HIGH)
162
+ ]
163
+ if critical_omissions:
164
+ lines.append("#### Critical/High Omissions")
165
+ lines.append("| # | Fact (preview) | Relevance | Severity |")
166
+ lines.append("|---|----------------|-----------|----------|")
167
+ for i, o in enumerate(critical_omissions, 1):
168
+ preview = o.fact_text_preview[:60].replace("|", "\\|")
169
+ lines.append(
170
+ f"| {i} | {preview} | {o.fact_relevance_score:.2f} "
171
+ f"| {o.severity.value} |"
172
+ )
173
+ lines.append("")
174
+
175
+ if fid.contradictions:
176
+ lines.append("#### Contradictions Detected")
177
+ for i, c in enumerate(fid.contradictions, 1):
178
+ lines.append(
179
+ f"{i}. **{c.contradiction_type}** (severity {c.severity:.2f}): "
180
+ f"Claim #{c.claim_a_index} vs Claim #{c.claim_b_index} — {c.detail[:120]}"
181
+ )
182
+ lines.append("")
183
+
184
+ if not (fid.distortions or fid.fabrications or critical_omissions or fid.contradictions):
185
+ lines.append("No fidelity issues detected. ✅")
186
+ lines.append("")
187
+
188
+ # Semantic entailment section
189
+ if report.entailment_results:
190
+ lines.append("### Semantic Entailment Verification")
191
+ contradictions = [
192
+ er for er in report.entailment_results
193
+ if er.label == EntailmentLabel.CONTRADICTION
194
+ ]
195
+ if contradictions:
196
+ lines.append(f"**{len(contradictions)} semantic contradiction(s) detected** ⚠️")
197
+ lines.append("")
198
+ lines.append("| # | Claim (preview) | vs Fact | P(contra) | P(entail) | Method |")
199
+ lines.append("|---|-----------------|---------|-----------|-----------|--------|")
200
+ for i, er in enumerate(contradictions, 1):
201
+ preview = er.claim_text[:50].replace("|", "\\|")
202
+ method = "NLI model" if er.used_model else "Heuristic"
203
+ lines.append(
204
+ f"| {i} | {preview} | {er.fact_id[:12]} "
205
+ f"| {er.contradiction_score:.2f} | {er.entailment_score:.2f} "
206
+ f"| {method} |"
207
+ )
208
+ lines.append("")
209
+ else:
210
+ entailed = sum(
211
+ 1 for er in report.entailment_results
212
+ if er.label == EntailmentLabel.ENTAILED
213
+ )
214
+ lines.append(
215
+ f"All {len(report.entailment_results)} claim-fact pairs checked. "
216
+ f"{entailed} entailed, 0 contradictions. ✅"
217
+ )
218
+ lines.append("")
219
+
220
+ # Hallucination risk section
221
+ risk = report.risk_report
222
+ if risk is not None and risk.assessments:
223
+ lines.append("### Hallucination Risk Assessment")
224
+ risk_emoji = {
225
+ HallucinationRisk.LOW: "🟢",
226
+ HallucinationRisk.MEDIUM: "🟡",
227
+ HallucinationRisk.HIGH: "🟠",
228
+ HallucinationRisk.CRITICAL: "🔴",
229
+ }
230
+ lines.append(
231
+ f"**Window risk level:** {risk_emoji.get(risk.window_risk_level, '')} "
232
+ f"{risk.window_risk_level.value} "
233
+ f"(mean score: {risk.mean_risk_score:.2f})"
234
+ )
235
+ if risk.critical_risk_count > 0:
236
+ lines.append(f"- **{risk.critical_risk_count}** CRITICAL risk claim(s)")
237
+ if risk.high_risk_count > 0:
238
+ lines.append(f"- **{risk.high_risk_count}** HIGH risk claim(s)")
239
+ lines.append("")
240
+
241
+ # Show high/critical risk claims
242
+ high_risk = [
243
+ a for a in risk.assessments
244
+ if a.risk_level in (HallucinationRisk.HIGH, HallucinationRisk.CRITICAL)
245
+ ]
246
+ if high_risk:
247
+ lines.append("#### High/Critical Risk Claims")
248
+ lines.append("| # | Claim (preview) | Risk | Score | Factors |")
249
+ lines.append("|---|-----------------|------|-------|---------|")
250
+ for a in high_risk:
251
+ preview = a.claim_text[:50].replace("|", "\\|")
252
+ factors = "; ".join(a.risk_factors[:3])
253
+ lines.append(
254
+ f"| {a.claim_index} | {preview} "
255
+ f"| {a.risk_level.value} | {a.risk_score:.2f} "
256
+ f"| {factors[:100]} |"
257
+ )
258
+ lines.append("")
259
+
260
+ return "\n".join(lines)
261
+
262
+
263
+ # ---------------------------------------------------------------------------
264
+ # JSON report
265
+ # ---------------------------------------------------------------------------
266
+
267
+
268
+ def generate_json_report(report: ProvenanceReport) -> dict[str, Any]:
269
+ """Generate a machine-readable JSON provenance report.
270
+
271
+ Suitable for API responses, database storage, or integration with
272
+ compliance management systems.
273
+
274
+ Args:
275
+ report: Complete provenance report from the DPE pipeline.
276
+
277
+ Returns:
278
+ JSON-serializable dictionary.
279
+ """
280
+ return {
281
+ "report_type": "decision_provenance",
282
+ "version": "1.0.0",
283
+ "session_id": report.session_id,
284
+ "window_id": report.window_id,
285
+ "timestamp": report.timestamp,
286
+ "summary": {
287
+ "total_claims": report.total_claims,
288
+ "factual_claims": report.factual_claims,
289
+ "opinion_claims": report.opinion_claims,
290
+ "procedural_claims": report.procedural_claims,
291
+ "hedge_claims": report.hedge_claims,
292
+ "connective_claims": report.connective_claims,
293
+ "context_grounded": report.context_grounded_count,
294
+ "parametric": report.parametric_count,
295
+ "mixed": report.mixed_count,
296
+ "uncertain": report.uncertain_count,
297
+ "grounding_ratio": report.grounding_ratio,
298
+ },
299
+ "attributions": [
300
+ {
301
+ "claim_text": attr.claim_text[:200],
302
+ "claim_index": attr.claim_index,
303
+ "claim_type": attr.claim_type.value,
304
+ "attribution_type": attr.attribution_type.value,
305
+ "top_score": attr.top_score,
306
+ "confidence": attr.confidence,
307
+ "top_facts": [
308
+ {
309
+ "fact_id": fs.fact_id,
310
+ "fact_preview": fs.fact_text_preview,
311
+ "semantic_similarity": fs.semantic_similarity,
312
+ "lexical_overlap": fs.lexical_overlap,
313
+ "composite_score": fs.composite_score,
314
+ "source_window": fs.fact_source_window,
315
+ "extraction_stage": fs.fact_extraction_stage,
316
+ }
317
+ for fs in attr.attributed_facts[:3]
318
+ ],
319
+ }
320
+ for attr in report.attributions
321
+ ],
322
+ "chains": [
323
+ {
324
+ "claim_text": chain.claim_text[:200],
325
+ "claim_index": chain.claim_index,
326
+ "attribution_type": chain.attribution_type.value,
327
+ "links": [
328
+ {
329
+ "level": link.level,
330
+ "label": link.label,
331
+ "detail": link.detail,
332
+ }
333
+ for link in chain.links
334
+ ],
335
+ }
336
+ for chain in report.chains
337
+ ],
338
+ "integrity": {
339
+ "chain_verified": report.chain_verified,
340
+ "envelope_facts_count": report.envelope_facts_count,
341
+ "output_token_count": report.output_token_count,
342
+ },
343
+ "fidelity": _serialize_fidelity(report.fidelity),
344
+ "entailment": [
345
+ {
346
+ "claim_index": er.claim_index,
347
+ "claim_text": er.claim_text[:200],
348
+ "fact_id": er.fact_id,
349
+ "label": er.label.value,
350
+ "confidence": er.confidence,
351
+ "entailment_score": er.entailment_score,
352
+ "contradiction_score": er.contradiction_score,
353
+ "neutral_score": er.neutral_score,
354
+ "used_model": er.used_model,
355
+ }
356
+ for er in report.entailment_results
357
+ ],
358
+ "risk_assessment": _serialize_risk(report.risk_report),
359
+ }
360
+
361
+
362
+ def _serialize_risk(risk: object) -> dict[str, object] | None:
363
+ """Serialize HallucinationRiskReport for JSON output."""
364
+ if risk is None:
365
+ return None
366
+ return {
367
+ "window_risk_level": risk.window_risk_level.value, # type: ignore[union-attr]
368
+ "mean_risk_score": risk.mean_risk_score, # type: ignore[union-attr]
369
+ "high_risk_count": risk.high_risk_count, # type: ignore[union-attr]
370
+ "critical_risk_count": risk.critical_risk_count, # type: ignore[union-attr]
371
+ "assessments": [
372
+ {
373
+ "claim_index": a.claim_index,
374
+ "claim_text": a.claim_text[:200],
375
+ "risk_level": a.risk_level.value,
376
+ "risk_score": a.risk_score,
377
+ "attribution_signal": a.attribution_signal,
378
+ "fidelity_signal": a.fidelity_signal,
379
+ "entailment_signal": a.entailment_signal,
380
+ "specificity_signal": a.specificity_signal,
381
+ "risk_factors": a.risk_factors,
382
+ }
383
+ for a in risk.assessments # type: ignore[union-attr]
384
+ ],
385
+ }
386
+
387
+
388
+ def _serialize_fidelity(fid: object) -> dict[str, Any] | None:
389
+ """Serialize FidelityReport for JSON output."""
390
+ if fid is None:
391
+ return None
392
+ # fid is a FidelityReport but we avoid import cycle via duck typing
393
+ return {
394
+ "fidelity_score": fid.fidelity_score, # type: ignore[union-attr]
395
+ "distortion_count": fid.distortion_count, # type: ignore[union-attr]
396
+ "fabrication_count": fid.fabrication_count, # type: ignore[union-attr]
397
+ "critical_omission_count": fid.critical_omission_count, # type: ignore[union-attr]
398
+ "contradiction_count": fid.contradiction_count, # type: ignore[union-attr]
399
+ "distortions": [
400
+ {
401
+ "claim_index": d.claim_index,
402
+ "claim_text": d.claim_text[:200],
403
+ "distortion_type": d.distortion_type.value,
404
+ "severity": d.severity,
405
+ "detail": d.detail,
406
+ "claim_value": d.claim_value,
407
+ "fact_value": d.fact_value,
408
+ }
409
+ for d in fid.distortions # type: ignore[union-attr]
410
+ ],
411
+ "fabrications": [
412
+ {
413
+ "claim_index": f.claim_index,
414
+ "claim_text": f.claim_text[:200],
415
+ "fabricated_entity": f.fabricated_entity,
416
+ "entity_type": f.entity_type.value,
417
+ "severity": f.severity,
418
+ }
419
+ for f in fid.fabrications # type: ignore[union-attr]
420
+ ],
421
+ "omissions": [
422
+ {
423
+ "fact_id": o.fact_id,
424
+ "fact_text_preview": o.fact_text_preview[:120],
425
+ "relevance_score": o.fact_relevance_score,
426
+ "severity": o.severity.value,
427
+ }
428
+ for o in fid.omissions # type: ignore[union-attr]
429
+ ],
430
+ "contradictions": [
431
+ {
432
+ "claim_a_index": c.claim_a_index,
433
+ "claim_b_index": c.claim_b_index,
434
+ "contradiction_type": c.contradiction_type,
435
+ "severity": c.severity,
436
+ "detail": c.detail,
437
+ }
438
+ for c in fid.contradictions # type: ignore[union-attr]
439
+ ],
440
+ }
@@ -0,0 +1,43 @@
1
+ # Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
2
+ # Licensed under Elastic License 2.0 — see LICENSE.md for details.
3
+ """LLM provider adapters — OpenAI, Anthropic, Ollama, llama.cpp, custom.
4
+
5
+ All adapters implement :class:`LLMProvider` and can be passed directly
6
+ to ``crp.Client(provider=...)``.
7
+
8
+ Usage::
9
+
10
+ from crp.providers import OpenAIAdapter, OllamaAdapter
11
+
12
+ provider = OpenAIAdapter(model="gpt-4o")
13
+ # or
14
+ provider = OllamaAdapter(model="llama3.1")
15
+ """
16
+
17
+ from crp.providers.base import LLMProvider
18
+ from crp.providers.custom import CustomProvider
19
+ from crp.providers.llamacpp import LlamaCppAdapter
20
+ from crp.providers.ollama import OllamaAdapter
21
+ from crp.providers.openai import OpenAIAdapter
22
+
23
+ # Lazy import for optional dependency: Anthropic
24
+ def __getattr__(name: str):
25
+ if name == "AnthropicAdapter":
26
+ from crp.providers.anthropic import AnthropicAdapter
27
+ return AnthropicAdapter
28
+ raise AttributeError(f"module 'crp.providers' has no attribute {name!r}")
29
+
30
+ # Convenience alias matching docs
31
+ CallableAdapter = CustomProvider
32
+ BaseAdapter = LLMProvider
33
+
34
+ __all__ = [
35
+ "LLMProvider",
36
+ "BaseAdapter",
37
+ "OpenAIAdapter",
38
+ "AnthropicAdapter",
39
+ "OllamaAdapter",
40
+ "LlamaCppAdapter",
41
+ "CustomProvider",
42
+ "CallableAdapter",
43
+ ]