kalibr 1.0.17__py3-none-any.whl → 1.0.18__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.py ADDED
@@ -0,0 +1,249 @@
1
+ # kalibr/kalibr.py
2
+
3
+ from fastapi import FastAPI, Request
4
+ from fastapi.responses import JSONResponse
5
+ from typing import Callable, Dict, Any, get_type_hints
6
+ import inspect
7
+
8
+ class Kalibr:
9
+ """
10
+ A framework for creating API endpoints that can be easily integrated with AI models.
11
+ Kalibr simplifies the process of exposing Python functions as API actions,
12
+ providing automatic documentation, request handling, and metadata generation
13
+ for services like Claude's MCP and OpenAI's function calling.
14
+ """
15
+ def __init__(self, title="Kalibr API", version="1.0.0", base_url="http://localhost:8000"):
16
+ """
17
+ Initializes the Kalibr API.
18
+
19
+ Args:
20
+ title (str): The title of the API. Defaults to "Kalibr API".
21
+ version (str): The version of the API. Defaults to "1.0.0".
22
+ base_url (str): The base URL of the API, used for generating tool URLs.
23
+ Defaults to "http://localhost:8000".
24
+ """
25
+ self.app = FastAPI(title=title, version=version)
26
+ self.base_url = base_url
27
+ self.actions = {} # Stores registered actions and their metadata
28
+ self._setup_routes()
29
+
30
+ def action(self, name: str, description: str = ""):
31
+ """
32
+ Decorator to register a Python function as an API action.
33
+
34
+ This decorator automatically handles request routing (both GET and POST),
35
+ parameter extraction, and response formatting. It also generates metadata
36
+ required by AI model integrations.
37
+
38
+ Args:
39
+ name (str): The unique name of the action. This will be used as the
40
+ API endpoint path and in AI model tool definitions.
41
+ description (str): A human-readable description of what the action does.
42
+ This is used in AI model tool descriptions. Defaults to "".
43
+
44
+ Returns:
45
+ Callable: A decorator function.
46
+ """
47
+ def decorator(func: Callable):
48
+ # Store the function and its metadata
49
+ self.actions[name] = {
50
+ "func": func,
51
+ "description": description,
52
+ "params": self._extract_params(func)
53
+ }
54
+
55
+ # Define the endpoint path for this action
56
+ endpoint_path = f"/proxy/{name}"
57
+
58
+ # Create a unified handler that accepts both GET (query params) and POST (JSON body)
59
+ async def endpoint_handler(request: Request):
60
+ params = {}
61
+ if request.method == "POST":
62
+ # For POST requests, try to get parameters from the JSON body
63
+ try:
64
+ body = await request.json()
65
+ params = body if isinstance(body, dict) else {}
66
+ except Exception:
67
+ # If JSON parsing fails or body is not a dict, treat as empty params
68
+ params = {}
69
+ else:
70
+ # For GET requests, use query parameters
71
+ params = dict(request.query_params)
72
+
73
+ # Call the original registered function with extracted parameters
74
+ try:
75
+ result = func(**params)
76
+ # If the result is a coroutine, await it
77
+ if inspect.isawaitable(result):
78
+ result = await result
79
+ return JSONResponse(content=result)
80
+ except Exception as e:
81
+ # Basic error handling for function execution
82
+ return JSONResponse(content={"error": str(e)}, status_code=500)
83
+
84
+ # Register both POST and GET endpoints for the same path
85
+ self.app.post(endpoint_path)(endpoint_handler)
86
+ self.app.get(endpoint_path)(endpoint_handler)
87
+
88
+ return func # Return the original function
89
+ return decorator
90
+
91
+ def _extract_params(self, func: Callable) -> Dict:
92
+ """
93
+ Extracts parameter names, types, and requirements from a function's signature.
94
+
95
+ This method inspects the function's signature and type hints to generate
96
+ a schema representation of its parameters, suitable for API documentation
97
+ and AI model integrations.
98
+
99
+ Args:
100
+ func (Callable): The function to inspect.
101
+
102
+ Returns:
103
+ Dict: A dictionary where keys are parameter names and values are dictionaries
104
+ containing 'type' (JSON schema type) and 'required' (boolean) information.
105
+ """
106
+ sig = inspect.signature(func)
107
+ params = {}
108
+
109
+ # Get type hints from annotations if available
110
+ type_hints = get_type_hints(func) if hasattr(func, '__annotations__') else {}
111
+
112
+ for param_name, param in sig.parameters.items():
113
+ param_type = "string" # Default type if none is inferred
114
+
115
+ # Determine the annotation for the parameter
116
+ if param_name in type_hints:
117
+ anno = type_hints[param_name]
118
+ elif param.annotation != inspect.Parameter.empty:
119
+ anno = param.annotation
120
+ else:
121
+ anno = str # Fallback to string if no annotation
122
+
123
+ # Map common Python types to their JSON schema equivalents
124
+ if anno == int:
125
+ param_type = "integer"
126
+ elif anno == bool:
127
+ param_type = "boolean"
128
+ elif anno == float:
129
+ param_type = "number"
130
+ elif anno == list or anno == dict:
131
+ # For lists and dicts, we can't automatically infer element/key types
132
+ # without more complex introspection or explicit type hints like List[str], Dict[str, int]
133
+ # For simplicity, we'll mark them as general objects/arrays.
134
+ # A more robust implementation might use a library like pydantic for schema generation.
135
+ if anno == list:
136
+ param_type = "array"
137
+ else:
138
+ param_type = "object"
139
+
140
+ # Determine if the parameter is required
141
+ is_required = param.default == inspect.Parameter.empty
142
+
143
+ params[param_name] = {
144
+ "type": param_type,
145
+ "required": is_required
146
+ }
147
+
148
+ return params
149
+
150
+ def _setup_routes(self):
151
+ """
152
+ Sets up the core routes for the Kalibr API.
153
+
154
+ This includes:
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.
159
+ """
160
+
161
+ @self.app.get("/")
162
+ def root():
163
+ """
164
+ Root endpoint providing API status and a list of available actions.
165
+ """
166
+ return {"message": "Kalibr API is running", "actions": list(self.actions.keys())}
167
+
168
+ @self.app.get("/mcp.json")
169
+ def mcp_manifest():
170
+ """
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.
175
+ """
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
+ }
207
+
208
+ # Override FastAPI's default OpenAPI generation to include servers configuration
209
+ def custom_openapi():
210
+ """
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).
215
+ """
216
+ if self.app.openapi_schema:
217
+ return self.app.openapi_schema
218
+
219
+ from fastapi.openapi.utils import get_openapi
220
+ # Generate the default OpenAPI schema
221
+ openapi_schema = get_openapi(
222
+ title=self.app.title,
223
+ version=self.app.version,
224
+ routes=self.app.routes,
225
+ )
226
+
227
+ # Add the 'servers' block to the schema
228
+ openapi_schema["servers"] = [{"url": self.base_url}]
229
+
230
+ self.app.openapi_schema = openapi_schema
231
+ return self.app.openapi_schema
232
+
233
+ # Assign the custom OpenAPI generator to the FastAPI app
234
+ self.app.openapi = custom_openapi
235
+
236
+ def get_app(self):
237
+ """
238
+ Returns the FastAPI application instance.
239
+
240
+ This allows the Kalibr API to be run using standard ASGI servers like Uvicorn.
241
+
242
+ Returns:
243
+ FastAPI: The configured FastAPI application.
244
+ """
245
+ return self.app
246
+
247
+ if __name__ == '__main__':
248
+ print("Kalibr SDK loaded. Use this class to build your API.")
249
+ print("See the __main__ block for example usage.")
kalibr/kalibr_app.py CHANGED
@@ -1,38 +1,68 @@
1
- import importlib.util
2
- import os
3
- import uvicorn
4
1
  from fastapi import FastAPI
2
+ import importlib.util
3
+ import json
4
+ import inspect
5
+ from pydantic import BaseModel
6
+
7
+ class KalibrApp:
8
+ """
9
+ Minimal Kalibr SDK App that auto-generates schemas
10
+ for GPT Actions, Claude MCP, Gemini, etc.
11
+ """
12
+
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"}
5
36
 
6
- class KalibrApp(FastAPI):
7
- def __init__(self):
8
- super().__init__()
9
- print("🚀 KalibrApp initialized. Ready to serve AI tools.")
37
+ async def route(payload: RequestSchema):
38
+ result = func(**payload.dict())
39
+ return {"result": result}
10
40
 
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)
41
+ self.app.post(f"/tools/{name}")(route)
14
42
 
15
- # --- CLI helper functions ---
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()
50
+ }
51
+ schemas[name] = {
52
+ "description": func.__doc__ or "",
53
+ "parameters": params,
54
+ "returns": str(sig.return_annotation),
55
+ }
56
+ return {
57
+ "gpt_actions": schemas,
58
+ "claude_mcp": schemas,
59
+ "gemini_tools": schemas,
60
+ }
16
61
 
17
62
  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.")
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)
@@ -1,13 +1,212 @@
1
- import json
2
-
3
- def generate_openapi_schema(app):
4
- return app.openapi()
5
-
6
- def generate_mcp_schema(app):
7
- return {
8
- "mcp": "1.0",
9
- "tools": [
10
- {"name": name, "description": func.__doc__ or ""}
11
- for name, func in app.actions.items()
12
- ],
13
- }
1
+ """
2
+ Multi-model schema generators for different AI platforms
3
+ """
4
+ from typing import Dict, Any, List
5
+ from abc import ABC, abstractmethod
6
+
7
+ class BaseSchemaGenerator(ABC):
8
+ """Base class for AI model schema generators"""
9
+
10
+ @abstractmethod
11
+ def generate_schema(self, actions: Dict, base_url: str) -> Dict[str, Any]:
12
+ """Generate schema for the specific AI model"""
13
+ pass
14
+
15
+ class MCPSchemaGenerator(BaseSchemaGenerator):
16
+ """Claude MCP schema generator"""
17
+
18
+ def generate_schema(self, actions: Dict, base_url: str) -> Dict[str, Any]:
19
+ tools = []
20
+ for action_name, action_data in actions.items():
21
+ properties = {}
22
+ required = []
23
+
24
+ # Construct the input schema for the tool
25
+ for param_name, param_info in action_data["params"].items():
26
+ properties[param_name] = {"type": param_info["type"]}
27
+ if param_info["required"]:
28
+ required.append(param_name)
29
+
30
+ tools.append({
31
+ "name": action_name,
32
+ "description": action_data["description"],
33
+ "input_schema": {
34
+ "type": "object",
35
+ "properties": properties,
36
+ "required": required
37
+ },
38
+ "server": {
39
+ "url": f"{base_url}/proxy/{action_name}"
40
+ }
41
+ })
42
+
43
+ return {
44
+ "mcp": "1.0",
45
+ "name": "kalibr-enhanced",
46
+ "tools": tools
47
+ }
48
+
49
+ class OpenAPISchemaGenerator(BaseSchemaGenerator):
50
+ """GPT Actions OpenAPI schema generator"""
51
+
52
+ def generate_schema(self, actions: Dict, base_url: str) -> Dict[str, Any]:
53
+ paths = {}
54
+
55
+ for action_name, action_data in actions.items():
56
+ properties = {}
57
+ required = []
58
+
59
+ for param_name, param_info in action_data["params"].items():
60
+ properties[param_name] = {"type": param_info["type"]}
61
+ if param_info["required"]:
62
+ required.append(param_name)
63
+
64
+ paths[f"/proxy/{action_name}"] = {
65
+ "post": {
66
+ "summary": action_data["description"],
67
+ "operationId": action_name,
68
+ "requestBody": {
69
+ "required": True,
70
+ "content": {
71
+ "application/json": {
72
+ "schema": {
73
+ "type": "object",
74
+ "properties": properties,
75
+ "required": required
76
+ }
77
+ }
78
+ }
79
+ },
80
+ "responses": {
81
+ "200": {
82
+ "description": "Successful response",
83
+ "content": {
84
+ "application/json": {
85
+ "schema": {"type": "object"}
86
+ }
87
+ }
88
+ }
89
+ }
90
+ }
91
+ }
92
+
93
+ return {
94
+ "openapi": "3.0.0",
95
+ "info": {
96
+ "title": "Kalibr Enhanced API",
97
+ "version": "2.0.0",
98
+ "description": "Enhanced Kalibr API with app-level capabilities"
99
+ },
100
+ "servers": [{"url": base_url}],
101
+ "paths": paths
102
+ }
103
+
104
+ class GeminiSchemaGenerator(BaseSchemaGenerator):
105
+ """Google Gemini Extensions schema generator"""
106
+
107
+ def generate_schema(self, actions: Dict, base_url: str) -> Dict[str, Any]:
108
+ functions = []
109
+
110
+ for action_name, action_data in actions.items():
111
+ parameters = {
112
+ "type": "object",
113
+ "properties": {},
114
+ "required": []
115
+ }
116
+
117
+ for param_name, param_info in action_data["params"].items():
118
+ parameters["properties"][param_name] = {
119
+ "type": param_info["type"],
120
+ "description": f"Parameter {param_name}"
121
+ }
122
+ if param_info["required"]:
123
+ parameters["required"].append(param_name)
124
+
125
+ functions.append({
126
+ "name": action_name,
127
+ "description": action_data["description"],
128
+ "parameters": parameters,
129
+ "server": {
130
+ "url": f"{base_url}/proxy/{action_name}"
131
+ }
132
+ })
133
+
134
+ return {
135
+ "gemini_extension": "1.0",
136
+ "name": "kalibr_enhanced",
137
+ "description": "Enhanced Kalibr API for Gemini integration",
138
+ "functions": functions
139
+ }
140
+
141
+ class CopilotSchemaGenerator(BaseSchemaGenerator):
142
+ """Microsoft Copilot plugin schema generator"""
143
+
144
+ def generate_schema(self, actions: Dict, base_url: str) -> Dict[str, Any]:
145
+ apis = []
146
+
147
+ for action_name, action_data in actions.items():
148
+ request_schema = {
149
+ "type": "object",
150
+ "properties": {},
151
+ "required": []
152
+ }
153
+
154
+ for param_name, param_info in action_data["params"].items():
155
+ request_schema["properties"][param_name] = {
156
+ "type": param_info["type"]
157
+ }
158
+ if param_info["required"]:
159
+ request_schema["required"].append(param_name)
160
+
161
+ apis.append({
162
+ "name": action_name,
163
+ "description": action_data["description"],
164
+ "url": f"{base_url}/proxy/{action_name}",
165
+ "method": "POST",
166
+ "request_schema": request_schema,
167
+ "response_schema": {
168
+ "type": "object",
169
+ "description": "API response"
170
+ }
171
+ })
172
+
173
+ return {
174
+ "schema_version": "v1",
175
+ "name_for_model": "kalibr_enhanced",
176
+ "name_for_human": "Enhanced Kalibr API",
177
+ "description_for_model": "Enhanced Kalibr API with advanced capabilities",
178
+ "description_for_human": "API for advanced AI model integrations",
179
+ "auth": {
180
+ "type": "none"
181
+ },
182
+ "api": {
183
+ "type": "openapi",
184
+ "url": f"{base_url}/openapi.json"
185
+ },
186
+ "apis": apis
187
+ }
188
+
189
+ class CustomModelSchemaGenerator(BaseSchemaGenerator):
190
+ """Extensible generator for future AI models"""
191
+
192
+ def __init__(self, model_name: str, schema_format: str):
193
+ self.model_name = model_name
194
+ self.schema_format = schema_format
195
+
196
+ def generate_schema(self, actions: Dict, base_url: str) -> Dict[str, Any]:
197
+ # Generic schema format that can be customized
198
+ return {
199
+ "model": self.model_name,
200
+ "format": self.schema_format,
201
+ "version": "2.0.0",
202
+ "base_url": base_url,
203
+ "actions": [
204
+ {
205
+ "name": name,
206
+ "description": data["description"],
207
+ "parameters": data["params"],
208
+ "endpoint": f"{base_url}/proxy/{name}"
209
+ }
210
+ for name, data in actions.items()
211
+ ]
212
+ }
kalibr/types.py ADDED
@@ -0,0 +1,106 @@
1
+ """
2
+ Enhanced data types for Kalibr app-level framework
3
+ """
4
+ from pydantic import BaseModel, Field
5
+ from typing import Optional, Dict, Any, List, Union, AsyncGenerator
6
+ from datetime import datetime
7
+ import uuid
8
+ import io
9
+
10
+ class FileUpload(BaseModel):
11
+ """Enhanced file upload handling for AI model integrations"""
12
+ filename: str
13
+ content_type: str
14
+ size: int
15
+ content: bytes
16
+ upload_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
17
+ uploaded_at: datetime = Field(default_factory=datetime.now)
18
+
19
+ class Config:
20
+ arbitrary_types_allowed = True
21
+
22
+ class ImageData(BaseModel):
23
+ """Image data type for AI vision capabilities"""
24
+ filename: str
25
+ content_type: str
26
+ width: Optional[int] = None
27
+ height: Optional[int] = None
28
+ format: str # jpeg, png, webp, etc.
29
+ content: bytes
30
+ image_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
31
+
32
+ class Config:
33
+ arbitrary_types_allowed = True
34
+
35
+ class TableData(BaseModel):
36
+ """Structured table data for AI analysis"""
37
+ headers: List[str]
38
+ rows: List[List[Any]]
39
+ table_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
40
+ metadata: Optional[Dict[str, Any]] = None
41
+
42
+ class StreamingResponse(BaseModel):
43
+ """Base class for streaming responses"""
44
+ chunk_id: str
45
+ content: Any
46
+ is_final: bool = False
47
+ timestamp: datetime = Field(default_factory=datetime.now)
48
+
49
+ class Session(BaseModel):
50
+ """Session management for stateful interactions"""
51
+ session_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
52
+ user_id: Optional[str] = None
53
+ created_at: datetime = Field(default_factory=datetime.now)
54
+ last_accessed: datetime = Field(default_factory=datetime.now)
55
+ data: Dict[str, Any] = Field(default_factory=dict)
56
+ expires_at: Optional[datetime] = None
57
+
58
+ def get(self, key: str, default=None):
59
+ """Get session data"""
60
+ return self.data.get(key, default)
61
+
62
+ def set(self, key: str, value: Any):
63
+ """Set session data"""
64
+ self.data[key] = value
65
+ self.last_accessed = datetime.now()
66
+
67
+ def delete(self, key: str):
68
+ """Delete session data"""
69
+ if key in self.data:
70
+ del self.data[key]
71
+
72
+ class AuthenticatedUser(BaseModel):
73
+ """Authenticated user context"""
74
+ user_id: str
75
+ username: str
76
+ email: Optional[str] = None
77
+ roles: List[str] = Field(default_factory=list)
78
+ permissions: List[str] = Field(default_factory=list)
79
+ auth_method: str # "jwt", "oauth", "api_key", etc.
80
+
81
+ class FileDownload(BaseModel):
82
+ """File download response"""
83
+ filename: str
84
+ content_type: str
85
+ content: bytes
86
+
87
+ class Config:
88
+ arbitrary_types_allowed = True
89
+
90
+ class AnalysisResult(BaseModel):
91
+ """Generic analysis result structure"""
92
+ result_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
93
+ status: str # "success", "error", "pending"
94
+ data: Dict[str, Any] = Field(default_factory=dict)
95
+ created_at: datetime = Field(default_factory=datetime.now)
96
+ processing_time: Optional[float] = None
97
+ metadata: Optional[Dict[str, Any]] = None
98
+
99
+ class WorkflowState(BaseModel):
100
+ """Workflow state management"""
101
+ workflow_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
102
+ step: str
103
+ status: str
104
+ data: Dict[str, Any] = Field(default_factory=dict)
105
+ created_at: datetime = Field(default_factory=datetime.now)
106
+ updated_at: datetime = Field(default_factory=datetime.now)