crca 1.4.0__py3-none-any.whl → 1.5.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 (306) hide show
  1. CRCA.py +172 -7
  2. MODEL_CARD.md +53 -0
  3. PKG-INFO +8 -2
  4. RELEASE_NOTES.md +17 -0
  5. STABILITY.md +19 -0
  6. architecture/hybrid/consistency_engine.py +362 -0
  7. architecture/hybrid/conversation_manager.py +421 -0
  8. architecture/hybrid/explanation_generator.py +452 -0
  9. architecture/hybrid/few_shot_learner.py +533 -0
  10. architecture/hybrid/graph_compressor.py +286 -0
  11. architecture/hybrid/hybrid_agent.py +4398 -0
  12. architecture/hybrid/language_compiler.py +623 -0
  13. architecture/hybrid/main,py +0 -0
  14. architecture/hybrid/reasoning_tracker.py +322 -0
  15. architecture/hybrid/self_verifier.py +524 -0
  16. architecture/hybrid/task_decomposer.py +567 -0
  17. architecture/hybrid/text_corrector.py +341 -0
  18. benchmark_results/crca_core_benchmarks.json +178 -0
  19. branches/crca_sd/crca_sd_realtime.py +6 -2
  20. branches/general_agent/__init__.py +102 -0
  21. branches/general_agent/general_agent.py +1400 -0
  22. branches/general_agent/personality.py +169 -0
  23. branches/general_agent/utils/__init__.py +19 -0
  24. branches/general_agent/utils/prompt_builder.py +170 -0
  25. {crca-1.4.0.dist-info → crca-1.5.0.dist-info}/METADATA +8 -2
  26. {crca-1.4.0.dist-info → crca-1.5.0.dist-info}/RECORD +303 -20
  27. crca_core/__init__.py +35 -0
  28. crca_core/benchmarks/__init__.py +14 -0
  29. crca_core/benchmarks/synthetic_scm.py +103 -0
  30. crca_core/core/__init__.py +23 -0
  31. crca_core/core/api.py +120 -0
  32. crca_core/core/estimate.py +208 -0
  33. crca_core/core/godclass.py +72 -0
  34. crca_core/core/intervention_design.py +174 -0
  35. crca_core/core/lifecycle.py +48 -0
  36. crca_core/discovery/__init__.py +9 -0
  37. crca_core/discovery/tabular.py +193 -0
  38. crca_core/identify/__init__.py +171 -0
  39. crca_core/identify/backdoor.py +39 -0
  40. crca_core/identify/frontdoor.py +48 -0
  41. crca_core/identify/graph.py +106 -0
  42. crca_core/identify/id_algorithm.py +43 -0
  43. crca_core/identify/iv.py +48 -0
  44. crca_core/models/__init__.py +67 -0
  45. crca_core/models/provenance.py +56 -0
  46. crca_core/models/refusal.py +39 -0
  47. crca_core/models/result.py +83 -0
  48. crca_core/models/spec.py +151 -0
  49. crca_core/models/validation.py +68 -0
  50. crca_core/scm/__init__.py +9 -0
  51. crca_core/scm/linear_gaussian.py +198 -0
  52. crca_core/timeseries/__init__.py +6 -0
  53. crca_core/timeseries/pcmci.py +181 -0
  54. crca_llm/__init__.py +12 -0
  55. crca_llm/client.py +85 -0
  56. crca_llm/coauthor.py +118 -0
  57. crca_llm/orchestrator.py +289 -0
  58. crca_llm/types.py +21 -0
  59. crca_reasoning/__init__.py +16 -0
  60. crca_reasoning/critique.py +54 -0
  61. crca_reasoning/godclass.py +206 -0
  62. crca_reasoning/memory.py +24 -0
  63. crca_reasoning/rationale.py +10 -0
  64. crca_reasoning/react_controller.py +81 -0
  65. crca_reasoning/tool_router.py +97 -0
  66. crca_reasoning/types.py +40 -0
  67. crca_sd/__init__.py +15 -0
  68. crca_sd/crca_sd_core.py +2 -0
  69. crca_sd/crca_sd_governance.py +2 -0
  70. crca_sd/crca_sd_mpc.py +2 -0
  71. crca_sd/crca_sd_realtime.py +2 -0
  72. crca_sd/crca_sd_tui.py +2 -0
  73. cuda-keyring_1.1-1_all.deb +0 -0
  74. cuda-keyring_1.1-1_all.deb.1 +0 -0
  75. docs/IMAGE_ANNOTATION_USAGE.md +539 -0
  76. docs/INSTALL_DEEPSPEED.md +125 -0
  77. docs/api/branches/crca-cg.md +19 -0
  78. docs/api/branches/crca-q.md +27 -0
  79. docs/api/branches/crca-sd.md +37 -0
  80. docs/api/branches/general-agent.md +24 -0
  81. docs/api/branches/overview.md +19 -0
  82. docs/api/crca/agent-methods.md +62 -0
  83. docs/api/crca/operations.md +79 -0
  84. docs/api/crca/overview.md +32 -0
  85. docs/api/image-annotation/engine.md +52 -0
  86. docs/api/image-annotation/overview.md +17 -0
  87. docs/api/schemas/annotation.md +34 -0
  88. docs/api/schemas/core-schemas.md +82 -0
  89. docs/api/schemas/overview.md +32 -0
  90. docs/api/schemas/policy.md +30 -0
  91. docs/api/utils/conversation.md +22 -0
  92. docs/api/utils/graph-reasoner.md +32 -0
  93. docs/api/utils/overview.md +21 -0
  94. docs/api/utils/router.md +19 -0
  95. docs/api/utils/utilities.md +97 -0
  96. docs/architecture/causal-graphs.md +41 -0
  97. docs/architecture/data-flow.md +29 -0
  98. docs/architecture/design-principles.md +33 -0
  99. docs/architecture/hybrid-agent/components.md +38 -0
  100. docs/architecture/hybrid-agent/consistency.md +26 -0
  101. docs/architecture/hybrid-agent/overview.md +44 -0
  102. docs/architecture/hybrid-agent/reasoning.md +22 -0
  103. docs/architecture/llm-integration.md +26 -0
  104. docs/architecture/modular-structure.md +37 -0
  105. docs/architecture/overview.md +69 -0
  106. docs/architecture/policy-engine-arch.md +29 -0
  107. docs/branches/crca-cg/corposwarm.md +39 -0
  108. docs/branches/crca-cg/esg-scoring.md +30 -0
  109. docs/branches/crca-cg/multi-agent.md +35 -0
  110. docs/branches/crca-cg/overview.md +40 -0
  111. docs/branches/crca-q/alternative-data.md +55 -0
  112. docs/branches/crca-q/architecture.md +71 -0
  113. docs/branches/crca-q/backtesting.md +45 -0
  114. docs/branches/crca-q/causal-engine.md +33 -0
  115. docs/branches/crca-q/execution.md +39 -0
  116. docs/branches/crca-q/market-data.md +60 -0
  117. docs/branches/crca-q/overview.md +58 -0
  118. docs/branches/crca-q/philosophy.md +60 -0
  119. docs/branches/crca-q/portfolio-optimization.md +66 -0
  120. docs/branches/crca-q/risk-management.md +102 -0
  121. docs/branches/crca-q/setup.md +65 -0
  122. docs/branches/crca-q/signal-generation.md +61 -0
  123. docs/branches/crca-q/signal-validation.md +43 -0
  124. docs/branches/crca-sd/core.md +84 -0
  125. docs/branches/crca-sd/governance.md +53 -0
  126. docs/branches/crca-sd/mpc-solver.md +65 -0
  127. docs/branches/crca-sd/overview.md +59 -0
  128. docs/branches/crca-sd/realtime.md +28 -0
  129. docs/branches/crca-sd/tui.md +20 -0
  130. docs/branches/general-agent/overview.md +37 -0
  131. docs/branches/general-agent/personality.md +36 -0
  132. docs/branches/general-agent/prompt-builder.md +30 -0
  133. docs/changelog/index.md +79 -0
  134. docs/contributing/code-style.md +69 -0
  135. docs/contributing/documentation.md +43 -0
  136. docs/contributing/overview.md +29 -0
  137. docs/contributing/testing.md +29 -0
  138. docs/core/crcagent/async-operations.md +65 -0
  139. docs/core/crcagent/automatic-extraction.md +107 -0
  140. docs/core/crcagent/batch-prediction.md +80 -0
  141. docs/core/crcagent/bayesian-inference.md +60 -0
  142. docs/core/crcagent/causal-graph.md +92 -0
  143. docs/core/crcagent/counterfactuals.md +96 -0
  144. docs/core/crcagent/deterministic-simulation.md +78 -0
  145. docs/core/crcagent/dual-mode-operation.md +82 -0
  146. docs/core/crcagent/initialization.md +88 -0
  147. docs/core/crcagent/optimization.md +65 -0
  148. docs/core/crcagent/overview.md +63 -0
  149. docs/core/crcagent/time-series.md +57 -0
  150. docs/core/schemas/annotation.md +30 -0
  151. docs/core/schemas/core-schemas.md +82 -0
  152. docs/core/schemas/overview.md +30 -0
  153. docs/core/schemas/policy.md +41 -0
  154. docs/core/templates/base-agent.md +31 -0
  155. docs/core/templates/feature-mixins.md +31 -0
  156. docs/core/templates/overview.md +29 -0
  157. docs/core/templates/templates-guide.md +75 -0
  158. docs/core/tools/mcp-client.md +34 -0
  159. docs/core/tools/overview.md +24 -0
  160. docs/core/utils/conversation.md +27 -0
  161. docs/core/utils/graph-reasoner.md +29 -0
  162. docs/core/utils/overview.md +27 -0
  163. docs/core/utils/router.md +27 -0
  164. docs/core/utils/utilities.md +97 -0
  165. docs/css/custom.css +84 -0
  166. docs/examples/basic-usage.md +57 -0
  167. docs/examples/general-agent/general-agent-examples.md +50 -0
  168. docs/examples/hybrid-agent/hybrid-agent-examples.md +56 -0
  169. docs/examples/image-annotation/image-annotation-examples.md +54 -0
  170. docs/examples/integration/integration-examples.md +58 -0
  171. docs/examples/overview.md +37 -0
  172. docs/examples/trading/trading-examples.md +46 -0
  173. docs/features/causal-reasoning/advanced-topics.md +101 -0
  174. docs/features/causal-reasoning/counterfactuals.md +43 -0
  175. docs/features/causal-reasoning/do-calculus.md +50 -0
  176. docs/features/causal-reasoning/overview.md +47 -0
  177. docs/features/causal-reasoning/structural-models.md +52 -0
  178. docs/features/hybrid-agent/advanced-components.md +55 -0
  179. docs/features/hybrid-agent/core-components.md +64 -0
  180. docs/features/hybrid-agent/overview.md +34 -0
  181. docs/features/image-annotation/engine.md +82 -0
  182. docs/features/image-annotation/features.md +113 -0
  183. docs/features/image-annotation/integration.md +75 -0
  184. docs/features/image-annotation/overview.md +53 -0
  185. docs/features/image-annotation/quickstart.md +73 -0
  186. docs/features/policy-engine/doctrine-ledger.md +105 -0
  187. docs/features/policy-engine/monitoring.md +44 -0
  188. docs/features/policy-engine/mpc-control.md +89 -0
  189. docs/features/policy-engine/overview.md +46 -0
  190. docs/getting-started/configuration.md +225 -0
  191. docs/getting-started/first-agent.md +164 -0
  192. docs/getting-started/installation.md +144 -0
  193. docs/getting-started/quickstart.md +137 -0
  194. docs/index.md +118 -0
  195. docs/js/mathjax.js +13 -0
  196. docs/lrm/discovery_proof_notes.md +25 -0
  197. docs/lrm/finetune_full.md +83 -0
  198. docs/lrm/math_appendix.md +120 -0
  199. docs/lrm/overview.md +32 -0
  200. docs/mkdocs.yml +238 -0
  201. docs/stylesheets/extra.css +21 -0
  202. docs_generated/crca_core/CounterfactualResult.md +12 -0
  203. docs_generated/crca_core/DiscoveryHypothesisResult.md +13 -0
  204. docs_generated/crca_core/DraftSpec.md +13 -0
  205. docs_generated/crca_core/EstimateResult.md +13 -0
  206. docs_generated/crca_core/IdentificationResult.md +17 -0
  207. docs_generated/crca_core/InterventionDesignResult.md +12 -0
  208. docs_generated/crca_core/LockedSpec.md +15 -0
  209. docs_generated/crca_core/RefusalResult.md +12 -0
  210. docs_generated/crca_core/ValidationReport.md +9 -0
  211. docs_generated/crca_core/index.md +13 -0
  212. examples/general_agent_example.py +277 -0
  213. examples/general_agent_quickstart.py +202 -0
  214. examples/general_agent_simple.py +92 -0
  215. examples/hybrid_agent_auto_extraction.py +84 -0
  216. examples/hybrid_agent_dictionary_demo.py +104 -0
  217. examples/hybrid_agent_enhanced.py +179 -0
  218. examples/hybrid_agent_general_knowledge.py +107 -0
  219. examples/image_annotation_quickstart.py +328 -0
  220. examples/test_hybrid_fixes.py +77 -0
  221. image_annotation/__init__.py +27 -0
  222. image_annotation/annotation_engine.py +2593 -0
  223. install_cuda_wsl2.sh +59 -0
  224. install_deepspeed.sh +56 -0
  225. install_deepspeed_simple.sh +87 -0
  226. mkdocs.yml +252 -0
  227. ollama/Modelfile +8 -0
  228. prompts/__init__.py +2 -1
  229. prompts/default_crca.py +9 -1
  230. prompts/general_agent.py +227 -0
  231. prompts/image_annotation.py +56 -0
  232. pyproject.toml +17 -2
  233. requirements-docs.txt +10 -0
  234. requirements.txt +21 -2
  235. schemas/__init__.py +26 -1
  236. schemas/annotation.py +222 -0
  237. schemas/conversation.py +193 -0
  238. schemas/hybrid.py +211 -0
  239. schemas/reasoning.py +276 -0
  240. schemas_export/crca_core/CounterfactualResult.schema.json +108 -0
  241. schemas_export/crca_core/DiscoveryHypothesisResult.schema.json +113 -0
  242. schemas_export/crca_core/DraftSpec.schema.json +635 -0
  243. schemas_export/crca_core/EstimateResult.schema.json +113 -0
  244. schemas_export/crca_core/IdentificationResult.schema.json +145 -0
  245. schemas_export/crca_core/InterventionDesignResult.schema.json +111 -0
  246. schemas_export/crca_core/LockedSpec.schema.json +646 -0
  247. schemas_export/crca_core/RefusalResult.schema.json +90 -0
  248. schemas_export/crca_core/ValidationReport.schema.json +62 -0
  249. scripts/build_lrm_dataset.py +80 -0
  250. scripts/export_crca_core_schemas.py +54 -0
  251. scripts/export_hf_lrm.py +37 -0
  252. scripts/export_ollama_gguf.py +45 -0
  253. scripts/generate_changelog.py +157 -0
  254. scripts/generate_crca_core_docs_from_schemas.py +86 -0
  255. scripts/run_crca_core_benchmarks.py +163 -0
  256. scripts/run_full_finetune.py +198 -0
  257. scripts/run_lrm_eval.py +31 -0
  258. templates/graph_management.py +29 -0
  259. tests/conftest.py +9 -0
  260. tests/test_core.py +2 -3
  261. tests/test_crca_core_discovery_tabular.py +15 -0
  262. tests/test_crca_core_estimate_dowhy.py +36 -0
  263. tests/test_crca_core_identify.py +18 -0
  264. tests/test_crca_core_intervention_design.py +36 -0
  265. tests/test_crca_core_linear_gaussian_scm.py +69 -0
  266. tests/test_crca_core_spec.py +25 -0
  267. tests/test_crca_core_timeseries_pcmci.py +15 -0
  268. tests/test_crca_llm_coauthor.py +12 -0
  269. tests/test_crca_llm_orchestrator.py +80 -0
  270. tests/test_hybrid_agent_llm_enhanced.py +556 -0
  271. tests/test_image_annotation_demo.py +376 -0
  272. tests/test_image_annotation_operational.py +408 -0
  273. tests/test_image_annotation_unit.py +551 -0
  274. tests/test_training_moe.py +13 -0
  275. training/__init__.py +42 -0
  276. training/datasets.py +140 -0
  277. training/deepspeed_zero2_0_5b.json +22 -0
  278. training/deepspeed_zero2_1_5b.json +22 -0
  279. training/deepspeed_zero3_0_5b.json +28 -0
  280. training/deepspeed_zero3_14b.json +28 -0
  281. training/deepspeed_zero3_h100_3gpu.json +20 -0
  282. training/deepspeed_zero3_offload.json +28 -0
  283. training/eval.py +92 -0
  284. training/finetune.py +516 -0
  285. training/public_datasets.py +89 -0
  286. training_data/react_train.jsonl +7473 -0
  287. utils/agent_discovery.py +311 -0
  288. utils/batch_processor.py +317 -0
  289. utils/conversation.py +78 -0
  290. utils/edit_distance.py +118 -0
  291. utils/formatter.py +33 -0
  292. utils/graph_reasoner.py +530 -0
  293. utils/rate_limiter.py +283 -0
  294. utils/router.py +2 -2
  295. utils/tool_discovery.py +307 -0
  296. webui/__init__.py +10 -0
  297. webui/app.py +229 -0
  298. webui/config.py +104 -0
  299. webui/static/css/style.css +332 -0
  300. webui/static/js/main.js +284 -0
  301. webui/templates/index.html +42 -0
  302. tests/test_crca_excel.py +0 -166
  303. tests/test_data_broker.py +0 -424
  304. tests/test_palantir.py +0 -349
  305. {crca-1.4.0.dist-info → crca-1.5.0.dist-info}/WHEEL +0 -0
  306. {crca-1.4.0.dist-info → crca-1.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,181 @@
1
+ """Time-series causal discovery via Tigramite PCMCI/PCMCI+ (wrap-first).
2
+
3
+ If Tigramite is not installed, this module returns a structured refusal.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any, Dict, List, Literal, Optional
9
+
10
+ from pydantic import BaseModel, Field
11
+
12
+ from crca_core.models.provenance import ProvenanceManifest
13
+ from crca_core.models.refusal import RefusalChecklistItem, RefusalReasonCode, RefusalResult
14
+ from crca_core.models.result import DiscoveryHypothesisResult
15
+ from utils.canonical import stable_hash
16
+
17
+
18
+ class PCMCIConfig(BaseModel):
19
+ max_lag: int = Field(default=5, ge=1)
20
+ alpha: float = Field(default=0.05, gt=0.0, lt=1.0)
21
+ variant: Literal["pcmci", "pcmci_plus"] = "pcmci"
22
+ time_index_column: Optional[str] = None
23
+ assume_sorted: bool = False
24
+ min_samples: int = Field(default=200, ge=20)
25
+ notes: Optional[str] = None
26
+
27
+
28
+ def _tigramite_available() -> bool:
29
+ try:
30
+ import importlib.util
31
+
32
+ return importlib.util.find_spec("tigramite") is not None
33
+ except Exception:
34
+ return False
35
+
36
+
37
+ def discover_timeseries_pcmci(
38
+ data: Any,
39
+ config: Optional[PCMCIConfig] = None,
40
+ assumptions: Optional[List[str]] = None,
41
+ ) -> DiscoveryHypothesisResult | RefusalResult:
42
+ cfg = config or PCMCIConfig()
43
+ assumptions = assumptions or []
44
+
45
+ # Schema-only signature for provenance (no raw data hashing by default).
46
+ schema_sig: Dict[str, Any] = {}
47
+ try:
48
+ import pandas as pd # type: ignore
49
+
50
+ if isinstance(data, pd.DataFrame):
51
+ schema_sig = {c: str(t) for c, t in data.dtypes.items()}
52
+ else:
53
+ schema_sig = {"type": str(type(data))}
54
+ except Exception:
55
+ schema_sig = {"type": str(type(data))}
56
+
57
+ spec_hash = stable_hash({"discovery": "pcmci", "config": cfg.model_dump(), "schema": schema_sig})
58
+ prov = ProvenanceManifest.minimal(
59
+ spec_hash=spec_hash,
60
+ data_hash=stable_hash(schema_sig),
61
+ algorithm_config=cfg.model_dump(),
62
+ )
63
+
64
+ if not _tigramite_available():
65
+ return RefusalResult(
66
+ message="Time-series causal discovery backend (tigramite) not available.",
67
+ reason_codes=[RefusalReasonCode.UNSUPPORTED_OPERATION],
68
+ checklist=[
69
+ RefusalChecklistItem(
70
+ item="Install tigramite",
71
+ rationale="PCMCI/PCMCI+ discovery is wrap-first; we refuse rather than run unvalidated heuristics.",
72
+ )
73
+ ],
74
+ suggested_next_steps=["pip install tigramite"],
75
+ )
76
+
77
+ try:
78
+ import pandas as pd # type: ignore
79
+ import numpy as np # type: ignore
80
+ from tigramite import data_processing as pp # type: ignore
81
+ from tigramite.pcmci import PCMCI # type: ignore
82
+ from tigramite.independence_tests import ParCorr # type: ignore
83
+ except Exception as e:
84
+ return RefusalResult(
85
+ message=f"Failed to import tigramite: {e}",
86
+ reason_codes=[RefusalReasonCode.UNSUPPORTED_OPERATION],
87
+ checklist=[
88
+ RefusalChecklistItem(
89
+ item="Install tigramite",
90
+ rationale="PCMCI/PCMCI+ discovery requires tigramite backend.",
91
+ )
92
+ ],
93
+ suggested_next_steps=["pip install tigramite"],
94
+ )
95
+
96
+ if not isinstance(data, pd.DataFrame):
97
+ return RefusalResult(
98
+ message="PCMCI requires pandas DataFrame input.",
99
+ reason_codes=[RefusalReasonCode.INPUT_INVALID],
100
+ checklist=[
101
+ RefusalChecklistItem(
102
+ item="Provide pandas DataFrame",
103
+ rationale="Tigramite expects tabular time-indexed data.",
104
+ )
105
+ ],
106
+ suggested_next_steps=["Convert your data to pandas.DataFrame and retry."],
107
+ )
108
+
109
+ df = data.copy()
110
+ if cfg.time_index_column:
111
+ if cfg.time_index_column not in df.columns:
112
+ return RefusalResult(
113
+ message="time_index_column not found in data.",
114
+ reason_codes=[RefusalReasonCode.TIME_INDEX_INVALID],
115
+ checklist=[
116
+ RefusalChecklistItem(
117
+ item="Provide valid time index column",
118
+ rationale="PCMCI requires a consistent time index.",
119
+ )
120
+ ],
121
+ suggested_next_steps=["Set PCMCIConfig.time_index_column to a valid column."],
122
+ )
123
+ df = df.sort_values(cfg.time_index_column)
124
+ elif not cfg.assume_sorted:
125
+ # If no column specified, require index monotonicity
126
+ try:
127
+ if not df.index.is_monotonic_increasing:
128
+ return RefusalResult(
129
+ message="DataFrame index is not monotonic; provide a time index column or sort data.",
130
+ reason_codes=[RefusalReasonCode.TIME_INDEX_INVALID],
131
+ checklist=[
132
+ RefusalChecklistItem(
133
+ item="Provide time index or sorted data",
134
+ rationale="PCMCI requires time-ordered samples.",
135
+ )
136
+ ],
137
+ suggested_next_steps=["Sort by time or set PCMCIConfig.time_index_column."],
138
+ )
139
+ except Exception:
140
+ pass
141
+
142
+ values = df.to_numpy(dtype=float)
143
+ if values.shape[0] < cfg.min_samples:
144
+ return RefusalResult(
145
+ message="Insufficient samples for PCMCI discovery.",
146
+ reason_codes=[RefusalReasonCode.INPUT_INVALID],
147
+ checklist=[
148
+ RefusalChecklistItem(
149
+ item="Increase sample size",
150
+ rationale=f"Need at least {cfg.min_samples} rows for stable lagged discovery.",
151
+ )
152
+ ],
153
+ suggested_next_steps=["Collect more samples or lower min_samples (not recommended)."],
154
+ )
155
+ dataframe = pp.DataFrame(values)
156
+ pcmci = PCMCI(dataframe=dataframe, cond_ind_test=ParCorr())
157
+
158
+ if cfg.variant == "pcmci_plus":
159
+ results = pcmci.run_pcmciplus(tau_max=cfg.max_lag, pc_alpha=cfg.alpha)
160
+ else:
161
+ results = pcmci.run_pcmci(tau_max=cfg.max_lag, pc_alpha=cfg.alpha)
162
+
163
+ graph_hypothesis = {
164
+ "graph_type": "pcmci",
165
+ "method": cfg.variant,
166
+ "max_lag": cfg.max_lag,
167
+ "graph": results.get("graph"),
168
+ "val_matrix": results.get("val_matrix"),
169
+ }
170
+
171
+ return DiscoveryHypothesisResult(
172
+ provenance=prov,
173
+ assumptions=assumptions,
174
+ limitations=[
175
+ "Time-series discovery is hypothesis generation under explicit assumptions (stationarity, lag sufficiency, no hidden confounding, etc.).",
176
+ "Returned graph depends on CI test assumptions and lag selection.",
177
+ ],
178
+ graph_hypothesis=graph_hypothesis,
179
+ stability_report={"alpha": cfg.alpha},
180
+ )
181
+
crca_llm/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ """crca_llm: non-authoritative LLM coauthor layer.
2
+
3
+ This package is intentionally optional and must never emit numeric causal
4
+ outputs on its own. It drafts specs and checklists only.
5
+ """
6
+
7
+ from crca_llm.coauthor import CoauthorConfig, DraftBundle, LLMCoauthor
8
+ from crca_llm.orchestrator import LLMOrchestrator
9
+ from crca_llm.types import LLMRunResult
10
+
11
+ __all__ = ["CoauthorConfig", "DraftBundle", "LLMCoauthor", "LLMOrchestrator", "LLMRunResult"]
12
+
crca_llm/client.py ADDED
@@ -0,0 +1,85 @@
1
+ """OpenAI-compatible client for crca_llm (chat completions only)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass
7
+ import time
8
+ import random
9
+ from loguru import logger
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ import requests
13
+
14
+
15
+ class MissingApiKeyError(RuntimeError):
16
+ pass
17
+
18
+
19
+ @dataclass
20
+ class OpenAIClient:
21
+ base_url: str = "https://api.openai.com"
22
+ api_key: Optional[str] = None
23
+ default_model: str = "gpt-4o-mini"
24
+ max_retries: int = 3
25
+ timeout_seconds: int = 60
26
+ enable_audit_log: bool = True
27
+
28
+ @classmethod
29
+ def from_env(cls) -> "OpenAIClient":
30
+ base_url = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com")
31
+ api_key = os.environ.get("OPENAI_API_KEY")
32
+ model = os.environ.get("OPENAI_MODEL", "gpt-4o-mini")
33
+ return cls(base_url=base_url, api_key=api_key, default_model=model)
34
+
35
+ def _require_key(self) -> None:
36
+ if not self.api_key:
37
+ raise MissingApiKeyError("OPENAI_API_KEY is required for LLM calls.")
38
+
39
+ def chat_completion(
40
+ self,
41
+ *,
42
+ messages: List[Dict[str, Any]],
43
+ model: Optional[str] = None,
44
+ temperature: float = 0.0,
45
+ max_tokens: int = 800,
46
+ response_format: Optional[Dict[str, Any]] = None,
47
+ ) -> str:
48
+ """Send a chat completion request and return the text response."""
49
+ self._require_key()
50
+ url = self.base_url.rstrip("/") + "/v1/chat/completions"
51
+ payload: Dict[str, Any] = {
52
+ "model": model or self.default_model,
53
+ "messages": messages,
54
+ "temperature": temperature,
55
+ "max_tokens": max_tokens,
56
+ }
57
+ if response_format is not None:
58
+ payload["response_format"] = response_format
59
+
60
+ headers = {
61
+ "Authorization": f"Bearer {self.api_key}",
62
+ "Content-Type": "application/json",
63
+ }
64
+ attempt = 0
65
+ while True:
66
+ try:
67
+ if self.enable_audit_log:
68
+ logger.info(
69
+ "LLM request model={} tokens={} messages={}",
70
+ payload["model"],
71
+ max_tokens,
72
+ len(messages),
73
+ )
74
+ resp = requests.post(url, json=payload, headers=headers, timeout=self.timeout_seconds)
75
+ resp.raise_for_status()
76
+ data = resp.json()
77
+ return data["choices"][0]["message"]["content"]
78
+ except requests.RequestException as exc:
79
+ attempt += 1
80
+ if attempt > self.max_retries:
81
+ raise
82
+ # exponential backoff with jitter
83
+ sleep_s = (2 ** (attempt - 1)) + random.uniform(0, 0.5)
84
+ time.sleep(sleep_s)
85
+
crca_llm/coauthor.py ADDED
@@ -0,0 +1,118 @@
1
+ """Non-authoritative LLM coauthor layer.
2
+
3
+ This module is deliberately constrained:
4
+ - It may draft candidate `DraftSpec` objects and review checklists.
5
+ - It must never produce numeric causal outputs.
6
+ - It must never create `LockedSpec` objects.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ from dataclasses import dataclass, field
13
+ from typing import Any, Dict, List, Optional, Sequence
14
+
15
+ from pydantic import BaseModel, Field
16
+
17
+ from crca_core.models.spec import (
18
+ AssumptionItem,
19
+ AssumptionSpec,
20
+ AssumptionStatus,
21
+ CausalGraphSpec,
22
+ DataColumnSpec,
23
+ DataSpec,
24
+ DraftSpec,
25
+ NodeSpec,
26
+ RoleSpec,
27
+ )
28
+
29
+
30
+ class CoauthorConfig(BaseModel):
31
+ model: str = Field(default="gpt-4o-mini")
32
+ n_candidates: int = Field(default=3, ge=1, le=5)
33
+
34
+
35
+ class DraftBundle(BaseModel):
36
+ """LLM coauthor output: draft specs + checklists (no numbers)."""
37
+
38
+ drafts: List[DraftSpec]
39
+ review_checklist: List[str] = Field(default_factory=list)
40
+ disclaimers: List[str] = Field(
41
+ default_factory=lambda: [
42
+ "DRAFT-NON-AUTHORITATIVE: This spec is a hypothesis and must be reviewed/locked by a human.",
43
+ "No numeric causal claims are produced by the coauthor layer.",
44
+ ]
45
+ )
46
+
47
+
48
+ def _default_checklist() -> List[str]:
49
+ return [
50
+ "Define treatment and outcome variables explicitly.",
51
+ "Confirm time ordering (what can cause what).",
52
+ "List plausible confounders and whether they are measured.",
53
+ "Check for collider variables that must not be adjusted for.",
54
+ "Define intervention semantics (set vs shift vs mechanism-change) if counterfactuals are needed.",
55
+ "State discovery assumptions if running causal discovery (faithfulness, causal sufficiency/latent confounding).",
56
+ ]
57
+
58
+
59
+ def _offline_draft(user_text: str, observed_columns: Optional[Sequence[str]], n: int) -> DraftBundle:
60
+ cols = list(observed_columns or [])
61
+ data_spec = DataSpec(
62
+ columns=[DataColumnSpec(name=c, dtype="unknown") for c in cols],
63
+ measurement_error_notes="unknown",
64
+ )
65
+
66
+ # Minimal placeholder graph: nodes from observed columns (no edges).
67
+ graph = CausalGraphSpec(nodes=[NodeSpec(name=c) for c in cols], edges=[])
68
+
69
+ assumptions = AssumptionSpec(
70
+ items=[
71
+ AssumptionItem(
72
+ name="DRAFT_ONLY",
73
+ status=AssumptionStatus.unknown,
74
+ description="This spec was generated offline without LLM; treat as a template only.",
75
+ )
76
+ ],
77
+ falsification_plan=[
78
+ "Collect domain constraints/time ordering and re-run drafting with an LLM (optional).",
79
+ ],
80
+ )
81
+
82
+ drafts = [
83
+ DraftSpec(
84
+ data=data_spec,
85
+ graph=graph,
86
+ roles=RoleSpec(),
87
+ assumptions=assumptions,
88
+ draft_notes=f"Offline draft template generated from observed columns. User text: {user_text[:200]}",
89
+ )
90
+ for _ in range(n)
91
+ ]
92
+ return DraftBundle(drafts=drafts, review_checklist=_default_checklist())
93
+
94
+
95
+ @dataclass
96
+ class LLMCoauthor:
97
+ """Coauthor that can draft candidate specs, but is never authoritative."""
98
+
99
+ config: CoauthorConfig = field(default_factory=CoauthorConfig)
100
+
101
+ def draft_specs(
102
+ self,
103
+ *,
104
+ user_text: str,
105
+ observed_columns: Optional[Sequence[str]] = None,
106
+ ) -> DraftBundle:
107
+ """Draft multiple candidate DraftSpec objects.
108
+
109
+ If no OpenAI API key is configured, returns an offline template bundle.
110
+ """
111
+
112
+ if os.environ.get("OPENAI_API_KEY") is None:
113
+ return _offline_draft(user_text, observed_columns, self.config.n_candidates)
114
+
115
+ # Online LLM drafting is intentionally not implemented yet in this repo’s default test environment.
116
+ # We return offline drafts to preserve deterministic behavior and avoid accidental numeric leakage.
117
+ return _offline_draft(user_text, observed_columns, self.config.n_candidates)
118
+
@@ -0,0 +1,289 @@
1
+ """LLM tool orchestration (DraftSpec-only + gated core calls)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from dataclasses import dataclass, field
8
+ from typing import Any, Dict, List, Optional, Sequence
9
+
10
+ from crca_core.core.api import (
11
+ EstimatorConfig,
12
+ FeasibilityConstraints,
13
+ PCMCIConfig,
14
+ TabularDiscoveryConfig,
15
+ TargetQuery,
16
+ discover_tabular,
17
+ discover_timeseries_pcmci,
18
+ design_intervention,
19
+ identify_effect,
20
+ estimate_effect_dowhy,
21
+ simulate_counterfactual,
22
+ )
23
+ from crca_core.models.refusal import RefusalChecklistItem, RefusalReasonCode, RefusalResult
24
+ from crca_core.models.result import AnyResult, IdentificationResult
25
+ from crca_core.models.spec import (
26
+ AssumptionItem,
27
+ AssumptionSpec,
28
+ AssumptionStatus,
29
+ CausalGraphSpec,
30
+ DataColumnSpec,
31
+ DataSpec,
32
+ DraftSpec,
33
+ EdgeSpec,
34
+ LockedSpec,
35
+ NodeSpec,
36
+ RoleSpec,
37
+ )
38
+ from crca_llm.client import MissingApiKeyError, OpenAIClient
39
+ from crca_llm.coauthor import DraftBundle
40
+ from crca_llm.types import LLMRunResult
41
+
42
+
43
+ def _fallback_bundle(user_text: str, observed_columns: Optional[Sequence[str]]) -> DraftBundle:
44
+ cols = list(observed_columns or [])
45
+ data_spec = DataSpec(
46
+ columns=[DataColumnSpec(name=c, dtype="unknown") for c in cols],
47
+ measurement_error_notes="unknown",
48
+ )
49
+ graph = CausalGraphSpec(nodes=[NodeSpec(name=c) for c in cols], edges=[])
50
+ assumptions = AssumptionSpec(
51
+ items=[
52
+ AssumptionItem(
53
+ name="DRAFT_ONLY",
54
+ status=AssumptionStatus.unknown,
55
+ description="Fallback draft used due to LLM parse failure.",
56
+ )
57
+ ],
58
+ falsification_plan=["Collect domain constraints/time ordering and re-run drafting."],
59
+ )
60
+ drafts = [
61
+ DraftSpec(
62
+ data=data_spec,
63
+ graph=graph,
64
+ roles=RoleSpec(),
65
+ assumptions=assumptions,
66
+ draft_notes=f"Fallback draft from observed columns. User text: {user_text[:200]}",
67
+ )
68
+ ]
69
+ return DraftBundle(drafts=drafts, review_checklist=[])
70
+
71
+
72
+ def _parse_llm_drafts(payload: dict) -> DraftBundle:
73
+ drafts_payload = payload.get("drafts", [])
74
+ drafts: List[DraftSpec] = []
75
+ for item in drafts_payload:
76
+ nodes = [NodeSpec(name=n) for n in item.get("nodes", [])]
77
+ edges = [EdgeSpec(source=a, target=b) for a, b in item.get("edges", [])]
78
+ graph = CausalGraphSpec(nodes=nodes, edges=edges)
79
+ roles = RoleSpec(
80
+ treatments=item.get("treatments", []),
81
+ outcomes=item.get("outcomes", []),
82
+ mediators=item.get("mediators", []),
83
+ instruments=item.get("instruments", []),
84
+ adjustment_candidates=item.get("adjustment_candidates", []),
85
+ prohibited_controls=item.get("prohibited_controls", []),
86
+ )
87
+ data_spec = DataSpec(
88
+ columns=[DataColumnSpec(name=c, dtype="unknown") for c in item.get("columns", [])],
89
+ measurement_error_notes="unknown",
90
+ )
91
+ assumptions = AssumptionSpec(items=[], falsification_plan=[])
92
+ drafts.append(DraftSpec(data=data_spec, graph=graph, roles=roles, assumptions=assumptions))
93
+ checklist = payload.get("review_checklist", [])
94
+ return DraftBundle(drafts=drafts, review_checklist=checklist)
95
+
96
+
97
+ @dataclass
98
+ class LLMOrchestrator:
99
+ client: Optional[OpenAIClient] = None
100
+ default_model: str = "gpt-4o-mini"
101
+ enable_audit_log: bool = True
102
+
103
+ def __post_init__(self) -> None:
104
+ env_model = os.getenv("CRCA_MOE_MODEL") or os.getenv("CRCA_LLM_MODEL")
105
+ if env_model:
106
+ self.default_model = env_model
107
+
108
+ def _draft_with_llm(self, user_text: str, observed_columns: Optional[Sequence[str]]) -> DraftBundle:
109
+ client = self.client or OpenAIClient.from_env()
110
+ client.enable_audit_log = self.enable_audit_log
111
+ prompt = {
112
+ "role": "user",
113
+ "content": (
114
+ "You are drafting causal specs. Return JSON with keys: "
115
+ "`drafts` (list), `review_checklist` (list). Each draft has: "
116
+ "`nodes` (list of strings), `edges` (list of [source,target]), "
117
+ "`treatments`, `outcomes`, `mediators`, `instruments`, "
118
+ "`adjustment_candidates`, `prohibited_controls`, `columns`.\n"
119
+ f"User text: {user_text}\n"
120
+ f"Observed columns: {list(observed_columns or [])}\n"
121
+ "Return JSON only."
122
+ ),
123
+ }
124
+ try:
125
+ content = client.chat_completion(
126
+ messages=[prompt],
127
+ model=self.default_model,
128
+ response_format={"type": "json_object"},
129
+ )
130
+ except MissingApiKeyError as exc:
131
+ raise
132
+
133
+ try:
134
+ payload = json.loads(content)
135
+ return _parse_llm_drafts(payload)
136
+ except Exception:
137
+ return _fallback_bundle(user_text, observed_columns)
138
+
139
+ def run(
140
+ self,
141
+ *,
142
+ user_text: str,
143
+ observed_columns: Optional[Sequence[str]] = None,
144
+ locked_spec: Optional[LockedSpec] = None,
145
+ data: Optional[Any] = None,
146
+ actions: Optional[Sequence[str]] = None,
147
+ target_query: Optional[TargetQuery] = None,
148
+ constraints: Optional[FeasibilityConstraints] = None,
149
+ factual_observation: Optional[Dict[str, float]] = None,
150
+ intervention: Optional[Dict[str, float]] = None,
151
+ ) -> LLMRunResult:
152
+ actions = list(actions or [])
153
+ refusals: List[RefusalResult] = []
154
+ core_results: List[AnyResult] = []
155
+
156
+ # Drafting is mandatory and requires API key.
157
+ try:
158
+ draft_bundle = self._draft_with_llm(user_text, observed_columns)
159
+ except MissingApiKeyError as exc:
160
+ refusal = RefusalResult(
161
+ message=str(exc),
162
+ reason_codes=[RefusalReasonCode.INPUT_INVALID],
163
+ checklist=[
164
+ RefusalChecklistItem(
165
+ item="Set OPENAI_API_KEY",
166
+ rationale="LLM orchestration requires an API key.",
167
+ )
168
+ ],
169
+ suggested_next_steps=["Export OPENAI_API_KEY and retry."],
170
+ )
171
+ return LLMRunResult(draft_bundle=DraftBundle(drafts=[], review_checklist=[]), refusals=[refusal])
172
+
173
+ if actions and locked_spec is None:
174
+ refusal = RefusalResult(
175
+ message="LockedSpec is required to run core tools.",
176
+ reason_codes=[RefusalReasonCode.SPEC_NOT_LOCKED],
177
+ checklist=[RefusalChecklistItem(item="Provide LockedSpec", rationale="Core tools are gated.")],
178
+ suggested_next_steps=["Lock a spec first, then re-run with locked_spec."],
179
+ )
180
+ refusals.append(refusal)
181
+ return LLMRunResult(draft_bundle=draft_bundle, refusals=refusals)
182
+
183
+ identification_result: IdentificationResult | None = None
184
+
185
+ for action in actions:
186
+ if action == "discover_tabular":
187
+ if data is None:
188
+ refusals.append(
189
+ RefusalResult(
190
+ message="Tabular discovery requires data.",
191
+ reason_codes=[RefusalReasonCode.INPUT_INVALID],
192
+ checklist=[RefusalChecklistItem(item="Provide data", rationale="Missing DataFrame.")],
193
+ )
194
+ )
195
+ continue
196
+ result = discover_tabular(data, TabularDiscoveryConfig())
197
+ core_results.append(result)
198
+ if isinstance(result, RefusalResult):
199
+ refusals.append(result)
200
+ elif action == "discover_timeseries":
201
+ if data is None:
202
+ refusals.append(
203
+ RefusalResult(
204
+ message="Time-series discovery requires data.",
205
+ reason_codes=[RefusalReasonCode.INPUT_INVALID],
206
+ checklist=[RefusalChecklistItem(item="Provide data", rationale="Missing DataFrame.")],
207
+ )
208
+ )
209
+ continue
210
+ result = discover_timeseries_pcmci(data, PCMCIConfig())
211
+ core_results.append(result)
212
+ if isinstance(result, RefusalResult):
213
+ refusals.append(result)
214
+ elif action == "design_intervention":
215
+ if target_query is None:
216
+ refusals.append(
217
+ RefusalResult(
218
+ message="Intervention design requires TargetQuery.",
219
+ reason_codes=[RefusalReasonCode.INPUT_INVALID],
220
+ checklist=[RefusalChecklistItem(item="Provide TargetQuery", rationale="Missing target query.")],
221
+ )
222
+ )
223
+ continue
224
+ result = design_intervention(
225
+ locked_spec=locked_spec,
226
+ target_query=target_query,
227
+ constraints=constraints or FeasibilityConstraints(),
228
+ )
229
+ core_results.append(result)
230
+ elif action == "identify":
231
+ result = identify_effect(
232
+ locked_spec=locked_spec,
233
+ treatment=locked_spec.roles.treatments[0] if locked_spec.roles.treatments else "",
234
+ outcome=locked_spec.roles.outcomes[0] if locked_spec.roles.outcomes else "",
235
+ )
236
+ core_results.append(result)
237
+ if isinstance(result, RefusalResult):
238
+ refusals.append(result)
239
+ else:
240
+ identification_result = result
241
+ elif action == "estimate":
242
+ if data is None:
243
+ refusals.append(
244
+ RefusalResult(
245
+ message="Estimation requires data.",
246
+ reason_codes=[RefusalReasonCode.INPUT_INVALID],
247
+ checklist=[RefusalChecklistItem(item="Provide data", rationale="Missing DataFrame.")],
248
+ )
249
+ )
250
+ continue
251
+ result = estimate_effect_dowhy(
252
+ data=data,
253
+ locked_spec=locked_spec,
254
+ treatment=locked_spec.roles.treatments[0] if locked_spec.roles.treatments else "",
255
+ outcome=locked_spec.roles.outcomes[0] if locked_spec.roles.outcomes else "",
256
+ identification_result=identification_result,
257
+ config=EstimatorConfig(),
258
+ )
259
+ core_results.append(result)
260
+ if isinstance(result, RefusalResult):
261
+ refusals.append(result)
262
+ elif action == "counterfactual":
263
+ if factual_observation is None or intervention is None:
264
+ refusals.append(
265
+ RefusalResult(
266
+ message="Counterfactual requires factual_observation and intervention.",
267
+ reason_codes=[RefusalReasonCode.INPUT_INVALID],
268
+ checklist=[
269
+ RefusalChecklistItem(item="Provide factual_observation", rationale="Missing factual data."),
270
+ RefusalChecklistItem(item="Provide intervention", rationale="Missing intervention."),
271
+ ],
272
+ )
273
+ )
274
+ continue
275
+ result = simulate_counterfactual(
276
+ locked_spec=locked_spec,
277
+ factual_observation=factual_observation,
278
+ intervention=intervention,
279
+ )
280
+ core_results.append(result)
281
+ if isinstance(result, RefusalResult):
282
+ refusals.append(result)
283
+
284
+ return LLMRunResult(
285
+ draft_bundle=draft_bundle,
286
+ core_results=core_results,
287
+ refusals=refusals,
288
+ )
289
+