jit-protocol 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: jit-protocol
3
+ Version: 1.0.0
4
+ Summary: Just-In-Time Protocol Synthesis Framework: Dynamic-to-Static API Generation using TypeSafe (Jev) and Needle
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: pydantic>=2.0.0
8
+ Requires-Dist: requests>=2.28.0
9
+ Requires-Dist: python-dotenv>=1.0.0
10
+ Requires-Dist: cactus-needle>=3.0.0
11
+ Provides-Extra: fastapi
12
+ Requires-Dist: fastapi>=0.100.0; extra == "fastapi"
13
+ Requires-Dist: uvicorn>=0.20.0; extra == "fastapi"
14
+ Provides-Extra: test
15
+ Requires-Dist: pytest>=7.0.0; extra == "test"
16
+ Requires-Dist: pytest-asyncio>=0.20.0; extra == "test"
17
+
18
+ # JIT Protocol Synthesis Framework - Python Engine (`jit-api-python`)
19
+
20
+ > **基於 TypeSafe (Jev) 與 Cactus Needle 3.0 的原生 Python 動態至靜態 API 合成引擎**
21
+ > *Pythonic Native Host Engine for Just-In-Time Protocol Synthesis*
22
+
23
+ ---
24
+
25
+ ## 🌟 特色
26
+
27
+ 1. **同進程 Cactus Needle 3.0 端側推論**:
28
+ 在無 `TYPESAFE_API_KEY` 或離線狀態下,直接調用進程內的 Needle 3 SAN (2-bit, 35MB) 進行毫秒級 Tool Calling 與結構萃取,**0 網路轉發延遲、0 API 費用**。
29
+ 2. **極致優雅的裝飾器語法**:
30
+ 使用 `@engine.route(...)` 宣告語意意圖,無縫支援 `async def` 與同步 `def` 函式。
31
+ 3. **Pydantic v2 動態模型極速路徑**:
32
+ 當路由結構在過去 N 次請求中穩定後,自動生成動態 Pydantic v2 BaseModel,Phase 3 靜態驗證僅需 **0.03ms (0ms AI 延遲)**!
33
+ 4. **自動版本演進與降級保護**:
34
+ 若遇到型態漂移(Schema Drift),自動降級回 Phase 1 並開啟 v2 / v3 版本的觀測與合約重新凍結。
35
+ 5. **多語言程式碼生成積木**:
36
+ 凍結時同步輸出 TypeScript (Zod)、Golang (.proto / struct) 與 Python (Pydantic / FastAPI)。
37
+
38
+ ---
39
+
40
+ ## 🚀 快速上手 (Quick Start)
41
+
42
+ ### 執行環境 (Conda `toby`)
43
+ ```bash
44
+ conda activate toby
45
+ ```
46
+
47
+ ### 宣告端點與執行
48
+ ```python
49
+ import asyncio
50
+ from jit_api import JITEngine
51
+
52
+ engine = JITEngine(stability_threshold=3)
53
+
54
+ @engine.route(
55
+ name="create_invoice",
56
+ description="Issue a customer billing invoice",
57
+ intent="Bill or invoice a customer for items",
58
+ enum_fields={"currency": {"USD": "US Dollar", "EUR": "Euro"}}
59
+ )
60
+ async def create_invoice(payload, ctx):
61
+ return {
62
+ "invoice_id": "INV-100",
63
+ "customer": payload.get("customer"),
64
+ "amount": payload.get("amount"),
65
+ "phase": ctx.phase,
66
+ }
67
+
68
+ async def main():
69
+ # 支援自然語言或鬆散 JSON
70
+ res = await engine.execute({
71
+ "message": "Please bill Wayne Corp for 500 dollars urgently",
72
+ "customer": "Wayne Corp",
73
+ "amount": 500,
74
+ "currency": "USD"
75
+ })
76
+ print(res.data)
77
+
78
+ if __name__ == "__main__":
79
+ asyncio.run(main())
80
+ ```
81
+
82
+ ---
83
+
84
+ ## 🧪 執行測試與演示
85
+
86
+ ```bash
87
+ # 執行全套 pytest 測試
88
+ PYTHONPATH=python /home/toby/miniconda3/envs/toby/bin/pytest python/tests/ -v
89
+
90
+ # 執行端到端生命週期演示
91
+ PYTHONPATH=python /home/toby/miniconda3/envs/toby/bin/python python/examples/demo_lifecycle.py
92
+ ```
@@ -0,0 +1,75 @@
1
+ # JIT Protocol Synthesis Framework - Python Engine (`jit-api-python`)
2
+
3
+ > **基於 TypeSafe (Jev) 與 Cactus Needle 3.0 的原生 Python 動態至靜態 API 合成引擎**
4
+ > *Pythonic Native Host Engine for Just-In-Time Protocol Synthesis*
5
+
6
+ ---
7
+
8
+ ## 🌟 特色
9
+
10
+ 1. **同進程 Cactus Needle 3.0 端側推論**:
11
+ 在無 `TYPESAFE_API_KEY` 或離線狀態下,直接調用進程內的 Needle 3 SAN (2-bit, 35MB) 進行毫秒級 Tool Calling 與結構萃取,**0 網路轉發延遲、0 API 費用**。
12
+ 2. **極致優雅的裝飾器語法**:
13
+ 使用 `@engine.route(...)` 宣告語意意圖,無縫支援 `async def` 與同步 `def` 函式。
14
+ 3. **Pydantic v2 動態模型極速路徑**:
15
+ 當路由結構在過去 N 次請求中穩定後,自動生成動態 Pydantic v2 BaseModel,Phase 3 靜態驗證僅需 **0.03ms (0ms AI 延遲)**!
16
+ 4. **自動版本演進與降級保護**:
17
+ 若遇到型態漂移(Schema Drift),自動降級回 Phase 1 並開啟 v2 / v3 版本的觀測與合約重新凍結。
18
+ 5. **多語言程式碼生成積木**:
19
+ 凍結時同步輸出 TypeScript (Zod)、Golang (.proto / struct) 與 Python (Pydantic / FastAPI)。
20
+
21
+ ---
22
+
23
+ ## 🚀 快速上手 (Quick Start)
24
+
25
+ ### 執行環境 (Conda `toby`)
26
+ ```bash
27
+ conda activate toby
28
+ ```
29
+
30
+ ### 宣告端點與執行
31
+ ```python
32
+ import asyncio
33
+ from jit_api import JITEngine
34
+
35
+ engine = JITEngine(stability_threshold=3)
36
+
37
+ @engine.route(
38
+ name="create_invoice",
39
+ description="Issue a customer billing invoice",
40
+ intent="Bill or invoice a customer for items",
41
+ enum_fields={"currency": {"USD": "US Dollar", "EUR": "Euro"}}
42
+ )
43
+ async def create_invoice(payload, ctx):
44
+ return {
45
+ "invoice_id": "INV-100",
46
+ "customer": payload.get("customer"),
47
+ "amount": payload.get("amount"),
48
+ "phase": ctx.phase,
49
+ }
50
+
51
+ async def main():
52
+ # 支援自然語言或鬆散 JSON
53
+ res = await engine.execute({
54
+ "message": "Please bill Wayne Corp for 500 dollars urgently",
55
+ "customer": "Wayne Corp",
56
+ "amount": 500,
57
+ "currency": "USD"
58
+ })
59
+ print(res.data)
60
+
61
+ if __name__ == "__main__":
62
+ asyncio.run(main())
63
+ ```
64
+
65
+ ---
66
+
67
+ ## 🧪 執行測試與演示
68
+
69
+ ```bash
70
+ # 執行全套 pytest 測試
71
+ PYTHONPATH=python /home/toby/miniconda3/envs/toby/bin/pytest python/tests/ -v
72
+
73
+ # 執行端到端生命週期演示
74
+ PYTHONPATH=python /home/toby/miniconda3/envs/toby/bin/python python/examples/demo_lifecycle.py
75
+ ```
@@ -0,0 +1,39 @@
1
+ """
2
+ JIT Protocol Synthesis Framework - Python Host Engine (jit-api-python)
3
+ Dynamic Semantic Negotiation ➔ Static Code Freeze ➔ Fallback
4
+ Powered by TypeSafe (Jev) and Cactus Needle 3.0
5
+ """
6
+
7
+ from .engine import JITEngine
8
+ from .types import (
9
+ IRField,
10
+ IRSchema,
11
+ JITExecutionResult,
12
+ JITRequestContext,
13
+ LifecyclePhase,
14
+ RouteDefinition,
15
+ StabilityMetrics,
16
+ )
17
+ from .typesafe_client import TypeSafeClient
18
+ from .needle_engine import NeedleEngine
19
+ from .router import TypeSafeRouter
20
+ from .observer import SchemaObserver
21
+ from .fallback_handler import FallbackHandler
22
+ from .codegen import CodegenEngine
23
+
24
+ __all__ = [
25
+ "JITEngine",
26
+ "TypeSafeClient",
27
+ "NeedleEngine",
28
+ "TypeSafeRouter",
29
+ "SchemaObserver",
30
+ "FallbackHandler",
31
+ "CodegenEngine",
32
+ "IRField",
33
+ "IRSchema",
34
+ "JITExecutionResult",
35
+ "JITRequestContext",
36
+ "LifecyclePhase",
37
+ "RouteDefinition",
38
+ "StabilityMetrics",
39
+ ]
@@ -0,0 +1,188 @@
1
+ import json
2
+ import os
3
+ from typing import Any, Dict, Optional
4
+ from .types import IRField, IRSchema
5
+
6
+
7
+ class CodegenEngine:
8
+ """
9
+ Compiles IRSchema into multi-language contracts (TypeScript, Go, Python, IR JSON).
10
+ """
11
+
12
+ def __init__(self, output_dir: Optional[str] = None):
13
+ self.output_dir = output_dir or os.path.abspath("generated")
14
+
15
+ def _ensure_dirs(self) -> None:
16
+ for sub in ["typescript", "golang", "python", "schemas"]:
17
+ os.makedirs(os.path.join(self.output_dir, sub), exist_ok=True)
18
+
19
+ def generate_python(self, schema: IRSchema) -> str:
20
+ fields_code = []
21
+ for f in schema.fields.values():
22
+ py_t = "str"
23
+ if f.type == "number":
24
+ py_t = "float"
25
+ elif f.type == "boolean":
26
+ py_t = "bool"
27
+ elif f.type == "array":
28
+ item_t = "float" if f.item_type == "number" else ("bool" if f.item_type == "boolean" else "str")
29
+ py_t = f"list[{item_t}]"
30
+ elif f.type == "object":
31
+ py_t = "dict[str, Any]"
32
+
33
+ if not f.required:
34
+ py_t = f"Optional[{py_t}] = None"
35
+
36
+ fields_code.append(f" {f.name}: {py_t}")
37
+
38
+ return f'''"""
39
+ AUTO-GENERATED BY JIT PROTOCOL SYNTHESIS FRAMEWORK (Python)
40
+ Route: {schema.route} (v{schema.version})
41
+ Frozen At: {schema.frozen_at}
42
+ """
43
+
44
+ from typing import Any, Optional
45
+ from pydantic import BaseModel
46
+ from fastapi import APIRouter
47
+
48
+ router = APIRouter(prefix="/{schema.route}", tags=["{schema.route}"])
49
+
50
+
51
+ class {schema.name}(BaseModel):
52
+ {chr(10).join(fields_code)}
53
+
54
+
55
+ @router.post("/", response_model=dict[str, Any])
56
+ async def handle_{schema.route.replace("-", "_")}(payload: {schema.name}):
57
+ return {{
58
+ "status": "success",
59
+ "route": "{schema.route}",
60
+ "version": {schema.version},
61
+ "data": payload.model_dump(),
62
+ }}
63
+ '''
64
+
65
+ def generate_typescript(self, schema: IRSchema) -> str:
66
+ fields_code = []
67
+ for f in schema.fields.values():
68
+ z_t = "z.string()"
69
+ if f.type == "number":
70
+ z_t = "z.number()"
71
+ elif f.type == "boolean":
72
+ z_t = "z.boolean()"
73
+ elif f.type == "array":
74
+ item_z = "z.number()" if f.item_type == "number" else ("z.boolean()" if f.item_type == "boolean" else "z.string()")
75
+ z_t = f"z.array({item_z})"
76
+ elif f.type == "object":
77
+ z_t = "z.record(z.unknown())"
78
+
79
+ if not f.required:
80
+ z_t += ".optional()"
81
+ fields_code.append(f" {f.name}: {z_t},")
82
+
83
+ return f'''/**
84
+ * AUTO-GENERATED BY JIT PROTOCOL SYNTHESIS FRAMEWORK (Python Host)
85
+ * Route: {schema.route} (v{schema.version})
86
+ */
87
+
88
+ import {{ z }} from 'zod';
89
+
90
+ export const {schema.name}Schema = z.object({{
91
+ {chr(10).join(fields_code)}
92
+ }});
93
+
94
+ export type {schema.name} = z.infer<typeof {schema.name}Schema>;
95
+ '''
96
+
97
+ def generate_golang(self, schema: IRSchema) -> Dict[str, str]:
98
+ # 1. Proto3
99
+ proto_fields = []
100
+ for idx, f in enumerate(schema.fields.values(), 1):
101
+ pr_t = "string"
102
+ if f.type == "number":
103
+ pr_t = "double"
104
+ elif f.type == "boolean":
105
+ pr_t = "bool"
106
+ elif f.type == "array":
107
+ pr_t = f"repeated {'double' if f.item_type == 'number' else 'string'}"
108
+ proto_fields.append(f" {pr_t} {f.name} = {idx};")
109
+
110
+ proto = f'''// AUTO-GENERATED BY JIT PROTOCOL SYNTHESIS FRAMEWORK
111
+ syntax = "proto3";
112
+
113
+ package jit.v{schema.version};
114
+
115
+ option go_package = "jit/gen/v{schema.version};jitv{schema.version}";
116
+
117
+ message {schema.name} {{
118
+ {chr(10).join(proto_fields)}
119
+ }}
120
+ '''
121
+ # 2. Go Struct
122
+ go_fields = []
123
+ for f in schema.fields.values():
124
+ field_name = "".join(part.capitalize() for part in f.name.split("_"))
125
+ go_t = "string"
126
+ if f.type == "number":
127
+ go_t = "float64"
128
+ elif f.type == "boolean":
129
+ go_t = "bool"
130
+ elif f.type == "array":
131
+ go_t = "[]float64" if f.item_type == "number" else "[]string"
132
+ elif f.type == "object":
133
+ go_t = "map[string]interface{}"
134
+ go_fields.append(f'\t{field_name} {go_t} `json:"{f.name}"`')
135
+
136
+ go_struct = f'''// AUTO-GENERATED BY JIT PROTOCOL SYNTHESIS FRAMEWORK
137
+ package models
138
+
139
+ type {schema.name} struct {{
140
+ {chr(10).join(go_fields)}
141
+ }}
142
+ '''
143
+ return {"proto": proto, "golang": go_struct}
144
+
145
+ def compile(self, schema: IRSchema) -> Dict[str, Any]:
146
+ self._ensure_dirs()
147
+ base = schema.route.replace("-", "_")
148
+
149
+ py_code = self.generate_python(schema)
150
+ py_path = os.path.join(self.output_dir, "python", f"{base}.py")
151
+ with open(py_path, "w", encoding="utf-8") as f:
152
+ f.write(py_code)
153
+
154
+ ts_code = self.generate_typescript(schema)
155
+ ts_path = os.path.join(self.output_dir, "typescript", f"{base}.ts")
156
+ with open(ts_path, "w", encoding="utf-8") as f:
157
+ f.write(ts_code)
158
+
159
+ go_res = self.generate_golang(schema)
160
+ proto_path = os.path.join(self.output_dir, "golang", f"{base}.proto")
161
+ with open(proto_path, "w", encoding="utf-8") as f:
162
+ f.write(go_res["proto"])
163
+
164
+ go_path = os.path.join(self.output_dir, "golang", f"{base}.go")
165
+ with open(go_path, "w", encoding="utf-8") as f:
166
+ f.write(go_res["golang"])
167
+
168
+ ir_dict = {
169
+ "name": schema.name,
170
+ "version": schema.version,
171
+ "route": schema.route,
172
+ "fields": {k: v.__dict__ for k, v in schema.fields.items()},
173
+ "frozen_at": schema.frozen_at,
174
+ }
175
+ ir_path = os.path.join(self.output_dir, "schemas", f"{base}.json")
176
+ with open(ir_path, "w", encoding="utf-8") as f:
177
+ json.dump(ir_dict, f, indent=2)
178
+
179
+ return {
180
+ "schema": schema,
181
+ "files": {
182
+ "python": py_path,
183
+ "typescript": ts_path,
184
+ "proto": proto_path,
185
+ "golang": go_path,
186
+ "ir_json": ir_path,
187
+ },
188
+ }
@@ -0,0 +1,200 @@
1
+ import inspect
2
+ import time
3
+ from typing import Any, Callable, Dict, List, Optional, Type, Union
4
+ from pydantic import BaseModel, ValidationError, create_model
5
+
6
+ from .codegen import CodegenEngine
7
+ from .fallback_handler import FallbackHandler
8
+ from .needle_engine import NeedleEngine
9
+ from .observer import SchemaObserver
10
+ from .router import TypeSafeRouter
11
+ from .types import (
12
+ IRSchema,
13
+ JITExecutionResult,
14
+ JITRequestContext,
15
+ RouteDefinition,
16
+ RouteHandler,
17
+ )
18
+ from .typesafe_client import TypeSafeClient
19
+
20
+
21
+ class JITEngine:
22
+ def __init__(
23
+ self,
24
+ client: Optional[TypeSafeClient] = None,
25
+ needle_engine: Optional[NeedleEngine] = None,
26
+ stability_threshold: int = 5,
27
+ confidence_threshold: float = 0.85,
28
+ fallback_to_needle: bool = True,
29
+ force_needle: bool = False,
30
+ codegen_output_dir: Optional[str] = None,
31
+ on_freeze: Optional[Callable[[Dict[str, Any]], None]] = None,
32
+ on_drift: Optional[Callable[[str, str], None]] = None,
33
+ ):
34
+ self.router = TypeSafeRouter(
35
+ client=client,
36
+ needle_engine=needle_engine,
37
+ fallback_to_needle=fallback_to_needle,
38
+ force_needle=force_needle,
39
+ )
40
+ self.codegen = CodegenEngine(codegen_output_dir)
41
+ self.fast_path_validators: Dict[str, Type[BaseModel]] = {}
42
+ self.on_freeze_callback = on_freeze
43
+ self.on_drift_callback = on_drift
44
+
45
+ async def _internal_on_freeze(schema: IRSchema):
46
+ # 1. Compile static files across TS, Go, Python, IR
47
+ codegen_res = self.codegen.compile(schema)
48
+
49
+ # 2. Build dynamic in-memory Pydantic v2 validator for Phase 3
50
+ self._mount_fast_path_validator(schema)
51
+
52
+ if self.on_freeze_callback:
53
+ if inspect.iscoroutinefunction(self.on_freeze_callback):
54
+ await self.on_freeze_callback(codegen_res)
55
+ else:
56
+ self.on_freeze_callback(codegen_res)
57
+
58
+ def _internal_on_drift(route: str, error: str, payload: Dict[str, Any]):
59
+ # Remove fast-path validator to re-enter dynamic Phase 1 mode
60
+ self.fast_path_validators.pop(route, None)
61
+ if self.on_drift_callback:
62
+ self.on_drift_callback(route, error)
63
+
64
+ self.observer = SchemaObserver(
65
+ stability_threshold=stability_threshold,
66
+ confidence_threshold=confidence_threshold,
67
+ on_freeze=_internal_on_freeze,
68
+ )
69
+
70
+ self.fallback = FallbackHandler(
71
+ router=self.router,
72
+ observer=self.observer,
73
+ on_drift_detected=_internal_on_drift,
74
+ )
75
+
76
+ def register(self, route_def: RouteDefinition) -> "JITEngine":
77
+ self.router.register(route_def)
78
+ return self
79
+
80
+ def route(
81
+ self,
82
+ name: str,
83
+ description: str,
84
+ intent: str,
85
+ enum_fields: Optional[Dict[str, Dict[str, str]]] = None,
86
+ ) -> Callable[[RouteHandler], RouteHandler]:
87
+ """
88
+ Pythonic decorator to register a route handler:
89
+ @engine.route(name="create_invoice", description="...", intent="...")
90
+ async def create_invoice(payload, ctx):
91
+ return {"status": "ok"}
92
+ """
93
+ return self.router.route(
94
+ name=name,
95
+ description=description,
96
+ intent=intent,
97
+ enum_fields=enum_fields,
98
+ )
99
+
100
+ def _mount_fast_path_validator(self, schema: IRSchema) -> None:
101
+ """
102
+ Dynamically create high-speed Pydantic v2 model for 0ms Phase 3 execution
103
+ """
104
+ field_definitions: Dict[str, Any] = {}
105
+
106
+ for name, field_meta in schema.fields.items():
107
+ py_type = str
108
+ if field_meta.type == "number":
109
+ py_type = float
110
+ elif field_meta.type == "boolean":
111
+ py_type = bool
112
+ elif field_meta.type == "array":
113
+ item_t = (
114
+ float
115
+ if field_meta.item_type == "number"
116
+ else (bool if field_meta.item_type == "boolean" else str)
117
+ )
118
+ py_type = List[item_t]
119
+ elif field_meta.type == "object":
120
+ py_type = Dict[str, Any]
121
+
122
+ default_val = ... if field_meta.required else None
123
+ field_definitions[name] = (py_type, default_val)
124
+
125
+ model = create_model(f"{schema.name}FastValidator", **field_definitions)
126
+ self.fast_path_validators[schema.route] = model
127
+
128
+ async def execute(
129
+ self, payload: Dict[str, Any], explicit_route: Optional[str] = None
130
+ ) -> JITExecutionResult:
131
+ start_time = time.time()
132
+ target_route = explicit_route or (
133
+ payload.get("route") if isinstance(payload.get("route"), str) else None
134
+ )
135
+
136
+ is_target_frozen = target_route and self.observer.is_frozen(target_route)
137
+
138
+ # -------------------------------------------------------------
139
+ # Phase 3: Static Fast-Path (Pydantic v2, 0ms AI Latency)
140
+ # -------------------------------------------------------------
141
+ if is_target_frozen and target_route:
142
+ validator = self.fast_path_validators.get(target_route)
143
+ route_def = self.router.get_route(target_route)
144
+
145
+ if validator and route_def:
146
+ try:
147
+ # Validate natively in-memory via Pydantic v2 C-extension
148
+ validated = validator.model_validate(payload)
149
+ clean_data = validated.model_dump()
150
+
151
+ exec_time_ms = round((time.time() - start_time) * 1000, 2)
152
+ ctx = JITRequestContext(
153
+ route=target_route,
154
+ phase="phase3_frozen",
155
+ execution_time_ms=exec_time_ms,
156
+ ai_latency_ms=0.0, # 0ms AI latency!
157
+ intent_confidence=1.0,
158
+ is_fallback=False,
159
+ )
160
+
161
+ data = await self.router._invoke_handler(
162
+ route_def.handler, clean_data, ctx
163
+ )
164
+ return JITExecutionResult(success=True, data=data, context=ctx)
165
+ except ValidationError as val_err:
166
+ # Schema Drift detected! Downgrade to Phase 1 & re-arm Phase 2 for v2
167
+ fallback_res = await self.fallback.handle_fallback(
168
+ target_route, str(val_err), payload
169
+ )
170
+ return JITExecutionResult(
171
+ success=True,
172
+ data=fallback_res["result"],
173
+ context=fallback_res["context"],
174
+ )
175
+
176
+ # -------------------------------------------------------------
177
+ # Phase 1: Dynamic Semantic Routing (TypeSafe Jev or Needle)
178
+ # -------------------------------------------------------------
179
+ res = await self.router.handle(payload, preferred_route=target_route)
180
+
181
+ # Phase 2: Observation & Stability Tracking
182
+ obs = await self.observer.observe(
183
+ res["route"],
184
+ res["normalized_payload"],
185
+ res["context"].intent_confidence,
186
+ )
187
+
188
+ res["context"].phase = "phase3_frozen" if obs["stable"] else "phase1_dynamic"
189
+
190
+ return JITExecutionResult(
191
+ success=True, data=res["result"], context=res["context"]
192
+ )
193
+
194
+ def get_route_status(self, route: str) -> Dict[str, Any]:
195
+ return {
196
+ "route": route,
197
+ "is_frozen": self.observer.is_frozen(route),
198
+ "metrics": self.observer.get_metrics(route),
199
+ "frozen_schema": self.observer.get_frozen_schema(route),
200
+ }
@@ -0,0 +1,51 @@
1
+ import sys
2
+ from typing import Any, Callable, Dict, Optional
3
+ from .observer import SchemaObserver
4
+ from .router import TypeSafeRouter
5
+
6
+
7
+ class FallbackHandler:
8
+ """
9
+ Handles Phase 3 Fast-Path validation errors (Schema Drift)
10
+ by downgrading the request back to Phase 1 dynamic semantic resolution
11
+ and re-arming Phase 2 observation for the next contract version (v2, v3...).
12
+ """
13
+
14
+ def __init__(
15
+ self,
16
+ router: TypeSafeRouter,
17
+ observer: SchemaObserver,
18
+ on_drift_detected: Optional[Callable[[str, str, Dict[str, Any]], None]] = None,
19
+ ):
20
+ self.router = router
21
+ self.observer = observer
22
+ self.on_drift_detected = on_drift_detected or self._default_log_drift
23
+
24
+ @staticmethod
25
+ def _default_log_drift(route: str, error: str, payload: Dict[str, Any]) -> None:
26
+ sys.stderr.write(
27
+ f"[FallbackHandler] ⚠️ Schema drift detected on '{route}': {error}. Downgrading to Phase 1.\n"
28
+ )
29
+ sys.stderr.flush()
30
+
31
+ async def handle_fallback(
32
+ self, route: str, validation_error: str, raw_payload: Dict[str, Any]
33
+ ) -> Dict[str, Any]:
34
+ # 1. Notify drift
35
+ self.on_drift_detected(route, validation_error, raw_payload)
36
+
37
+ # 2. Reset observer for this route to initiate v2 observation
38
+ self.observer.reset_route(route)
39
+
40
+ # 3. Reroute through Phase 1 dynamic semantic router
41
+ res = await self.router.handle(raw_payload, preferred_route=route)
42
+
43
+ # 4. Mark context as fallback
44
+ res["context"].is_fallback = True
45
+
46
+ # 5. Record drifted payload as sample 1 of the new version
47
+ await self.observer.observe(
48
+ route, res["normalized_payload"], res["context"].intent_confidence
49
+ )
50
+
51
+ return res