llama-deploy-appserver 0.2.7a1__py3-none-any.whl → 0.3.0a1__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.
- llama_deploy/appserver/__main__.py +0 -4
- llama_deploy/appserver/app.py +105 -25
- llama_deploy/appserver/bootstrap.py +76 -24
- llama_deploy/appserver/deployment.py +7 -421
- llama_deploy/appserver/deployment_config_parser.py +35 -59
- llama_deploy/appserver/routers/__init__.py +4 -3
- llama_deploy/appserver/routers/deployments.py +162 -385
- llama_deploy/appserver/routers/status.py +4 -31
- llama_deploy/appserver/routers/ui_proxy.py +213 -0
- llama_deploy/appserver/settings.py +57 -55
- llama_deploy/appserver/types.py +0 -3
- llama_deploy/appserver/workflow_loader.py +383 -0
- {llama_deploy_appserver-0.2.7a1.dist-info → llama_deploy_appserver-0.3.0a1.dist-info}/METADATA +3 -6
- llama_deploy_appserver-0.3.0a1.dist-info/RECORD +17 -0
- {llama_deploy_appserver-0.2.7a1.dist-info → llama_deploy_appserver-0.3.0a1.dist-info}/WHEEL +1 -1
- llama_deploy/appserver/client/__init__.py +0 -3
- llama_deploy/appserver/client/base.py +0 -30
- llama_deploy/appserver/client/client.py +0 -49
- llama_deploy/appserver/client/models/__init__.py +0 -4
- llama_deploy/appserver/client/models/apiserver.py +0 -356
- llama_deploy/appserver/client/models/model.py +0 -82
- llama_deploy/appserver/run_autodeploy.py +0 -141
- llama_deploy/appserver/server.py +0 -60
- llama_deploy/appserver/source_managers/__init__.py +0 -5
- llama_deploy/appserver/source_managers/base.py +0 -33
- llama_deploy/appserver/source_managers/git.py +0 -48
- llama_deploy/appserver/source_managers/local.py +0 -51
- llama_deploy/appserver/tracing.py +0 -237
- llama_deploy_appserver-0.2.7a1.dist-info/RECORD +0 -28
@@ -0,0 +1,213 @@
|
|
1
|
+
import asyncio
|
2
|
+
import logging
|
3
|
+
from typing import List, Optional
|
4
|
+
|
5
|
+
from fastapi.staticfiles import StaticFiles
|
6
|
+
from fastapi import FastAPI
|
7
|
+
import httpx
|
8
|
+
from llama_deploy.appserver.deployment_config_parser import DeploymentConfig
|
9
|
+
from llama_deploy.appserver.settings import ApiserverSettings
|
10
|
+
import websockets
|
11
|
+
from fastapi import (
|
12
|
+
APIRouter,
|
13
|
+
HTTPException,
|
14
|
+
Request,
|
15
|
+
WebSocket,
|
16
|
+
)
|
17
|
+
from fastapi.responses import StreamingResponse
|
18
|
+
from starlette.background import BackgroundTask
|
19
|
+
|
20
|
+
logger = logging.getLogger(__name__)
|
21
|
+
|
22
|
+
|
23
|
+
async def _ws_proxy(ws: WebSocket, upstream_url: str) -> None:
|
24
|
+
"""Proxy WebSocket connection to upstream server."""
|
25
|
+
await ws.accept()
|
26
|
+
|
27
|
+
# Forward most headers except WebSocket-specific ones
|
28
|
+
header_blacklist = {
|
29
|
+
"host",
|
30
|
+
"connection",
|
31
|
+
"upgrade",
|
32
|
+
"sec-websocket-key",
|
33
|
+
"sec-websocket-version",
|
34
|
+
"sec-websocket-extensions",
|
35
|
+
}
|
36
|
+
hdrs = [(k, v) for k, v in ws.headers.items() if k.lower() not in header_blacklist]
|
37
|
+
|
38
|
+
try:
|
39
|
+
# Parse subprotocols if present
|
40
|
+
subprotocols: Optional[List[websockets.Subprotocol]] = None
|
41
|
+
if "sec-websocket-protocol" in ws.headers:
|
42
|
+
# Parse comma-separated subprotocols
|
43
|
+
subprotocols = [
|
44
|
+
websockets.Subprotocol(p.strip())
|
45
|
+
for p in ws.headers["sec-websocket-protocol"].split(",")
|
46
|
+
]
|
47
|
+
|
48
|
+
# Open upstream WebSocket connection
|
49
|
+
async with websockets.connect(
|
50
|
+
upstream_url,
|
51
|
+
additional_headers=hdrs,
|
52
|
+
subprotocols=subprotocols,
|
53
|
+
open_timeout=None,
|
54
|
+
ping_interval=None,
|
55
|
+
) as upstream:
|
56
|
+
|
57
|
+
async def client_to_upstream() -> None:
|
58
|
+
try:
|
59
|
+
while True:
|
60
|
+
msg = await ws.receive()
|
61
|
+
if msg["type"] == "websocket.receive":
|
62
|
+
if "text" in msg:
|
63
|
+
await upstream.send(msg["text"])
|
64
|
+
elif "bytes" in msg:
|
65
|
+
await upstream.send(msg["bytes"])
|
66
|
+
elif msg["type"] == "websocket.disconnect":
|
67
|
+
break
|
68
|
+
except Exception as e:
|
69
|
+
logger.debug(f"Client to upstream connection ended: {e}")
|
70
|
+
|
71
|
+
async def upstream_to_client() -> None:
|
72
|
+
try:
|
73
|
+
async for message in upstream:
|
74
|
+
if isinstance(message, str):
|
75
|
+
await ws.send_text(message)
|
76
|
+
else:
|
77
|
+
await ws.send_bytes(message)
|
78
|
+
except Exception as e:
|
79
|
+
logger.debug(f"Upstream to client connection ended: {e}")
|
80
|
+
|
81
|
+
# Pump both directions concurrently
|
82
|
+
await asyncio.gather(
|
83
|
+
client_to_upstream(), upstream_to_client(), return_exceptions=True
|
84
|
+
)
|
85
|
+
|
86
|
+
except Exception as e:
|
87
|
+
logger.error(f"WebSocket proxy error: {e}")
|
88
|
+
finally:
|
89
|
+
try:
|
90
|
+
await ws.close()
|
91
|
+
except Exception as e:
|
92
|
+
logger.debug(f"Error closing client connection: {e}")
|
93
|
+
|
94
|
+
|
95
|
+
def create_ui_proxy_router(name: str, port: int) -> APIRouter:
|
96
|
+
deployment_router = APIRouter(
|
97
|
+
prefix=f"/deployments/{name}",
|
98
|
+
tags=["deployments"],
|
99
|
+
)
|
100
|
+
|
101
|
+
@deployment_router.websocket("/ui/{path:path}")
|
102
|
+
@deployment_router.websocket("/ui")
|
103
|
+
async def websocket_proxy(
|
104
|
+
websocket: WebSocket,
|
105
|
+
path: str | None = None,
|
106
|
+
) -> None:
|
107
|
+
# Build the upstream WebSocket URL using FastAPI's extracted path parameter
|
108
|
+
slash_path = f"/{path}" if path else ""
|
109
|
+
upstream_path = f"/deployments/{name}/ui{slash_path}"
|
110
|
+
|
111
|
+
# Convert to WebSocket URL
|
112
|
+
upstream_url = f"ws://localhost:{port}{upstream_path}"
|
113
|
+
if websocket.url.query:
|
114
|
+
upstream_url += f"?{websocket.url.query}"
|
115
|
+
|
116
|
+
logger.debug(f"Proxying WebSocket {websocket.url} -> {upstream_url}")
|
117
|
+
|
118
|
+
await _ws_proxy(websocket, upstream_url)
|
119
|
+
|
120
|
+
@deployment_router.api_route(
|
121
|
+
"/ui/{path:path}",
|
122
|
+
methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH"],
|
123
|
+
)
|
124
|
+
@deployment_router.api_route(
|
125
|
+
"/ui",
|
126
|
+
methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH"],
|
127
|
+
)
|
128
|
+
async def proxy(
|
129
|
+
request: Request,
|
130
|
+
path: str | None = None,
|
131
|
+
) -> StreamingResponse:
|
132
|
+
# Build the upstream URL using FastAPI's extracted path parameter
|
133
|
+
slash_path = f"/{path}" if path else ""
|
134
|
+
upstream_path = f"/deployments/{name}/ui{slash_path}"
|
135
|
+
|
136
|
+
upstream_url = httpx.URL(f"http://localhost:{port}{upstream_path}").copy_with(
|
137
|
+
params=request.query_params
|
138
|
+
)
|
139
|
+
|
140
|
+
# Debug logging
|
141
|
+
logger.debug(f"Proxying {request.method} {request.url} -> {upstream_url}")
|
142
|
+
|
143
|
+
# Strip hop-by-hop headers + host
|
144
|
+
hop_by_hop = {
|
145
|
+
"connection",
|
146
|
+
"keep-alive",
|
147
|
+
"proxy-authenticate",
|
148
|
+
"proxy-authorization",
|
149
|
+
"te", # codespell:ignore
|
150
|
+
"trailers",
|
151
|
+
"transfer-encoding",
|
152
|
+
"upgrade",
|
153
|
+
"host",
|
154
|
+
}
|
155
|
+
headers = {
|
156
|
+
k: v for k, v in request.headers.items() if k.lower() not in hop_by_hop
|
157
|
+
}
|
158
|
+
|
159
|
+
try:
|
160
|
+
client = httpx.AsyncClient(timeout=None)
|
161
|
+
|
162
|
+
req = client.build_request(
|
163
|
+
request.method,
|
164
|
+
upstream_url,
|
165
|
+
headers=headers,
|
166
|
+
content=request.stream(), # stream uploads
|
167
|
+
)
|
168
|
+
upstream = await client.send(req, stream=True)
|
169
|
+
|
170
|
+
resp_headers = {
|
171
|
+
k: v for k, v in upstream.headers.items() if k.lower() not in hop_by_hop
|
172
|
+
}
|
173
|
+
|
174
|
+
# Close client when upstream response is done
|
175
|
+
async def cleanup() -> None:
|
176
|
+
await upstream.aclose()
|
177
|
+
await client.aclose()
|
178
|
+
|
179
|
+
return StreamingResponse(
|
180
|
+
upstream.aiter_raw(), # stream downloads
|
181
|
+
status_code=upstream.status_code,
|
182
|
+
headers=resp_headers,
|
183
|
+
background=BackgroundTask(cleanup), # tidy up when finished
|
184
|
+
)
|
185
|
+
|
186
|
+
except httpx.ConnectError:
|
187
|
+
raise HTTPException(status_code=502, detail="Upstream server unavailable")
|
188
|
+
except httpx.TimeoutException:
|
189
|
+
raise HTTPException(status_code=504, detail="Upstream server timeout")
|
190
|
+
except Exception as e:
|
191
|
+
logger.error(f"Proxy error: {e}")
|
192
|
+
raise HTTPException(status_code=502, detail="Proxy error")
|
193
|
+
|
194
|
+
return deployment_router
|
195
|
+
|
196
|
+
|
197
|
+
def mount_static_files(
|
198
|
+
app: FastAPI, config: DeploymentConfig, settings: ApiserverSettings
|
199
|
+
) -> None:
|
200
|
+
if not config.ui or not config.ui.source:
|
201
|
+
return
|
202
|
+
|
203
|
+
ui_path = settings.config_parent / config.ui.source.location / "dist"
|
204
|
+
if not ui_path.exists():
|
205
|
+
return
|
206
|
+
|
207
|
+
# Serve index.html when accessing the directory path
|
208
|
+
app.mount(
|
209
|
+
f"/deployments/{config.name}/ui",
|
210
|
+
StaticFiles(directory=str(ui_path), html=True),
|
211
|
+
name=f"ui-static-{config.name}",
|
212
|
+
)
|
213
|
+
return None
|
@@ -1,7 +1,35 @@
|
|
1
|
+
import os
|
1
2
|
from pathlib import Path
|
2
3
|
|
3
4
|
from pydantic import Field
|
4
5
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
6
|
+
from llama_deploy.core.config import DEFAULT_DEPLOYMENT_FILE_PATH
|
7
|
+
|
8
|
+
|
9
|
+
class BootstrapSettings(BaseSettings):
|
10
|
+
"""
|
11
|
+
Settings configurable via env vars for controlling how an application is
|
12
|
+
created from a git repository.
|
13
|
+
"""
|
14
|
+
|
15
|
+
model_config = SettingsConfigDict(env_prefix="LLAMA_DEPLOY_")
|
16
|
+
repo_url: str | None = Field(
|
17
|
+
default=None, description="The URL of the git repository to clone"
|
18
|
+
)
|
19
|
+
auth_token: str | None = Field(
|
20
|
+
default=None, description="The token to use to clone the git repository"
|
21
|
+
)
|
22
|
+
git_ref: str | None = Field(
|
23
|
+
default=None, description="The git reference to checkout"
|
24
|
+
)
|
25
|
+
git_sha: str | None = Field(default=None, description="The git SHA to checkout")
|
26
|
+
deployment_file_path: str = Field(
|
27
|
+
default="llama_deploy.yaml",
|
28
|
+
description="The path to the deployment file, relative to the root of the repository",
|
29
|
+
)
|
30
|
+
deployment_name: str | None = Field(
|
31
|
+
default=None, description="The name of the deployment"
|
32
|
+
)
|
5
33
|
|
6
34
|
|
7
35
|
class ApiserverSettings(BaseSettings):
|
@@ -15,69 +43,43 @@ class ApiserverSettings(BaseSettings):
|
|
15
43
|
default=4501,
|
16
44
|
description="The TCP port where to bind the API Server",
|
17
45
|
)
|
18
|
-
rc_path: Path = Field(
|
19
|
-
default=Path("./.llama_deploy_rc"),
|
20
|
-
description="Path to the folder containing the deployment configs that will be loaded at startup",
|
21
|
-
)
|
22
|
-
deployments_path: Path | None = Field(
|
23
|
-
default=None,
|
24
|
-
description="Path to the folder where deployments will create their root path, defaults to a temp dir",
|
25
|
-
)
|
26
|
-
deployment_file_path: str | None = Field(
|
27
|
-
default=None,
|
28
|
-
description="Optional path, relative to the rc_path, where the deployment file is located. If not provided, will glob all .yml/.yaml files in the rc_path",
|
29
|
-
)
|
30
|
-
use_tls: bool = Field(
|
31
|
-
default=False,
|
32
|
-
description="Use TLS (HTTPS) to communicate with the API Server",
|
33
|
-
)
|
34
46
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
description="Whether to enable the Prometheus metrics exporter along with the API Server",
|
47
|
+
app_root: Path = Field(
|
48
|
+
default=Path("."),
|
49
|
+
description="The root of the application",
|
39
50
|
)
|
40
|
-
|
41
|
-
|
42
|
-
|
51
|
+
|
52
|
+
deployment_file_path: Path = Field(
|
53
|
+
default=Path(DEFAULT_DEPLOYMENT_FILE_PATH),
|
54
|
+
description="path, relative to the repository root, where the deployment file is located. If not provided, will look for ./llama_deploy.yaml",
|
43
55
|
)
|
44
56
|
|
45
|
-
|
46
|
-
tracing_enabled: bool = Field(
|
57
|
+
proxy_ui: bool = Field(
|
47
58
|
default=False,
|
48
|
-
description="
|
49
|
-
)
|
50
|
-
tracing_service_name: str = Field(
|
51
|
-
default="llama-deploy-appserver",
|
52
|
-
description="Service name for tracing. Defaults to 'llama-deploy-appserver'.",
|
53
|
-
)
|
54
|
-
tracing_exporter: str = Field(
|
55
|
-
default="console",
|
56
|
-
description="Trace exporter type: 'console', 'jaeger', 'otlp'. Defaults to 'console'.",
|
57
|
-
)
|
58
|
-
tracing_endpoint: str | None = Field(
|
59
|
-
default=None,
|
60
|
-
description="Trace exporter endpoint. Required for 'jaeger' and 'otlp' exporters.",
|
61
|
-
)
|
62
|
-
tracing_sample_rate: float = Field(
|
63
|
-
default=1.0,
|
64
|
-
description="Trace sampling rate (0.0 to 1.0). Defaults to 1.0 (100% sampling).",
|
65
|
-
)
|
66
|
-
tracing_insecure: bool = Field(
|
67
|
-
default=True,
|
68
|
-
description="Use insecure connection for OTLP exporter. Defaults to True.",
|
69
|
-
)
|
70
|
-
tracing_timeout: int = Field(
|
71
|
-
default=30,
|
72
|
-
description="Timeout in seconds for trace export. Defaults to 30.",
|
59
|
+
description="If true, proxy a development UI server instead of serving built assets",
|
73
60
|
)
|
74
61
|
|
75
62
|
@property
|
76
|
-
def
|
77
|
-
|
78
|
-
if self.port == 80:
|
79
|
-
return f"{protocol}{self.host}"
|
80
|
-
return f"{protocol}{self.host}:{self.port}"
|
63
|
+
def config_parent(self) -> Path:
|
64
|
+
return (self.app_root / self.deployment_file_path).parent
|
81
65
|
|
82
66
|
|
83
67
|
settings = ApiserverSettings()
|
68
|
+
|
69
|
+
|
70
|
+
def configure_settings(
|
71
|
+
proxy_ui: bool | None = None,
|
72
|
+
deployment_file_path: Path | None = None,
|
73
|
+
app_root: Path | None = None,
|
74
|
+
) -> None:
|
75
|
+
if proxy_ui is not None:
|
76
|
+
settings.proxy_ui = proxy_ui
|
77
|
+
os.environ["LLAMA_DEPLOY_APISERVER_PROXY_UI"] = "true" if proxy_ui else "false"
|
78
|
+
if deployment_file_path is not None:
|
79
|
+
settings.deployment_file_path = deployment_file_path
|
80
|
+
os.environ["LLAMA_DEPLOY_APISERVER_DEPLOYMENT_FILE_PATH"] = str(
|
81
|
+
deployment_file_path
|
82
|
+
)
|
83
|
+
if app_root is not None:
|
84
|
+
settings.app_root = app_root
|
85
|
+
os.environ["LLAMA_DEPLOY_APISERVER_APP_ROOT"] = str(app_root)
|
llama_deploy/appserver/types.py
CHANGED