kalibr 1.0.20__tar.gz → 1.0.21__tar.gz
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-1.0.20/kalibr.egg-info → kalibr-1.0.21}/PKG-INFO +1 -1
- {kalibr-1.0.20 → kalibr-1.0.21}/kalibr/kalibr_app.py +124 -271
- {kalibr-1.0.20 → kalibr-1.0.21/kalibr.egg-info}/PKG-INFO +1 -1
- {kalibr-1.0.20 → kalibr-1.0.21}/pyproject.toml +1 -1
- {kalibr-1.0.20 → kalibr-1.0.21}/setup.py +1 -1
- {kalibr-1.0.20 → kalibr-1.0.21}/LICENSE +0 -0
- {kalibr-1.0.20 → kalibr-1.0.21}/MANIFEST.in +0 -0
- {kalibr-1.0.20 → kalibr-1.0.21}/README.md +0 -0
- {kalibr-1.0.20 → kalibr-1.0.21}/examples/README.md +0 -0
- {kalibr-1.0.20 → kalibr-1.0.21}/examples/__init__.py +0 -0
- {kalibr-1.0.20 → kalibr-1.0.21}/examples/basic_kalibr_example.py +0 -0
- {kalibr-1.0.20 → kalibr-1.0.21}/examples/enhanced_kalibr_example.py +0 -0
- {kalibr-1.0.20 → kalibr-1.0.21}/kalibr/__init__.py +0 -0
- {kalibr-1.0.20 → kalibr-1.0.21}/kalibr/__main__.py +0 -0
- {kalibr-1.0.20 → kalibr-1.0.21}/kalibr/deployment.py +0 -0
- {kalibr-1.0.20 → kalibr-1.0.21}/kalibr/kalibr.py +0 -0
- {kalibr-1.0.20 → kalibr-1.0.21}/kalibr/schema_generators.py +0 -0
- {kalibr-1.0.20 → kalibr-1.0.21}/kalibr/types.py +0 -0
- {kalibr-1.0.20 → kalibr-1.0.21}/kalibr.egg-info/SOURCES.txt +0 -0
- {kalibr-1.0.20 → kalibr-1.0.21}/kalibr.egg-info/dependency_links.txt +0 -0
- {kalibr-1.0.20 → kalibr-1.0.21}/kalibr.egg-info/entry_points.txt +0 -0
- {kalibr-1.0.20 → kalibr-1.0.21}/kalibr.egg-info/requires.txt +0 -0
- {kalibr-1.0.20 → kalibr-1.0.21}/kalibr.egg-info/top_level.txt +0 -0
- {kalibr-1.0.20 → kalibr-1.0.21}/setup.cfg +0 -0
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
from fastapi import FastAPI, Request, UploadFile, File, Depends
|
|
1
|
+
from fastapi import FastAPI, Request, UploadFile, File
|
|
4
2
|
from fastapi.responses import JSONResponse, StreamingResponse as FastAPIStreamingResponse
|
|
5
3
|
from typing import Callable, Dict, Any, List, Optional, get_type_hints
|
|
6
4
|
import inspect
|
|
7
5
|
import asyncio
|
|
8
6
|
from datetime import datetime
|
|
9
7
|
import uuid
|
|
8
|
+
import os
|
|
10
9
|
|
|
11
10
|
from kalibr.types import FileUpload, Session, WorkflowState
|
|
12
11
|
|
|
@@ -20,63 +19,67 @@ class KalibrApp:
|
|
|
20
19
|
- Complex workflows
|
|
21
20
|
- Multi-model schema generation
|
|
22
21
|
"""
|
|
23
|
-
|
|
24
|
-
def __init__(self, title="Kalibr Enhanced API", version="2.0.0", base_url=
|
|
22
|
+
|
|
23
|
+
def __init__(self, title="Kalibr Enhanced API", version="2.0.0", base_url: Optional[str] = None):
|
|
25
24
|
"""
|
|
26
25
|
Initialize the Kalibr enhanced app.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
Automatically determines correct base URL for deployed environments.
|
|
27
|
+
|
|
28
|
+
Priority:
|
|
29
|
+
1. Explicit `base_url` passed by user
|
|
30
|
+
2. Env var `KALIBR_BASE_URL`
|
|
31
|
+
3. Env var `FLY_APP_NAME` -> https://<fly_app_name>.fly.dev
|
|
32
|
+
4. Default localhost for dev
|
|
32
33
|
"""
|
|
33
34
|
self.app = FastAPI(title=title, version=version)
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
|
|
36
|
+
if base_url:
|
|
37
|
+
self.base_url = base_url
|
|
38
|
+
elif os.getenv("KALIBR_BASE_URL"):
|
|
39
|
+
self.base_url = os.getenv("KALIBR_BASE_URL")
|
|
40
|
+
elif os.getenv("FLY_APP_NAME"):
|
|
41
|
+
self.base_url = f"https://{os.getenv('FLY_APP_NAME')}.fly.dev"
|
|
42
|
+
else:
|
|
43
|
+
self.base_url = "http://localhost:8000"
|
|
44
|
+
|
|
36
45
|
# Storage for different action types
|
|
37
|
-
self.actions = {}
|
|
38
|
-
self.file_handlers = {}
|
|
39
|
-
self.session_actions = {}
|
|
40
|
-
self.stream_actions = {}
|
|
41
|
-
self.workflows = {}
|
|
42
|
-
|
|
43
|
-
# Session
|
|
46
|
+
self.actions: Dict[str, Any] = {}
|
|
47
|
+
self.file_handlers: Dict[str, Any] = {}
|
|
48
|
+
self.session_actions: Dict[str, Any] = {}
|
|
49
|
+
self.stream_actions: Dict[str, Any] = {}
|
|
50
|
+
self.workflows: Dict[str, Any] = {}
|
|
51
|
+
|
|
52
|
+
# Session and workflow memory
|
|
44
53
|
self.sessions: Dict[str, Session] = {}
|
|
45
|
-
|
|
46
|
-
# Workflow state storage
|
|
47
54
|
self.workflow_states: Dict[str, WorkflowState] = {}
|
|
48
|
-
|
|
55
|
+
|
|
49
56
|
self._setup_routes()
|
|
50
|
-
|
|
57
|
+
|
|
58
|
+
# -------------------------------------------------------------------------
|
|
59
|
+
# Action registration decorators
|
|
60
|
+
# -------------------------------------------------------------------------
|
|
61
|
+
|
|
51
62
|
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
63
|
def decorator(func: Callable):
|
|
61
64
|
self.actions[name] = {
|
|
62
65
|
"func": func,
|
|
63
66
|
"description": description,
|
|
64
|
-
"params": self._extract_params(func)
|
|
67
|
+
"params": self._extract_params(func),
|
|
65
68
|
}
|
|
66
|
-
|
|
69
|
+
|
|
67
70
|
endpoint_path = f"/proxy/{name}"
|
|
68
|
-
|
|
71
|
+
|
|
69
72
|
async def endpoint_handler(request: Request):
|
|
70
73
|
params = {}
|
|
71
74
|
if request.method == "POST":
|
|
72
75
|
try:
|
|
73
76
|
body = await request.json()
|
|
74
77
|
params = body if isinstance(body, dict) else {}
|
|
75
|
-
except:
|
|
78
|
+
except Exception:
|
|
76
79
|
params = {}
|
|
77
80
|
else:
|
|
78
81
|
params = dict(request.query_params)
|
|
79
|
-
|
|
82
|
+
|
|
80
83
|
try:
|
|
81
84
|
result = func(**params)
|
|
82
85
|
if inspect.isawaitable(result):
|
|
@@ -84,92 +87,67 @@ class KalibrApp:
|
|
|
84
87
|
return JSONResponse(content=result)
|
|
85
88
|
except Exception as e:
|
|
86
89
|
return JSONResponse(content={"error": str(e)}, status_code=500)
|
|
87
|
-
|
|
90
|
+
|
|
88
91
|
self.app.post(endpoint_path)(endpoint_handler)
|
|
89
92
|
self.app.get(endpoint_path)(endpoint_handler)
|
|
90
|
-
|
|
91
93
|
return func
|
|
94
|
+
|
|
92
95
|
return decorator
|
|
93
|
-
|
|
96
|
+
|
|
94
97
|
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
98
|
def decorator(func: Callable):
|
|
104
99
|
self.file_handlers[name] = {
|
|
105
100
|
"func": func,
|
|
106
101
|
"description": description,
|
|
107
102
|
"allowed_extensions": allowed_extensions or [],
|
|
108
|
-
"params": self._extract_params(func)
|
|
103
|
+
"params": self._extract_params(func),
|
|
109
104
|
}
|
|
110
|
-
|
|
105
|
+
|
|
111
106
|
endpoint_path = f"/files/{name}"
|
|
112
|
-
|
|
107
|
+
|
|
113
108
|
async def file_endpoint(file: UploadFile = File(...)):
|
|
114
109
|
try:
|
|
115
|
-
# Validate file extension
|
|
116
110
|
if allowed_extensions:
|
|
117
111
|
file_ext = "." + file.filename.split(".")[-1] if "." in file.filename else ""
|
|
118
112
|
if file_ext not in allowed_extensions:
|
|
119
113
|
return JSONResponse(
|
|
120
114
|
content={"error": f"File type {file_ext} not allowed. Allowed: {allowed_extensions}"},
|
|
121
|
-
status_code=400
|
|
115
|
+
status_code=400,
|
|
122
116
|
)
|
|
123
|
-
|
|
124
|
-
# Read file content
|
|
117
|
+
|
|
125
118
|
content = await file.read()
|
|
126
|
-
|
|
127
|
-
# Create FileUpload object
|
|
128
119
|
file_upload = FileUpload(
|
|
129
120
|
filename=file.filename,
|
|
130
121
|
content_type=file.content_type or "application/octet-stream",
|
|
131
122
|
size=len(content),
|
|
132
|
-
content=content
|
|
123
|
+
content=content,
|
|
133
124
|
)
|
|
134
|
-
|
|
135
|
-
# Call handler
|
|
125
|
+
|
|
136
126
|
result = func(file_upload)
|
|
137
127
|
if inspect.isawaitable(result):
|
|
138
128
|
result = await result
|
|
139
|
-
|
|
140
129
|
return JSONResponse(content=result)
|
|
141
130
|
except Exception as e:
|
|
142
131
|
return JSONResponse(content={"error": str(e)}, status_code=500)
|
|
143
|
-
|
|
132
|
+
|
|
144
133
|
self.app.post(endpoint_path)(file_endpoint)
|
|
145
|
-
|
|
146
134
|
return func
|
|
135
|
+
|
|
147
136
|
return decorator
|
|
148
|
-
|
|
137
|
+
|
|
149
138
|
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
139
|
def decorator(func: Callable):
|
|
160
140
|
self.session_actions[name] = {
|
|
161
141
|
"func": func,
|
|
162
142
|
"description": description,
|
|
163
|
-
"params": self._extract_params(func)
|
|
143
|
+
"params": self._extract_params(func),
|
|
164
144
|
}
|
|
165
|
-
|
|
145
|
+
|
|
166
146
|
endpoint_path = f"/session/{name}"
|
|
167
|
-
|
|
147
|
+
|
|
168
148
|
async def session_endpoint(request: Request):
|
|
169
149
|
try:
|
|
170
|
-
# Get or create session
|
|
171
150
|
session_id = request.headers.get("X-Session-ID") or request.cookies.get("session_id")
|
|
172
|
-
|
|
173
151
|
if not session_id or session_id not in self.sessions:
|
|
174
152
|
session_id = str(uuid.uuid4())
|
|
175
153
|
session = Session(session_id=session_id)
|
|
@@ -177,67 +155,50 @@ class KalibrApp:
|
|
|
177
155
|
else:
|
|
178
156
|
session = self.sessions[session_id]
|
|
179
157
|
session.last_accessed = datetime.now()
|
|
180
|
-
|
|
181
|
-
# Get request parameters
|
|
158
|
+
|
|
182
159
|
body = await request.json() if request.method == "POST" else {}
|
|
183
|
-
|
|
184
|
-
# Call function with session
|
|
160
|
+
|
|
185
161
|
sig = inspect.signature(func)
|
|
186
|
-
if
|
|
187
|
-
|
|
188
|
-
func_params = {k: v for k, v in body.items() if k != 'session'}
|
|
162
|
+
if "session" in sig.parameters:
|
|
163
|
+
func_params = {k: v for k, v in body.items() if k != "session"}
|
|
189
164
|
result = func(session=session, **func_params)
|
|
190
165
|
else:
|
|
191
166
|
result = func(**body)
|
|
192
|
-
|
|
167
|
+
|
|
193
168
|
if inspect.isawaitable(result):
|
|
194
169
|
result = await result
|
|
195
|
-
|
|
196
|
-
# Return result with session ID
|
|
170
|
+
|
|
197
171
|
response = JSONResponse(content=result)
|
|
198
172
|
response.set_cookie("session_id", session_id)
|
|
199
173
|
response.headers["X-Session-ID"] = session_id
|
|
200
|
-
|
|
201
174
|
return response
|
|
202
175
|
except Exception as e:
|
|
203
176
|
return JSONResponse(content={"error": str(e)}, status_code=500)
|
|
204
|
-
|
|
177
|
+
|
|
205
178
|
self.app.post(endpoint_path)(session_endpoint)
|
|
206
|
-
|
|
207
179
|
return func
|
|
180
|
+
|
|
208
181
|
return decorator
|
|
209
|
-
|
|
182
|
+
|
|
210
183
|
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
184
|
def decorator(func: Callable):
|
|
222
185
|
self.stream_actions[name] = {
|
|
223
186
|
"func": func,
|
|
224
187
|
"description": description,
|
|
225
|
-
"params": self._extract_params(func)
|
|
188
|
+
"params": self._extract_params(func),
|
|
226
189
|
}
|
|
227
|
-
|
|
190
|
+
|
|
228
191
|
endpoint_path = f"/stream/{name}"
|
|
229
|
-
|
|
192
|
+
|
|
230
193
|
async def stream_endpoint(request: Request):
|
|
231
194
|
try:
|
|
232
|
-
# Get parameters
|
|
233
195
|
params = dict(request.query_params) if request.method == "GET" else {}
|
|
234
196
|
if request.method == "POST":
|
|
235
197
|
body = await request.json()
|
|
236
198
|
params.update(body)
|
|
237
|
-
|
|
238
|
-
# Convert parameter types based on function signature
|
|
199
|
+
|
|
239
200
|
sig = inspect.signature(func)
|
|
240
|
-
type_hints = get_type_hints(func) if hasattr(func,
|
|
201
|
+
type_hints = get_type_hints(func) if hasattr(func, "__annotations__") else {}
|
|
241
202
|
converted_params = {}
|
|
242
203
|
for key, value in params.items():
|
|
243
204
|
if key in sig.parameters:
|
|
@@ -248,222 +209,114 @@ class KalibrApp:
|
|
|
248
209
|
elif param_type == float:
|
|
249
210
|
converted_params[key] = float(value)
|
|
250
211
|
elif param_type == bool:
|
|
251
|
-
converted_params[key] = value.lower() in (
|
|
212
|
+
converted_params[key] = value.lower() in ("true", "1", "yes")
|
|
252
213
|
else:
|
|
253
214
|
converted_params[key] = value
|
|
254
|
-
except
|
|
215
|
+
except Exception:
|
|
255
216
|
converted_params[key] = value
|
|
256
|
-
|
|
257
|
-
# Call generator function
|
|
217
|
+
|
|
258
218
|
result = func(**converted_params)
|
|
259
|
-
|
|
260
|
-
# Create streaming generator
|
|
219
|
+
|
|
261
220
|
async def generate():
|
|
221
|
+
import json
|
|
262
222
|
if inspect.isasyncgen(result):
|
|
263
223
|
async for item in result:
|
|
264
|
-
import json
|
|
265
224
|
yield json.dumps(item) + "\n"
|
|
266
225
|
elif inspect.isgenerator(result):
|
|
267
226
|
for item in result:
|
|
268
|
-
import json
|
|
269
227
|
yield json.dumps(item) + "\n"
|
|
270
|
-
|
|
228
|
+
|
|
271
229
|
return FastAPIStreamingResponse(generate(), media_type="application/x-ndjson")
|
|
272
230
|
except Exception as e:
|
|
273
231
|
return JSONResponse(content={"error": str(e)}, status_code=500)
|
|
274
|
-
|
|
232
|
+
|
|
275
233
|
self.app.get(endpoint_path)(stream_endpoint)
|
|
276
234
|
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
235
|
return func
|
|
236
|
+
|
|
343
237
|
return decorator
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
238
|
+
|
|
239
|
+
# -------------------------------------------------------------------------
|
|
240
|
+
# Schema generation routes
|
|
241
|
+
# -------------------------------------------------------------------------
|
|
242
|
+
|
|
386
243
|
def _setup_routes(self):
|
|
387
|
-
"""Setup core API routes."""
|
|
388
244
|
from kalibr.schema_generators import (
|
|
389
245
|
OpenAPISchemaGenerator,
|
|
390
246
|
MCPSchemaGenerator,
|
|
391
247
|
GeminiSchemaGenerator,
|
|
392
|
-
CopilotSchemaGenerator
|
|
248
|
+
CopilotSchemaGenerator,
|
|
393
249
|
)
|
|
394
|
-
|
|
395
|
-
# Initialize schema generators
|
|
250
|
+
|
|
396
251
|
openapi_gen = OpenAPISchemaGenerator()
|
|
397
252
|
mcp_gen = MCPSchemaGenerator()
|
|
398
253
|
gemini_gen = GeminiSchemaGenerator()
|
|
399
254
|
copilot_gen = CopilotSchemaGenerator()
|
|
400
|
-
|
|
255
|
+
|
|
401
256
|
@self.app.get("/")
|
|
402
257
|
def root():
|
|
403
|
-
"""Root endpoint with API information."""
|
|
404
258
|
return {
|
|
405
259
|
"message": "Kalibr Enhanced API is running",
|
|
406
260
|
"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
261
|
"schemas": {
|
|
412
262
|
"gpt_actions": f"{self.base_url}/gpt-actions.json",
|
|
413
|
-
"openapi_swagger": f"{self.base_url}/openapi.json",
|
|
414
263
|
"claude_mcp": f"{self.base_url}/mcp.json",
|
|
415
264
|
"gemini": f"{self.base_url}/schemas/gemini",
|
|
416
|
-
"copilot": f"{self.base_url}/schemas/copilot"
|
|
417
|
-
}
|
|
265
|
+
"copilot": f"{self.base_url}/schemas/copilot",
|
|
266
|
+
},
|
|
418
267
|
}
|
|
419
|
-
|
|
268
|
+
|
|
420
269
|
@self.app.get("/gpt-actions.json")
|
|
421
270
|
def gpt_actions_schema():
|
|
422
|
-
"""Generate GPT Actions schema from all registered actions."""
|
|
423
|
-
# Combine all action types for schema generation
|
|
424
271
|
all_actions = {**self.actions, **self.file_handlers, **self.session_actions}
|
|
425
272
|
return openapi_gen.generate_schema(all_actions, self.base_url)
|
|
426
|
-
|
|
273
|
+
|
|
427
274
|
@self.app.get("/mcp.json")
|
|
428
275
|
def mcp_manifest():
|
|
429
|
-
"""Generate Claude MCP manifest."""
|
|
430
276
|
all_actions = {**self.actions, **self.file_handlers, **self.session_actions}
|
|
431
277
|
return mcp_gen.generate_schema(all_actions, self.base_url)
|
|
432
|
-
|
|
278
|
+
|
|
433
279
|
@self.app.get("/schemas/gemini")
|
|
434
280
|
def gemini_schema():
|
|
435
|
-
"""Generate Gemini Extensions schema."""
|
|
436
281
|
all_actions = {**self.actions, **self.file_handlers, **self.session_actions}
|
|
437
282
|
return gemini_gen.generate_schema(all_actions, self.base_url)
|
|
438
|
-
|
|
283
|
+
|
|
439
284
|
@self.app.get("/schemas/copilot")
|
|
440
285
|
def copilot_schema():
|
|
441
|
-
"""Generate Microsoft Copilot schema."""
|
|
442
286
|
all_actions = {**self.actions, **self.file_handlers, **self.session_actions}
|
|
443
287
|
return copilot_gen.generate_schema(all_actions, self.base_url)
|
|
444
|
-
|
|
445
|
-
# Health check
|
|
288
|
+
|
|
446
289
|
@self.app.get("/health")
|
|
447
290
|
def health_check():
|
|
448
|
-
return {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
)
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
291
|
+
return {"status": "healthy", "service": "Kalibr Enhanced API"}
|
|
292
|
+
|
|
293
|
+
# -------------------------------------------------------------------------
|
|
294
|
+
# Helpers
|
|
295
|
+
# -------------------------------------------------------------------------
|
|
296
|
+
|
|
297
|
+
def _extract_params(self, func: Callable) -> Dict[str, Any]:
|
|
298
|
+
sig = inspect.signature(func)
|
|
299
|
+
params = {}
|
|
300
|
+
type_hints = get_type_hints(func) if hasattr(func, "__annotations__") else {}
|
|
301
|
+
|
|
302
|
+
for param_name, param in sig.parameters.items():
|
|
303
|
+
if param_name in ["session", "workflow_state", "file"]:
|
|
304
|
+
continue
|
|
305
|
+
|
|
306
|
+
param_type = "string"
|
|
307
|
+
anno = type_hints.get(param_name, param.annotation)
|
|
308
|
+
if anno == int:
|
|
309
|
+
param_type = "integer"
|
|
310
|
+
elif anno == bool:
|
|
311
|
+
param_type = "boolean"
|
|
312
|
+
elif anno == float:
|
|
313
|
+
param_type = "number"
|
|
314
|
+
elif anno == list:
|
|
315
|
+
param_type = "array"
|
|
316
|
+
elif anno == dict:
|
|
317
|
+
param_type = "object"
|
|
318
|
+
|
|
319
|
+
is_required = param.default == inspect.Parameter.empty
|
|
320
|
+
params[param_name] = {"type": param_type, "required": is_required}
|
|
321
|
+
|
|
322
|
+
return params
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|