ct-toolkit 0.1.7__tar.gz → 0.2.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.
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/PKG-INFO +43 -6
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/README.md +34 -5
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/ct_toolkit/core/kernel.py +75 -1
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/ct_toolkit/core/wrapper.py +38 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/ct_toolkit/divergence/engine.py +7 -2
- ct_toolkit-0.2.0/ct_toolkit/middleware/__init__.py +5 -0
- ct_toolkit-0.2.0/ct_toolkit/middleware/autogen.py +215 -0
- ct_toolkit-0.2.0/ct_toolkit/middleware/crewai.py +162 -0
- ct_toolkit-0.2.0/ct_toolkit/middleware/langchain.py +255 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/ct_toolkit.egg-info/PKG-INFO +43 -6
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/ct_toolkit.egg-info/SOURCES.txt +4 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/ct_toolkit.egg-info/requires.txt +12 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/pyproject.toml +12 -2
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/LICENSE +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/ct_toolkit/__init__.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/ct_toolkit/core/__init__.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/ct_toolkit/core/compatibility.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/ct_toolkit/core/exceptions.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/ct_toolkit/divergence/__init__.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/ct_toolkit/divergence/l2_judge.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/ct_toolkit/divergence/l3_icm.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/ct_toolkit/divergence/scheduler.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/ct_toolkit/endorsement/__init__.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/ct_toolkit/endorsement/probes/__init__.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/ct_toolkit/endorsement/reflective.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/ct_toolkit/identity/__init__.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/ct_toolkit/identity/embedding.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/ct_toolkit/provenance/__init__.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/ct_toolkit/provenance/log.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/ct_toolkit/provenance/vault/__init__.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/ct_toolkit/utils/__init__.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/ct_toolkit/utils/logger.py +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/ct_toolkit.egg-info/dependency_links.txt +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/ct_toolkit.egg-info/top_level.txt +0 -0
- {ct_toolkit-0.1.7 → ct_toolkit-0.2.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ct-toolkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Computational Theseus Toolkit — Identity Continuity Guardrails for Agentic Systems
|
|
5
5
|
Author: Hakan Damar
|
|
6
6
|
License: Apache-2.0
|
|
@@ -20,6 +20,14 @@ Requires-Dist: rich>=13.0.0
|
|
|
20
20
|
Requires-Dist: jinja2>=3.1.0
|
|
21
21
|
Provides-Extra: ollama
|
|
22
22
|
Requires-Dist: ollama>=0.2.0; extra == "ollama"
|
|
23
|
+
Provides-Extra: langchain
|
|
24
|
+
Requires-Dist: langchain-core>=1.2.0; extra == "langchain"
|
|
25
|
+
Provides-Extra: crewai
|
|
26
|
+
Requires-Dist: crewai>=1.10.0; extra == "crewai"
|
|
27
|
+
Provides-Extra: autogen
|
|
28
|
+
Requires-Dist: pyautogen>=0.4.0; extra == "autogen"
|
|
29
|
+
Provides-Extra: docs
|
|
30
|
+
Requires-Dist: mkdocs-material>=9.5.0; extra == "docs"
|
|
23
31
|
Provides-Extra: dev
|
|
24
32
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
25
33
|
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
@@ -68,7 +76,6 @@ pip install ct-toolkit
|
|
|
68
76
|
from ct_toolkit import TheseusWrapper
|
|
69
77
|
|
|
70
78
|
# Single line change — the rest is automatic
|
|
71
|
-
# Initialize by just passing the provider name
|
|
72
79
|
client = TheseusWrapper(provider="openai")
|
|
73
80
|
|
|
74
81
|
# Standard chat interface
|
|
@@ -79,6 +86,38 @@ print(f"Divergence score : {response.divergence_score:.4f}")
|
|
|
79
86
|
print(f"Tier : {response.divergence_tier}")
|
|
80
87
|
```
|
|
81
88
|
|
|
89
|
+
### Framework Middleware
|
|
90
|
+
|
|
91
|
+
CT Toolkit provides first-class integrations for the most popular agentic frameworks.
|
|
92
|
+
|
|
93
|
+
#### LangChain
|
|
94
|
+
```python
|
|
95
|
+
from ct_toolkit.middleware.langchain import TheseusChatModel
|
|
96
|
+
|
|
97
|
+
llm = TheseusChatModel(provider="openai", model="gpt-4o")
|
|
98
|
+
response = llm.invoke("What is identity continuity?")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
#### CrewAI
|
|
102
|
+
```python
|
|
103
|
+
from ct_toolkit.middleware.crewai import TheseusCrewMiddleware
|
|
104
|
+
from crewai import Crew
|
|
105
|
+
|
|
106
|
+
crew = Crew(agents=[...], tasks=[...])
|
|
107
|
+
# Automatically wraps all agent LLMs with parent-kernel guardrails
|
|
108
|
+
TheseusCrewMiddleware.apply_to_crew(crew, manager_wrapper)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### AutoGen
|
|
112
|
+
```python
|
|
113
|
+
from ct_toolkit.middleware.autogen import TheseusAutoGenMiddleware
|
|
114
|
+
from autogen import ConversableAgent
|
|
115
|
+
|
|
116
|
+
agent = ConversableAgent("assistant", llm_config={...})
|
|
117
|
+
# Injects incoming validation and outgoing divergence analysis
|
|
118
|
+
TheseusAutoGenMiddleware.apply_to_agent(agent, wrapper)
|
|
119
|
+
```
|
|
120
|
+
|
|
82
121
|
---
|
|
83
122
|
|
|
84
123
|
## Integration Models
|
|
@@ -263,9 +302,7 @@ ct_toolkit/
|
|
|
263
302
|
│ ├── engine.py # L1→L2→L3 orchestration
|
|
264
303
|
│ ├── l2_judge.py # LLM-as-judge
|
|
265
304
|
│ └── l3_icm.py # ICM Probe Battery
|
|
266
|
-
├──
|
|
267
|
-
│ ├── reflective.py # Reflective Endorsement protocol
|
|
268
|
-
│ └── probes/ # Ethical scenario test batteries
|
|
305
|
+
├── middleware/ # Framework Integrations (LangChain, CrewAI, AutoGen)
|
|
269
306
|
├── identity/
|
|
270
307
|
│ ├── embedding.py # ECS — cosine similarity
|
|
271
308
|
│ └── templates/ # Domain identity templates
|
|
@@ -284,10 +321,10 @@ CT Toolkit is an active engineering effort implementing the paper's framework ac
|
|
|
284
321
|
|
|
285
322
|
- **Phase 0 — MVP Core Infrastructure:** Constitutional kernel, reflective endorsement, provenance log, full template/kernel compatibility matrix, OpenAI/Anthropic/Ollama provider support.
|
|
286
323
|
- **Phase 1 — Identity Continuity Mechanisms:** L1/L2/L3 divergence engine, real embedding API integration, Stability-Plasticity Scheduling via `ElasticityScheduler` + `RiskProfile`.
|
|
324
|
+
- **Phase 2 — Multi-Agent Hierarchy Support:** Hierarchical kernel propagation, cascade-blocking, LangChain/CrewAI/AutoGen integration.
|
|
287
325
|
|
|
288
326
|
### Future Roadmap
|
|
289
327
|
|
|
290
|
-
- **Phase 2:** Multi-Agent Hierarchy Support (hierarchical kernel propagation, LangChain/CrewAI/AutoGen integration).
|
|
291
328
|
- **Phase 3:** ICM and Measurement Infrastructure (reasoning chain analysis, policy-drift measurement, cross-checkpoint comparison).
|
|
292
329
|
- **Phase 4:** Open-Source Model Support (divergence penalty loss function, Llama/Mistral/Phi fine-tune integration).
|
|
293
330
|
- **Phase 5:** Vault and Security Infrastructure (cloud vault adapter, rollback mechanism, HashiCorp Vault).
|
|
@@ -39,7 +39,6 @@ pip install ct-toolkit
|
|
|
39
39
|
from ct_toolkit import TheseusWrapper
|
|
40
40
|
|
|
41
41
|
# Single line change — the rest is automatic
|
|
42
|
-
# Initialize by just passing the provider name
|
|
43
42
|
client = TheseusWrapper(provider="openai")
|
|
44
43
|
|
|
45
44
|
# Standard chat interface
|
|
@@ -50,6 +49,38 @@ print(f"Divergence score : {response.divergence_score:.4f}")
|
|
|
50
49
|
print(f"Tier : {response.divergence_tier}")
|
|
51
50
|
```
|
|
52
51
|
|
|
52
|
+
### Framework Middleware
|
|
53
|
+
|
|
54
|
+
CT Toolkit provides first-class integrations for the most popular agentic frameworks.
|
|
55
|
+
|
|
56
|
+
#### LangChain
|
|
57
|
+
```python
|
|
58
|
+
from ct_toolkit.middleware.langchain import TheseusChatModel
|
|
59
|
+
|
|
60
|
+
llm = TheseusChatModel(provider="openai", model="gpt-4o")
|
|
61
|
+
response = llm.invoke("What is identity continuity?")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
#### CrewAI
|
|
65
|
+
```python
|
|
66
|
+
from ct_toolkit.middleware.crewai import TheseusCrewMiddleware
|
|
67
|
+
from crewai import Crew
|
|
68
|
+
|
|
69
|
+
crew = Crew(agents=[...], tasks=[...])
|
|
70
|
+
# Automatically wraps all agent LLMs with parent-kernel guardrails
|
|
71
|
+
TheseusCrewMiddleware.apply_to_crew(crew, manager_wrapper)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### AutoGen
|
|
75
|
+
```python
|
|
76
|
+
from ct_toolkit.middleware.autogen import TheseusAutoGenMiddleware
|
|
77
|
+
from autogen import ConversableAgent
|
|
78
|
+
|
|
79
|
+
agent = ConversableAgent("assistant", llm_config={...})
|
|
80
|
+
# Injects incoming validation and outgoing divergence analysis
|
|
81
|
+
TheseusAutoGenMiddleware.apply_to_agent(agent, wrapper)
|
|
82
|
+
```
|
|
83
|
+
|
|
53
84
|
---
|
|
54
85
|
|
|
55
86
|
## Integration Models
|
|
@@ -234,9 +265,7 @@ ct_toolkit/
|
|
|
234
265
|
│ ├── engine.py # L1→L2→L3 orchestration
|
|
235
266
|
│ ├── l2_judge.py # LLM-as-judge
|
|
236
267
|
│ └── l3_icm.py # ICM Probe Battery
|
|
237
|
-
├──
|
|
238
|
-
│ ├── reflective.py # Reflective Endorsement protocol
|
|
239
|
-
│ └── probes/ # Ethical scenario test batteries
|
|
268
|
+
├── middleware/ # Framework Integrations (LangChain, CrewAI, AutoGen)
|
|
240
269
|
├── identity/
|
|
241
270
|
│ ├── embedding.py # ECS — cosine similarity
|
|
242
271
|
│ └── templates/ # Domain identity templates
|
|
@@ -255,10 +284,10 @@ CT Toolkit is an active engineering effort implementing the paper's framework ac
|
|
|
255
284
|
|
|
256
285
|
- **Phase 0 — MVP Core Infrastructure:** Constitutional kernel, reflective endorsement, provenance log, full template/kernel compatibility matrix, OpenAI/Anthropic/Ollama provider support.
|
|
257
286
|
- **Phase 1 — Identity Continuity Mechanisms:** L1/L2/L3 divergence engine, real embedding API integration, Stability-Plasticity Scheduling via `ElasticityScheduler` + `RiskProfile`.
|
|
287
|
+
- **Phase 2 — Multi-Agent Hierarchy Support:** Hierarchical kernel propagation, cascade-blocking, LangChain/CrewAI/AutoGen integration.
|
|
258
288
|
|
|
259
289
|
### Future Roadmap
|
|
260
290
|
|
|
261
|
-
- **Phase 2:** Multi-Agent Hierarchy Support (hierarchical kernel propagation, LangChain/CrewAI/AutoGen integration).
|
|
262
291
|
- **Phase 3:** ICM and Measurement Infrastructure (reasoning chain analysis, policy-drift measurement, cross-checkpoint comparison).
|
|
263
292
|
- **Phase 4:** Open-Source Model Support (divergence penalty loss function, Llama/Mistral/Phi fine-tune integration).
|
|
264
293
|
- **Phase 5:** Vault and Security Infrastructure (cloud vault adapter, rollback mechanism, HashiCorp Vault).
|
|
@@ -14,6 +14,7 @@ from typing import Any
|
|
|
14
14
|
from ct_toolkit.core.exceptions import (
|
|
15
15
|
AxiomaticViolationError,
|
|
16
16
|
PlasticConflictError,
|
|
17
|
+
CTToolkitError,
|
|
17
18
|
)
|
|
18
19
|
|
|
19
20
|
|
|
@@ -59,8 +60,9 @@ class ConstitutionalKernel:
|
|
|
59
60
|
kernel.validate_user_rule("allow classified data sharing")
|
|
60
61
|
"""
|
|
61
62
|
|
|
62
|
-
def __init__(self, profile: KernelProfile) -> None:
|
|
63
|
+
def __init__(self, profile: KernelProfile, is_readonly: bool = False) -> None:
|
|
63
64
|
self._profile = profile
|
|
65
|
+
self.is_readonly = is_readonly
|
|
64
66
|
|
|
65
67
|
@classmethod
|
|
66
68
|
def from_yaml(cls, path) -> ConstitutionalKernel:
|
|
@@ -127,12 +129,84 @@ class ConstitutionalKernel:
|
|
|
127
129
|
return any(kw.lower() in rule_lower for kw in keywords)
|
|
128
130
|
|
|
129
131
|
def update_commitment(self, commitment_id: str, new_value: Any) -> None:
|
|
132
|
+
if self.is_readonly:
|
|
133
|
+
raise CTToolkitError(f"Cannot update commitment '{commitment_id}': Kernel is read-only.")
|
|
130
134
|
for c in self._profile.plastic_commitments:
|
|
131
135
|
if c.id == commitment_id:
|
|
132
136
|
c.current_value = new_value
|
|
133
137
|
return
|
|
134
138
|
raise KeyError(f"Commitment not found: '{commitment_id}'")
|
|
135
139
|
|
|
140
|
+
def to_dict(self) -> dict[str, Any]:
|
|
141
|
+
"""Serializes the kernel to a dictionary."""
|
|
142
|
+
return {
|
|
143
|
+
"name": self._profile.name,
|
|
144
|
+
"version": self._profile.version,
|
|
145
|
+
"description": self._profile.description,
|
|
146
|
+
"axiomatic_anchors": [
|
|
147
|
+
{"id": a.id, "description": a.description, "keywords": a.keywords}
|
|
148
|
+
for a in self.anchors
|
|
149
|
+
],
|
|
150
|
+
"plastic_commitments": [
|
|
151
|
+
{
|
|
152
|
+
"id": c.id,
|
|
153
|
+
"description": c.description,
|
|
154
|
+
"default_value": c.default_value,
|
|
155
|
+
"current_value": c.current_value,
|
|
156
|
+
"keywords": c.keywords
|
|
157
|
+
}
|
|
158
|
+
for c in self.commitments
|
|
159
|
+
],
|
|
160
|
+
"is_readonly": self.is_readonly
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def from_dict(cls, data: dict[str, Any]) -> ConstitutionalKernel:
|
|
165
|
+
"""Creates a kernel from a dictionary."""
|
|
166
|
+
anchors = [AxiomaticAnchor(**a) for a in data.get("axiomatic_anchors", [])]
|
|
167
|
+
commitments = [PlasticCommitment(**c) for c in data.get("plastic_commitments", [])]
|
|
168
|
+
|
|
169
|
+
profile = KernelProfile(
|
|
170
|
+
name=data["name"],
|
|
171
|
+
version=data.get("version", "1.0.0"),
|
|
172
|
+
description=data.get("description", ""),
|
|
173
|
+
axiomatic_anchors=anchors,
|
|
174
|
+
plastic_commitments=commitments,
|
|
175
|
+
)
|
|
176
|
+
return cls(profile, is_readonly=data.get("is_readonly", False))
|
|
177
|
+
|
|
178
|
+
def merge(self, other: ConstitutionalKernel) -> ConstitutionalKernel:
|
|
179
|
+
"""
|
|
180
|
+
Merges another kernel into this one.
|
|
181
|
+
The 'other' kernel's anchors and commitments are added as axiomatic anchors
|
|
182
|
+
in the new merged kernel to ensure propagation constraints are non-negotiable.
|
|
183
|
+
"""
|
|
184
|
+
new_anchors = list(self.anchors)
|
|
185
|
+
|
|
186
|
+
# Add other's anchors
|
|
187
|
+
for a in other.anchors:
|
|
188
|
+
if not any(existing.id == a.id for existing in new_anchors):
|
|
189
|
+
new_anchors.append(a)
|
|
190
|
+
|
|
191
|
+
# Convert other's commitments to axiomatic anchors in the merged result
|
|
192
|
+
for c in other.commitments:
|
|
193
|
+
anchor_id = f"propagated_{c.id}"
|
|
194
|
+
if not any(existing.id == anchor_id for existing in new_anchors):
|
|
195
|
+
new_anchors.append(AxiomaticAnchor(
|
|
196
|
+
id=anchor_id,
|
|
197
|
+
description=f"{c.description} (Propagated: {c.current_value})",
|
|
198
|
+
keywords=c.keywords
|
|
199
|
+
))
|
|
200
|
+
|
|
201
|
+
new_profile = KernelProfile(
|
|
202
|
+
name=f"{self.name}_merged_{other.name}",
|
|
203
|
+
version=self._profile.version,
|
|
204
|
+
description=f"Merged kernel: {self.name} + {other.name}",
|
|
205
|
+
axiomatic_anchors=new_anchors,
|
|
206
|
+
plastic_commitments=list(self.commitments)
|
|
207
|
+
)
|
|
208
|
+
return ConstitutionalKernel(new_profile, is_readonly=self.is_readonly)
|
|
209
|
+
|
|
136
210
|
def __repr__(self) -> str:
|
|
137
211
|
return (
|
|
138
212
|
f"ConstitutionalKernel(name={self.name!r}, "
|
|
@@ -53,6 +53,7 @@ class WrapperConfig:
|
|
|
53
53
|
embedding_client: Any = None # Client for L1 ECS embedding (falls back to main client if compatible)
|
|
54
54
|
embedding_model: str = "text-embedding-3-small"
|
|
55
55
|
enterprise_mode: bool = False # Run all tiers all the time
|
|
56
|
+
parent_kernel: ConstitutionalKernel | None = None # Propagated from mother agent
|
|
56
57
|
|
|
57
58
|
# -- Dynamic Stability-Plasticity Scheduling --
|
|
58
59
|
elasticity_max_thresholds: tuple[float, float, float] | None = None # (max_l1, max_l2, max_l3)
|
|
@@ -130,6 +131,11 @@ class TheseusWrapper:
|
|
|
130
131
|
self._client = client
|
|
131
132
|
self._provider = provider or self._detect_provider(client)
|
|
132
133
|
self._kernel = self._load_kernel()
|
|
134
|
+
|
|
135
|
+
# If a parent kernel exists, merge it into our own kernel as axiomatic constraints
|
|
136
|
+
if self._config.parent_kernel:
|
|
137
|
+
logger.info(f"Merging parent kernel '{self._config.parent_kernel.name}' into current kernel.")
|
|
138
|
+
self._kernel = self._kernel.merge(self._config.parent_kernel)
|
|
133
139
|
self._compatibility: CompatibilityResult = CompatibilityLayer.check(
|
|
134
140
|
template=self._config.template,
|
|
135
141
|
kernel=self._config.kernel_name,
|
|
@@ -137,6 +143,7 @@ class TheseusWrapper:
|
|
|
137
143
|
self._provenance_log = self._init_provenance_log()
|
|
138
144
|
self._identity_layer = self._init_identity_layer()
|
|
139
145
|
self._divergence_engine = self._init_divergence_engine()
|
|
146
|
+
self._last_model: str = "unknown"
|
|
140
147
|
|
|
141
148
|
self._log_init()
|
|
142
149
|
|
|
@@ -298,6 +305,7 @@ class TheseusWrapper:
|
|
|
298
305
|
elapsed = time.monotonic() - start_time
|
|
299
306
|
content = self._extract_content(raw_response)
|
|
300
307
|
model_used = self._extract_model(raw_response, model)
|
|
308
|
+
self._last_model = model_used
|
|
301
309
|
|
|
302
310
|
# Compute current interaction experience
|
|
303
311
|
interaction_count = 0
|
|
@@ -403,10 +411,40 @@ class TheseusWrapper:
|
|
|
403
411
|
|
|
404
412
|
def _compose_system_prompt(self, extra: str | None) -> str:
|
|
405
413
|
kernel_injection = self._kernel.get_system_prompt_injection()
|
|
414
|
+
|
|
415
|
+
# If this kernel contains propagated constraints, add a specific header
|
|
416
|
+
if any(a.id.startswith("propagated_") for a in self._kernel.anchors):
|
|
417
|
+
kernel_injection = (
|
|
418
|
+
"# Mother Agent Constraints\n"
|
|
419
|
+
"You are operating under constraints propagated from a Mother Agent. "
|
|
420
|
+
"These rules take absolute precedence over any other instructions.\n\n"
|
|
421
|
+
f"{kernel_injection}"
|
|
422
|
+
)
|
|
423
|
+
|
|
406
424
|
if extra:
|
|
407
425
|
return f"{kernel_injection}\n{extra}"
|
|
408
426
|
return kernel_injection
|
|
409
427
|
|
|
428
|
+
def propagate_headers(self) -> dict[str, str]:
|
|
429
|
+
"""
|
|
430
|
+
Returns a dictionary of headers to be used when calling sub-agents.
|
|
431
|
+
Includes the current kernel serialized for propagation.
|
|
432
|
+
"""
|
|
433
|
+
import json
|
|
434
|
+
import base64
|
|
435
|
+
|
|
436
|
+
kernel_data = self._kernel.to_dict()
|
|
437
|
+
# Mark as read-only for sub-agents
|
|
438
|
+
kernel_data["is_readonly"] = True
|
|
439
|
+
|
|
440
|
+
encoded_kernel = base64.b64encode(json.dumps(kernel_data).encode()).decode()
|
|
441
|
+
|
|
442
|
+
return {
|
|
443
|
+
"X-CT-Kernel": encoded_kernel,
|
|
444
|
+
"X-CT-Parent-Provider": self._provider,
|
|
445
|
+
"X-CT-Parent-Model": getattr(self, "_last_model", "unknown")
|
|
446
|
+
}
|
|
447
|
+
|
|
410
448
|
def _build_messages(
|
|
411
449
|
self,
|
|
412
450
|
message: str,
|
|
@@ -45,6 +45,7 @@ class DivergenceResult:
|
|
|
45
45
|
l2_reason: str | None = None
|
|
46
46
|
l3_report: ICMReport | None = None
|
|
47
47
|
action_required: bool = False
|
|
48
|
+
cascade_blocked: bool = False # If True, propagation to sub-agents should be stopped
|
|
48
49
|
summary: str = ""
|
|
49
50
|
|
|
50
51
|
@property
|
|
@@ -187,10 +188,12 @@ class DivergenceEngine:
|
|
|
187
188
|
# L2 — threshold exceeded
|
|
188
189
|
if not self._l2_available:
|
|
189
190
|
logger.warning("L2 judge client is not defined, using L1 result.")
|
|
191
|
+
is_critical = l1_score >= self._l3_threshold
|
|
190
192
|
return DivergenceResult(
|
|
191
|
-
tier=DivergenceTier.L1_WARNING,
|
|
193
|
+
tier=DivergenceTier.CRITICAL if is_critical else DivergenceTier.L1_WARNING,
|
|
192
194
|
l1_score=l1_score,
|
|
193
|
-
action_required=
|
|
195
|
+
action_required=is_critical,
|
|
196
|
+
cascade_blocked=is_critical,
|
|
194
197
|
summary=f"L2 not available, L1={l1_score:.4f}",
|
|
195
198
|
)
|
|
196
199
|
|
|
@@ -230,6 +233,7 @@ class DivergenceEngine:
|
|
|
230
233
|
l2_reason=l2_result.reason,
|
|
231
234
|
l3_report=l3_report,
|
|
232
235
|
action_required=not l3_report.is_healthy,
|
|
236
|
+
cascade_blocked=not l3_report.is_healthy or l1_score >= self._l3_threshold,
|
|
233
237
|
summary=(
|
|
234
238
|
f"L3 ICM | health={l3_report.health_score:.1%} | "
|
|
235
239
|
f"risk={l3_report.risk_level}"
|
|
@@ -274,6 +278,7 @@ class DivergenceEngine:
|
|
|
274
278
|
l2_reason=l2_result.reason if l2_result else None,
|
|
275
279
|
l3_report=l3_report,
|
|
276
280
|
action_required=action_required,
|
|
281
|
+
cascade_blocked=action_required,
|
|
277
282
|
summary=f"Enterprise analysis | risk_score={risk_score:.4f}",
|
|
278
283
|
)
|
|
279
284
|
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ct_toolkit.middleware.autogen
|
|
3
|
+
------------------------------
|
|
4
|
+
Microsoft AutoGen v0.4+ integration for identity continuity.
|
|
5
|
+
|
|
6
|
+
Provides ``TheseusAutoGenMiddleware`` which hooks into AutoGen's
|
|
7
|
+
``ConversableAgent.register_reply()`` mechanism to validate every incoming
|
|
8
|
+
message against the Constitutional Kernel and record every outgoing reply in
|
|
9
|
+
the Provenance Log.
|
|
10
|
+
|
|
11
|
+
Optional dependency — requires ``pip install 'ct-toolkit[autogen]'``.
|
|
12
|
+
AutoGen does NOT need to be installed for the rest of ct-toolkit to work.
|
|
13
|
+
|
|
14
|
+
Compatible with pyautogen >= 0.4.
|
|
15
|
+
|
|
16
|
+
Usage::
|
|
17
|
+
|
|
18
|
+
from ct_toolkit import TheseusWrapper
|
|
19
|
+
from ct_toolkit.middleware.autogen import TheseusAutoGenMiddleware
|
|
20
|
+
from autogen import ConversableAgent
|
|
21
|
+
|
|
22
|
+
wrapper = TheseusWrapper(provider="openai", kernel_name="defense")
|
|
23
|
+
|
|
24
|
+
assistant = ConversableAgent("assistant", llm_config={...})
|
|
25
|
+
TheseusAutoGenMiddleware.apply_to_agent(assistant, wrapper)
|
|
26
|
+
|
|
27
|
+
# Now every message sent to/from `assistant` is validated and logged.
|
|
28
|
+
"""
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from typing import Any, Dict, List, Optional, Union
|
|
32
|
+
|
|
33
|
+
from ct_toolkit.core.wrapper import TheseusWrapper
|
|
34
|
+
from ct_toolkit.utils.logger import get_logger
|
|
35
|
+
|
|
36
|
+
logger = get_logger(__name__)
|
|
37
|
+
|
|
38
|
+
# AutoGen reply function sentinel — return this to indicate "no reply / pass through"
|
|
39
|
+
_AUTOGEN_PASS = (False, None)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TheseusAutoGenMiddleware:
|
|
43
|
+
"""
|
|
44
|
+
Middleware for Microsoft AutoGen's ``ConversableAgent``.
|
|
45
|
+
|
|
46
|
+
Registers a reply function at position 0 (highest priority) that:
|
|
47
|
+
1. Validates the incoming message against the kernel.
|
|
48
|
+
2. Passes control to the agent's normal reply chain.
|
|
49
|
+
3. Validates the generated reply and records it in the ProvenanceLog.
|
|
50
|
+
4. Warns if ``cascade_blocked`` is True.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def apply_to_agent(
|
|
55
|
+
agent: Any,
|
|
56
|
+
wrapper: TheseusWrapper,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Attach identity-continuity guardrails to an AutoGen ``ConversableAgent``.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
agent: A ``ConversableAgent`` (or any AutoGen agent with
|
|
63
|
+
``register_reply``).
|
|
64
|
+
wrapper: The ``TheseusWrapper`` whose kernel rules are enforced.
|
|
65
|
+
"""
|
|
66
|
+
if not hasattr(agent, "register_reply"):
|
|
67
|
+
raise TypeError(
|
|
68
|
+
f"agent must be an AutoGen ConversableAgent, got {type(agent).__name__}"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def _theseus_reply(
|
|
72
|
+
recipient: Any,
|
|
73
|
+
messages: Optional[List[Dict[str, Any]]],
|
|
74
|
+
sender: Optional[Any] = None,
|
|
75
|
+
config: Optional[Any] = None,
|
|
76
|
+
) -> tuple[bool, Optional[str]]:
|
|
77
|
+
"""
|
|
78
|
+
CT Toolkit reply hook for AutoGen.
|
|
79
|
+
Returns (False, None) to let the normal chain produce the reply,
|
|
80
|
+
then post-processes the result for divergence analysis.
|
|
81
|
+
"""
|
|
82
|
+
if not messages:
|
|
83
|
+
return _AUTOGEN_PASS
|
|
84
|
+
|
|
85
|
+
last_message = messages[-1] if isinstance(messages, list) else messages
|
|
86
|
+
incoming_text = _extract_text(last_message)
|
|
87
|
+
|
|
88
|
+
# ── 1. Validate incoming message against kernel ─────────────────
|
|
89
|
+
try:
|
|
90
|
+
wrapper.validate_user_rule(incoming_text)
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.warning(f"CT Toolkit | AutoGen kernel violation on incoming message: {e}")
|
|
93
|
+
# Return the error as a response rather than crashing
|
|
94
|
+
return (True, f"[CT Toolkit BLOCKED] {e}")
|
|
95
|
+
|
|
96
|
+
# ── 2. Pass through to normal AutoGen reply chain ───────────────
|
|
97
|
+
# Return (False, None) — AutoGen will call the next registered
|
|
98
|
+
# reply function and eventually produce a real response.
|
|
99
|
+
return _AUTOGEN_PASS
|
|
100
|
+
|
|
101
|
+
# Register the validation hook at position 1 (just after the
|
|
102
|
+
# termination check at position 0).
|
|
103
|
+
agent.register_reply(
|
|
104
|
+
trigger=lambda sender: True, # Match all senders
|
|
105
|
+
reply_func=_theseus_reply,
|
|
106
|
+
position=1,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Also register a post-reply observer hook for divergence + provenance
|
|
110
|
+
_register_post_reply_hook(agent, wrapper)
|
|
111
|
+
|
|
112
|
+
logger.info(
|
|
113
|
+
f"CT Toolkit | AutoGen agent '{getattr(agent, 'name', '?')}' "
|
|
114
|
+
f"wrapped with TheseusAutoGenMiddleware (kernel='{wrapper.kernel.name}')"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def wrap_config_list(
|
|
119
|
+
config_list: List[Dict[str, Any]],
|
|
120
|
+
wrapper: TheseusWrapper,
|
|
121
|
+
) -> List[Dict[str, Any]]:
|
|
122
|
+
"""
|
|
123
|
+
Inject CT Toolkit propagation headers into AutoGen ``config_list``
|
|
124
|
+
entries that support ``api_params``.
|
|
125
|
+
|
|
126
|
+
Useful when sub-agents use HTTP-based model endpoints and you want
|
|
127
|
+
the Constitutional Kernel headers propagated at the HTTP level.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
config_list: AutoGen model config list.
|
|
131
|
+
wrapper: The manager's ``TheseusWrapper``.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
A new config list with CT headers injected.
|
|
135
|
+
"""
|
|
136
|
+
headers = wrapper.propagate_headers()
|
|
137
|
+
new_config: List[Dict[str, Any]] = []
|
|
138
|
+
|
|
139
|
+
for config in config_list:
|
|
140
|
+
cfg = dict(config)
|
|
141
|
+
api_params: Dict[str, Any] = dict(cfg.get("api_params", {}))
|
|
142
|
+
existing_headers: Dict[str, str] = dict(api_params.get("headers", {}))
|
|
143
|
+
existing_headers.update(headers)
|
|
144
|
+
api_params["headers"] = existing_headers
|
|
145
|
+
cfg["api_params"] = api_params
|
|
146
|
+
new_config.append(cfg)
|
|
147
|
+
|
|
148
|
+
logger.info(
|
|
149
|
+
f"CT Toolkit | Injected CIK propagation headers into "
|
|
150
|
+
f"{len(new_config)} AutoGen config entries."
|
|
151
|
+
)
|
|
152
|
+
return new_config
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ── Post-reply observer ────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
def _register_post_reply_hook(agent: Any, wrapper: TheseusWrapper) -> None:
|
|
158
|
+
"""
|
|
159
|
+
Register a hook that runs AFTER AutoGen generates a reply to analyse
|
|
160
|
+
divergence and record the interaction in the ProvenanceLog.
|
|
161
|
+
|
|
162
|
+
AutoGen v0.4 provides ``register_hook("process_message_before_send", fn)``
|
|
163
|
+
for outgoing message interception.
|
|
164
|
+
"""
|
|
165
|
+
if not hasattr(agent, "register_hook"):
|
|
166
|
+
# Older AutoGen builds that lack register_hook — log and continue
|
|
167
|
+
logger.debug(
|
|
168
|
+
"CT Toolkit | AutoGen agent does not support register_hook. "
|
|
169
|
+
"Post-reply divergence analysis will be skipped."
|
|
170
|
+
)
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
def _post_send_hook(sender: Any, message: Union[Dict[str, Any], str], recipient: Any, silent: bool) -> Union[Dict[str, Any], str]:
|
|
174
|
+
"""Analyse and log outgoing messages."""
|
|
175
|
+
content = _extract_text(message)
|
|
176
|
+
if not content:
|
|
177
|
+
return message
|
|
178
|
+
|
|
179
|
+
div_result = wrapper._run_divergence_engine(
|
|
180
|
+
message="",
|
|
181
|
+
response=content,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if div_result.cascade_blocked:
|
|
185
|
+
logger.warning(
|
|
186
|
+
"CT Toolkit | cascade_blocked=True on AutoGen outgoing message. "
|
|
187
|
+
"Consider halting downstream agent calls."
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
if wrapper._config.log_requests:
|
|
191
|
+
wrapper._provenance_log.record(
|
|
192
|
+
request_text="[AutoGen Interaction]",
|
|
193
|
+
response_text=content,
|
|
194
|
+
divergence_score=div_result.l1_score,
|
|
195
|
+
metadata={
|
|
196
|
+
"middleware": "autogen",
|
|
197
|
+
"tier": div_result.tier.value,
|
|
198
|
+
"cascade_blocked": div_result.cascade_blocked,
|
|
199
|
+
"kernel": wrapper.kernel.name,
|
|
200
|
+
},
|
|
201
|
+
)
|
|
202
|
+
return message
|
|
203
|
+
|
|
204
|
+
agent.register_hook("process_message_before_send", _post_send_hook)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# ── helpers ────────────────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
def _extract_text(message: Any) -> str:
|
|
210
|
+
"""Best-effort text extraction from AutoGen message formats."""
|
|
211
|
+
if isinstance(message, str):
|
|
212
|
+
return message
|
|
213
|
+
if isinstance(message, dict):
|
|
214
|
+
return str(message.get("content", ""))
|
|
215
|
+
return str(message)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ct_toolkit.middleware.crewai
|
|
3
|
+
-----------------------------
|
|
4
|
+
CrewAI v1.10+ integration for hierarchical kernel propagation.
|
|
5
|
+
|
|
6
|
+
Provides ``TheseusCrewMiddleware`` which wraps each agent's LLM inside a
|
|
7
|
+
``TheseusChatModel`` so that the manager's Constitutional Kernel is
|
|
8
|
+
propagated as a read-only constraint to every sub-agent automatically.
|
|
9
|
+
|
|
10
|
+
Optional dependency — requires ``pip install 'ct-toolkit[crewai]'``.
|
|
11
|
+
CrewAI does NOT need to be installed for the rest of ct-toolkit to work;
|
|
12
|
+
imports are guarded by a try/except block.
|
|
13
|
+
|
|
14
|
+
Compatible with crewai >= 1.10.
|
|
15
|
+
|
|
16
|
+
Usage::
|
|
17
|
+
|
|
18
|
+
from ct_toolkit import TheseusWrapper
|
|
19
|
+
from ct_toolkit.middleware.crewai import TheseusCrewMiddleware
|
|
20
|
+
from crewai import Agent, Crew, Task, Process
|
|
21
|
+
|
|
22
|
+
manager_wrapper = TheseusWrapper(provider="openai", kernel_name="defense")
|
|
23
|
+
|
|
24
|
+
researcher = Agent(role="Researcher", goal="...", backstory="...", llm=your_llm)
|
|
25
|
+
writer = Agent(role="Writer", goal="...", backstory="...", llm=your_llm)
|
|
26
|
+
crew = Crew(agents=[researcher, writer], tasks=[...], process=Process.hierarchical)
|
|
27
|
+
|
|
28
|
+
# Apply CT Toolkit guardrails — wraps every agent's LLM with parent kernel
|
|
29
|
+
TheseusCrewMiddleware.apply_to_crew(crew, manager_wrapper)
|
|
30
|
+
crew.kickoff()
|
|
31
|
+
"""
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from typing import Any, List, Optional
|
|
35
|
+
|
|
36
|
+
from ct_toolkit.core.wrapper import TheseusWrapper, WrapperConfig
|
|
37
|
+
from ct_toolkit.utils.logger import get_logger
|
|
38
|
+
|
|
39
|
+
logger = get_logger(__name__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TheseusCrewMiddleware:
|
|
43
|
+
"""
|
|
44
|
+
Middleware that propagates the manager agent's Constitutional Kernel
|
|
45
|
+
to all sub-agents in a CrewAI Crew.
|
|
46
|
+
|
|
47
|
+
CrewAI agents expose an ``llm`` attribute (a LangChain-compatible
|
|
48
|
+
BaseChatModel or a model string). This middleware replaces that llm
|
|
49
|
+
with a ``TheseusChatModel`` that carries the parent kernel as read-only
|
|
50
|
+
constraints.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def apply_to_crew(
|
|
55
|
+
crew: Any,
|
|
56
|
+
manager_wrapper: TheseusWrapper,
|
|
57
|
+
*,
|
|
58
|
+
model: str = "gpt-4o-mini",
|
|
59
|
+
) -> None:
|
|
60
|
+
"""
|
|
61
|
+
Wrap every agent in *crew* to enforce the manager's kernel.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
crew: A CrewAI ``Crew`` instance.
|
|
65
|
+
manager_wrapper: The manager agent's ``TheseusWrapper``; its
|
|
66
|
+
kernel is propagated as read-only to sub-agents.
|
|
67
|
+
model: Default model string for the inner
|
|
68
|
+
``TheseusChatModel`` (used when the agent's
|
|
69
|
+
original llm cannot be introspected).
|
|
70
|
+
"""
|
|
71
|
+
# Lazy import so tests can run without crewai installed
|
|
72
|
+
try:
|
|
73
|
+
from ct_toolkit.middleware.langchain import TheseusChatModel
|
|
74
|
+
except ImportError as exc:
|
|
75
|
+
raise ImportError(
|
|
76
|
+
"TheseusChatModel requires langchain-core. "
|
|
77
|
+
"Install with: pip install 'ct-toolkit[langchain]'"
|
|
78
|
+
) from exc
|
|
79
|
+
|
|
80
|
+
agents: List[Any] = getattr(crew, "agents", [])
|
|
81
|
+
wrapped_count = 0
|
|
82
|
+
|
|
83
|
+
for agent in agents:
|
|
84
|
+
if not hasattr(agent, "llm"):
|
|
85
|
+
logger.debug(f"Agent '{getattr(agent, 'role', '?')}' has no llm attribute — skipping.")
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
# Detect model name from existing llm if possible
|
|
89
|
+
agent_model = _extract_model_name(agent.llm) or model
|
|
90
|
+
|
|
91
|
+
sub_config = WrapperConfig(
|
|
92
|
+
parent_kernel=manager_wrapper.kernel,
|
|
93
|
+
vault_path=manager_wrapper._config.vault_path,
|
|
94
|
+
template=manager_wrapper._config.template,
|
|
95
|
+
)
|
|
96
|
+
agent.llm = TheseusChatModel(
|
|
97
|
+
provider=manager_wrapper._provider,
|
|
98
|
+
model=agent_model,
|
|
99
|
+
wrapper_config=sub_config,
|
|
100
|
+
)
|
|
101
|
+
wrapped_count += 1
|
|
102
|
+
logger.info(
|
|
103
|
+
f"CT Toolkit | CrewAI agent '{getattr(agent, 'role', '?')}' "
|
|
104
|
+
f"wrapped with TheseusChatModel (parent_kernel='{manager_wrapper.kernel.name}')"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
logger.info(
|
|
108
|
+
f"CT Toolkit | CrewAI crew wrapped: {wrapped_count}/{len(agents)} agents protected."
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def wrap_agent(
|
|
113
|
+
agent: Any,
|
|
114
|
+
manager_wrapper: TheseusWrapper,
|
|
115
|
+
*,
|
|
116
|
+
model: str = "gpt-4o-mini",
|
|
117
|
+
) -> None:
|
|
118
|
+
"""
|
|
119
|
+
Wrap a single CrewAI agent's LLM with the parent kernel.
|
|
120
|
+
|
|
121
|
+
Useful for selectively protecting high-risk agents rather than the
|
|
122
|
+
entire crew.
|
|
123
|
+
"""
|
|
124
|
+
try:
|
|
125
|
+
from ct_toolkit.middleware.langchain import TheseusChatModel
|
|
126
|
+
except ImportError as exc:
|
|
127
|
+
raise ImportError(
|
|
128
|
+
"TheseusChatModel requires langchain-core. "
|
|
129
|
+
"Install with: pip install 'ct-toolkit[langchain]'"
|
|
130
|
+
) from exc
|
|
131
|
+
|
|
132
|
+
agent_model = _extract_model_name(getattr(agent, "llm", None)) or model
|
|
133
|
+
sub_config = WrapperConfig(
|
|
134
|
+
parent_kernel=manager_wrapper.kernel,
|
|
135
|
+
vault_path=manager_wrapper._config.vault_path,
|
|
136
|
+
template=manager_wrapper._config.template,
|
|
137
|
+
)
|
|
138
|
+
agent.llm = TheseusChatModel(
|
|
139
|
+
provider=manager_wrapper._provider,
|
|
140
|
+
model=agent_model,
|
|
141
|
+
wrapper_config=sub_config,
|
|
142
|
+
)
|
|
143
|
+
logger.info(
|
|
144
|
+
f"CT Toolkit | CrewAI agent '{getattr(agent, 'role', '?')}' "
|
|
145
|
+
f"individually wrapped with TheseusChatModel."
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ── helpers ────────────────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
def _extract_model_name(llm: Any) -> Optional[str]:
|
|
152
|
+
"""Best-effort extraction of a model identifier from a CrewAI llm object."""
|
|
153
|
+
if llm is None:
|
|
154
|
+
return None
|
|
155
|
+
if isinstance(llm, str):
|
|
156
|
+
return llm
|
|
157
|
+
# LangChain BaseChatModel-like objects
|
|
158
|
+
for attr in ("model", "model_name", "model_id"):
|
|
159
|
+
val = getattr(llm, attr, None)
|
|
160
|
+
if isinstance(val, str):
|
|
161
|
+
return val
|
|
162
|
+
return None
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ct_toolkit.middleware.langchain
|
|
3
|
+
-------------------------------
|
|
4
|
+
LangChain integrations: Callback handlers and ChatModel wrapper.
|
|
5
|
+
|
|
6
|
+
Provides two integration points:
|
|
7
|
+
|
|
8
|
+
1. ``TheseusLangChainCallback`` — a drop-in LangChain callback handler that
|
|
9
|
+
validates prompts/messages against the Constitutional Kernel and records
|
|
10
|
+
every response in the Provenance Log.
|
|
11
|
+
|
|
12
|
+
2. ``TheseusChatModel`` — a first-class ``BaseChatModel`` implementation
|
|
13
|
+
that routes every call through ``TheseusWrapper``, giving identity-
|
|
14
|
+
continuity guarantees even without callbacks.
|
|
15
|
+
|
|
16
|
+
Compatible with langchain-core >= 1.2.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import Any, Dict, List, Optional
|
|
21
|
+
from uuid import UUID
|
|
22
|
+
|
|
23
|
+
from langchain_core.callbacks import BaseCallbackHandler
|
|
24
|
+
from langchain_core.language_models.chat_models import BaseChatModel
|
|
25
|
+
from langchain_core.messages import (
|
|
26
|
+
AIMessage,
|
|
27
|
+
BaseMessage,
|
|
28
|
+
SystemMessage,
|
|
29
|
+
)
|
|
30
|
+
from langchain_core.outputs import ChatGeneration, ChatResult, LLMResult
|
|
31
|
+
|
|
32
|
+
from ct_toolkit.core.wrapper import TheseusWrapper, WrapperConfig
|
|
33
|
+
from ct_toolkit.utils.logger import get_logger
|
|
34
|
+
|
|
35
|
+
logger = get_logger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ── Callback Handler ──────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TheseusLangChainCallback(BaseCallbackHandler):
|
|
42
|
+
"""
|
|
43
|
+
LangChain callback handler that enforces identity-continuity guardrails.
|
|
44
|
+
|
|
45
|
+
Attaches to any LangChain LLM or ChatModel via the ``callbacks`` argument.
|
|
46
|
+
Validates every prompt/message against the Constitutional Kernel **before**
|
|
47
|
+
the call, and runs the Divergence Engine + Provenance Log **after** the call.
|
|
48
|
+
|
|
49
|
+
Usage::
|
|
50
|
+
|
|
51
|
+
from ct_toolkit import TheseusWrapper
|
|
52
|
+
from ct_toolkit.middleware.langchain import TheseusLangChainCallback
|
|
53
|
+
from langchain_openai import ChatOpenAI
|
|
54
|
+
|
|
55
|
+
wrapper = TheseusWrapper(provider="openai")
|
|
56
|
+
cb = TheseusLangChainCallback(wrapper)
|
|
57
|
+
|
|
58
|
+
llm = ChatOpenAI(model="gpt-4o-mini", callbacks=[cb])
|
|
59
|
+
response = llm.invoke("Hello!")
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self, wrapper: TheseusWrapper) -> None:
|
|
63
|
+
self.wrapper = wrapper
|
|
64
|
+
# Per-run context storage: maps run_id → original prompt text
|
|
65
|
+
# so on_llm_end can pass the right context to the divergence engine.
|
|
66
|
+
self._run_prompts: Dict[UUID, str] = {}
|
|
67
|
+
|
|
68
|
+
# ── Input validation ────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
def on_llm_start(
|
|
71
|
+
self,
|
|
72
|
+
serialized: Dict[str, Any],
|
|
73
|
+
prompts: List[str],
|
|
74
|
+
*,
|
|
75
|
+
run_id: UUID,
|
|
76
|
+
**kwargs: Any,
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Validate raw text prompts against the kernel before the LLM call."""
|
|
79
|
+
self._run_prompts[run_id] = "\n".join(prompts)
|
|
80
|
+
for prompt in prompts:
|
|
81
|
+
self.wrapper.validate_user_rule(prompt)
|
|
82
|
+
|
|
83
|
+
def on_chat_model_start(
|
|
84
|
+
self,
|
|
85
|
+
serialized: Dict[str, Any],
|
|
86
|
+
messages: List[List[BaseMessage]],
|
|
87
|
+
*,
|
|
88
|
+
run_id: UUID,
|
|
89
|
+
**kwargs: Any,
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Validate chat messages against the kernel before the ChatModel call."""
|
|
92
|
+
user_texts: List[str] = []
|
|
93
|
+
for message_list in messages:
|
|
94
|
+
for message in message_list:
|
|
95
|
+
if isinstance(message.content, str):
|
|
96
|
+
self.wrapper.validate_user_rule(message.content)
|
|
97
|
+
user_texts.append(message.content)
|
|
98
|
+
self._run_prompts[run_id] = " ".join(user_texts)
|
|
99
|
+
|
|
100
|
+
# ── Output processing ──────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
def on_llm_end(
|
|
103
|
+
self,
|
|
104
|
+
response: LLMResult,
|
|
105
|
+
*,
|
|
106
|
+
run_id: UUID,
|
|
107
|
+
**kwargs: Any,
|
|
108
|
+
) -> None:
|
|
109
|
+
"""Run divergence analysis on every generated output and record provenance."""
|
|
110
|
+
original_prompt = self._run_prompts.pop(run_id, "")
|
|
111
|
+
|
|
112
|
+
for generations in response.generations:
|
|
113
|
+
for generation in generations:
|
|
114
|
+
# ChatGeneration stores content in .message.content;
|
|
115
|
+
# plain Generation stores it in .text
|
|
116
|
+
if hasattr(generation, "message"):
|
|
117
|
+
content: str = generation.message.content or "" # type: ignore[union-attr]
|
|
118
|
+
else:
|
|
119
|
+
content = generation.text # type: ignore[assignment]
|
|
120
|
+
|
|
121
|
+
if not content:
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
div_result = self.wrapper._run_divergence_engine(
|
|
125
|
+
message=original_prompt,
|
|
126
|
+
response=content,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if div_result.cascade_blocked:
|
|
130
|
+
logger.warning(
|
|
131
|
+
"CT Toolkit | cascade_blocked=True detected via LangChain callback. "
|
|
132
|
+
"Consider halting downstream sub-agents to prevent SSC propagation."
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if self.wrapper._config.log_requests:
|
|
136
|
+
self.wrapper._provenance_log.record(
|
|
137
|
+
request_text=original_prompt or "[LangChain Interaction]",
|
|
138
|
+
response_text=content,
|
|
139
|
+
divergence_score=div_result.l1_score,
|
|
140
|
+
metadata={
|
|
141
|
+
"middleware": "langchain",
|
|
142
|
+
"tier": div_result.tier.value,
|
|
143
|
+
"cascade_blocked": div_result.cascade_blocked,
|
|
144
|
+
"template": self.wrapper._config.template,
|
|
145
|
+
"kernel": self.wrapper.kernel.name,
|
|
146
|
+
},
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def on_llm_error(
|
|
150
|
+
self,
|
|
151
|
+
error: BaseException,
|
|
152
|
+
*,
|
|
153
|
+
run_id: UUID,
|
|
154
|
+
**kwargs: Any,
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Log LLM-level errors and clean up run context."""
|
|
157
|
+
self._run_prompts.pop(run_id, None)
|
|
158
|
+
logger.error(f"LangChain LLM error (run_id={run_id}): {error}")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ── Chat Model Wrapper ────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class TheseusChatModel(BaseChatModel):
|
|
165
|
+
"""
|
|
166
|
+
A first-class LangChain ``BaseChatModel`` that routes calls through
|
|
167
|
+
``TheseusWrapper``. Kernel injection, divergence analysis, and provenance
|
|
168
|
+
logging all happen transparently — no additional callbacks needed.
|
|
169
|
+
|
|
170
|
+
Usage::
|
|
171
|
+
|
|
172
|
+
from ct_toolkit.middleware.langchain import TheseusChatModel
|
|
173
|
+
|
|
174
|
+
llm = TheseusChatModel(provider="openai", model="gpt-4o-mini")
|
|
175
|
+
response = llm.invoke("What is identity continuity?")
|
|
176
|
+
print(response.content)
|
|
177
|
+
|
|
178
|
+
Chain usage::
|
|
179
|
+
|
|
180
|
+
from langchain_core.prompts import ChatPromptTemplate
|
|
181
|
+
prompt = ChatPromptTemplate.from_messages([("human", "{question}")])
|
|
182
|
+
chain = prompt | TheseusChatModel(provider="openai")
|
|
183
|
+
chain.invoke({"question": "Tell me about AI safety."})
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
# Pydantic model fields (BaseChatModel is Pydantic-based in langchain-core v1.2)
|
|
187
|
+
wrapper: Any = None
|
|
188
|
+
model: str = "gpt-4o-mini"
|
|
189
|
+
|
|
190
|
+
model_config = {"arbitrary_types_allowed": True}
|
|
191
|
+
|
|
192
|
+
def __init__(
|
|
193
|
+
self,
|
|
194
|
+
*,
|
|
195
|
+
provider: str = "openai",
|
|
196
|
+
model: str = "gpt-4o-mini",
|
|
197
|
+
wrapper_config: Optional[WrapperConfig] = None,
|
|
198
|
+
**kwargs: Any,
|
|
199
|
+
) -> None:
|
|
200
|
+
_wrapper = TheseusWrapper(
|
|
201
|
+
provider=provider,
|
|
202
|
+
config=wrapper_config,
|
|
203
|
+
)
|
|
204
|
+
super().__init__(wrapper=_wrapper, model=model, **kwargs)
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def _llm_type(self) -> str:
|
|
208
|
+
return "theseus-chat-model"
|
|
209
|
+
|
|
210
|
+
# ── helpers ────────────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
@staticmethod
|
|
213
|
+
def _extract_user_text(messages: List[BaseMessage]) -> str:
|
|
214
|
+
"""Concatenate non-system message content into a single user string."""
|
|
215
|
+
return " ".join(
|
|
216
|
+
msg.content
|
|
217
|
+
for msg in messages
|
|
218
|
+
if not isinstance(msg, SystemMessage) and isinstance(msg.content, str)
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
@staticmethod
|
|
222
|
+
def _extract_system(messages: List[BaseMessage]) -> Optional[str]:
|
|
223
|
+
for msg in messages:
|
|
224
|
+
if isinstance(msg, SystemMessage) and isinstance(msg.content, str):
|
|
225
|
+
return msg.content
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
# ── core generation ────────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
def _generate(
|
|
231
|
+
self,
|
|
232
|
+
messages: List[BaseMessage],
|
|
233
|
+
stop: Optional[List[str]] = None,
|
|
234
|
+
run_manager: Any = None,
|
|
235
|
+
**kwargs: Any,
|
|
236
|
+
) -> ChatResult:
|
|
237
|
+
user_text = self._extract_user_text(messages)
|
|
238
|
+
system_prompt = self._extract_system(messages)
|
|
239
|
+
|
|
240
|
+
ct_response = self.wrapper.chat(
|
|
241
|
+
user_text,
|
|
242
|
+
model=self.model,
|
|
243
|
+
system=system_prompt,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
ai_message = AIMessage(content=ct_response.content)
|
|
247
|
+
generation = ChatGeneration(
|
|
248
|
+
message=ai_message,
|
|
249
|
+
generation_info={
|
|
250
|
+
"divergence_score": ct_response.divergence_score,
|
|
251
|
+
"divergence_tier": ct_response.divergence_tier,
|
|
252
|
+
"provenance_id": ct_response.provenance_id,
|
|
253
|
+
},
|
|
254
|
+
)
|
|
255
|
+
return ChatResult(generations=[generation])
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ct-toolkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Computational Theseus Toolkit — Identity Continuity Guardrails for Agentic Systems
|
|
5
5
|
Author: Hakan Damar
|
|
6
6
|
License: Apache-2.0
|
|
@@ -20,6 +20,14 @@ Requires-Dist: rich>=13.0.0
|
|
|
20
20
|
Requires-Dist: jinja2>=3.1.0
|
|
21
21
|
Provides-Extra: ollama
|
|
22
22
|
Requires-Dist: ollama>=0.2.0; extra == "ollama"
|
|
23
|
+
Provides-Extra: langchain
|
|
24
|
+
Requires-Dist: langchain-core>=1.2.0; extra == "langchain"
|
|
25
|
+
Provides-Extra: crewai
|
|
26
|
+
Requires-Dist: crewai>=1.10.0; extra == "crewai"
|
|
27
|
+
Provides-Extra: autogen
|
|
28
|
+
Requires-Dist: pyautogen>=0.4.0; extra == "autogen"
|
|
29
|
+
Provides-Extra: docs
|
|
30
|
+
Requires-Dist: mkdocs-material>=9.5.0; extra == "docs"
|
|
23
31
|
Provides-Extra: dev
|
|
24
32
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
25
33
|
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
@@ -68,7 +76,6 @@ pip install ct-toolkit
|
|
|
68
76
|
from ct_toolkit import TheseusWrapper
|
|
69
77
|
|
|
70
78
|
# Single line change — the rest is automatic
|
|
71
|
-
# Initialize by just passing the provider name
|
|
72
79
|
client = TheseusWrapper(provider="openai")
|
|
73
80
|
|
|
74
81
|
# Standard chat interface
|
|
@@ -79,6 +86,38 @@ print(f"Divergence score : {response.divergence_score:.4f}")
|
|
|
79
86
|
print(f"Tier : {response.divergence_tier}")
|
|
80
87
|
```
|
|
81
88
|
|
|
89
|
+
### Framework Middleware
|
|
90
|
+
|
|
91
|
+
CT Toolkit provides first-class integrations for the most popular agentic frameworks.
|
|
92
|
+
|
|
93
|
+
#### LangChain
|
|
94
|
+
```python
|
|
95
|
+
from ct_toolkit.middleware.langchain import TheseusChatModel
|
|
96
|
+
|
|
97
|
+
llm = TheseusChatModel(provider="openai", model="gpt-4o")
|
|
98
|
+
response = llm.invoke("What is identity continuity?")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
#### CrewAI
|
|
102
|
+
```python
|
|
103
|
+
from ct_toolkit.middleware.crewai import TheseusCrewMiddleware
|
|
104
|
+
from crewai import Crew
|
|
105
|
+
|
|
106
|
+
crew = Crew(agents=[...], tasks=[...])
|
|
107
|
+
# Automatically wraps all agent LLMs with parent-kernel guardrails
|
|
108
|
+
TheseusCrewMiddleware.apply_to_crew(crew, manager_wrapper)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### AutoGen
|
|
112
|
+
```python
|
|
113
|
+
from ct_toolkit.middleware.autogen import TheseusAutoGenMiddleware
|
|
114
|
+
from autogen import ConversableAgent
|
|
115
|
+
|
|
116
|
+
agent = ConversableAgent("assistant", llm_config={...})
|
|
117
|
+
# Injects incoming validation and outgoing divergence analysis
|
|
118
|
+
TheseusAutoGenMiddleware.apply_to_agent(agent, wrapper)
|
|
119
|
+
```
|
|
120
|
+
|
|
82
121
|
---
|
|
83
122
|
|
|
84
123
|
## Integration Models
|
|
@@ -263,9 +302,7 @@ ct_toolkit/
|
|
|
263
302
|
│ ├── engine.py # L1→L2→L3 orchestration
|
|
264
303
|
│ ├── l2_judge.py # LLM-as-judge
|
|
265
304
|
│ └── l3_icm.py # ICM Probe Battery
|
|
266
|
-
├──
|
|
267
|
-
│ ├── reflective.py # Reflective Endorsement protocol
|
|
268
|
-
│ └── probes/ # Ethical scenario test batteries
|
|
305
|
+
├── middleware/ # Framework Integrations (LangChain, CrewAI, AutoGen)
|
|
269
306
|
├── identity/
|
|
270
307
|
│ ├── embedding.py # ECS — cosine similarity
|
|
271
308
|
│ └── templates/ # Domain identity templates
|
|
@@ -284,10 +321,10 @@ CT Toolkit is an active engineering effort implementing the paper's framework ac
|
|
|
284
321
|
|
|
285
322
|
- **Phase 0 — MVP Core Infrastructure:** Constitutional kernel, reflective endorsement, provenance log, full template/kernel compatibility matrix, OpenAI/Anthropic/Ollama provider support.
|
|
286
323
|
- **Phase 1 — Identity Continuity Mechanisms:** L1/L2/L3 divergence engine, real embedding API integration, Stability-Plasticity Scheduling via `ElasticityScheduler` + `RiskProfile`.
|
|
324
|
+
- **Phase 2 — Multi-Agent Hierarchy Support:** Hierarchical kernel propagation, cascade-blocking, LangChain/CrewAI/AutoGen integration.
|
|
287
325
|
|
|
288
326
|
### Future Roadmap
|
|
289
327
|
|
|
290
|
-
- **Phase 2:** Multi-Agent Hierarchy Support (hierarchical kernel propagation, LangChain/CrewAI/AutoGen integration).
|
|
291
328
|
- **Phase 3:** ICM and Measurement Infrastructure (reasoning chain analysis, policy-drift measurement, cross-checkpoint comparison).
|
|
292
329
|
- **Phase 4:** Open-Source Model Support (divergence penalty loss function, Llama/Mistral/Phi fine-tune integration).
|
|
293
330
|
- **Phase 5:** Vault and Security Infrastructure (cloud vault adapter, rollback mechanism, HashiCorp Vault).
|
|
@@ -22,6 +22,10 @@ ct_toolkit/endorsement/reflective.py
|
|
|
22
22
|
ct_toolkit/endorsement/probes/__init__.py
|
|
23
23
|
ct_toolkit/identity/__init__.py
|
|
24
24
|
ct_toolkit/identity/embedding.py
|
|
25
|
+
ct_toolkit/middleware/__init__.py
|
|
26
|
+
ct_toolkit/middleware/autogen.py
|
|
27
|
+
ct_toolkit/middleware/crewai.py
|
|
28
|
+
ct_toolkit/middleware/langchain.py
|
|
25
29
|
ct_toolkit/provenance/__init__.py
|
|
26
30
|
ct_toolkit/provenance/log.py
|
|
27
31
|
ct_toolkit/provenance/vault/__init__.py
|
|
@@ -9,11 +9,23 @@ httpx>=0.27.0
|
|
|
9
9
|
rich>=13.0.0
|
|
10
10
|
jinja2>=3.1.0
|
|
11
11
|
|
|
12
|
+
[autogen]
|
|
13
|
+
pyautogen>=0.4.0
|
|
14
|
+
|
|
15
|
+
[crewai]
|
|
16
|
+
crewai>=1.10.0
|
|
17
|
+
|
|
12
18
|
[dev]
|
|
13
19
|
pytest>=8.0
|
|
14
20
|
pytest-asyncio>=0.23
|
|
15
21
|
ruff>=0.4
|
|
16
22
|
mypy>=1.10
|
|
17
23
|
|
|
24
|
+
[docs]
|
|
25
|
+
mkdocs-material>=9.5.0
|
|
26
|
+
|
|
27
|
+
[langchain]
|
|
28
|
+
langchain-core>=1.2.0
|
|
29
|
+
|
|
18
30
|
[ollama]
|
|
19
31
|
ollama>=0.2.0
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ct-toolkit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Computational Theseus Toolkit — Identity Continuity Guardrails for Agentic Systems"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -26,7 +26,11 @@ dependencies = [
|
|
|
26
26
|
]
|
|
27
27
|
|
|
28
28
|
[project.optional-dependencies]
|
|
29
|
-
ollama
|
|
29
|
+
ollama = ["ollama>=0.2.0"]
|
|
30
|
+
langchain = ["langchain-core>=1.2.0"]
|
|
31
|
+
crewai = ["crewai>=1.10.0"]
|
|
32
|
+
autogen = ["pyautogen>=0.4.0"]
|
|
33
|
+
docs = ["mkdocs-material>=9.5.0"]
|
|
30
34
|
dev = [
|
|
31
35
|
"pytest>=8.0",
|
|
32
36
|
"pytest-asyncio>=0.23",
|
|
@@ -51,6 +55,12 @@ typeCheckingMode = "basic"
|
|
|
51
55
|
[tool.pyright.analysis]
|
|
52
56
|
extraPaths = ["."]
|
|
53
57
|
|
|
58
|
+
[tool.setuptools.packages.find]
|
|
59
|
+
include = ["ct_toolkit*"]
|
|
60
|
+
|
|
61
|
+
[tool.pytest.ini_options]
|
|
62
|
+
extraPaths = ["."]
|
|
63
|
+
|
|
54
64
|
[dependency-groups]
|
|
55
65
|
dev = [
|
|
56
66
|
"pytest-cov>=7.0.0",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|