llama-deploy-appserver 0.2.7a1__py3-none-any.whl → 0.3.0a2__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 (29) hide show
  1. llama_deploy/appserver/__main__.py +0 -4
  2. llama_deploy/appserver/app.py +105 -25
  3. llama_deploy/appserver/bootstrap.py +76 -24
  4. llama_deploy/appserver/deployment.py +7 -421
  5. llama_deploy/appserver/deployment_config_parser.py +35 -59
  6. llama_deploy/appserver/routers/__init__.py +4 -3
  7. llama_deploy/appserver/routers/deployments.py +162 -385
  8. llama_deploy/appserver/routers/status.py +4 -31
  9. llama_deploy/appserver/routers/ui_proxy.py +213 -0
  10. llama_deploy/appserver/settings.py +57 -55
  11. llama_deploy/appserver/types.py +0 -3
  12. llama_deploy/appserver/workflow_loader.py +383 -0
  13. {llama_deploy_appserver-0.2.7a1.dist-info → llama_deploy_appserver-0.3.0a2.dist-info}/METADATA +3 -6
  14. llama_deploy_appserver-0.3.0a2.dist-info/RECORD +17 -0
  15. {llama_deploy_appserver-0.2.7a1.dist-info → llama_deploy_appserver-0.3.0a2.dist-info}/WHEEL +1 -1
  16. llama_deploy/appserver/client/__init__.py +0 -3
  17. llama_deploy/appserver/client/base.py +0 -30
  18. llama_deploy/appserver/client/client.py +0 -49
  19. llama_deploy/appserver/client/models/__init__.py +0 -4
  20. llama_deploy/appserver/client/models/apiserver.py +0 -356
  21. llama_deploy/appserver/client/models/model.py +0 -82
  22. llama_deploy/appserver/run_autodeploy.py +0 -141
  23. llama_deploy/appserver/server.py +0 -60
  24. llama_deploy/appserver/source_managers/__init__.py +0 -5
  25. llama_deploy/appserver/source_managers/base.py +0 -33
  26. llama_deploy/appserver/source_managers/git.py +0 -48
  27. llama_deploy/appserver/source_managers/local.py +0 -51
  28. llama_deploy/appserver/tracing.py +0 -237
  29. 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
- # Metrics collection settings
36
- prometheus_enabled: bool = Field(
37
- default=True,
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
- prometheus_port: int = Field(
41
- default=9000,
42
- description="The port where to serve Prometheus metrics",
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
- # Tracing settings
46
- tracing_enabled: bool = Field(
57
+ proxy_ui: bool = Field(
47
58
  default=False,
48
- description="Enable OpenTelemetry tracing. Defaults to False.",
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 url(self) -> str:
77
- protocol = "https://" if self.use_tls else "http://"
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)
@@ -91,9 +91,6 @@ class StatusEnum(Enum):
91
91
 
92
92
  class Status(BaseModel):
93
93
  status: StatusEnum
94
- status_message: str
95
- max_deployments: int | None = None
96
- deployments: list[str] | None = None
97
94
 
98
95
 
99
96
  class DeploymentDefinition(BaseModel):