llamactl 0.3.10__tar.gz → 0.3.12__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.
- {llamactl-0.3.10 → llamactl-0.3.12}/PKG-INFO +3 -4
- {llamactl-0.3.10 → llamactl-0.3.12}/pyproject.toml +3 -4
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/auth/client.py +6 -2
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/commands/init.py +141 -43
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/commands/serve.py +19 -1
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/options.py +47 -12
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/textual/deployment_form.py +100 -6
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/textual/deployment_help.py +6 -0
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/textual/styles.tcss +112 -0
- llamactl-0.3.12/src/llama_deploy/cli/utils/version.py +11 -0
- {llamactl-0.3.10 → llamactl-0.3.12}/README.md +0 -0
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/__init__.py +0 -0
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/app.py +0 -0
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/client.py +0 -0
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/commands/aliased_group.py +0 -0
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/commands/auth.py +0 -0
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/commands/deployment.py +0 -0
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/commands/env.py +0 -0
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/config/_config.py +0 -0
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/config/_migrations.py +0 -0
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/config/auth_service.py +0 -0
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/config/env_service.py +0 -0
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/config/migrations/0001_init.sql +0 -0
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/config/migrations/0002_add_auth_fields.sql +0 -0
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/config/migrations/__init__.py +0 -0
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/config/schema.py +0 -0
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/debug.py +0 -0
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/env.py +0 -0
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/interactive_prompts/session_utils.py +0 -0
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/interactive_prompts/utils.py +0 -0
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/py.typed +0 -0
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/styles.py +0 -0
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/textual/deployment_monitor.py +0 -0
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/textual/git_validation.py +0 -0
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/textual/github_callback_server.py +0 -0
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/textual/llama_loader.py +0 -0
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/textual/secrets_form.py +0 -0
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/utils/env_inject.py +0 -0
- {llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/utils/redact.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: llamactl
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.12
|
|
4
4
|
Summary: A command-line interface for managing LlamaDeploy projects and deployments
|
|
5
5
|
Author: Adrian Lyjak
|
|
6
6
|
Author-email: Adrian Lyjak <adrianlyjak@gmail.com>
|
|
7
7
|
License: MIT
|
|
8
|
-
Requires-Dist: llama-deploy-core[client]>=0.3.
|
|
9
|
-
Requires-Dist: llama-deploy-appserver>=0.3.
|
|
8
|
+
Requires-Dist: llama-deploy-core[client]>=0.3.12,<0.4.0
|
|
9
|
+
Requires-Dist: llama-deploy-appserver>=0.3.12,<0.4.0
|
|
10
10
|
Requires-Dist: httpx>=0.24.0,<1.0.0
|
|
11
11
|
Requires-Dist: rich>=13.0.0
|
|
12
12
|
Requires-Dist: questionary>=2.0.0
|
|
@@ -17,7 +17,6 @@ Requires-Dist: textual>=6.0.0
|
|
|
17
17
|
Requires-Dist: aiohttp>=3.12.14
|
|
18
18
|
Requires-Dist: copier>=9.9.0
|
|
19
19
|
Requires-Dist: pyjwt[crypto]>=2.10.1
|
|
20
|
-
Requires-Dist: vibe-llama>=0.4.4,<0.5.0
|
|
21
20
|
Requires-Python: >=3.11, <4
|
|
22
21
|
Description-Content-Type: text/markdown
|
|
23
22
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "llamactl"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.12"
|
|
4
4
|
description = "A command-line interface for managing LlamaDeploy projects and deployments"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -9,8 +9,8 @@ authors = [
|
|
|
9
9
|
]
|
|
10
10
|
requires-python = ">=3.11, <4"
|
|
11
11
|
dependencies = [
|
|
12
|
-
"llama-deploy-core[client]>=0.3.
|
|
13
|
-
"llama-deploy-appserver>=0.3.
|
|
12
|
+
"llama-deploy-core[client]>=0.3.12,<0.4.0",
|
|
13
|
+
"llama-deploy-appserver>=0.3.12,<0.4.0",
|
|
14
14
|
"httpx>=0.24.0,<1.0.0",
|
|
15
15
|
"rich>=13.0.0",
|
|
16
16
|
"questionary>=2.0.0",
|
|
@@ -21,7 +21,6 @@ dependencies = [
|
|
|
21
21
|
"aiohttp>=3.12.14",
|
|
22
22
|
"copier>=9.9.0",
|
|
23
23
|
"pyjwt[crypto]>=2.10.1",
|
|
24
|
-
"vibe-llama>=0.4.4,<0.5.0",
|
|
25
24
|
]
|
|
26
25
|
|
|
27
26
|
[project.scripts]
|
|
@@ -9,6 +9,7 @@ import httpx
|
|
|
9
9
|
import jwt
|
|
10
10
|
from jwt.algorithms import RSAAlgorithm # type: ignore[possibly-unbound-import]
|
|
11
11
|
from llama_deploy.cli.config.schema import DeviceOIDC
|
|
12
|
+
from llama_deploy.core.client.ssl_util import get_httpx_verify_param
|
|
12
13
|
from pydantic import BaseModel
|
|
13
14
|
|
|
14
15
|
logger = logging.getLogger(__name__)
|
|
@@ -57,10 +58,13 @@ class AuthMeResponse(BaseModel):
|
|
|
57
58
|
class ClientContextManager(AsyncContextManager):
|
|
58
59
|
def __init__(self, base_url: str | None, auth: httpx.Auth | None = None) -> None:
|
|
59
60
|
self.base_url = base_url.rstrip("/") if base_url else None
|
|
61
|
+
verify = get_httpx_verify_param()
|
|
60
62
|
if self.base_url:
|
|
61
|
-
self.client = httpx.AsyncClient(
|
|
63
|
+
self.client = httpx.AsyncClient(
|
|
64
|
+
base_url=self.base_url, auth=auth, verify=verify
|
|
65
|
+
)
|
|
62
66
|
else:
|
|
63
|
-
self.client = httpx.AsyncClient(auth=auth)
|
|
67
|
+
self.client = httpx.AsyncClient(auth=auth, verify=verify)
|
|
64
68
|
|
|
65
69
|
async def close(self) -> None:
|
|
66
70
|
try:
|
|
@@ -9,12 +9,18 @@ from pathlib import Path
|
|
|
9
9
|
|
|
10
10
|
import click
|
|
11
11
|
import copier
|
|
12
|
+
import httpx
|
|
12
13
|
import questionary
|
|
13
14
|
from click.exceptions import Exit
|
|
14
15
|
from llama_deploy.cli.app import app
|
|
15
|
-
from llama_deploy.cli.options import
|
|
16
|
+
from llama_deploy.cli.options import (
|
|
17
|
+
global_options,
|
|
18
|
+
interactive_option,
|
|
19
|
+
)
|
|
16
20
|
from llama_deploy.cli.styles import HEADER_COLOR_HEX
|
|
21
|
+
from llama_deploy.core.client.ssl_util import get_httpx_verify_param
|
|
17
22
|
from rich import print as rprint
|
|
23
|
+
from rich.text import Text
|
|
18
24
|
|
|
19
25
|
|
|
20
26
|
@app.command()
|
|
@@ -40,23 +46,24 @@ from rich import print as rprint
|
|
|
40
46
|
help="Force overwrite the directory if it exists",
|
|
41
47
|
)
|
|
42
48
|
@global_options
|
|
49
|
+
@interactive_option
|
|
43
50
|
def init(
|
|
44
51
|
update: bool,
|
|
45
52
|
template: str | None,
|
|
46
53
|
dir: Path | None,
|
|
47
54
|
force: bool,
|
|
55
|
+
interactive: bool,
|
|
48
56
|
) -> None:
|
|
49
57
|
"""Create a new app repository from a template"""
|
|
50
58
|
if update:
|
|
51
59
|
_update()
|
|
52
60
|
else:
|
|
53
|
-
_create(template, dir, force)
|
|
54
|
-
|
|
61
|
+
_create(template, dir, force, interactive)
|
|
55
62
|
|
|
56
|
-
def _create(template: str | None, dir: Path | None, force: bool) -> None:
|
|
57
|
-
# defer loading to improve cli startup time
|
|
58
|
-
from vibe_llama.sdk import VibeLlamaStarter
|
|
59
63
|
|
|
64
|
+
def _create(
|
|
65
|
+
template: str | None, dir: Path | None, force: bool, interactive: bool
|
|
66
|
+
) -> None:
|
|
60
67
|
@dataclass
|
|
61
68
|
class TemplateOption:
|
|
62
69
|
id: str
|
|
@@ -79,6 +86,15 @@ def _create(template: str | None, dir: Path | None, force: bool) -> None:
|
|
|
79
86
|
),
|
|
80
87
|
llama_cloud=False,
|
|
81
88
|
),
|
|
89
|
+
TemplateOption(
|
|
90
|
+
id="showcase",
|
|
91
|
+
name="Showcase",
|
|
92
|
+
description="A collection of workflow and UI patterns to build LlamaDeploy apps",
|
|
93
|
+
source=GithubTemplateRepo(
|
|
94
|
+
url="https://github.com/run-llama/template-workflow-showcase"
|
|
95
|
+
),
|
|
96
|
+
llama_cloud=False,
|
|
97
|
+
),
|
|
82
98
|
TemplateOption(
|
|
83
99
|
id="document-qa",
|
|
84
100
|
name="Document Question & Answer",
|
|
@@ -156,7 +172,23 @@ def _create(template: str | None, dir: Path | None, force: bool) -> None:
|
|
|
156
172
|
),
|
|
157
173
|
]
|
|
158
174
|
|
|
159
|
-
if
|
|
175
|
+
# Initialize git repository if git is available
|
|
176
|
+
has_git = False
|
|
177
|
+
git_initialized = False
|
|
178
|
+
try:
|
|
179
|
+
subprocess.run(["git", "--version"], check=True, capture_output=True)
|
|
180
|
+
has_git = True
|
|
181
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
182
|
+
# git is not available or broken; continue without git
|
|
183
|
+
has_git = False
|
|
184
|
+
|
|
185
|
+
if not has_git:
|
|
186
|
+
rprint(
|
|
187
|
+
"git is required to initialize a template. Make sure you have it installed and available in your PATH."
|
|
188
|
+
)
|
|
189
|
+
raise Exit(1)
|
|
190
|
+
|
|
191
|
+
if template is None and interactive:
|
|
160
192
|
rprint(
|
|
161
193
|
"[bold]Select a template to start from.[/bold] Either with javascript frontend UI, or just a python workflow that can be used as an API."
|
|
162
194
|
)
|
|
@@ -182,16 +214,26 @@ def _create(template: str | None, dir: Path | None, force: bool) -> None:
|
|
|
182
214
|
),
|
|
183
215
|
).ask()
|
|
184
216
|
if template is None:
|
|
185
|
-
|
|
217
|
+
options = [o.id for o in ui_options + headless_options]
|
|
218
|
+
rprint(
|
|
219
|
+
Text(
|
|
220
|
+
f"No template selected. Select a template or pass a template name with --template <{'|'.join(options)}>"
|
|
221
|
+
)
|
|
222
|
+
)
|
|
186
223
|
raise Exit(1)
|
|
187
224
|
if dir is None:
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
225
|
+
if interactive:
|
|
226
|
+
dir_str = questionary.text(
|
|
227
|
+
"Enter the directory to create the new app in", default=template
|
|
228
|
+
).ask()
|
|
229
|
+
if dir_str:
|
|
230
|
+
dir = Path(dir_str)
|
|
231
|
+
else:
|
|
232
|
+
return
|
|
233
|
+
else:
|
|
234
|
+
rprint(f"[yellow]No directory provided. Defaulting to {template}[/]")
|
|
235
|
+
dir = Path(template)
|
|
236
|
+
|
|
195
237
|
resolved_template: TemplateOption | None = next(
|
|
196
238
|
(o for o in ui_options + headless_options if o.id == template), None
|
|
197
239
|
)
|
|
@@ -199,48 +241,44 @@ def _create(template: str | None, dir: Path | None, force: bool) -> None:
|
|
|
199
241
|
rprint(f"Template {template} not found")
|
|
200
242
|
raise Exit(1)
|
|
201
243
|
if dir.exists():
|
|
202
|
-
is_ok = (
|
|
203
|
-
|
|
204
|
-
|
|
244
|
+
is_ok = force or (
|
|
245
|
+
interactive
|
|
246
|
+
and questionary.confirm("Directory exists. Overwrite?", default=False).ask()
|
|
205
247
|
)
|
|
248
|
+
|
|
206
249
|
if not is_ok:
|
|
250
|
+
rprint(
|
|
251
|
+
f"[yellow]Try again with another directory or pass --force to overwrite the existing directory '{str(dir)}'[/]"
|
|
252
|
+
)
|
|
207
253
|
raise Exit(1)
|
|
208
254
|
else:
|
|
209
255
|
shutil.rmtree(dir, ignore_errors=True)
|
|
210
256
|
|
|
211
257
|
copier.run_copy(
|
|
212
|
-
resolved_template.source.url,
|
|
213
|
-
dir,
|
|
214
|
-
quiet=True,
|
|
258
|
+
resolved_template.source.url, dir, quiet=True, defaults=not interactive
|
|
215
259
|
)
|
|
216
|
-
# Initialize git repository if git is available
|
|
217
|
-
has_git = False
|
|
218
|
-
try:
|
|
219
|
-
subprocess.run(["git", "--version"], check=True, capture_output=True)
|
|
220
|
-
has_git = True
|
|
221
|
-
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
222
|
-
# git is not available or broken; continue without git
|
|
223
|
-
has_git = False
|
|
224
260
|
|
|
225
261
|
# Change to the new directory and initialize git repo
|
|
226
262
|
original_cwd = Path.cwd()
|
|
227
263
|
os.chdir(dir)
|
|
228
264
|
|
|
229
265
|
try:
|
|
230
|
-
# Dump in a bunch of docs for AI agents
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
+ (["LlamaCloud Services"] if resolved_template.llama_cloud else []),
|
|
266
|
+
# Dump in a bunch of docs for AI agents (best-effort)
|
|
267
|
+
docs_downloaded = asyncio.run(
|
|
268
|
+
_download_and_write_agents_md(
|
|
269
|
+
include_llama_cloud=resolved_template.llama_cloud
|
|
270
|
+
)
|
|
236
271
|
)
|
|
237
|
-
asyncio.run(vibe_llama_starter.write_instructions(overwrite=True))
|
|
238
272
|
# Create symlink for Claude.md to point to AGENTS.md
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
273
|
+
if docs_downloaded:
|
|
274
|
+
for alternate in [
|
|
275
|
+
"CLAUDE.md",
|
|
276
|
+
"GEMINI.md",
|
|
277
|
+
]: # don't support AGENTS.md (yet?)
|
|
278
|
+
claude_path = Path(alternate) # not supported yet
|
|
279
|
+
agents_path = Path("AGENTS.md")
|
|
280
|
+
if agents_path.exists() and not claude_path.exists():
|
|
281
|
+
claude_path.symlink_to("AGENTS.md")
|
|
244
282
|
|
|
245
283
|
# Initialize a git repo (best-effort). If anything fails, show a friendly note and continue.
|
|
246
284
|
if has_git:
|
|
@@ -252,6 +290,7 @@ def _create(template: str | None, dir: Path | None, force: bool) -> None:
|
|
|
252
290
|
check=True,
|
|
253
291
|
capture_output=True,
|
|
254
292
|
)
|
|
293
|
+
git_initialized = True
|
|
255
294
|
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
|
256
295
|
# Extract a short error message if present
|
|
257
296
|
err_msg = ""
|
|
@@ -295,7 +334,8 @@ def _create(template: str | None, dir: Path | None, force: bool) -> None:
|
|
|
295
334
|
rprint(" [orange3]uvx[/] llamactl serve")
|
|
296
335
|
rprint("")
|
|
297
336
|
rprint("[bold]To deploy:[/]")
|
|
298
|
-
if
|
|
337
|
+
# Only show manual git init steps if repository failed to initialize earlier
|
|
338
|
+
if not git_initialized:
|
|
299
339
|
rprint(" [orange3]git[/] init")
|
|
300
340
|
rprint(" [orange3]git[/] add .")
|
|
301
341
|
rprint(" [orange3]git[/] commit -m 'Initial commit'")
|
|
@@ -306,7 +346,7 @@ def _create(template: str | None, dir: Path | None, force: bool) -> None:
|
|
|
306
346
|
rprint(" [orange3]git[/] push -u origin main")
|
|
307
347
|
rprint("")
|
|
308
348
|
# rprint(" [orange3]uvx[/] llamactl login")
|
|
309
|
-
rprint(" [orange3]uvx[/] llamactl deploy")
|
|
349
|
+
rprint(" [orange3]uvx[/] llamactl deploy create")
|
|
310
350
|
rprint("")
|
|
311
351
|
|
|
312
352
|
|
|
@@ -357,3 +397,61 @@ def _update():
|
|
|
357
397
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
358
398
|
# Git not available or not in a git repo - continue silently
|
|
359
399
|
pass
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
async def _download_and_write_agents_md(include_llama_cloud: bool) -> bool:
|
|
403
|
+
"""Fetch a small set of reference docs and write AGENTS.md.
|
|
404
|
+
|
|
405
|
+
Replaces the previous vibe-llama usage with direct HTTP downloads.
|
|
406
|
+
|
|
407
|
+
Returns True if any documentation was fetched, False otherwise.
|
|
408
|
+
"""
|
|
409
|
+
BASE_URL = "https://raw.githubusercontent.com/run-llama/vibe-llama/main"
|
|
410
|
+
|
|
411
|
+
services: dict[str, str] = {
|
|
412
|
+
"LlamaIndex": f"{BASE_URL}/documentation/llamaindex.md",
|
|
413
|
+
"LlamaCloud Services": f"{BASE_URL}/documentation/llamacloud.md",
|
|
414
|
+
"llama-index-workflows": f"{BASE_URL}/documentation/llama-index-workflows.md",
|
|
415
|
+
"LlamaDeploy": f"{BASE_URL}/documentation/llamadeploy.md",
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
selected_services: list[str] = [
|
|
419
|
+
"LlamaDeploy",
|
|
420
|
+
"LlamaIndex",
|
|
421
|
+
"llama-index-workflows",
|
|
422
|
+
]
|
|
423
|
+
if include_llama_cloud:
|
|
424
|
+
selected_services.append("LlamaCloud Services")
|
|
425
|
+
|
|
426
|
+
urls: list[str] = [(s, u) for s in selected_services if (u := services.get(s))]
|
|
427
|
+
|
|
428
|
+
contents: list[str] = []
|
|
429
|
+
timeout = httpx.Timeout(5.0)
|
|
430
|
+
async with httpx.AsyncClient(
|
|
431
|
+
timeout=timeout, verify=get_httpx_verify_param()
|
|
432
|
+
) as client:
|
|
433
|
+
|
|
434
|
+
async def get_docs(service: str, url: str) -> str | None:
|
|
435
|
+
try:
|
|
436
|
+
resp = await client.get(url)
|
|
437
|
+
resp.raise_for_status()
|
|
438
|
+
text = resp.text.strip()
|
|
439
|
+
if text:
|
|
440
|
+
return text
|
|
441
|
+
except Exception:
|
|
442
|
+
# best-effort: skip failures
|
|
443
|
+
rprint(
|
|
444
|
+
f"[yellow]Failed to fetch documentation for {service}, skipping[/]"
|
|
445
|
+
)
|
|
446
|
+
return None
|
|
447
|
+
|
|
448
|
+
results = await asyncio.gather(
|
|
449
|
+
*[get_docs(service, url) for service, url in urls]
|
|
450
|
+
)
|
|
451
|
+
contents = [r for r in results if r is not None]
|
|
452
|
+
|
|
453
|
+
if contents:
|
|
454
|
+
agents_md = "\n\n---\n\n".join(contents) + "\n"
|
|
455
|
+
Path("AGENTS.md").write_text(agents_md, encoding="utf-8")
|
|
456
|
+
|
|
457
|
+
return bool(contents)
|
|
@@ -10,7 +10,7 @@ from click.exceptions import Abort, Exit
|
|
|
10
10
|
from llama_deploy.cli.commands.auth import validate_authenticated_profile
|
|
11
11
|
from llama_deploy.cli.config.env_service import service
|
|
12
12
|
from llama_deploy.cli.config.schema import Auth
|
|
13
|
-
from llama_deploy.cli.options import interactive_option
|
|
13
|
+
from llama_deploy.cli.options import global_options, interactive_option
|
|
14
14
|
from llama_deploy.cli.styles import WARNING
|
|
15
15
|
from llama_deploy.cli.utils.redact import redact_api_key
|
|
16
16
|
from llama_deploy.core.client.manage_client import ControlPlaneClient
|
|
@@ -72,7 +72,13 @@ logger = logging.getLogger(__name__)
|
|
|
72
72
|
type=click.Path(dir_okay=True, resolve_path=True, path_type=Path),
|
|
73
73
|
help="The path to the sqlite database to use for the workflow server if using local persistence",
|
|
74
74
|
)
|
|
75
|
+
@click.option(
|
|
76
|
+
"--host",
|
|
77
|
+
type=str,
|
|
78
|
+
help="The host to run the API server on. Default is 127.0.0.1. Use 0.0.0.0 to allow remote access.",
|
|
79
|
+
)
|
|
75
80
|
@interactive_option
|
|
81
|
+
@global_options
|
|
76
82
|
def serve(
|
|
77
83
|
deployment_file: Path,
|
|
78
84
|
no_install: bool,
|
|
@@ -85,6 +91,7 @@ def serve(
|
|
|
85
91
|
log_format: str | None = None,
|
|
86
92
|
persistence: Literal["memory", "local", "cloud"] | None = None,
|
|
87
93
|
local_persistence_path: Path | None = None,
|
|
94
|
+
host: str | None = None,
|
|
88
95
|
interactive: bool = False,
|
|
89
96
|
) -> None:
|
|
90
97
|
"""Run llama_deploy API Server in the foreground. Reads the deployment configuration from the current directory. Can optionally specify a deployment file path."""
|
|
@@ -92,6 +99,16 @@ def serve(
|
|
|
92
99
|
rprint(f"[red]Deployment file '{deployment_file}' not found[/red]")
|
|
93
100
|
raise click.Abort()
|
|
94
101
|
|
|
102
|
+
# Early check: appserver requires a pyproject.toml in the config directory
|
|
103
|
+
config_dir = deployment_file if deployment_file.is_dir() else deployment_file.parent
|
|
104
|
+
if not (config_dir / "pyproject.toml").exists():
|
|
105
|
+
rprint(
|
|
106
|
+
"[red]No pyproject.toml found at[/red] "
|
|
107
|
+
f"[bold]{config_dir}[/bold].\n"
|
|
108
|
+
"Add a pyproject.toml to your project and re-run 'llamactl serve'."
|
|
109
|
+
)
|
|
110
|
+
raise click.Abort()
|
|
111
|
+
|
|
95
112
|
try:
|
|
96
113
|
# Pre-check: if the template requires llama cloud access, ensure credentials
|
|
97
114
|
_maybe_inject_llama_cloud_credentials(
|
|
@@ -131,6 +148,7 @@ def serve(
|
|
|
131
148
|
cloud_persistence_name=f"_public:serve_workflows_{deployment_config.name}"
|
|
132
149
|
if persistence == "cloud"
|
|
133
150
|
else None,
|
|
151
|
+
host=host,
|
|
134
152
|
)
|
|
135
153
|
|
|
136
154
|
except (Exit, Abort):
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import os
|
|
2
3
|
from typing import Callable, ParamSpec, TypeVar
|
|
3
4
|
|
|
4
5
|
import click
|
|
@@ -13,6 +14,52 @@ R = TypeVar("R")
|
|
|
13
14
|
def global_options(f: Callable[P, R]) -> Callable[P, R]:
|
|
14
15
|
"""Common decorator to add global options to command groups"""
|
|
15
16
|
|
|
17
|
+
return native_tls_option(file_logging(f))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def interactive_option(f: Callable[P, R]) -> Callable[P, R]:
|
|
21
|
+
"""Add an interactive option to the command"""
|
|
22
|
+
|
|
23
|
+
default = is_interactive_session()
|
|
24
|
+
return click.option(
|
|
25
|
+
"--interactive/--no-interactive",
|
|
26
|
+
help="Run in interactive mode. If not provided, will default to the current session's interactive state.",
|
|
27
|
+
is_flag=True,
|
|
28
|
+
default=default,
|
|
29
|
+
)(f)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def native_tls_option(f: Callable[P, R]) -> Callable[P, R]:
|
|
33
|
+
"""Enable native TLS to trust system configured trust store rather than python bundled trust stores.
|
|
34
|
+
|
|
35
|
+
When enabled, we set:
|
|
36
|
+
- UV_NATIVE_TLS=1 to instruct uv to use the platform trust store
|
|
37
|
+
- LLAMA_DEPLOY_USE_TRUSTSTORE=1 to use system certificate store for Python httpx clients
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def _enable_native_tls(
|
|
41
|
+
ctx: click.Context, param: click.Parameter, value: bool
|
|
42
|
+
) -> bool:
|
|
43
|
+
if value:
|
|
44
|
+
# Don't override if user explicitly set a value
|
|
45
|
+
os.environ.setdefault("UV_NATIVE_TLS", "1")
|
|
46
|
+
os.environ.setdefault("LLAMA_DEPLOY_USE_TRUSTSTORE", "1")
|
|
47
|
+
return value
|
|
48
|
+
|
|
49
|
+
return click.option(
|
|
50
|
+
"--native-tls",
|
|
51
|
+
is_flag=True,
|
|
52
|
+
help=(
|
|
53
|
+
"Enable native TLS mode to use system certificate store rather than runtime defaults. Can be set via LLAMACTL_NATIVE_TLS=1"
|
|
54
|
+
),
|
|
55
|
+
callback=_enable_native_tls,
|
|
56
|
+
expose_value=False,
|
|
57
|
+
is_eager=True,
|
|
58
|
+
envvar=["LLAMACTL_NATIVE_TLS"],
|
|
59
|
+
)(f)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def file_logging(f: Callable[P, R]) -> Callable[P, R]:
|
|
16
63
|
def debug_callback(ctx: click.Context, param: click.Parameter, value: str) -> str:
|
|
17
64
|
if value:
|
|
18
65
|
setup_file_logging(level=logging._nameToLevel.get(value, logging.INFO))
|
|
@@ -29,15 +76,3 @@ def global_options(f: Callable[P, R]) -> Callable[P, R]:
|
|
|
29
76
|
is_eager=True,
|
|
30
77
|
hidden=True,
|
|
31
78
|
)(f)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def interactive_option(f: Callable[P, R]) -> Callable[P, R]:
|
|
35
|
-
"""Add an interactive option to the command"""
|
|
36
|
-
|
|
37
|
-
default = is_interactive_session()
|
|
38
|
-
return click.option(
|
|
39
|
-
"--interactive/--no-interactive",
|
|
40
|
-
help="Run in interactive mode. If not provided, will default to the current session's interactive state.",
|
|
41
|
-
is_flag=True,
|
|
42
|
-
default=default,
|
|
43
|
-
)(f)
|
|
@@ -5,6 +5,7 @@ import logging
|
|
|
5
5
|
import re
|
|
6
6
|
from dataclasses import dataclass, field
|
|
7
7
|
from pathlib import Path
|
|
8
|
+
from textwrap import dedent
|
|
8
9
|
from urllib.parse import urlsplit
|
|
9
10
|
|
|
10
11
|
from llama_deploy.cli.client import get_project_client as get_client
|
|
@@ -23,6 +24,7 @@ from llama_deploy.cli.textual.git_validation import (
|
|
|
23
24
|
ValidationResultMessage,
|
|
24
25
|
)
|
|
25
26
|
from llama_deploy.cli.textual.secrets_form import SecretsWidget
|
|
27
|
+
from llama_deploy.cli.utils.version import get_installed_appserver_version
|
|
26
28
|
from llama_deploy.core.deployment_config import (
|
|
27
29
|
DEFAULT_DEPLOYMENT_NAME,
|
|
28
30
|
read_deployment_config,
|
|
@@ -40,6 +42,7 @@ from llama_deploy.core.schema.deployments import (
|
|
|
40
42
|
DeploymentResponse,
|
|
41
43
|
DeploymentUpdate,
|
|
42
44
|
)
|
|
45
|
+
from packaging.version import Version
|
|
43
46
|
from textual import events
|
|
44
47
|
from textual.app import App, ComposeResult
|
|
45
48
|
from textual.containers import Container, HorizontalGroup, Widget
|
|
@@ -47,7 +50,7 @@ from textual.content import Content
|
|
|
47
50
|
from textual.message import Message
|
|
48
51
|
from textual.reactive import reactive
|
|
49
52
|
from textual.validation import Length
|
|
50
|
-
from textual.widgets import Button, Input, Label, Static
|
|
53
|
+
from textual.widgets import Button, Input, Label, Select, Static
|
|
51
54
|
|
|
52
55
|
|
|
53
56
|
@dataclass
|
|
@@ -75,11 +78,20 @@ class DeploymentForm:
|
|
|
75
78
|
warnings: list[str] = field(default_factory=list)
|
|
76
79
|
# env info
|
|
77
80
|
env_info_messages: str | None = None
|
|
81
|
+
# appserver version fields
|
|
82
|
+
installed_appserver_version: str | None = None
|
|
83
|
+
existing_llama_deploy_version: str | None = None
|
|
84
|
+
selected_appserver_version: str | None = None
|
|
78
85
|
|
|
79
86
|
@classmethod
|
|
80
87
|
def from_deployment(cls, deployment: DeploymentResponse) -> "DeploymentForm":
|
|
81
88
|
secret_names = deployment.secret_names or []
|
|
82
89
|
|
|
90
|
+
installed = get_installed_appserver_version()
|
|
91
|
+
existing = deployment.llama_deploy_version
|
|
92
|
+
# If versions match (or existing is None), treat as non-editable like create
|
|
93
|
+
selected = existing or installed
|
|
94
|
+
|
|
83
95
|
return DeploymentForm(
|
|
84
96
|
name=deployment.name,
|
|
85
97
|
id=deployment.id,
|
|
@@ -91,8 +103,15 @@ class DeploymentForm:
|
|
|
91
103
|
secrets={},
|
|
92
104
|
initial_secrets=set(secret_names),
|
|
93
105
|
is_editing=True,
|
|
106
|
+
installed_appserver_version=installed,
|
|
107
|
+
existing_llama_deploy_version=existing,
|
|
108
|
+
selected_appserver_version=selected,
|
|
94
109
|
)
|
|
95
110
|
|
|
111
|
+
@staticmethod
|
|
112
|
+
def appserver_version() -> str | None:
|
|
113
|
+
return get_installed_appserver_version()
|
|
114
|
+
|
|
96
115
|
def to_update(self) -> DeploymentUpdate:
|
|
97
116
|
"""Convert form data to API format"""
|
|
98
117
|
|
|
@@ -100,6 +119,8 @@ class DeploymentForm:
|
|
|
100
119
|
for secret in self.removed_secrets:
|
|
101
120
|
secrets[secret] = None
|
|
102
121
|
|
|
122
|
+
appserver_version = self.selected_appserver_version
|
|
123
|
+
|
|
103
124
|
data = DeploymentUpdate(
|
|
104
125
|
repo_url=self.repo_url,
|
|
105
126
|
git_ref=self.git_ref or "main",
|
|
@@ -110,12 +131,14 @@ class DeploymentForm:
|
|
|
110
131
|
else self.personal_access_token
|
|
111
132
|
),
|
|
112
133
|
secrets=secrets,
|
|
134
|
+
llama_deploy_version=appserver_version,
|
|
113
135
|
)
|
|
114
136
|
|
|
115
137
|
return data
|
|
116
138
|
|
|
117
139
|
def to_create(self) -> DeploymentCreate:
|
|
118
140
|
"""Convert form data to API format"""
|
|
141
|
+
appserver_version = self.selected_appserver_version
|
|
119
142
|
|
|
120
143
|
return DeploymentCreate(
|
|
121
144
|
name=self.name,
|
|
@@ -124,6 +147,7 @@ class DeploymentForm:
|
|
|
124
147
|
git_ref=self.git_ref or "main",
|
|
125
148
|
personal_access_token=self.personal_access_token,
|
|
126
149
|
secrets=self.secrets,
|
|
150
|
+
llama_deploy_version=appserver_version,
|
|
127
151
|
)
|
|
128
152
|
|
|
129
153
|
|
|
@@ -149,12 +173,23 @@ class DeploymentFormWidget(Widget):
|
|
|
149
173
|
def compose(self) -> ComposeResult:
|
|
150
174
|
title = "Edit Deployment" if self.form_data.is_editing else "Create Deployment"
|
|
151
175
|
|
|
152
|
-
|
|
153
|
-
Content.from_markup(
|
|
154
|
-
f"{title} [italic][@click=app.show_help()]More info[/][/italic]"
|
|
155
|
-
),
|
|
176
|
+
with HorizontalGroup(
|
|
156
177
|
classes="primary-message",
|
|
157
|
-
)
|
|
178
|
+
):
|
|
179
|
+
yield Static(
|
|
180
|
+
Content.from_markup(
|
|
181
|
+
f"{title} [italic][@click=app.show_help()]More info[/][/]"
|
|
182
|
+
),
|
|
183
|
+
classes="w-1fr",
|
|
184
|
+
)
|
|
185
|
+
yield Static(
|
|
186
|
+
Content.from_markup(
|
|
187
|
+
dedent("""
|
|
188
|
+
[italic]Tab or click to navigate.[/]
|
|
189
|
+
""").strip()
|
|
190
|
+
),
|
|
191
|
+
classes="text-right w-1fr",
|
|
192
|
+
)
|
|
158
193
|
yield Static(
|
|
159
194
|
self.error_message,
|
|
160
195
|
id="error-message",
|
|
@@ -200,6 +235,11 @@ class DeploymentFormWidget(Widget):
|
|
|
200
235
|
compact=True,
|
|
201
236
|
)
|
|
202
237
|
|
|
238
|
+
yield Static(classes="full-width")
|
|
239
|
+
yield Static(
|
|
240
|
+
Content.from_markup("[italic]Advanced[/]"),
|
|
241
|
+
classes="text-center full-width",
|
|
242
|
+
)
|
|
203
243
|
yield Label("Config File:", classes="form-label", shrink=True)
|
|
204
244
|
yield Input(
|
|
205
245
|
value=self.form_data.deployment_file_path,
|
|
@@ -207,6 +247,7 @@ class DeploymentFormWidget(Widget):
|
|
|
207
247
|
id="deployment_file_path",
|
|
208
248
|
compact=True,
|
|
209
249
|
)
|
|
250
|
+
|
|
210
251
|
yield Label("Personal Access Token:", classes="form-label", shrink=True)
|
|
211
252
|
if self.form_data.has_existing_pat:
|
|
212
253
|
yield Button(
|
|
@@ -226,6 +267,45 @@ class DeploymentFormWidget(Widget):
|
|
|
226
267
|
compact=True,
|
|
227
268
|
)
|
|
228
269
|
|
|
270
|
+
# Appserver version display/selector
|
|
271
|
+
yield Label("Appserver Version:", classes="form-label", shrink=True)
|
|
272
|
+
versions_differ = (
|
|
273
|
+
self.form_data.is_editing
|
|
274
|
+
and self.form_data.installed_appserver_version
|
|
275
|
+
and self.form_data.existing_llama_deploy_version
|
|
276
|
+
and self.form_data.installed_appserver_version
|
|
277
|
+
!= self.form_data.existing_llama_deploy_version
|
|
278
|
+
)
|
|
279
|
+
if versions_differ:
|
|
280
|
+
# Show dropdown selector for version choice
|
|
281
|
+
installed_version = self.form_data.installed_appserver_version
|
|
282
|
+
existing_version = self.form_data.existing_llama_deploy_version
|
|
283
|
+
current_selection = (
|
|
284
|
+
self.form_data.selected_appserver_version
|
|
285
|
+
or existing_version
|
|
286
|
+
or installed_version
|
|
287
|
+
)
|
|
288
|
+
is_upgrade = Version(installed_version) > Version(existing_version)
|
|
289
|
+
label = "Upgrade" if is_upgrade else "Downgrade"
|
|
290
|
+
yield Select(
|
|
291
|
+
[
|
|
292
|
+
(f"{label} to {installed_version}", installed_version),
|
|
293
|
+
(f"Keep {existing_version}", existing_version),
|
|
294
|
+
],
|
|
295
|
+
value=current_selection,
|
|
296
|
+
id="appserver_version_select",
|
|
297
|
+
allow_blank=False,
|
|
298
|
+
compact=True,
|
|
299
|
+
)
|
|
300
|
+
else:
|
|
301
|
+
# Non-editable display of version
|
|
302
|
+
readonly_version = (
|
|
303
|
+
self.form_data.installed_appserver_version
|
|
304
|
+
or self.form_data.existing_llama_deploy_version
|
|
305
|
+
or "unknown"
|
|
306
|
+
)
|
|
307
|
+
yield Static(readonly_version, id="appserver_version_readonly")
|
|
308
|
+
|
|
229
309
|
# Secrets section
|
|
230
310
|
yield SecretsWidget(
|
|
231
311
|
initial_secrets=self.form_data.secrets,
|
|
@@ -249,6 +329,13 @@ class DeploymentFormWidget(Widget):
|
|
|
249
329
|
# Post message to parent app to handle cancel
|
|
250
330
|
self.post_message(CancelFormMessage())
|
|
251
331
|
|
|
332
|
+
def on_select_changed(self, event: Select.Changed) -> None:
|
|
333
|
+
"""Handle version selection changes"""
|
|
334
|
+
if event.select.id == "appserver_version_select" and event.value:
|
|
335
|
+
updated_form = dataclasses.replace(self.resolve_form_data())
|
|
336
|
+
updated_form.selected_appserver_version = str(event.value)
|
|
337
|
+
self.form_data = updated_form
|
|
338
|
+
|
|
252
339
|
def _save(self) -> None:
|
|
253
340
|
self.form_data = self.resolve_form_data()
|
|
254
341
|
if self._validate_form():
|
|
@@ -318,6 +405,9 @@ class DeploymentFormWidget(Widget):
|
|
|
318
405
|
removed_secrets=self.original_form_data.initial_secrets.difference(
|
|
319
406
|
updated_prior_secrets
|
|
320
407
|
),
|
|
408
|
+
installed_appserver_version=self.form_data.installed_appserver_version,
|
|
409
|
+
existing_llama_deploy_version=self.form_data.existing_llama_deploy_version,
|
|
410
|
+
selected_appserver_version=self.form_data.selected_appserver_version,
|
|
321
411
|
)
|
|
322
412
|
|
|
323
413
|
|
|
@@ -583,6 +673,8 @@ def _initialize_deployment_data() -> DeploymentForm:
|
|
|
583
673
|
if len(secrets) > 0:
|
|
584
674
|
env_info_message = "Secrets were automatically seeded from your .env file. Remove or change any that should not be set. They must be manually configured after creation."
|
|
585
675
|
|
|
676
|
+
installed = get_installed_appserver_version()
|
|
677
|
+
|
|
586
678
|
form = DeploymentForm(
|
|
587
679
|
name=name or "",
|
|
588
680
|
repo_url=repo_url or "",
|
|
@@ -591,6 +683,8 @@ def _initialize_deployment_data() -> DeploymentForm:
|
|
|
591
683
|
deployment_file_path=config_file_path or "",
|
|
592
684
|
warnings=warnings,
|
|
593
685
|
env_info_messages=env_info_message,
|
|
686
|
+
installed_appserver_version=installed,
|
|
687
|
+
selected_appserver_version=installed,
|
|
594
688
|
)
|
|
595
689
|
return form
|
|
596
690
|
|
|
@@ -39,6 +39,12 @@ class DeploymentHelpWidget(Widget):
|
|
|
39
39
|
[b]Config File[/b]
|
|
40
40
|
Path to a directory or file containing a `[slategrey reverse]pyproject.toml[/]` or `[slategrey reverse]llama_deploy.yaml[/]` containing the llama deploy configuration. Only necessary if you have the configuration not at the root of the repo, or you have an unconventional configuration file.
|
|
41
41
|
|
|
42
|
+
[b]Personal Access Token[/b]
|
|
43
|
+
A personal access token to access the git repository. Can be used instead of the github integration.
|
|
44
|
+
|
|
45
|
+
[b]Appserver Version[/b]
|
|
46
|
+
The version of the appserver to deploy. Affects features and functionality. By default this is set to the current llamactl version, and then retained until manually upgraded.
|
|
47
|
+
|
|
42
48
|
[b]Secrets[/b]
|
|
43
49
|
Secrets to add as environment variables to the deployment. e.g. to access a database or an API. Supports adding in `[slategrey reverse].env[/]` file format.
|
|
44
50
|
|
|
@@ -60,6 +60,10 @@ Input.disabled {
|
|
|
60
60
|
/* UTILITIES, MESSAGES & NOTIFICATIONS */
|
|
61
61
|
/* =============================================== */
|
|
62
62
|
|
|
63
|
+
.mb-0 {
|
|
64
|
+
margin-bottom: 0 !important;
|
|
65
|
+
}
|
|
66
|
+
|
|
63
67
|
.mb-1 {
|
|
64
68
|
margin-bottom: 1;
|
|
65
69
|
}
|
|
@@ -68,6 +72,10 @@ Input.disabled {
|
|
|
68
72
|
margin-bottom: 2;
|
|
69
73
|
}
|
|
70
74
|
|
|
75
|
+
.mt-0 {
|
|
76
|
+
margin-top: 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
71
79
|
.mt-1 {
|
|
72
80
|
margin-top: 1;
|
|
73
81
|
}
|
|
@@ -76,6 +84,10 @@ Input.disabled {
|
|
|
76
84
|
margin-top: 2;
|
|
77
85
|
}
|
|
78
86
|
|
|
87
|
+
.m-0 {
|
|
88
|
+
margin: 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
79
91
|
.m-1 {
|
|
80
92
|
margin: 1;
|
|
81
93
|
}
|
|
@@ -102,6 +114,7 @@ Input.disabled {
|
|
|
102
114
|
padding: 0 0 0 1
|
|
103
115
|
}
|
|
104
116
|
|
|
117
|
+
|
|
105
118
|
.error-message {
|
|
106
119
|
color: $text-error;
|
|
107
120
|
background: $error-muted;
|
|
@@ -201,3 +214,102 @@ Button.secondary {
|
|
|
201
214
|
align: left middle;
|
|
202
215
|
}
|
|
203
216
|
|
|
217
|
+
|
|
218
|
+
.top-left {
|
|
219
|
+
align: left top;
|
|
220
|
+
}
|
|
221
|
+
.top-center {
|
|
222
|
+
align: left middle;
|
|
223
|
+
}
|
|
224
|
+
.top-right {
|
|
225
|
+
align: right top;
|
|
226
|
+
}
|
|
227
|
+
.middle-left {
|
|
228
|
+
align: left middle;
|
|
229
|
+
}
|
|
230
|
+
.middle-center {
|
|
231
|
+
align: center middle;
|
|
232
|
+
}
|
|
233
|
+
.middle-right {
|
|
234
|
+
align: right middle;
|
|
235
|
+
}
|
|
236
|
+
.bottom-left {
|
|
237
|
+
align: left bottom;
|
|
238
|
+
}
|
|
239
|
+
.bottom-center {
|
|
240
|
+
align: right bottom;
|
|
241
|
+
}
|
|
242
|
+
.bottom-right {
|
|
243
|
+
align: right bottom;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
.text-right {
|
|
248
|
+
text-align: right;
|
|
249
|
+
}
|
|
250
|
+
.text-left {
|
|
251
|
+
text-align: left;
|
|
252
|
+
}
|
|
253
|
+
.text-center {
|
|
254
|
+
text-align: center;
|
|
255
|
+
}
|
|
256
|
+
.text-justify {
|
|
257
|
+
text-align: justify;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.background-red {
|
|
261
|
+
background: red;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
.w-1 {
|
|
266
|
+
width: 1;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.w-2 {
|
|
270
|
+
width: 2;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.w-3 {
|
|
274
|
+
width: 3;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.w-1fr {
|
|
278
|
+
width: 1fr;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.w-100 {
|
|
282
|
+
width: 100%;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.w-2fr {
|
|
286
|
+
width: 2fr;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.h-1 {
|
|
290
|
+
height: 1;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.h-2 {
|
|
294
|
+
height: 2;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.h-3 {
|
|
298
|
+
height: 3;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.h-1fr {
|
|
302
|
+
height: 1fr;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.h-100 {
|
|
306
|
+
height: 100%;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.h-2fr {
|
|
310
|
+
height: 2fr;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.h-100 {
|
|
314
|
+
height: 100%;
|
|
315
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Version utilities shared across CLI components."""
|
|
2
|
+
|
|
3
|
+
from importlib import metadata as importlib_metadata
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_installed_appserver_version() -> str | None:
|
|
7
|
+
"""Return the installed version of `llama-deploy-appserver`, if available."""
|
|
8
|
+
try:
|
|
9
|
+
return importlib_metadata.version("llama-deploy-appserver")
|
|
10
|
+
except Exception:
|
|
11
|
+
return None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/config/migrations/0002_add_auth_fields.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{llamactl-0.3.10 → llamactl-0.3.12}/src/llama_deploy/cli/interactive_prompts/session_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|