kalibr 1.0.18__py3-none-any.whl → 1.0.21__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 +3 -4
- kalibr/__main__.py +63 -6
- kalibr/kalibr.py +53 -43
- kalibr/kalibr_app.py +314 -60
- kalibr-1.0.21.data/data/examples/README.md +173 -0
- kalibr-1.0.21.data/data/examples/basic_kalibr_example.py +66 -0
- kalibr-1.0.21.data/data/examples/enhanced_kalibr_example.py +347 -0
- kalibr-1.0.21.dist-info/METADATA +302 -0
- kalibr-1.0.21.dist-info/RECORD +16 -0
- kalibr-1.0.18.dist-info/METADATA +0 -94
- kalibr-1.0.18.dist-info/RECORD +0 -13
- {kalibr-1.0.18.dist-info → kalibr-1.0.21.dist-info}/WHEEL +0 -0
- {kalibr-1.0.18.dist-info → kalibr-1.0.21.dist-info}/entry_points.txt +0 -0
- {kalibr-1.0.18.dist-info → kalibr-1.0.21.dist-info}/licenses/LICENSE +0 -0
- {kalibr-1.0.18.dist-info → kalibr-1.0.21.dist-info}/top_level.txt +0 -0
kalibr/__init__.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"""Kalibr SDK - Multi-Model AI Integration Framework"""
|
|
2
2
|
|
|
3
3
|
from kalibr.kalibr import Kalibr
|
|
4
|
-
|
|
5
|
-
# from kalibr.kalibr_app import KalibrApp
|
|
4
|
+
from kalibr.kalibr_app import KalibrApp
|
|
6
5
|
|
|
7
|
-
__version__ = "1.0.
|
|
8
|
-
__all__ = ["Kalibr"
|
|
6
|
+
__version__ = "1.0.20"
|
|
7
|
+
__all__ = ["Kalibr", "KalibrApp"]
|
kalibr/__main__.py
CHANGED
|
@@ -48,10 +48,7 @@ def serve(
|
|
|
48
48
|
raise typer.Exit(1)
|
|
49
49
|
|
|
50
50
|
# Import Kalibr classes
|
|
51
|
-
from kalibr import Kalibr
|
|
52
|
-
# KalibrApp import will be enabled once it's properly implemented
|
|
53
|
-
# from kalibr import KalibrApp
|
|
54
|
-
KalibrApp = None # Placeholder until implemented
|
|
51
|
+
from kalibr import Kalibr, KalibrApp
|
|
55
52
|
kalibr_instance = None
|
|
56
53
|
|
|
57
54
|
# Iterate through the attributes of the loaded module
|
|
@@ -69,8 +66,14 @@ def serve(
|
|
|
69
66
|
print(f"❌ Error: No Kalibr/KalibrApp instance found in {file}")
|
|
70
67
|
raise typer.Exit(1)
|
|
71
68
|
|
|
72
|
-
# Get the FastAPI application from the Kalibr instance
|
|
73
|
-
|
|
69
|
+
# Get the FastAPI application from the Kalibr/KalibrApp instance
|
|
70
|
+
if hasattr(kalibr_instance, 'get_app'):
|
|
71
|
+
fastapi_app = kalibr_instance.get_app()
|
|
72
|
+
elif hasattr(kalibr_instance, 'app'):
|
|
73
|
+
fastapi_app = kalibr_instance.app
|
|
74
|
+
else:
|
|
75
|
+
print(f"❌ Error: Kalibr instance has no get_app() method or app attribute")
|
|
76
|
+
raise typer.Exit(1)
|
|
74
77
|
|
|
75
78
|
# Print server information
|
|
76
79
|
is_enhanced = KalibrApp is not None and isinstance(kalibr_instance, KalibrApp)
|
|
@@ -589,6 +592,60 @@ def test(
|
|
|
589
592
|
print(f"❌ Could not connect to {url}")
|
|
590
593
|
print("Make sure the Kalibr server is running")
|
|
591
594
|
|
|
595
|
+
@app.command()
|
|
596
|
+
def examples():
|
|
597
|
+
"""Copy example files to current directory."""
|
|
598
|
+
import shutil
|
|
599
|
+
from pathlib import Path
|
|
600
|
+
import sys
|
|
601
|
+
import kalibr
|
|
602
|
+
|
|
603
|
+
# Find examples directory - check multiple possible locations
|
|
604
|
+
kalibr_path = Path(kalibr.__file__).parent
|
|
605
|
+
|
|
606
|
+
# Location 1: Sibling to kalibr package (development install)
|
|
607
|
+
examples_src = kalibr_path.parent / "examples"
|
|
608
|
+
|
|
609
|
+
# Location 2: In site-packages parent (wheel install with data_files)
|
|
610
|
+
if not examples_src.exists():
|
|
611
|
+
site_packages = Path(kalibr.__file__).parent.parent
|
|
612
|
+
examples_src = site_packages.parent / "examples"
|
|
613
|
+
|
|
614
|
+
# Location 3: Check sys.prefix/examples
|
|
615
|
+
if not examples_src.exists():
|
|
616
|
+
examples_src = Path(sys.prefix) / "examples"
|
|
617
|
+
|
|
618
|
+
if not examples_src.exists():
|
|
619
|
+
print(f"❌ Examples directory not found.")
|
|
620
|
+
print(f" Checked locations:")
|
|
621
|
+
print(f" - {kalibr_path.parent / 'examples'}")
|
|
622
|
+
print(f" - {Path(kalibr.__file__).parent.parent.parent / 'examples'}")
|
|
623
|
+
print(f" - {Path(sys.prefix) / 'examples'}")
|
|
624
|
+
print("This might happen if kalibr was installed without examples.")
|
|
625
|
+
raise typer.Exit(1)
|
|
626
|
+
|
|
627
|
+
# Copy to current directory
|
|
628
|
+
examples_dest = Path.cwd() / "kalibr_examples"
|
|
629
|
+
|
|
630
|
+
if examples_dest.exists():
|
|
631
|
+
print(f"⚠️ Directory 'kalibr_examples' already exists")
|
|
632
|
+
overwrite = typer.confirm("Do you want to overwrite it?")
|
|
633
|
+
if not overwrite:
|
|
634
|
+
print("Cancelled.")
|
|
635
|
+
raise typer.Exit(0)
|
|
636
|
+
shutil.rmtree(examples_dest)
|
|
637
|
+
|
|
638
|
+
shutil.copytree(examples_src, examples_dest)
|
|
639
|
+
|
|
640
|
+
print(f"✅ Examples copied to: {examples_dest}")
|
|
641
|
+
print(f"\n📚 Available examples:")
|
|
642
|
+
for example in examples_dest.glob("*.py"):
|
|
643
|
+
print(f" - {example.name}")
|
|
644
|
+
|
|
645
|
+
print(f"\n🚀 Try running:")
|
|
646
|
+
print(f" kalibr-connect serve kalibr_examples/basic_kalibr_example.py")
|
|
647
|
+
print(f" kalibr-connect serve kalibr_examples/enhanced_kalibr_example.py")
|
|
648
|
+
|
|
592
649
|
@app.command()
|
|
593
650
|
def version():
|
|
594
651
|
"""Show Kalibr version information."""
|
kalibr/kalibr.py
CHANGED
|
@@ -153,65 +153,75 @@ class Kalibr:
|
|
|
153
153
|
|
|
154
154
|
This includes:
|
|
155
155
|
- A root endpoint ("/") for basic API status and available actions.
|
|
156
|
-
-
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
- Multi-model schema endpoints for all major AI platforms:
|
|
157
|
+
- /openapi.json (GPT Actions)
|
|
158
|
+
- /mcp.json (Claude MCP)
|
|
159
|
+
- /schemas/gemini (Google Gemini)
|
|
160
|
+
- /schemas/copilot (Microsoft Copilot)
|
|
159
161
|
"""
|
|
162
|
+
from kalibr.schema_generators import (
|
|
163
|
+
OpenAPISchemaGenerator,
|
|
164
|
+
MCPSchemaGenerator,
|
|
165
|
+
GeminiSchemaGenerator,
|
|
166
|
+
CopilotSchemaGenerator
|
|
167
|
+
)
|
|
160
168
|
|
|
161
169
|
@self.app.get("/")
|
|
162
170
|
def root():
|
|
163
171
|
"""
|
|
164
172
|
Root endpoint providing API status and a list of available actions.
|
|
165
173
|
"""
|
|
166
|
-
return {
|
|
174
|
+
return {
|
|
175
|
+
"message": "Kalibr API is running",
|
|
176
|
+
"actions": list(self.actions.keys()),
|
|
177
|
+
"schemas": {
|
|
178
|
+
"gpt_actions": f"{self.base_url}/gpt-actions.json",
|
|
179
|
+
"openapi_swagger": f"{self.base_url}/openapi.json",
|
|
180
|
+
"claude_mcp": f"{self.base_url}/mcp.json",
|
|
181
|
+
"gemini": f"{self.base_url}/schemas/gemini",
|
|
182
|
+
"copilot": f"{self.base_url}/schemas/copilot"
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
# Initialize schema generators
|
|
187
|
+
openapi_gen = OpenAPISchemaGenerator()
|
|
188
|
+
mcp_gen = MCPSchemaGenerator()
|
|
189
|
+
gemini_gen = GeminiSchemaGenerator()
|
|
190
|
+
copilot_gen = CopilotSchemaGenerator()
|
|
191
|
+
|
|
192
|
+
@self.app.get("/gpt-actions.json")
|
|
193
|
+
def gpt_actions_schema():
|
|
194
|
+
"""
|
|
195
|
+
Generates OpenAPI 3.0 schema for GPT Actions integration.
|
|
196
|
+
(Alternative endpoint since /openapi.json is used by FastAPI)
|
|
197
|
+
"""
|
|
198
|
+
return openapi_gen.generate_schema(self.actions, self.base_url)
|
|
167
199
|
|
|
168
200
|
@self.app.get("/mcp.json")
|
|
169
201
|
def mcp_manifest():
|
|
170
202
|
"""
|
|
171
|
-
Generates
|
|
172
|
-
|
|
173
|
-
This manifest describes the available tools (actions) and their input schemas
|
|
174
|
-
in a format consumable by Claude.
|
|
203
|
+
Generates Claude MCP manifest for AI model integration.
|
|
175
204
|
"""
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
"type": "object",
|
|
192
|
-
"properties": properties,
|
|
193
|
-
"required": required
|
|
194
|
-
},
|
|
195
|
-
# The server URL points to the proxy endpoint for this action
|
|
196
|
-
"server": {
|
|
197
|
-
"url": f"{self.base_url}/proxy/{action_name}"
|
|
198
|
-
}
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
# Return the MCP manifest structure
|
|
202
|
-
return {
|
|
203
|
-
"mcp": "1.0",
|
|
204
|
-
"name": "kalibr", # Name of the AI agent or toolset
|
|
205
|
-
"tools": tools
|
|
206
|
-
}
|
|
205
|
+
return mcp_gen.generate_schema(self.actions, self.base_url)
|
|
206
|
+
|
|
207
|
+
@self.app.get("/schemas/gemini")
|
|
208
|
+
def gemini_schema():
|
|
209
|
+
"""
|
|
210
|
+
Generates Google Gemini Extensions schema.
|
|
211
|
+
"""
|
|
212
|
+
return gemini_gen.generate_schema(self.actions, self.base_url)
|
|
213
|
+
|
|
214
|
+
@self.app.get("/schemas/copilot")
|
|
215
|
+
def copilot_schema():
|
|
216
|
+
"""
|
|
217
|
+
Generates Microsoft Copilot plugin schema.
|
|
218
|
+
"""
|
|
219
|
+
return copilot_gen.generate_schema(self.actions, self.base_url)
|
|
207
220
|
|
|
208
221
|
# Override FastAPI's default OpenAPI generation to include servers configuration
|
|
209
222
|
def custom_openapi():
|
|
210
223
|
"""
|
|
211
|
-
Customizes the OpenAPI schema generation.
|
|
212
|
-
|
|
213
|
-
Adds a 'servers' block to the OpenAPI schema, which is often required
|
|
214
|
-
by AI model integrations (e.g., OpenAI function calling).
|
|
224
|
+
Customizes the OpenAPI schema generation for Swagger UI.
|
|
215
225
|
"""
|
|
216
226
|
if self.app.openapi_schema:
|
|
217
227
|
return self.app.openapi_schema
|
kalibr/kalibr_app.py
CHANGED
|
@@ -1,68 +1,322 @@
|
|
|
1
|
-
from fastapi import FastAPI
|
|
2
|
-
import
|
|
3
|
-
import
|
|
1
|
+
from fastapi import FastAPI, Request, UploadFile, File
|
|
2
|
+
from fastapi.responses import JSONResponse, StreamingResponse as FastAPIStreamingResponse
|
|
3
|
+
from typing import Callable, Dict, Any, List, Optional, get_type_hints
|
|
4
4
|
import inspect
|
|
5
|
-
|
|
5
|
+
import asyncio
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
import uuid
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
from kalibr.types import FileUpload, Session, WorkflowState
|
|
11
|
+
|
|
6
12
|
|
|
7
13
|
class KalibrApp:
|
|
8
14
|
"""
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
Enhanced app-level Kalibr framework with advanced capabilities:
|
|
16
|
+
- File upload handling
|
|
17
|
+
- Session management
|
|
18
|
+
- Streaming responses
|
|
19
|
+
- Complex workflows
|
|
20
|
+
- Multi-model schema generation
|
|
11
21
|
"""
|
|
12
22
|
|
|
13
|
-
def __init__(self,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
23
|
+
def __init__(self, title="Kalibr Enhanced API", version="2.0.0", base_url: Optional[str] = None):
|
|
24
|
+
"""
|
|
25
|
+
Initialize the Kalibr enhanced app.
|
|
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
|
|
33
|
+
"""
|
|
34
|
+
self.app = FastAPI(title=title, version=version)
|
|
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
|
+
|
|
45
|
+
# Storage for different action types
|
|
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
|
|
53
|
+
self.sessions: Dict[str, Session] = {}
|
|
54
|
+
self.workflow_states: Dict[str, WorkflowState] = {}
|
|
55
|
+
|
|
56
|
+
self._setup_routes()
|
|
57
|
+
|
|
58
|
+
# -------------------------------------------------------------------------
|
|
59
|
+
# Action registration decorators
|
|
60
|
+
# -------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
def action(self, name: str, description: str = ""):
|
|
63
|
+
def decorator(func: Callable):
|
|
64
|
+
self.actions[name] = {
|
|
65
|
+
"func": func,
|
|
66
|
+
"description": description,
|
|
67
|
+
"params": self._extract_params(func),
|
|
50
68
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
69
|
+
|
|
70
|
+
endpoint_path = f"/proxy/{name}"
|
|
71
|
+
|
|
72
|
+
async def endpoint_handler(request: Request):
|
|
73
|
+
params = {}
|
|
74
|
+
if request.method == "POST":
|
|
75
|
+
try:
|
|
76
|
+
body = await request.json()
|
|
77
|
+
params = body if isinstance(body, dict) else {}
|
|
78
|
+
except Exception:
|
|
79
|
+
params = {}
|
|
80
|
+
else:
|
|
81
|
+
params = dict(request.query_params)
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
result = func(**params)
|
|
85
|
+
if inspect.isawaitable(result):
|
|
86
|
+
result = await result
|
|
87
|
+
return JSONResponse(content=result)
|
|
88
|
+
except Exception as e:
|
|
89
|
+
return JSONResponse(content={"error": str(e)}, status_code=500)
|
|
90
|
+
|
|
91
|
+
self.app.post(endpoint_path)(endpoint_handler)
|
|
92
|
+
self.app.get(endpoint_path)(endpoint_handler)
|
|
93
|
+
return func
|
|
94
|
+
|
|
95
|
+
return decorator
|
|
96
|
+
|
|
97
|
+
def file_handler(self, name: str, allowed_extensions: List[str] = None, description: str = ""):
|
|
98
|
+
def decorator(func: Callable):
|
|
99
|
+
self.file_handlers[name] = {
|
|
100
|
+
"func": func,
|
|
101
|
+
"description": description,
|
|
102
|
+
"allowed_extensions": allowed_extensions or [],
|
|
103
|
+
"params": self._extract_params(func),
|
|
55
104
|
}
|
|
56
|
-
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
105
|
+
|
|
106
|
+
endpoint_path = f"/files/{name}"
|
|
107
|
+
|
|
108
|
+
async def file_endpoint(file: UploadFile = File(...)):
|
|
109
|
+
try:
|
|
110
|
+
if allowed_extensions:
|
|
111
|
+
file_ext = "." + file.filename.split(".")[-1] if "." in file.filename else ""
|
|
112
|
+
if file_ext not in allowed_extensions:
|
|
113
|
+
return JSONResponse(
|
|
114
|
+
content={"error": f"File type {file_ext} not allowed. Allowed: {allowed_extensions}"},
|
|
115
|
+
status_code=400,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
content = await file.read()
|
|
119
|
+
file_upload = FileUpload(
|
|
120
|
+
filename=file.filename,
|
|
121
|
+
content_type=file.content_type or "application/octet-stream",
|
|
122
|
+
size=len(content),
|
|
123
|
+
content=content,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
result = func(file_upload)
|
|
127
|
+
if inspect.isawaitable(result):
|
|
128
|
+
result = await result
|
|
129
|
+
return JSONResponse(content=result)
|
|
130
|
+
except Exception as e:
|
|
131
|
+
return JSONResponse(content={"error": str(e)}, status_code=500)
|
|
132
|
+
|
|
133
|
+
self.app.post(endpoint_path)(file_endpoint)
|
|
134
|
+
return func
|
|
135
|
+
|
|
136
|
+
return decorator
|
|
137
|
+
|
|
138
|
+
def session_action(self, name: str, description: str = ""):
|
|
139
|
+
def decorator(func: Callable):
|
|
140
|
+
self.session_actions[name] = {
|
|
141
|
+
"func": func,
|
|
142
|
+
"description": description,
|
|
143
|
+
"params": self._extract_params(func),
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
endpoint_path = f"/session/{name}"
|
|
147
|
+
|
|
148
|
+
async def session_endpoint(request: Request):
|
|
149
|
+
try:
|
|
150
|
+
session_id = request.headers.get("X-Session-ID") or request.cookies.get("session_id")
|
|
151
|
+
if not session_id or session_id not in self.sessions:
|
|
152
|
+
session_id = str(uuid.uuid4())
|
|
153
|
+
session = Session(session_id=session_id)
|
|
154
|
+
self.sessions[session_id] = session
|
|
155
|
+
else:
|
|
156
|
+
session = self.sessions[session_id]
|
|
157
|
+
session.last_accessed = datetime.now()
|
|
158
|
+
|
|
159
|
+
body = await request.json() if request.method == "POST" else {}
|
|
160
|
+
|
|
161
|
+
sig = inspect.signature(func)
|
|
162
|
+
if "session" in sig.parameters:
|
|
163
|
+
func_params = {k: v for k, v in body.items() if k != "session"}
|
|
164
|
+
result = func(session=session, **func_params)
|
|
165
|
+
else:
|
|
166
|
+
result = func(**body)
|
|
167
|
+
|
|
168
|
+
if inspect.isawaitable(result):
|
|
169
|
+
result = await result
|
|
170
|
+
|
|
171
|
+
response = JSONResponse(content=result)
|
|
172
|
+
response.set_cookie("session_id", session_id)
|
|
173
|
+
response.headers["X-Session-ID"] = session_id
|
|
174
|
+
return response
|
|
175
|
+
except Exception as e:
|
|
176
|
+
return JSONResponse(content={"error": str(e)}, status_code=500)
|
|
177
|
+
|
|
178
|
+
self.app.post(endpoint_path)(session_endpoint)
|
|
179
|
+
return func
|
|
180
|
+
|
|
181
|
+
return decorator
|
|
182
|
+
|
|
183
|
+
def stream_action(self, name: str, description: str = ""):
|
|
184
|
+
def decorator(func: Callable):
|
|
185
|
+
self.stream_actions[name] = {
|
|
186
|
+
"func": func,
|
|
187
|
+
"description": description,
|
|
188
|
+
"params": self._extract_params(func),
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
endpoint_path = f"/stream/{name}"
|
|
192
|
+
|
|
193
|
+
async def stream_endpoint(request: Request):
|
|
194
|
+
try:
|
|
195
|
+
params = dict(request.query_params) if request.method == "GET" else {}
|
|
196
|
+
if request.method == "POST":
|
|
197
|
+
body = await request.json()
|
|
198
|
+
params.update(body)
|
|
199
|
+
|
|
200
|
+
sig = inspect.signature(func)
|
|
201
|
+
type_hints = get_type_hints(func) if hasattr(func, "__annotations__") else {}
|
|
202
|
+
converted_params = {}
|
|
203
|
+
for key, value in params.items():
|
|
204
|
+
if key in sig.parameters:
|
|
205
|
+
param_type = type_hints.get(key, str)
|
|
206
|
+
try:
|
|
207
|
+
if param_type == int:
|
|
208
|
+
converted_params[key] = int(value)
|
|
209
|
+
elif param_type == float:
|
|
210
|
+
converted_params[key] = float(value)
|
|
211
|
+
elif param_type == bool:
|
|
212
|
+
converted_params[key] = value.lower() in ("true", "1", "yes")
|
|
213
|
+
else:
|
|
214
|
+
converted_params[key] = value
|
|
215
|
+
except Exception:
|
|
216
|
+
converted_params[key] = value
|
|
217
|
+
|
|
218
|
+
result = func(**converted_params)
|
|
219
|
+
|
|
220
|
+
async def generate():
|
|
221
|
+
import json
|
|
222
|
+
if inspect.isasyncgen(result):
|
|
223
|
+
async for item in result:
|
|
224
|
+
yield json.dumps(item) + "\n"
|
|
225
|
+
elif inspect.isgenerator(result):
|
|
226
|
+
for item in result:
|
|
227
|
+
yield json.dumps(item) + "\n"
|
|
228
|
+
|
|
229
|
+
return FastAPIStreamingResponse(generate(), media_type="application/x-ndjson")
|
|
230
|
+
except Exception as e:
|
|
231
|
+
return JSONResponse(content={"error": str(e)}, status_code=500)
|
|
232
|
+
|
|
233
|
+
self.app.get(endpoint_path)(stream_endpoint)
|
|
234
|
+
self.app.post(endpoint_path)(stream_endpoint)
|
|
235
|
+
return func
|
|
236
|
+
|
|
237
|
+
return decorator
|
|
238
|
+
|
|
239
|
+
# -------------------------------------------------------------------------
|
|
240
|
+
# Schema generation routes
|
|
241
|
+
# -------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
def _setup_routes(self):
|
|
244
|
+
from kalibr.schema_generators import (
|
|
245
|
+
OpenAPISchemaGenerator,
|
|
246
|
+
MCPSchemaGenerator,
|
|
247
|
+
GeminiSchemaGenerator,
|
|
248
|
+
CopilotSchemaGenerator,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
openapi_gen = OpenAPISchemaGenerator()
|
|
252
|
+
mcp_gen = MCPSchemaGenerator()
|
|
253
|
+
gemini_gen = GeminiSchemaGenerator()
|
|
254
|
+
copilot_gen = CopilotSchemaGenerator()
|
|
255
|
+
|
|
256
|
+
@self.app.get("/")
|
|
257
|
+
def root():
|
|
258
|
+
return {
|
|
259
|
+
"message": "Kalibr Enhanced API is running",
|
|
260
|
+
"actions": list(self.actions.keys()),
|
|
261
|
+
"schemas": {
|
|
262
|
+
"gpt_actions": f"{self.base_url}/gpt-actions.json",
|
|
263
|
+
"claude_mcp": f"{self.base_url}/mcp.json",
|
|
264
|
+
"gemini": f"{self.base_url}/schemas/gemini",
|
|
265
|
+
"copilot": f"{self.base_url}/schemas/copilot",
|
|
266
|
+
},
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
@self.app.get("/gpt-actions.json")
|
|
270
|
+
def gpt_actions_schema():
|
|
271
|
+
all_actions = {**self.actions, **self.file_handlers, **self.session_actions}
|
|
272
|
+
return openapi_gen.generate_schema(all_actions, self.base_url)
|
|
273
|
+
|
|
274
|
+
@self.app.get("/mcp.json")
|
|
275
|
+
def mcp_manifest():
|
|
276
|
+
all_actions = {**self.actions, **self.file_handlers, **self.session_actions}
|
|
277
|
+
return mcp_gen.generate_schema(all_actions, self.base_url)
|
|
278
|
+
|
|
279
|
+
@self.app.get("/schemas/gemini")
|
|
280
|
+
def gemini_schema():
|
|
281
|
+
all_actions = {**self.actions, **self.file_handlers, **self.session_actions}
|
|
282
|
+
return gemini_gen.generate_schema(all_actions, self.base_url)
|
|
283
|
+
|
|
284
|
+
@self.app.get("/schemas/copilot")
|
|
285
|
+
def copilot_schema():
|
|
286
|
+
all_actions = {**self.actions, **self.file_handlers, **self.session_actions}
|
|
287
|
+
return copilot_gen.generate_schema(all_actions, self.base_url)
|
|
288
|
+
|
|
289
|
+
@self.app.get("/health")
|
|
290
|
+
def health_check():
|
|
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
|