kalibr 1.0.18__py3-none-any.whl → 1.0.20__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/kalibr_app.py CHANGED
@@ -1,68 +1,469 @@
1
- from fastapi import FastAPI
2
- import importlib.util
3
- import json
1
+ # kalibr/kalibr_app.py - Full App-Level Implementation
2
+
3
+ from fastapi import FastAPI, Request, UploadFile, File, Depends
4
+ from fastapi.responses import JSONResponse, StreamingResponse as FastAPIStreamingResponse
5
+ from typing import Callable, Dict, Any, List, Optional, get_type_hints
4
6
  import inspect
5
- from pydantic import BaseModel
7
+ import asyncio
8
+ from datetime import datetime
9
+ import uuid
10
+
11
+ from kalibr.types import FileUpload, Session, WorkflowState
12
+
6
13
 
7
14
  class KalibrApp:
8
15
  """
9
- Minimal Kalibr SDK App that auto-generates schemas
10
- for GPT Actions, Claude MCP, Gemini, etc.
16
+ Enhanced app-level Kalibr framework with advanced capabilities:
17
+ - File upload handling
18
+ - Session management
19
+ - Streaming responses
20
+ - Complex workflows
21
+ - Multi-model schema generation
11
22
  """
12
-
13
- def __init__(self, file_path: str):
14
- self.file_path = file_path
15
- self.app = FastAPI(title="Kalibr App")
16
- self.functions = self._load_tools()
17
-
18
- def _load_tools(self):
19
- """Dynamically import all @tool-decorated functions from the user app."""
20
- spec = importlib.util.spec_from_file_location("user_app", self.file_path)
21
- user_app = importlib.util.module_from_spec(spec)
22
- spec.loader.exec_module(user_app)
23
- funcs = {}
24
-
25
- for name, obj in inspect.getmembers(user_app, inspect.isfunction):
26
- if hasattr(obj, "_kalibr_tool"):
27
- funcs[name] = obj
28
- self._register_route(name, obj)
29
-
30
- return funcs
31
-
32
- def _register_route(self, name, func):
33
- """Register each tool as a POST endpoint."""
34
- class RequestSchema(BaseModel):
35
- __annotations__ = {k: v for k, v in func.__annotations__.items() if k != "return"}
36
-
37
- async def route(payload: RequestSchema):
38
- result = func(**payload.dict())
39
- return {"result": result}
40
-
41
- self.app.post(f"/tools/{name}")(route)
42
-
43
- def generate_schemas(self):
44
- """Generate model-specific schemas for GPT Actions, Claude MCP, Gemini."""
45
- schemas = {}
46
- for name, func in self.functions.items():
47
- sig = inspect.signature(func)
48
- params = {
49
- k: str(v.annotation) for k, v in sig.parameters.items()
23
+
24
+ def __init__(self, title="Kalibr Enhanced API", version="2.0.0", base_url="http://localhost:8000"):
25
+ """
26
+ Initialize the Kalibr enhanced app.
27
+
28
+ Args:
29
+ title: API title
30
+ version: API version
31
+ base_url: Base URL for schema generation
32
+ """
33
+ self.app = FastAPI(title=title, version=version)
34
+ self.base_url = base_url
35
+
36
+ # Storage for different action types
37
+ self.actions = {} # Regular actions
38
+ self.file_handlers = {} # File upload handlers
39
+ self.session_actions = {} # Session-aware actions
40
+ self.stream_actions = {} # Streaming actions
41
+ self.workflows = {} # Workflow handlers
42
+
43
+ # Session storage (in-memory for simplicity)
44
+ self.sessions: Dict[str, Session] = {}
45
+
46
+ # Workflow state storage
47
+ self.workflow_states: Dict[str, WorkflowState] = {}
48
+
49
+ self._setup_routes()
50
+
51
+ def action(self, name: str, description: str = ""):
52
+ """
53
+ Decorator to register a regular action.
54
+
55
+ Usage:
56
+ @app.action("greet", "Greet someone")
57
+ def greet(name: str):
58
+ return {"message": f"Hello, {name}!"}
59
+ """
60
+ def decorator(func: Callable):
61
+ self.actions[name] = {
62
+ "func": func,
63
+ "description": description,
64
+ "params": self._extract_params(func)
50
65
  }
51
- schemas[name] = {
52
- "description": func.__doc__ or "",
53
- "parameters": params,
54
- "returns": str(sig.return_annotation),
66
+
67
+ endpoint_path = f"/proxy/{name}"
68
+
69
+ async def endpoint_handler(request: Request):
70
+ params = {}
71
+ if request.method == "POST":
72
+ try:
73
+ body = await request.json()
74
+ params = body if isinstance(body, dict) else {}
75
+ except:
76
+ params = {}
77
+ else:
78
+ params = dict(request.query_params)
79
+
80
+ try:
81
+ result = func(**params)
82
+ if inspect.isawaitable(result):
83
+ result = await result
84
+ return JSONResponse(content=result)
85
+ except Exception as e:
86
+ return JSONResponse(content={"error": str(e)}, status_code=500)
87
+
88
+ self.app.post(endpoint_path)(endpoint_handler)
89
+ self.app.get(endpoint_path)(endpoint_handler)
90
+
91
+ return func
92
+ return decorator
93
+
94
+ def file_handler(self, name: str, allowed_extensions: List[str] = None, description: str = ""):
95
+ """
96
+ Decorator to register a file upload handler.
97
+
98
+ Usage:
99
+ @app.file_handler("process_document", [".pdf", ".docx"])
100
+ async def process_document(file: FileUpload):
101
+ return {"filename": file.filename, "size": file.size}
102
+ """
103
+ def decorator(func: Callable):
104
+ self.file_handlers[name] = {
105
+ "func": func,
106
+ "description": description,
107
+ "allowed_extensions": allowed_extensions or [],
108
+ "params": self._extract_params(func)
55
109
  }
56
- return {
57
- "gpt_actions": schemas,
58
- "claude_mcp": schemas,
59
- "gemini_tools": schemas,
60
- }
61
-
62
- def serve_app(file_path: str):
63
- """Run the Kalibr app locally."""
64
- from uvicorn import run
65
- app_instance = KalibrApp(file_path)
66
- print(" Generated Schemas:")
67
- print(json.dumps(app_instance.generate_schemas(), indent=2))
68
- run(app_instance.app, host="127.0.0.1", port=8000)
110
+
111
+ endpoint_path = f"/files/{name}"
112
+
113
+ async def file_endpoint(file: UploadFile = File(...)):
114
+ try:
115
+ # Validate file extension
116
+ if allowed_extensions:
117
+ file_ext = "." + file.filename.split(".")[-1] if "." in file.filename else ""
118
+ if file_ext not in allowed_extensions:
119
+ return JSONResponse(
120
+ content={"error": f"File type {file_ext} not allowed. Allowed: {allowed_extensions}"},
121
+ status_code=400
122
+ )
123
+
124
+ # Read file content
125
+ content = await file.read()
126
+
127
+ # Create FileUpload object
128
+ file_upload = FileUpload(
129
+ filename=file.filename,
130
+ content_type=file.content_type or "application/octet-stream",
131
+ size=len(content),
132
+ content=content
133
+ )
134
+
135
+ # Call handler
136
+ result = func(file_upload)
137
+ if inspect.isawaitable(result):
138
+ result = await result
139
+
140
+ return JSONResponse(content=result)
141
+ except Exception as e:
142
+ return JSONResponse(content={"error": str(e)}, status_code=500)
143
+
144
+ self.app.post(endpoint_path)(file_endpoint)
145
+
146
+ return func
147
+ return decorator
148
+
149
+ def session_action(self, name: str, description: str = ""):
150
+ """
151
+ Decorator to register a session-aware action.
152
+
153
+ Usage:
154
+ @app.session_action("save_data", "Save data to session")
155
+ async def save_data(session: Session, data: dict):
156
+ session.set("my_data", data)
157
+ return {"saved": True}
158
+ """
159
+ def decorator(func: Callable):
160
+ self.session_actions[name] = {
161
+ "func": func,
162
+ "description": description,
163
+ "params": self._extract_params(func)
164
+ }
165
+
166
+ endpoint_path = f"/session/{name}"
167
+
168
+ async def session_endpoint(request: Request):
169
+ try:
170
+ # Get or create session
171
+ session_id = request.headers.get("X-Session-ID") or request.cookies.get("session_id")
172
+
173
+ if not session_id or session_id not in self.sessions:
174
+ session_id = str(uuid.uuid4())
175
+ session = Session(session_id=session_id)
176
+ self.sessions[session_id] = session
177
+ else:
178
+ session = self.sessions[session_id]
179
+ session.last_accessed = datetime.now()
180
+
181
+ # Get request parameters
182
+ body = await request.json() if request.method == "POST" else {}
183
+
184
+ # Call function with session
185
+ sig = inspect.signature(func)
186
+ if 'session' in sig.parameters:
187
+ # Remove 'session' from params, pass separately
188
+ func_params = {k: v for k, v in body.items() if k != 'session'}
189
+ result = func(session=session, **func_params)
190
+ else:
191
+ result = func(**body)
192
+
193
+ if inspect.isawaitable(result):
194
+ result = await result
195
+
196
+ # Return result with session ID
197
+ response = JSONResponse(content=result)
198
+ response.set_cookie("session_id", session_id)
199
+ response.headers["X-Session-ID"] = session_id
200
+
201
+ return response
202
+ except Exception as e:
203
+ return JSONResponse(content={"error": str(e)}, status_code=500)
204
+
205
+ self.app.post(endpoint_path)(session_endpoint)
206
+
207
+ return func
208
+ return decorator
209
+
210
+ def stream_action(self, name: str, description: str = ""):
211
+ """
212
+ Decorator to register a streaming action.
213
+
214
+ Usage:
215
+ @app.stream_action("live_feed", "Stream live data")
216
+ async def live_feed(count: int = 10):
217
+ for i in range(count):
218
+ yield {"item": i, "timestamp": datetime.now().isoformat()}
219
+ await asyncio.sleep(0.5)
220
+ """
221
+ def decorator(func: Callable):
222
+ self.stream_actions[name] = {
223
+ "func": func,
224
+ "description": description,
225
+ "params": self._extract_params(func)
226
+ }
227
+
228
+ endpoint_path = f"/stream/{name}"
229
+
230
+ async def stream_endpoint(request: Request):
231
+ try:
232
+ # Get parameters
233
+ params = dict(request.query_params) if request.method == "GET" else {}
234
+ if request.method == "POST":
235
+ body = await request.json()
236
+ params.update(body)
237
+
238
+ # Convert parameter types based on function signature
239
+ sig = inspect.signature(func)
240
+ type_hints = get_type_hints(func) if hasattr(func, '__annotations__') else {}
241
+ converted_params = {}
242
+ for key, value in params.items():
243
+ if key in sig.parameters:
244
+ param_type = type_hints.get(key, str)
245
+ try:
246
+ if param_type == int:
247
+ converted_params[key] = int(value)
248
+ elif param_type == float:
249
+ converted_params[key] = float(value)
250
+ elif param_type == bool:
251
+ converted_params[key] = value.lower() in ('true', '1', 'yes')
252
+ else:
253
+ converted_params[key] = value
254
+ except (ValueError, AttributeError):
255
+ converted_params[key] = value
256
+
257
+ # Call generator function
258
+ result = func(**converted_params)
259
+
260
+ # Create streaming generator
261
+ async def generate():
262
+ if inspect.isasyncgen(result):
263
+ async for item in result:
264
+ import json
265
+ yield json.dumps(item) + "\n"
266
+ elif inspect.isgenerator(result):
267
+ for item in result:
268
+ import json
269
+ yield json.dumps(item) + "\n"
270
+
271
+ return FastAPIStreamingResponse(generate(), media_type="application/x-ndjson")
272
+ except Exception as e:
273
+ return JSONResponse(content={"error": str(e)}, status_code=500)
274
+
275
+ self.app.get(endpoint_path)(stream_endpoint)
276
+ self.app.post(endpoint_path)(stream_endpoint)
277
+
278
+ return func
279
+ return decorator
280
+
281
+ def workflow(self, name: str, description: str = ""):
282
+ """
283
+ Decorator to register a workflow.
284
+
285
+ Usage:
286
+ @app.workflow("process_order", "Process customer order")
287
+ async def process_order(order_data: dict, workflow_state: WorkflowState):
288
+ workflow_state.step = "validation"
289
+ # ... process steps
290
+ return {"workflow_id": workflow_state.workflow_id}
291
+ """
292
+ def decorator(func: Callable):
293
+ self.workflows[name] = {
294
+ "func": func,
295
+ "description": description,
296
+ "params": self._extract_params(func)
297
+ }
298
+
299
+ endpoint_path = f"/workflow/{name}"
300
+
301
+ async def workflow_endpoint(request: Request):
302
+ try:
303
+ # Get workflow ID from headers or create new
304
+ workflow_id = request.headers.get("X-Workflow-ID")
305
+
306
+ if not workflow_id or workflow_id not in self.workflow_states:
307
+ workflow_id = str(uuid.uuid4())
308
+ workflow_state = WorkflowState(
309
+ workflow_id=workflow_id,
310
+ step="init",
311
+ status="running"
312
+ )
313
+ self.workflow_states[workflow_id] = workflow_state
314
+ else:
315
+ workflow_state = self.workflow_states[workflow_id]
316
+ workflow_state.updated_at = datetime.now()
317
+
318
+ # Get request data
319
+ body = await request.json() if request.method == "POST" else {}
320
+
321
+ # Call workflow function
322
+ sig = inspect.signature(func)
323
+ if 'workflow_state' in sig.parameters:
324
+ func_params = {k: v for k, v in body.items() if k != 'workflow_state'}
325
+ result = func(workflow_state=workflow_state, **func_params)
326
+ else:
327
+ result = func(**body)
328
+
329
+ if inspect.isawaitable(result):
330
+ result = await result
331
+
332
+ # Return result with workflow ID
333
+ response = JSONResponse(content=result)
334
+ response.headers["X-Workflow-ID"] = workflow_id
335
+
336
+ return response
337
+ except Exception as e:
338
+ return JSONResponse(content={"error": str(e)}, status_code=500)
339
+
340
+ self.app.post(endpoint_path)(workflow_endpoint)
341
+
342
+ return func
343
+ return decorator
344
+
345
+ def _extract_params(self, func: Callable) -> Dict:
346
+ """Extract parameter information from function signature."""
347
+ sig = inspect.signature(func)
348
+ params = {}
349
+ type_hints = get_type_hints(func) if hasattr(func, '__annotations__') else {}
350
+
351
+ for param_name, param in sig.parameters.items():
352
+ # Skip special parameters
353
+ if param_name in ['session', 'workflow_state', 'file']:
354
+ continue
355
+
356
+ param_type = "string"
357
+
358
+ if param_name in type_hints:
359
+ anno = type_hints[param_name]
360
+ elif param.annotation != inspect.Parameter.empty:
361
+ anno = param.annotation
362
+ else:
363
+ anno = str
364
+
365
+ # Map types
366
+ if anno == int:
367
+ param_type = "integer"
368
+ elif anno == bool:
369
+ param_type = "boolean"
370
+ elif anno == float:
371
+ param_type = "number"
372
+ elif anno == list:
373
+ param_type = "array"
374
+ elif anno == dict:
375
+ param_type = "object"
376
+
377
+ is_required = param.default == inspect.Parameter.empty
378
+
379
+ params[param_name] = {
380
+ "type": param_type,
381
+ "required": is_required
382
+ }
383
+
384
+ return params
385
+
386
+ def _setup_routes(self):
387
+ """Setup core API routes."""
388
+ from kalibr.schema_generators import (
389
+ OpenAPISchemaGenerator,
390
+ MCPSchemaGenerator,
391
+ GeminiSchemaGenerator,
392
+ CopilotSchemaGenerator
393
+ )
394
+
395
+ # Initialize schema generators
396
+ openapi_gen = OpenAPISchemaGenerator()
397
+ mcp_gen = MCPSchemaGenerator()
398
+ gemini_gen = GeminiSchemaGenerator()
399
+ copilot_gen = CopilotSchemaGenerator()
400
+
401
+ @self.app.get("/")
402
+ def root():
403
+ """Root endpoint with API information."""
404
+ return {
405
+ "message": "Kalibr Enhanced API is running",
406
+ "actions": list(self.actions.keys()),
407
+ "file_handlers": list(self.file_handlers.keys()),
408
+ "session_actions": list(self.session_actions.keys()),
409
+ "stream_actions": list(self.stream_actions.keys()),
410
+ "workflows": list(self.workflows.keys()),
411
+ "schemas": {
412
+ "gpt_actions": f"{self.base_url}/gpt-actions.json",
413
+ "openapi_swagger": f"{self.base_url}/openapi.json",
414
+ "claude_mcp": f"{self.base_url}/mcp.json",
415
+ "gemini": f"{self.base_url}/schemas/gemini",
416
+ "copilot": f"{self.base_url}/schemas/copilot"
417
+ }
418
+ }
419
+
420
+ @self.app.get("/gpt-actions.json")
421
+ def gpt_actions_schema():
422
+ """Generate GPT Actions schema from all registered actions."""
423
+ # Combine all action types for schema generation
424
+ all_actions = {**self.actions, **self.file_handlers, **self.session_actions}
425
+ return openapi_gen.generate_schema(all_actions, self.base_url)
426
+
427
+ @self.app.get("/mcp.json")
428
+ def mcp_manifest():
429
+ """Generate Claude MCP manifest."""
430
+ all_actions = {**self.actions, **self.file_handlers, **self.session_actions}
431
+ return mcp_gen.generate_schema(all_actions, self.base_url)
432
+
433
+ @self.app.get("/schemas/gemini")
434
+ def gemini_schema():
435
+ """Generate Gemini Extensions schema."""
436
+ all_actions = {**self.actions, **self.file_handlers, **self.session_actions}
437
+ return gemini_gen.generate_schema(all_actions, self.base_url)
438
+
439
+ @self.app.get("/schemas/copilot")
440
+ def copilot_schema():
441
+ """Generate Microsoft Copilot schema."""
442
+ all_actions = {**self.actions, **self.file_handlers, **self.session_actions}
443
+ return copilot_gen.generate_schema(all_actions, self.base_url)
444
+
445
+ # Health check
446
+ @self.app.get("/health")
447
+ def health_check():
448
+ return {
449
+ "status": "healthy",
450
+ "service": "Kalibr Enhanced API",
451
+ "features": ["actions", "file_uploads", "sessions", "streaming", "workflows"]
452
+ }
453
+
454
+ # Override FastAPI OpenAPI for Swagger UI
455
+ def custom_openapi():
456
+ if self.app.openapi_schema:
457
+ return self.app.openapi_schema
458
+
459
+ from fastapi.openapi.utils import get_openapi
460
+ openapi_schema = get_openapi(
461
+ title=self.app.title,
462
+ version=self.app.version,
463
+ routes=self.app.routes,
464
+ )
465
+ openapi_schema["servers"] = [{"url": self.base_url}]
466
+ self.app.openapi_schema = openapi_schema
467
+ return self.app.openapi_schema
468
+
469
+ self.app.openapi = custom_openapi