jit-protocol 1.0.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.
- jit_api/__init__.py +39 -0
- jit_api/codegen.py +188 -0
- jit_api/engine.py +200 -0
- jit_api/fallback_handler.py +51 -0
- jit_api/needle_engine.py +159 -0
- jit_api/observer.py +183 -0
- jit_api/router.py +194 -0
- jit_api/types.py +69 -0
- jit_api/typesafe_client.py +75 -0
- jit_protocol-1.0.0.dist-info/METADATA +92 -0
- jit_protocol-1.0.0.dist-info/RECORD +13 -0
- jit_protocol-1.0.0.dist-info/WHEEL +5 -0
- jit_protocol-1.0.0.dist-info/top_level.txt +1 -0
jit_api/__init__.py
ADDED
|
@@ -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
|
+
]
|
jit_api/codegen.py
ADDED
|
@@ -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
|
+
}
|
jit_api/engine.py
ADDED
|
@@ -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
|
jit_api/needle_engine.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
from .types import RouteDefinition
|
|
6
|
+
|
|
7
|
+
NEEDLE_NATIVE_AVAILABLE = False
|
|
8
|
+
try:
|
|
9
|
+
import needle
|
|
10
|
+
NEEDLE_NATIVE_AVAILABLE = True
|
|
11
|
+
except ImportError:
|
|
12
|
+
NEEDLE_NATIVE_AVAILABLE = False
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class NeedleEngine:
|
|
16
|
+
"""
|
|
17
|
+
In-process Native Cactus Needle 3.0 Engine.
|
|
18
|
+
Executes tool calling and structured extraction locally with 0 API cost.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, enable_guardrail: bool = True):
|
|
22
|
+
self.enable_guardrail = enable_guardrail
|
|
23
|
+
self.is_native_available = NEEDLE_NATIVE_AVAILABLE
|
|
24
|
+
|
|
25
|
+
def route_to_needle_tool(self, route_def: RouteDefinition) -> Dict[str, Any]:
|
|
26
|
+
properties: Dict[str, Any] = {}
|
|
27
|
+
|
|
28
|
+
if route_def.enum_fields:
|
|
29
|
+
for field_name, criteria in route_def.enum_fields.items():
|
|
30
|
+
properties[field_name] = {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"enum": list(criteria.keys()),
|
|
33
|
+
"description": f"Allowed options: {', '.join(criteria.keys())}",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
"name": route_def.route,
|
|
38
|
+
"description": f"{route_def.description}. Intent criteria: {route_def.intent_criteria}",
|
|
39
|
+
"parameters": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"properties": properties,
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
def check_security_guardrail(self, payload: Dict[str, Any]) -> None:
|
|
46
|
+
if not self.enable_guardrail:
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
dangerous_patterns = [
|
|
50
|
+
r"('|\b;|--|\/\*|\b(DROP\s+TABLE|UNION\s+SELECT|ALTER\s+TABLE|EXEC\s*\()\b)",
|
|
51
|
+
r"<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>",
|
|
52
|
+
r"\b(rm\s+-rf|chmod\s+777|wget\s+http|curl\s+http)\b",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
text = json.dumps(payload)
|
|
56
|
+
for pattern in dangerous_patterns:
|
|
57
|
+
if re.search(pattern, text, re.IGNORECASE):
|
|
58
|
+
raise PermissionError(
|
|
59
|
+
"[NeedleEngine] Request blocked by local security guardrail (Matched exploit pattern)"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def execute(
|
|
63
|
+
self,
|
|
64
|
+
raw_input: Dict[str, Any],
|
|
65
|
+
routes: List[RouteDefinition],
|
|
66
|
+
preferred_route: Optional[str] = None,
|
|
67
|
+
) -> Dict[str, Any]:
|
|
68
|
+
start_time = time.time()
|
|
69
|
+
|
|
70
|
+
# 1. Local Guardrail Check
|
|
71
|
+
self.check_security_guardrail(raw_input)
|
|
72
|
+
|
|
73
|
+
# Bypass if preferredRoute is explicitly targeted
|
|
74
|
+
if preferred_route and any(r.route == preferred_route for r in routes):
|
|
75
|
+
latency_ms = round((time.time() - start_time) * 1000, 2)
|
|
76
|
+
return {
|
|
77
|
+
"route": preferred_route,
|
|
78
|
+
"normalized_payload": dict(raw_input),
|
|
79
|
+
"confidence": 1.0,
|
|
80
|
+
"latency_ms": latency_ms,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
tools = [self.route_to_needle_tool(r) for r in routes]
|
|
84
|
+
query = (
|
|
85
|
+
raw_input.get("message")
|
|
86
|
+
or raw_input.get("text")
|
|
87
|
+
or json.dumps(raw_input)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# 2. Try Native Needle In-Process Execution
|
|
91
|
+
if self.is_native_available:
|
|
92
|
+
try:
|
|
93
|
+
agent = needle.Needle(tools=tools)
|
|
94
|
+
res = agent.run(str(query))
|
|
95
|
+
latency_ms = round((time.time() - start_time) * 1000, 2)
|
|
96
|
+
|
|
97
|
+
function_calls = res.get("function_calls", [])
|
|
98
|
+
if function_calls:
|
|
99
|
+
call = function_calls[0]
|
|
100
|
+
normalized = dict(raw_input)
|
|
101
|
+
normalized.update(call.get("arguments", {}))
|
|
102
|
+
return {
|
|
103
|
+
"route": call["name"],
|
|
104
|
+
"normalized_payload": normalized,
|
|
105
|
+
"confidence": float(res.get("confidence", 0.95)),
|
|
106
|
+
"latency_ms": latency_ms,
|
|
107
|
+
"reasoning": res.get("reasoning"),
|
|
108
|
+
}
|
|
109
|
+
except Exception:
|
|
110
|
+
# If weights download is offline, fall back to semantic matcher
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
# 3. Deterministic In-Memory Matcher Fallback
|
|
114
|
+
return self._embedded_match(str(query), raw_input, routes, start_time)
|
|
115
|
+
|
|
116
|
+
def _embedded_match(
|
|
117
|
+
self,
|
|
118
|
+
query: str,
|
|
119
|
+
raw_input: Dict[str, Any],
|
|
120
|
+
routes: List[RouteDefinition],
|
|
121
|
+
start_time: float,
|
|
122
|
+
) -> Dict[str, Any]:
|
|
123
|
+
q_lower = query.lower()
|
|
124
|
+
best_route = routes[0]
|
|
125
|
+
highest_score = -1
|
|
126
|
+
|
|
127
|
+
for r in routes:
|
|
128
|
+
score = 0
|
|
129
|
+
text_corpus = f"{r.route} {r.description} {r.intent_criteria}".lower()
|
|
130
|
+
words = [w for w in re.findall(r"\w+", text_corpus) if len(w) > 2]
|
|
131
|
+
|
|
132
|
+
for w in words:
|
|
133
|
+
if w in q_lower:
|
|
134
|
+
score += 1
|
|
135
|
+
|
|
136
|
+
if score > highest_score:
|
|
137
|
+
highest_score = score
|
|
138
|
+
best_route = r
|
|
139
|
+
|
|
140
|
+
normalized_payload = dict(raw_input)
|
|
141
|
+
|
|
142
|
+
if best_route.enum_fields:
|
|
143
|
+
for field, options in best_route.enum_fields.items():
|
|
144
|
+
if field not in normalized_payload:
|
|
145
|
+
for opt_key in options.keys():
|
|
146
|
+
if opt_key.lower() in q_lower:
|
|
147
|
+
normalized_payload[field] = opt_key
|
|
148
|
+
break
|
|
149
|
+
|
|
150
|
+
confidence = 0.94 if highest_score > 0 else 0.86
|
|
151
|
+
latency_ms = round((time.time() - start_time) * 1000, 2)
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
"route": best_route.route,
|
|
155
|
+
"normalized_payload": normalized_payload,
|
|
156
|
+
"confidence": confidence,
|
|
157
|
+
"latency_ms": latency_ms,
|
|
158
|
+
"reasoning": f"Needle matched route '{best_route.route}' with score {highest_score}",
|
|
159
|
+
}
|
jit_api/observer.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import inspect
|
|
3
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
4
|
+
from .types import IRField, IRFieldType, IRSchema, StabilityMetrics
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RouteObservationState:
|
|
8
|
+
def __init__(self):
|
|
9
|
+
self.samples: List[Dict[str, Any]] = []
|
|
10
|
+
self.current_signature: Optional[str] = None
|
|
11
|
+
self.consecutive_matches: int = 0
|
|
12
|
+
self.is_frozen: bool = False
|
|
13
|
+
self.frozen_schema: Optional[IRSchema] = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SchemaObserver:
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
stability_threshold: int = 5,
|
|
20
|
+
confidence_threshold: float = 0.85,
|
|
21
|
+
on_freeze: Optional[Callable[[IRSchema], Any]] = None,
|
|
22
|
+
):
|
|
23
|
+
self.stability_threshold = stability_threshold
|
|
24
|
+
self.confidence_threshold = confidence_threshold
|
|
25
|
+
self.on_freeze = on_freeze
|
|
26
|
+
self.states: Dict[str, RouteObservationState] = {}
|
|
27
|
+
self.route_versions: Dict[str, int] = {}
|
|
28
|
+
|
|
29
|
+
def infer_type(self, value: Any) -> IRFieldType:
|
|
30
|
+
if isinstance(value, bool):
|
|
31
|
+
return "boolean"
|
|
32
|
+
if isinstance(value, (int, float)):
|
|
33
|
+
return "number"
|
|
34
|
+
if isinstance(value, list):
|
|
35
|
+
return "array"
|
|
36
|
+
if isinstance(value, dict):
|
|
37
|
+
return "object"
|
|
38
|
+
return "string"
|
|
39
|
+
|
|
40
|
+
def generate_signature(self, payload: Dict[str, Any]) -> str:
|
|
41
|
+
parts = []
|
|
42
|
+
for key in sorted(payload.keys()):
|
|
43
|
+
val = payload[key]
|
|
44
|
+
val_type = self.infer_type(val)
|
|
45
|
+
if val_type == "object" and isinstance(val, dict):
|
|
46
|
+
parts.append(f"{key}:{{{self.generate_signature(val)}}}")
|
|
47
|
+
elif val_type == "array" and isinstance(val, list):
|
|
48
|
+
item_type = self.infer_type(val[0]) if val else "unknown"
|
|
49
|
+
parts.append(f"{key}:array[{item_type}]")
|
|
50
|
+
else:
|
|
51
|
+
parts.append(f"{key}:{val_type}")
|
|
52
|
+
return ";".join(parts)
|
|
53
|
+
|
|
54
|
+
def extract_ir_schema(
|
|
55
|
+
self, route: str, sample: Dict[str, Any], version: int = 1
|
|
56
|
+
) -> IRSchema:
|
|
57
|
+
fields: Dict[str, IRField] = {}
|
|
58
|
+
|
|
59
|
+
for key, val in sample.items():
|
|
60
|
+
field_type = self.infer_type(val)
|
|
61
|
+
field = IRField(name=key, type=field_type, required=True)
|
|
62
|
+
|
|
63
|
+
if field_type == "array" and isinstance(val, list):
|
|
64
|
+
field.item_type = self.infer_type(val[0]) if val else "string"
|
|
65
|
+
elif field_type == "object" and isinstance(val, dict):
|
|
66
|
+
nested: Dict[str, IRField] = {}
|
|
67
|
+
for sub_k, sub_v in val.items():
|
|
68
|
+
nested[sub_k] = IRField(
|
|
69
|
+
name=sub_k, type=self.infer_type(sub_v), required=True
|
|
70
|
+
)
|
|
71
|
+
field.properties = nested
|
|
72
|
+
|
|
73
|
+
fields[key] = field
|
|
74
|
+
|
|
75
|
+
pascal_name = "".join(part.capitalize() for part in route.split("_")) + "Request"
|
|
76
|
+
|
|
77
|
+
return IRSchema(
|
|
78
|
+
name=pascal_name,
|
|
79
|
+
version=version,
|
|
80
|
+
route=route,
|
|
81
|
+
fields=fields,
|
|
82
|
+
sample_payload=sample,
|
|
83
|
+
frozen_at=datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def get_route_version(self, route: str) -> int:
|
|
87
|
+
return self.route_versions.get(route, 1)
|
|
88
|
+
|
|
89
|
+
def reset_route(self, route: str) -> None:
|
|
90
|
+
next_version = self.get_route_version(route) + 1
|
|
91
|
+
self.route_versions[route] = next_version
|
|
92
|
+
if route in self.states:
|
|
93
|
+
del self.states[route]
|
|
94
|
+
|
|
95
|
+
def is_frozen(self, route: str) -> bool:
|
|
96
|
+
return self.states.get(route, RouteObservationState()).is_frozen
|
|
97
|
+
|
|
98
|
+
def get_frozen_schema(self, route: str) -> Optional[IRSchema]:
|
|
99
|
+
return self.states.get(route, RouteObservationState()).frozen_schema
|
|
100
|
+
|
|
101
|
+
def get_metrics(self, route: str) -> StabilityMetrics:
|
|
102
|
+
state = self.states.get(route)
|
|
103
|
+
if not state or not state.samples:
|
|
104
|
+
return StabilityMetrics(
|
|
105
|
+
sample_count=0,
|
|
106
|
+
consecutive_matches=0,
|
|
107
|
+
required_threshold=self.stability_threshold,
|
|
108
|
+
confidence_threshold=self.confidence_threshold,
|
|
109
|
+
avg_confidence=0.0,
|
|
110
|
+
is_stable=False,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
recent = state.samples[-self.stability_threshold :]
|
|
114
|
+
avg_conf = sum(s["confidence"] for s in recent) / len(recent)
|
|
115
|
+
is_stable = (
|
|
116
|
+
state.consecutive_matches >= self.stability_threshold
|
|
117
|
+
and avg_conf >= self.confidence_threshold
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return StabilityMetrics(
|
|
121
|
+
sample_count=len(state.samples),
|
|
122
|
+
consecutive_matches=state.consecutive_matches,
|
|
123
|
+
required_threshold=self.stability_threshold,
|
|
124
|
+
confidence_threshold=self.confidence_threshold,
|
|
125
|
+
avg_confidence=round(avg_conf, 3),
|
|
126
|
+
is_stable=is_stable,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
async def observe(
|
|
130
|
+
self, route: str, payload: Dict[str, Any], confidence: float = 1.0
|
|
131
|
+
) -> Dict[str, Any]:
|
|
132
|
+
state = self.states.setdefault(route, RouteObservationState())
|
|
133
|
+
|
|
134
|
+
if state.is_frozen and state.frozen_schema:
|
|
135
|
+
return {
|
|
136
|
+
"stable": True,
|
|
137
|
+
"consecutive_matches": state.consecutive_matches,
|
|
138
|
+
"metrics": self.get_metrics(route),
|
|
139
|
+
"frozen_schema": state.frozen_schema,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
signature = self.generate_signature(payload)
|
|
143
|
+
state.samples.append(
|
|
144
|
+
{
|
|
145
|
+
"payload": payload,
|
|
146
|
+
"signature": signature,
|
|
147
|
+
"confidence": confidence,
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if state.current_signature == signature:
|
|
152
|
+
state.consecutive_matches += 1
|
|
153
|
+
else:
|
|
154
|
+
state.current_signature = signature
|
|
155
|
+
state.consecutive_matches = 1
|
|
156
|
+
|
|
157
|
+
metrics = self.get_metrics(route)
|
|
158
|
+
|
|
159
|
+
if metrics.is_stable and not state.is_frozen:
|
|
160
|
+
state.is_frozen = True
|
|
161
|
+
schema = self.extract_ir_schema(
|
|
162
|
+
route, payload, version=self.get_route_version(route)
|
|
163
|
+
)
|
|
164
|
+
state.frozen_schema = schema
|
|
165
|
+
|
|
166
|
+
if self.on_freeze:
|
|
167
|
+
if inspect.iscoroutinefunction(self.on_freeze):
|
|
168
|
+
await self.on_freeze(schema)
|
|
169
|
+
else:
|
|
170
|
+
self.on_freeze(schema)
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
"stable": True,
|
|
174
|
+
"consecutive_matches": state.consecutive_matches,
|
|
175
|
+
"metrics": metrics,
|
|
176
|
+
"frozen_schema": schema,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
"stable": False,
|
|
181
|
+
"consecutive_matches": state.consecutive_matches,
|
|
182
|
+
"metrics": metrics,
|
|
183
|
+
}
|
jit_api/router.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import inspect
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
5
|
+
from .needle_engine import NeedleEngine
|
|
6
|
+
from .types import JITRequestContext, RouteDefinition, RouteHandler
|
|
7
|
+
from .typesafe_client import TypeSafeClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TypeSafeRouter:
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
client: Optional[TypeSafeClient] = None,
|
|
14
|
+
needle_engine: Optional[NeedleEngine] = None,
|
|
15
|
+
fallback_to_needle: bool = True,
|
|
16
|
+
force_needle: bool = False,
|
|
17
|
+
security_threshold: float = 0.8,
|
|
18
|
+
):
|
|
19
|
+
self.client = client or TypeSafeClient()
|
|
20
|
+
self.needle_engine = needle_engine or NeedleEngine()
|
|
21
|
+
self.fallback_to_needle = fallback_to_needle
|
|
22
|
+
self.force_needle = force_needle
|
|
23
|
+
self.security_threshold = security_threshold
|
|
24
|
+
self.routes: Dict[str, RouteDefinition] = {}
|
|
25
|
+
|
|
26
|
+
def register(self, route_def: RouteDefinition) -> "TypeSafeRouter":
|
|
27
|
+
self.routes[route_def.route] = route_def
|
|
28
|
+
return self
|
|
29
|
+
|
|
30
|
+
def route(
|
|
31
|
+
self,
|
|
32
|
+
name: str,
|
|
33
|
+
description: str,
|
|
34
|
+
intent: str,
|
|
35
|
+
enum_fields: Optional[Dict[str, Dict[str, str]]] = None,
|
|
36
|
+
) -> Callable[[RouteHandler], RouteHandler]:
|
|
37
|
+
"""
|
|
38
|
+
Decorator to register a route handler cleanly:
|
|
39
|
+
@router.route(name="create_invoice", description="...", intent="...")
|
|
40
|
+
def handler(payload, ctx):
|
|
41
|
+
...
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def decorator(fn: RouteHandler) -> RouteHandler:
|
|
45
|
+
self.register(
|
|
46
|
+
RouteDefinition(
|
|
47
|
+
route=name,
|
|
48
|
+
description=description,
|
|
49
|
+
intent_criteria=intent,
|
|
50
|
+
handler=fn,
|
|
51
|
+
enum_fields=enum_fields,
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
return fn
|
|
55
|
+
|
|
56
|
+
return decorator
|
|
57
|
+
|
|
58
|
+
def get_route(self, route_name: str) -> Optional[RouteDefinition]:
|
|
59
|
+
return self.routes.get(route_name)
|
|
60
|
+
|
|
61
|
+
def get_routes(self) -> List[RouteDefinition]:
|
|
62
|
+
return list(self.routes.values())
|
|
63
|
+
|
|
64
|
+
async def _invoke_handler(
|
|
65
|
+
self, handler: RouteHandler, payload: Dict[str, Any], ctx: JITRequestContext
|
|
66
|
+
) -> Any:
|
|
67
|
+
if inspect.iscoroutinefunction(handler):
|
|
68
|
+
return await handler(payload, ctx)
|
|
69
|
+
return handler(payload, ctx)
|
|
70
|
+
|
|
71
|
+
async def handle(
|
|
72
|
+
self, raw_input: Dict[str, Any], preferred_route: Optional[str] = None
|
|
73
|
+
) -> Dict[str, Any]:
|
|
74
|
+
start_time = time.time()
|
|
75
|
+
|
|
76
|
+
if not self.routes:
|
|
77
|
+
raise RuntimeError("[TypeSafeRouter] No routes registered.")
|
|
78
|
+
|
|
79
|
+
use_needle = self.force_needle or (
|
|
80
|
+
not self.client.has_api_key() and self.fallback_to_needle
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# -------------------------------------------------------------
|
|
84
|
+
# Branch A: Needle Local In-Process SLM Fallback
|
|
85
|
+
# -------------------------------------------------------------
|
|
86
|
+
if use_needle:
|
|
87
|
+
routes_list = list(self.routes.values())
|
|
88
|
+
needle_res = self.needle_engine.execute(
|
|
89
|
+
raw_input, routes_list, preferred_route
|
|
90
|
+
)
|
|
91
|
+
matched_route = needle_res["route"]
|
|
92
|
+
route_def = self.routes.get(matched_route)
|
|
93
|
+
if not route_def:
|
|
94
|
+
raise RuntimeError(
|
|
95
|
+
f"[TypeSafeRouter] Matched route '{matched_route}' not found in registry."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
exec_time_ms = round((time.time() - start_time) * 1000, 2)
|
|
99
|
+
ctx = JITRequestContext(
|
|
100
|
+
route=matched_route,
|
|
101
|
+
phase="phase1_dynamic",
|
|
102
|
+
execution_time_ms=exec_time_ms,
|
|
103
|
+
ai_latency_ms=needle_res["latency_ms"],
|
|
104
|
+
intent_confidence=needle_res["confidence"],
|
|
105
|
+
engine_used="needle",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
result = await self._invoke_handler(
|
|
109
|
+
route_def.handler, needle_res["normalized_payload"], ctx
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
"route": matched_route,
|
|
114
|
+
"normalized_payload": needle_res["normalized_payload"],
|
|
115
|
+
"result": result,
|
|
116
|
+
"context": ctx,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# -------------------------------------------------------------
|
|
120
|
+
# Branch B: TypeSafe Jev Cloud API (Choice + Select + Noul)
|
|
121
|
+
# -------------------------------------------------------------
|
|
122
|
+
questions = {
|
|
123
|
+
"is_malicious": TypeSafeClient.noul(
|
|
124
|
+
"The input payload contains malicious injection attacks, SQLi, code execution, or security exploits"
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if not preferred_route or preferred_route not in self.routes:
|
|
129
|
+
criteria = {
|
|
130
|
+
r.route: r.intent_criteria or r.description
|
|
131
|
+
for r in self.routes.values()
|
|
132
|
+
}
|
|
133
|
+
questions["intent"] = TypeSafeClient.choice(criteria)
|
|
134
|
+
|
|
135
|
+
res_data = self.client.system_one(state=raw_input, questions=questions)
|
|
136
|
+
response = res_data["response"]
|
|
137
|
+
latency_ms = res_data["latency_ms"]
|
|
138
|
+
answers = response.get("answers", {})
|
|
139
|
+
|
|
140
|
+
# Security check
|
|
141
|
+
malicious_ans = answers.get("is_malicious")
|
|
142
|
+
if malicious_ans and malicious_ans.get("noul", 0) >= self.security_threshold:
|
|
143
|
+
risk = malicious_ans.get("noul", 0)
|
|
144
|
+
raise PermissionError(
|
|
145
|
+
f"[TypeSafeRouter] Request blocked by Noul security guardrail (Risk score: {risk:.2f} >= {self.security_threshold})"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Route decision
|
|
149
|
+
matched_route = preferred_route
|
|
150
|
+
confidence = 1.0
|
|
151
|
+
|
|
152
|
+
if not matched_route or matched_route not in self.routes:
|
|
153
|
+
intent_ans = answers.get("intent", {})
|
|
154
|
+
matched_route = intent_ans.get("choice")
|
|
155
|
+
confidence = intent_ans.get("confidence", 1.0)
|
|
156
|
+
|
|
157
|
+
route_def = self.routes.get(matched_route)
|
|
158
|
+
if not route_def:
|
|
159
|
+
raise RuntimeError(
|
|
160
|
+
f"[TypeSafeRouter] Matched route '{matched_route}' not found in registry."
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Enum Extraction
|
|
164
|
+
normalized_payload = dict(raw_input)
|
|
165
|
+
if route_def.enum_fields:
|
|
166
|
+
enum_q = {
|
|
167
|
+
field: TypeSafeClient.choice(opts)
|
|
168
|
+
for field, opts in route_def.enum_fields.items()
|
|
169
|
+
}
|
|
170
|
+
enum_res = self.client.system_one(state=raw_input, questions=enum_q)
|
|
171
|
+
for f_name, f_ans in enum_res.get("response", {}).get("answers", {}).items():
|
|
172
|
+
if f_ans.get("type") == "choice":
|
|
173
|
+
normalized_payload[f_name] = f_ans.get("choice")
|
|
174
|
+
|
|
175
|
+
exec_time_ms = round((time.time() - start_time) * 1000, 2)
|
|
176
|
+
ctx = JITRequestContext(
|
|
177
|
+
route=matched_route,
|
|
178
|
+
phase="phase1_dynamic",
|
|
179
|
+
execution_time_ms=exec_time_ms,
|
|
180
|
+
ai_latency_ms=latency_ms,
|
|
181
|
+
intent_confidence=confidence,
|
|
182
|
+
engine_used="typesafe",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
result = await self._invoke_handler(
|
|
186
|
+
route_def.handler, normalized_payload, ctx
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
"route": matched_route,
|
|
191
|
+
"normalized_payload": normalized_payload,
|
|
192
|
+
"result": result,
|
|
193
|
+
"context": ctx,
|
|
194
|
+
}
|
jit_api/types.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Any, Callable, Coroutine, Dict, List, Literal, Optional, Union
|
|
3
|
+
|
|
4
|
+
LifecyclePhase = Literal["phase1_dynamic", "phase2_observing", "phase3_frozen"]
|
|
5
|
+
IRFieldType = Literal["string", "number", "boolean", "enum", "array", "object"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class IRField:
|
|
10
|
+
name: str
|
|
11
|
+
type: IRFieldType
|
|
12
|
+
required: bool = True
|
|
13
|
+
enum_values: Optional[List[str]] = None
|
|
14
|
+
description: Optional[str] = None
|
|
15
|
+
item_type: Optional[IRFieldType] = None
|
|
16
|
+
properties: Optional[Dict[str, "IRField"]] = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class IRSchema:
|
|
21
|
+
name: str
|
|
22
|
+
version: int
|
|
23
|
+
route: str
|
|
24
|
+
fields: Dict[str, IRField]
|
|
25
|
+
description: Optional[str] = None
|
|
26
|
+
sample_payload: Optional[Dict[str, Any]] = None
|
|
27
|
+
frozen_at: Optional[str] = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class JITRequestContext:
|
|
32
|
+
route: str
|
|
33
|
+
phase: LifecyclePhase
|
|
34
|
+
execution_time_ms: float
|
|
35
|
+
ai_latency_ms: float
|
|
36
|
+
intent_confidence: float = 1.0
|
|
37
|
+
is_fallback: bool = False
|
|
38
|
+
engine_used: Optional[Literal["typesafe", "needle"]] = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class JITExecutionResult:
|
|
43
|
+
success: bool
|
|
44
|
+
data: Any = None
|
|
45
|
+
error: Optional[str] = None
|
|
46
|
+
context: Optional[JITRequestContext] = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class StabilityMetrics:
|
|
51
|
+
sample_count: int
|
|
52
|
+
consecutive_matches: int
|
|
53
|
+
required_threshold: int
|
|
54
|
+
confidence_threshold: float
|
|
55
|
+
avg_confidence: float
|
|
56
|
+
is_stable: bool
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Route handler can be async or sync
|
|
60
|
+
RouteHandler = Callable[[Dict[str, Any], JITRequestContext], Union[Any, Coroutine[Any, Any, Any]]]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class RouteDefinition:
|
|
65
|
+
route: str
|
|
66
|
+
description: str
|
|
67
|
+
intent_criteria: str
|
|
68
|
+
handler: RouteHandler
|
|
69
|
+
enum_fields: Optional[Dict[str, Dict[str, str]]] = None
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
load_dotenv()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TypeSafeClient:
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
api_key: Optional[str] = None,
|
|
14
|
+
base_url: Optional[str] = None,
|
|
15
|
+
model: Optional[str] = None,
|
|
16
|
+
timeout_sec: float = 10.0,
|
|
17
|
+
):
|
|
18
|
+
self.api_key = (
|
|
19
|
+
api_key if api_key is not None else os.getenv("TYPESAFE_API_KEY", "")
|
|
20
|
+
)
|
|
21
|
+
self.base_url = base_url or "https://api.typesafe.ai/v1/systemone"
|
|
22
|
+
self.default_model = model or "jev-latest"
|
|
23
|
+
self.timeout_sec = timeout_sec
|
|
24
|
+
|
|
25
|
+
if not self.has_api_key():
|
|
26
|
+
# Will trigger Needle fallback automatically
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
def has_api_key(self) -> bool:
|
|
30
|
+
return bool(self.api_key and self.api_key.strip())
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def choice(criteria: Dict[str, str]) -> Dict[str, Any]:
|
|
34
|
+
return {"type": "choice", "criteria": criteria}
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def noul(instructions: str) -> Dict[str, Any]:
|
|
38
|
+
return {"type": "noul", "instructions": instructions}
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def score(levels: Dict[str, str]) -> Dict[str, Any]:
|
|
42
|
+
return {"type": "score", "levels": levels}
|
|
43
|
+
|
|
44
|
+
def system_one(
|
|
45
|
+
self,
|
|
46
|
+
state: Dict[str, Any],
|
|
47
|
+
questions: Dict[str, Any],
|
|
48
|
+
model: Optional[str] = None,
|
|
49
|
+
) -> Dict[str, Any]:
|
|
50
|
+
start_time = time.time()
|
|
51
|
+
payload = {
|
|
52
|
+
"model": model or self.default_model,
|
|
53
|
+
"state": state,
|
|
54
|
+
"questions": questions,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
headers = {
|
|
58
|
+
"Content-Type": "application/json",
|
|
59
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
resp = requests.post(
|
|
63
|
+
self.base_url,
|
|
64
|
+
json=payload,
|
|
65
|
+
headers=headers,
|
|
66
|
+
timeout=self.timeout_sec,
|
|
67
|
+
)
|
|
68
|
+
latency_ms = round((time.time() - start_time) * 1000, 2)
|
|
69
|
+
|
|
70
|
+
if not resp.ok:
|
|
71
|
+
raise RuntimeError(
|
|
72
|
+
f"TypeSafe Jev API error ({resp.status_code}): {resp.text}"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return {"response": resp.json(), "latency_ms": latency_ms}
|
|
@@ -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,13 @@
|
|
|
1
|
+
jit_api/__init__.py,sha256=vP1f_zvwGTGUwcltgPmSYRPxvDLLmezjB8W7edXlB5U,933
|
|
2
|
+
jit_api/codegen.py,sha256=e_InQU3Zikive9olRR0ewEmiVXLAUxnNVG_ZEVzioyM,6179
|
|
3
|
+
jit_api/engine.py,sha256=L6H3gT8XTFuvLQQrpWdtQfSNY_mLhYES-2igMsXjzvo,7745
|
|
4
|
+
jit_api/fallback_handler.py,sha256=GZ0FHg0HxTDVUq9hBttttAn12aLmL_U03GLUzHeLIys,1777
|
|
5
|
+
jit_api/needle_engine.py,sha256=IMmTDuqYZuFUsESWt8LHEotLFHp1GJhstskyx1ao7ds,5594
|
|
6
|
+
jit_api/observer.py,sha256=5Z4KJS1odGXwdCs0SgYy8H3MCOvHyzsPHBsAPQIlcTI,6558
|
|
7
|
+
jit_api/router.py,sha256=ANFPGKysC5e0GYj3U0Aj6ArsFrgw02OfDmmdsaFYxuQ,7119
|
|
8
|
+
jit_api/types.py,sha256=e-wx0VjwTmVYRIJ-d0OLu8JFOYICZDkH8sciexc5ufw,1721
|
|
9
|
+
jit_api/typesafe_client.py,sha256=8oCoH47o96yPpe5Jl_PF-GdX7Vg2nwrrMLMgiIHkFDg,2133
|
|
10
|
+
jit_protocol-1.0.0.dist-info/METADATA,sha256=Rh67Qt7yB9dMv3sc-Zl9AYU-DdwiALUsbxwFe2SUQ6E,3055
|
|
11
|
+
jit_protocol-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
12
|
+
jit_protocol-1.0.0.dist-info/top_level.txt,sha256=9Q3BcLP9-M-McZx7hdVi5kCLXMzDuikk6IpXm4HKrtg,8
|
|
13
|
+
jit_protocol-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
jit_api
|