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 CHANGED
@@ -1,8 +1,7 @@
1
1
  """Kalibr SDK - Multi-Model AI Integration Framework"""
2
2
 
3
3
  from kalibr.kalibr import Kalibr
4
- # KalibrApp will be imported once it's properly implemented
5
- # from kalibr.kalibr_app import KalibrApp
4
+ from kalibr.kalibr_app import KalibrApp
6
5
 
7
- __version__ = "1.0.18"
8
- __all__ = ["Kalibr"] # "KalibrApp" will be added once implemented
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
- fastapi_app = kalibr_instance.get_app()
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
- - An MCP (Model Communication Protocol) manifest endpoint ("/mcp.json")
157
- for AI model integrations.
158
- - Customizes OpenAPI schema generation to include server URLs.
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 {"message": "Kalibr API is running", "actions": list(self.actions.keys())}
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 the MCP manifest (Claude specific) for registered actions.
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
- tools = []
177
- for action_name, action_data in self.actions.items():
178
- properties = {}
179
- required = []
180
-
181
- # Construct the input schema for the tool
182
- for param_name, param_info in action_data["params"].items():
183
- properties[param_name] = {"type": param_info["type"]}
184
- if param_info["required"]:
185
- required.append(param_name)
186
-
187
- tools.append({
188
- "name": action_name,
189
- "description": action_data["description"],
190
- "input_schema": {
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 importlib.util
3
- import json
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
- from pydantic import BaseModel
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
- Minimal Kalibr SDK App that auto-generates schemas
10
- for GPT Actions, Claude MCP, Gemini, etc.
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, 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
+ 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
- schemas[name] = {
52
- "description": func.__doc__ or "",
53
- "parameters": params,
54
- "returns": str(sig.return_annotation),
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
- 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)
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