llama-deploy-appserver 0.2.7a1__py3-none-any.whl → 0.3.0a2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- llama_deploy/appserver/__main__.py +0 -4
- llama_deploy/appserver/app.py +105 -25
- llama_deploy/appserver/bootstrap.py +76 -24
- llama_deploy/appserver/deployment.py +7 -421
- llama_deploy/appserver/deployment_config_parser.py +35 -59
- llama_deploy/appserver/routers/__init__.py +4 -3
- llama_deploy/appserver/routers/deployments.py +162 -385
- llama_deploy/appserver/routers/status.py +4 -31
- llama_deploy/appserver/routers/ui_proxy.py +213 -0
- llama_deploy/appserver/settings.py +57 -55
- llama_deploy/appserver/types.py +0 -3
- llama_deploy/appserver/workflow_loader.py +383 -0
- {llama_deploy_appserver-0.2.7a1.dist-info → llama_deploy_appserver-0.3.0a2.dist-info}/METADATA +3 -6
- llama_deploy_appserver-0.3.0a2.dist-info/RECORD +17 -0
- {llama_deploy_appserver-0.2.7a1.dist-info → llama_deploy_appserver-0.3.0a2.dist-info}/WHEEL +1 -1
- llama_deploy/appserver/client/__init__.py +0 -3
- llama_deploy/appserver/client/base.py +0 -30
- llama_deploy/appserver/client/client.py +0 -49
- llama_deploy/appserver/client/models/__init__.py +0 -4
- llama_deploy/appserver/client/models/apiserver.py +0 -356
- llama_deploy/appserver/client/models/model.py +0 -82
- llama_deploy/appserver/run_autodeploy.py +0 -141
- llama_deploy/appserver/server.py +0 -60
- llama_deploy/appserver/source_managers/__init__.py +0 -5
- llama_deploy/appserver/source_managers/base.py +0 -33
- llama_deploy/appserver/source_managers/git.py +0 -48
- llama_deploy/appserver/source_managers/local.py +0 -51
- llama_deploy/appserver/tracing.py +0 -237
- llama_deploy_appserver-0.2.7a1.dist-info/RECORD +0 -28
@@ -1,356 +0,0 @@
|
|
1
|
-
"""Client functionalities to operate on the API Server.
|
2
|
-
|
3
|
-
This module allows the client to use all the functionalities
|
4
|
-
from the LlamaDeploy API Server. For this to work, the API
|
5
|
-
Server must be up and its URL (by default `http://localhost:4501`)
|
6
|
-
reachable by the host executing the client code.
|
7
|
-
"""
|
8
|
-
|
9
|
-
import asyncio
|
10
|
-
import json
|
11
|
-
from typing import Any, AsyncGenerator, TextIO
|
12
|
-
|
13
|
-
import httpx
|
14
|
-
from llama_deploy.appserver.types import (
|
15
|
-
EventDefinition,
|
16
|
-
SessionDefinition,
|
17
|
-
Status,
|
18
|
-
StatusEnum,
|
19
|
-
TaskDefinition,
|
20
|
-
TaskResult,
|
21
|
-
)
|
22
|
-
from pydantic import Field
|
23
|
-
from workflows.context import JsonSerializer
|
24
|
-
from workflows.events import Event
|
25
|
-
|
26
|
-
from .model import Collection, Model
|
27
|
-
|
28
|
-
|
29
|
-
class SessionCollection(Collection):
|
30
|
-
"""A model representing a collection of session for a given deployment."""
|
31
|
-
|
32
|
-
deployment_id: str = Field(
|
33
|
-
description="The ID of the deployment containing the sessions."
|
34
|
-
)
|
35
|
-
|
36
|
-
async def delete(self, session_id: str) -> None:
|
37
|
-
"""Deletes the session with the provided `session_id`.
|
38
|
-
|
39
|
-
Args:
|
40
|
-
session_id: The id of the session that will be removed
|
41
|
-
|
42
|
-
Raises:
|
43
|
-
HTTPException: If the session couldn't be found with the id provided.
|
44
|
-
"""
|
45
|
-
delete_url = f"{self.client.api_server_url}/deployments/{self.deployment_id}/sessions/delete"
|
46
|
-
|
47
|
-
await self.client.request(
|
48
|
-
"POST",
|
49
|
-
delete_url,
|
50
|
-
params={"session_id": session_id},
|
51
|
-
verify=not self.client.disable_ssl,
|
52
|
-
timeout=self.client.timeout,
|
53
|
-
)
|
54
|
-
|
55
|
-
async def create(self) -> SessionDefinition:
|
56
|
-
"""Create a new session."""
|
57
|
-
create_url = f"{self.client.api_server_url}/deployments/{self.deployment_id}/sessions/create"
|
58
|
-
|
59
|
-
r = await self.client.request(
|
60
|
-
"POST",
|
61
|
-
create_url,
|
62
|
-
verify=not self.client.disable_ssl,
|
63
|
-
timeout=self.client.timeout,
|
64
|
-
)
|
65
|
-
|
66
|
-
return SessionDefinition(**r.json())
|
67
|
-
|
68
|
-
async def list(self) -> list[SessionDefinition]:
|
69
|
-
"""Returns a collection of all the sessions in the given deployment."""
|
70
|
-
sessions_url = (
|
71
|
-
f"{self.client.api_server_url}/deployments/{self.deployment_id}/sessions"
|
72
|
-
)
|
73
|
-
r = await self.client.request(
|
74
|
-
"GET",
|
75
|
-
sessions_url,
|
76
|
-
verify=not self.client.disable_ssl,
|
77
|
-
timeout=self.client.timeout,
|
78
|
-
)
|
79
|
-
|
80
|
-
return r.json()
|
81
|
-
|
82
|
-
async def get(self, id: str) -> SessionDefinition:
|
83
|
-
"""Gets a deployment by id."""
|
84
|
-
get_url = f"{self.client.api_server_url}/deployments/{self.deployment_id}/sessions/{id}"
|
85
|
-
await self.client.request(
|
86
|
-
"GET",
|
87
|
-
get_url,
|
88
|
-
verify=not self.client.disable_ssl,
|
89
|
-
timeout=self.client.timeout,
|
90
|
-
)
|
91
|
-
model_class = self._prepare(SessionDefinition)
|
92
|
-
return model_class(client=self.client, id=id)
|
93
|
-
|
94
|
-
|
95
|
-
class Task(Model):
|
96
|
-
"""A model representing a task belonging to a given session in the given deployment."""
|
97
|
-
|
98
|
-
deployment_id: str = Field(
|
99
|
-
description="The ID of the deployment this task belongs to."
|
100
|
-
)
|
101
|
-
session_id: str = Field(description="The ID of the session this task belongs to.")
|
102
|
-
|
103
|
-
async def results(self) -> TaskResult | None:
|
104
|
-
"""Returns the result of a given task."""
|
105
|
-
results_url = f"{self.client.api_server_url}/deployments/{self.deployment_id}/tasks/{self.id}/results"
|
106
|
-
|
107
|
-
r = await self.client.request(
|
108
|
-
"GET",
|
109
|
-
results_url,
|
110
|
-
verify=not self.client.disable_ssl,
|
111
|
-
params={"session_id": self.session_id},
|
112
|
-
timeout=self.client.timeout,
|
113
|
-
)
|
114
|
-
if r.json():
|
115
|
-
return TaskResult.model_validate(r.json())
|
116
|
-
return None
|
117
|
-
|
118
|
-
async def send_event(self, ev: Event, service_name: str) -> EventDefinition:
|
119
|
-
"""Sends a human response event."""
|
120
|
-
url = f"{self.client.api_server_url}/deployments/{self.deployment_id}/tasks/{self.id}/events"
|
121
|
-
|
122
|
-
serializer = JsonSerializer()
|
123
|
-
event_def = EventDefinition(
|
124
|
-
event_obj_str=serializer.serialize(ev), service_id=service_name
|
125
|
-
)
|
126
|
-
|
127
|
-
r = await self.client.request(
|
128
|
-
"POST",
|
129
|
-
url,
|
130
|
-
verify=not self.client.disable_ssl,
|
131
|
-
params={"session_id": self.session_id},
|
132
|
-
json=event_def.model_dump(),
|
133
|
-
timeout=self.client.timeout,
|
134
|
-
)
|
135
|
-
return EventDefinition.model_validate(r.json())
|
136
|
-
|
137
|
-
async def events(self) -> AsyncGenerator[dict[str, Any], None]: # pragma: no cover
|
138
|
-
"""Returns a generator object to consume the events streamed from a service."""
|
139
|
-
events_url = f"{self.client.api_server_url}/deployments/{self.deployment_id}/tasks/{self.id}/events"
|
140
|
-
|
141
|
-
while True:
|
142
|
-
try:
|
143
|
-
async with httpx.AsyncClient(
|
144
|
-
verify=not self.client.disable_ssl
|
145
|
-
) as client:
|
146
|
-
async with client.stream(
|
147
|
-
"GET", events_url, params={"session_id": self.session_id}
|
148
|
-
) as response:
|
149
|
-
response.raise_for_status()
|
150
|
-
async for line in response.aiter_lines():
|
151
|
-
json_line = json.loads(line)
|
152
|
-
yield json_line
|
153
|
-
break # Exit the function if successful
|
154
|
-
except httpx.HTTPStatusError as e:
|
155
|
-
if e.response.status_code != 404:
|
156
|
-
raise # Re-raise if it's not a 404 error
|
157
|
-
await asyncio.sleep(self.client.poll_interval)
|
158
|
-
|
159
|
-
|
160
|
-
class TaskCollection(Collection):
|
161
|
-
"""A model representing a collection of tasks for a given deployment."""
|
162
|
-
|
163
|
-
deployment_id: str = Field(
|
164
|
-
description="The ID of the deployment these tasks belong to."
|
165
|
-
)
|
166
|
-
|
167
|
-
async def run(self, task: TaskDefinition) -> Any:
|
168
|
-
"""Runs a task and returns the results once it's done.
|
169
|
-
|
170
|
-
Args:
|
171
|
-
task: The definition of the task we want to run.
|
172
|
-
"""
|
173
|
-
run_url = (
|
174
|
-
f"{self.client.api_server_url}/deployments/{self.deployment_id}/tasks/run"
|
175
|
-
)
|
176
|
-
if task.session_id:
|
177
|
-
run_url += f"?session_id={task.session_id}"
|
178
|
-
|
179
|
-
r = await self.client.request(
|
180
|
-
"POST",
|
181
|
-
run_url,
|
182
|
-
verify=not self.client.disable_ssl,
|
183
|
-
json=task.model_dump(),
|
184
|
-
timeout=self.client.timeout,
|
185
|
-
)
|
186
|
-
|
187
|
-
return r.json()
|
188
|
-
|
189
|
-
async def create(self, task: TaskDefinition) -> Task:
|
190
|
-
"""Runs a task returns it immediately, without waiting for the results."""
|
191
|
-
create_url = f"{self.client.api_server_url}/deployments/{self.deployment_id}/tasks/create"
|
192
|
-
|
193
|
-
r = await self.client.request(
|
194
|
-
"POST",
|
195
|
-
create_url,
|
196
|
-
verify=not self.client.disable_ssl,
|
197
|
-
json=task.model_dump(),
|
198
|
-
timeout=self.client.timeout,
|
199
|
-
)
|
200
|
-
response_fields = r.json()
|
201
|
-
|
202
|
-
model_class = self._prepare(Task)
|
203
|
-
return model_class(
|
204
|
-
client=self.client,
|
205
|
-
deployment_id=self.deployment_id,
|
206
|
-
id=response_fields["task_id"],
|
207
|
-
session_id=response_fields["session_id"],
|
208
|
-
)
|
209
|
-
|
210
|
-
async def list(self) -> list[Task]:
|
211
|
-
"""Returns the list of tasks from this collection."""
|
212
|
-
tasks_url = (
|
213
|
-
f"{self.client.api_server_url}/deployments/{self.deployment_id}/tasks"
|
214
|
-
)
|
215
|
-
r = await self.client.request(
|
216
|
-
"GET",
|
217
|
-
tasks_url,
|
218
|
-
verify=not self.client.disable_ssl,
|
219
|
-
timeout=self.client.timeout,
|
220
|
-
)
|
221
|
-
task_model_class = self._prepare(Task)
|
222
|
-
items = {
|
223
|
-
"id": task_model_class(
|
224
|
-
client=self.client,
|
225
|
-
id=task_def.task_id,
|
226
|
-
session_id=task_def.session_id,
|
227
|
-
deployment_id=self.deployment_id,
|
228
|
-
)
|
229
|
-
for task_def in r.json()
|
230
|
-
}
|
231
|
-
model_class = self._prepare(TaskCollection)
|
232
|
-
return model_class(
|
233
|
-
client=self.client, deployment_id=self.deployment_id, items=items
|
234
|
-
)
|
235
|
-
|
236
|
-
|
237
|
-
class Deployment(Model):
|
238
|
-
"""A model representing a deployment."""
|
239
|
-
|
240
|
-
@property
|
241
|
-
def tasks(self) -> TaskCollection:
|
242
|
-
"""Returns a collection of tasks from all the sessions in the given deployment."""
|
243
|
-
|
244
|
-
model_class = self._prepare(TaskCollection)
|
245
|
-
return model_class(client=self.client, deployment_id=self.id, items={})
|
246
|
-
|
247
|
-
@property
|
248
|
-
def sessions(self) -> SessionCollection:
|
249
|
-
"""Returns a collection of all the sessions in the given deployment."""
|
250
|
-
|
251
|
-
coll_model_class = self._prepare(SessionCollection)
|
252
|
-
return coll_model_class(client=self.client, deployment_id=self.id, items={})
|
253
|
-
|
254
|
-
|
255
|
-
class DeploymentCollection(Collection):
|
256
|
-
"""A model representing a collection of deployments currently active."""
|
257
|
-
|
258
|
-
async def create(
|
259
|
-
self, config: TextIO, base_path: str, reload: bool = False, local: bool = False
|
260
|
-
) -> Deployment:
|
261
|
-
"""Creates a new deployment from a deployment file.
|
262
|
-
|
263
|
-
If `reload` is true, an existing deployment will be reloaded, otherwise
|
264
|
-
an error will be raised.
|
265
|
-
|
266
|
-
If `local` is true, the sync managers won't attempt at syncing data.
|
267
|
-
This is mostly for supporting local development.
|
268
|
-
|
269
|
-
Example:
|
270
|
-
```
|
271
|
-
with open("deployment.yml") as f:
|
272
|
-
await client.apiserver.deployments.create(f)
|
273
|
-
```
|
274
|
-
"""
|
275
|
-
create_url = f"{self.client.api_server_url}/deployments/create"
|
276
|
-
|
277
|
-
files = {"config_file": config.read()}
|
278
|
-
r = await self.client.request(
|
279
|
-
"POST",
|
280
|
-
create_url,
|
281
|
-
files=files,
|
282
|
-
params={"reload": reload, "local": local, "base_path": base_path},
|
283
|
-
verify=not self.client.disable_ssl,
|
284
|
-
timeout=self.client.timeout,
|
285
|
-
)
|
286
|
-
|
287
|
-
model_class = self._prepare(Deployment)
|
288
|
-
return model_class(client=self.client, id=r.json().get("name"))
|
289
|
-
|
290
|
-
async def get(self, id: str) -> Deployment:
|
291
|
-
"""Gets a deployment by id."""
|
292
|
-
get_url = f"{self.client.api_server_url}/deployments/{id}"
|
293
|
-
# Current version of apiserver doesn't returns anything useful in this endpoint, let's just ignore it
|
294
|
-
await self.client.request(
|
295
|
-
"GET",
|
296
|
-
get_url,
|
297
|
-
verify=not self.client.disable_ssl,
|
298
|
-
timeout=self.client.timeout,
|
299
|
-
)
|
300
|
-
model_class = self._prepare(Deployment)
|
301
|
-
return model_class(client=self.client, id=id)
|
302
|
-
|
303
|
-
async def list(self) -> list[Deployment]:
|
304
|
-
"""Return a list of Deployment instances for this collection."""
|
305
|
-
deployments_url = f"{self.client.api_server_url}/deployments/"
|
306
|
-
r = await self.client.request("GET", deployments_url)
|
307
|
-
model_class = self._prepare(Deployment)
|
308
|
-
deployments = [model_class(client=self.client, id=name) for name in r.json()]
|
309
|
-
return deployments
|
310
|
-
|
311
|
-
|
312
|
-
class ApiServer(Model):
|
313
|
-
"""A model representing the API Server instance."""
|
314
|
-
|
315
|
-
async def status(self) -> Status:
|
316
|
-
"""Returns the status of the API Server."""
|
317
|
-
status_url = f"{self.client.api_server_url}/status/"
|
318
|
-
|
319
|
-
try:
|
320
|
-
r = await self.client.request(
|
321
|
-
"GET",
|
322
|
-
status_url,
|
323
|
-
verify=not self.client.disable_ssl,
|
324
|
-
timeout=self.client.timeout,
|
325
|
-
)
|
326
|
-
except httpx.ConnectError:
|
327
|
-
return Status(
|
328
|
-
status=StatusEnum.DOWN,
|
329
|
-
status_message="API Server is down",
|
330
|
-
)
|
331
|
-
|
332
|
-
if r.status_code >= 400:
|
333
|
-
body = r.json()
|
334
|
-
return Status(status=StatusEnum.UNHEALTHY, status_message=r.text)
|
335
|
-
|
336
|
-
description = "LlamaDeploy is up and running."
|
337
|
-
body = r.json()
|
338
|
-
deployments = body.get("deployments") or []
|
339
|
-
if deployments:
|
340
|
-
description += "\nActive deployments:"
|
341
|
-
for d in deployments:
|
342
|
-
description += f"\n- {d}"
|
343
|
-
else:
|
344
|
-
description += "\nCurrently there are no active deployments"
|
345
|
-
|
346
|
-
return Status(
|
347
|
-
status=StatusEnum.HEALTHY,
|
348
|
-
status_message=description,
|
349
|
-
deployments=deployments,
|
350
|
-
)
|
351
|
-
|
352
|
-
@property
|
353
|
-
def deployments(self) -> DeploymentCollection:
|
354
|
-
"""Returns a collection of deployments currently active in the API Server."""
|
355
|
-
model_class = self._prepare(DeploymentCollection)
|
356
|
-
return model_class(client=self.client, items={})
|
@@ -1,82 +0,0 @@
|
|
1
|
-
import asyncio
|
2
|
-
import inspect
|
3
|
-
from typing import Any, AsyncGenerator, Callable, Generic, TypeVar
|
4
|
-
|
5
|
-
from asgiref.sync import async_to_sync
|
6
|
-
from llama_deploy.appserver.client.base import _BaseClient
|
7
|
-
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr
|
8
|
-
from typing_extensions import ParamSpec
|
9
|
-
|
10
|
-
|
11
|
-
class _Base(BaseModel):
|
12
|
-
"""The base model provides fields and functionalities common to derived models and collections."""
|
13
|
-
|
14
|
-
client: _BaseClient = Field(exclude=True)
|
15
|
-
_instance_is_sync: bool = PrivateAttr(default=False)
|
16
|
-
|
17
|
-
model_config = ConfigDict(arbitrary_types_allowed=True)
|
18
|
-
|
19
|
-
def _prepare(self, _class: type) -> type:
|
20
|
-
if self._instance_is_sync:
|
21
|
-
return make_sync(_class)
|
22
|
-
return _class
|
23
|
-
|
24
|
-
|
25
|
-
T = TypeVar("T", bound=_Base)
|
26
|
-
|
27
|
-
|
28
|
-
class Model(_Base):
|
29
|
-
id: str
|
30
|
-
|
31
|
-
|
32
|
-
class Collection(_Base, Generic[T]):
|
33
|
-
"""A generic container of items of the same model type."""
|
34
|
-
|
35
|
-
items: dict[str, T]
|
36
|
-
|
37
|
-
def get(self, id: str) -> T:
|
38
|
-
"""Returns an item from the collection."""
|
39
|
-
return self.items[id]
|
40
|
-
|
41
|
-
async def list(self) -> list[T]:
|
42
|
-
"""Returns a list of all the items in the collection."""
|
43
|
-
return [self.get(id) for id in self.items.keys()]
|
44
|
-
|
45
|
-
|
46
|
-
# Generic type for what's returned by the async generator
|
47
|
-
_G = TypeVar("_G")
|
48
|
-
# Generic parameter for the wrapped generator method
|
49
|
-
_P = ParamSpec("_P")
|
50
|
-
# Generic parameter for the wrapped generator method return value
|
51
|
-
_R = TypeVar("_R")
|
52
|
-
|
53
|
-
|
54
|
-
async def _async_gen_to_list(async_gen: AsyncGenerator[_G, None]) -> list[_G]:
|
55
|
-
return [item async for item in async_gen]
|
56
|
-
|
57
|
-
|
58
|
-
def make_sync(_class: type[T]) -> Any:
|
59
|
-
"""Wraps the methods of the given model class so that they can be called without `await`."""
|
60
|
-
|
61
|
-
class ModelWrapper(_class): # type: ignore
|
62
|
-
_instance_is_sync: bool = True
|
63
|
-
|
64
|
-
def generator_wrapper(
|
65
|
-
func: Callable[_P, AsyncGenerator[_G, None]], # ty: ignore[invalid-type-form] - https://github.com/astral-sh/ty/issues/157
|
66
|
-
/,
|
67
|
-
*args: Any,
|
68
|
-
**kwargs: Any,
|
69
|
-
) -> Callable[_P, list[_G]]: # ty: ignore[invalid-type-form] - https://github.com/astral-sh/ty/issues/157
|
70
|
-
def new_func(*fargs: Any, **fkwargs: Any) -> list[_G]:
|
71
|
-
return asyncio.run(_async_gen_to_list(func(*fargs, **fkwargs)))
|
72
|
-
|
73
|
-
return new_func
|
74
|
-
|
75
|
-
for name, method in _class.__dict__.items():
|
76
|
-
# Only wrap async public methods
|
77
|
-
if inspect.isasyncgenfunction(method):
|
78
|
-
setattr(ModelWrapper, name, generator_wrapper(method))
|
79
|
-
elif asyncio.iscoroutinefunction(method) and not name.startswith("_"):
|
80
|
-
setattr(ModelWrapper, name, async_to_sync(method))
|
81
|
-
|
82
|
-
return ModelWrapper
|
@@ -1,141 +0,0 @@
|
|
1
|
-
import os
|
2
|
-
import shutil
|
3
|
-
import subprocess
|
4
|
-
from pathlib import Path
|
5
|
-
|
6
|
-
import uvicorn
|
7
|
-
import yaml
|
8
|
-
from prometheus_client import start_http_server
|
9
|
-
|
10
|
-
from llama_deploy.appserver.settings import settings
|
11
|
-
|
12
|
-
CLONED_REPO_FOLDER = Path("cloned_repo")
|
13
|
-
RC_PATH = Path("/data")
|
14
|
-
|
15
|
-
|
16
|
-
def run_process(args: list[str], cwd: str | None = None) -> None:
|
17
|
-
kwargs = {
|
18
|
-
"args": args,
|
19
|
-
"capture_output": True,
|
20
|
-
"text": True,
|
21
|
-
"check": False,
|
22
|
-
}
|
23
|
-
if cwd:
|
24
|
-
kwargs["cwd"] = cwd
|
25
|
-
process = subprocess.run(**kwargs) # type: ignore
|
26
|
-
if process.returncode != 0:
|
27
|
-
stderr = process.stderr or ""
|
28
|
-
raise Exception(stderr)
|
29
|
-
|
30
|
-
|
31
|
-
def setup_repo(
|
32
|
-
work_dir: Path, source: str, token: str | None = None, force: bool = False
|
33
|
-
) -> None:
|
34
|
-
repo_url, ref_name = _parse_source(source, token)
|
35
|
-
dest_dir = work_dir / CLONED_REPO_FOLDER
|
36
|
-
|
37
|
-
# Remove existing repo if force=True
|
38
|
-
if dest_dir.exists() and force:
|
39
|
-
shutil.rmtree(dest_dir)
|
40
|
-
|
41
|
-
if not dest_dir.exists():
|
42
|
-
# need to do a full clone to resolve any kind of ref without exploding in
|
43
|
-
# complexity (tag, branch, commit, short commit)
|
44
|
-
clone_args = ["git", "clone", repo_url, str(dest_dir.absolute())]
|
45
|
-
run_process(clone_args, cwd=str(work_dir.absolute()))
|
46
|
-
else:
|
47
|
-
run_process(["git", "fetch", "origin"], cwd=str(dest_dir.absolute()))
|
48
|
-
|
49
|
-
# Checkout the ref (let git resolve it)
|
50
|
-
if ref_name:
|
51
|
-
run_process(["git", "checkout", ref_name], cwd=str(dest_dir.absolute()))
|
52
|
-
# If no ref specified, stay on whatever the clone gave us (default branch)
|
53
|
-
|
54
|
-
|
55
|
-
def _is_valid_uri(uri: str) -> bool:
|
56
|
-
"""Check if string looks like a valid URI"""
|
57
|
-
return "://" in uri and "/" in uri.split("://", 1)[1]
|
58
|
-
|
59
|
-
|
60
|
-
def _parse_source(source: str, pat: str | None = None) -> tuple[str, str | None]:
|
61
|
-
"""Accept Github urls like https://github.com/run-llama/llama_deploy.git@main
|
62
|
-
or https://user:token@github.com/run-llama/llama_deploy.git@v1.0.0
|
63
|
-
Returns the final URL (with auth if needed) and ref name (branch, tag, or commit SHA)"""
|
64
|
-
|
65
|
-
# Try splitting on last @ to see if we have a ref specifier
|
66
|
-
url = source
|
67
|
-
ref_name = None
|
68
|
-
|
69
|
-
if "@" in source:
|
70
|
-
potential_url, potential_ref = source.rsplit("@", 1)
|
71
|
-
if _is_valid_uri(potential_url):
|
72
|
-
url = potential_url
|
73
|
-
ref_name = potential_ref
|
74
|
-
|
75
|
-
# Inject PAT auth if provided and URL doesn't already have auth
|
76
|
-
if pat and "://" in url and "@" not in url:
|
77
|
-
url = url.replace("https://", f"https://{pat}@")
|
78
|
-
|
79
|
-
return url, ref_name
|
80
|
-
|
81
|
-
|
82
|
-
def copy_sources(work_dir: Path, deployment_file_path: Path) -> None:
|
83
|
-
app_folder = deployment_file_path.parent
|
84
|
-
for item in app_folder.iterdir():
|
85
|
-
if item.is_dir():
|
86
|
-
# For directories, use copytree with dirs_exist_ok=True
|
87
|
-
shutil.copytree(
|
88
|
-
item, f"{work_dir.absolute()}/{item.name}", dirs_exist_ok=True
|
89
|
-
)
|
90
|
-
else:
|
91
|
-
# For files, use copy2 to preserve metadata
|
92
|
-
shutil.copy2(item, str(work_dir))
|
93
|
-
|
94
|
-
|
95
|
-
if __name__ == "__main__":
|
96
|
-
if settings.prometheus_enabled:
|
97
|
-
start_http_server(settings.prometheus_port)
|
98
|
-
|
99
|
-
repo_url = os.environ.get("REPO_URL", "")
|
100
|
-
if not repo_url.startswith("https://") and not repo_url.startswith("http://"):
|
101
|
-
raise ValueError("Git remote must HTTP(S)")
|
102
|
-
repo_token = os.environ.get("GITHUB_PAT")
|
103
|
-
work_dir = Path(os.environ.get("WORK_DIR", RC_PATH))
|
104
|
-
work_dir.mkdir(exist_ok=True, parents=True)
|
105
|
-
|
106
|
-
setup_repo(work_dir, repo_url, repo_token)
|
107
|
-
|
108
|
-
if not settings.deployment_file_path:
|
109
|
-
# first fall back to none LLAMA_DEPLOY_APISERVER_ prefixed env var (settings requires the prefix)
|
110
|
-
settings.deployment_file_path = os.environ.get(
|
111
|
-
"DEPLOYMENT_FILE_PATH", "deployment.yml"
|
112
|
-
)
|
113
|
-
deployment_file_path = settings.deployment_file_path
|
114
|
-
deployment_file_abspath = work_dir / CLONED_REPO_FOLDER / deployment_file_path
|
115
|
-
if not deployment_file_abspath.exists():
|
116
|
-
raise ValueError(f"File {deployment_file_abspath} does not exist")
|
117
|
-
|
118
|
-
deployment_override_name = os.environ.get("DEPLOYMENT_NAME")
|
119
|
-
if deployment_override_name:
|
120
|
-
with open(deployment_file_abspath) as f:
|
121
|
-
# Replace deployment name with the overridden value
|
122
|
-
data = yaml.safe_load(f)
|
123
|
-
|
124
|
-
# Avoid failing here if the deployment config file has a wrong format,
|
125
|
-
# let's do nothing if there's no field `name`
|
126
|
-
if "name" in data:
|
127
|
-
data["name"] = deployment_override_name
|
128
|
-
with open(deployment_file_abspath, "w") as f:
|
129
|
-
yaml.safe_dump(data, f)
|
130
|
-
|
131
|
-
copy_sources(work_dir, deployment_file_abspath)
|
132
|
-
shutil.rmtree(work_dir / CLONED_REPO_FOLDER)
|
133
|
-
|
134
|
-
# update rc_path directly, as it has already been loaded, so setting the environment variable
|
135
|
-
# doesn't work
|
136
|
-
settings.rc_path = work_dir
|
137
|
-
uvicorn.run(
|
138
|
-
"llama_deploy.appserver.app:app",
|
139
|
-
host=settings.host,
|
140
|
-
port=settings.port,
|
141
|
-
)
|
llama_deploy/appserver/server.py
DELETED
@@ -1,60 +0,0 @@
|
|
1
|
-
import asyncio
|
2
|
-
import logging
|
3
|
-
from contextlib import asynccontextmanager
|
4
|
-
from typing import Any, AsyncGenerator
|
5
|
-
|
6
|
-
from fastapi import FastAPI
|
7
|
-
|
8
|
-
from .deployment import Manager
|
9
|
-
from .deployment_config_parser import DeploymentConfig
|
10
|
-
from .settings import settings
|
11
|
-
from .stats import apiserver_state
|
12
|
-
|
13
|
-
logger = logging.getLogger("uvicorn.info")
|
14
|
-
manager = Manager()
|
15
|
-
|
16
|
-
|
17
|
-
@asynccontextmanager
|
18
|
-
async def lifespan(app: FastAPI) -> AsyncGenerator[None, Any]:
|
19
|
-
apiserver_state.state("starting")
|
20
|
-
|
21
|
-
manager.set_deployments_path(settings.deployments_path)
|
22
|
-
t = asyncio.create_task(manager.serve())
|
23
|
-
await asyncio.sleep(0)
|
24
|
-
|
25
|
-
logger.info(f"deployments folder: {settings.deployments_path}")
|
26
|
-
logger.info(f"rc folder: {settings.rc_path}")
|
27
|
-
|
28
|
-
if settings.rc_path.exists():
|
29
|
-
if settings.deployment_file_path:
|
30
|
-
logger.info(
|
31
|
-
f"Browsing the rc folder {settings.rc_path} for deployment file {settings.deployment_file_path}"
|
32
|
-
)
|
33
|
-
else:
|
34
|
-
logger.info(
|
35
|
-
f"Browsing the rc folder {settings.rc_path} for deployments to start"
|
36
|
-
)
|
37
|
-
|
38
|
-
# if a deployment_file_path is provided, use it, otherwise glob all .yml/.yaml files
|
39
|
-
# q match both .yml and .yaml files with the glob
|
40
|
-
files = (
|
41
|
-
[settings.rc_path / settings.deployment_file_path]
|
42
|
-
if settings.deployment_file_path
|
43
|
-
else [
|
44
|
-
x for x in settings.rc_path.iterdir() if x.suffix in (".yml", ".yaml")
|
45
|
-
]
|
46
|
-
)
|
47
|
-
for yaml_file in files:
|
48
|
-
try:
|
49
|
-
logger.info(f"Deploying startup configuration from {yaml_file}")
|
50
|
-
config = DeploymentConfig.from_yaml(yaml_file)
|
51
|
-
await manager.deploy(config, base_path=str(settings.rc_path))
|
52
|
-
except Exception as e:
|
53
|
-
logger.error(f"Failed to deploy {yaml_file}: {str(e)}")
|
54
|
-
|
55
|
-
apiserver_state.state("running")
|
56
|
-
yield
|
57
|
-
|
58
|
-
t.cancel()
|
59
|
-
|
60
|
-
apiserver_state.state("stopped")
|
@@ -1,33 +0,0 @@
|
|
1
|
-
from abc import ABC, abstractmethod
|
2
|
-
from pathlib import Path
|
3
|
-
|
4
|
-
from llama_deploy.appserver.deployment_config_parser import DeploymentConfig, SyncPolicy
|
5
|
-
|
6
|
-
|
7
|
-
class SourceManager(ABC):
|
8
|
-
"""Protocol to be implemented by classes responsible for managing Deployment sources."""
|
9
|
-
|
10
|
-
def __init__(self, config: DeploymentConfig, base_path: Path | None = None) -> None:
|
11
|
-
self._config = config
|
12
|
-
self._base_path = base_path
|
13
|
-
|
14
|
-
@abstractmethod
|
15
|
-
def sync(
|
16
|
-
self,
|
17
|
-
source: str,
|
18
|
-
destination: str | None = None,
|
19
|
-
sync_policy: SyncPolicy = SyncPolicy.REPLACE,
|
20
|
-
) -> None: # pragma: no cover
|
21
|
-
"""Fetches resources from `source` so they can be used in a deployment.
|
22
|
-
|
23
|
-
Optionally uses `destination` to store data when this makes sense for the
|
24
|
-
specific source type.
|
25
|
-
"""
|
26
|
-
|
27
|
-
def relative_path(self, source: str) -> str:
|
28
|
-
"""Unfortunately, there's a difference in behavior of how the source managers sync.
|
29
|
-
The local source manager syncs the source into the <destination_path>/<source>, whereas
|
30
|
-
the git source manager just syncs the source into the <destination_path>. This is a temporary shim, since
|
31
|
-
changing this behavior is a breaking change to deployment.yaml configurations. Local source manager
|
32
|
-
overrides it. In a future major version, this behavior will be made consistent"""
|
33
|
-
return ""
|