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/__init__.py +7 -0
- kalibr/__main__.py +672 -26
- kalibr/deployment.py +26 -0
- kalibr/kalibr.py +259 -0
- kalibr/kalibr_app.py +465 -34
- kalibr/schema_generators.py +212 -13
- kalibr/types.py +106 -0
- kalibr-1.0.20.data/data/examples/README.md +173 -0
- kalibr-1.0.20.data/data/examples/basic_kalibr_example.py +66 -0
- kalibr-1.0.20.data/data/examples/enhanced_kalibr_example.py +347 -0
- kalibr-1.0.20.dist-info/METADATA +302 -0
- kalibr-1.0.20.dist-info/RECORD +16 -0
- kalibr-1.0.17.dist-info/METADATA +0 -120
- kalibr-1.0.17.dist-info/RECORD +0 -10
- {kalibr-1.0.17.dist-info → kalibr-1.0.20.dist-info}/WHEEL +0 -0
- {kalibr-1.0.17.dist-info → kalibr-1.0.20.dist-info}/entry_points.txt +0 -0
- /kalibr-1.0.17.dist-info/licenses/LICENSE.txt → /kalibr-1.0.20.dist-info/licenses/LICENSE +0 -0
- {kalibr-1.0.17.dist-info → kalibr-1.0.20.dist-info}/top_level.txt +0 -0
kalibr/kalibr_app.py
CHANGED
|
@@ -1,38 +1,469 @@
|
|
|
1
|
-
|
|
2
|
-
import os
|
|
3
|
-
import uvicorn
|
|
4
|
-
from fastapi import FastAPI
|
|
1
|
+
# kalibr/kalibr_app.py - Full App-Level Implementation
|
|
5
2
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|