ct-toolkit 0.3.6__tar.gz → 0.3.7__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.3.6 → ct_toolkit-0.3.7}/PKG-INFO +5 -3
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/README.md +1 -1
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/__init__.py +1 -1
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/cli.py +32 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/core/wrapper.py +40 -37
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/divergence/l3_icm.py +19 -16
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/identity/embedding.py +16 -14
- ct_toolkit-0.3.7/ct_toolkit/middleware/litellm.py +344 -0
- ct_toolkit-0.3.7/ct_toolkit/server.py +174 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit.egg-info/PKG-INFO +5 -3
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit.egg-info/SOURCES.txt +2 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit.egg-info/requires.txt +3 -1
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/pyproject.toml +4 -2
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/LICENSE +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/__main__.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/core/__init__.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/core/compatibility.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/core/compression_guard.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/core/exceptions.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/core/integrity.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/core/kernel.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/divergence/__init__.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/divergence/analysis.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/divergence/engine.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/divergence/l2_judge.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/divergence/loss.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/divergence/scheduler.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/endorsement/__init__.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/endorsement/probes/__init__.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/endorsement/reflective.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/identity/__init__.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/identity/templates/__init__.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/identity/templates/defense.yaml +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/identity/templates/finance.yaml +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/identity/templates/general.yaml +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/identity/templates/medical.yaml +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/kernels/__init__.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/kernels/default.yaml +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/kernels/defense.yaml +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/middleware/__init__.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/middleware/autogen.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/middleware/crewai.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/middleware/deepagents.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/middleware/langchain.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/provenance/__init__.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/provenance/log.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/provenance/vault/__init__.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/utils/__init__.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit/utils/logger.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit.egg-info/dependency_links.txt +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit.egg-info/entry_points.txt +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/ct_toolkit.egg-info/top_level.txt +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/setup.cfg +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/tests/test_analysis.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/tests/test_cli.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/tests/test_cli_extended.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/tests/test_deep_agents_integration.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/tests/test_loss.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.7}/tests/test_summarization_drift.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ct-toolkit
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.7
|
|
4
4
|
Summary: Computational Theseus Toolkit — Identity Continuity Guardrails for Agentic Systems
|
|
5
5
|
Author: Hakan Damar
|
|
6
6
|
License: Apache-2.0
|
|
@@ -9,7 +9,7 @@ Requires-Python: >=3.11
|
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Requires-Dist: instructor>=1.3.0
|
|
12
|
-
Requires-Dist:
|
|
12
|
+
Requires-Dist: litellm>=1.40.0
|
|
13
13
|
Requires-Dist: numpy>=1.26.0
|
|
14
14
|
Requires-Dist: pyyaml>=6.0
|
|
15
15
|
Requires-Dist: pydantic>=2.5.0
|
|
@@ -20,6 +20,8 @@ Requires-Dist: rich>=13.0.0
|
|
|
20
20
|
Requires-Dist: jinja2>=3.1.0
|
|
21
21
|
Requires-Dist: pyasn1>=0.6.3
|
|
22
22
|
Requires-Dist: typer[all]>=0.12.0
|
|
23
|
+
Requires-Dist: fastapi>=0.110.0
|
|
24
|
+
Requires-Dist: uvicorn>=0.29.0
|
|
23
25
|
Provides-Extra: ollama
|
|
24
26
|
Requires-Dist: ollama>=0.2.0; extra == "ollama"
|
|
25
27
|
Provides-Extra: langchain
|
|
@@ -116,7 +118,7 @@ ct-toolkit list-templates
|
|
|
116
118
|
|
|
117
119
|
| Metric | Status |
|
|
118
120
|
| :--------------- | :------------------------------------------------------ |
|
|
119
|
-
| **Tests** | ✅
|
|
121
|
+
| **Tests** | ✅ 281/292 passing (95% coverage) |
|
|
120
122
|
| **Last Phase** | ✅ Phase 6: Auditor Mode (Complete) |
|
|
121
123
|
| **Current Goal** | 🔶 Phase 7: Multi-Agent Synchronization (Integration) |
|
|
122
124
|
|
|
@@ -75,7 +75,7 @@ ct-toolkit list-templates
|
|
|
75
75
|
|
|
76
76
|
| Metric | Status |
|
|
77
77
|
| :--------------- | :------------------------------------------------------ |
|
|
78
|
-
| **Tests** | ✅
|
|
78
|
+
| **Tests** | ✅ 281/292 passing (95% coverage) |
|
|
79
79
|
| **Last Phase** | ✅ Phase 6: Auditor Mode (Complete) |
|
|
80
80
|
| **Current Goal** | 🔶 Phase 7: Multi-Agent Synchronization (Integration) |
|
|
81
81
|
|
|
@@ -17,6 +17,7 @@ from rich.table import Table
|
|
|
17
17
|
from ct_toolkit import TheseusWrapper, WrapperConfig, __version__
|
|
18
18
|
from ct_toolkit.divergence.l3_icm import ICMRunner
|
|
19
19
|
from ct_toolkit.core.kernel import ConstitutionalKernel
|
|
20
|
+
from ct_toolkit.server import start_server
|
|
20
21
|
|
|
21
22
|
app = typer.Typer(
|
|
22
23
|
help="Computational Theseus Toolkit — Identity Continuity Guardrails for Agentic Systems.",
|
|
@@ -135,6 +136,37 @@ def audit(
|
|
|
135
136
|
if not report.is_healthy:
|
|
136
137
|
raise typer.Exit(code=1)
|
|
137
138
|
|
|
139
|
+
@app.command()
|
|
140
|
+
def serve(
|
|
141
|
+
host: str = typer.Option("127.0.0.1", "--host", help="Host to bind the server to."),
|
|
142
|
+
port: int = typer.Option(8001, "--port", help="Port to bind the server to."),
|
|
143
|
+
kernel: str = typer.Option("general", "--kernel", help="Name of the Constitutional Kernel to use."),
|
|
144
|
+
template: str = typer.Option("general", "--template", help="Name of the Identity Template to use."),
|
|
145
|
+
vault_path: str = typer.Option("./ct_provenance.db", "--vault", help="Path to the provenance log database."),
|
|
146
|
+
judge_provider: str = typer.Option("openai", "--judge-provider", help="LLM provider for the L2 judge."),
|
|
147
|
+
judge_model: Optional[str] = typer.Option(None, "--judge-model", help="Model ID for the L2 judge."),
|
|
148
|
+
):
|
|
149
|
+
"""Start the CT-Toolkit Guardrail Server for LiteLLM integration."""
|
|
150
|
+
console.print(f"Starting Guardrail Server...")
|
|
151
|
+
console.print(f" - Kernel: [bold yellow]{kernel}[/bold yellow]")
|
|
152
|
+
console.print(f" - Template: [bold green]{template}[/bold green]")
|
|
153
|
+
console.print(f" - Bind: [bold cyan]{host}:{port}[/bold cyan]")
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
config = WrapperConfig(
|
|
157
|
+
kernel_name=kernel,
|
|
158
|
+
template=template,
|
|
159
|
+
vault_path=vault_path,
|
|
160
|
+
project_root=Path.cwd()
|
|
161
|
+
)
|
|
162
|
+
wrapper = TheseusWrapper(provider=judge_provider, config=config)
|
|
163
|
+
|
|
164
|
+
# Start the uvicorn server
|
|
165
|
+
start_server(wrapper=wrapper, host=host, port=port)
|
|
166
|
+
except Exception as e:
|
|
167
|
+
console.print(f"[bold red]Failed to start server:[/bold red] {e}")
|
|
168
|
+
raise typer.Exit(code=1)
|
|
169
|
+
|
|
138
170
|
@app.command()
|
|
139
171
|
def list_kernels():
|
|
140
172
|
"""List available Constitutional Kernels."""
|
|
@@ -25,9 +25,7 @@ import os
|
|
|
25
25
|
from dataclasses import dataclass, field
|
|
26
26
|
from pathlib import Path
|
|
27
27
|
from typing import Any, Iterator, Optional, Callable, Dict
|
|
28
|
-
import
|
|
29
|
-
from any_llm import AnyLLM
|
|
30
|
-
import glob
|
|
28
|
+
import litellm
|
|
31
29
|
|
|
32
30
|
from ct_toolkit.core.kernel import ConstitutionalKernel
|
|
33
31
|
from ct_toolkit.core.compatibility import CompatibilityLayer, CompatibilityResult
|
|
@@ -97,7 +95,7 @@ class TheseusWrapper:
|
|
|
97
95
|
Identity-continuity proxy wrapping the LLM API client.
|
|
98
96
|
|
|
99
97
|
Supported providers: openai (default), anthropic, ollama, google, etc.
|
|
100
|
-
(Any provider supported by
|
|
98
|
+
(Any provider supported by LiteLLM)
|
|
101
99
|
|
|
102
100
|
Usage:
|
|
103
101
|
from ct_toolkit import TheseusWrapper
|
|
@@ -133,7 +131,7 @@ class TheseusWrapper:
|
|
|
133
131
|
Optional if 'provider' is specified.
|
|
134
132
|
config: Optional WrapperConfig instance.
|
|
135
133
|
provider: Provider name (e.g. "openai", "anthropic", "ollama", "google", "cohere").
|
|
136
|
-
If provided,
|
|
134
|
+
If provided, LiteLLM will be used to initialize the client internally.
|
|
137
135
|
project_root: Path to the user's project root directory. Used to find a custom 'config/' folder.
|
|
138
136
|
Defaults to the current working directory.
|
|
139
137
|
"""
|
|
@@ -243,7 +241,7 @@ class TheseusWrapper:
|
|
|
243
241
|
return client.lower()
|
|
244
242
|
|
|
245
243
|
try:
|
|
246
|
-
# Check for
|
|
244
|
+
# Check for LiteLLM specific attributes if any
|
|
247
245
|
if hasattr(client, "provider") and isinstance(client.provider, str):
|
|
248
246
|
return client.provider
|
|
249
247
|
|
|
@@ -501,7 +499,7 @@ class TheseusWrapper:
|
|
|
501
499
|
model: str | None,
|
|
502
500
|
**kwargs: Any,
|
|
503
501
|
) -> Any:
|
|
504
|
-
"""Uses
|
|
502
|
+
"""Uses LiteLLM for unified provider calling."""
|
|
505
503
|
# --- Passive Context Compression Detection ---
|
|
506
504
|
if self._config.compression_passive_detection and self._shadow_history:
|
|
507
505
|
# Simple heuristic: message count dropped significantly
|
|
@@ -514,8 +512,8 @@ class TheseusWrapper:
|
|
|
514
512
|
|
|
515
513
|
# Update shadow history for next call
|
|
516
514
|
self._shadow_history = list(messages)
|
|
517
|
-
|
|
518
|
-
#
|
|
515
|
+
|
|
516
|
+
# LiteLLM handles different message formats internally
|
|
519
517
|
|
|
520
518
|
# Set default models per provider if not specified
|
|
521
519
|
if not model:
|
|
@@ -524,32 +522,29 @@ class TheseusWrapper:
|
|
|
524
522
|
elif self._provider == "ollama": model = "llama3"
|
|
525
523
|
else: model = "gpt-4o-mini"
|
|
526
524
|
|
|
527
|
-
#
|
|
525
|
+
# LiteLLM expects "provider/model" format for non-default providers
|
|
526
|
+
# Usually OpenAI models don't need a prefix.
|
|
528
527
|
full_model = model
|
|
529
|
-
if ":"
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
"model": full_model,
|
|
535
|
-
"messages": messages,
|
|
536
|
-
**kwargs
|
|
537
|
-
}
|
|
528
|
+
if ":" in model:
|
|
529
|
+
# If it has the legacy format "provider:model", convert it
|
|
530
|
+
full_model = model.replace(":", "/", 1)
|
|
531
|
+
elif self._provider not in ("openai", "unknown"):
|
|
532
|
+
full_model = f"{self._provider}/{model}"
|
|
538
533
|
|
|
539
534
|
# Extract connection info from client if possible
|
|
540
535
|
if hasattr(self._client, "base_url") and self._client.base_url:
|
|
541
|
-
|
|
536
|
+
kwargs["api_base"] = str(self._client.base_url)
|
|
542
537
|
if hasattr(self._client, "api_key") and self._client.api_key:
|
|
543
|
-
|
|
538
|
+
kwargs["api_key"] = self._client.api_key
|
|
544
539
|
|
|
545
540
|
# Transfer common client configurations if available
|
|
546
|
-
if hasattr(self._client, "timeout") and "timeout" not in
|
|
547
|
-
|
|
541
|
+
if hasattr(self._client, "timeout") and "timeout" not in kwargs:
|
|
542
|
+
kwargs["timeout"] = self._client.timeout
|
|
548
543
|
|
|
549
544
|
try:
|
|
550
|
-
return
|
|
545
|
+
return litellm.completion(model=full_model, messages=messages, **kwargs)
|
|
551
546
|
except Exception as e:
|
|
552
|
-
logger.error(f"
|
|
547
|
+
logger.error(f"litellm call failed: {e}")
|
|
553
548
|
raise
|
|
554
549
|
|
|
555
550
|
# ── Helper Methods ──────────────────────────────────────────────────────
|
|
@@ -603,25 +598,33 @@ class TheseusWrapper:
|
|
|
603
598
|
return messages
|
|
604
599
|
|
|
605
600
|
def _extract_content(self, raw: Any) -> str:
|
|
606
|
-
# Handle dict responses
|
|
601
|
+
# Handle dict responses
|
|
607
602
|
if isinstance(raw, dict):
|
|
608
603
|
if "choices" in raw and isinstance(raw["choices"], list) and raw["choices"]:
|
|
609
|
-
# OpenAI dict format
|
|
610
604
|
return raw["choices"][0].get("message", {}).get("content", "") or ""
|
|
611
605
|
if "message" in raw:
|
|
612
|
-
# Ollama
|
|
606
|
+
# Ollama dictate format
|
|
613
607
|
return raw["message"].get("content", "") or ""
|
|
614
608
|
return str(raw)
|
|
615
609
|
|
|
616
|
-
#
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
610
|
+
# LiteLLM ModelResponse (Standardized for all providers)
|
|
611
|
+
try:
|
|
612
|
+
if hasattr(raw, "choices") and raw.choices:
|
|
613
|
+
choice = raw.choices[0]
|
|
614
|
+
if hasattr(choice, "message") and choice.message:
|
|
615
|
+
return choice.message.content or ""
|
|
616
|
+
# Some older/alternative formats
|
|
617
|
+
if hasattr(choice, "text"):
|
|
618
|
+
return choice.text or ""
|
|
619
|
+
except Exception:
|
|
620
|
+
pass
|
|
621
|
+
|
|
622
|
+
# Fallback raw extraction for edge cases
|
|
623
|
+
if hasattr(raw, "content") and isinstance(raw.content, list): # Anthropic style
|
|
624
|
+
return raw.content[0].text if raw.content else ""
|
|
625
|
+
if hasattr(raw, "message"): # Ollama generic style
|
|
626
|
+
return getattr(raw.message, "content", "") or ""
|
|
627
|
+
|
|
625
628
|
return str(raw)
|
|
626
629
|
|
|
627
630
|
def _extract_model(self, raw: Any, fallback: str | None) -> str:
|
|
@@ -19,7 +19,7 @@ from dataclasses import dataclass, field
|
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
from typing import Any
|
|
21
21
|
|
|
22
|
-
import
|
|
22
|
+
import litellm
|
|
23
23
|
|
|
24
24
|
from ct_toolkit.utils.logger import get_logger
|
|
25
25
|
|
|
@@ -290,11 +290,14 @@ class ICMRunner:
|
|
|
290
290
|
def _call_model(self, prompt: str) -> str:
|
|
291
291
|
system = self._kernel.get_system_prompt_injection()
|
|
292
292
|
|
|
293
|
+
# LiteLLM format
|
|
293
294
|
full_model = self._model
|
|
294
|
-
if ":"
|
|
295
|
-
full_model =
|
|
295
|
+
if ":" in self._model:
|
|
296
|
+
full_model = self._model.replace(":", "/", 1)
|
|
297
|
+
elif self._provider not in ("openai", "unknown"):
|
|
298
|
+
full_model = f"{self._provider}/{self._model}"
|
|
296
299
|
|
|
297
|
-
|
|
300
|
+
kwargs: dict[str, Any] = {
|
|
298
301
|
"model": full_model,
|
|
299
302
|
"messages": [
|
|
300
303
|
{"role": "system", "content": system},
|
|
@@ -306,13 +309,20 @@ class ICMRunner:
|
|
|
306
309
|
|
|
307
310
|
# Extract connection info from client if possible
|
|
308
311
|
if hasattr(self._client, "base_url") and self._client.base_url:
|
|
309
|
-
|
|
312
|
+
kwargs["api_base"] = str(self._client.base_url)
|
|
310
313
|
if hasattr(self._client, "api_key") and self._client.api_key:
|
|
311
|
-
|
|
314
|
+
kwargs["api_key"] = self._client.api_key
|
|
312
315
|
|
|
313
316
|
try:
|
|
314
|
-
resp =
|
|
315
|
-
|
|
317
|
+
resp = litellm.completion(**kwargs)
|
|
318
|
+
|
|
319
|
+
# Extract content from LiteLLM response
|
|
320
|
+
if hasattr(resp, "choices") and resp.choices:
|
|
321
|
+
choice = resp.choices[0]
|
|
322
|
+
if hasattr(choice, "message") and choice.message:
|
|
323
|
+
return choice.message.content or ""
|
|
324
|
+
|
|
325
|
+
# Fallback
|
|
316
326
|
if isinstance(resp, dict):
|
|
317
327
|
if "choices" in resp and resp["choices"]:
|
|
318
328
|
return resp["choices"][0].get("message", {}).get("content", "")
|
|
@@ -320,16 +330,9 @@ class ICMRunner:
|
|
|
320
330
|
return resp["message"].get("content", "")
|
|
321
331
|
return str(resp)
|
|
322
332
|
|
|
323
|
-
if hasattr(resp, "choices"):
|
|
324
|
-
return resp.choices[0].message.content or ""
|
|
325
|
-
if hasattr(resp, "content") and isinstance(resp.content, list):
|
|
326
|
-
return resp.content[0].text if resp.content else ""
|
|
327
|
-
if hasattr(resp, "message"):
|
|
328
|
-
return resp.message.content or ""
|
|
329
|
-
|
|
330
333
|
return str(resp)
|
|
331
334
|
except Exception as e:
|
|
332
|
-
logger.error(f"ICM probe
|
|
335
|
+
logger.error(f"ICM probe litellm call failed: {e}")
|
|
333
336
|
raise
|
|
334
337
|
|
|
335
338
|
# ── Probe Loading ──────────────────────────────────────────────────────────
|
|
@@ -9,9 +9,7 @@ Step 3 MVP implementation:
|
|
|
9
9
|
- Each response embedding is calculated
|
|
10
10
|
- Cosine distance = 1 - cosine_similarity → divergence score
|
|
11
11
|
"""
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import json
|
|
12
|
+
import litellm
|
|
15
13
|
import hashlib
|
|
16
14
|
from pathlib import Path
|
|
17
15
|
from typing import Any
|
|
@@ -155,17 +153,21 @@ class IdentityEmbeddingLayer:
|
|
|
155
153
|
"""
|
|
156
154
|
if self._embedding_client is not None:
|
|
157
155
|
try:
|
|
158
|
-
#
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
156
|
+
# Prepare arguments for litellm.embedding
|
|
157
|
+
kwargs: dict[str, Any] = {
|
|
158
|
+
"model": self._embedding_model,
|
|
159
|
+
"input": [text]
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# Transfer client configuration if available
|
|
163
|
+
if hasattr(self._embedding_client, "base_url") and self._embedding_client.base_url:
|
|
164
|
+
kwargs["api_base"] = str(self._embedding_client.base_url)
|
|
165
|
+
if hasattr(self._embedding_client, "api_key") and self._embedding_client.api_key:
|
|
166
|
+
kwargs["api_key"] = self._embedding_client.api_key
|
|
167
|
+
|
|
168
|
+
response = litellm.embedding(**kwargs)
|
|
169
|
+
embedding = response.data[0]["embedding"]
|
|
170
|
+
return np.array(embedding, dtype=np.float32)
|
|
169
171
|
except Exception as e:
|
|
170
172
|
if self._strict_embedding:
|
|
171
173
|
raise RuntimeError(f"Embedding API failed: {e}") from e
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ct_toolkit.middleware.litellm
|
|
3
|
+
------------------------------
|
|
4
|
+
LiteLLM integration for identity-continuity guardrails.
|
|
5
|
+
|
|
6
|
+
Provides ``TheseusLiteLLMCallback`` which hooks into LiteLLM's CustomLogger
|
|
7
|
+
system to enforce Constitutional Kernel rules on every LLM call — regardless
|
|
8
|
+
of which provider or model is being used underneath.
|
|
9
|
+
|
|
10
|
+
This is the "Governance Proxy Middleware" pattern:
|
|
11
|
+
- Works with ANY provider LiteLLM supports (100+ models)
|
|
12
|
+
- No agent code changes required
|
|
13
|
+
- Pre-call: kernel validation → blocks axiomatic violations
|
|
14
|
+
- Post-call: divergence analysis + provenance logging
|
|
15
|
+
|
|
16
|
+
Optional dependency — requires ``pip install 'ct-toolkit[litellm]'``.
|
|
17
|
+
LiteLLM does NOT need to be installed for the rest of ct-toolkit to work.
|
|
18
|
+
|
|
19
|
+
## SDK usage (direct litellm calls)::
|
|
20
|
+
|
|
21
|
+
import litellm
|
|
22
|
+
from ct_toolkit import TheseusWrapper, WrapperConfig
|
|
23
|
+
from ct_toolkit.middleware.litellm import TheseusLiteLLMCallback
|
|
24
|
+
|
|
25
|
+
wrapper = TheseusWrapper(provider="openai", kernel_name="defense")
|
|
26
|
+
handler = TheseusLiteLLMCallback(wrapper)
|
|
27
|
+
|
|
28
|
+
litellm.callbacks = [handler]
|
|
29
|
+
|
|
30
|
+
# All litellm.completion() calls are now identity-protected
|
|
31
|
+
response = litellm.completion(
|
|
32
|
+
model="gpt-4o-mini",
|
|
33
|
+
messages=[{"role": "user", "content": "Hello!"}]
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
## LiteLLM Proxy (proxy_config.yaml)::
|
|
37
|
+
|
|
38
|
+
model_list:
|
|
39
|
+
- model_name: gpt-4o-mini
|
|
40
|
+
litellm_params:
|
|
41
|
+
model: gpt-4o-mini
|
|
42
|
+
|
|
43
|
+
litellm_settings:
|
|
44
|
+
callbacks: ct_toolkit.middleware.litellm.proxy_handler_instance
|
|
45
|
+
|
|
46
|
+
Compatible with litellm >= 1.40.0
|
|
47
|
+
"""
|
|
48
|
+
from __future__ import annotations
|
|
49
|
+
|
|
50
|
+
import time
|
|
51
|
+
from typing import Any, Optional
|
|
52
|
+
|
|
53
|
+
from ct_toolkit.core.wrapper import TheseusWrapper, WrapperConfig
|
|
54
|
+
from ct_toolkit.utils.logger import get_logger
|
|
55
|
+
|
|
56
|
+
logger = get_logger(__name__)
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
from litellm.integrations.custom_logger import CustomLogger
|
|
60
|
+
except ImportError as _exc:
|
|
61
|
+
raise ImportError(
|
|
62
|
+
"TheseusLiteLLMCallback requires litellm. "
|
|
63
|
+
"Install with: pip install 'litellm'"
|
|
64
|
+
) from _exc
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TheseusLiteLLMCallback(CustomLogger):
|
|
68
|
+
"""
|
|
69
|
+
LiteLLM CustomLogger that enforces CT-Toolkit identity guardrails.
|
|
70
|
+
|
|
71
|
+
Hooks:
|
|
72
|
+
async_pre_call_hook → Kernel validation (blocks axiomatic violations)
|
|
73
|
+
async_log_success_event → Divergence engine + Provenance Log
|
|
74
|
+
async_log_failure_event → Failure logging
|
|
75
|
+
log_success_event → Sync fallback for non-async callers
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(self, wrapper: TheseusWrapper) -> None:
|
|
79
|
+
super().__init__()
|
|
80
|
+
self.wrapper = wrapper
|
|
81
|
+
logger.info(
|
|
82
|
+
f"CT Toolkit | TheseusLiteLLMCallback initialized "
|
|
83
|
+
f"(kernel='{wrapper.kernel.name}', template='{wrapper._config.template}')"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# ── Pre-call: Axiomatic Kernel Validation ─────────────────────────────────
|
|
87
|
+
|
|
88
|
+
async def async_pre_call_hook(
|
|
89
|
+
self,
|
|
90
|
+
user_api_key_dict: Any,
|
|
91
|
+
cache: Any,
|
|
92
|
+
data: dict,
|
|
93
|
+
call_type: str,
|
|
94
|
+
) -> Optional[str]:
|
|
95
|
+
"""
|
|
96
|
+
Runs BEFORE the LLM API call.
|
|
97
|
+
Extracts user message and validates against Constitutional Kernel.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
None → call proceeds normally
|
|
101
|
+
str → LiteLLM returns this string as the response (call blocked)
|
|
102
|
+
"""
|
|
103
|
+
messages = data.get("messages", [])
|
|
104
|
+
user_text = _extract_user_text(messages)
|
|
105
|
+
|
|
106
|
+
if not user_text:
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
self.wrapper.validate_user_rule(user_text)
|
|
111
|
+
return None # All good, proceed
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.warning(
|
|
114
|
+
f"CT Toolkit | LiteLLM pre-call kernel violation: {e} | "
|
|
115
|
+
f"model={data.get('model', 'unknown')}"
|
|
116
|
+
)
|
|
117
|
+
# Return blocked message — LiteLLM formats this into a proper response
|
|
118
|
+
return (
|
|
119
|
+
f"[CT Toolkit BLOCKED] This request violates an identity constraint "
|
|
120
|
+
f"and cannot be processed. Reason: {e}"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# ── Post-call: Divergence Analysis + Provenance ───────────────────────────
|
|
124
|
+
|
|
125
|
+
async def async_log_success_event(
|
|
126
|
+
self,
|
|
127
|
+
kwargs: dict,
|
|
128
|
+
response_obj: Any,
|
|
129
|
+
start_time: float,
|
|
130
|
+
end_time: float,
|
|
131
|
+
) -> None:
|
|
132
|
+
"""
|
|
133
|
+
Runs AFTER a successful LLM call.
|
|
134
|
+
Runs Divergence Engine and writes to Provenance Log.
|
|
135
|
+
"""
|
|
136
|
+
try:
|
|
137
|
+
messages = kwargs.get("messages", [])
|
|
138
|
+
user_text = _extract_user_text(messages)
|
|
139
|
+
response_text = _extract_response_text(response_obj)
|
|
140
|
+
model = kwargs.get("model", "unknown")
|
|
141
|
+
|
|
142
|
+
if not response_text:
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
# Interaction count for Elasticity Scheduler
|
|
146
|
+
interaction_count = 0
|
|
147
|
+
if self.wrapper._config.log_requests:
|
|
148
|
+
interaction_count = self.wrapper._provenance_log.get_interaction_count(
|
|
149
|
+
template=self.wrapper._config.template,
|
|
150
|
+
kernel_name=self.wrapper.kernel.name,
|
|
151
|
+
model=model,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Run Divergence Engine (L1 → L2 → L3)
|
|
155
|
+
div_result = self.wrapper._run_divergence_engine(
|
|
156
|
+
message=user_text,
|
|
157
|
+
response=response_text,
|
|
158
|
+
interaction_count=interaction_count,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if getattr(div_result, 'cascade_blocked', False):
|
|
162
|
+
logger.warning(
|
|
163
|
+
f"CT Toolkit | cascade_blocked=True via LiteLLM callback. "
|
|
164
|
+
f"model={model} | score={div_result.l1_score:.4f} | "
|
|
165
|
+
f"Consider halting downstream agent calls."
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Provenance Log
|
|
169
|
+
if self.wrapper._config.log_requests:
|
|
170
|
+
elapsed_ms = (end_time - start_time) * 1000 if isinstance(start_time, float) else 0.0
|
|
171
|
+
self.wrapper._provenance_log.record(
|
|
172
|
+
request_text=user_text or "[LiteLLM Interaction]",
|
|
173
|
+
response_text=response_text,
|
|
174
|
+
divergence_score=div_result.l1_score,
|
|
175
|
+
metadata={
|
|
176
|
+
"middleware": "litellm",
|
|
177
|
+
"provider": kwargs.get("custom_llm_provider", "unknown"),
|
|
178
|
+
"model": model,
|
|
179
|
+
"elapsed_ms": round(elapsed_ms, 2),
|
|
180
|
+
"tier": div_result.tier.value,
|
|
181
|
+
"cascade_blocked": getattr(div_result, 'cascade_blocked', False),
|
|
182
|
+
"template": self.wrapper._config.template,
|
|
183
|
+
"kernel": self.wrapper.kernel.name,
|
|
184
|
+
},
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
logger.debug(
|
|
188
|
+
f"CT Toolkit | LiteLLM success | model={model} | "
|
|
189
|
+
f"divergence={div_result.l1_score:.4f} | tier={div_result.tier.value}"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
except Exception as e:
|
|
193
|
+
# Never crash the caller — log and continue
|
|
194
|
+
logger.error(f"CT Toolkit | LiteLLM callback error (success event): {e}")
|
|
195
|
+
|
|
196
|
+
async def async_log_failure_event(
|
|
197
|
+
self,
|
|
198
|
+
kwargs: dict,
|
|
199
|
+
response_obj: Any,
|
|
200
|
+
start_time: float,
|
|
201
|
+
end_time: float,
|
|
202
|
+
) -> None:
|
|
203
|
+
"""Logs LLM failures to provenance for audit trail completeness."""
|
|
204
|
+
try:
|
|
205
|
+
model = kwargs.get("model", "unknown")
|
|
206
|
+
exception = kwargs.get("exception", "unknown error")
|
|
207
|
+
logger.warning(f"CT Toolkit | LiteLLM failure | model={model} | error={exception}")
|
|
208
|
+
|
|
209
|
+
if self.wrapper._config.log_requests:
|
|
210
|
+
messages = kwargs.get("messages", [])
|
|
211
|
+
self.wrapper._provenance_log.record(
|
|
212
|
+
request_text=_extract_user_text(messages) or "[LiteLLM Failed Interaction]",
|
|
213
|
+
response_text=f"[ERROR: {exception}]",
|
|
214
|
+
divergence_score=None,
|
|
215
|
+
metadata={
|
|
216
|
+
"middleware": "litellm",
|
|
217
|
+
"model": model,
|
|
218
|
+
"status": "failed",
|
|
219
|
+
"template": self.wrapper._config.template,
|
|
220
|
+
"kernel": self.wrapper.kernel.name,
|
|
221
|
+
},
|
|
222
|
+
)
|
|
223
|
+
except Exception as e:
|
|
224
|
+
logger.error(f"CT Toolkit | LiteLLM callback error (failure event): {e}")
|
|
225
|
+
|
|
226
|
+
# ── Sync fallback ─────────────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
def log_success_event(
|
|
229
|
+
self,
|
|
230
|
+
kwargs: dict,
|
|
231
|
+
response_obj: Any,
|
|
232
|
+
start_time: float,
|
|
233
|
+
end_time: float,
|
|
234
|
+
) -> None:
|
|
235
|
+
"""Sync fallback — runs divergence + provenance for non-async callers."""
|
|
236
|
+
try:
|
|
237
|
+
messages = kwargs.get("messages", [])
|
|
238
|
+
user_text = _extract_user_text(messages)
|
|
239
|
+
response_text = _extract_response_text(response_obj)
|
|
240
|
+
model = kwargs.get("model", "unknown")
|
|
241
|
+
|
|
242
|
+
if not response_text:
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
div_result = self.wrapper._run_divergence_engine(
|
|
246
|
+
message=user_text,
|
|
247
|
+
response=response_text,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
if self.wrapper._config.log_requests:
|
|
251
|
+
self.wrapper._provenance_log.record(
|
|
252
|
+
request_text=user_text or "[LiteLLM Interaction]",
|
|
253
|
+
response_text=response_text,
|
|
254
|
+
divergence_score=div_result.l1_score,
|
|
255
|
+
metadata={
|
|
256
|
+
"middleware": "litellm",
|
|
257
|
+
"model": model,
|
|
258
|
+
"tier": div_result.tier.value,
|
|
259
|
+
"template": self.wrapper._config.template,
|
|
260
|
+
"kernel": self.wrapper.kernel.name,
|
|
261
|
+
},
|
|
262
|
+
)
|
|
263
|
+
except Exception as e:
|
|
264
|
+
logger.error(f"CT Toolkit | LiteLLM callback error (sync success): {e}")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# ── Proxy Mode: singleton instance for proxy_config.yaml ─────────────────────
|
|
268
|
+
|
|
269
|
+
def create_proxy_handler(
|
|
270
|
+
kernel_name: str = "default",
|
|
271
|
+
template: str = "general",
|
|
272
|
+
vault_path: str = "./ct_provenance.db",
|
|
273
|
+
enterprise_mode: bool = False,
|
|
274
|
+
) -> TheseusLiteLLMCallback:
|
|
275
|
+
"""
|
|
276
|
+
Factory for LiteLLM Proxy mode.
|
|
277
|
+
|
|
278
|
+
Usage in proxy_config.yaml::
|
|
279
|
+
|
|
280
|
+
litellm_settings:
|
|
281
|
+
callbacks: ct_toolkit.middleware.litellm.proxy_handler_instance
|
|
282
|
+
|
|
283
|
+
Or with custom config::
|
|
284
|
+
|
|
285
|
+
# custom_callbacks.py
|
|
286
|
+
from ct_toolkit.middleware.litellm import create_proxy_handler
|
|
287
|
+
proxy_handler_instance = create_proxy_handler(kernel_name="defense", template="defense")
|
|
288
|
+
"""
|
|
289
|
+
wrapper = TheseusWrapper(
|
|
290
|
+
provider="openai", # Proxy doesn't need a specific provider
|
|
291
|
+
config=WrapperConfig(
|
|
292
|
+
kernel_name=kernel_name,
|
|
293
|
+
template=template,
|
|
294
|
+
vault_path=vault_path,
|
|
295
|
+
enterprise_mode=enterprise_mode,
|
|
296
|
+
),
|
|
297
|
+
)
|
|
298
|
+
return TheseusLiteLLMCallback(wrapper)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# Default proxy singleton — works out-of-the-box for proxy_config.yaml
|
|
302
|
+
proxy_handler_instance = create_proxy_handler()
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
def _extract_user_text(messages: list[dict]) -> str:
|
|
308
|
+
"""Extract non-system message content from LiteLLM messages list."""
|
|
309
|
+
parts = []
|
|
310
|
+
for msg in messages:
|
|
311
|
+
if isinstance(msg, dict) and msg.get("role") != "system":
|
|
312
|
+
content = msg.get("content", "")
|
|
313
|
+
if isinstance(content, str):
|
|
314
|
+
parts.append(content)
|
|
315
|
+
elif isinstance(content, list):
|
|
316
|
+
# Handle multimodal content blocks
|
|
317
|
+
for block in content:
|
|
318
|
+
if isinstance(block, dict) and block.get("type") == "text":
|
|
319
|
+
parts.append(block.get("text", ""))
|
|
320
|
+
return " ".join(parts)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _extract_response_text(response_obj: Any) -> str:
|
|
324
|
+
"""Extract text content from LiteLLM ModelResponse."""
|
|
325
|
+
if response_obj is None:
|
|
326
|
+
return ""
|
|
327
|
+
try:
|
|
328
|
+
# Standard LiteLLM ModelResponse
|
|
329
|
+
if hasattr(response_obj, "choices"):
|
|
330
|
+
if not response_obj.choices:
|
|
331
|
+
return "" # Empty choices list
|
|
332
|
+
choice = response_obj.choices[0]
|
|
333
|
+
if hasattr(choice, "message") and choice.message:
|
|
334
|
+
return choice.message.content or ""
|
|
335
|
+
if hasattr(choice, "text"):
|
|
336
|
+
return choice.text or ""
|
|
337
|
+
# Dict format
|
|
338
|
+
if isinstance(response_obj, dict):
|
|
339
|
+
choices = response_obj.get("choices", [])
|
|
340
|
+
if choices:
|
|
341
|
+
return choices[0].get("message", {}).get("content", "")
|
|
342
|
+
except Exception:
|
|
343
|
+
pass
|
|
344
|
+
return str(response_obj)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ct_toolkit.server
|
|
3
|
+
-----------------
|
|
4
|
+
FastAPI server for LiteLLM Generic Guardrail API.
|
|
5
|
+
Exposes /guardrail/check to LiteLLM for pre-call and post-call validation.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
from fastapi import FastAPI, Body, HTTPException
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
import uvicorn
|
|
13
|
+
|
|
14
|
+
from ct_toolkit.core.wrapper import TheseusWrapper
|
|
15
|
+
from ct_toolkit.utils.logger import get_logger
|
|
16
|
+
|
|
17
|
+
logger = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
# This will be injected by the CLI when starting the server
|
|
20
|
+
_wrapper_instance: Optional[TheseusWrapper] = None
|
|
21
|
+
|
|
22
|
+
app = FastAPI(
|
|
23
|
+
title="CT-Toolkit Guardrail Server",
|
|
24
|
+
description="Identity Continuity Guardrails for Agentic Systems via LiteLLM Generic Guardrail API.",
|
|
25
|
+
version="1.0.0"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
class GuardrailRequest(BaseModel):
|
|
29
|
+
"""LiteLLM Generic Guardrail API Request Schema."""
|
|
30
|
+
texts: Optional[List[str]] = Field(None, description="Extracted text from the request.")
|
|
31
|
+
input_type: str = Field(..., description="Phase of the call: 'request' or 'response'.")
|
|
32
|
+
structured_messages: Optional[List[Dict[str, Any]]] = Field(None, description="Full messages in OpenAI format.")
|
|
33
|
+
images: Optional[List[str]] = None
|
|
34
|
+
tools: Optional[List[Any]] = None
|
|
35
|
+
tool_calls: Optional[List[Any]] = None
|
|
36
|
+
request_data: Optional[Dict[str, Any]] = None
|
|
37
|
+
litellm_call_id: Optional[str] = None
|
|
38
|
+
litellm_trace_id: Optional[str] = None
|
|
39
|
+
|
|
40
|
+
class GuardrailResponse(BaseModel):
|
|
41
|
+
"""LiteLLM Generic Guardrail API Response Schema."""
|
|
42
|
+
action: str = Field(..., description="'BLOCKED' | 'NONE' | 'GUARDRAIL_INTERVENED'")
|
|
43
|
+
blocked_reason: Optional[str] = Field(None, description="Reason for denial (required if BLOCKED).")
|
|
44
|
+
texts: Optional[List[str]] = Field(None, description="Optional array of modified text strings.")
|
|
45
|
+
|
|
46
|
+
@app.post("/guardrail/check", response_model=GuardrailResponse)
|
|
47
|
+
async def check_guardrail(request: GuardrailRequest):
|
|
48
|
+
"""
|
|
49
|
+
Main endpoint for LiteLLM Generic Guardrail API.
|
|
50
|
+
"""
|
|
51
|
+
global _wrapper_instance
|
|
52
|
+
if not _wrapper_instance:
|
|
53
|
+
raise HTTPException(status_code=500, detail="Server not initialized with a Constitutional Kernel.")
|
|
54
|
+
|
|
55
|
+
logger.info(f"Received {request.input_type} check (call_id={request.litellm_call_id})")
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
if request.input_type == "request":
|
|
59
|
+
return await _handle_pre_call(request)
|
|
60
|
+
elif request.input_type == "response":
|
|
61
|
+
return await _handle_post_call(request)
|
|
62
|
+
else:
|
|
63
|
+
logger.warning(f"Unknown input_type: {request.input_type}")
|
|
64
|
+
return GuardrailResponse(action="NONE")
|
|
65
|
+
except Exception as e:
|
|
66
|
+
logger.error(f"Error during guardrail check: {e}")
|
|
67
|
+
# Default to NONE to avoid breaking production flows on server error
|
|
68
|
+
return GuardrailResponse(action="NONE")
|
|
69
|
+
|
|
70
|
+
async def _handle_pre_call(request: GuardrailRequest) -> GuardrailResponse:
|
|
71
|
+
"""Validate user message against Constitutional Kernel before LLM call."""
|
|
72
|
+
user_text = ""
|
|
73
|
+
if request.texts is not None:
|
|
74
|
+
user_text = " ".join(request.texts)
|
|
75
|
+
elif request.structured_messages is not None:
|
|
76
|
+
user_text = _extract_user_text(request.structured_messages)
|
|
77
|
+
|
|
78
|
+
if user_text:
|
|
79
|
+
try:
|
|
80
|
+
_wrapper_instance.validate_user_rule(user_text)
|
|
81
|
+
return GuardrailResponse(action="NONE")
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logger.warning(f"Pre-call kernel violation: {e}")
|
|
84
|
+
return GuardrailResponse(
|
|
85
|
+
action="BLOCKED",
|
|
86
|
+
blocked_reason=f"Identity constraint violation: {e}"
|
|
87
|
+
)
|
|
88
|
+
return GuardrailResponse(action="NONE")
|
|
89
|
+
|
|
90
|
+
async def _handle_post_call(request: GuardrailRequest) -> GuardrailResponse:
|
|
91
|
+
"""Analyze LLM response for divergence after call."""
|
|
92
|
+
user_text = ""
|
|
93
|
+
if request.structured_messages is not None:
|
|
94
|
+
user_text = _extract_user_text(request.structured_messages)
|
|
95
|
+
|
|
96
|
+
response_text = ""
|
|
97
|
+
if request.texts is not None:
|
|
98
|
+
response_text = " ".join(request.texts)
|
|
99
|
+
|
|
100
|
+
if not response_text:
|
|
101
|
+
return GuardrailResponse(action="NONE")
|
|
102
|
+
|
|
103
|
+
model = "unknown"
|
|
104
|
+
if request.request_data:
|
|
105
|
+
# LiteLLM might not send the model directly here, but we can try to extract from metadata
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
interaction_count = 0
|
|
109
|
+
if _wrapper_instance._config.log_requests:
|
|
110
|
+
interaction_count = _wrapper_instance._provenance_log.get_interaction_count(
|
|
111
|
+
template=_wrapper_instance._config.template,
|
|
112
|
+
kernel_name=_wrapper_instance.kernel.name,
|
|
113
|
+
model=model,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Run Divergence Engine
|
|
117
|
+
div_result = _wrapper_instance._run_divergence_engine(
|
|
118
|
+
message=user_text,
|
|
119
|
+
response=response_text,
|
|
120
|
+
interaction_count=interaction_count,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Record to Provenance Log
|
|
124
|
+
if _wrapper_instance._config.log_requests:
|
|
125
|
+
_wrapper_instance._provenance_log.record(
|
|
126
|
+
request_text=user_text or "[Generic Guardrail Interaction]",
|
|
127
|
+
response_text=response_text,
|
|
128
|
+
divergence_score=div_result.l1_score,
|
|
129
|
+
metadata={
|
|
130
|
+
"middleware": "generic_guardrail_api",
|
|
131
|
+
"model": model,
|
|
132
|
+
"tier": div_result.tier.value,
|
|
133
|
+
"template": _wrapper_instance._config.template,
|
|
134
|
+
"kernel": _wrapper_instance.kernel.name,
|
|
135
|
+
"litellm_call_id": request.litellm_call_id,
|
|
136
|
+
},
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Handle cascade_blocked
|
|
140
|
+
if getattr(div_result, 'cascade_blocked', False):
|
|
141
|
+
return GuardrailResponse(
|
|
142
|
+
action="BLOCKED",
|
|
143
|
+
blocked_reason=f"Identity drift detected (score={div_result.l1_score:.4f})"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return GuardrailResponse(action="NONE")
|
|
147
|
+
|
|
148
|
+
def _extract_user_text(messages: List[Dict[str, Any]]) -> str:
|
|
149
|
+
"""Extract non-system message content from messages list."""
|
|
150
|
+
parts = []
|
|
151
|
+
for msg in messages:
|
|
152
|
+
if msg.get("role") != "system":
|
|
153
|
+
content = msg.get("content", "")
|
|
154
|
+
if isinstance(content, str):
|
|
155
|
+
parts.append(content)
|
|
156
|
+
return " ".join(parts)
|
|
157
|
+
|
|
158
|
+
def _extract_response_text(response_obj: Any) -> str:
|
|
159
|
+
"""Extract text content from LiteLLM response object."""
|
|
160
|
+
try:
|
|
161
|
+
if isinstance(response_obj, dict):
|
|
162
|
+
choices = response_obj.get("choices", [])
|
|
163
|
+
if choices:
|
|
164
|
+
return choices[0].get("message", {}).get("content", "")
|
|
165
|
+
except Exception:
|
|
166
|
+
pass
|
|
167
|
+
return ""
|
|
168
|
+
|
|
169
|
+
def start_server(wrapper: TheseusWrapper, host: str, port: int):
|
|
170
|
+
"""Inject wrapper and start uvicorn server."""
|
|
171
|
+
global _wrapper_instance
|
|
172
|
+
_wrapper_instance = wrapper
|
|
173
|
+
logger.info(f"Starting Guardrail Server on {host}:{port}")
|
|
174
|
+
uvicorn.run(app, host=host, port=port)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ct-toolkit
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.7
|
|
4
4
|
Summary: Computational Theseus Toolkit — Identity Continuity Guardrails for Agentic Systems
|
|
5
5
|
Author: Hakan Damar
|
|
6
6
|
License: Apache-2.0
|
|
@@ -9,7 +9,7 @@ Requires-Python: >=3.11
|
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Requires-Dist: instructor>=1.3.0
|
|
12
|
-
Requires-Dist:
|
|
12
|
+
Requires-Dist: litellm>=1.40.0
|
|
13
13
|
Requires-Dist: numpy>=1.26.0
|
|
14
14
|
Requires-Dist: pyyaml>=6.0
|
|
15
15
|
Requires-Dist: pydantic>=2.5.0
|
|
@@ -20,6 +20,8 @@ Requires-Dist: rich>=13.0.0
|
|
|
20
20
|
Requires-Dist: jinja2>=3.1.0
|
|
21
21
|
Requires-Dist: pyasn1>=0.6.3
|
|
22
22
|
Requires-Dist: typer[all]>=0.12.0
|
|
23
|
+
Requires-Dist: fastapi>=0.110.0
|
|
24
|
+
Requires-Dist: uvicorn>=0.29.0
|
|
23
25
|
Provides-Extra: ollama
|
|
24
26
|
Requires-Dist: ollama>=0.2.0; extra == "ollama"
|
|
25
27
|
Provides-Extra: langchain
|
|
@@ -116,7 +118,7 @@ ct-toolkit list-templates
|
|
|
116
118
|
|
|
117
119
|
| Metric | Status |
|
|
118
120
|
| :--------------- | :------------------------------------------------------ |
|
|
119
|
-
| **Tests** | ✅
|
|
121
|
+
| **Tests** | ✅ 281/292 passing (95% coverage) |
|
|
120
122
|
| **Last Phase** | ✅ Phase 6: Auditor Mode (Complete) |
|
|
121
123
|
| **Current Goal** | 🔶 Phase 7: Multi-Agent Synchronization (Integration) |
|
|
122
124
|
|
|
@@ -4,6 +4,7 @@ pyproject.toml
|
|
|
4
4
|
ct_toolkit/__init__.py
|
|
5
5
|
ct_toolkit/__main__.py
|
|
6
6
|
ct_toolkit/cli.py
|
|
7
|
+
ct_toolkit/server.py
|
|
7
8
|
ct_toolkit.egg-info/PKG-INFO
|
|
8
9
|
ct_toolkit.egg-info/SOURCES.txt
|
|
9
10
|
ct_toolkit.egg-info/dependency_links.txt
|
|
@@ -42,6 +43,7 @@ ct_toolkit/middleware/autogen.py
|
|
|
42
43
|
ct_toolkit/middleware/crewai.py
|
|
43
44
|
ct_toolkit/middleware/deepagents.py
|
|
44
45
|
ct_toolkit/middleware/langchain.py
|
|
46
|
+
ct_toolkit/middleware/litellm.py
|
|
45
47
|
ct_toolkit/provenance/__init__.py
|
|
46
48
|
ct_toolkit/provenance/log.py
|
|
47
49
|
ct_toolkit/provenance/vault/__init__.py
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
instructor>=1.3.0
|
|
2
|
-
|
|
2
|
+
litellm>=1.40.0
|
|
3
3
|
numpy>=1.26.0
|
|
4
4
|
pyyaml>=6.0
|
|
5
5
|
pydantic>=2.5.0
|
|
@@ -10,6 +10,8 @@ rich>=13.0.0
|
|
|
10
10
|
jinja2>=3.1.0
|
|
11
11
|
pyasn1>=0.6.3
|
|
12
12
|
typer[all]>=0.12.0
|
|
13
|
+
fastapi>=0.110.0
|
|
14
|
+
uvicorn>=0.29.0
|
|
13
15
|
|
|
14
16
|
[autogen]
|
|
15
17
|
pyautogen>=0.4.0
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ct-toolkit"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.7"
|
|
8
8
|
description = "Computational Theseus Toolkit — Identity Continuity Guardrails for Agentic Systems"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -14,7 +14,7 @@ keywords = ["llm", "ai-safety", "identity-continuity", "guardrails", "alignment"
|
|
|
14
14
|
|
|
15
15
|
dependencies = [
|
|
16
16
|
"instructor>=1.3.0",
|
|
17
|
-
"
|
|
17
|
+
"litellm>=1.40.0",
|
|
18
18
|
"numpy>=1.26.0",
|
|
19
19
|
"pyyaml>=6.0",
|
|
20
20
|
"pydantic>=2.5.0",
|
|
@@ -25,6 +25,8 @@ dependencies = [
|
|
|
25
25
|
"jinja2>=3.1.0",
|
|
26
26
|
"pyasn1>=0.6.3",
|
|
27
27
|
"typer[all]>=0.12.0",
|
|
28
|
+
"fastapi>=0.110.0",
|
|
29
|
+
"uvicorn>=0.29.0",
|
|
28
30
|
]
|
|
29
31
|
|
|
30
32
|
[project.optional-dependencies]
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|