kalibr 1.0.22__tar.gz → 1.0.23__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.22/kalibr.egg-info → kalibr-1.0.23}/PKG-INFO +1 -1
- kalibr-1.0.23/kalibr/__main__.py +206 -0
- kalibr-1.0.23/kalibr/deployment.py +41 -0
- {kalibr-1.0.22 → kalibr-1.0.23}/kalibr/kalibr_app.py +32 -11
- kalibr-1.0.23/kalibr/packager.py +43 -0
- kalibr-1.0.23/kalibr/runtime_router.py +138 -0
- {kalibr-1.0.22 → kalibr-1.0.23}/kalibr/schema_generators.py +21 -74
- kalibr-1.0.23/kalibr/validator.py +70 -0
- {kalibr-1.0.22 → kalibr-1.0.23/kalibr.egg-info}/PKG-INFO +1 -1
- {kalibr-1.0.22 → kalibr-1.0.23}/kalibr.egg-info/SOURCES.txt +3 -0
- {kalibr-1.0.22 → kalibr-1.0.23}/pyproject.toml +1 -1
- {kalibr-1.0.22 → kalibr-1.0.23}/setup.py +1 -1
- kalibr-1.0.22/kalibr/__main__.py +0 -677
- kalibr-1.0.22/kalibr/deployment.py +0 -26
- {kalibr-1.0.22 → kalibr-1.0.23}/LICENSE +0 -0
- {kalibr-1.0.22 → kalibr-1.0.23}/MANIFEST.in +0 -0
- {kalibr-1.0.22 → kalibr-1.0.23}/README.md +0 -0
- {kalibr-1.0.22 → kalibr-1.0.23}/examples/README.md +0 -0
- {kalibr-1.0.22 → kalibr-1.0.23}/examples/__init__.py +0 -0
- {kalibr-1.0.22 → kalibr-1.0.23}/examples/basic_kalibr_example.py +0 -0
- {kalibr-1.0.22 → kalibr-1.0.23}/examples/enhanced_kalibr_example.py +0 -0
- {kalibr-1.0.22 → kalibr-1.0.23}/kalibr/__init__.py +0 -0
- {kalibr-1.0.22 → kalibr-1.0.23}/kalibr/kalibr.py +0 -0
- {kalibr-1.0.22 → kalibr-1.0.23}/kalibr/types.py +0 -0
- {kalibr-1.0.22 → kalibr-1.0.23}/kalibr.egg-info/dependency_links.txt +0 -0
- {kalibr-1.0.22 → kalibr-1.0.23}/kalibr.egg-info/entry_points.txt +0 -0
- {kalibr-1.0.22 → kalibr-1.0.23}/kalibr.egg-info/requires.txt +0 -0
- {kalibr-1.0.22 → kalibr-1.0.23}/kalibr.egg-info/top_level.txt +0 -0
- {kalibr-1.0.22 → kalibr-1.0.23}/setup.cfg +0 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import uvicorn
|
|
3
|
+
import sys
|
|
4
|
+
import importlib.util
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import os
|
|
7
|
+
import requests
|
|
8
|
+
import json
|
|
9
|
+
|
|
10
|
+
app = typer.Typer()
|
|
11
|
+
|
|
12
|
+
def _load_user_module(file: str):
|
|
13
|
+
file_path = Path(file).resolve()
|
|
14
|
+
if not file_path.exists():
|
|
15
|
+
print(f"❌ Error: {file} not found")
|
|
16
|
+
raise typer.Exit(1)
|
|
17
|
+
spec = importlib.util.spec_from_file_location("user_app", file_path)
|
|
18
|
+
if not spec or not spec.loader:
|
|
19
|
+
print(f"❌ Error: Could not load {file}")
|
|
20
|
+
raise typer.Exit(1)
|
|
21
|
+
module = importlib.util.module_from_spec(spec)
|
|
22
|
+
sys.modules["user_app"] = module
|
|
23
|
+
try:
|
|
24
|
+
spec.loader.exec_module(module)
|
|
25
|
+
except Exception as e:
|
|
26
|
+
print(f"❌ Error loading {file}: {e}")
|
|
27
|
+
raise typer.Exit(1)
|
|
28
|
+
return module
|
|
29
|
+
|
|
30
|
+
@app.command()
|
|
31
|
+
def serve(
|
|
32
|
+
file: str = typer.Argument("kalibr_app.py", help="Python file with Kalibr app"),
|
|
33
|
+
host: str = typer.Option("0.0.0.0", "--host", "-h"),
|
|
34
|
+
port: int = typer.Option(8000, "--port", "-p"),
|
|
35
|
+
base_url: str = typer.Option("http://localhost:8000", "--base-url", "-b"),
|
|
36
|
+
):
|
|
37
|
+
"""Serve a Kalibr-powered API locally."""
|
|
38
|
+
module = _load_user_module(file)
|
|
39
|
+
|
|
40
|
+
# Import Kalibr classes
|
|
41
|
+
from kalibr import Kalibr, KalibrApp
|
|
42
|
+
kalibr_instance = None
|
|
43
|
+
for attr_name in dir(module):
|
|
44
|
+
attr = getattr(module, attr_name)
|
|
45
|
+
if isinstance(attr, Kalibr) or (KalibrApp and isinstance(attr, KalibrApp)):
|
|
46
|
+
kalibr_instance = attr
|
|
47
|
+
kalibr_instance.base_url = base_url
|
|
48
|
+
break
|
|
49
|
+
if not kalibr_instance:
|
|
50
|
+
print(f"❌ Error: No Kalibr/KalibrApp instance found in {file}")
|
|
51
|
+
raise typer.Exit(1)
|
|
52
|
+
|
|
53
|
+
if hasattr(kalibr_instance, 'get_app'):
|
|
54
|
+
fastapi_app = kalibr_instance.get_app()
|
|
55
|
+
elif hasattr(kalibr_instance, 'app'):
|
|
56
|
+
fastapi_app = kalibr_instance.app
|
|
57
|
+
else:
|
|
58
|
+
print(f"❌ Error: Kalibr instance has no get_app() method or app attribute")
|
|
59
|
+
raise typer.Exit(1)
|
|
60
|
+
|
|
61
|
+
is_enhanced = 'KalibrApp' in str(type(kalibr_instance))
|
|
62
|
+
print(f"🚀 Starting {'Enhanced ' if is_enhanced else ''}Kalibr server from {file}")
|
|
63
|
+
print(f"📍 GPT (OpenAPI): {base_url}/openapi.json")
|
|
64
|
+
print(f"📍 Claude (MCP): {base_url}/mcp.json")
|
|
65
|
+
if is_enhanced:
|
|
66
|
+
print(f"📍 Gemini: {base_url}/schemas/gemini")
|
|
67
|
+
print(f"📍 Copilot: {base_url}/schemas/copilot")
|
|
68
|
+
print(f"📍 Supported Models: {base_url}/models/supported")
|
|
69
|
+
print(f"📍 Health Check: {base_url}/health")
|
|
70
|
+
print(f"📍 Swagger UI: {base_url}/docs")
|
|
71
|
+
print(f"🔌 Actions registered: {list(kalibr_instance.actions.keys())}")
|
|
72
|
+
|
|
73
|
+
uvicorn.run(fastapi_app, host=host, port=port)
|
|
74
|
+
|
|
75
|
+
@app.command()
|
|
76
|
+
def package(
|
|
77
|
+
app_dir: str = typer.Option(".", "--app-dir", "-d", help="Directory containing your app"),
|
|
78
|
+
output: str = typer.Option("kalibr_bundle.zip", "--output", "-o", help="Bundle file"),
|
|
79
|
+
models: str = typer.Option("mcp,gpt-actions,gemini,copilot", "--models", "-m", help="Comma-separated models supported")
|
|
80
|
+
):
|
|
81
|
+
"""Create a deployable MCP bundle (code + schemas + metadata)."""
|
|
82
|
+
from kalibr.packager import package_app
|
|
83
|
+
try:
|
|
84
|
+
models_supported = [x.strip() for x in models.split(",") if x.strip()]
|
|
85
|
+
# Version best-effort
|
|
86
|
+
try:
|
|
87
|
+
from kalibr import __version__ as kalibr_version
|
|
88
|
+
except Exception:
|
|
89
|
+
kalibr_version = "unknown"
|
|
90
|
+
bundle_path = package_app(app_dir=app_dir, output=output, models_supported=models_supported, kalibr_version=kalibr_version)
|
|
91
|
+
print(f"📦 Bundle created: {bundle_path}")
|
|
92
|
+
except Exception as e:
|
|
93
|
+
print(f"❌ Packaging error: {e}")
|
|
94
|
+
raise typer.Exit(1)
|
|
95
|
+
|
|
96
|
+
@app.command()
|
|
97
|
+
def deploy(
|
|
98
|
+
file: str = typer.Argument(..., help="Python file to serve/deploy (e.g., kalibr_app.py)"),
|
|
99
|
+
name: str = typer.Option("", "--name", "-n", help="App name (defaults to filename)"),
|
|
100
|
+
runtime: str = typer.Option("local", "--runtime", "-r", help="Runtime: local|fly|render"),
|
|
101
|
+
host: str = typer.Option("0.0.0.0", "--host"),
|
|
102
|
+
port: int = typer.Option(8000, "--port"),
|
|
103
|
+
base_url: str = typer.Option("http://localhost:8000", "--base-url"),
|
|
104
|
+
):
|
|
105
|
+
"""Deploy via runtime router (no hosting burden on Kalibr)."""
|
|
106
|
+
from kalibr.runtime_router import deploy as router_deploy
|
|
107
|
+
file_path = Path(file)
|
|
108
|
+
if not file_path.exists():
|
|
109
|
+
print(f"❌ Error: {file} not found")
|
|
110
|
+
raise typer.Exit(1)
|
|
111
|
+
if not name:
|
|
112
|
+
name = file_path.stem.replace('_', '-').replace('.', '-')
|
|
113
|
+
try:
|
|
114
|
+
result = router_deploy(runtime=runtime, app_name=name, app_file=str(file_path), host=host, port=port, base_url=base_url)
|
|
115
|
+
if result.get("status") in ("success", "started"):
|
|
116
|
+
print("🎉 Deploy OK")
|
|
117
|
+
eps = result.get("endpoints", {})
|
|
118
|
+
if eps:
|
|
119
|
+
print("📍 Endpoints:")
|
|
120
|
+
for k, v in eps.items():
|
|
121
|
+
print(f" - {k}: {v}")
|
|
122
|
+
else:
|
|
123
|
+
print("⚠️ Deploy finished with unknown status:", result)
|
|
124
|
+
except Exception as e:
|
|
125
|
+
print(f"❌ Deployment error: {e}")
|
|
126
|
+
raise typer.Exit(1)
|
|
127
|
+
|
|
128
|
+
@app.command()
|
|
129
|
+
def validate(
|
|
130
|
+
url: str = typer.Option("http://localhost:8000/mcp.json", "--mcp-url", help="URL to MCP manifest"),
|
|
131
|
+
):
|
|
132
|
+
"""Validate MCP manifest against minimal JSON schema & version hint."""
|
|
133
|
+
from kalibr.validator import validate_mcp_manifest
|
|
134
|
+
try:
|
|
135
|
+
resp = requests.get(url, timeout=10)
|
|
136
|
+
resp.raise_for_status()
|
|
137
|
+
manifest = resp.json()
|
|
138
|
+
validate_mcp_manifest(manifest)
|
|
139
|
+
print("✅ MCP manifest looks structurally valid.")
|
|
140
|
+
except Exception as e:
|
|
141
|
+
print(f"❌ Validation failed: {e}")
|
|
142
|
+
raise typer.Exit(1)
|
|
143
|
+
|
|
144
|
+
@app.command()
|
|
145
|
+
def update_schemas():
|
|
146
|
+
"""Stub: instruct users to upgrade SDK and regenerate manifests."""
|
|
147
|
+
from kalibr.validator import update_schemas as _upd
|
|
148
|
+
_upd()
|
|
149
|
+
|
|
150
|
+
@app.command()
|
|
151
|
+
def status(
|
|
152
|
+
app_url: str = typer.Argument(..., help="URL of deployed Kalibr app"),
|
|
153
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed information")
|
|
154
|
+
):
|
|
155
|
+
"""Check status of a deployed Kalibr app."""
|
|
156
|
+
try:
|
|
157
|
+
health_url = f"{app_url.rstrip('/')}/health"
|
|
158
|
+
response = requests.get(health_url, timeout=10)
|
|
159
|
+
if response.status_code == 200:
|
|
160
|
+
health_data = response.json()
|
|
161
|
+
print(f"✅ App is healthy at {app_url}")
|
|
162
|
+
print(f" Version: {health_data.get('version', 'unknown')}")
|
|
163
|
+
print(f" Features: {health_data.get('features', {})}")
|
|
164
|
+
if verbose:
|
|
165
|
+
schemas = ["mcp.json", "openapi.json", "schemas/gemini", "schemas/copilot"]
|
|
166
|
+
print(f"\n📊 Available AI model schemas:")
|
|
167
|
+
for schema in schemas:
|
|
168
|
+
schema_url = f"{app_url.rstrip('/')}/{schema}"
|
|
169
|
+
try:
|
|
170
|
+
schema_response = requests.get(schema_url, timeout=5)
|
|
171
|
+
status_emoji = "✅" if schema_response.status_code == 200 else "❌"
|
|
172
|
+
model_name = schema.replace(".json", "").replace("schemas/", "")
|
|
173
|
+
print(f" {status_emoji} {model_name.upper()}: {schema_url}")
|
|
174
|
+
except:
|
|
175
|
+
print(f" ❌ {schema}: Connection failed")
|
|
176
|
+
else:
|
|
177
|
+
print(f"❌ App at {app_url} is not responding (HTTP {response.status_code})")
|
|
178
|
+
raise typer.Exit(1)
|
|
179
|
+
except requests.exceptions.RequestException as e:
|
|
180
|
+
print(f"❌ Cannot connect to {app_url}: {e}")
|
|
181
|
+
raise typer.Exit(1)
|
|
182
|
+
|
|
183
|
+
@app.command()
|
|
184
|
+
def version():
|
|
185
|
+
try:
|
|
186
|
+
from kalibr import __version__
|
|
187
|
+
except (ImportError, AttributeError):
|
|
188
|
+
__version__ = "unknown"
|
|
189
|
+
print(f"Kalibr SDK version: {__version__}")
|
|
190
|
+
print("Enhanced multi-model AI integration framework")
|
|
191
|
+
print("Supports: GPT Actions, Claude MCP, Gemini Extensions, Copilot Plugins")
|
|
192
|
+
print("GitHub: https://github.com/devonakelley/kalibr-sdk")
|
|
193
|
+
|
|
194
|
+
def main():
|
|
195
|
+
config_file = Path.home() / ".kalibr" / "config.json"
|
|
196
|
+
if config_file.exists():
|
|
197
|
+
try:
|
|
198
|
+
config = json.loads(config_file.read_text())
|
|
199
|
+
if "api_token" in config and not os.environ.get("KALIBR_TOKEN"):
|
|
200
|
+
os.environ["KALIBR_TOKEN"] = config["api_token"]
|
|
201
|
+
except:
|
|
202
|
+
pass
|
|
203
|
+
app()
|
|
204
|
+
|
|
205
|
+
if __name__ == "__main__":
|
|
206
|
+
main()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kalibr Deployment
|
|
3
|
+
-----------------
|
|
4
|
+
Thin wrapper that forwards to the runtime router.
|
|
5
|
+
Keeps a simple API surface for backwards-compat commands.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Dict, Any
|
|
11
|
+
from kalibr.runtime_router import deploy as router_deploy
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class DeploymentConfig:
|
|
15
|
+
app_name: str
|
|
16
|
+
memory_mb: int = 512
|
|
17
|
+
timeout_seconds: int = 30
|
|
18
|
+
environment_vars: Dict[str, str] = field(default_factory=dict)
|
|
19
|
+
|
|
20
|
+
def deploy_app(file_path: str, config: DeploymentConfig, platform: str = "local") -> Dict[str, Any]:
|
|
21
|
+
# Map older "platform" to runtime names used by router
|
|
22
|
+
runtime = {
|
|
23
|
+
"local": "local",
|
|
24
|
+
"fly": "fly",
|
|
25
|
+
"aws-lambda": "local", # not supported; punt to local
|
|
26
|
+
"render": "render",
|
|
27
|
+
}.get(platform, platform)
|
|
28
|
+
|
|
29
|
+
result = router_deploy(runtime=runtime, app_name=config.app_name, app_file=file_path)
|
|
30
|
+
if result.get("status") in ("success", "started"):
|
|
31
|
+
eps = result.get("endpoints", {})
|
|
32
|
+
return {
|
|
33
|
+
"status": "success",
|
|
34
|
+
"endpoints": {
|
|
35
|
+
"root": eps.get("root", ""),
|
|
36
|
+
"mcp": eps.get("mcp", ""),
|
|
37
|
+
"openapi": eps.get("openapi", ""),
|
|
38
|
+
"health": eps.get("health", ""),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return {"status": "error", "error": "unknown deploy outcome", "raw": result}
|
|
@@ -9,7 +9,6 @@ import os
|
|
|
9
9
|
|
|
10
10
|
from kalibr.types import FileUpload, Session, WorkflowState
|
|
11
11
|
|
|
12
|
-
|
|
13
12
|
class KalibrApp:
|
|
14
13
|
"""
|
|
15
14
|
Enhanced app-level Kalibr framework with advanced capabilities:
|
|
@@ -20,7 +19,7 @@ class KalibrApp:
|
|
|
20
19
|
- Multi-model schema generation
|
|
21
20
|
"""
|
|
22
21
|
|
|
23
|
-
def __init__(self, title="Kalibr Enhanced API", version="2.0.0", base_url: Optional[str] = None):
|
|
22
|
+
def __init__(self, title="Kalibr Enhanced API", version="2.0.0", base_url: Optional[str] = None, models: Optional[List[str]] = None):
|
|
24
23
|
"""
|
|
25
24
|
Initialize the Kalibr enhanced app.
|
|
26
25
|
Automatically determines correct base URL for deployed environments.
|
|
@@ -30,6 +29,9 @@ class KalibrApp:
|
|
|
30
29
|
2. Env var `KALIBR_BASE_URL`
|
|
31
30
|
3. Env var `FLY_APP_NAME` -> https://<fly_app_name>.fly.dev
|
|
32
31
|
4. Default localhost for dev
|
|
32
|
+
|
|
33
|
+
`models`: optional list like ["mcp"] or ["mcp","gpt-actions","gemini","copilot"]
|
|
34
|
+
controls which schema endpoints are advertised.
|
|
33
35
|
"""
|
|
34
36
|
self.app = FastAPI(title=title, version=version)
|
|
35
37
|
|
|
@@ -42,6 +44,8 @@ class KalibrApp:
|
|
|
42
44
|
else:
|
|
43
45
|
self.base_url = "http://localhost:8000"
|
|
44
46
|
|
|
47
|
+
self.models_supported = models or ["mcp", "gpt-actions", "gemini", "copilot"]
|
|
48
|
+
|
|
45
49
|
# Storage for different action types
|
|
46
50
|
self.actions: Dict[str, Any] = {}
|
|
47
51
|
self.file_handlers: Dict[str, Any] = {}
|
|
@@ -221,10 +225,10 @@ class KalibrApp:
|
|
|
221
225
|
import json
|
|
222
226
|
if inspect.isasyncgen(result):
|
|
223
227
|
async for item in result:
|
|
224
|
-
yield json.dumps(item) + "
|
|
228
|
+
yield json.dumps(item) + "\\n"
|
|
225
229
|
elif inspect.isgenerator(result):
|
|
226
230
|
for item in result:
|
|
227
|
-
yield json.dumps(item) + "
|
|
231
|
+
yield json.dumps(item) + "\\n"
|
|
228
232
|
|
|
229
233
|
return FastAPIStreamingResponse(generate(), media_type="application/x-ndjson")
|
|
230
234
|
except Exception as e:
|
|
@@ -255,17 +259,25 @@ class KalibrApp:
|
|
|
255
259
|
|
|
256
260
|
@self.app.get("/")
|
|
257
261
|
def root():
|
|
262
|
+
schemas = {}
|
|
263
|
+
if "gpt-actions" in self.models_supported:
|
|
264
|
+
schemas["gpt_actions"] = f"{self.base_url}/gpt-actions.json"
|
|
265
|
+
schemas["claude_mcp"] = f"{self.base_url}/mcp.json" # MCP is the default lingua franca
|
|
266
|
+
if "gemini" in self.models_supported:
|
|
267
|
+
schemas["gemini"] = f"{self.base_url}/schemas/gemini"
|
|
268
|
+
if "copilot" in self.models_supported:
|
|
269
|
+
schemas["copilot"] = f"{self.base_url}/schemas/copilot"
|
|
270
|
+
|
|
258
271
|
return {
|
|
259
272
|
"message": "Kalibr Enhanced API is running",
|
|
260
273
|
"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
|
-
},
|
|
274
|
+
"schemas": schemas,
|
|
267
275
|
}
|
|
268
276
|
|
|
277
|
+
@self.app.get("/models/supported")
|
|
278
|
+
def supported_models():
|
|
279
|
+
return {"models": self.models_supported}
|
|
280
|
+
|
|
269
281
|
@self.app.get("/gpt-actions.json")
|
|
270
282
|
def gpt_actions_schema():
|
|
271
283
|
all_actions = {**self.actions, **self.file_handlers, **self.session_actions}
|
|
@@ -288,7 +300,16 @@ class KalibrApp:
|
|
|
288
300
|
|
|
289
301
|
@self.app.get("/health")
|
|
290
302
|
def health_check():
|
|
291
|
-
return {
|
|
303
|
+
return {
|
|
304
|
+
"status": "healthy",
|
|
305
|
+
"service": "Kalibr Enhanced API",
|
|
306
|
+
"version": self.app.version,
|
|
307
|
+
"features": {
|
|
308
|
+
"file_handlers": bool(self.file_handlers),
|
|
309
|
+
"sessions": True,
|
|
310
|
+
"streams": bool(self.stream_actions),
|
|
311
|
+
},
|
|
312
|
+
}
|
|
292
313
|
|
|
293
314
|
# -------------------------------------------------------------------------
|
|
294
315
|
# Helpers
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Packager
|
|
3
|
+
--------
|
|
4
|
+
Create a deployable MCP bundle (code + manifests + metadata).
|
|
5
|
+
This does not host anything; it prepares artifacts for any runtime.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import shutil
|
|
11
|
+
import json
|
|
12
|
+
import tempfile
|
|
13
|
+
from typing import Dict, Any, Optional
|
|
14
|
+
|
|
15
|
+
DEFAULT_BUNDLE = "kalibr_bundle.zip"
|
|
16
|
+
|
|
17
|
+
def package_app(app_dir: str = ".", output: str = DEFAULT_BUNDLE, models_supported: Optional[list] = None, kalibr_version: str = "unknown") -> str:
|
|
18
|
+
app_dir = Path(app_dir).resolve()
|
|
19
|
+
out_path = Path(output).resolve()
|
|
20
|
+
|
|
21
|
+
# Assemble temp dir with metadata
|
|
22
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
23
|
+
tmpdir = Path(tmp)
|
|
24
|
+
# Copy source tree
|
|
25
|
+
for item in app_dir.iterdir():
|
|
26
|
+
if item.name == out_path.name:
|
|
27
|
+
continue
|
|
28
|
+
dest = tmpdir / item.name
|
|
29
|
+
if item.is_dir():
|
|
30
|
+
shutil.copytree(item, dest)
|
|
31
|
+
else:
|
|
32
|
+
shutil.copy2(item, dest)
|
|
33
|
+
|
|
34
|
+
# Write bundle metadata
|
|
35
|
+
(tmpdir / "kalibr_manifest.json").write_text(json.dumps({
|
|
36
|
+
"kalibr_version": kalibr_version,
|
|
37
|
+
"models_supported": models_supported or ["mcp", "gpt-actions", "gemini", "copilot"],
|
|
38
|
+
}, indent=2))
|
|
39
|
+
|
|
40
|
+
# Zip
|
|
41
|
+
shutil.make_archive(out_path.with_suffix(""), "zip", tmpdir)
|
|
42
|
+
|
|
43
|
+
return str(out_path)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Runtime Router
|
|
3
|
+
--------------
|
|
4
|
+
Abstraction over deployment targets without hosting them ourselves.
|
|
5
|
+
Generates minimal configs and invokes the target's CLI/API where possible.
|
|
6
|
+
|
|
7
|
+
Supported:
|
|
8
|
+
- local (uvicorn)
|
|
9
|
+
- fly (fly.io) -> generates fly.toml and basic Dockerfile
|
|
10
|
+
- render -> generates render.yaml
|
|
11
|
+
|
|
12
|
+
Note: We do not ship vendor SDKs. We shell out to their CLIs if present.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
import subprocess
|
|
18
|
+
import shutil
|
|
19
|
+
import os
|
|
20
|
+
import json
|
|
21
|
+
from typing import Dict, Any, Optional, Tuple
|
|
22
|
+
|
|
23
|
+
HERE = Path(__file__).parent
|
|
24
|
+
|
|
25
|
+
def which(cmd: str) -> Optional[str]:
|
|
26
|
+
return shutil.which(cmd)
|
|
27
|
+
|
|
28
|
+
def ensure_file(path: Path, content: str):
|
|
29
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
if not path.exists():
|
|
31
|
+
path.write_text(content)
|
|
32
|
+
|
|
33
|
+
def generate_fly_files(app_name: str) -> Tuple[Path, Path]:
|
|
34
|
+
fly_toml = Path("fly.toml")
|
|
35
|
+
dockerfile = Path("Dockerfile")
|
|
36
|
+
ensure_file(fly_toml, f"""# fly.toml
|
|
37
|
+
app = "{app_name}"
|
|
38
|
+
primary_region = "iad"
|
|
39
|
+
|
|
40
|
+
[build]
|
|
41
|
+
dockerfile = "Dockerfile"
|
|
42
|
+
|
|
43
|
+
[http_service]
|
|
44
|
+
internal_port = 8000
|
|
45
|
+
force_https = true
|
|
46
|
+
auto_stop_machines = "off"
|
|
47
|
+
auto_start_machines = true
|
|
48
|
+
min_machines_running = 1
|
|
49
|
+
""")
|
|
50
|
+
ensure_file(dockerfile, """# Dockerfile
|
|
51
|
+
FROM python:3.11-slim
|
|
52
|
+
WORKDIR /app
|
|
53
|
+
COPY . /app
|
|
54
|
+
RUN pip install --no-cache-dir -U pip && \
|
|
55
|
+
pip install --no-cache-dir fastapi uvicorn typer pydantic requests
|
|
56
|
+
EXPOSE 8000
|
|
57
|
+
CMD ["python", "-m", "kalibr", "serve", "kalibr_app.py", "--host", "0.0.0.0", "--port", "8000", "--base-url", "http://0.0.0.0:8000"]
|
|
58
|
+
""")
|
|
59
|
+
return fly_toml, dockerfile
|
|
60
|
+
|
|
61
|
+
def generate_render_file(service_name: str) -> Path:
|
|
62
|
+
render_yaml = Path("render.yaml")
|
|
63
|
+
ensure_file(render_yaml, f"""# render.yaml
|
|
64
|
+
services:
|
|
65
|
+
- type: web
|
|
66
|
+
name: {service_name}
|
|
67
|
+
env: docker
|
|
68
|
+
plan: free
|
|
69
|
+
dockerfilePath: ./Dockerfile
|
|
70
|
+
autoDeploy: true
|
|
71
|
+
""")
|
|
72
|
+
return render_yaml
|
|
73
|
+
|
|
74
|
+
def deploy_local(app_file: str, host: str = "0.0.0.0", port: int = 8000, base_url: str = "http://localhost:8000") -> Dict[str, Any]:
|
|
75
|
+
# Run uvicorn inline (non-blocking not handled here - CLI uses this to print guidance)
|
|
76
|
+
cmd = ["python", "-m", "kalibr", "serve", app_file, "--host", host, "--port", str(port), "--base-url", base_url]
|
|
77
|
+
print("▶︎", " ".join(cmd))
|
|
78
|
+
subprocess.run(cmd, check=False)
|
|
79
|
+
return {
|
|
80
|
+
"status": "started",
|
|
81
|
+
"endpoints": {
|
|
82
|
+
"root": f"{base_url}/",
|
|
83
|
+
"mcp": f"{base_url}/mcp.json",
|
|
84
|
+
"openapi": f"{base_url}/openapi.json",
|
|
85
|
+
"health": f"{base_url}/health"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
def deploy_fly(app_name: str) -> Dict[str, Any]:
|
|
90
|
+
if not which("flyctl"):
|
|
91
|
+
raise RuntimeError("flyctl is not installed. See https://fly.io/docs/flyctl/install/")
|
|
92
|
+
# Ensure files exist
|
|
93
|
+
generate_fly_files(app_name)
|
|
94
|
+
# Launch or deploy
|
|
95
|
+
print("▶︎ flyctl apps list")
|
|
96
|
+
subprocess.run(["flyctl", "apps", "list"], check=False)
|
|
97
|
+
print(f"▶︎ flyctl deploy --app {app_name}")
|
|
98
|
+
subprocess.run(["flyctl", "deploy", "--app", app_name], check=False)
|
|
99
|
+
url = f"https://{app_name}.fly.dev"
|
|
100
|
+
return {
|
|
101
|
+
"status": "success",
|
|
102
|
+
"endpoints": {
|
|
103
|
+
"root": f"{url}/",
|
|
104
|
+
"mcp": f"{url}/mcp.json",
|
|
105
|
+
"openapi": f"{url}/openapi.json",
|
|
106
|
+
"health": f"{url}/health"
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
def deploy_render(service_name: str) -> Dict[str, Any]:
|
|
111
|
+
# We just generate render.yaml and Dockerfile. User connects repo in Render UI.
|
|
112
|
+
generate_render_file(service_name)
|
|
113
|
+
ensure_file(Path("Dockerfile"), """# Dockerfile for Render
|
|
114
|
+
FROM python:3.11-slim
|
|
115
|
+
WORKDIR /app
|
|
116
|
+
COPY . /app
|
|
117
|
+
RUN pip install --no-cache-dir -U pip && \
|
|
118
|
+
pip install --no-cache-dir fastapi uvicorn typer pydantic requests
|
|
119
|
+
EXPOSE 8000
|
|
120
|
+
CMD ["python", "-m", "kalibr", "serve", "kalibr_app.py", "--host", "0.0.0.0", "--port", "8000", "--base-url", "https://$RENDER_EXTERNAL_URL"]
|
|
121
|
+
""")
|
|
122
|
+
print("📄 Generated render.yaml and Dockerfile. Connect your repo in Render.com and auto-deploy.")
|
|
123
|
+
return {
|
|
124
|
+
"status": "success",
|
|
125
|
+
"endpoints": {},
|
|
126
|
+
"note": "Connect this repository to Render; it will build from render.yaml."
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
def deploy(runtime: str, app_name: str, app_file: str, host: str = "0.0.0.0", port: int = 8000, base_url: str = "http://localhost:8000") -> Dict[str, Any]:
|
|
130
|
+
runtime = runtime.lower()
|
|
131
|
+
if runtime in ("local", "dev"):
|
|
132
|
+
return deploy_local(app_file, host, port, base_url)
|
|
133
|
+
if runtime in ("fly", "flyio"):
|
|
134
|
+
return deploy_fly(app_name)
|
|
135
|
+
if runtime == "render":
|
|
136
|
+
return deploy_render(app_name)
|
|
137
|
+
raise ValueError(f"Unknown runtime: {runtime}")
|
|
138
|
+
|