llama-deploy-appserver 0.3.0a6__tar.gz → 0.3.0a8__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.
- {llama_deploy_appserver-0.3.0a6 → llama_deploy_appserver-0.3.0a8}/PKG-INFO +3 -3
- {llama_deploy_appserver-0.3.0a6 → llama_deploy_appserver-0.3.0a8}/pyproject.toml +3 -3
- {llama_deploy_appserver-0.3.0a6 → llama_deploy_appserver-0.3.0a8}/src/llama_deploy/appserver/app.py +98 -17
- {llama_deploy_appserver-0.3.0a6 → llama_deploy_appserver-0.3.0a8}/src/llama_deploy/appserver/bootstrap.py +9 -6
- {llama_deploy_appserver-0.3.0a6 → llama_deploy_appserver-0.3.0a8}/src/llama_deploy/appserver/deployment.py +7 -0
- {llama_deploy_appserver-0.3.0a6 → llama_deploy_appserver-0.3.0a8}/src/llama_deploy/appserver/deployment_config_parser.py +6 -4
- {llama_deploy_appserver-0.3.0a6 → llama_deploy_appserver-0.3.0a8}/src/llama_deploy/appserver/process_utils.py +9 -0
- {llama_deploy_appserver-0.3.0a6 → llama_deploy_appserver-0.3.0a8}/src/llama_deploy/appserver/routers/deployments.py +1 -3
- {llama_deploy_appserver-0.3.0a6 → llama_deploy_appserver-0.3.0a8}/src/llama_deploy/appserver/routers/ui_proxy.py +7 -5
- {llama_deploy_appserver-0.3.0a6 → llama_deploy_appserver-0.3.0a8}/src/llama_deploy/appserver/settings.py +9 -4
- {llama_deploy_appserver-0.3.0a6 → llama_deploy_appserver-0.3.0a8}/src/llama_deploy/appserver/workflow_loader.py +75 -150
- {llama_deploy_appserver-0.3.0a6 → llama_deploy_appserver-0.3.0a8}/README.md +0 -0
- {llama_deploy_appserver-0.3.0a6 → llama_deploy_appserver-0.3.0a8}/src/llama_deploy/appserver/__init__.py +0 -0
- {llama_deploy_appserver-0.3.0a6 → llama_deploy_appserver-0.3.0a8}/src/llama_deploy/appserver/__main__.py +0 -0
- {llama_deploy_appserver-0.3.0a6 → llama_deploy_appserver-0.3.0a8}/src/llama_deploy/appserver/routers/__init__.py +0 -0
- {llama_deploy_appserver-0.3.0a6 → llama_deploy_appserver-0.3.0a8}/src/llama_deploy/appserver/routers/status.py +0 -0
- {llama_deploy_appserver-0.3.0a6 → llama_deploy_appserver-0.3.0a8}/src/llama_deploy/appserver/stats.py +0 -0
- {llama_deploy_appserver-0.3.0a6 → llama_deploy_appserver-0.3.0a8}/src/llama_deploy/appserver/types.py +0 -0
@@ -1,16 +1,16 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: llama-deploy-appserver
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.0a8
|
4
4
|
Summary: Application server components for LlamaDeploy
|
5
5
|
Author: Massimiliano Pippi
|
6
6
|
Author-email: Massimiliano Pippi <mpippi@gmail.com>
|
7
7
|
License: MIT
|
8
|
-
Requires-Dist: llama-index-workflows>=1.
|
8
|
+
Requires-Dist: llama-index-workflows[server]>=1.3.0
|
9
9
|
Requires-Dist: pydantic-settings>=2.10.1
|
10
10
|
Requires-Dist: uvicorn>=0.24.0
|
11
11
|
Requires-Dist: fastapi>=0.100.0
|
12
12
|
Requires-Dist: websockets>=12.0
|
13
|
-
Requires-Dist: llama-deploy-core>=0.3.
|
13
|
+
Requires-Dist: llama-deploy-core>=0.3.0a8,<0.4.0
|
14
14
|
Requires-Dist: httpx>=0.28.1
|
15
15
|
Requires-Dist: prometheus-fastapi-instrumentator>=7.1.0
|
16
16
|
Requires-Dist: packaging>=25.0
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "llama-deploy-appserver"
|
3
|
-
version = "0.3.
|
3
|
+
version = "0.3.0a8"
|
4
4
|
description = "Application server components for LlamaDeploy"
|
5
5
|
readme = "README.md"
|
6
6
|
license = { text = "MIT" }
|
@@ -9,12 +9,12 @@ authors = [
|
|
9
9
|
]
|
10
10
|
requires-python = ">=3.12, <4"
|
11
11
|
dependencies = [
|
12
|
-
"llama-index-workflows>=1.
|
12
|
+
"llama-index-workflows[server]>=1.3.0",
|
13
13
|
"pydantic-settings>=2.10.1",
|
14
14
|
"uvicorn>=0.24.0",
|
15
15
|
"fastapi>=0.100.0",
|
16
16
|
"websockets>=12.0",
|
17
|
-
"llama-deploy-core>=0.3.
|
17
|
+
"llama-deploy-core>=0.3.0a8,<0.4.0",
|
18
18
|
"httpx>=0.28.1",
|
19
19
|
"prometheus-fastapi-instrumentator>=7.1.0",
|
20
20
|
"packaging>=25.0",
|
{llama_deploy_appserver-0.3.0a6 → llama_deploy_appserver-0.3.0a8}/src/llama_deploy/appserver/app.py
RENAMED
@@ -5,12 +5,14 @@ import threading
|
|
5
5
|
import time
|
6
6
|
import webbrowser
|
7
7
|
from contextlib import asynccontextmanager
|
8
|
+
from importlib.metadata import version
|
8
9
|
from pathlib import Path
|
9
10
|
from typing import Any, AsyncGenerator
|
10
11
|
|
11
12
|
import uvicorn
|
12
13
|
from fastapi import FastAPI
|
13
14
|
from fastapi.middleware.cors import CORSMiddleware
|
15
|
+
from fastapi.openapi.utils import get_openapi
|
14
16
|
from llama_deploy.appserver.deployment_config_parser import (
|
15
17
|
get_deployment_config,
|
16
18
|
)
|
@@ -26,14 +28,17 @@ from llama_deploy.appserver.settings import configure_settings, settings
|
|
26
28
|
from llama_deploy.appserver.workflow_loader import (
|
27
29
|
_exclude_venv_warning,
|
28
30
|
build_ui,
|
29
|
-
find_python_pyproject,
|
30
31
|
inject_appserver_into_target,
|
31
32
|
install_ui,
|
32
33
|
load_environment_variables,
|
33
34
|
load_workflows,
|
34
35
|
start_dev_ui_process,
|
35
36
|
)
|
37
|
+
from llama_deploy.core.config import DEFAULT_DEPLOYMENT_FILE_PATH
|
36
38
|
from prometheus_fastapi_instrumentator import Instrumentator
|
39
|
+
from starlette.routing import Route
|
40
|
+
from starlette.schemas import SchemaGenerator
|
41
|
+
from workflows.server import WorkflowServer
|
37
42
|
|
38
43
|
from .deployment import Deployment
|
39
44
|
from .process_utils import run_process
|
@@ -52,12 +57,28 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, Any]:
|
|
52
57
|
deployment = Deployment(workflows)
|
53
58
|
base_router = create_base_router(config.name)
|
54
59
|
deploy_router = create_deployments_router(config.name, deployment)
|
60
|
+
server = deployment.create_workflow_server()
|
61
|
+
|
62
|
+
for route in server.app.routes:
|
63
|
+
# add routes directly rather than mounting, so that we can share a root
|
64
|
+
if isinstance(route, Route):
|
65
|
+
app.add_api_route(
|
66
|
+
f"/deployments/{config.name}{route.path}",
|
67
|
+
route.endpoint,
|
68
|
+
name=f"{config.name}_{route.name}",
|
69
|
+
methods=route.methods,
|
70
|
+
include_in_schema=True, # change to false when schemas are added to workflow server
|
71
|
+
tags=["workflows"],
|
72
|
+
)
|
73
|
+
|
55
74
|
app.include_router(base_router)
|
56
75
|
app.include_router(deploy_router)
|
57
|
-
|
76
|
+
|
77
|
+
_setup_openapi(config.name, app, server)
|
78
|
+
|
58
79
|
if config.ui is not None:
|
59
80
|
if settings.proxy_ui:
|
60
|
-
ui_router = create_ui_proxy_router(config.name,
|
81
|
+
ui_router = create_ui_proxy_router(config.name, settings.proxy_ui_port)
|
61
82
|
app.include_router(ui_router)
|
62
83
|
else:
|
63
84
|
# otherwise serve the pre-built if available
|
@@ -69,6 +90,50 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, Any]:
|
|
69
90
|
apiserver_state.state("stopped")
|
70
91
|
|
71
92
|
|
93
|
+
def _setup_openapi(name: str, app: FastAPI, server: WorkflowServer) -> None:
|
94
|
+
"""
|
95
|
+
extends the fastapi based openapi schema with starlette generated schema
|
96
|
+
"""
|
97
|
+
schema_title = "Llama Deploy App Server"
|
98
|
+
app_version = version("llama-deploy-appserver")
|
99
|
+
|
100
|
+
prefix = f"/deployments/{name}"
|
101
|
+
workflow_routes = [x for x in server.app.routes if isinstance(x, Route)]
|
102
|
+
server_app_routes = []
|
103
|
+
for route in workflow_routes:
|
104
|
+
server_app_routes.append(
|
105
|
+
Route(
|
106
|
+
path=prefix + route.path,
|
107
|
+
endpoint=route.endpoint,
|
108
|
+
methods=route.methods,
|
109
|
+
name=route.name,
|
110
|
+
include_in_schema=route.include_in_schema,
|
111
|
+
)
|
112
|
+
)
|
113
|
+
|
114
|
+
schemas = SchemaGenerator(
|
115
|
+
{"openapi": "3.0.0", "info": {"title": schema_title, "version": app_version}}
|
116
|
+
)
|
117
|
+
starlette_schema = schemas.get_schema(server_app_routes)
|
118
|
+
|
119
|
+
def custom_openapi():
|
120
|
+
if app.openapi_schema:
|
121
|
+
return app.openapi_schema
|
122
|
+
openapi_schema = get_openapi(
|
123
|
+
title=schema_title,
|
124
|
+
version=app_version,
|
125
|
+
routes=app.routes + server_app_routes,
|
126
|
+
)
|
127
|
+
openapi_schema["paths"] = {
|
128
|
+
**openapi_schema["paths"],
|
129
|
+
**starlette_schema["paths"],
|
130
|
+
}
|
131
|
+
app.openapi_schema = openapi_schema
|
132
|
+
return app.openapi_schema
|
133
|
+
|
134
|
+
app.openapi = custom_openapi # ty: ignore[invalid-assignment] - doesn't like us overwriting the method
|
135
|
+
|
136
|
+
|
72
137
|
app = FastAPI(lifespan=lifespan)
|
73
138
|
Instrumentator().instrument(app).expose(app)
|
74
139
|
|
@@ -98,14 +163,16 @@ def prepare_server(
|
|
98
163
|
install: bool = False,
|
99
164
|
build: bool = False,
|
100
165
|
) -> None:
|
101
|
-
configure_settings(
|
102
|
-
|
166
|
+
configure_settings(
|
167
|
+
deployment_file_path=deployment_file or Path(DEFAULT_DEPLOYMENT_FILE_PATH)
|
168
|
+
)
|
169
|
+
load_environment_variables(get_deployment_config(), settings.resolved_config_parent)
|
103
170
|
if install:
|
104
171
|
config = get_deployment_config()
|
105
|
-
inject_appserver_into_target(config, settings.
|
106
|
-
install_ui(config, settings.
|
172
|
+
inject_appserver_into_target(config, settings.resolved_config_parent)
|
173
|
+
install_ui(config, settings.resolved_config_parent)
|
107
174
|
if build:
|
108
|
-
build_ui(settings.
|
175
|
+
build_ui(settings.resolved_config_parent, get_deployment_config(), settings)
|
109
176
|
|
110
177
|
|
111
178
|
def start_server(
|
@@ -119,15 +186,15 @@ def start_server(
|
|
119
186
|
configure_settings(
|
120
187
|
proxy_ui=proxy_ui,
|
121
188
|
app_root=cwd,
|
122
|
-
deployment_file_path=deployment_file,
|
189
|
+
deployment_file_path=deployment_file or Path(DEFAULT_DEPLOYMENT_FILE_PATH),
|
123
190
|
reload=reload,
|
124
191
|
)
|
125
|
-
load_environment_variables(get_deployment_config(), settings.
|
192
|
+
load_environment_variables(get_deployment_config(), settings.resolved_config_parent)
|
126
193
|
|
127
194
|
ui_process = None
|
128
195
|
if proxy_ui:
|
129
196
|
ui_process = start_dev_ui_process(
|
130
|
-
settings.
|
197
|
+
settings.resolved_config_parent, settings, get_deployment_config()
|
131
198
|
)
|
132
199
|
try:
|
133
200
|
if open_browser:
|
@@ -150,11 +217,20 @@ def start_server_in_target_venv(
|
|
150
217
|
cwd: Path | None = None,
|
151
218
|
deployment_file: Path | None = None,
|
152
219
|
open_browser: bool = False,
|
220
|
+
port: int | None = None,
|
221
|
+
ui_port: int | None = None,
|
153
222
|
) -> None:
|
154
|
-
|
155
|
-
path = find_python_pyproject(cwd or Path.cwd(), cfg)
|
223
|
+
# Ensure settings reflect the intended working directory before computing paths
|
156
224
|
|
157
|
-
|
225
|
+
configure_settings(
|
226
|
+
app_root=cwd,
|
227
|
+
deployment_file_path=deployment_file,
|
228
|
+
reload=reload,
|
229
|
+
proxy_ui=proxy_ui,
|
230
|
+
)
|
231
|
+
base_dir = cwd or Path.cwd()
|
232
|
+
path = settings.resolved_config_parent.relative_to(base_dir)
|
233
|
+
args = ["uv", "run", "--no-progress", "python", "-m", "llama_deploy.appserver.app"]
|
158
234
|
if proxy_ui:
|
159
235
|
args.append("--proxy-ui")
|
160
236
|
if reload:
|
@@ -164,11 +240,17 @@ def start_server_in_target_venv(
|
|
164
240
|
args.append(str(deployment_file))
|
165
241
|
if open_browser:
|
166
242
|
args.append("--open-browser")
|
167
|
-
|
243
|
+
|
244
|
+
env = os.environ.copy()
|
245
|
+
if port:
|
246
|
+
env["LLAMA_DEPLOY_APISERVER_PORT"] = str(port)
|
247
|
+
if ui_port:
|
248
|
+
env["LLAMA_DEPLOY_APISERVER_PROXY_UI_PORT"] = str(ui_port)
|
249
|
+
|
168
250
|
ret = run_process(
|
169
251
|
args,
|
170
252
|
cwd=path,
|
171
|
-
env=
|
253
|
+
env=env,
|
172
254
|
line_transform=_exclude_venv_warning,
|
173
255
|
)
|
174
256
|
|
@@ -177,7 +259,6 @@ def start_server_in_target_venv(
|
|
177
259
|
|
178
260
|
|
179
261
|
if __name__ == "__main__":
|
180
|
-
print("starting server")
|
181
262
|
parser = argparse.ArgumentParser()
|
182
263
|
parser.add_argument("--proxy-ui", action="store_true")
|
183
264
|
parser.add_argument("--reload", action="store_true")
|
@@ -8,7 +8,11 @@ import os
|
|
8
8
|
from pathlib import Path
|
9
9
|
|
10
10
|
from llama_deploy.appserver.deployment_config_parser import get_deployment_config
|
11
|
-
from llama_deploy.appserver.settings import
|
11
|
+
from llama_deploy.appserver.settings import (
|
12
|
+
BootstrapSettings,
|
13
|
+
configure_settings,
|
14
|
+
settings,
|
15
|
+
)
|
12
16
|
from llama_deploy.appserver.workflow_loader import (
|
13
17
|
build_ui,
|
14
18
|
inject_appserver_into_target,
|
@@ -45,8 +49,7 @@ def bootstrap_app_from_repo(
|
|
45
49
|
deployment_file_path=Path(bootstrap_settings.deployment_file_path),
|
46
50
|
)
|
47
51
|
config = get_deployment_config()
|
48
|
-
|
49
|
-
load_environment_variables(config, base_path)
|
52
|
+
load_environment_variables(config, settings.resolved_config_parent)
|
50
53
|
|
51
54
|
sdists = None
|
52
55
|
if bootstrap_settings.bootstrap_sdists:
|
@@ -59,9 +62,9 @@ def bootstrap_app_from_repo(
|
|
59
62
|
sdists = None
|
60
63
|
# Use the explicit base path rather than relying on global settings so tests
|
61
64
|
# can safely mock configure_settings without affecting call arguments.
|
62
|
-
inject_appserver_into_target(config,
|
63
|
-
install_ui(config,
|
64
|
-
build_ui(
|
65
|
+
inject_appserver_into_target(config, settings.resolved_config_parent, sdists)
|
66
|
+
install_ui(config, settings.resolved_config_parent)
|
67
|
+
build_ui(settings.resolved_config_parent, config, settings)
|
65
68
|
|
66
69
|
pass
|
67
70
|
|
@@ -7,6 +7,7 @@ from llama_deploy.appserver.types import generate_id
|
|
7
7
|
from llama_deploy.appserver.workflow_loader import DEFAULT_SERVICE_ID
|
8
8
|
from workflows import Context, Workflow
|
9
9
|
from workflows.handler import WorkflowHandler
|
10
|
+
from workflows.server import WorkflowServer
|
10
11
|
|
11
12
|
logger = logging.getLogger()
|
12
13
|
|
@@ -78,3 +79,9 @@ class Deployment:
|
|
78
79
|
self._handlers[handler_id] = handler
|
79
80
|
self._handler_inputs[handler_id] = json.dumps(run_kwargs)
|
80
81
|
return handler_id, session_id
|
82
|
+
|
83
|
+
def create_workflow_server(self) -> WorkflowServer:
|
84
|
+
server = WorkflowServer()
|
85
|
+
for service_id, workflow in self._workflow_services.items():
|
86
|
+
server.add_workflow(service_id, workflow)
|
87
|
+
return server
|
@@ -1,13 +1,15 @@
|
|
1
1
|
import functools
|
2
2
|
|
3
3
|
from llama_deploy.appserver.settings import BootstrapSettings, settings
|
4
|
-
from llama_deploy.core.deployment_config import DeploymentConfig
|
4
|
+
from llama_deploy.core.deployment_config import DeploymentConfig, read_deployment_config
|
5
5
|
|
6
6
|
|
7
|
-
@functools.
|
7
|
+
@functools.cache
|
8
8
|
def get_deployment_config() -> DeploymentConfig:
|
9
9
|
base_settings = BootstrapSettings()
|
10
10
|
base = settings.app_root.resolve()
|
11
|
-
yaml_file = base / settings.deployment_file_path
|
12
11
|
name = base_settings.deployment_name
|
13
|
-
|
12
|
+
parsed = read_deployment_config(base, settings.deployment_file_path)
|
13
|
+
if name is not None:
|
14
|
+
parsed.name = name
|
15
|
+
return parsed
|
@@ -30,6 +30,7 @@ def run_process(
|
|
30
30
|
threads: list[threading.Thread] = []
|
31
31
|
try:
|
32
32
|
cleanup()
|
33
|
+
_log_command(cmd, prefixer)
|
33
34
|
threads = _start_stream_threads(sources, prefixer)
|
34
35
|
ret = process.wait()
|
35
36
|
if ret != 0:
|
@@ -58,6 +59,7 @@ def spawn_process(
|
|
58
59
|
|
59
60
|
process, sources, cleanup = _spawn_process(cmd, cwd=cwd, env=env, use_pty=use_pty)
|
60
61
|
cleanup()
|
62
|
+
_log_command(cmd, prefixer)
|
61
63
|
_start_stream_threads(sources, prefixer)
|
62
64
|
return process
|
63
65
|
|
@@ -187,6 +189,13 @@ def _stream_source(
|
|
187
189
|
pass
|
188
190
|
|
189
191
|
|
192
|
+
def _log_command(cmd: list[str], transform: Callable[[str], str | None] | None) -> None:
|
193
|
+
cmd_str = "> " + " ".join(cmd)
|
194
|
+
if transform:
|
195
|
+
cmd_str = transform(cmd_str)
|
196
|
+
sys.stderr.write(cmd_str + "\n")
|
197
|
+
|
198
|
+
|
190
199
|
def _start_stream_threads(
|
191
200
|
sources: list[tuple[int | TextIO, TextIO]],
|
192
201
|
transform: Callable[[str], str | None] | None,
|
@@ -33,9 +33,7 @@ def create_base_router(name: str) -> APIRouter:
|
|
33
33
|
return base_router
|
34
34
|
|
35
35
|
|
36
|
-
def create_deployments_router(
|
37
|
-
name: str, deployment: Deployment, serve_static: bool = False
|
38
|
-
) -> APIRouter:
|
36
|
+
def create_deployments_router(name: str, deployment: Deployment) -> APIRouter:
|
39
37
|
deployments_router = APIRouter(
|
40
38
|
prefix="/deployments",
|
41
39
|
)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import asyncio
|
2
2
|
import logging
|
3
|
-
from typing import List
|
3
|
+
from typing import List
|
4
4
|
|
5
5
|
import httpx
|
6
6
|
import websockets
|
@@ -120,10 +120,12 @@ def create_ui_proxy_router(name: str, port: int) -> APIRouter:
|
|
120
120
|
@deployment_router.api_route(
|
121
121
|
"/ui/{path:path}",
|
122
122
|
methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH"],
|
123
|
+
include_in_schema=False,
|
123
124
|
)
|
124
125
|
@deployment_router.api_route(
|
125
126
|
"/ui",
|
126
127
|
methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH"],
|
128
|
+
include_in_schema=False,
|
127
129
|
)
|
128
130
|
async def proxy(
|
129
131
|
request: Request,
|
@@ -197,17 +199,17 @@ def create_ui_proxy_router(name: str, port: int) -> APIRouter:
|
|
197
199
|
def mount_static_files(
|
198
200
|
app: FastAPI, config: DeploymentConfig, settings: ApiserverSettings
|
199
201
|
) -> None:
|
200
|
-
|
202
|
+
path = settings.app_root / config.build_output_path()
|
203
|
+
if not path:
|
201
204
|
return
|
202
205
|
|
203
|
-
|
204
|
-
if not ui_path.exists():
|
206
|
+
if not path.exists():
|
205
207
|
return
|
206
208
|
|
207
209
|
# Serve index.html when accessing the directory path
|
208
210
|
app.mount(
|
209
211
|
f"/deployments/{config.name}/ui",
|
210
|
-
StaticFiles(directory=str(
|
212
|
+
StaticFiles(directory=str(path), html=True),
|
211
213
|
name=f"ui-static-{config.name}",
|
212
214
|
)
|
213
215
|
return None
|
@@ -2,6 +2,7 @@ import os
|
|
2
2
|
from pathlib import Path
|
3
3
|
|
4
4
|
from llama_deploy.core.config import DEFAULT_DEPLOYMENT_FILE_PATH
|
5
|
+
from llama_deploy.core.deployment_config import resolve_config_parent
|
5
6
|
from pydantic import Field
|
6
7
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
7
8
|
|
@@ -24,7 +25,7 @@ class BootstrapSettings(BaseSettings):
|
|
24
25
|
)
|
25
26
|
git_sha: str | None = Field(default=None, description="The git SHA to checkout")
|
26
27
|
deployment_file_path: str = Field(
|
27
|
-
default="
|
28
|
+
default=".",
|
28
29
|
description="The path to the deployment file, relative to the root of the repository",
|
29
30
|
)
|
30
31
|
deployment_name: str | None = Field(
|
@@ -55,13 +56,17 @@ class ApiserverSettings(BaseSettings):
|
|
55
56
|
|
56
57
|
deployment_file_path: Path = Field(
|
57
58
|
default=Path(DEFAULT_DEPLOYMENT_FILE_PATH),
|
58
|
-
description="path, relative to the repository root, where the
|
59
|
+
description="path, relative to the repository root, where the pyproject.toml file is located",
|
59
60
|
)
|
60
61
|
|
61
62
|
proxy_ui: bool = Field(
|
62
63
|
default=False,
|
63
64
|
description="If true, proxy a development UI server instead of serving built assets",
|
64
65
|
)
|
66
|
+
proxy_ui_port: int = Field(
|
67
|
+
default=4502,
|
68
|
+
description="The TCP port where to bind the UI proxy server",
|
69
|
+
)
|
65
70
|
|
66
71
|
reload: bool = Field(
|
67
72
|
default=False,
|
@@ -69,8 +74,8 @@ class ApiserverSettings(BaseSettings):
|
|
69
74
|
)
|
70
75
|
|
71
76
|
@property
|
72
|
-
def
|
73
|
-
return (self.app_root
|
77
|
+
def resolved_config_parent(self) -> Path:
|
78
|
+
return resolve_config_parent(self.app_root, self.deployment_file_path)
|
74
79
|
|
75
80
|
|
76
81
|
settings = ApiserverSettings()
|
@@ -13,9 +13,11 @@ from llama_deploy.appserver.deployment_config_parser import (
|
|
13
13
|
DeploymentConfig,
|
14
14
|
)
|
15
15
|
from llama_deploy.appserver.process_utils import run_process, spawn_process
|
16
|
+
from llama_deploy.appserver.settings import ApiserverSettings, settings
|
16
17
|
from llama_deploy.core.ui_build import ui_build_output_path
|
17
18
|
from packaging.version import InvalidVersion, Version
|
18
19
|
from workflows import Workflow
|
20
|
+
from workflows.server import WorkflowServer
|
19
21
|
|
20
22
|
logger = logging.getLogger(__name__)
|
21
23
|
|
@@ -27,44 +29,28 @@ def load_workflows(config: DeploymentConfig) -> dict[str, Workflow]:
|
|
27
29
|
Creates WorkflowService instances according to the configuration object.
|
28
30
|
|
29
31
|
"""
|
30
|
-
workflow_services = {}
|
31
|
-
|
32
|
-
# Pre-compute per-service import info
|
33
|
-
per_service: list[tuple[str, str]] = []
|
34
|
-
for service_id, service_config in config.services.items():
|
35
|
-
if service_config.import_path is None:
|
36
|
-
continue
|
37
|
-
raw_mod_path, workflow_name = service_config.import_path.split(":", 1)
|
38
|
-
module_name = Path(raw_mod_path).name
|
39
|
-
per_service.append((service_id, workflow_name))
|
40
|
-
|
41
|
-
for service_id, workflow_name in per_service:
|
42
|
-
import_path = config.services[service_id].import_path
|
43
|
-
if import_path is None:
|
44
|
-
continue
|
45
|
-
raw_mod_path = import_path.split(":", 1)[0]
|
46
|
-
module_name = Path(raw_mod_path).name
|
32
|
+
workflow_services: dict[str, Workflow] = {}
|
47
33
|
|
34
|
+
if config.app:
|
35
|
+
module_name, app_name = config.app.split(":", 1)
|
48
36
|
module = importlib.import_module(module_name)
|
49
|
-
|
50
|
-
if
|
37
|
+
workflow = getattr(module, app_name)
|
38
|
+
if not isinstance(workflow, WorkflowServer):
|
39
|
+
raise ValueError(
|
40
|
+
f"Workflow {app_name} in {module_name} is not a WorkflowServer object"
|
41
|
+
)
|
42
|
+
# kludge to get the workflows
|
43
|
+
workflow_services = workflow._workflows
|
44
|
+
else:
|
45
|
+
for service_id, workflow_name in config.workflows.items():
|
46
|
+
module_name, workflow_name = workflow_name.split(":", 1)
|
47
|
+
module = importlib.import_module(module_name)
|
51
48
|
workflow = getattr(module, workflow_name)
|
52
49
|
if not isinstance(workflow, Workflow):
|
53
50
|
logger.warning(
|
54
51
|
f"Workflow {workflow_name} in {module_name} is not a Workflow object",
|
55
52
|
)
|
56
53
|
workflow_services[service_id] = workflow
|
57
|
-
else:
|
58
|
-
logger.warning("Workflow %s not found in %s", workflow_name, module_name)
|
59
|
-
|
60
|
-
if config.default_service:
|
61
|
-
if config.default_service in workflow_services:
|
62
|
-
workflow_services[DEFAULT_SERVICE_ID] = workflow_services[
|
63
|
-
config.default_service
|
64
|
-
]
|
65
|
-
else:
|
66
|
-
msg = f"Service with id '{config.default_service}' does not exist, cannot set it as default."
|
67
|
-
logger.warning(msg)
|
68
54
|
|
69
55
|
return workflow_services
|
70
56
|
|
@@ -73,15 +59,14 @@ def load_environment_variables(config: DeploymentConfig, source_root: Path) -> N
|
|
73
59
|
"""
|
74
60
|
Load environment variables from the deployment config.
|
75
61
|
"""
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
os.environ[key] = value
|
62
|
+
env_vars = {**config.env} if config.env else {}
|
63
|
+
for env_file in config.env_files or []:
|
64
|
+
env_file_path = source_root / env_file
|
65
|
+
values = dotenv_values(env_file_path)
|
66
|
+
env_vars.update(**values)
|
67
|
+
for key, value in env_vars.items():
|
68
|
+
if value:
|
69
|
+
os.environ[key] = value
|
85
70
|
|
86
71
|
|
87
72
|
@functools.cache
|
@@ -116,12 +101,7 @@ def inject_appserver_into_target(
|
|
116
101
|
source_root: The root directory of the deployment
|
117
102
|
sdists: A list of tar.gz sdists files to install instead of installing the appserver
|
118
103
|
"""
|
119
|
-
path =
|
120
|
-
if path is None:
|
121
|
-
logger.warning(
|
122
|
-
"No python_dependencies and no root pyproject.toml; skipping dependency installation."
|
123
|
-
)
|
124
|
-
return
|
104
|
+
path = settings.resolved_config_parent
|
125
105
|
logger.info(f"Installing ensuring venv at {path} and adding appserver to it")
|
126
106
|
_ensure_uv_available()
|
127
107
|
_add_appserver_if_missing(path, source_root, sdists=sdists)
|
@@ -178,69 +158,61 @@ def _add_appserver_if_missing(
|
|
178
158
|
)
|
179
159
|
return
|
180
160
|
|
181
|
-
def
|
161
|
+
def run_uv(cmd: str, args: list[str]):
|
162
|
+
run_process(
|
163
|
+
["uv", cmd] + args,
|
164
|
+
cwd=source_root / path,
|
165
|
+
prefix=f"[uv {cmd}]",
|
166
|
+
color_code="36",
|
167
|
+
use_tty=False,
|
168
|
+
line_transform=_exclude_venv_warning,
|
169
|
+
)
|
170
|
+
|
171
|
+
def ensure_venv(path: Path, force: bool = False) -> Path:
|
182
172
|
venv_path = source_root / path / ".venv"
|
183
|
-
if not venv_path.exists():
|
184
|
-
|
185
|
-
["uv", "venv", str(venv_path)],
|
186
|
-
cwd=source_root / path,
|
187
|
-
prefix="[uv venv]",
|
188
|
-
color_code="36",
|
189
|
-
)
|
173
|
+
if force or not venv_path.exists():
|
174
|
+
run_uv("venv", [str(venv_path)])
|
190
175
|
return venv_path
|
191
176
|
|
192
177
|
if sdists:
|
193
|
-
|
194
|
-
|
178
|
+
run_uv(
|
179
|
+
"pip",
|
180
|
+
["install"]
|
195
181
|
+ [str(s.absolute()) for s in sdists]
|
196
182
|
+ ["--prefix", str(ensure_venv(path))],
|
197
|
-
cwd=source_root / path,
|
198
|
-
prefix="[uv pip install]",
|
199
|
-
color_code="36",
|
200
183
|
)
|
201
184
|
elif are_we_editable_mode():
|
202
185
|
pyproject = _find_development_pyproject()
|
203
186
|
if pyproject is None:
|
204
187
|
raise RuntimeError("No pyproject.toml found in llama-deploy-appserver")
|
205
188
|
target = f"file://{str(pyproject.relative_to(source_root.resolve() / path, walk_up=True))}"
|
206
|
-
|
189
|
+
|
190
|
+
run_uv(
|
191
|
+
"pip",
|
207
192
|
[
|
208
|
-
"uv",
|
209
|
-
"pip",
|
210
193
|
"install",
|
211
|
-
"--reinstall",
|
194
|
+
"--reinstall-package",
|
195
|
+
"llama-deploy-appserver",
|
212
196
|
target,
|
213
197
|
"--prefix",
|
214
|
-
str(ensure_venv(path)),
|
198
|
+
str(ensure_venv(path, force=True)),
|
215
199
|
],
|
216
|
-
cwd=source_root / path,
|
217
|
-
prefix="[uv pip install]",
|
218
|
-
color_code="36",
|
219
200
|
)
|
201
|
+
|
220
202
|
else:
|
221
203
|
version = _is_missing_or_outdated(path)
|
222
204
|
if version is not None:
|
223
205
|
if save_version:
|
224
|
-
|
225
|
-
["uv", "add", f"llama-deploy-appserver>={version}"],
|
226
|
-
cwd=source_root / path,
|
227
|
-
prefix="[uv add]",
|
228
|
-
color_code="36",
|
229
|
-
line_transform=_exclude_venv_warning,
|
230
|
-
)
|
206
|
+
run_uv("add", [f"llama-deploy-appserver>={version}"])
|
231
207
|
else:
|
232
|
-
|
208
|
+
run_uv(
|
209
|
+
"pip",
|
233
210
|
[
|
234
|
-
"uv",
|
235
|
-
"pip",
|
236
211
|
"install",
|
237
212
|
f"llama-deploy-appserver=={version}",
|
238
213
|
"--prefix",
|
239
214
|
str(ensure_venv(path)),
|
240
215
|
],
|
241
|
-
cwd=source_root / path,
|
242
|
-
prefix="[uv pip install]",
|
243
|
-
color_code="36",
|
244
216
|
)
|
245
217
|
|
246
218
|
|
@@ -253,27 +225,6 @@ def _find_development_pyproject() -> Path | None:
|
|
253
225
|
return dir
|
254
226
|
|
255
227
|
|
256
|
-
def find_python_pyproject(base: Path, config: DeploymentConfig) -> Path | None:
|
257
|
-
path: Path | None = None
|
258
|
-
for service_id, service_config in config.services.items():
|
259
|
-
if service_config.python_dependencies:
|
260
|
-
if len(service_config.python_dependencies) > 1:
|
261
|
-
logger.warning(
|
262
|
-
"Llama Deploy now only supports installing from a single pyproject.toml path"
|
263
|
-
)
|
264
|
-
this_path = Path(service_config.python_dependencies[0])
|
265
|
-
if path is not None and this_path != path:
|
266
|
-
logger.warning(
|
267
|
-
f"Llama Deploy now only supports installing from a single pyproject.toml path, ignoring {this_path}"
|
268
|
-
)
|
269
|
-
else:
|
270
|
-
path = this_path
|
271
|
-
if path is None:
|
272
|
-
if (base / "pyproject.toml").exists():
|
273
|
-
path = Path(".")
|
274
|
-
return path
|
275
|
-
|
276
|
-
|
277
228
|
def _exclude_venv_warning(line: str) -> str | None:
|
278
229
|
if "use `--active` to target the active environment instead" in line:
|
279
230
|
return None
|
@@ -311,82 +262,56 @@ def _ensure_uv_available() -> None:
|
|
311
262
|
raise RuntimeError(msg)
|
312
263
|
|
313
264
|
|
314
|
-
def _find_install_target(base: Path, config: DeploymentConfig) -> Path | None:
|
315
|
-
return find_python_pyproject(base, config)
|
316
|
-
|
317
|
-
|
318
|
-
def _validate_path_is_safe(
|
319
|
-
path: Path, source_root: Path, path_type: str = "path"
|
320
|
-
) -> None:
|
321
|
-
"""Validates that a path is within the source root to prevent path traversal attacks.
|
322
|
-
|
323
|
-
Args:
|
324
|
-
path: The path to validate
|
325
|
-
source_root: The root directory that paths should be relative to
|
326
|
-
path_type: Description of the path type for error messages
|
327
|
-
|
328
|
-
Raises:
|
329
|
-
DeploymentError: If the path is outside the source root
|
330
|
-
"""
|
331
|
-
resolved_path = (source_root / path).resolve()
|
332
|
-
resolved_source_root = source_root.resolve()
|
333
|
-
|
334
|
-
if not resolved_path.is_relative_to(resolved_source_root):
|
335
|
-
msg = (
|
336
|
-
f"{path_type} {path} is not a subdirectory of the source root {source_root}"
|
337
|
-
)
|
338
|
-
raise RuntimeError(msg)
|
339
|
-
|
340
|
-
|
341
265
|
def install_ui(config: DeploymentConfig, config_parent: Path) -> None:
|
342
266
|
if config.ui is None:
|
343
267
|
return
|
344
|
-
|
345
|
-
_validate_path_is_safe(path, config_parent, "ui_source")
|
268
|
+
package_manager = config.ui.package_manager
|
346
269
|
run_process(
|
347
|
-
[
|
348
|
-
cwd=config_parent /
|
349
|
-
prefix="[
|
270
|
+
[package_manager, "install"],
|
271
|
+
cwd=config_parent / config.ui.directory,
|
272
|
+
prefix=f"[{package_manager} install]",
|
350
273
|
color_code="33",
|
351
274
|
)
|
352
275
|
|
353
276
|
|
354
|
-
def _ui_env(config: DeploymentConfig) -> dict[str, str]:
|
277
|
+
def _ui_env(config: DeploymentConfig, settings: ApiserverSettings) -> dict[str, str]:
|
355
278
|
env = os.environ.copy()
|
356
279
|
env["LLAMA_DEPLOY_DEPLOYMENT_URL_ID"] = config.name
|
357
280
|
env["LLAMA_DEPLOY_DEPLOYMENT_BASE_PATH"] = f"/deployments/{config.name}/ui"
|
358
281
|
if config.ui is not None:
|
359
|
-
env["PORT"] = str(
|
282
|
+
env["PORT"] = str(settings.proxy_ui_port)
|
360
283
|
return env
|
361
284
|
|
362
285
|
|
363
|
-
def build_ui(
|
286
|
+
def build_ui(
|
287
|
+
config_parent: Path, config: DeploymentConfig, settings: ApiserverSettings
|
288
|
+
) -> bool:
|
364
289
|
"""
|
365
290
|
Returns True if the UI was built (and supports building), otherwise False if there's no build command
|
366
291
|
"""
|
367
292
|
if config.ui is None:
|
368
293
|
return False
|
369
|
-
path = Path(config.ui.
|
370
|
-
|
371
|
-
env = _ui_env(config)
|
294
|
+
path = Path(config.ui.directory)
|
295
|
+
env = _ui_env(config, settings)
|
372
296
|
|
373
297
|
has_build = ui_build_output_path(config_parent, config)
|
374
298
|
if has_build is None:
|
375
299
|
return False
|
376
300
|
|
377
301
|
run_process(
|
378
|
-
["
|
302
|
+
["npm", "run", "build"],
|
379
303
|
cwd=config_parent / path,
|
380
304
|
env=env,
|
381
|
-
prefix="[
|
305
|
+
prefix="[npm run build]",
|
382
306
|
color_code="34",
|
383
307
|
)
|
384
308
|
return True
|
385
309
|
|
386
310
|
|
387
311
|
def start_dev_ui_process(
|
388
|
-
root: Path,
|
312
|
+
root: Path, settings: ApiserverSettings, config: DeploymentConfig
|
389
313
|
) -> None | subprocess.Popen:
|
314
|
+
ui_port = settings.proxy_ui_port
|
390
315
|
ui = config.ui
|
391
316
|
if ui is None:
|
392
317
|
return None
|
@@ -400,29 +325,29 @@ def start_dev_ui_process(
|
|
400
325
|
except Exception:
|
401
326
|
return False
|
402
327
|
|
403
|
-
if _is_port_open(
|
328
|
+
if _is_port_open(ui_port):
|
404
329
|
logger.info(
|
405
|
-
"Detected process already running on port
|
406
|
-
ui.port,
|
330
|
+
f"Detected process already running on port {ui_port}; not starting a new one."
|
407
331
|
)
|
408
332
|
return None
|
409
333
|
# start the ui process
|
410
|
-
env = _ui_env(config)
|
334
|
+
env = _ui_env(config, settings)
|
411
335
|
# Transform first 20 lines to replace the default UI port with the main server port
|
412
336
|
line_counter = 0
|
413
337
|
|
414
338
|
def _transform(line: str) -> str:
|
415
339
|
nonlocal line_counter
|
416
340
|
if line_counter < 20:
|
417
|
-
line = line.replace(f":{
|
341
|
+
line = line.replace(f":{ui_port}", f":{settings.port}")
|
418
342
|
line_counter += 1
|
419
343
|
return line
|
420
344
|
|
421
345
|
return spawn_process(
|
422
|
-
["
|
423
|
-
cwd=root / (ui.
|
346
|
+
["npm", "run", ui.serve_command],
|
347
|
+
cwd=root / (ui.directory),
|
424
348
|
env=env,
|
425
|
-
prefix="[
|
426
|
-
color_code="35",
|
349
|
+
prefix=f"[npm run {ui.serve_command}]",
|
350
|
+
color_code="35",
|
427
351
|
line_transform=_transform,
|
352
|
+
use_tty=False,
|
428
353
|
)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|