cognitive-modules 0.3.0__py3-none-any.whl → 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cognitive/__init__.py +1 -1
- cognitive/cli.py +371 -14
- cognitive/loader.py +180 -14
- cognitive/mcp_server.py +245 -0
- cognitive/migrate.py +624 -0
- cognitive/registry.py +325 -11
- cognitive/runner.py +409 -80
- cognitive/server.py +294 -0
- cognitive/validator.py +380 -122
- cognitive_modules-0.5.0.dist-info/METADATA +431 -0
- cognitive_modules-0.5.0.dist-info/RECORD +18 -0
- cognitive_modules-0.5.0.dist-info/entry_points.txt +2 -0
- cognitive_modules-0.3.0.dist-info/METADATA +0 -418
- cognitive_modules-0.3.0.dist-info/RECORD +0 -15
- cognitive_modules-0.3.0.dist-info/entry_points.txt +0 -2
- {cognitive_modules-0.3.0.dist-info → cognitive_modules-0.5.0.dist-info}/WHEEL +0 -0
- {cognitive_modules-0.3.0.dist-info → cognitive_modules-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {cognitive_modules-0.3.0.dist-info → cognitive_modules-0.5.0.dist-info}/top_level.txt +0 -0
cognitive/runner.py
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Module Runner - Execute cognitive modules with validation.
|
|
3
|
-
Supports v2 envelope format
|
|
3
|
+
Supports v2.2 envelope format with Control/Data plane separation.
|
|
4
|
+
|
|
5
|
+
v2.2 Features:
|
|
6
|
+
- meta (Control Plane): confidence, risk, explain
|
|
7
|
+
- data (Data Plane): business payload + rationale
|
|
8
|
+
- Repair pass for schema validation failures
|
|
9
|
+
- Auto-wrap v2.1 payloads to v2.2 envelope
|
|
4
10
|
"""
|
|
5
11
|
|
|
6
12
|
import json
|
|
7
13
|
from pathlib import Path
|
|
8
|
-
from typing import Optional, TypedDict, Union
|
|
14
|
+
from typing import Optional, TypedDict, Union, Literal
|
|
9
15
|
|
|
10
16
|
import jsonschema
|
|
11
17
|
import yaml
|
|
@@ -15,25 +21,84 @@ from .loader import load_module
|
|
|
15
21
|
from .providers import call_llm
|
|
16
22
|
|
|
17
23
|
|
|
24
|
+
# =============================================================================
|
|
25
|
+
# Type Definitions (v2.2)
|
|
26
|
+
# =============================================================================
|
|
27
|
+
|
|
28
|
+
RiskLevel = Literal["none", "low", "medium", "high"]
|
|
29
|
+
|
|
30
|
+
class EnvelopeMeta(TypedDict, total=False):
|
|
31
|
+
"""Control plane metadata - unified across all modules."""
|
|
32
|
+
confidence: float # 0-1
|
|
33
|
+
risk: RiskLevel
|
|
34
|
+
explain: str # max 280 chars
|
|
35
|
+
trace_id: str
|
|
36
|
+
model: str
|
|
37
|
+
latency_ms: float
|
|
38
|
+
|
|
39
|
+
|
|
18
40
|
class EnvelopeError(TypedDict):
|
|
19
41
|
code: str
|
|
20
42
|
message: str
|
|
21
43
|
|
|
22
44
|
|
|
23
|
-
class
|
|
24
|
-
ok:
|
|
45
|
+
class EnvelopeSuccessV22(TypedDict):
|
|
46
|
+
ok: Literal[True]
|
|
47
|
+
meta: EnvelopeMeta
|
|
25
48
|
data: dict
|
|
26
49
|
|
|
27
50
|
|
|
28
|
-
class
|
|
29
|
-
ok:
|
|
51
|
+
class EnvelopeFailureV22(TypedDict, total=False):
|
|
52
|
+
ok: Literal[False]
|
|
53
|
+
meta: EnvelopeMeta
|
|
30
54
|
error: EnvelopeError
|
|
31
55
|
partial_data: Optional[dict]
|
|
32
56
|
|
|
33
57
|
|
|
34
|
-
|
|
58
|
+
EnvelopeResponseV22 = Union[EnvelopeSuccessV22, EnvelopeFailureV22]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# Legacy types for compatibility
|
|
62
|
+
class EnvelopeSuccessV21(TypedDict):
|
|
63
|
+
ok: Literal[True]
|
|
64
|
+
data: dict
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class EnvelopeFailureV21(TypedDict, total=False):
|
|
68
|
+
ok: Literal[False]
|
|
69
|
+
error: EnvelopeError
|
|
70
|
+
partial_data: Optional[dict]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
EnvelopeResponse = Union[EnvelopeResponseV22, EnvelopeSuccessV21, EnvelopeFailureV21]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# =============================================================================
|
|
77
|
+
# Risk Aggregation
|
|
78
|
+
# =============================================================================
|
|
79
|
+
|
|
80
|
+
RISK_LEVELS = {"none": 0, "low": 1, "medium": 2, "high": 3}
|
|
81
|
+
RISK_NAMES = ["none", "low", "medium", "high"]
|
|
35
82
|
|
|
36
83
|
|
|
84
|
+
def aggregate_risk(changes: list[dict]) -> RiskLevel:
|
|
85
|
+
"""Compute max risk from list of changes."""
|
|
86
|
+
if not changes:
|
|
87
|
+
return "medium" # Default conservative
|
|
88
|
+
|
|
89
|
+
max_level = 0
|
|
90
|
+
for change in changes:
|
|
91
|
+
risk = change.get("risk", "medium")
|
|
92
|
+
level = RISK_LEVELS.get(risk, 2)
|
|
93
|
+
max_level = max(max_level, level)
|
|
94
|
+
|
|
95
|
+
return RISK_NAMES[max_level]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# =============================================================================
|
|
99
|
+
# Schema Validation
|
|
100
|
+
# =============================================================================
|
|
101
|
+
|
|
37
102
|
def validate_data(data: dict, schema: dict, label: str = "Data") -> list[str]:
|
|
38
103
|
"""Validate data against schema. Returns list of errors."""
|
|
39
104
|
errors = []
|
|
@@ -48,9 +113,191 @@ def validate_data(data: dict, schema: dict, label: str = "Data") -> list[str]:
|
|
|
48
113
|
return errors
|
|
49
114
|
|
|
50
115
|
|
|
116
|
+
# =============================================================================
|
|
117
|
+
# Repair Pass (v2.2)
|
|
118
|
+
# =============================================================================
|
|
119
|
+
|
|
120
|
+
def repair_envelope(
|
|
121
|
+
data: dict,
|
|
122
|
+
meta_schema: Optional[dict] = None,
|
|
123
|
+
max_explain_length: int = 280
|
|
124
|
+
) -> dict:
|
|
125
|
+
"""
|
|
126
|
+
Attempt to repair envelope format issues without changing semantics.
|
|
127
|
+
|
|
128
|
+
Repairs:
|
|
129
|
+
- Missing meta fields (fill with conservative defaults)
|
|
130
|
+
- Truncate explain if too long
|
|
131
|
+
- Normalize risk enum values
|
|
132
|
+
"""
|
|
133
|
+
repaired = dict(data)
|
|
134
|
+
|
|
135
|
+
# Ensure meta exists
|
|
136
|
+
if "meta" not in repaired:
|
|
137
|
+
repaired["meta"] = {}
|
|
138
|
+
|
|
139
|
+
meta = repaired["meta"]
|
|
140
|
+
data_payload = repaired.get("data", {})
|
|
141
|
+
|
|
142
|
+
# Repair confidence
|
|
143
|
+
if "confidence" not in meta:
|
|
144
|
+
# Try to extract from data (v2.1 compatibility)
|
|
145
|
+
meta["confidence"] = data_payload.get("confidence", 0.5)
|
|
146
|
+
|
|
147
|
+
# Ensure confidence is in valid range
|
|
148
|
+
if isinstance(meta.get("confidence"), (int, float)):
|
|
149
|
+
meta["confidence"] = max(0.0, min(1.0, float(meta["confidence"])))
|
|
150
|
+
|
|
151
|
+
# Repair risk
|
|
152
|
+
if "risk" not in meta:
|
|
153
|
+
# Aggregate from changes if available
|
|
154
|
+
changes = data_payload.get("changes", [])
|
|
155
|
+
meta["risk"] = aggregate_risk(changes)
|
|
156
|
+
|
|
157
|
+
# Normalize risk value
|
|
158
|
+
risk = str(meta.get("risk", "medium")).lower()
|
|
159
|
+
if risk not in RISK_LEVELS:
|
|
160
|
+
meta["risk"] = "medium"
|
|
161
|
+
else:
|
|
162
|
+
meta["risk"] = risk
|
|
163
|
+
|
|
164
|
+
# Repair explain
|
|
165
|
+
if "explain" not in meta:
|
|
166
|
+
# Try to extract from rationale
|
|
167
|
+
rationale = data_payload.get("rationale", "")
|
|
168
|
+
if rationale:
|
|
169
|
+
meta["explain"] = rationale[:max_explain_length]
|
|
170
|
+
else:
|
|
171
|
+
meta["explain"] = "No explanation provided"
|
|
172
|
+
|
|
173
|
+
# Truncate explain if too long
|
|
174
|
+
if len(meta.get("explain", "")) > max_explain_length:
|
|
175
|
+
meta["explain"] = meta["explain"][:max_explain_length - 3] + "..."
|
|
176
|
+
|
|
177
|
+
return repaired
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def repair_error_envelope(
|
|
181
|
+
data: dict,
|
|
182
|
+
max_explain_length: int = 280
|
|
183
|
+
) -> dict:
|
|
184
|
+
"""Repair error envelope format."""
|
|
185
|
+
repaired = dict(data)
|
|
186
|
+
|
|
187
|
+
# Ensure meta exists for errors
|
|
188
|
+
if "meta" not in repaired:
|
|
189
|
+
repaired["meta"] = {}
|
|
190
|
+
|
|
191
|
+
meta = repaired["meta"]
|
|
192
|
+
|
|
193
|
+
# Set default meta for errors
|
|
194
|
+
if "confidence" not in meta:
|
|
195
|
+
meta["confidence"] = 0.0
|
|
196
|
+
if "risk" not in meta:
|
|
197
|
+
meta["risk"] = "high"
|
|
198
|
+
if "explain" not in meta:
|
|
199
|
+
error = repaired.get("error", {})
|
|
200
|
+
meta["explain"] = error.get("message", "An error occurred")[:max_explain_length]
|
|
201
|
+
|
|
202
|
+
return repaired
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# =============================================================================
|
|
206
|
+
# Envelope Detection & Conversion
|
|
207
|
+
# =============================================================================
|
|
208
|
+
|
|
209
|
+
def is_envelope_response(data: dict) -> bool:
|
|
210
|
+
"""Check if response is in envelope format (v2.1 or v2.2)."""
|
|
211
|
+
return isinstance(data.get("ok"), bool)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def is_v22_envelope(data: dict) -> bool:
|
|
215
|
+
"""Check if response is in v2.2 envelope format (has meta)."""
|
|
216
|
+
return is_envelope_response(data) and "meta" in data
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def wrap_v21_to_v22(v21_response: dict) -> EnvelopeResponseV22:
|
|
220
|
+
"""
|
|
221
|
+
Convert v2.1 envelope to v2.2 envelope.
|
|
222
|
+
Adds meta field by extracting/computing from data.
|
|
223
|
+
"""
|
|
224
|
+
if is_v22_envelope(v21_response):
|
|
225
|
+
return v21_response # Already v2.2
|
|
226
|
+
|
|
227
|
+
if v21_response.get("ok") is True:
|
|
228
|
+
data = v21_response.get("data", {})
|
|
229
|
+
|
|
230
|
+
# Extract or compute meta fields
|
|
231
|
+
confidence = data.get("confidence", 0.5)
|
|
232
|
+
rationale = data.get("rationale", "")
|
|
233
|
+
changes = data.get("changes", [])
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
"ok": True,
|
|
237
|
+
"meta": {
|
|
238
|
+
"confidence": confidence,
|
|
239
|
+
"risk": aggregate_risk(changes),
|
|
240
|
+
"explain": rationale[:280] if rationale else "No explanation provided"
|
|
241
|
+
},
|
|
242
|
+
"data": data
|
|
243
|
+
}
|
|
244
|
+
else:
|
|
245
|
+
error = v21_response.get("error", {"code": "UNKNOWN", "message": "Unknown error"})
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
"ok": False,
|
|
249
|
+
"meta": {
|
|
250
|
+
"confidence": 0.0,
|
|
251
|
+
"risk": "high",
|
|
252
|
+
"explain": error.get("message", "An error occurred")[:280]
|
|
253
|
+
},
|
|
254
|
+
"error": error,
|
|
255
|
+
"partial_data": v21_response.get("partial_data")
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def convert_legacy_to_envelope(data: dict, is_error: bool = False) -> EnvelopeResponseV22:
|
|
260
|
+
"""Convert legacy format (no envelope) to v2.2 envelope."""
|
|
261
|
+
if is_error or "error" in data:
|
|
262
|
+
error = data.get("error", {})
|
|
263
|
+
error_msg = error.get("message", str(error)) if isinstance(error, dict) else str(error)
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
"ok": False,
|
|
267
|
+
"meta": {
|
|
268
|
+
"confidence": 0.0,
|
|
269
|
+
"risk": "high",
|
|
270
|
+
"explain": error_msg[:280]
|
|
271
|
+
},
|
|
272
|
+
"error": {
|
|
273
|
+
"code": error.get("code", "UNKNOWN") if isinstance(error, dict) else "UNKNOWN",
|
|
274
|
+
"message": error_msg
|
|
275
|
+
},
|
|
276
|
+
"partial_data": None
|
|
277
|
+
}
|
|
278
|
+
else:
|
|
279
|
+
# Legacy success response - data is the payload itself
|
|
280
|
+
confidence = data.get("confidence", 0.5)
|
|
281
|
+
rationale = data.get("rationale", "")
|
|
282
|
+
changes = data.get("changes", [])
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
"ok": True,
|
|
286
|
+
"meta": {
|
|
287
|
+
"confidence": confidence,
|
|
288
|
+
"risk": aggregate_risk(changes),
|
|
289
|
+
"explain": rationale[:280] if rationale else "No explanation provided"
|
|
290
|
+
},
|
|
291
|
+
"data": data
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# =============================================================================
|
|
296
|
+
# Prompt Building
|
|
297
|
+
# =============================================================================
|
|
298
|
+
|
|
51
299
|
def substitute_arguments(text: str, input_data: dict) -> str:
|
|
52
300
|
"""Substitute $ARGUMENTS and $N placeholders in text."""
|
|
53
|
-
# Get arguments
|
|
54
301
|
args_value = input_data.get("$ARGUMENTS", input_data.get("query", input_data.get("code", "")))
|
|
55
302
|
|
|
56
303
|
# Replace $ARGUMENTS
|
|
@@ -66,7 +313,7 @@ def substitute_arguments(text: str, input_data: dict) -> str:
|
|
|
66
313
|
return text
|
|
67
314
|
|
|
68
315
|
|
|
69
|
-
def build_prompt(module: dict, input_data: dict, use_envelope: bool = False) -> str:
|
|
316
|
+
def build_prompt(module: dict, input_data: dict, use_envelope: bool = False, use_v22: bool = False) -> str:
|
|
70
317
|
"""Build the complete prompt for the LLM."""
|
|
71
318
|
# Substitute $ARGUMENTS in prompt
|
|
72
319
|
prompt = substitute_arguments(module["prompt"], input_data)
|
|
@@ -82,13 +329,23 @@ def build_prompt(module: dict, input_data: dict, use_envelope: bool = False) ->
|
|
|
82
329
|
]
|
|
83
330
|
|
|
84
331
|
if use_envelope:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
332
|
+
if use_v22:
|
|
333
|
+
parts.extend([
|
|
334
|
+
"\n## Response Format (Envelope v2.2)\n",
|
|
335
|
+
"You MUST wrap your response in the v2.2 envelope format with separate meta and data:\n",
|
|
336
|
+
"- Success: { \"ok\": true, \"meta\": { \"confidence\": 0.9, \"risk\": \"low\", \"explain\": \"short summary\" }, \"data\": { ...payload... } }\n",
|
|
337
|
+
"- Error: { \"ok\": false, \"meta\": { \"confidence\": 0.0, \"risk\": \"high\", \"explain\": \"error summary\" }, \"error\": { \"code\": \"ERROR_CODE\", \"message\": \"...\" } }\n",
|
|
338
|
+
"Note: meta.explain must be ≤280 characters. data.rationale can be longer for detailed reasoning.\n",
|
|
339
|
+
"Return ONLY valid JSON.\n",
|
|
340
|
+
])
|
|
341
|
+
else:
|
|
342
|
+
parts.extend([
|
|
343
|
+
"\n## Response Format (Envelope)\n",
|
|
344
|
+
"You MUST wrap your response in the envelope format:\n",
|
|
345
|
+
"- Success: { \"ok\": true, \"data\": { ...your output... } }\n",
|
|
346
|
+
"- Error: { \"ok\": false, \"error\": { \"code\": \"ERROR_CODE\", \"message\": \"...\" } }\n",
|
|
347
|
+
"Return ONLY valid JSON.\n",
|
|
348
|
+
])
|
|
92
349
|
else:
|
|
93
350
|
parts.extend([
|
|
94
351
|
"\n## Instructions\n",
|
|
@@ -99,6 +356,10 @@ def build_prompt(module: dict, input_data: dict, use_envelope: bool = False) ->
|
|
|
99
356
|
return "".join(parts)
|
|
100
357
|
|
|
101
358
|
|
|
359
|
+
# =============================================================================
|
|
360
|
+
# LLM Response Parsing
|
|
361
|
+
# =============================================================================
|
|
362
|
+
|
|
102
363
|
def parse_llm_response(response: str) -> dict:
|
|
103
364
|
"""Parse LLM response, handling potential markdown code blocks."""
|
|
104
365
|
text = response.strip()
|
|
@@ -117,44 +378,9 @@ def parse_llm_response(response: str) -> dict:
|
|
|
117
378
|
return json.loads(text)
|
|
118
379
|
|
|
119
380
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def parse_envelope_response(data: dict) -> EnvelopeResponse:
|
|
126
|
-
"""Parse and normalize envelope response."""
|
|
127
|
-
if data.get("ok") is True:
|
|
128
|
-
return {
|
|
129
|
-
"ok": True,
|
|
130
|
-
"data": data.get("data", {})
|
|
131
|
-
}
|
|
132
|
-
else:
|
|
133
|
-
return {
|
|
134
|
-
"ok": False,
|
|
135
|
-
"error": data.get("error", {"code": "UNKNOWN", "message": "Unknown error"}),
|
|
136
|
-
"partial_data": data.get("partial_data")
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
def convert_to_envelope(data: dict, is_error: bool = False) -> EnvelopeResponse:
|
|
141
|
-
"""Convert legacy format to envelope format."""
|
|
142
|
-
if is_error or "error" in data:
|
|
143
|
-
error = data.get("error", {})
|
|
144
|
-
return {
|
|
145
|
-
"ok": False,
|
|
146
|
-
"error": {
|
|
147
|
-
"code": error.get("code", "UNKNOWN"),
|
|
148
|
-
"message": error.get("message", str(error))
|
|
149
|
-
},
|
|
150
|
-
"partial_data": None
|
|
151
|
-
}
|
|
152
|
-
else:
|
|
153
|
-
return {
|
|
154
|
-
"ok": True,
|
|
155
|
-
"data": data
|
|
156
|
-
}
|
|
157
|
-
|
|
381
|
+
# =============================================================================
|
|
382
|
+
# Main Runner
|
|
383
|
+
# =============================================================================
|
|
158
384
|
|
|
159
385
|
def run_module(
|
|
160
386
|
name_or_path: str,
|
|
@@ -163,10 +389,12 @@ def run_module(
|
|
|
163
389
|
validate_output: bool = True,
|
|
164
390
|
model: Optional[str] = None,
|
|
165
391
|
use_envelope: Optional[bool] = None,
|
|
166
|
-
|
|
392
|
+
use_v22: Optional[bool] = None,
|
|
393
|
+
enable_repair: bool = True,
|
|
394
|
+
) -> EnvelopeResponseV22:
|
|
167
395
|
"""
|
|
168
396
|
Run a cognitive module with the given input.
|
|
169
|
-
Returns envelope format response.
|
|
397
|
+
Returns v2.2 envelope format response.
|
|
170
398
|
|
|
171
399
|
Args:
|
|
172
400
|
name_or_path: Module name or path to module directory
|
|
@@ -175,10 +403,15 @@ def run_module(
|
|
|
175
403
|
validate_output: Whether to validate output against schema
|
|
176
404
|
model: Optional model override
|
|
177
405
|
use_envelope: Force envelope format (auto-detect if None)
|
|
406
|
+
use_v22: Force v2.2 envelope format (auto-detect if None)
|
|
407
|
+
enable_repair: Enable repair pass for validation failures
|
|
178
408
|
|
|
179
409
|
Returns:
|
|
180
|
-
|
|
410
|
+
EnvelopeResponseV22 with ok=True/False, meta, and data/error
|
|
181
411
|
"""
|
|
412
|
+
import time
|
|
413
|
+
start_time = time.time()
|
|
414
|
+
|
|
182
415
|
# Find module path
|
|
183
416
|
path = Path(name_or_path)
|
|
184
417
|
if path.exists() and path.is_dir():
|
|
@@ -188,6 +421,11 @@ def run_module(
|
|
|
188
421
|
if not module_path:
|
|
189
422
|
return {
|
|
190
423
|
"ok": False,
|
|
424
|
+
"meta": {
|
|
425
|
+
"confidence": 1.0,
|
|
426
|
+
"risk": "high",
|
|
427
|
+
"explain": f"Module '{name_or_path}' not found."
|
|
428
|
+
},
|
|
191
429
|
"error": {"code": "MODULE_NOT_FOUND", "message": f"Module not found: {name_or_path}"},
|
|
192
430
|
"partial_data": None
|
|
193
431
|
}
|
|
@@ -195,57 +433,114 @@ def run_module(
|
|
|
195
433
|
# Load module (auto-detects format)
|
|
196
434
|
module = load_module(module_path)
|
|
197
435
|
|
|
198
|
-
# Determine
|
|
436
|
+
# Determine envelope version
|
|
437
|
+
compat = module.get("compat", {})
|
|
438
|
+
is_v22_module = module.get("tier") is not None or "meta_schema" in module
|
|
439
|
+
|
|
199
440
|
should_use_envelope = use_envelope
|
|
200
441
|
if should_use_envelope is None:
|
|
201
|
-
# Auto-detect: use envelope for v2 format or if output.envelope is True
|
|
202
442
|
output_contract = module.get("output_contract", {})
|
|
203
443
|
should_use_envelope = (
|
|
204
444
|
module.get("format") == "v2" or
|
|
205
445
|
output_contract.get("envelope", False)
|
|
206
446
|
)
|
|
207
447
|
|
|
448
|
+
should_use_v22 = use_v22
|
|
449
|
+
if should_use_v22 is None:
|
|
450
|
+
should_use_v22 = is_v22_module or compat.get("runtime_auto_wrap", False)
|
|
451
|
+
|
|
208
452
|
# Validate input
|
|
209
|
-
if validate_input and module
|
|
453
|
+
if validate_input and module.get("input_schema"):
|
|
210
454
|
errors = validate_data(input_data, module["input_schema"], "Input")
|
|
211
455
|
if errors:
|
|
212
456
|
return {
|
|
213
457
|
"ok": False,
|
|
458
|
+
"meta": {
|
|
459
|
+
"confidence": 1.0,
|
|
460
|
+
"risk": "none",
|
|
461
|
+
"explain": "Input validation failed."
|
|
462
|
+
},
|
|
214
463
|
"error": {"code": "INVALID_INPUT", "message": str(errors)},
|
|
215
464
|
"partial_data": None
|
|
216
465
|
}
|
|
217
466
|
|
|
218
467
|
# Build prompt and call LLM
|
|
219
|
-
full_prompt = build_prompt(
|
|
468
|
+
full_prompt = build_prompt(
|
|
469
|
+
module,
|
|
470
|
+
input_data,
|
|
471
|
+
use_envelope=should_use_envelope,
|
|
472
|
+
use_v22=should_use_v22
|
|
473
|
+
)
|
|
220
474
|
response = call_llm(full_prompt, model=model)
|
|
221
475
|
|
|
476
|
+
# Calculate latency
|
|
477
|
+
latency_ms = (time.time() - start_time) * 1000
|
|
478
|
+
|
|
222
479
|
# Parse response
|
|
223
480
|
try:
|
|
224
481
|
output_data = parse_llm_response(response)
|
|
225
482
|
except json.JSONDecodeError as e:
|
|
226
483
|
return {
|
|
227
484
|
"ok": False,
|
|
485
|
+
"meta": {
|
|
486
|
+
"confidence": 0.0,
|
|
487
|
+
"risk": "high",
|
|
488
|
+
"explain": "Failed to parse LLM response as JSON."
|
|
489
|
+
},
|
|
228
490
|
"error": {"code": "PARSE_ERROR", "message": f"Failed to parse JSON: {e}"},
|
|
229
491
|
"partial_data": None
|
|
230
492
|
}
|
|
231
493
|
|
|
232
|
-
#
|
|
233
|
-
if
|
|
234
|
-
result =
|
|
494
|
+
# Convert to v2.2 envelope
|
|
495
|
+
if is_v22_envelope(output_data):
|
|
496
|
+
result = output_data
|
|
497
|
+
elif is_envelope_response(output_data):
|
|
498
|
+
# v2.1 envelope -> v2.2
|
|
499
|
+
result = wrap_v21_to_v22(output_data)
|
|
235
500
|
else:
|
|
236
|
-
#
|
|
237
|
-
result =
|
|
501
|
+
# Legacy format -> v2.2
|
|
502
|
+
result = convert_legacy_to_envelope(output_data)
|
|
238
503
|
|
|
239
|
-
#
|
|
240
|
-
if
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
504
|
+
# Add latency to meta
|
|
505
|
+
if "meta" in result:
|
|
506
|
+
result["meta"]["latency_ms"] = latency_ms
|
|
507
|
+
if model:
|
|
508
|
+
result["meta"]["model"] = model
|
|
509
|
+
|
|
510
|
+
# Validate and potentially repair
|
|
511
|
+
if result.get("ok") and validate_output:
|
|
512
|
+
# Get data schema (support both "data" and "output" aliases)
|
|
513
|
+
data_schema = module.get("data_schema") or module.get("output_schema")
|
|
514
|
+
meta_schema = module.get("meta_schema")
|
|
515
|
+
|
|
516
|
+
if data_schema:
|
|
517
|
+
data_to_validate = result.get("data", {})
|
|
518
|
+
errors = validate_data(data_to_validate, data_schema, "Data")
|
|
519
|
+
|
|
520
|
+
if errors and enable_repair:
|
|
521
|
+
# Attempt repair pass
|
|
522
|
+
result = repair_envelope(result, meta_schema)
|
|
523
|
+
|
|
524
|
+
# Re-validate after repair
|
|
525
|
+
errors = validate_data(result.get("data", {}), data_schema, "Data")
|
|
526
|
+
|
|
527
|
+
if errors:
|
|
528
|
+
return {
|
|
529
|
+
"ok": False,
|
|
530
|
+
"meta": {
|
|
531
|
+
"confidence": 0.0,
|
|
532
|
+
"risk": "high",
|
|
533
|
+
"explain": "Schema validation failed after repair attempt."
|
|
534
|
+
},
|
|
535
|
+
"error": {"code": "SCHEMA_VALIDATION_FAILED", "message": str(errors)},
|
|
536
|
+
"partial_data": result.get("data")
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
# Validate meta if schema exists
|
|
540
|
+
if meta_schema:
|
|
541
|
+
meta_errors = validate_data(result.get("meta", {}), meta_schema, "Meta")
|
|
542
|
+
if meta_errors and enable_repair:
|
|
543
|
+
result = repair_envelope(result, meta_schema)
|
|
249
544
|
|
|
250
545
|
return result
|
|
251
546
|
|
|
@@ -267,10 +562,44 @@ def run_module_legacy(
|
|
|
267
562
|
validate_input=validate_input,
|
|
268
563
|
validate_output=validate_output,
|
|
269
564
|
model=model,
|
|
270
|
-
use_envelope=False
|
|
565
|
+
use_envelope=False,
|
|
566
|
+
use_v22=False
|
|
271
567
|
)
|
|
272
568
|
|
|
273
|
-
if result
|
|
274
|
-
return result
|
|
569
|
+
if result.get("ok"):
|
|
570
|
+
return result.get("data", {})
|
|
275
571
|
else:
|
|
276
|
-
|
|
572
|
+
error = result.get("error", {})
|
|
573
|
+
raise ValueError(f"{error.get('code', 'UNKNOWN')}: {error.get('message', 'Unknown error')}")
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
# =============================================================================
|
|
577
|
+
# Convenience Functions
|
|
578
|
+
# =============================================================================
|
|
579
|
+
|
|
580
|
+
def extract_meta(result: EnvelopeResponseV22) -> EnvelopeMeta:
|
|
581
|
+
"""Extract meta from v2.2 envelope for routing/logging."""
|
|
582
|
+
return result.get("meta", {
|
|
583
|
+
"confidence": 0.5,
|
|
584
|
+
"risk": "medium",
|
|
585
|
+
"explain": "No meta available"
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def should_escalate(result: EnvelopeResponseV22, confidence_threshold: float = 0.7) -> bool:
|
|
590
|
+
"""Determine if result should be escalated to human review based on meta."""
|
|
591
|
+
meta = extract_meta(result)
|
|
592
|
+
|
|
593
|
+
# Escalate if low confidence
|
|
594
|
+
if meta.get("confidence", 0) < confidence_threshold:
|
|
595
|
+
return True
|
|
596
|
+
|
|
597
|
+
# Escalate if high risk
|
|
598
|
+
if meta.get("risk") == "high":
|
|
599
|
+
return True
|
|
600
|
+
|
|
601
|
+
# Escalate if error
|
|
602
|
+
if not result.get("ok"):
|
|
603
|
+
return True
|
|
604
|
+
|
|
605
|
+
return False
|