ct-toolkit 0.2.6__tar.gz → 0.2.9__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.2.6 → ct_toolkit-0.2.9}/PKG-INFO +1 -1
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/core/exceptions.py +15 -0
- ct_toolkit-0.2.9/ct_toolkit/core/integrity.py +56 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/core/wrapper.py +98 -5
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/divergence/engine.py +4 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/divergence/l2_judge.py +20 -9
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/divergence/l3_icm.py +18 -7
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/identity/embedding.py +38 -22
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit.egg-info/PKG-INFO +1 -1
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit.egg-info/SOURCES.txt +1 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/pyproject.toml +7 -1
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/LICENSE +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/README.md +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/__init__.py +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/core/__init__.py +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/core/compatibility.py +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/core/kernel.py +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/divergence/__init__.py +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/divergence/analysis.py +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/divergence/loss.py +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/divergence/scheduler.py +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/endorsement/__init__.py +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/endorsement/probes/__init__.py +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/endorsement/reflective.py +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/identity/__init__.py +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/identity/templates/__init__.py +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/identity/templates/defense.yaml +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/identity/templates/finance.yaml +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/identity/templates/general.yaml +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/identity/templates/medical.yaml +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/kernels/__init__.py +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/kernels/default.yaml +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/kernels/defense.yaml +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/middleware/__init__.py +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/middleware/autogen.py +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/middleware/crewai.py +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/middleware/deepagents.py +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/middleware/langchain.py +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/provenance/__init__.py +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/provenance/log.py +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/provenance/vault/__init__.py +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/utils/__init__.py +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/utils/logger.py +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit.egg-info/dependency_links.txt +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit.egg-info/requires.txt +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit.egg-info/top_level.txt +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/setup.cfg +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/tests/test_analysis.py +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/tests/test_deep_agents_integration.py +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/tests/test_loss.py +0 -0
- {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/tests/test_summarization_drift.py +0 -0
|
@@ -103,3 +103,18 @@ class ChainIntegrityError(ProvenanceError):
|
|
|
103
103
|
|
|
104
104
|
class VaultError(ProvenanceError):
|
|
105
105
|
"""Vault connection or authorization error."""
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ── File Integrity Errors ────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
class ConfigurationTamperingError(CTToolkitError):
|
|
111
|
+
"""
|
|
112
|
+
Raised when a configuration file has been modified after initialization.
|
|
113
|
+
Indicates a potential security breach or unintended change.
|
|
114
|
+
"""
|
|
115
|
+
def __init__(self, file_path: str):
|
|
116
|
+
self.file_path = file_path
|
|
117
|
+
super().__init__(
|
|
118
|
+
f"SECURITY ALERT: Tampering detected in configuration file: {file_path}. "
|
|
119
|
+
"Execution halted to prevent security bypass."
|
|
120
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ct_toolkit.core.integrity
|
|
3
|
+
-------------------------
|
|
4
|
+
Cryptographic integrity monitoring for configuration files.
|
|
5
|
+
"""
|
|
6
|
+
import hashlib
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from ct_toolkit.utils.logger import get_logger
|
|
9
|
+
from ct_toolkit.core.exceptions import ConfigurationTamperingError
|
|
10
|
+
|
|
11
|
+
logger = get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
class IntegrityMonitor:
|
|
14
|
+
"""
|
|
15
|
+
Monitors the cryptographic integrity of CT configuration files.
|
|
16
|
+
Prevents runtime tampering by agents or malicious actors.
|
|
17
|
+
"""
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self._file_hashes: dict[str, str] = {}
|
|
20
|
+
|
|
21
|
+
def _calculate_hash(self, file_path: Path) -> str:
|
|
22
|
+
"""Calculates the SHA-256 hash of a given file."""
|
|
23
|
+
hasher = hashlib.sha256()
|
|
24
|
+
try:
|
|
25
|
+
with open(file_path, 'rb') as f:
|
|
26
|
+
buf = f.read()
|
|
27
|
+
hasher.update(buf)
|
|
28
|
+
return hasher.hexdigest()
|
|
29
|
+
except FileNotFoundError:
|
|
30
|
+
logger.error(f"Integrity check failed: File not found at {file_path}")
|
|
31
|
+
raise
|
|
32
|
+
|
|
33
|
+
def register_file(self, file_path: Path) -> None:
|
|
34
|
+
"""Registers a file and stores its initial hash."""
|
|
35
|
+
if not file_path.is_file():
|
|
36
|
+
logger.warning(f"Integrity check: Cannot register non-file path: {file_path}")
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
file_hash = self._calculate_hash(file_path)
|
|
40
|
+
self._file_hashes[str(file_path)] = file_hash
|
|
41
|
+
logger.debug(f"Registered integrity hash for {file_path.name}")
|
|
42
|
+
|
|
43
|
+
def verify_integrity(self) -> None:
|
|
44
|
+
"""
|
|
45
|
+
Verifies that none of the registered files have been modified.
|
|
46
|
+
Raises ConfigurationTamperingError if a mismatch is found.
|
|
47
|
+
"""
|
|
48
|
+
if not self._file_hashes:
|
|
49
|
+
return # Nothing to verify
|
|
50
|
+
|
|
51
|
+
for path_str, original_hash in self._file_hashes.items():
|
|
52
|
+
current_hash = self._calculate_hash(Path(path_str))
|
|
53
|
+
if current_hash != original_hash:
|
|
54
|
+
raise ConfigurationTamperingError(path_str)
|
|
55
|
+
logger.debug("Integrity verification passed for all registered files.")
|
|
56
|
+
|
|
@@ -21,15 +21,18 @@ from __future__ import annotations
|
|
|
21
21
|
|
|
22
22
|
import time
|
|
23
23
|
import hashlib
|
|
24
|
+
import os
|
|
24
25
|
from dataclasses import dataclass, field
|
|
25
26
|
from pathlib import Path
|
|
26
27
|
from typing import Any, Iterator
|
|
27
28
|
import any_llm
|
|
28
29
|
from any_llm import AnyLLM
|
|
30
|
+
import glob
|
|
29
31
|
|
|
30
32
|
from ct_toolkit.core.kernel import ConstitutionalKernel
|
|
31
33
|
from ct_toolkit.core.compatibility import CompatibilityLayer, CompatibilityResult
|
|
32
34
|
from ct_toolkit.core.exceptions import CTToolkitError, MissingClientError
|
|
35
|
+
from ct_toolkit.core.integrity import IntegrityMonitor
|
|
33
36
|
from ct_toolkit.divergence.scheduler import RiskProfile
|
|
34
37
|
from ct_toolkit.utils.logger import get_logger
|
|
35
38
|
|
|
@@ -39,6 +42,7 @@ logger = get_logger(__name__)
|
|
|
39
42
|
@dataclass
|
|
40
43
|
class WrapperConfig:
|
|
41
44
|
"""TheseusWrapper configuration."""
|
|
45
|
+
project_root: str | Path | None = None
|
|
42
46
|
kernel_path: str | Path | None = None
|
|
43
47
|
template: str = "general"
|
|
44
48
|
kernel_name: str = "default"
|
|
@@ -108,6 +112,7 @@ class TheseusWrapper:
|
|
|
108
112
|
kernel_path: str | Path | None = None,
|
|
109
113
|
template: str = "general",
|
|
110
114
|
kernel_name: str = "default",
|
|
115
|
+
project_root: str | Path | None = None,
|
|
111
116
|
) -> None:
|
|
112
117
|
"""
|
|
113
118
|
Initializes TheseusWrapper.
|
|
@@ -118,11 +123,14 @@ class TheseusWrapper:
|
|
|
118
123
|
config: Optional WrapperConfig instance.
|
|
119
124
|
provider: Provider name (e.g. "openai", "anthropic", "ollama", "google", "cohere").
|
|
120
125
|
If provided, 'any-llm' will be used to initialize the client internally.
|
|
126
|
+
project_root: Path to the user's project root directory. Used to find a custom 'config/' folder.
|
|
127
|
+
Defaults to the current working directory.
|
|
121
128
|
"""
|
|
122
129
|
self._config = config or WrapperConfig(
|
|
123
130
|
kernel_path=kernel_path,
|
|
124
131
|
template=template,
|
|
125
132
|
kernel_name=kernel_name,
|
|
133
|
+
project_root=project_root,
|
|
126
134
|
)
|
|
127
135
|
|
|
128
136
|
if client is None and provider is None:
|
|
@@ -131,6 +139,11 @@ class TheseusWrapper:
|
|
|
131
139
|
|
|
132
140
|
self._client = client
|
|
133
141
|
self._provider = provider or self._detect_provider(client)
|
|
142
|
+
self._project_root = (
|
|
143
|
+
Path(self._config.project_root)
|
|
144
|
+
if self._config.project_root
|
|
145
|
+
else Path(os.getcwd())
|
|
146
|
+
)
|
|
134
147
|
self._kernel = self._load_kernel()
|
|
135
148
|
|
|
136
149
|
# If a parent kernel exists, merge it into our own kernel as axiomatic constraints
|
|
@@ -146,8 +159,57 @@ class TheseusWrapper:
|
|
|
146
159
|
self._divergence_engine = self._init_divergence_engine()
|
|
147
160
|
self._last_model: str = "unknown"
|
|
148
161
|
|
|
162
|
+
# ── Integrity Monitoring ──
|
|
163
|
+
self._integrity_monitor = IntegrityMonitor()
|
|
164
|
+
self._register_monitored_files()
|
|
165
|
+
|
|
149
166
|
self._log_init()
|
|
150
167
|
|
|
168
|
+
def _register_monitored_files(self):
|
|
169
|
+
"""
|
|
170
|
+
Finds and registers all critical configuration files for integrity monitoring,
|
|
171
|
+
including both built-in package files and user-provided custom configs.
|
|
172
|
+
"""
|
|
173
|
+
logger.debug("Registering configuration files for integrity monitoring.")
|
|
174
|
+
|
|
175
|
+
# 1. Scan built-in package files
|
|
176
|
+
try:
|
|
177
|
+
from importlib.resources import files
|
|
178
|
+
package_root = files("ct_toolkit")
|
|
179
|
+
|
|
180
|
+
internal_patterns = [
|
|
181
|
+
"kernels/**/*.yaml",
|
|
182
|
+
"identity/templates/**/*.yaml",
|
|
183
|
+
"endorsement/probes/**/*.json",
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
for pattern in internal_patterns:
|
|
187
|
+
for file_path in package_root.rglob(pattern):
|
|
188
|
+
if file_path.is_file():
|
|
189
|
+
self._integrity_monitor.register_file(file_path)
|
|
190
|
+
|
|
191
|
+
except (ImportError, ModuleNotFoundError):
|
|
192
|
+
# Fallback for older Python or different environments
|
|
193
|
+
package_root = Path(__file__).parent.parent
|
|
194
|
+
# This is less ideal as it might find non-package files, but it's a
|
|
195
|
+
# reasonable fallback for development environments.
|
|
196
|
+
for pattern in internal_patterns:
|
|
197
|
+
for file_path in package_root.rglob(pattern):
|
|
198
|
+
if file_path.is_file():
|
|
199
|
+
self._integrity_monitor.register_file(file_path)
|
|
200
|
+
|
|
201
|
+
# 2. Scan user's custom config directory
|
|
202
|
+
if self._project_root:
|
|
203
|
+
user_config_dir = self._project_root / "config"
|
|
204
|
+
if user_config_dir.is_dir():
|
|
205
|
+
logger.debug(f"Scanning user config directory for integrity monitoring: {user_config_dir}")
|
|
206
|
+
user_patterns = ["*_kernel.yaml", "*_identity.yaml", "*_probes.json", "*.yaml", "*.json"]
|
|
207
|
+
for pattern in user_patterns:
|
|
208
|
+
for file_path in user_config_dir.rglob(pattern):
|
|
209
|
+
if file_path.is_file():
|
|
210
|
+
self._integrity_monitor.register_file(file_path)
|
|
211
|
+
|
|
212
|
+
|
|
151
213
|
# ── Factory / Init ─────────────────────────────────────────────────────────
|
|
152
214
|
|
|
153
215
|
def _detect_provider(self, client: Any) -> str:
|
|
@@ -180,30 +242,40 @@ class TheseusWrapper:
|
|
|
180
242
|
return "unknown"
|
|
181
243
|
|
|
182
244
|
def _load_kernel(self) -> ConstitutionalKernel:
|
|
245
|
+
# 1. Check for explicit path
|
|
183
246
|
if self._config.kernel_path:
|
|
247
|
+
logger.debug(f"Loading kernel from explicit path: {self._config.kernel_path}")
|
|
184
248
|
return ConstitutionalKernel.from_yaml(self._config.kernel_path)
|
|
185
|
-
|
|
186
|
-
import os
|
|
249
|
+
|
|
187
250
|
safe_name = os.path.basename(self._config.kernel_name)
|
|
188
251
|
|
|
189
|
-
#
|
|
252
|
+
# 2. Check for user-defined config directory
|
|
253
|
+
if self._project_root:
|
|
254
|
+
user_config_path = self._project_root / "config" / f"{safe_name}.yaml"
|
|
255
|
+
if user_config_path.exists():
|
|
256
|
+
logger.debug(f"Loading kernel from user config: {user_config_path}")
|
|
257
|
+
return ConstitutionalKernel.from_yaml(user_config_path)
|
|
258
|
+
|
|
259
|
+
# 3. Load built-in kernel using importlib.resources
|
|
190
260
|
try:
|
|
191
261
|
from importlib.resources import files
|
|
192
262
|
kernel_resource = files("ct_toolkit.kernels").joinpath(f"{safe_name}.yaml")
|
|
193
263
|
if kernel_resource.is_file():
|
|
264
|
+
logger.debug(f"Loading built-in kernel: {safe_name}.yaml")
|
|
194
265
|
return ConstitutionalKernel.from_yaml(kernel_resource)
|
|
195
266
|
except (ImportError, FileNotFoundError):
|
|
196
|
-
# Fallback
|
|
267
|
+
# Fallback for older python versions
|
|
197
268
|
kernel_path = (
|
|
198
269
|
Path(__file__).parent.parent
|
|
199
270
|
/ "kernels"
|
|
200
271
|
/ f"{safe_name}.yaml"
|
|
201
272
|
)
|
|
202
273
|
if kernel_path.exists():
|
|
274
|
+
logger.debug(f"Loading built-in kernel (fallback): {kernel_path}")
|
|
203
275
|
return ConstitutionalKernel.from_yaml(kernel_path)
|
|
204
276
|
|
|
205
277
|
logger.warning(
|
|
206
|
-
f"Kernel '{self._config.kernel_name}' not found
|
|
278
|
+
f"Kernel '{self._config.kernel_name}' not found in user config or built-ins. Using default."
|
|
207
279
|
)
|
|
208
280
|
return ConstitutionalKernel.default()
|
|
209
281
|
|
|
@@ -232,6 +304,7 @@ class TheseusWrapper:
|
|
|
232
304
|
template=self._config.template,
|
|
233
305
|
embedding_client=emb_client,
|
|
234
306
|
embedding_model=self._config.embedding_model,
|
|
307
|
+
project_root=self._project_root,
|
|
235
308
|
)
|
|
236
309
|
|
|
237
310
|
def _init_divergence_engine(self) -> Any:
|
|
@@ -263,6 +336,7 @@ class TheseusWrapper:
|
|
|
263
336
|
l3_threshold=self._config.divergence_l3_threshold,
|
|
264
337
|
enterprise_mode=self._config.enterprise_mode,
|
|
265
338
|
scheduler=scheduler,
|
|
339
|
+
project_root=self._project_root,
|
|
266
340
|
provenance_log=self._provenance_log,
|
|
267
341
|
)
|
|
268
342
|
|
|
@@ -299,6 +373,9 @@ class TheseusWrapper:
|
|
|
299
373
|
system: Additional system prompt (appended to kernel)
|
|
300
374
|
history: Conversation history [{"role": ..., "content": ...}]
|
|
301
375
|
"""
|
|
376
|
+
# --- Pre-flight Checks ---
|
|
377
|
+
self._integrity_monitor.verify_integrity()
|
|
378
|
+
|
|
302
379
|
composed_system = self._compose_system_prompt(system)
|
|
303
380
|
messages = self._build_messages(message, history, composed_system)
|
|
304
381
|
|
|
@@ -416,6 +493,10 @@ class TheseusWrapper:
|
|
|
416
493
|
if hasattr(self._client, "api_key") and self._client.api_key:
|
|
417
494
|
any_llm_kwargs["api_key"] = self._client.api_key
|
|
418
495
|
|
|
496
|
+
# Transfer common client configurations if available
|
|
497
|
+
if hasattr(self._client, "timeout") and "timeout" not in any_llm_kwargs:
|
|
498
|
+
any_llm_kwargs["timeout"] = self._client.timeout
|
|
499
|
+
|
|
419
500
|
try:
|
|
420
501
|
return any_llm.completion(**any_llm_kwargs)
|
|
421
502
|
except Exception as e:
|
|
@@ -473,6 +554,16 @@ class TheseusWrapper:
|
|
|
473
554
|
return messages
|
|
474
555
|
|
|
475
556
|
def _extract_content(self, raw: Any) -> str:
|
|
557
|
+
# Handle dict responses (e.g. from some mockers or raw APIs)
|
|
558
|
+
if isinstance(raw, dict):
|
|
559
|
+
if "choices" in raw and isinstance(raw["choices"], list) and raw["choices"]:
|
|
560
|
+
# OpenAI dict format
|
|
561
|
+
return raw["choices"][0].get("message", {}).get("content", "") or ""
|
|
562
|
+
if "message" in raw:
|
|
563
|
+
# Ollama dict format
|
|
564
|
+
return raw["message"].get("content", "") or ""
|
|
565
|
+
return str(raw)
|
|
566
|
+
|
|
476
567
|
# OpenAI
|
|
477
568
|
if hasattr(raw, "choices"):
|
|
478
569
|
return raw.choices[0].message.content or ""
|
|
@@ -485,6 +576,8 @@ class TheseusWrapper:
|
|
|
485
576
|
return str(raw)
|
|
486
577
|
|
|
487
578
|
def _extract_model(self, raw: Any, fallback: str | None) -> str:
|
|
579
|
+
if isinstance(raw, dict) and "model" in raw:
|
|
580
|
+
return raw["model"]
|
|
488
581
|
if hasattr(raw, "model"):
|
|
489
582
|
return raw.model
|
|
490
583
|
return fallback or "unknown"
|
|
@@ -17,6 +17,7 @@ from __future__ import annotations
|
|
|
17
17
|
import time
|
|
18
18
|
from dataclasses import dataclass
|
|
19
19
|
from enum import Enum
|
|
20
|
+
from pathlib import Path
|
|
20
21
|
from typing import Any
|
|
21
22
|
|
|
22
23
|
from ct_toolkit.identity.embedding import IdentityEmbeddingLayer
|
|
@@ -116,6 +117,7 @@ class DivergenceEngine:
|
|
|
116
117
|
enterprise_mode: bool = False, # Run all tiers all the time
|
|
117
118
|
scheduler: Any | None = None, # ElasticityScheduler instance
|
|
118
119
|
provenance_log: Any | None = None,
|
|
120
|
+
project_root: Path | None = None,
|
|
119
121
|
) -> None:
|
|
120
122
|
self._identity = identity_layer
|
|
121
123
|
self._kernel = kernel
|
|
@@ -123,6 +125,7 @@ class DivergenceEngine:
|
|
|
123
125
|
self._provider = provider
|
|
124
126
|
self._judge_client = judge_client
|
|
125
127
|
self._log = provenance_log
|
|
128
|
+
self._project_root = project_root
|
|
126
129
|
|
|
127
130
|
# Base thresholds
|
|
128
131
|
self._base_l1_threshold = l1_threshold
|
|
@@ -331,5 +334,6 @@ class DivergenceEngine:
|
|
|
331
334
|
kernel=self._kernel,
|
|
332
335
|
template=self._template,
|
|
333
336
|
max_probes=max_probes,
|
|
337
|
+
project_root=self._project_root,
|
|
334
338
|
)
|
|
335
339
|
return runner.run()
|
|
@@ -139,15 +139,26 @@ class LLMJudge:
|
|
|
139
139
|
|
|
140
140
|
try:
|
|
141
141
|
# Call using instructor for structured data
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
142
|
+
if self._provider == "anthropic":
|
|
143
|
+
data: JudgeResponse = self._instructor_client.messages.create(
|
|
144
|
+
model=self._model,
|
|
145
|
+
response_model=JudgeResponse,
|
|
146
|
+
system=system_prompt,
|
|
147
|
+
messages=[
|
|
148
|
+
{"role": "user", "content": user_prompt},
|
|
149
|
+
],
|
|
150
|
+
max_retries=2
|
|
151
|
+
)
|
|
152
|
+
else:
|
|
153
|
+
data: JudgeResponse = self._instructor_client.chat.completions.create(
|
|
154
|
+
model=self._model,
|
|
155
|
+
response_model=JudgeResponse,
|
|
156
|
+
messages=[
|
|
157
|
+
{"role": "system", "content": system_prompt},
|
|
158
|
+
{"role": "user", "content": user_prompt},
|
|
159
|
+
],
|
|
160
|
+
max_retries=2
|
|
161
|
+
)
|
|
151
162
|
|
|
152
163
|
result = JudgeResult(
|
|
153
164
|
verdict=data.verdict,
|
|
@@ -209,6 +209,7 @@ class ICMRunner:
|
|
|
209
209
|
model: str | None = None,
|
|
210
210
|
include_domain_probes: bool = True,
|
|
211
211
|
max_probes: int | None = None,
|
|
212
|
+
project_root: Path | None = None,
|
|
212
213
|
) -> None:
|
|
213
214
|
self._client = client
|
|
214
215
|
self._provider = provider
|
|
@@ -216,6 +217,7 @@ class ICMRunner:
|
|
|
216
217
|
self._template = template
|
|
217
218
|
self._model = model or self._default_model(provider)
|
|
218
219
|
self._include_domain = include_domain_probes
|
|
220
|
+
self._project_root = project_root
|
|
219
221
|
self._max_probes = max_probes
|
|
220
222
|
|
|
221
223
|
@staticmethod
|
|
@@ -332,13 +334,22 @@ class ICMRunner:
|
|
|
332
334
|
|
|
333
335
|
# Domain probes — based on template
|
|
334
336
|
if self._include_domain:
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
337
|
+
# Check user config first
|
|
338
|
+
domain_loaded = False
|
|
339
|
+
if self._project_root:
|
|
340
|
+
user_probe_path = self._project_root / "config" / f"{self._template}_probes.json"
|
|
341
|
+
if user_probe_path.exists():
|
|
342
|
+
with open(user_probe_path, encoding="utf-8") as f:
|
|
343
|
+
probes.extend(json.load(f))
|
|
344
|
+
logger.info(f"Custom domain probes loaded: {user_probe_path}")
|
|
345
|
+
domain_loaded = True
|
|
346
|
+
|
|
347
|
+
if not domain_loaded:
|
|
348
|
+
domain_path = self.PROBES_DIR / "domain_probes" / f"{self._template}_probes.json"
|
|
349
|
+
if domain_path.exists():
|
|
350
|
+
with open(domain_path, encoding="utf-8") as f:
|
|
351
|
+
probes.extend(json.load(f))
|
|
352
|
+
logger.info(f"Domain probes loaded: {self._template}")
|
|
342
353
|
|
|
343
354
|
if not probes:
|
|
344
355
|
logger.warning("Probe file not found. Empty battery.")
|
|
@@ -43,6 +43,7 @@ class IdentityEmbeddingLayer:
|
|
|
43
43
|
template: str = "general",
|
|
44
44
|
embedding_client: Any = None, # Provider client for real embeddings
|
|
45
45
|
embedding_model: str = "text-embedding-3-small",
|
|
46
|
+
project_root: Path | None = None,
|
|
46
47
|
) -> None:
|
|
47
48
|
self._template = template
|
|
48
49
|
self._embedding_client = embedding_client
|
|
@@ -50,6 +51,7 @@ class IdentityEmbeddingLayer:
|
|
|
50
51
|
self._reference_vector: np.ndarray | None = None
|
|
51
52
|
self._template_keywords: list[str] = []
|
|
52
53
|
self._reference_text: str = "" # stored for lazy computation
|
|
54
|
+
self._project_root = project_root
|
|
53
55
|
self._embedding_method: str = "uninitialized"
|
|
54
56
|
self._load_template()
|
|
55
57
|
|
|
@@ -58,34 +60,48 @@ class IdentityEmbeddingLayer:
|
|
|
58
60
|
def _load_template(self) -> None:
|
|
59
61
|
import os
|
|
60
62
|
import yaml
|
|
61
|
-
|
|
63
|
+
|
|
64
|
+
data = None
|
|
62
65
|
|
|
63
66
|
# Sanitize template name to prevent path traversal
|
|
64
67
|
safe_template = os.path.basename(self._template)
|
|
65
68
|
|
|
69
|
+
# 1. Check user config first if project_root is provided
|
|
70
|
+
if self._project_root:
|
|
71
|
+
# Check for both "name.yaml" and "name_identity.yaml"
|
|
72
|
+
for fname in [f"{safe_template}.yaml", f"{safe_template}_identity.yaml"]:
|
|
73
|
+
user_path = self._project_root / "config" / fname
|
|
74
|
+
if user_path.exists():
|
|
75
|
+
logger.debug(f"Loading identity template from user config: {user_path}")
|
|
76
|
+
with open(user_path, "r", encoding="utf-8") as f:
|
|
77
|
+
data = yaml.safe_load(f)
|
|
78
|
+
break
|
|
79
|
+
|
|
66
80
|
# Load built-in template using importlib.resources
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
logger.warning(
|
|
83
|
-
f"Template '{self._template}' not found, using 'general'."
|
|
81
|
+
if data is None:
|
|
82
|
+
try:
|
|
83
|
+
from importlib.resources import files
|
|
84
|
+
template_resource = files("ct_toolkit.identity.templates").joinpath(f"{safe_template}.yaml")
|
|
85
|
+
if template_resource.is_file():
|
|
86
|
+
with template_resource.open("r", encoding="utf-8") as f:
|
|
87
|
+
data = yaml.safe_load(f)
|
|
88
|
+
else:
|
|
89
|
+
raise FileNotFoundError(f"Template resource not found: {template_resource}")
|
|
90
|
+
except (ImportError, FileNotFoundError):
|
|
91
|
+
# Fallback for development or older environments
|
|
92
|
+
template_path = (
|
|
93
|
+
Path(__file__).parent
|
|
94
|
+
/ "templates"
|
|
95
|
+
/ f"{safe_template}.yaml"
|
|
84
96
|
)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
97
|
+
if not template_path.exists():
|
|
98
|
+
logger.warning(
|
|
99
|
+
f"Template '{self._template}' not found, using 'general'."
|
|
100
|
+
)
|
|
101
|
+
template_path = Path(__file__).parent / "templates" / "general.yaml"
|
|
102
|
+
|
|
103
|
+
with open(template_path, "r", encoding="utf-8") as f:
|
|
104
|
+
data = yaml.safe_load(f)
|
|
89
105
|
|
|
90
106
|
self._template_keywords = data.get("identity_keywords", [])
|
|
91
107
|
self._reference_text = data.get("reference_text", " ".join(self._template_keywords))
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ct-toolkit"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.9"
|
|
8
8
|
description = "Computational Theseus Toolkit — Identity Continuity Guardrails for Agentic Systems"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -71,3 +71,9 @@ dev = [
|
|
|
71
71
|
"pytest-cov>=7.0.0",
|
|
72
72
|
"twine>=6.2.0",
|
|
73
73
|
]
|
|
74
|
+
|
|
75
|
+
[tool.semantic_release]
|
|
76
|
+
version_toml = ["pyproject.toml:project.version"]
|
|
77
|
+
branch = "main"
|
|
78
|
+
changelog_file = "CHANGELOG.md"
|
|
79
|
+
build_command = "pip install build && python -m build"
|
|
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
|