kalibr 1.0.26__py3-none-any.whl → 1.1.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.
- kalibr/__init__.py +170 -3
- kalibr/__main__.py +3 -203
- kalibr/capsule_middleware.py +108 -0
- kalibr/cli/__init__.py +5 -0
- kalibr/cli/capsule_cmd.py +174 -0
- kalibr/cli/deploy_cmd.py +114 -0
- kalibr/cli/main.py +67 -0
- kalibr/cli/run.py +200 -0
- kalibr/cli/serve.py +59 -0
- kalibr/client.py +293 -0
- kalibr/collector.py +173 -0
- kalibr/context.py +132 -0
- kalibr/cost_adapter.py +222 -0
- kalibr/decorators.py +140 -0
- kalibr/instrumentation/__init__.py +13 -0
- kalibr/instrumentation/anthropic_instr.py +282 -0
- kalibr/instrumentation/base.py +108 -0
- kalibr/instrumentation/google_instr.py +281 -0
- kalibr/instrumentation/openai_instr.py +265 -0
- kalibr/instrumentation/registry.py +153 -0
- kalibr/kalibr.py +144 -230
- kalibr/kalibr_app.py +53 -314
- kalibr/middleware/__init__.py +5 -0
- kalibr/middleware/auto_tracer.py +356 -0
- kalibr/models.py +41 -0
- kalibr/redaction.py +44 -0
- kalibr/schemas.py +116 -0
- kalibr/simple_tracer.py +255 -0
- kalibr/tokens.py +52 -0
- kalibr/trace_capsule.py +296 -0
- kalibr/trace_models.py +201 -0
- kalibr/tracer.py +354 -0
- kalibr/types.py +25 -93
- kalibr/utils.py +198 -0
- kalibr-1.1.0.dist-info/METADATA +97 -0
- kalibr-1.1.0.dist-info/RECORD +40 -0
- kalibr-1.1.0.dist-info/entry_points.txt +2 -0
- kalibr-1.1.0.dist-info/licenses/LICENSE +21 -0
- kalibr/deployment.py +0 -41
- kalibr/packager.py +0 -43
- kalibr/runtime_router.py +0 -138
- kalibr/schema_generators.py +0 -159
- kalibr/validator.py +0 -70
- kalibr-1.0.26.data/data/examples/README.md +0 -173
- kalibr-1.0.26.data/data/examples/basic_kalibr_example.py +0 -66
- kalibr-1.0.26.data/data/examples/enhanced_kalibr_example.py +0 -347
- kalibr-1.0.26.dist-info/METADATA +0 -176
- kalibr-1.0.26.dist-info/RECORD +0 -19
- kalibr-1.0.26.dist-info/entry_points.txt +0 -2
- kalibr-1.0.26.dist-info/licenses/LICENSE +0 -11
- {kalibr-1.0.26.dist-info → kalibr-1.1.0.dist-info}/WHEEL +0 -0
- {kalibr-1.0.26.dist-info → kalibr-1.1.0.dist-info}/top_level.txt +0 -0
kalibr/kalibr_app.py
CHANGED
|
@@ -1,343 +1,82 @@
|
|
|
1
|
-
|
|
2
|
-
from fastapi.responses import JSONResponse, StreamingResponse as FastAPIStreamingResponse
|
|
3
|
-
from typing import Callable, Dict, Any, List, Optional, get_type_hints
|
|
4
|
-
import inspect
|
|
5
|
-
import asyncio
|
|
6
|
-
from datetime import datetime
|
|
7
|
-
import uuid
|
|
8
|
-
import os
|
|
1
|
+
"""KalibrApp - Advanced app framework with async, file uploads, sessions"""
|
|
9
2
|
|
|
10
|
-
from
|
|
3
|
+
from typing import Callable, List, Optional
|
|
11
4
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
- Streaming responses
|
|
18
|
-
- Complex workflows
|
|
19
|
-
- Multi-model schema generation
|
|
20
|
-
"""
|
|
5
|
+
import uvicorn
|
|
6
|
+
from fastapi import FastAPI, File, UploadFile
|
|
7
|
+
from fastapi.responses import StreamingResponse
|
|
8
|
+
from kalibr.kalibr import Kalibr
|
|
9
|
+
from kalibr.types import FileUpload, Session
|
|
21
10
|
|
|
22
|
-
def __init__(self, title="Kalibr Enhanced API", version="2.0.0", base_url: Optional[str] = None, models: Optional[List[str]] = None):
|
|
23
|
-
"""
|
|
24
|
-
Initialize the Kalibr enhanced app.
|
|
25
|
-
Automatically determines correct base URL for deployed environments.
|
|
26
11
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
2. Env var `KALIBR_BASE_URL`
|
|
30
|
-
3. Env var `FLY_APP_NAME` -> https://<fly_app_name>.fly.dev
|
|
31
|
-
4. Default localhost for dev
|
|
12
|
+
class KalibrApp(Kalibr):
|
|
13
|
+
"""Advanced Kalibr app with file uploads, sessions, and streaming"""
|
|
32
14
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
self.app = FastAPI(title=title, version=version)
|
|
15
|
+
def __init__(self, title: str = "Kalibr API", version: str = "1.0.0"):
|
|
16
|
+
super().__init__(title=title, version=version)
|
|
17
|
+
self.sessions = {} # In-memory session store
|
|
37
18
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
elif os.getenv("KALIBR_BASE_URL"):
|
|
41
|
-
self.base_url = os.getenv("KALIBR_BASE_URL")
|
|
42
|
-
elif os.getenv("FLY_APP_NAME"):
|
|
43
|
-
self.base_url = f"https://{os.getenv('FLY_APP_NAME')}.fly.dev"
|
|
44
|
-
else:
|
|
45
|
-
self.base_url = "http://localhost:8000"
|
|
19
|
+
def file_handler(self, name: str, allowed_extensions: List[str]):
|
|
20
|
+
"""Decorator for file upload handlers"""
|
|
46
21
|
|
|
47
|
-
self.models_supported = models or ["mcp", "gpt-actions", "gemini", "copilot"]
|
|
48
|
-
|
|
49
|
-
# Storage for different action types
|
|
50
|
-
self.actions: Dict[str, Any] = {}
|
|
51
|
-
self.file_handlers: Dict[str, Any] = {}
|
|
52
|
-
self.session_actions: Dict[str, Any] = {}
|
|
53
|
-
self.stream_actions: Dict[str, Any] = {}
|
|
54
|
-
self.workflows: Dict[str, Any] = {}
|
|
55
|
-
|
|
56
|
-
# Session and workflow memory
|
|
57
|
-
self.sessions: Dict[str, Session] = {}
|
|
58
|
-
self.workflow_states: Dict[str, WorkflowState] = {}
|
|
59
|
-
|
|
60
|
-
self._setup_routes()
|
|
61
|
-
|
|
62
|
-
# -------------------------------------------------------------------------
|
|
63
|
-
# Action registration decorators
|
|
64
|
-
# -------------------------------------------------------------------------
|
|
65
|
-
|
|
66
|
-
def action(self, name: str, description: str = ""):
|
|
67
22
|
def decorator(func: Callable):
|
|
68
|
-
self.
|
|
69
|
-
"func": func,
|
|
70
|
-
"description": description,
|
|
71
|
-
"params": self._extract_params(func),
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
endpoint_path = f"/proxy/{name}"
|
|
75
|
-
|
|
76
|
-
async def endpoint_handler(request: Request):
|
|
77
|
-
params = {}
|
|
78
|
-
if request.method == "POST":
|
|
79
|
-
try:
|
|
80
|
-
body = await request.json()
|
|
81
|
-
params = body if isinstance(body, dict) else {}
|
|
82
|
-
except Exception:
|
|
83
|
-
params = {}
|
|
84
|
-
else:
|
|
85
|
-
params = dict(request.query_params)
|
|
86
|
-
|
|
87
|
-
try:
|
|
88
|
-
result = func(**params)
|
|
89
|
-
if inspect.isawaitable(result):
|
|
90
|
-
result = await result
|
|
91
|
-
return JSONResponse(content=result)
|
|
92
|
-
except Exception as e:
|
|
93
|
-
return JSONResponse(content={"error": str(e)}, status_code=500)
|
|
94
|
-
|
|
95
|
-
self.app.post(endpoint_path)(endpoint_handler)
|
|
96
|
-
self.app.get(endpoint_path)(endpoint_handler)
|
|
97
|
-
return func
|
|
98
|
-
|
|
99
|
-
return decorator
|
|
100
|
-
|
|
101
|
-
def file_handler(self, name: str, allowed_extensions: List[str] = None, description: str = ""):
|
|
102
|
-
def decorator(func: Callable):
|
|
103
|
-
self.file_handlers[name] = {
|
|
104
|
-
"func": func,
|
|
105
|
-
"description": description,
|
|
106
|
-
"allowed_extensions": allowed_extensions or [],
|
|
107
|
-
"params": self._extract_params(func),
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
endpoint_path = f"/files/{name}"
|
|
111
|
-
|
|
23
|
+
@self.app.post(f"/proxy/{name}")
|
|
112
24
|
async def file_endpoint(file: UploadFile = File(...)):
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if inspect.isawaitable(result):
|
|
132
|
-
result = await result
|
|
133
|
-
return JSONResponse(content=result)
|
|
134
|
-
except Exception as e:
|
|
135
|
-
return JSONResponse(content={"error": str(e)}, status_code=500)
|
|
25
|
+
# Validate file extension
|
|
26
|
+
if allowed_extensions:
|
|
27
|
+
ext = f".{file.filename.split('.')[-1]}"
|
|
28
|
+
if ext not in allowed_extensions:
|
|
29
|
+
return {"error": f"File type not allowed. Allowed: {allowed_extensions}"}
|
|
30
|
+
|
|
31
|
+
# Create FileUpload object
|
|
32
|
+
content = await file.read()
|
|
33
|
+
file_obj = FileUpload(
|
|
34
|
+
filename=file.filename,
|
|
35
|
+
content_type=file.content_type,
|
|
36
|
+
size=len(content),
|
|
37
|
+
content=content,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Call handler
|
|
41
|
+
result = await func(file_obj)
|
|
42
|
+
return result
|
|
136
43
|
|
|
137
|
-
self.app.post(endpoint_path)(file_endpoint)
|
|
138
44
|
return func
|
|
139
45
|
|
|
140
46
|
return decorator
|
|
141
47
|
|
|
142
48
|
def session_action(self, name: str, description: str = ""):
|
|
143
|
-
|
|
144
|
-
self.session_actions[name] = {
|
|
145
|
-
"func": func,
|
|
146
|
-
"description": description,
|
|
147
|
-
"params": self._extract_params(func),
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
endpoint_path = f"/session/{name}"
|
|
151
|
-
|
|
152
|
-
async def session_endpoint(request: Request):
|
|
153
|
-
try:
|
|
154
|
-
session_id = request.headers.get("X-Session-ID") or request.cookies.get("session_id")
|
|
155
|
-
if not session_id or session_id not in self.sessions:
|
|
156
|
-
session_id = str(uuid.uuid4())
|
|
157
|
-
session = Session(session_id=session_id)
|
|
158
|
-
self.sessions[session_id] = session
|
|
159
|
-
else:
|
|
160
|
-
session = self.sessions[session_id]
|
|
161
|
-
session.last_accessed = datetime.now()
|
|
49
|
+
"""Decorator for session-based actions"""
|
|
162
50
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
else:
|
|
170
|
-
result = func(**body)
|
|
51
|
+
def decorator(func: Callable):
|
|
52
|
+
@self.app.post(f"/proxy/{name}")
|
|
53
|
+
async def session_endpoint(session_id: str, **kwargs):
|
|
54
|
+
# Get or create session
|
|
55
|
+
if session_id not in self.sessions:
|
|
56
|
+
self.sessions[session_id] = Session(session_id)
|
|
171
57
|
|
|
172
|
-
|
|
173
|
-
result = await result
|
|
58
|
+
session = self.sessions[session_id]
|
|
174
59
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return response
|
|
179
|
-
except Exception as e:
|
|
180
|
-
return JSONResponse(content={"error": str(e)}, status_code=500)
|
|
60
|
+
# Call handler
|
|
61
|
+
result = await func(session, **kwargs)
|
|
62
|
+
return result
|
|
181
63
|
|
|
182
|
-
self.app.post(endpoint_path)(session_endpoint)
|
|
183
64
|
return func
|
|
184
65
|
|
|
185
66
|
return decorator
|
|
186
67
|
|
|
187
|
-
def stream_action(self, name: str
|
|
188
|
-
|
|
189
|
-
self.stream_actions[name] = {
|
|
190
|
-
"func": func,
|
|
191
|
-
"description": description,
|
|
192
|
-
"params": self._extract_params(func),
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
endpoint_path = f"/stream/{name}"
|
|
196
|
-
|
|
197
|
-
async def stream_endpoint(request: Request):
|
|
198
|
-
try:
|
|
199
|
-
params = dict(request.query_params) if request.method == "GET" else {}
|
|
200
|
-
if request.method == "POST":
|
|
201
|
-
body = await request.json()
|
|
202
|
-
params.update(body)
|
|
68
|
+
def stream_action(self, name: str):
|
|
69
|
+
"""Decorator for streaming responses"""
|
|
203
70
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
try:
|
|
211
|
-
if param_type == int:
|
|
212
|
-
converted_params[key] = int(value)
|
|
213
|
-
elif param_type == float:
|
|
214
|
-
converted_params[key] = float(value)
|
|
215
|
-
elif param_type == bool:
|
|
216
|
-
converted_params[key] = value.lower() in ("true", "1", "yes")
|
|
217
|
-
else:
|
|
218
|
-
converted_params[key] = value
|
|
219
|
-
except Exception:
|
|
220
|
-
converted_params[key] = value
|
|
221
|
-
|
|
222
|
-
result = func(**converted_params)
|
|
223
|
-
|
|
224
|
-
async def generate():
|
|
225
|
-
import json
|
|
226
|
-
if inspect.isasyncgen(result):
|
|
227
|
-
async for item in result:
|
|
228
|
-
yield json.dumps(item) + "\\n"
|
|
229
|
-
elif inspect.isgenerator(result):
|
|
230
|
-
for item in result:
|
|
231
|
-
yield json.dumps(item) + "\\n"
|
|
71
|
+
def decorator(func: Callable):
|
|
72
|
+
@self.app.post(f"/proxy/{name}")
|
|
73
|
+
async def stream_endpoint(**kwargs):
|
|
74
|
+
async def generate():
|
|
75
|
+
async for chunk in func(**kwargs):
|
|
76
|
+
yield chunk
|
|
232
77
|
|
|
233
|
-
|
|
234
|
-
except Exception as e:
|
|
235
|
-
return JSONResponse(content={"error": str(e)}, status_code=500)
|
|
78
|
+
return StreamingResponse(generate(), media_type="text/event-stream")
|
|
236
79
|
|
|
237
|
-
self.app.get(endpoint_path)(stream_endpoint)
|
|
238
|
-
self.app.post(endpoint_path)(stream_endpoint)
|
|
239
80
|
return func
|
|
240
81
|
|
|
241
82
|
return decorator
|
|
242
|
-
|
|
243
|
-
# -------------------------------------------------------------------------
|
|
244
|
-
# Schema generation routes
|
|
245
|
-
# -------------------------------------------------------------------------
|
|
246
|
-
|
|
247
|
-
def _setup_routes(self):
|
|
248
|
-
from kalibr.schema_generators import (
|
|
249
|
-
OpenAPISchemaGenerator,
|
|
250
|
-
MCPSchemaGenerator,
|
|
251
|
-
GeminiSchemaGenerator,
|
|
252
|
-
CopilotSchemaGenerator,
|
|
253
|
-
)
|
|
254
|
-
|
|
255
|
-
openapi_gen = OpenAPISchemaGenerator()
|
|
256
|
-
mcp_gen = MCPSchemaGenerator()
|
|
257
|
-
gemini_gen = GeminiSchemaGenerator()
|
|
258
|
-
copilot_gen = CopilotSchemaGenerator()
|
|
259
|
-
|
|
260
|
-
@self.app.get("/")
|
|
261
|
-
def root():
|
|
262
|
-
schemas = {}
|
|
263
|
-
if "gpt-actions" in self.models_supported:
|
|
264
|
-
schemas["gpt_actions"] = f"{self.base_url}/gpt-actions.json"
|
|
265
|
-
schemas["claude_mcp"] = f"{self.base_url}/mcp.json" # MCP is the default lingua franca
|
|
266
|
-
if "gemini" in self.models_supported:
|
|
267
|
-
schemas["gemini"] = f"{self.base_url}/schemas/gemini"
|
|
268
|
-
if "copilot" in self.models_supported:
|
|
269
|
-
schemas["copilot"] = f"{self.base_url}/schemas/copilot"
|
|
270
|
-
|
|
271
|
-
return {
|
|
272
|
-
"message": "Kalibr Enhanced API is running",
|
|
273
|
-
"actions": list(self.actions.keys()),
|
|
274
|
-
"schemas": schemas,
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
@self.app.get("/models/supported")
|
|
278
|
-
def supported_models():
|
|
279
|
-
return {"models": self.models_supported}
|
|
280
|
-
|
|
281
|
-
@self.app.get("/gpt-actions.json")
|
|
282
|
-
def gpt_actions_schema():
|
|
283
|
-
all_actions = {**self.actions, **self.file_handlers, **self.session_actions}
|
|
284
|
-
return openapi_gen.generate_schema(all_actions, self.base_url)
|
|
285
|
-
|
|
286
|
-
@self.app.get("/mcp.json")
|
|
287
|
-
def mcp_manifest():
|
|
288
|
-
all_actions = {**self.actions, **self.file_handlers, **self.session_actions}
|
|
289
|
-
return mcp_gen.generate_schema(all_actions, self.base_url)
|
|
290
|
-
|
|
291
|
-
@self.app.get("/schemas/gemini")
|
|
292
|
-
def gemini_schema():
|
|
293
|
-
all_actions = {**self.actions, **self.file_handlers, **self.session_actions}
|
|
294
|
-
return gemini_gen.generate_schema(all_actions, self.base_url)
|
|
295
|
-
|
|
296
|
-
@self.app.get("/schemas/copilot")
|
|
297
|
-
def copilot_schema():
|
|
298
|
-
all_actions = {**self.actions, **self.file_handlers, **self.session_actions}
|
|
299
|
-
return copilot_gen.generate_schema(all_actions, self.base_url)
|
|
300
|
-
|
|
301
|
-
@self.app.get("/health")
|
|
302
|
-
def health_check():
|
|
303
|
-
return {
|
|
304
|
-
"status": "healthy",
|
|
305
|
-
"service": "Kalibr Enhanced API",
|
|
306
|
-
"version": self.app.version,
|
|
307
|
-
"features": {
|
|
308
|
-
"file_handlers": bool(self.file_handlers),
|
|
309
|
-
"sessions": True,
|
|
310
|
-
"streams": bool(self.stream_actions),
|
|
311
|
-
},
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
# -------------------------------------------------------------------------
|
|
315
|
-
# Helpers
|
|
316
|
-
# -------------------------------------------------------------------------
|
|
317
|
-
|
|
318
|
-
def _extract_params(self, func: Callable) -> Dict[str, Any]:
|
|
319
|
-
sig = inspect.signature(func)
|
|
320
|
-
params = {}
|
|
321
|
-
type_hints = get_type_hints(func) if hasattr(func, "__annotations__") else {}
|
|
322
|
-
|
|
323
|
-
for param_name, param in sig.parameters.items():
|
|
324
|
-
if param_name in ["session", "workflow_state", "file"]:
|
|
325
|
-
continue
|
|
326
|
-
|
|
327
|
-
param_type = "string"
|
|
328
|
-
anno = type_hints.get(param_name, param.annotation)
|
|
329
|
-
if anno == int:
|
|
330
|
-
param_type = "integer"
|
|
331
|
-
elif anno == bool:
|
|
332
|
-
param_type = "boolean"
|
|
333
|
-
elif anno == float:
|
|
334
|
-
param_type = "number"
|
|
335
|
-
elif anno == list:
|
|
336
|
-
param_type = "array"
|
|
337
|
-
elif anno == dict:
|
|
338
|
-
param_type = "object"
|
|
339
|
-
|
|
340
|
-
is_required = param.default == inspect.Parameter.empty
|
|
341
|
-
params[param_name] = {"type": param_type, "required": is_required}
|
|
342
|
-
|
|
343
|
-
return params
|