kalibr 1.0.25__py3-none-any.whl → 1.1.2a0__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.
Files changed (61) hide show
  1. kalibr/__init__.py +170 -3
  2. kalibr/__main__.py +3 -203
  3. kalibr/capsule_middleware.py +108 -0
  4. kalibr/cli/__init__.py +5 -0
  5. kalibr/cli/capsule_cmd.py +174 -0
  6. kalibr/cli/deploy_cmd.py +114 -0
  7. kalibr/cli/main.py +67 -0
  8. kalibr/cli/run.py +203 -0
  9. kalibr/cli/serve.py +59 -0
  10. kalibr/client.py +293 -0
  11. kalibr/collector.py +173 -0
  12. kalibr/context.py +132 -0
  13. kalibr/cost_adapter.py +222 -0
  14. kalibr/decorators.py +140 -0
  15. kalibr/instrumentation/__init__.py +13 -0
  16. kalibr/instrumentation/anthropic_instr.py +282 -0
  17. kalibr/instrumentation/base.py +108 -0
  18. kalibr/instrumentation/google_instr.py +281 -0
  19. kalibr/instrumentation/openai_instr.py +265 -0
  20. kalibr/instrumentation/registry.py +153 -0
  21. kalibr/kalibr.py +144 -230
  22. kalibr/kalibr_app.py +53 -314
  23. kalibr/middleware/__init__.py +5 -0
  24. kalibr/middleware/auto_tracer.py +356 -0
  25. kalibr/models.py +41 -0
  26. kalibr/redaction.py +44 -0
  27. kalibr/schemas.py +116 -0
  28. kalibr/simple_tracer.py +258 -0
  29. kalibr/tokens.py +52 -0
  30. kalibr/trace_capsule.py +296 -0
  31. kalibr/trace_models.py +201 -0
  32. kalibr/tracer.py +354 -0
  33. kalibr/types.py +25 -93
  34. kalibr/utils.py +198 -0
  35. kalibr-1.1.2a0.dist-info/METADATA +236 -0
  36. kalibr-1.1.2a0.dist-info/RECORD +48 -0
  37. kalibr-1.1.2a0.dist-info/entry_points.txt +2 -0
  38. kalibr-1.1.2a0.dist-info/licenses/LICENSE +21 -0
  39. kalibr-1.1.2a0.dist-info/top_level.txt +4 -0
  40. kalibr_crewai/__init__.py +65 -0
  41. kalibr_crewai/callbacks.py +539 -0
  42. kalibr_crewai/instrumentor.py +513 -0
  43. kalibr_langchain/__init__.py +47 -0
  44. kalibr_langchain/async_callback.py +850 -0
  45. kalibr_langchain/callback.py +1064 -0
  46. kalibr_openai_agents/__init__.py +43 -0
  47. kalibr_openai_agents/processor.py +554 -0
  48. kalibr/deployment.py +0 -41
  49. kalibr/packager.py +0 -43
  50. kalibr/runtime_router.py +0 -138
  51. kalibr/schema_generators.py +0 -159
  52. kalibr/validator.py +0 -70
  53. kalibr-1.0.25.data/data/examples/README.md +0 -173
  54. kalibr-1.0.25.data/data/examples/basic_kalibr_example.py +0 -66
  55. kalibr-1.0.25.data/data/examples/enhanced_kalibr_example.py +0 -347
  56. kalibr-1.0.25.dist-info/METADATA +0 -231
  57. kalibr-1.0.25.dist-info/RECORD +0 -19
  58. kalibr-1.0.25.dist-info/entry_points.txt +0 -2
  59. kalibr-1.0.25.dist-info/licenses/LICENSE +0 -11
  60. kalibr-1.0.25.dist-info/top_level.txt +0 -1
  61. {kalibr-1.0.25.dist-info → kalibr-1.1.2a0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,153 @@
1
+ """
2
+ Instrumentation Registry
3
+
4
+ Handles auto-discovery and registration of LLM SDK instrumentations.
5
+ Provides a central place to manage which SDKs are instrumented.
6
+ """
7
+
8
+ import os
9
+ from typing import Dict, List, Set
10
+
11
+ # Track which providers have been instrumented
12
+ _instrumented_providers: Set[str] = set()
13
+
14
+
15
+ def auto_instrument(providers: List[str] = None) -> Dict[str, bool]:
16
+ """
17
+ Auto-discover and instrument LLM SDKs
18
+
19
+ Args:
20
+ providers: List of provider names to instrument.
21
+ If None, attempts to instrument all supported providers.
22
+ Supported: ["openai", "anthropic", "google"]
23
+
24
+ Returns:
25
+ Dictionary mapping provider names to instrumentation success status
26
+ """
27
+ global _instrumented_providers
28
+
29
+ # Default to all providers if none specified
30
+ if providers is None:
31
+ providers = ["openai", "anthropic", "google"]
32
+
33
+ results = {}
34
+
35
+ for provider in providers:
36
+ provider_lower = provider.lower()
37
+
38
+ # Skip if already instrumented
39
+ if provider_lower in _instrumented_providers:
40
+ results[provider_lower] = True
41
+ continue
42
+
43
+ try:
44
+ if provider_lower == "openai":
45
+ from . import openai_instr
46
+
47
+ success = openai_instr.instrument()
48
+ results[provider_lower] = success
49
+ if success:
50
+ _instrumented_providers.add(provider_lower)
51
+ print(f"✅ Instrumented OpenAI SDK")
52
+
53
+ elif provider_lower == "anthropic":
54
+ from . import anthropic_instr
55
+
56
+ success = anthropic_instr.instrument()
57
+ results[provider_lower] = success
58
+ if success:
59
+ _instrumented_providers.add(provider_lower)
60
+ print(f"✅ Instrumented Anthropic SDK")
61
+
62
+ elif provider_lower == "google":
63
+ from . import google_instr
64
+
65
+ success = google_instr.instrument()
66
+ results[provider_lower] = success
67
+ if success:
68
+ _instrumented_providers.add(provider_lower)
69
+ print(f"✅ Instrumented Google Generative AI SDK")
70
+
71
+ else:
72
+ print(f"⚠️ Unknown provider: {provider}")
73
+ results[provider_lower] = False
74
+
75
+ except ImportError as e:
76
+ print(f"⚠️ {provider} SDK not installed, skipping instrumentation")
77
+ results[provider_lower] = False
78
+ except Exception as e:
79
+ print(f"❌ Failed to instrument {provider}: {e}")
80
+ results[provider_lower] = False
81
+
82
+ return results
83
+
84
+
85
+ def uninstrument_all() -> Dict[str, bool]:
86
+ """
87
+ Remove instrumentation from all previously instrumented SDKs
88
+
89
+ Returns:
90
+ Dictionary mapping provider names to uninstrumentation success status
91
+ """
92
+ global _instrumented_providers
93
+
94
+ results = {}
95
+ providers_to_uninstrument = list(_instrumented_providers)
96
+
97
+ for provider in providers_to_uninstrument:
98
+ try:
99
+ if provider == "openai":
100
+ from . import openai_instr
101
+
102
+ success = openai_instr.uninstrument()
103
+ results[provider] = success
104
+ if success:
105
+ _instrumented_providers.discard(provider)
106
+ print(f"✅ Uninstrumented OpenAI SDK")
107
+
108
+ elif provider == "anthropic":
109
+ from . import anthropic_instr
110
+
111
+ success = anthropic_instr.uninstrument()
112
+ results[provider] = success
113
+ if success:
114
+ _instrumented_providers.discard(provider)
115
+ print(f"✅ Uninstrumented Anthropic SDK")
116
+
117
+ elif provider == "google":
118
+ from . import google_instr
119
+
120
+ success = google_instr.uninstrument()
121
+ results[provider] = success
122
+ if success:
123
+ _instrumented_providers.discard(provider)
124
+ print(f"✅ Uninstrumented Google Generative AI SDK")
125
+
126
+ except Exception as e:
127
+ print(f"❌ Failed to uninstrument {provider}: {e}")
128
+ results[provider] = False
129
+
130
+ return results
131
+
132
+
133
+ def get_instrumented_providers() -> List[str]:
134
+ """
135
+ Get list of currently instrumented providers
136
+
137
+ Returns:
138
+ List of provider names that are currently instrumented
139
+ """
140
+ return list(_instrumented_providers)
141
+
142
+
143
+ def is_instrumented(provider: str) -> bool:
144
+ """
145
+ Check if a specific provider is instrumented
146
+
147
+ Args:
148
+ provider: Provider name to check
149
+
150
+ Returns:
151
+ True if provider is instrumented, False otherwise
152
+ """
153
+ return provider.lower() in _instrumented_providers
kalibr/kalibr.py CHANGED
@@ -1,259 +1,173 @@
1
- # kalibr/kalibr.py
1
+ """Kalibr - Simple function-level API builder"""
2
2
 
3
+ import inspect
4
+ import os
5
+ from typing import Any, Callable, Dict, List, Optional
6
+
7
+ import uvicorn
3
8
  from fastapi import FastAPI, Request
4
9
  from fastapi.responses import JSONResponse
5
- from typing import Callable, Dict, Any, get_type_hints
6
- import inspect
10
+ from kalibr.middleware.auto_tracer import AutoTracerMiddleware
11
+ from kalibr.schemas import (
12
+ generate_copilot_schema,
13
+ generate_gemini_schema,
14
+ generate_mcp_schema,
15
+ get_base_url,
16
+ get_supported_models,
17
+ )
18
+ from pydantic import BaseModel, create_model
19
+
7
20
 
8
21
  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
- """
22
+ """Simple function-level API builder for multi-model integration"""
23
+
24
+ def __init__(
25
+ self,
26
+ title: str = "Kalibr API",
27
+ version: str = "1.0.0",
28
+ auto_trace: bool = True,
29
+ agent_name: str = None,
30
+ ):
25
31
  self.app = FastAPI(title=title, version=version)
26
- self.base_url = base_url
27
- self.actions = {} # Stores registered actions and their metadata
32
+ self.actions: List[Dict[str, Any]] = []
33
+ self.base_url = get_base_url()
34
+ self.agent_name = agent_name or title
35
+
36
+ # Phase 3B: Auto-attach tracing middleware
37
+ if auto_trace and os.getenv("KALIBR_TRACE_ENABLED", "true").lower() == "true":
38
+ self.app.add_middleware(
39
+ AutoTracerMiddleware,
40
+ agent_name=self.agent_name,
41
+ )
42
+
28
43
  self._setup_routes()
29
44
 
30
45
  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
- """
46
+ """Decorator to register a function as an action"""
47
+
47
48
  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
49
+ # Extract function signature
50
+ sig = inspect.signature(func)
51
+ schema = self._generate_schema(sig)
52
+
53
+ # Store action metadata
54
+ self.actions.append(
55
+ {
56
+ "name": name,
57
+ "description": description or func.__doc__ or "",
58
+ "function": func,
59
+ "schema": schema,
60
+ }
61
+ )
62
+
63
+ # Register proxy endpoint
64
+ self._register_proxy_endpoint(name, func, schema)
65
+
66
+ return func
67
+
89
68
  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
-
69
+
70
+ def _generate_schema(self, sig: inspect.Signature) -> Dict[str, Any]:
71
+ """Generate JSON schema from function signature"""
72
+ properties = {}
73
+ required = []
74
+
112
75
  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:
76
+ param_type = "string" # Default type
77
+
78
+ # Map Python types to JSON schema types
79
+ if param.annotation != inspect.Parameter.empty:
80
+ if param.annotation == int:
81
+ param_type = "integer"
82
+ elif param.annotation == float:
83
+ param_type = "number"
84
+ elif param.annotation == bool:
85
+ param_type = "boolean"
86
+ elif param.annotation == list:
136
87
  param_type = "array"
137
- else:
88
+ elif param.annotation == dict:
138
89
  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
-
90
+
91
+ properties[param_name] = {"type": param_type}
92
+
93
+ # Check if parameter is required (no default value)
94
+ if param.default == inspect.Parameter.empty:
95
+ required.append(param_name)
96
+
97
+ return {"type": "object", "properties": properties, "required": required}
98
+
99
+ def _register_proxy_endpoint(self, name: str, func: Callable, schema: Dict[str, Any]):
100
+ """Register a proxy endpoint for the action"""
101
+ # Create Pydantic model for request validation
102
+ fields = {}
103
+ for prop_name, prop_schema in schema["properties"].items():
104
+ field_type = str # Default
105
+ if prop_schema["type"] == "integer":
106
+ field_type = int
107
+ elif prop_schema["type"] == "number":
108
+ field_type = float
109
+ elif prop_schema["type"] == "boolean":
110
+ field_type = bool
111
+
112
+ # Check if required
113
+ if prop_name in schema.get("required", []):
114
+ fields[prop_name] = (field_type, ...)
115
+ else:
116
+ fields[prop_name] = (Optional[field_type], None)
117
+
118
+ RequestModel = create_model(f"{name}_Request", **fields)
119
+
120
+ @self.app.post(f"/proxy/{name}")
121
+ async def proxy_endpoint(request: RequestModel):
122
+ try:
123
+ result = func(**request.dict(exclude_none=True))
124
+ return result
125
+ except Exception as e:
126
+ return JSONResponse(status_code=500, content={"error": str(e)})
127
+
150
128
  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
- - 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)
161
- """
162
- from kalibr.schema_generators import (
163
- OpenAPISchemaGenerator,
164
- MCPSchemaGenerator,
165
- GeminiSchemaGenerator,
166
- CopilotSchemaGenerator
167
- )
168
-
129
+ """Setup built-in routes"""
130
+
169
131
  @self.app.get("/")
170
- def root():
171
- """
172
- Root endpoint providing API status and a list of available actions.
173
- """
132
+ async def root():
174
133
  return {
175
- "message": "Kalibr API is running",
176
- "actions": list(self.actions.keys()),
134
+ "message": "Kalibr API is running",
135
+ "actions": [action["name"] for action in self.actions],
177
136
  "schemas": {
178
137
  "gpt_actions": f"{self.base_url}/gpt-actions.json",
179
138
  "openapi_swagger": f"{self.base_url}/openapi.json",
180
139
  "claude_mcp": f"{self.base_url}/mcp.json",
181
140
  "gemini": f"{self.base_url}/schemas/gemini",
182
- "copilot": f"{self.base_url}/schemas/copilot"
183
- }
141
+ "copilot": f"{self.base_url}/schemas/copilot",
142
+ },
184
143
  }
185
-
186
- # Initialize schema generators
187
- openapi_gen = OpenAPISchemaGenerator()
188
- mcp_gen = MCPSchemaGenerator()
189
- gemini_gen = GeminiSchemaGenerator()
190
- copilot_gen = CopilotSchemaGenerator()
191
-
144
+
192
145
  @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)
199
-
146
+ async def gpt_actions_schema():
147
+ """Generates OpenAPI 3.0 schema for GPT Actions integration.
148
+ (Alternative endpoint since /openapi.json is used by FastAPI)"""
149
+ return self.app.openapi()
150
+
200
151
  @self.app.get("/mcp.json")
201
- def mcp_manifest():
202
- """
203
- Generates Claude MCP manifest for AI model integration.
204
- """
205
- return mcp_gen.generate_schema(self.actions, self.base_url)
206
-
152
+ async def mcp_schema():
153
+ return generate_mcp_schema(self.actions, self.base_url)
154
+
207
155
  @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
-
156
+ async def gemini_schema():
157
+ return generate_gemini_schema(self.actions, self.base_url)
158
+
214
159
  @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)
220
-
221
- # Override FastAPI's default OpenAPI generation to include servers configuration
222
- def custom_openapi():
223
- """
224
- Customizes the OpenAPI schema generation for Swagger UI.
225
- """
226
- if self.app.openapi_schema:
227
- return self.app.openapi_schema
228
-
229
- from fastapi.openapi.utils import get_openapi
230
- # Generate the default OpenAPI schema
231
- openapi_schema = get_openapi(
232
- title=self.app.title,
233
- version=self.app.version,
234
- routes=self.app.routes,
235
- )
236
-
237
- # Add the 'servers' block to the schema
238
- openapi_schema["servers"] = [{"url": self.base_url}]
239
-
240
- self.app.openapi_schema = openapi_schema
241
- return self.app.openapi_schema
242
-
243
- # Assign the custom OpenAPI generator to the FastAPI app
244
- self.app.openapi = custom_openapi
245
-
246
- def get_app(self):
247
- """
248
- Returns the FastAPI application instance.
249
-
250
- This allows the Kalibr API to be run using standard ASGI servers like Uvicorn.
251
-
252
- Returns:
253
- FastAPI: The configured FastAPI application.
254
- """
160
+ async def copilot_schema():
161
+ return generate_copilot_schema(self.actions, self.base_url)
162
+
163
+ @self.app.get("/models/supported")
164
+ async def models_supported():
165
+ return get_supported_models()
166
+
167
+ def get_app(self) -> FastAPI:
168
+ """Get the FastAPI app instance"""
255
169
  return self.app
256
170
 
257
- if __name__ == '__main__':
258
- print("Kalibr SDK loaded. Use this class to build your API.")
259
- print("See the __main__ block for example usage.")
171
+ def run(self, host: str = "0.0.0.0", port: int = 8000):
172
+ """Run the Kalibr server"""
173
+ uvicorn.run(self.app, host=host, port=port)