llama-deploy-appserver 0.2.7a1__tar.gz → 0.3.0a2__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.
Files changed (38) hide show
  1. {llama_deploy_appserver-0.2.7a1 → llama_deploy_appserver-0.3.0a2}/PKG-INFO +3 -6
  2. {llama_deploy_appserver-0.2.7a1 → llama_deploy_appserver-0.3.0a2}/pyproject.toml +10 -6
  3. {llama_deploy_appserver-0.2.7a1 → llama_deploy_appserver-0.3.0a2}/src/llama_deploy/appserver/__main__.py +0 -4
  4. llama_deploy_appserver-0.3.0a2/src/llama_deploy/appserver/app.py +129 -0
  5. llama_deploy_appserver-0.3.0a2/src/llama_deploy/appserver/bootstrap.py +95 -0
  6. llama_deploy_appserver-0.3.0a2/src/llama_deploy/appserver/deployment.py +81 -0
  7. llama_deploy_appserver-0.3.0a2/src/llama_deploy/appserver/deployment_config_parser.py +109 -0
  8. llama_deploy_appserver-0.3.0a2/src/llama_deploy/appserver/routers/__init__.py +5 -0
  9. llama_deploy_appserver-0.3.0a2/src/llama_deploy/appserver/routers/deployments.py +210 -0
  10. llama_deploy_appserver-0.3.0a2/src/llama_deploy/appserver/routers/status.py +13 -0
  11. llama_deploy_appserver-0.3.0a2/src/llama_deploy/appserver/routers/ui_proxy.py +213 -0
  12. llama_deploy_appserver-0.3.0a2/src/llama_deploy/appserver/settings.py +85 -0
  13. {llama_deploy_appserver-0.2.7a1 → llama_deploy_appserver-0.3.0a2}/src/llama_deploy/appserver/types.py +0 -3
  14. llama_deploy_appserver-0.3.0a2/src/llama_deploy/appserver/workflow_loader.py +383 -0
  15. llama_deploy_appserver-0.2.7a1/src/llama_deploy/appserver/app.py +0 -49
  16. llama_deploy_appserver-0.2.7a1/src/llama_deploy/appserver/bootstrap.py +0 -43
  17. llama_deploy_appserver-0.2.7a1/src/llama_deploy/appserver/client/__init__.py +0 -3
  18. llama_deploy_appserver-0.2.7a1/src/llama_deploy/appserver/client/base.py +0 -30
  19. llama_deploy_appserver-0.2.7a1/src/llama_deploy/appserver/client/client.py +0 -49
  20. llama_deploy_appserver-0.2.7a1/src/llama_deploy/appserver/client/models/__init__.py +0 -4
  21. llama_deploy_appserver-0.2.7a1/src/llama_deploy/appserver/client/models/apiserver.py +0 -356
  22. llama_deploy_appserver-0.2.7a1/src/llama_deploy/appserver/client/models/model.py +0 -82
  23. llama_deploy_appserver-0.2.7a1/src/llama_deploy/appserver/deployment.py +0 -495
  24. llama_deploy_appserver-0.2.7a1/src/llama_deploy/appserver/deployment_config_parser.py +0 -133
  25. llama_deploy_appserver-0.2.7a1/src/llama_deploy/appserver/routers/__init__.py +0 -4
  26. llama_deploy_appserver-0.2.7a1/src/llama_deploy/appserver/routers/deployments.py +0 -433
  27. llama_deploy_appserver-0.2.7a1/src/llama_deploy/appserver/routers/status.py +0 -40
  28. llama_deploy_appserver-0.2.7a1/src/llama_deploy/appserver/run_autodeploy.py +0 -141
  29. llama_deploy_appserver-0.2.7a1/src/llama_deploy/appserver/server.py +0 -60
  30. llama_deploy_appserver-0.2.7a1/src/llama_deploy/appserver/settings.py +0 -83
  31. llama_deploy_appserver-0.2.7a1/src/llama_deploy/appserver/source_managers/__init__.py +0 -5
  32. llama_deploy_appserver-0.2.7a1/src/llama_deploy/appserver/source_managers/base.py +0 -33
  33. llama_deploy_appserver-0.2.7a1/src/llama_deploy/appserver/source_managers/git.py +0 -48
  34. llama_deploy_appserver-0.2.7a1/src/llama_deploy/appserver/source_managers/local.py +0 -51
  35. llama_deploy_appserver-0.2.7a1/src/llama_deploy/appserver/tracing.py +0 -237
  36. {llama_deploy_appserver-0.2.7a1 → llama_deploy_appserver-0.3.0a2}/README.md +0 -0
  37. {llama_deploy_appserver-0.2.7a1 → llama_deploy_appserver-0.3.0a2}/src/llama_deploy/appserver/__init__.py +0 -0
  38. {llama_deploy_appserver-0.2.7a1 → llama_deploy_appserver-0.3.0a2}/src/llama_deploy/appserver/stats.py +0 -0
@@ -1,21 +1,18 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: llama-deploy-appserver
3
- Version: 0.2.7a1
3
+ Version: 0.3.0a2
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: asgiref>=3.9.1
9
8
  Requires-Dist: llama-index-workflows>=1.1.0
10
9
  Requires-Dist: pydantic-settings>=2.10.1
11
10
  Requires-Dist: uvicorn>=0.24.0
12
- Requires-Dist: prometheus-client>=0.20.0
13
- Requires-Dist: python-multipart>=0.0.18,<0.0.19
14
11
  Requires-Dist: fastapi>=0.100.0
15
12
  Requires-Dist: websockets>=12.0
16
- Requires-Dist: gitpython>=3.1.40,<4
17
- Requires-Dist: llama-deploy-core>=0.2.7a1,<0.3.0
13
+ Requires-Dist: llama-deploy-core>=0.3.0a2,<0.4.0
18
14
  Requires-Dist: httpx>=0.28.1
15
+ Requires-Dist: prometheus-fastapi-instrumentator>=7.1.0
19
16
  Requires-Python: >=3.12, <4
20
17
  Description-Content-Type: text/markdown
21
18
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "llama-deploy-appserver"
3
- version = "0.2.7a1"
3
+ version = "0.3.0a2"
4
4
  description = "Application server components for LlamaDeploy"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -9,17 +9,14 @@ authors = [
9
9
  ]
10
10
  requires-python = ">=3.12, <4"
11
11
  dependencies = [
12
- "asgiref>=3.9.1",
13
12
  "llama-index-workflows>=1.1.0",
14
13
  "pydantic-settings>=2.10.1",
15
14
  "uvicorn>=0.24.0",
16
- "prometheus-client>=0.20.0",
17
- "python-multipart>=0.0.18,<0.0.19",
18
15
  "fastapi>=0.100.0",
19
16
  "websockets>=12.0",
20
- "gitpython>=3.1.40,<4",
21
- "llama-deploy-core>=0.2.7a1,<0.3.0",
17
+ "llama-deploy-core>=0.3.0a2,<0.4.0",
22
18
  "httpx>=0.28.1",
19
+ "prometheus-fastapi-instrumentator>=7.1.0",
23
20
  ]
24
21
 
25
22
  [build-system]
@@ -32,3 +29,10 @@ module-name = "llama_deploy.appserver"
32
29
 
33
30
  [tool.uv.sources]
34
31
  llama-deploy-core = { workspace = true }
32
+
33
+ [dependency-groups]
34
+ dev = [
35
+ "pytest>=8.4.1",
36
+ "pytest-asyncio>=0.25.3",
37
+ "respx>=0.22.0",
38
+ ]
@@ -1,12 +1,8 @@
1
1
  import uvicorn
2
- from prometheus_client import start_http_server
3
2
 
4
3
  from .settings import settings
5
4
 
6
5
  if __name__ == "__main__":
7
- if settings.prometheus_enabled:
8
- start_http_server(settings.prometheus_port)
9
-
10
6
  uvicorn.run(
11
7
  "llama_deploy.appserver.app:app",
12
8
  host=settings.host,
@@ -0,0 +1,129 @@
1
+ import logging
2
+ import os
3
+ from pathlib import Path
4
+ import threading
5
+ import time
6
+ from llama_deploy.appserver.deployment_config_parser import (
7
+ get_deployment_config,
8
+ )
9
+ from llama_deploy.appserver.settings import configure_settings, settings
10
+
11
+ from fastapi import FastAPI
12
+ from fastapi.middleware.cors import CORSMiddleware
13
+ from llama_deploy.appserver.workflow_loader import (
14
+ build_ui,
15
+ do_install,
16
+ start_dev_ui_process,
17
+ )
18
+ import uvicorn
19
+
20
+ from .routers import health_router
21
+ from prometheus_fastapi_instrumentator import Instrumentator
22
+ from contextlib import asynccontextmanager
23
+ from typing import Any, AsyncGenerator
24
+
25
+ from llama_deploy.appserver.routers.deployments import (
26
+ create_base_router,
27
+ create_deployments_router,
28
+ )
29
+ from llama_deploy.appserver.routers.ui_proxy import (
30
+ create_ui_proxy_router,
31
+ mount_static_files,
32
+ )
33
+ from llama_deploy.appserver.workflow_loader import (
34
+ load_workflows,
35
+ )
36
+
37
+ from .deployment import Deployment
38
+ from .stats import apiserver_state
39
+ import webbrowser
40
+
41
+ logger = logging.getLogger("uvicorn.info")
42
+
43
+
44
+ @asynccontextmanager
45
+ async def lifespan(app: FastAPI) -> AsyncGenerator[None, Any]:
46
+ apiserver_state.state("starting")
47
+
48
+ config = get_deployment_config()
49
+
50
+ workflows = load_workflows(config)
51
+ deployment = Deployment(workflows)
52
+ base_router = create_base_router(config.name)
53
+ deploy_router = create_deployments_router(config.name, deployment)
54
+ app.include_router(base_router)
55
+ app.include_router(deploy_router)
56
+ # proxy UI in dev mode
57
+ if config.ui is not None:
58
+ if settings.proxy_ui:
59
+ ui_router = create_ui_proxy_router(config.name, config.ui.port)
60
+ app.include_router(ui_router)
61
+ else:
62
+ # otherwise serve the pre-built if available
63
+ mount_static_files(app, config, settings)
64
+
65
+ apiserver_state.state("running")
66
+ yield
67
+
68
+ apiserver_state.state("stopped")
69
+
70
+
71
+ app = FastAPI(lifespan=lifespan)
72
+ Instrumentator().instrument(app).expose(app)
73
+
74
+ # Configure CORS middleware if the environment variable is set
75
+ if not os.environ.get("DISABLE_CORS", False):
76
+ app.add_middleware(
77
+ CORSMiddleware,
78
+ allow_origins=["*"], # Allows all origins
79
+ allow_credentials=True,
80
+ allow_methods=["GET", "POST"],
81
+ allow_headers=["Content-Type", "Authorization"],
82
+ )
83
+
84
+ app.include_router(health_router)
85
+
86
+
87
+ def start_server(
88
+ proxy_ui: bool = False,
89
+ reload: bool = False,
90
+ cwd: Path | None = None,
91
+ deployment_file: Path | None = None,
92
+ install: bool = False,
93
+ build: bool = False,
94
+ open_browser: bool = False,
95
+ ) -> None:
96
+ # Configure via environment so uvicorn reload workers inherit the values
97
+ configure_settings(
98
+ proxy_ui=proxy_ui, app_root=cwd, deployment_file_path=deployment_file
99
+ )
100
+ if install:
101
+ do_install()
102
+ if build:
103
+ build_ui(settings.config_parent, get_deployment_config())
104
+
105
+ ui_process = None
106
+ if proxy_ui:
107
+ ui_process = start_dev_ui_process(
108
+ settings.config_parent, settings.port, get_deployment_config()
109
+ )
110
+ try:
111
+ if open_browser:
112
+
113
+ def open_with_delay():
114
+ time.sleep(1)
115
+ webbrowser.open(f"http://{settings.host}:{settings.port}")
116
+
117
+ threading.Thread(
118
+ target=open_with_delay,
119
+ ).start()
120
+
121
+ uvicorn.run(
122
+ "llama_deploy.appserver.app:app",
123
+ host=settings.host,
124
+ port=settings.port,
125
+ reload=reload,
126
+ )
127
+ finally:
128
+ if ui_process is not None:
129
+ ui_process.terminate()
@@ -0,0 +1,95 @@
1
+ """
2
+ Bootstraps an application from a remote github repository given environment variables.
3
+
4
+ This just sets up the files from the repository. It's more of a build process, does not start an application.
5
+ """
6
+
7
+ import os
8
+ from pathlib import Path
9
+ from llama_deploy.appserver.settings import settings
10
+ from llama_deploy.appserver.deployment_config_parser import get_deployment_config
11
+ from llama_deploy.appserver.workflow_loader import build_ui, do_install
12
+ from llama_deploy.core.git.git_util import (
13
+ clone_repo,
14
+ )
15
+ from llama_deploy.appserver.app import start_server
16
+ from llama_deploy.appserver.settings import BootstrapSettings, configure_settings
17
+
18
+ import argparse
19
+
20
+
21
+ def bootstrap_app_from_repo(
22
+ clone: bool = False,
23
+ build: bool = False,
24
+ serve: bool = False,
25
+ target_dir: str = "/opt/app/",
26
+ ):
27
+ bootstrap_settings = BootstrapSettings()
28
+ # Needs the github url+auth, and the deployment file path
29
+ # clones the repo to a standard directory
30
+ # (eventually) runs the UI build process and moves that to a standard directory for a file server
31
+ if clone:
32
+ repo_url = bootstrap_settings.repo_url
33
+ if repo_url is None:
34
+ raise ValueError("repo_url is required to bootstrap")
35
+ clone_repo(
36
+ repository_url=repo_url,
37
+ git_ref=bootstrap_settings.git_sha or bootstrap_settings.git_ref,
38
+ basic_auth=bootstrap_settings.auth_token,
39
+ dest_dir=target_dir,
40
+ )
41
+ # Ensure target_dir exists locally when running tests outside a container
42
+ os.makedirs(target_dir, exist_ok=True)
43
+ os.chdir(target_dir)
44
+ configure_settings(
45
+ app_root=Path(target_dir),
46
+ deployment_file_path=Path(bootstrap_settings.deployment_file_path),
47
+ )
48
+
49
+ built = True
50
+ if build:
51
+ do_install()
52
+ built = build_ui(settings.config_parent, get_deployment_config())
53
+
54
+ if serve:
55
+ start_server(
56
+ proxy_ui=not built,
57
+ )
58
+ pass
59
+
60
+
61
+ if __name__ == "__main__":
62
+ parser = argparse.ArgumentParser()
63
+ parser.add_argument(
64
+ "--clone",
65
+ action=argparse.BooleanOptionalAction,
66
+ default=False,
67
+ help="Clone the repository before bootstrapping (use --no-clone to disable)",
68
+ )
69
+ parser.add_argument(
70
+ "--build",
71
+ action=argparse.BooleanOptionalAction,
72
+ default=False,
73
+ help="Build the UI/assets (use --no-build to disable)",
74
+ )
75
+ parser.add_argument(
76
+ "--serve",
77
+ action=argparse.BooleanOptionalAction,
78
+ default=False,
79
+ help="Start the API server after bootstrap (use --no-serve to disable)",
80
+ )
81
+ args = parser.parse_args()
82
+ try:
83
+ bootstrap_app_from_repo(
84
+ clone=args.clone,
85
+ build=args.build,
86
+ serve=args.serve,
87
+ )
88
+ except Exception as e:
89
+ import logging
90
+
91
+ logging.exception("Error during bootstrap. Pausing for debugging.")
92
+ import time
93
+
94
+ time.sleep(1000000)
95
+ raise e
@@ -0,0 +1,81 @@
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ from typing import Any, Tuple
5
+
6
+ from llama_deploy.appserver.types import generate_id
7
+ from llama_deploy.appserver.workflow_loader import DEFAULT_SERVICE_ID
8
+ from workflows import Context, Workflow
9
+ from workflows.handler import WorkflowHandler
10
+
11
+
12
+ logger = logging.getLogger()
13
+
14
+
15
+ class DeploymentError(Exception): ...
16
+
17
+
18
+ class Deployment:
19
+ def __init__(
20
+ self,
21
+ workflows: dict[str, Workflow],
22
+ ) -> None:
23
+ """Creates a Deployment instance.
24
+
25
+ Args:
26
+ config: The configuration object defining this deployment
27
+ root_path: The path on the filesystem used to store deployment data
28
+ local: Whether the deployment is local. If true, sources won't be synced
29
+ """
30
+
31
+ self._default_service: str | None = workflows.get(DEFAULT_SERVICE_ID)
32
+ self._service_tasks: list[asyncio.Task] = []
33
+ # Ready to load services
34
+ self._workflow_services: dict[str, Workflow] = workflows
35
+ self._contexts: dict[str, Context] = {}
36
+ self._handlers: dict[str, WorkflowHandler] = {}
37
+ self._handler_inputs: dict[str, str] = {}
38
+
39
+ @property
40
+ def default_service(self) -> Workflow | None:
41
+ return self._default_service
42
+
43
+ @property
44
+ def name(self) -> str:
45
+ """Returns the name of this deployment."""
46
+ return self._name
47
+
48
+ @property
49
+ def service_names(self) -> list[str]:
50
+ """Returns the list of service names in this deployment."""
51
+ return list(self._workflow_services.keys())
52
+
53
+ async def run_workflow(
54
+ self, service_id: str, session_id: str | None = None, **run_kwargs: dict
55
+ ) -> Any:
56
+ workflow = self._workflow_services[service_id]
57
+ if session_id:
58
+ context = self._contexts[session_id]
59
+ return await workflow.run(context=context, **run_kwargs)
60
+
61
+ if run_kwargs:
62
+ return await workflow.run(**run_kwargs)
63
+
64
+ return await workflow.run()
65
+
66
+ def run_workflow_no_wait(
67
+ self, service_id: str, session_id: str | None = None, **run_kwargs: dict
68
+ ) -> Tuple[str, str]:
69
+ workflow = self._workflow_services[service_id]
70
+ if session_id:
71
+ context = self._contexts[session_id]
72
+ handler = workflow.run(context=context, **run_kwargs)
73
+ else:
74
+ handler = workflow.run(**run_kwargs)
75
+ session_id = generate_id()
76
+ self._contexts[session_id] = handler.ctx or Context(workflow)
77
+
78
+ handler_id = generate_id()
79
+ self._handlers[handler_id] = handler
80
+ self._handler_inputs[handler_id] = json.dumps(run_kwargs)
81
+ return handler_id, session_id
@@ -0,0 +1,109 @@
1
+ import functools
2
+ from pathlib import Path
3
+ from typing import Any
4
+
5
+
6
+ from llama_deploy.appserver.settings import settings, BootstrapSettings
7
+ import yaml
8
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
9
+
10
+
11
+ class ServiceSource(BaseModel):
12
+ """Configuration for where to load the workflow or other source. Path is relative to the config file its declared within."""
13
+
14
+ location: str
15
+
16
+ @model_validator(mode="before")
17
+ @classmethod
18
+ def validate_fields(cls, data: Any) -> Any:
19
+ if isinstance(data, dict):
20
+ if "name" in data:
21
+ data["location"] = data.pop("name")
22
+ return data
23
+
24
+
25
+ class Service(BaseModel):
26
+ """Configuration for a single service."""
27
+
28
+ source: ServiceSource | None = Field(None)
29
+ import_path: str | None = Field(None)
30
+ env: dict[str, str] | None = Field(None)
31
+ env_files: list[str] | None = Field(None)
32
+ python_dependencies: list[str] | None = Field(None)
33
+
34
+ @model_validator(mode="before")
35
+ @classmethod
36
+ def validate_fields(cls, data: Any) -> Any:
37
+ if isinstance(data, dict):
38
+ # Handle YAML aliases
39
+ if "path" in data:
40
+ data["import_path"] = data.pop("path")
41
+ if "import-path" in data:
42
+ data["import_path"] = data.pop("import-path")
43
+ if "env-files" in data:
44
+ data["env_files"] = data.pop("env-files")
45
+
46
+ return data
47
+
48
+ def module_location(self) -> tuple[str, str]:
49
+ """
50
+ Parses the import path, and target, discarding legacy file path portion, if any
51
+
52
+ "src/module.workflow:my_workflow" -> ("module.workflow", "my_workflow")
53
+ """
54
+ module_name, workflow_name = self.import_path.split(":")
55
+ return Path(module_name).name, workflow_name
56
+
57
+
58
+ class UIService(Service):
59
+ port: int = Field(
60
+ default=3000,
61
+ description="The TCP port to use for the nextjs server",
62
+ )
63
+
64
+
65
+ class DeploymentConfig(BaseModel):
66
+ """Model definition mapping a deployment config file."""
67
+
68
+ model_config = ConfigDict(populate_by_name=True, extra="ignore")
69
+
70
+ name: str
71
+ default_service: str | None = Field(None)
72
+ services: dict[str, Service]
73
+ ui: UIService | None = None
74
+
75
+ @model_validator(mode="before")
76
+ @classmethod
77
+ def validate_fields(cls, data: Any) -> Any:
78
+ # Handle YAML aliases
79
+ if isinstance(data, dict):
80
+ if "default-service" in data:
81
+ data["default_service"] = data.pop("default-service")
82
+
83
+ return data
84
+
85
+ @classmethod
86
+ def from_yaml_bytes(cls, src: bytes) -> "DeploymentConfig":
87
+ """Read config data from bytes containing yaml code."""
88
+ config = yaml.safe_load(src) or {}
89
+ return cls(**config)
90
+
91
+ @classmethod
92
+ def from_yaml(cls, path: Path, name: str | None = None) -> "DeploymentConfig":
93
+ """Read config data from a yaml file."""
94
+ with open(path, "r") as yaml_file:
95
+ config = yaml.safe_load(yaml_file) or {}
96
+
97
+ instance = cls(**config)
98
+ if name:
99
+ instance.name = name
100
+ return instance
101
+
102
+
103
+ @functools.lru_cache
104
+ def get_deployment_config() -> DeploymentConfig:
105
+ base_settings = BootstrapSettings()
106
+ base = settings.app_root.resolve()
107
+ yaml_file = base / settings.deployment_file_path
108
+ name = base_settings.deployment_name
109
+ return DeploymentConfig.from_yaml(yaml_file, name)
@@ -0,0 +1,5 @@
1
+ from .deployments import create_deployments_router
2
+ from .ui_proxy import create_ui_proxy_router
3
+ from .status import health_router
4
+
5
+ __all__ = ["create_deployments_router", "create_ui_proxy_router", "health_router"]