llama-deploy-appserver 0.3.27__tar.gz → 0.4.0__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 (25) hide show
  1. {llama_deploy_appserver-0.3.27 → llama_deploy_appserver-0.4.0}/PKG-INFO +2 -3
  2. {llama_deploy_appserver-0.3.27 → llama_deploy_appserver-0.4.0}/pyproject.toml +2 -3
  3. llama_deploy_appserver-0.4.0/src/llama_deploy/appserver/workflow_store/agent_data_store.py +216 -0
  4. llama_deploy_appserver-0.3.27/src/llama_deploy/appserver/workflow_store/agent_data_store.py +0 -124
  5. {llama_deploy_appserver-0.3.27 → llama_deploy_appserver-0.4.0}/README.md +0 -0
  6. {llama_deploy_appserver-0.3.27 → llama_deploy_appserver-0.4.0}/src/llama_deploy/appserver/__init__.py +0 -0
  7. {llama_deploy_appserver-0.3.27 → llama_deploy_appserver-0.4.0}/src/llama_deploy/appserver/app.py +0 -0
  8. {llama_deploy_appserver-0.3.27 → llama_deploy_appserver-0.4.0}/src/llama_deploy/appserver/bootstrap.py +0 -0
  9. {llama_deploy_appserver-0.3.27 → llama_deploy_appserver-0.4.0}/src/llama_deploy/appserver/configure_logging.py +0 -0
  10. {llama_deploy_appserver-0.3.27 → llama_deploy_appserver-0.4.0}/src/llama_deploy/appserver/correlation_id.py +0 -0
  11. {llama_deploy_appserver-0.3.27 → llama_deploy_appserver-0.4.0}/src/llama_deploy/appserver/deployment.py +0 -0
  12. {llama_deploy_appserver-0.3.27 → llama_deploy_appserver-0.4.0}/src/llama_deploy/appserver/deployment_config_parser.py +0 -0
  13. {llama_deploy_appserver-0.3.27 → llama_deploy_appserver-0.4.0}/src/llama_deploy/appserver/interrupts.py +0 -0
  14. {llama_deploy_appserver-0.3.27 → llama_deploy_appserver-0.4.0}/src/llama_deploy/appserver/process_utils.py +0 -0
  15. {llama_deploy_appserver-0.3.27 → llama_deploy_appserver-0.4.0}/src/llama_deploy/appserver/py.typed +0 -0
  16. {llama_deploy_appserver-0.3.27 → llama_deploy_appserver-0.4.0}/src/llama_deploy/appserver/routers/__init__.py +0 -0
  17. {llama_deploy_appserver-0.3.27 → llama_deploy_appserver-0.4.0}/src/llama_deploy/appserver/routers/deployments.py +0 -0
  18. {llama_deploy_appserver-0.3.27 → llama_deploy_appserver-0.4.0}/src/llama_deploy/appserver/routers/status.py +0 -0
  19. {llama_deploy_appserver-0.3.27 → llama_deploy_appserver-0.4.0}/src/llama_deploy/appserver/routers/ui_proxy.py +0 -0
  20. {llama_deploy_appserver-0.3.27 → llama_deploy_appserver-0.4.0}/src/llama_deploy/appserver/settings.py +0 -0
  21. {llama_deploy_appserver-0.3.27 → llama_deploy_appserver-0.4.0}/src/llama_deploy/appserver/stats.py +0 -0
  22. {llama_deploy_appserver-0.3.27 → llama_deploy_appserver-0.4.0}/src/llama_deploy/appserver/types.py +0 -0
  23. {llama_deploy_appserver-0.3.27 → llama_deploy_appserver-0.4.0}/src/llama_deploy/appserver/workflow_loader.py +0 -0
  24. {llama_deploy_appserver-0.3.27 → llama_deploy_appserver-0.4.0}/src/llama_deploy/appserver/workflow_store/keyed_lock.py +0 -0
  25. {llama_deploy_appserver-0.3.27 → llama_deploy_appserver-0.4.0}/src/llama_deploy/appserver/workflow_store/lru_cache.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: llama-deploy-appserver
3
- Version: 0.3.27
3
+ Version: 0.4.0
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>
@@ -9,14 +9,13 @@ Requires-Dist: llama-index-workflows[server]>=2.12.0,<2.13.0
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.27,<0.4.0
12
+ Requires-Dist: llama-deploy-core>=0.4.0,<0.5.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
16
16
  Requires-Dist: structlog>=25.4.0
17
17
  Requires-Dist: rich>=14.1.0
18
18
  Requires-Dist: pyyaml>=6.0.2
19
- Requires-Dist: llama-cloud-services>=0.6.60
20
19
  Requires-Dist: watchfiles>=1.1.0
21
20
  Requires-Dist: uvicorn>=0.35.0
22
21
  Requires-Dist: typing-extensions>=4.15.0 ; python_full_version < '3.12'
@@ -14,7 +14,7 @@ dev = [
14
14
 
15
15
  [project]
16
16
  name = "llama-deploy-appserver"
17
- version = "0.3.27"
17
+ version = "0.4.0"
18
18
  description = "Application server components for LlamaDeploy"
19
19
  readme = "README.md"
20
20
  license = {text = "MIT"}
@@ -28,14 +28,13 @@ dependencies = [
28
28
  "pydantic-settings>=2.10.1",
29
29
  "fastapi>=0.100.0",
30
30
  "websockets>=12.0",
31
- "llama-deploy-core>=0.3.27,<0.4.0",
31
+ "llama-deploy-core>=0.4.0,<0.5.0",
32
32
  "httpx>=0.24.0,<1.0.0",
33
33
  "prometheus-fastapi-instrumentator>=7.1.0",
34
34
  "packaging>=25.0",
35
35
  "structlog>=25.4.0",
36
36
  "rich>=14.1.0",
37
37
  "pyyaml>=6.0.2",
38
- "llama-cloud-services>=0.6.60",
39
38
  "watchfiles>=1.1.0",
40
39
  "uvicorn>=0.35.0",
41
40
  "typing-extensions>=4.15.0 ; python_full_version < '3.12'"
@@ -0,0 +1,216 @@
1
+ import asyncio
2
+ import logging
3
+ import os
4
+ import sys
5
+ from typing import Any, List
6
+
7
+ import httpx
8
+ from llama_deploy.appserver.settings import ApiserverSettings
9
+ from llama_deploy.core.client.ssl_util import get_httpx_verify_param
10
+ from llama_deploy.core.deployment_config import DeploymentConfig
11
+ from workflows.server import AbstractWorkflowStore, HandlerQuery, PersistentHandler
12
+
13
+ from .keyed_lock import AsyncKeyedLock
14
+ from .lru_cache import LRUCache
15
+
16
+ if sys.version_info <= (3, 11):
17
+ from typing_extensions import override
18
+ else:
19
+ from typing import override
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ DEFAULT_BASE_URL = "https://api.cloud.llamaindex.ai"
24
+
25
+
26
+ class AgentDataStore(AbstractWorkflowStore):
27
+ """Workflow store backed by LlamaCloud Agent Data API using raw httpx."""
28
+
29
+ def __init__(
30
+ self, settings: DeploymentConfig, server_settings: ApiserverSettings
31
+ ) -> None:
32
+ agent_url_id: str | None = server_settings.cloud_persistence_name
33
+ collection = "workflow_contexts"
34
+ if agent_url_id is not None:
35
+ parts = agent_url_id.split(":")
36
+ if len(parts) > 1:
37
+ collection = parts[1]
38
+ agent_url_id = parts[0]
39
+ else:
40
+ agent_url_id = settings.name
41
+
42
+ self.settings = settings
43
+ self.collection = collection
44
+ self.deployment_name = agent_url_id
45
+
46
+ self.base_url = os.getenv("LLAMA_CLOUD_BASE_URL", DEFAULT_BASE_URL).rstrip("/")
47
+ self.api_key = os.getenv("LLAMA_CLOUD_API_KEY")
48
+ self.project_id = os.getenv("LLAMA_DEPLOY_PROJECT_ID")
49
+
50
+ self.lock = AsyncKeyedLock()
51
+ # workflow id -> agent data id
52
+ self.cache = LRUCache[str, str](maxsize=1024)
53
+
54
+ def _get_headers(self) -> dict[str, str]:
55
+ """Build HTTP headers for API requests."""
56
+ headers = {
57
+ "Content-Type": "application/json",
58
+ }
59
+ if self.api_key:
60
+ headers["Authorization"] = f"Bearer {self.api_key}"
61
+ if self.project_id:
62
+ headers["Project-Id"] = self.project_id
63
+ return headers
64
+
65
+ def _get_client(self) -> httpx.AsyncClient:
66
+ """Create a new httpx client."""
67
+ return httpx.AsyncClient(
68
+ headers=self._get_headers(),
69
+ verify=get_httpx_verify_param(),
70
+ )
71
+
72
+ @override
73
+ async def query(self, query: HandlerQuery) -> List[PersistentHandler]:
74
+ filters = self._build_filters(query)
75
+ search_request = {
76
+ "deployment_name": self.deployment_name,
77
+ "collection": self.collection,
78
+ "filter": filters,
79
+ "page_size": 1000,
80
+ }
81
+ async with self._get_client() as client:
82
+ response = await client.post(
83
+ f"{self.base_url}/api/v1/beta/agent-data/:search",
84
+ json=search_request,
85
+ )
86
+ response.raise_for_status()
87
+ data = response.json()
88
+
89
+ items = data.get("items", [])
90
+ return [PersistentHandler(**item["data"]) for item in items]
91
+
92
+ @override
93
+ async def update(self, handler: PersistentHandler) -> None:
94
+ async with self.lock.acquire(handler.handler_id):
95
+ id = await self._get_item_id(handler)
96
+ if id is None:
97
+ item = await self._create_item(handler)
98
+ item_id = item.get("id")
99
+ if item_id is None:
100
+ raise ValueError(f"Failed to create handler {handler.handler_id}")
101
+ self.cache.set(handler.handler_id, item_id)
102
+ else:
103
+ await self._update_item(id, handler)
104
+
105
+ @override
106
+ async def delete(self, query: HandlerQuery) -> int:
107
+ filters = self._build_filters(query)
108
+ search_request = {
109
+ "deployment_name": self.deployment_name,
110
+ "collection": self.collection,
111
+ "filter": filters,
112
+ "page_size": 1000,
113
+ }
114
+ async with self._get_client() as client:
115
+ response = await client.post(
116
+ f"{self.base_url}/api/v1/beta/agent-data/:search",
117
+ json=search_request,
118
+ )
119
+ response.raise_for_status()
120
+ data = response.json()
121
+
122
+ items = data.get("items", [])
123
+ await asyncio.gather(
124
+ *[self._delete_item(item["id"]) for item in items if item.get("id")]
125
+ )
126
+ return len(items)
127
+
128
+ async def _create_item(self, handler: PersistentHandler) -> dict[str, Any]:
129
+ """Create a new agent data item."""
130
+ create_request = {
131
+ "deployment_name": self.deployment_name,
132
+ "collection": self.collection,
133
+ "data": handler.model_dump(mode="json"),
134
+ }
135
+ async with self._get_client() as client:
136
+ response = await client.post(
137
+ f"{self.base_url}/api/v1/beta/agent-data",
138
+ json=create_request,
139
+ )
140
+ response.raise_for_status()
141
+ return response.json()
142
+
143
+ async def _update_item(self, item_id: str, handler: PersistentHandler) -> None:
144
+ """Update an existing agent data item."""
145
+ update_request = {
146
+ "data": handler.model_dump(mode="json"),
147
+ }
148
+ async with self._get_client() as client:
149
+ response = await client.put(
150
+ f"{self.base_url}/api/v1/beta/agent-data/{item_id}",
151
+ json=update_request,
152
+ )
153
+ response.raise_for_status()
154
+
155
+ async def _delete_item(self, item_id: str) -> None:
156
+ """Delete an agent data item."""
157
+ async with self._get_client() as client:
158
+ response = await client.delete(
159
+ f"{self.base_url}/api/v1/beta/agent-data/{item_id}",
160
+ )
161
+ response.raise_for_status()
162
+
163
+ async def _get_item_id(self, handler: PersistentHandler) -> str | None:
164
+ cached_id = self.cache.get(handler.handler_id)
165
+ if cached_id is not None:
166
+ return cached_id
167
+ search_filter = {"handler_id": {"eq": handler.handler_id}}
168
+ search_request = {
169
+ "deployment_name": self.deployment_name,
170
+ "collection": self.collection,
171
+ "filter": search_filter,
172
+ "page_size": 1,
173
+ }
174
+ async with self._get_client() as client:
175
+ response = await client.post(
176
+ f"{self.base_url}/api/v1/beta/agent-data/:search",
177
+ json=search_request,
178
+ )
179
+ response.raise_for_status()
180
+ data = response.json()
181
+
182
+ items = data.get("items", [])
183
+ if not items:
184
+ return None
185
+ id = items[0].get("id")
186
+ if id is None:
187
+ return None
188
+ self.cache.set(handler.handler_id, id)
189
+ return id
190
+
191
+ def _build_filters(self, query: HandlerQuery) -> dict[str, Any]:
192
+ filters: dict[str, Any] = {}
193
+ if query.handler_id_in is not None:
194
+ filters["handler_id"] = {
195
+ "includes": query.handler_id_in,
196
+ }
197
+ if query.workflow_name_in is not None:
198
+ filters["workflow_name"] = {
199
+ "includes": query.workflow_name_in,
200
+ }
201
+ if query.status_in is not None:
202
+ filters["status"] = {
203
+ "includes": query.status_in,
204
+ }
205
+ if query.is_idle is not None:
206
+ if query.is_idle:
207
+ # Filter for handlers where idle_since is set (any valid datetime)
208
+ filters["idle_since"] = {
209
+ "gte": "1970-01-01T00:00:00Z",
210
+ }
211
+ else:
212
+ # Filter for handlers where idle_since is not set (null)
213
+ filters["idle_since"] = {
214
+ "eq": None,
215
+ }
216
+ return filters
@@ -1,124 +0,0 @@
1
- import asyncio
2
- import logging
3
- import os
4
- import sys
5
- from typing import Any, List, cast
6
-
7
- from llama_cloud.client import AsyncLlamaCloud, httpx
8
- from llama_cloud_services.beta.agent_data import AsyncAgentDataClient
9
- from llama_deploy.appserver.settings import ApiserverSettings
10
- from llama_deploy.core.client.ssl_util import get_httpx_verify_param
11
- from llama_deploy.core.deployment_config import DeploymentConfig
12
- from workflows.server import AbstractWorkflowStore, HandlerQuery, PersistentHandler
13
-
14
- from .keyed_lock import AsyncKeyedLock
15
- from .lru_cache import LRUCache
16
-
17
- if sys.version_info <= (3, 11):
18
- from typing_extensions import override
19
- else:
20
- from typing import override
21
-
22
- logger = logging.getLogger(__name__)
23
-
24
-
25
- class AgentDataStore(AbstractWorkflowStore):
26
- def __init__(
27
- self, settings: DeploymentConfig, server_settings: ApiserverSettings
28
- ) -> None:
29
- agent_url_id: str | None = server_settings.cloud_persistence_name
30
- collection = "workflow_contexts"
31
- if agent_url_id is not None:
32
- parts = agent_url_id.split(":")
33
- if len(parts) > 1:
34
- collection = parts[1]
35
- agent_url_id = parts[0]
36
- else:
37
- agent_url_id = settings.name
38
-
39
- self.settings = settings
40
- project_id = os.getenv("LLAMA_DEPLOY_PROJECT_ID")
41
- self.client = AsyncAgentDataClient(
42
- type=PersistentHandler,
43
- collection=collection,
44
- agent_url_id=agent_url_id,
45
- client=AsyncLlamaCloud(
46
- base_url=os.getenv("LLAMA_CLOUD_BASE_URL"),
47
- token=os.getenv("LLAMA_CLOUD_API_KEY"),
48
- httpx_client=httpx.AsyncClient(
49
- headers={"Project-Id": project_id} if project_id else None,
50
- verify=get_httpx_verify_param(),
51
- ),
52
- ),
53
- )
54
- self.lock = AsyncKeyedLock()
55
- # workflow id -> agent data id
56
- self.cache = LRUCache[str, str](maxsize=1024)
57
-
58
- @override
59
- async def query(self, query: HandlerQuery) -> List[PersistentHandler]:
60
- filters = self._build_filters(query)
61
- results = await self.client.search(
62
- filter=filters,
63
- page_size=1000,
64
- )
65
- return [x.data for x in results.items]
66
-
67
- @override
68
- async def update(self, handler: PersistentHandler) -> None:
69
- async with self.lock.acquire(handler.handler_id):
70
- id = await self._get_item_id(handler)
71
- if id is None:
72
- item = await self.client.create_item(
73
- data=handler,
74
- )
75
- if item.id is None:
76
- raise ValueError(f"Failed to create handler {handler.handler_id}")
77
- self.cache.set(handler.handler_id, item.id)
78
- else:
79
- await self.client.update_item(
80
- item_id=id,
81
- data=handler,
82
- )
83
-
84
- @override
85
- async def delete(self, query: HandlerQuery) -> int:
86
- filters = self._build_filters(query)
87
- results = await self.client.search(filter=filters, page_size=1000)
88
- await asyncio.gather(
89
- *[self.client.delete_item(item_id=x.id) for x in results.items if x.id]
90
- )
91
- return len(results.items)
92
-
93
- async def _get_item_id(self, handler: PersistentHandler) -> str | None:
94
- cached_id = self.cache.get(handler.handler_id)
95
- if cached_id is not None:
96
- return cached_id
97
- search_filter = {"handler_id": {"eq": handler.handler_id}}
98
- results = await self.client.search(
99
- filter=cast(Any, search_filter),
100
- page_size=1,
101
- )
102
- if not results.items:
103
- return None
104
- id = results.items[0].id
105
- if id is None:
106
- return None
107
- self.cache.set(handler.handler_id, id)
108
- return id
109
-
110
- def _build_filters(self, query: HandlerQuery) -> dict[str, Any]:
111
- filters: dict[str, Any] = {}
112
- if query.handler_id_in is not None:
113
- filters["handler_id"] = {
114
- "includes": query.handler_id_in,
115
- }
116
- if query.workflow_name_in is not None:
117
- filters["workflow_name"] = {
118
- "includes": query.workflow_name_in,
119
- }
120
- if query.status_in is not None:
121
- filters["status"] = {
122
- "includes": query.status_in,
123
- }
124
- return filters