llama-deploy-appserver 0.3.23__tar.gz → 0.3.25__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.23 → llama_deploy_appserver-0.3.25}/PKG-INFO +5 -4
- llama_deploy_appserver-0.3.25/pyproject.toml +48 -0
- {llama_deploy_appserver-0.3.23 → llama_deploy_appserver-0.3.25}/src/llama_deploy/appserver/app.py +95 -12
- {llama_deploy_appserver-0.3.23 → llama_deploy_appserver-0.3.25}/src/llama_deploy/appserver/bootstrap.py +1 -1
- {llama_deploy_appserver-0.3.23 → llama_deploy_appserver-0.3.25}/src/llama_deploy/appserver/configure_logging.py +3 -3
- {llama_deploy_appserver-0.3.23 → llama_deploy_appserver-0.3.25}/src/llama_deploy/appserver/deployment.py +9 -12
- {llama_deploy_appserver-0.3.23 → llama_deploy_appserver-0.3.25}/src/llama_deploy/appserver/interrupts.py +3 -3
- {llama_deploy_appserver-0.3.23 → llama_deploy_appserver-0.3.25}/src/llama_deploy/appserver/process_utils.py +7 -4
- {llama_deploy_appserver-0.3.23 → llama_deploy_appserver-0.3.25}/src/llama_deploy/appserver/routers/ui_proxy.py +9 -6
- {llama_deploy_appserver-0.3.23 → llama_deploy_appserver-0.3.25}/src/llama_deploy/appserver/workflow_loader.py +41 -26
- {llama_deploy_appserver-0.3.23 → llama_deploy_appserver-0.3.25}/src/llama_deploy/appserver/workflow_store/agent_data_store.py +14 -6
- {llama_deploy_appserver-0.3.23 → llama_deploy_appserver-0.3.25}/src/llama_deploy/appserver/workflow_store/keyed_lock.py +4 -3
- {llama_deploy_appserver-0.3.23 → llama_deploy_appserver-0.3.25}/src/llama_deploy/appserver/workflow_store/lru_cache.py +4 -3
- llama_deploy_appserver-0.3.23/pyproject.toml +0 -48
- {llama_deploy_appserver-0.3.23 → llama_deploy_appserver-0.3.25}/README.md +0 -0
- {llama_deploy_appserver-0.3.23 → llama_deploy_appserver-0.3.25}/src/llama_deploy/appserver/__init__.py +0 -0
- {llama_deploy_appserver-0.3.23 → llama_deploy_appserver-0.3.25}/src/llama_deploy/appserver/correlation_id.py +0 -0
- {llama_deploy_appserver-0.3.23 → llama_deploy_appserver-0.3.25}/src/llama_deploy/appserver/deployment_config_parser.py +0 -0
- {llama_deploy_appserver-0.3.23 → llama_deploy_appserver-0.3.25}/src/llama_deploy/appserver/py.typed +0 -0
- {llama_deploy_appserver-0.3.23 → llama_deploy_appserver-0.3.25}/src/llama_deploy/appserver/routers/__init__.py +0 -0
- {llama_deploy_appserver-0.3.23 → llama_deploy_appserver-0.3.25}/src/llama_deploy/appserver/routers/deployments.py +0 -0
- {llama_deploy_appserver-0.3.23 → llama_deploy_appserver-0.3.25}/src/llama_deploy/appserver/routers/status.py +0 -0
- {llama_deploy_appserver-0.3.23 → llama_deploy_appserver-0.3.25}/src/llama_deploy/appserver/settings.py +0 -0
- {llama_deploy_appserver-0.3.23 → llama_deploy_appserver-0.3.25}/src/llama_deploy/appserver/stats.py +0 -0
- {llama_deploy_appserver-0.3.23 → llama_deploy_appserver-0.3.25}/src/llama_deploy/appserver/types.py +0 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: llama-deploy-appserver
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.25
|
|
4
4
|
Summary: Application server components for LlamaDeploy
|
|
5
5
|
Author: Massimiliano Pippi, Adrian Lyjak
|
|
6
6
|
Author-email: Massimiliano Pippi <mpippi@gmail.com>, Adrian Lyjak <adrianlyjak@gmail.com>
|
|
7
7
|
License: MIT
|
|
8
|
-
Requires-Dist: llama-index-workflows[server]>=2.
|
|
8
|
+
Requires-Dist: llama-index-workflows[server]>=2.11.3
|
|
9
9
|
Requires-Dist: pydantic-settings>=2.10.1
|
|
10
10
|
Requires-Dist: fastapi>=0.100.0
|
|
11
11
|
Requires-Dist: websockets>=12.0
|
|
12
|
-
Requires-Dist: llama-deploy-core>=0.3.
|
|
12
|
+
Requires-Dist: llama-deploy-core>=0.3.25,<0.4.0
|
|
13
13
|
Requires-Dist: httpx>=0.24.0,<1.0.0
|
|
14
14
|
Requires-Dist: prometheus-fastapi-instrumentator>=7.1.0
|
|
15
15
|
Requires-Dist: packaging>=25.0
|
|
@@ -19,7 +19,8 @@ Requires-Dist: pyyaml>=6.0.2
|
|
|
19
19
|
Requires-Dist: llama-cloud-services>=0.6.60
|
|
20
20
|
Requires-Dist: watchfiles>=1.1.0
|
|
21
21
|
Requires-Dist: uvicorn>=0.35.0
|
|
22
|
-
Requires-
|
|
22
|
+
Requires-Dist: typing-extensions>=4.15.0 ; python_full_version < '3.12'
|
|
23
|
+
Requires-Python: >=3.10, <4
|
|
23
24
|
Description-Content-Type: text/markdown
|
|
24
25
|
|
|
25
26
|
# llama-deploy-appserver
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.7.20,<0.8.0"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[dependency-groups]
|
|
6
|
+
dev = [
|
|
7
|
+
"pytest>=8.4.1",
|
|
8
|
+
"pytest-asyncio>=0.25.3",
|
|
9
|
+
"respx>=0.22.0",
|
|
10
|
+
"pytest-xdist>=3.8.0",
|
|
11
|
+
"ty>=0.0.1a19",
|
|
12
|
+
"ruff>=0.12.9"
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project]
|
|
16
|
+
name = "llama-deploy-appserver"
|
|
17
|
+
version = "0.3.25"
|
|
18
|
+
description = "Application server components for LlamaDeploy"
|
|
19
|
+
readme = "README.md"
|
|
20
|
+
license = {text = "MIT"}
|
|
21
|
+
authors = [
|
|
22
|
+
{name = "Massimiliano Pippi", email = "mpippi@gmail.com"},
|
|
23
|
+
{name = "Adrian Lyjak", email = "adrianlyjak@gmail.com"}
|
|
24
|
+
]
|
|
25
|
+
requires-python = ">=3.10, <4"
|
|
26
|
+
dependencies = [
|
|
27
|
+
"llama-index-workflows[server]>=2.11.3",
|
|
28
|
+
"pydantic-settings>=2.10.1",
|
|
29
|
+
"fastapi>=0.100.0",
|
|
30
|
+
"websockets>=12.0",
|
|
31
|
+
"llama-deploy-core>=0.3.25,<0.4.0",
|
|
32
|
+
"httpx>=0.24.0,<1.0.0",
|
|
33
|
+
"prometheus-fastapi-instrumentator>=7.1.0",
|
|
34
|
+
"packaging>=25.0",
|
|
35
|
+
"structlog>=25.4.0",
|
|
36
|
+
"rich>=14.1.0",
|
|
37
|
+
"pyyaml>=6.0.2",
|
|
38
|
+
"llama-cloud-services>=0.6.60",
|
|
39
|
+
"watchfiles>=1.1.0",
|
|
40
|
+
"uvicorn>=0.35.0",
|
|
41
|
+
"typing-extensions>=4.15.0 ; python_full_version < '3.12'"
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[tool.uv.build-backend]
|
|
45
|
+
module-name = "llama_deploy.appserver"
|
|
46
|
+
|
|
47
|
+
[tool.uv.sources]
|
|
48
|
+
llama-deploy-core = {workspace = true}
|
{llama_deploy_appserver-0.3.23 → llama_deploy_appserver-0.3.25}/src/llama_deploy/appserver/app.py
RENAMED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import argparse
|
|
2
|
+
import json
|
|
2
3
|
import logging
|
|
3
4
|
import os
|
|
4
5
|
import threading
|
|
@@ -122,7 +123,7 @@ def _setup_openapi(name: str, app: FastAPI, server: WorkflowServer) -> None:
|
|
|
122
123
|
|
|
123
124
|
schema["paths"] = new_paths
|
|
124
125
|
|
|
125
|
-
def custom_openapi():
|
|
126
|
+
def custom_openapi() -> dict[str, object]:
|
|
126
127
|
return schema
|
|
127
128
|
|
|
128
129
|
app.openapi = custom_openapi # ty: ignore[invalid-assignment] - doesn't like us overwriting the method
|
|
@@ -139,16 +140,22 @@ app = FastAPI(
|
|
|
139
140
|
Instrumentator().instrument(app).expose(app, include_in_schema=False)
|
|
140
141
|
|
|
141
142
|
|
|
142
|
-
|
|
143
|
-
|
|
143
|
+
def _configure_cors(app: FastAPI) -> None:
|
|
144
|
+
"""Attach CORS middleware in a way that keeps type-checkers happy."""
|
|
145
|
+
# Use a cast here because ty's view of Starlette's middleware factory
|
|
146
|
+
# protocol is stricter than FastAPI's runtime expectations.
|
|
144
147
|
app.add_middleware(
|
|
145
|
-
CORSMiddleware,
|
|
148
|
+
cast(Any, CORSMiddleware),
|
|
146
149
|
allow_origins=["*"], # Allows all origins
|
|
147
150
|
allow_credentials=True,
|
|
148
151
|
allow_methods=["GET", "POST"],
|
|
149
152
|
allow_headers=["Content-Type", "Authorization"],
|
|
150
153
|
)
|
|
151
154
|
|
|
155
|
+
|
|
156
|
+
if not os.environ.get("DISABLE_CORS", False):
|
|
157
|
+
_configure_cors(app)
|
|
158
|
+
|
|
152
159
|
app.include_router(health_router)
|
|
153
160
|
add_log_middleware(app)
|
|
154
161
|
|
|
@@ -280,16 +287,13 @@ def start_server_in_target_venv(
|
|
|
280
287
|
if log_format:
|
|
281
288
|
env["LOG_FORMAT"] = log_format
|
|
282
289
|
|
|
283
|
-
|
|
290
|
+
run_process(
|
|
284
291
|
args,
|
|
285
292
|
cwd=path,
|
|
286
293
|
env=env,
|
|
287
294
|
line_transform=_exclude_venv_warning,
|
|
288
295
|
)
|
|
289
296
|
|
|
290
|
-
if ret != 0:
|
|
291
|
-
raise SystemExit(ret)
|
|
292
|
-
|
|
293
297
|
|
|
294
298
|
def start_preflight_in_target_venv(
|
|
295
299
|
cwd: Path | None = None,
|
|
@@ -297,7 +301,7 @@ def start_preflight_in_target_venv(
|
|
|
297
301
|
) -> None:
|
|
298
302
|
"""
|
|
299
303
|
Run preflight validation inside the target project's virtual environment using uv.
|
|
300
|
-
Mirrors the venv
|
|
304
|
+
Mirrors the venv targeting and invocation strategy used by start_server_in_target_venv.
|
|
301
305
|
"""
|
|
302
306
|
configure_settings(
|
|
303
307
|
app_root=cwd,
|
|
@@ -317,14 +321,51 @@ def start_preflight_in_target_venv(
|
|
|
317
321
|
if deployment_file:
|
|
318
322
|
args.extend(["--deployment-file", str(deployment_file)])
|
|
319
323
|
|
|
320
|
-
|
|
324
|
+
run_process(
|
|
325
|
+
args,
|
|
326
|
+
cwd=path,
|
|
327
|
+
env=os.environ.copy(),
|
|
328
|
+
line_transform=_exclude_venv_warning,
|
|
329
|
+
)
|
|
330
|
+
# Note: run_process doesn't return exit code; process runs to completion or raises
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def start_export_json_graph_in_target_venv(
|
|
334
|
+
cwd: Path | None = None,
|
|
335
|
+
deployment_file: Path | None = None,
|
|
336
|
+
output: Path | None = None,
|
|
337
|
+
) -> None:
|
|
338
|
+
"""
|
|
339
|
+
Run workflow graph export inside the target project's virtual environment using uv.
|
|
340
|
+
Mirrors the venv targeting and invocation strategy used by start_preflight_in_target_venv.
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
configure_settings(
|
|
344
|
+
app_root=cwd,
|
|
345
|
+
deployment_file_path=deployment_file or Path(DEFAULT_DEPLOYMENT_FILE_PATH),
|
|
346
|
+
)
|
|
347
|
+
base_dir = cwd or Path.cwd()
|
|
348
|
+
path = settings.resolved_config_parent.relative_to(base_dir)
|
|
349
|
+
args = [
|
|
350
|
+
"uv",
|
|
351
|
+
"run",
|
|
352
|
+
"--no-progress",
|
|
353
|
+
"python",
|
|
354
|
+
"-m",
|
|
355
|
+
"llama_deploy.appserver.app",
|
|
356
|
+
"--export-json-graph",
|
|
357
|
+
]
|
|
358
|
+
if deployment_file:
|
|
359
|
+
args.extend(["--deployment-file", str(deployment_file)])
|
|
360
|
+
if output is not None:
|
|
361
|
+
args.extend(["--export-output", str(output)])
|
|
362
|
+
|
|
363
|
+
run_process(
|
|
321
364
|
args,
|
|
322
365
|
cwd=path,
|
|
323
366
|
env=os.environ.copy(),
|
|
324
367
|
line_transform=_exclude_venv_warning,
|
|
325
368
|
)
|
|
326
|
-
if ret is not None and ret != 0:
|
|
327
|
-
raise SystemExit(ret)
|
|
328
369
|
|
|
329
370
|
|
|
330
371
|
class PreflightValidationError(Exception):
|
|
@@ -374,6 +415,40 @@ def preflight_validate(
|
|
|
374
415
|
raise PreflightValidationError(errors)
|
|
375
416
|
|
|
376
417
|
|
|
418
|
+
def export_json_graph(
|
|
419
|
+
cwd: Path | None = None,
|
|
420
|
+
deployment_file: Path | None = None,
|
|
421
|
+
output: Path | None = None,
|
|
422
|
+
) -> None:
|
|
423
|
+
"""
|
|
424
|
+
Export a JSON representation of the registered workflows' graph.
|
|
425
|
+
|
|
426
|
+
This follows the same initialization path as preflight validation and writes
|
|
427
|
+
a workflows.json-style structure compatible with the CLI expectations.
|
|
428
|
+
"""
|
|
429
|
+
from workflows.representation_utils import extract_workflow_structure
|
|
430
|
+
|
|
431
|
+
configure_settings(
|
|
432
|
+
app_root=cwd,
|
|
433
|
+
deployment_file_path=deployment_file or Path(DEFAULT_DEPLOYMENT_FILE_PATH),
|
|
434
|
+
)
|
|
435
|
+
cfg = get_deployment_config()
|
|
436
|
+
load_environment_variables(cfg, settings.resolved_config_parent)
|
|
437
|
+
|
|
438
|
+
workflows = load_workflows(cfg)
|
|
439
|
+
|
|
440
|
+
graph: dict[str, dict[str, Any]] = {}
|
|
441
|
+
for name, workflow in workflows.items():
|
|
442
|
+
wf_repr_dict = (
|
|
443
|
+
extract_workflow_structure(workflow).to_response_model().model_dump()
|
|
444
|
+
)
|
|
445
|
+
graph[name] = wf_repr_dict
|
|
446
|
+
|
|
447
|
+
output_path = output or (Path.cwd() / "workflows.json")
|
|
448
|
+
with output_path.open("w", encoding="utf-8") as f:
|
|
449
|
+
json.dump(graph, f, indent=2)
|
|
450
|
+
|
|
451
|
+
|
|
377
452
|
if __name__ == "__main__":
|
|
378
453
|
parser = argparse.ArgumentParser()
|
|
379
454
|
parser.add_argument("--proxy-ui", action="store_true")
|
|
@@ -381,10 +456,18 @@ if __name__ == "__main__":
|
|
|
381
456
|
parser.add_argument("--deployment-file", type=Path)
|
|
382
457
|
parser.add_argument("--open-browser", action="store_true")
|
|
383
458
|
parser.add_argument("--preflight", action="store_true")
|
|
459
|
+
parser.add_argument("--export-json-graph", action="store_true")
|
|
460
|
+
parser.add_argument("--export-output", type=Path)
|
|
384
461
|
|
|
385
462
|
args = parser.parse_args()
|
|
386
463
|
if args.preflight:
|
|
387
464
|
preflight_validate(cwd=Path.cwd(), deployment_file=args.deployment_file)
|
|
465
|
+
elif args.export_json_graph:
|
|
466
|
+
export_json_graph(
|
|
467
|
+
cwd=Path.cwd(),
|
|
468
|
+
deployment_file=args.deployment_file,
|
|
469
|
+
output=args.export_output,
|
|
470
|
+
)
|
|
388
471
|
else:
|
|
389
472
|
start_server(
|
|
390
473
|
proxy_ui=args.proxy_ui,
|
|
@@ -27,7 +27,7 @@ from llama_deploy.core.git.git_util import (
|
|
|
27
27
|
|
|
28
28
|
def bootstrap_app_from_repo(
|
|
29
29
|
target_dir: str = "/opt/app",
|
|
30
|
-
):
|
|
30
|
+
) -> None:
|
|
31
31
|
bootstrap_settings = BootstrapSettings()
|
|
32
32
|
# Needs the github url+auth, and the deployment file path
|
|
33
33
|
# clones the repo to a standard directory
|
|
@@ -22,18 +22,18 @@ def _get_or_create_correlation_id(request: Request) -> str:
|
|
|
22
22
|
return request.headers.get("X-Request-ID", create_correlation_id())
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
def add_log_middleware(app: FastAPI):
|
|
25
|
+
def add_log_middleware(app: FastAPI) -> None:
|
|
26
26
|
@app.middleware("http")
|
|
27
27
|
async def add_log_id(
|
|
28
28
|
request: Request, call_next: Callable[[Request], Awaitable[Response]]
|
|
29
|
-
):
|
|
29
|
+
) -> Response:
|
|
30
30
|
set_correlation_id(_get_or_create_correlation_id(request))
|
|
31
31
|
return await call_next(request)
|
|
32
32
|
|
|
33
33
|
@app.middleware("http")
|
|
34
34
|
async def access_log_middleware(
|
|
35
35
|
request: Request, call_next: Callable[[Request], Awaitable[Response]]
|
|
36
|
-
):
|
|
36
|
+
) -> Response:
|
|
37
37
|
if _is_proxy_request(request):
|
|
38
38
|
return await call_next(request)
|
|
39
39
|
start = time.perf_counter()
|
|
@@ -18,7 +18,7 @@ from starlette.responses import HTMLResponse
|
|
|
18
18
|
from starlette.routing import Route
|
|
19
19
|
from workflows import Context, Workflow
|
|
20
20
|
from workflows.handler import WorkflowHandler
|
|
21
|
-
from workflows.server import SqliteWorkflowStore, WorkflowServer
|
|
21
|
+
from workflows.server import AbstractWorkflowStore, SqliteWorkflowStore, WorkflowServer
|
|
22
22
|
from workflows.server.memory_workflow_store import MemoryWorkflowStore
|
|
23
23
|
|
|
24
24
|
logger = logging.getLogger()
|
|
@@ -40,7 +40,7 @@ class Deployment:
|
|
|
40
40
|
local: Whether the deployment is local. If true, sources won't be synced
|
|
41
41
|
"""
|
|
42
42
|
|
|
43
|
-
self._default_service:
|
|
43
|
+
self._default_service: Workflow | None = workflows.get(DEFAULT_SERVICE_ID)
|
|
44
44
|
self._service_tasks: list[asyncio.Task] = []
|
|
45
45
|
# Ready to load services
|
|
46
46
|
self._workflow_services: dict[str, Workflow] = workflows
|
|
@@ -50,13 +50,9 @@ class Deployment:
|
|
|
50
50
|
|
|
51
51
|
@property
|
|
52
52
|
def default_service(self) -> Workflow | None:
|
|
53
|
+
"""Return the default workflow, if any."""
|
|
53
54
|
return self._default_service
|
|
54
55
|
|
|
55
|
-
@property
|
|
56
|
-
def name(self) -> str:
|
|
57
|
-
"""Returns the name of this deployment."""
|
|
58
|
-
return self._name
|
|
59
|
-
|
|
60
56
|
@property
|
|
61
57
|
def service_names(self) -> list[str]:
|
|
62
58
|
"""Returns the list of service names in this deployment."""
|
|
@@ -66,7 +62,7 @@ class Deployment:
|
|
|
66
62
|
self, service_id: str, session_id: str | None = None, **run_kwargs: dict
|
|
67
63
|
) -> Any:
|
|
68
64
|
workflow = self._workflow_services[service_id]
|
|
69
|
-
if session_id:
|
|
65
|
+
if session_id is not None:
|
|
70
66
|
context = self._contexts[session_id]
|
|
71
67
|
return await workflow.run(context=context, **run_kwargs)
|
|
72
68
|
|
|
@@ -79,7 +75,7 @@ class Deployment:
|
|
|
79
75
|
self, service_id: str, session_id: str | None = None, **run_kwargs: dict
|
|
80
76
|
) -> Tuple[str, str]:
|
|
81
77
|
workflow = self._workflow_services[service_id]
|
|
82
|
-
if session_id:
|
|
78
|
+
if session_id is not None:
|
|
83
79
|
context = self._contexts[session_id]
|
|
84
80
|
handler = workflow.run(context=context, **run_kwargs)
|
|
85
81
|
else:
|
|
@@ -90,12 +86,13 @@ class Deployment:
|
|
|
90
86
|
handler_id = generate_id()
|
|
91
87
|
self._handlers[handler_id] = handler
|
|
92
88
|
self._handler_inputs[handler_id] = json.dumps(run_kwargs)
|
|
89
|
+
assert session_id is not None
|
|
93
90
|
return handler_id, session_id
|
|
94
91
|
|
|
95
92
|
def create_workflow_server(
|
|
96
93
|
self, deployment_config: DeploymentConfig, settings: ApiserverSettings
|
|
97
94
|
) -> WorkflowServer:
|
|
98
|
-
persistence = MemoryWorkflowStore()
|
|
95
|
+
persistence: AbstractWorkflowStore = MemoryWorkflowStore()
|
|
99
96
|
if settings.persistence == "local":
|
|
100
97
|
logger.info("Using local sqlite persistence for workflows")
|
|
101
98
|
persistence = SqliteWorkflowStore(
|
|
@@ -137,8 +134,8 @@ class Deployment:
|
|
|
137
134
|
"/debugger/index.html?api=" + quote_plus("/deployments/" + config.name)
|
|
138
135
|
)
|
|
139
136
|
|
|
140
|
-
@app.get("/debugger/index.html", include_in_schema=False)
|
|
141
|
-
def serve_debugger(api: str | None = None):
|
|
137
|
+
@app.get("/debugger/index.html", include_in_schema=False, response_model=None)
|
|
138
|
+
def serve_debugger(api: str | None = None) -> RedirectResponse | HTMLResponse:
|
|
142
139
|
if not api:
|
|
143
140
|
return RedirectResponse(
|
|
144
141
|
"/debugger/index.html?api="
|
|
@@ -2,7 +2,7 @@ import asyncio
|
|
|
2
2
|
import signal
|
|
3
3
|
from asyncio import Event
|
|
4
4
|
from contextlib import suppress
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import Any, Coroutine, TypeVar
|
|
6
6
|
|
|
7
7
|
shutdown_event = Event()
|
|
8
8
|
|
|
@@ -21,7 +21,7 @@ T = TypeVar("T")
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
async def wait_or_abort(
|
|
24
|
-
awaitable:
|
|
24
|
+
awaitable: Coroutine[Any, Any, T], shutdown_event: asyncio.Event = shutdown_event
|
|
25
25
|
) -> T:
|
|
26
26
|
"""Await an operation, aborting early if shutdown is requested.
|
|
27
27
|
|
|
@@ -32,7 +32,7 @@ async def wait_or_abort(
|
|
|
32
32
|
if event.is_set():
|
|
33
33
|
raise OperationAborted()
|
|
34
34
|
|
|
35
|
-
op_task = asyncio.create_task(awaitable)
|
|
35
|
+
op_task: asyncio.Task[T] = asyncio.create_task(awaitable)
|
|
36
36
|
stop_task = asyncio.create_task(event.wait())
|
|
37
37
|
try:
|
|
38
38
|
done, _ = await asyncio.wait(
|
|
@@ -112,7 +112,7 @@ def should_use_color() -> bool:
|
|
|
112
112
|
|
|
113
113
|
@dataclass
|
|
114
114
|
class SpawnProcessResult:
|
|
115
|
-
process: subprocess.Popen
|
|
115
|
+
process: subprocess.Popen[str] | subprocess.Popen[bytes]
|
|
116
116
|
sources: list[Tuple[int | TextIO, TextIO]]
|
|
117
117
|
cleanup: Callable[[], None]
|
|
118
118
|
|
|
@@ -124,6 +124,7 @@ def _spawn_process(
|
|
|
124
124
|
env: dict[str, str] | None,
|
|
125
125
|
use_pty: bool,
|
|
126
126
|
) -> SpawnProcessResult:
|
|
127
|
+
process: subprocess.Popen[str] | subprocess.Popen[bytes]
|
|
127
128
|
if use_pty:
|
|
128
129
|
import pty
|
|
129
130
|
|
|
@@ -164,7 +165,7 @@ def _spawn_process(
|
|
|
164
165
|
shell=use_shell,
|
|
165
166
|
)
|
|
166
167
|
|
|
167
|
-
def
|
|
168
|
+
def cleanup_non_pty() -> None:
|
|
168
169
|
return None
|
|
169
170
|
|
|
170
171
|
assert process.stdout is not None and process.stderr is not None
|
|
@@ -172,7 +173,7 @@ def _spawn_process(
|
|
|
172
173
|
(cast(int | TextIO, process.stdout), cast(TextIO, sys.stdout)),
|
|
173
174
|
(cast(int | TextIO, process.stderr), cast(TextIO, sys.stderr)),
|
|
174
175
|
]
|
|
175
|
-
return SpawnProcessResult(process, sources,
|
|
176
|
+
return SpawnProcessResult(process, sources, cleanup_non_pty)
|
|
176
177
|
|
|
177
178
|
|
|
178
179
|
def _stream_source(
|
|
@@ -212,7 +213,9 @@ def _stream_source(
|
|
|
212
213
|
def _log_command(cmd: list[str], transform: Callable[[str], str | None] | None) -> None:
|
|
213
214
|
cmd_str = "> " + " ".join(cmd)
|
|
214
215
|
if transform:
|
|
215
|
-
|
|
216
|
+
transformed = transform(cmd_str)
|
|
217
|
+
if transformed is not None:
|
|
218
|
+
cmd_str = transformed
|
|
216
219
|
sys.stderr.write(cmd_str + "\n")
|
|
217
220
|
|
|
218
221
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
|
+
from collections.abc import AsyncGenerator, Sequence
|
|
3
4
|
from contextlib import suppress
|
|
4
|
-
from typing import List
|
|
5
5
|
|
|
6
6
|
import httpx
|
|
7
7
|
import websockets
|
|
@@ -23,6 +23,7 @@ from llama_deploy.appserver.interrupts import (
|
|
|
23
23
|
from llama_deploy.appserver.settings import ApiserverSettings
|
|
24
24
|
from llama_deploy.core.client.ssl_util import get_httpx_verify_param
|
|
25
25
|
from llama_deploy.core.deployment_config import DeploymentConfig
|
|
26
|
+
from websockets.typing import Subprotocol
|
|
26
27
|
|
|
27
28
|
logger = logging.getLogger(__name__)
|
|
28
29
|
|
|
@@ -53,11 +54,12 @@ async def _ws_proxy(ws: WebSocket, upstream_url: str) -> None:
|
|
|
53
54
|
|
|
54
55
|
try:
|
|
55
56
|
# Parse subprotocols if present
|
|
56
|
-
subprotocols:
|
|
57
|
+
subprotocols: Sequence[Subprotocol] | None = None
|
|
57
58
|
requested = ws.headers.get("sec-websocket-protocol")
|
|
58
59
|
if requested:
|
|
59
60
|
# Parse comma-separated subprotocols (as plain strings)
|
|
60
|
-
|
|
61
|
+
parsed = [p.strip() for p in requested.split(",")]
|
|
62
|
+
subprotocols = [Subprotocol(p) for p in parsed if p]
|
|
61
63
|
|
|
62
64
|
# Open upstream WebSocket connection, offering the same subprotocols
|
|
63
65
|
async with websockets.connect(
|
|
@@ -210,7 +212,7 @@ def create_ui_proxy_router(name: str, port: int) -> APIRouter:
|
|
|
210
212
|
}
|
|
211
213
|
|
|
212
214
|
# Stream downloads and ensure cleanup in the generator's finally block
|
|
213
|
-
async def upstream_body():
|
|
215
|
+
async def upstream_body() -> AsyncGenerator[bytes, None]:
|
|
214
216
|
try:
|
|
215
217
|
async for chunk in upstream.aiter_raw():
|
|
216
218
|
yield chunk
|
|
@@ -240,9 +242,10 @@ def create_ui_proxy_router(name: str, port: int) -> APIRouter:
|
|
|
240
242
|
def mount_static_files(
|
|
241
243
|
app: FastAPI, config: DeploymentConfig, settings: ApiserverSettings
|
|
242
244
|
) -> None:
|
|
243
|
-
|
|
244
|
-
if
|
|
245
|
+
build_output = config.build_output_path()
|
|
246
|
+
if build_output is None:
|
|
245
247
|
return
|
|
248
|
+
path = settings.app_root / build_output
|
|
246
249
|
|
|
247
250
|
if not path.exists():
|
|
248
251
|
return
|
|
@@ -12,11 +12,9 @@ from pathlib import Path
|
|
|
12
12
|
from textwrap import dedent
|
|
13
13
|
|
|
14
14
|
from dotenv import dotenv_values
|
|
15
|
-
from llama_deploy.appserver.deployment_config_parser import (
|
|
16
|
-
DeploymentConfig,
|
|
17
|
-
)
|
|
18
15
|
from llama_deploy.appserver.process_utils import run_process, spawn_process
|
|
19
16
|
from llama_deploy.appserver.settings import ApiserverSettings, settings
|
|
17
|
+
from llama_deploy.core.deployment_config import DeploymentConfig
|
|
20
18
|
from llama_deploy.core.ui_build import ui_build_output_path
|
|
21
19
|
from packaging.version import InvalidVersion, Version
|
|
22
20
|
from workflows import Workflow
|
|
@@ -106,7 +104,8 @@ def parse_environment_variables(
|
|
|
106
104
|
for env_file in config.env_files or []:
|
|
107
105
|
env_file_path = source_root / env_file
|
|
108
106
|
values = dotenv_values(env_file_path)
|
|
109
|
-
|
|
107
|
+
str_values = {k: v for k, v in values.items() if isinstance(v, str)}
|
|
108
|
+
env_vars.update(str_values)
|
|
110
109
|
return env_vars
|
|
111
110
|
|
|
112
111
|
|
|
@@ -190,6 +189,34 @@ def _is_missing_or_outdated(path: Path) -> Version | None:
|
|
|
190
189
|
return None
|
|
191
190
|
|
|
192
191
|
|
|
192
|
+
def run_uv(
|
|
193
|
+
source_root: Path,
|
|
194
|
+
path: Path,
|
|
195
|
+
cmd: str,
|
|
196
|
+
args: list[str] = [],
|
|
197
|
+
extra_env: dict[str, str] | None = None,
|
|
198
|
+
) -> None:
|
|
199
|
+
env = os.environ.copy()
|
|
200
|
+
if extra_env:
|
|
201
|
+
env.update(extra_env)
|
|
202
|
+
run_process(
|
|
203
|
+
["uv", cmd] + args,
|
|
204
|
+
cwd=source_root / path,
|
|
205
|
+
prefix=f"[uv {cmd}]",
|
|
206
|
+
color_code="36",
|
|
207
|
+
use_tty=False,
|
|
208
|
+
line_transform=_exclude_venv_warning,
|
|
209
|
+
env=env,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def ensure_venv(source_root: Path, path: Path, force: bool = False) -> Path:
|
|
214
|
+
venv_path = source_root / path / ".venv"
|
|
215
|
+
if force or not venv_path.exists():
|
|
216
|
+
run_uv(source_root, path, "venv", [str(venv_path)])
|
|
217
|
+
return venv_path
|
|
218
|
+
|
|
219
|
+
|
|
193
220
|
def _install_and_add_appserver_if_missing(
|
|
194
221
|
path: Path,
|
|
195
222
|
source_root: Path,
|
|
@@ -206,29 +233,11 @@ def _install_and_add_appserver_if_missing(
|
|
|
206
233
|
)
|
|
207
234
|
return
|
|
208
235
|
|
|
209
|
-
def run_uv(cmd: str, args: list[str] = [], extra_env: dict[str, str] | None = None):
|
|
210
|
-
env = os.environ.copy()
|
|
211
|
-
if extra_env:
|
|
212
|
-
env.update(extra_env)
|
|
213
|
-
run_process(
|
|
214
|
-
["uv", cmd] + args,
|
|
215
|
-
cwd=source_root / path,
|
|
216
|
-
prefix=f"[uv {cmd}]",
|
|
217
|
-
color_code="36",
|
|
218
|
-
use_tty=False,
|
|
219
|
-
line_transform=_exclude_venv_warning,
|
|
220
|
-
env=env,
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
def ensure_venv(path: Path, force: bool = False) -> Path:
|
|
224
|
-
venv_path = source_root / path / ".venv"
|
|
225
|
-
if force or not venv_path.exists():
|
|
226
|
-
run_uv("venv", [str(venv_path)])
|
|
227
|
-
return venv_path
|
|
228
|
-
|
|
229
236
|
editable = are_we_editable_mode()
|
|
230
|
-
venv_path = ensure_venv(path, force=editable)
|
|
237
|
+
venv_path = ensure_venv(source_root, path, force=editable)
|
|
231
238
|
run_uv(
|
|
239
|
+
source_root,
|
|
240
|
+
path,
|
|
232
241
|
"sync",
|
|
233
242
|
["--no-dev", "--inexact"],
|
|
234
243
|
extra_env={"UV_PROJECT_ENVIRONMENT": str(venv_path)},
|
|
@@ -236,6 +245,8 @@ def _install_and_add_appserver_if_missing(
|
|
|
236
245
|
|
|
237
246
|
if sdists:
|
|
238
247
|
run_uv(
|
|
248
|
+
source_root,
|
|
249
|
+
path,
|
|
239
250
|
"pip",
|
|
240
251
|
["install"]
|
|
241
252
|
+ [str(s.absolute()) for s in sdists]
|
|
@@ -258,6 +269,8 @@ def _install_and_add_appserver_if_missing(
|
|
|
258
269
|
target = f"file://{str(rel)}"
|
|
259
270
|
|
|
260
271
|
run_uv(
|
|
272
|
+
source_root,
|
|
273
|
+
path,
|
|
261
274
|
"pip",
|
|
262
275
|
[
|
|
263
276
|
"install",
|
|
@@ -273,9 +286,11 @@ def _install_and_add_appserver_if_missing(
|
|
|
273
286
|
version = _is_missing_or_outdated(path)
|
|
274
287
|
if version is not None:
|
|
275
288
|
if save_version:
|
|
276
|
-
run_uv("add", [f"llama-deploy-appserver>={version}"])
|
|
289
|
+
run_uv(source_root, path, "add", [f"llama-deploy-appserver>={version}"])
|
|
277
290
|
else:
|
|
278
291
|
run_uv(
|
|
292
|
+
source_root,
|
|
293
|
+
path,
|
|
279
294
|
"pip",
|
|
280
295
|
[
|
|
281
296
|
"install",
|
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
3
|
import os
|
|
4
|
-
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Any, List, cast
|
|
5
6
|
|
|
6
7
|
from llama_cloud.client import AsyncLlamaCloud, httpx
|
|
7
8
|
from llama_cloud_services.beta.agent_data import AsyncAgentDataClient
|
|
8
9
|
from llama_deploy.appserver.settings import ApiserverSettings
|
|
9
10
|
from llama_deploy.core.client.ssl_util import get_httpx_verify_param
|
|
10
11
|
from llama_deploy.core.deployment_config import DeploymentConfig
|
|
11
|
-
from typing_extensions import override
|
|
12
12
|
from workflows.server import AbstractWorkflowStore, HandlerQuery, PersistentHandler
|
|
13
13
|
|
|
14
14
|
from .keyed_lock import AsyncKeyedLock
|
|
15
15
|
from .lru_cache import LRUCache
|
|
16
16
|
|
|
17
|
+
if sys.version_info <= (3, 11):
|
|
18
|
+
from typing_extensions import override
|
|
19
|
+
else:
|
|
20
|
+
from typing import override
|
|
21
|
+
|
|
17
22
|
logger = logging.getLogger(__name__)
|
|
18
23
|
|
|
19
24
|
|
|
@@ -77,8 +82,8 @@ class AgentDataStore(AbstractWorkflowStore):
|
|
|
77
82
|
)
|
|
78
83
|
|
|
79
84
|
@override
|
|
80
|
-
async def delete(self,
|
|
81
|
-
filters = self._build_filters(
|
|
85
|
+
async def delete(self, query: HandlerQuery) -> int:
|
|
86
|
+
filters = self._build_filters(query)
|
|
82
87
|
results = await self.client.search(filter=filters, page_size=1000)
|
|
83
88
|
await asyncio.gather(
|
|
84
89
|
*[self.client.delete_item(item_id=x.id) for x in results.items if x.id]
|
|
@@ -89,18 +94,21 @@ class AgentDataStore(AbstractWorkflowStore):
|
|
|
89
94
|
cached_id = self.cache.get(handler.handler_id)
|
|
90
95
|
if cached_id is not None:
|
|
91
96
|
return cached_id
|
|
97
|
+
search_filter = {"handler_id": {"eq": handler.handler_id}}
|
|
92
98
|
results = await self.client.search(
|
|
93
|
-
filter=
|
|
99
|
+
filter=cast(Any, search_filter),
|
|
94
100
|
page_size=1,
|
|
95
101
|
)
|
|
96
102
|
if not results.items:
|
|
97
103
|
return None
|
|
98
104
|
id = results.items[0].id
|
|
105
|
+
if id is None:
|
|
106
|
+
return None
|
|
99
107
|
self.cache.set(handler.handler_id, id)
|
|
100
108
|
return id
|
|
101
109
|
|
|
102
110
|
def _build_filters(self, query: HandlerQuery) -> dict[str, Any]:
|
|
103
|
-
filters = {}
|
|
111
|
+
filters: dict[str, Any] = {}
|
|
104
112
|
if query.handler_id_in is not None:
|
|
105
113
|
filters["handler_id"] = {
|
|
106
114
|
"includes": query.handler_id_in,
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from collections import Counter
|
|
3
|
+
from collections.abc import AsyncIterator
|
|
3
4
|
from contextlib import asynccontextmanager
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
class AsyncKeyedLock:
|
|
7
|
-
def __init__(self):
|
|
8
|
+
def __init__(self) -> None:
|
|
8
9
|
self._locks: dict[str, asyncio.Lock] = {}
|
|
9
|
-
self._refcnt = Counter()
|
|
10
|
+
self._refcnt: Counter[str] = Counter()
|
|
10
11
|
self._registry_lock = asyncio.Lock() # protects _locks/_refcnt
|
|
11
12
|
|
|
12
13
|
@asynccontextmanager
|
|
13
|
-
async def acquire(self, key: str):
|
|
14
|
+
async def acquire(self, key: str) -> AsyncIterator[None]:
|
|
14
15
|
async with self._registry_lock:
|
|
15
16
|
lock = self._locks.get(key)
|
|
16
17
|
if lock is None:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from collections import OrderedDict
|
|
2
|
+
from collections.abc import Iterator
|
|
2
3
|
from typing import Generic, TypeVar, overload
|
|
3
4
|
|
|
4
5
|
K = TypeVar("K")
|
|
@@ -21,7 +22,7 @@ class LRUCache(Generic[K, V]):
|
|
|
21
22
|
return default
|
|
22
23
|
return self[key]
|
|
23
24
|
|
|
24
|
-
def set(self, key: K, value: V):
|
|
25
|
+
def set(self, key: K, value: V) -> None:
|
|
25
26
|
if key in self._store:
|
|
26
27
|
# remove old so we can push to end
|
|
27
28
|
self._store.pop(key)
|
|
@@ -41,11 +42,11 @@ class LRUCache(Generic[K, V]):
|
|
|
41
42
|
self._store[key] = value
|
|
42
43
|
return value
|
|
43
44
|
|
|
44
|
-
def __setitem__(self, key: K, value: V):
|
|
45
|
+
def __setitem__(self, key: K, value: V) -> None:
|
|
45
46
|
self.set(key, value)
|
|
46
47
|
|
|
47
48
|
def __len__(self) -> int:
|
|
48
49
|
return len(self._store)
|
|
49
50
|
|
|
50
|
-
def __iter__(self):
|
|
51
|
+
def __iter__(self) -> Iterator[K]:
|
|
51
52
|
return iter(self._store)
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "llama-deploy-appserver"
|
|
3
|
-
version = "0.3.23"
|
|
4
|
-
description = "Application server components for LlamaDeploy"
|
|
5
|
-
readme = "README.md"
|
|
6
|
-
license = { text = "MIT" }
|
|
7
|
-
authors = [
|
|
8
|
-
{ name = "Massimiliano Pippi", email = "mpippi@gmail.com" },
|
|
9
|
-
{ name = "Adrian Lyjak", email = "adrianlyjak@gmail.com" },
|
|
10
|
-
]
|
|
11
|
-
requires-python = ">=3.11, <4"
|
|
12
|
-
dependencies = [
|
|
13
|
-
"llama-index-workflows[server]>=2.9.1",
|
|
14
|
-
"pydantic-settings>=2.10.1",
|
|
15
|
-
"fastapi>=0.100.0",
|
|
16
|
-
"websockets>=12.0",
|
|
17
|
-
"llama-deploy-core>=0.3.23,<0.4.0",
|
|
18
|
-
"httpx>=0.24.0,<1.0.0",
|
|
19
|
-
"prometheus-fastapi-instrumentator>=7.1.0",
|
|
20
|
-
"packaging>=25.0",
|
|
21
|
-
"structlog>=25.4.0",
|
|
22
|
-
"rich>=14.1.0",
|
|
23
|
-
"pyyaml>=6.0.2",
|
|
24
|
-
"llama-cloud-services>=0.6.60",
|
|
25
|
-
"watchfiles>=1.1.0",
|
|
26
|
-
"uvicorn>=0.35.0",
|
|
27
|
-
]
|
|
28
|
-
|
|
29
|
-
[build-system]
|
|
30
|
-
requires = ["uv_build>=0.7.20,<0.8.0"]
|
|
31
|
-
build-backend = "uv_build"
|
|
32
|
-
|
|
33
|
-
[tool.uv.build-backend]
|
|
34
|
-
module-name = "llama_deploy.appserver"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
[tool.uv.sources]
|
|
38
|
-
llama-deploy-core = { workspace = true }
|
|
39
|
-
|
|
40
|
-
[dependency-groups]
|
|
41
|
-
dev = [
|
|
42
|
-
"pytest>=8.4.1",
|
|
43
|
-
"pytest-asyncio>=0.25.3",
|
|
44
|
-
"respx>=0.22.0",
|
|
45
|
-
"pytest-xdist>=3.8.0",
|
|
46
|
-
"ty>=0.0.1a19",
|
|
47
|
-
"ruff>=0.12.9",
|
|
48
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{llama_deploy_appserver-0.3.23 → llama_deploy_appserver-0.3.25}/src/llama_deploy/appserver/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{llama_deploy_appserver-0.3.23 → llama_deploy_appserver-0.3.25}/src/llama_deploy/appserver/stats.py
RENAMED
|
File without changes
|
{llama_deploy_appserver-0.3.23 → llama_deploy_appserver-0.3.25}/src/llama_deploy/appserver/types.py
RENAMED
|
File without changes
|