kalibr 1.0.17__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,38 +1,469 @@
1
- import importlib.util
2
- import os
3
- import uvicorn
4
- from fastapi import FastAPI
1
+ # kalibr/kalibr_app.py - Full App-Level Implementation
5
2
 
6
- class KalibrApp(FastAPI):
7
- def __init__(self):
8
- super().__init__()
9
- print("🚀 KalibrApp initialized. Ready to serve AI tools.")
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
6
+ import inspect
7
+ import asyncio
8
+ from datetime import datetime
9
+ import uuid
10
10
 
11
- def run(self, host="127.0.0.1", port=8000):
12
- print(f"🚀 Starting Kalibr server on http://{host}:{port}")
13
- uvicorn.run(self, host=host, port=port)
11
+ from kalibr.types import FileUpload, Session, WorkflowState
14
12
 
15
- # --- CLI helper functions ---
16
13
 
17
- def serve_app(file_path: str):
18
- """Run a Kalibr app file (e.g. demo_app.py) locally."""
19
- if not os.path.exists(file_path):
20
- print(f"❌ File not found: {file_path}")
21
- return
22
-
23
- module_name = os.path.splitext(os.path.basename(file_path))[0]
24
- spec = importlib.util.spec_from_file_location(module_name, file_path)
25
- module = importlib.util.module_from_spec(spec)
26
- spec.loader.exec_module(module)
27
-
28
- app = getattr(module, "app", None)
29
- if not app:
30
- print("❌ No `app` instance found in the provided file.")
31
- return
32
-
33
- print(f"🚀 Serving {file_path} locally...")
34
- uvicorn.run(app, host="127.0.0.1", port=8000)
35
-
36
- def deploy_app(file_path: str):
37
- """Stub for future deploy command."""
38
- print(f"🚀 Deploying {file_path} (stub). Future versions will handle cloud deploys.")
14
+ class KalibrApp:
15
+ """
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
22
+ """
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)
65
+ }
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)
109
+ }
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