llamactl 0.3.9__tar.gz → 0.3.11__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.9 → llamactl-0.3.11}/PKG-INFO +3 -3
- {llamactl-0.3.9 → llamactl-0.3.11}/pyproject.toml +3 -3
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/commands/init.py +38 -23
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/commands/serve.py +98 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/config/auth_service.py +2 -5
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/textual/deployment_form.py +100 -6
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/textual/deployment_help.py +6 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/textual/styles.tcss +112 -0
- llamactl-0.3.11/src/llama_deploy/cli/utils/redact.py +29 -0
- llamactl-0.3.11/src/llama_deploy/cli/utils/version.py +11 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/README.md +0 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/__init__.py +0 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/app.py +0 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/auth/client.py +0 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/client.py +0 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/commands/aliased_group.py +0 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/commands/auth.py +0 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/commands/deployment.py +0 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/commands/env.py +0 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/config/_config.py +0 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/config/_migrations.py +0 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/config/env_service.py +0 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/config/migrations/0001_init.sql +0 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/config/migrations/0002_add_auth_fields.sql +0 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/config/migrations/__init__.py +0 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/config/schema.py +0 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/debug.py +0 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/env.py +0 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/interactive_prompts/session_utils.py +0 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/interactive_prompts/utils.py +0 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/options.py +0 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/py.typed +0 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/styles.py +0 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/textual/deployment_monitor.py +0 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/textual/git_validation.py +0 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/textual/github_callback_server.py +0 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/textual/llama_loader.py +0 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/textual/secrets_form.py +0 -0
- {llamactl-0.3.9 → llamactl-0.3.11}/src/llama_deploy/cli/utils/env_inject.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.11
|
|
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.11,<0.4.0
|
|
9
|
+
Requires-Dist: llama-deploy-appserver>=0.3.11,<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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "llamactl"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.11"
|
|
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.11,<0.4.0",
|
|
13
|
+
"llama-deploy-appserver>=0.3.11,<0.4.0",
|
|
14
14
|
"httpx>=0.24.0,<1.0.0",
|
|
15
15
|
"rich>=13.0.0",
|
|
16
16
|
"questionary>=2.0.0",
|
|
@@ -55,8 +55,6 @@ def init(
|
|
|
55
55
|
|
|
56
56
|
def _create(template: str | None, dir: Path | None, force: bool) -> None:
|
|
57
57
|
# defer loading to improve cli startup time
|
|
58
|
-
from vibe_llama.scaffold import create_scaffold
|
|
59
|
-
from vibe_llama.scaffold.scaffold import ProjectName
|
|
60
58
|
from vibe_llama.sdk import VibeLlamaStarter
|
|
61
59
|
|
|
62
60
|
@dataclass
|
|
@@ -64,13 +62,9 @@ def _create(template: str | None, dir: Path | None, force: bool) -> None:
|
|
|
64
62
|
id: str
|
|
65
63
|
name: str
|
|
66
64
|
description: str
|
|
67
|
-
source:
|
|
65
|
+
source: GithubTemplateRepo
|
|
68
66
|
llama_cloud: bool
|
|
69
67
|
|
|
70
|
-
@dataclass
|
|
71
|
-
class VibeLlamaTemplate:
|
|
72
|
-
name: ProjectName
|
|
73
|
-
|
|
74
68
|
@dataclass
|
|
75
69
|
class GithubTemplateRepo:
|
|
76
70
|
url: str
|
|
@@ -85,6 +79,15 @@ def _create(template: str | None, dir: Path | None, force: bool) -> None:
|
|
|
85
79
|
),
|
|
86
80
|
llama_cloud=False,
|
|
87
81
|
),
|
|
82
|
+
TemplateOption(
|
|
83
|
+
id="showcase",
|
|
84
|
+
name="Showcase",
|
|
85
|
+
description="A collection of workflow and UI patterns to build LlamaDeploy apps",
|
|
86
|
+
source=GithubTemplateRepo(
|
|
87
|
+
url="https://github.com/run-llama/template-workflow-showcase"
|
|
88
|
+
),
|
|
89
|
+
llama_cloud=False,
|
|
90
|
+
),
|
|
88
91
|
TemplateOption(
|
|
89
92
|
id="document-qa",
|
|
90
93
|
name="Document Question & Answer",
|
|
@@ -110,42 +113,54 @@ def _create(template: str | None, dir: Path | None, force: bool) -> None:
|
|
|
110
113
|
id="basic",
|
|
111
114
|
name="Basic Workflow",
|
|
112
115
|
description="A base example that showcases usage patterns for workflows",
|
|
113
|
-
source=
|
|
116
|
+
source=GithubTemplateRepo(
|
|
117
|
+
url="https://github.com/run-llama/template-workflow-basic"
|
|
118
|
+
),
|
|
114
119
|
llama_cloud=False,
|
|
115
120
|
),
|
|
116
121
|
TemplateOption(
|
|
117
122
|
id="document_parsing",
|
|
118
123
|
name="Document Parser",
|
|
119
124
|
description="A workflow that, using LlamaParse, parses unstructured documents and returns their raw text content",
|
|
120
|
-
source=
|
|
125
|
+
source=GithubTemplateRepo(
|
|
126
|
+
url="https://github.com/run-llama/template-workflow-document-parsing"
|
|
127
|
+
),
|
|
121
128
|
llama_cloud=True,
|
|
122
129
|
),
|
|
123
130
|
TemplateOption(
|
|
124
131
|
id="human_in_the_loop",
|
|
125
132
|
name="Human in the Loop",
|
|
126
133
|
description="A workflow showcasing how to use human in the loop with LlamaIndex workflows",
|
|
127
|
-
source=
|
|
134
|
+
source=GithubTemplateRepo(
|
|
135
|
+
url="https://github.com/run-llama/template-workflow-human-in-the-loop"
|
|
136
|
+
),
|
|
128
137
|
llama_cloud=False,
|
|
129
138
|
),
|
|
130
139
|
TemplateOption(
|
|
131
140
|
id="invoice_extraction",
|
|
132
141
|
name="Invoice Extraction",
|
|
133
142
|
description="A workflow that, given an invoice, extracts several key details using LlamaExtract",
|
|
134
|
-
source=
|
|
143
|
+
source=GithubTemplateRepo(
|
|
144
|
+
url="https://github.com/run-llama/template-workflow-invoice-extraction"
|
|
145
|
+
),
|
|
135
146
|
llama_cloud=True,
|
|
136
147
|
),
|
|
137
148
|
TemplateOption(
|
|
138
149
|
id="rag",
|
|
139
150
|
name="RAG",
|
|
140
151
|
description="A workflow that embeds, indexes and queries your documents on the fly, providing you with a simple RAG pipeline",
|
|
141
|
-
source=
|
|
152
|
+
source=GithubTemplateRepo(
|
|
153
|
+
url="https://github.com/run-llama/template-workflow-rag"
|
|
154
|
+
),
|
|
142
155
|
llama_cloud=False,
|
|
143
156
|
),
|
|
144
157
|
TemplateOption(
|
|
145
158
|
id="web_scraping",
|
|
146
159
|
name="Web Scraping",
|
|
147
160
|
description="A workflow that, given several urls, scrapes and summarizes their content using Google's Gemini API",
|
|
148
|
-
source=
|
|
161
|
+
source=GithubTemplateRepo(
|
|
162
|
+
url="https://github.com/run-llama/template-workflow-web-scraping"
|
|
163
|
+
),
|
|
149
164
|
llama_cloud=False,
|
|
150
165
|
),
|
|
151
166
|
]
|
|
@@ -202,16 +217,14 @@ def _create(template: str | None, dir: Path | None, force: bool) -> None:
|
|
|
202
217
|
else:
|
|
203
218
|
shutil.rmtree(dir, ignore_errors=True)
|
|
204
219
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
)
|
|
211
|
-
else:
|
|
212
|
-
asyncio.run(create_scaffold(resolved_template.source.name, str(dir)))
|
|
220
|
+
copier.run_copy(
|
|
221
|
+
resolved_template.source.url,
|
|
222
|
+
dir,
|
|
223
|
+
quiet=True,
|
|
224
|
+
)
|
|
213
225
|
# Initialize git repository if git is available
|
|
214
226
|
has_git = False
|
|
227
|
+
git_initialized = False
|
|
215
228
|
try:
|
|
216
229
|
subprocess.run(["git", "--version"], check=True, capture_output=True)
|
|
217
230
|
has_git = True
|
|
@@ -249,6 +262,7 @@ def _create(template: str | None, dir: Path | None, force: bool) -> None:
|
|
|
249
262
|
check=True,
|
|
250
263
|
capture_output=True,
|
|
251
264
|
)
|
|
265
|
+
git_initialized = True
|
|
252
266
|
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
|
253
267
|
# Extract a short error message if present
|
|
254
268
|
err_msg = ""
|
|
@@ -292,7 +306,8 @@ def _create(template: str | None, dir: Path | None, force: bool) -> None:
|
|
|
292
306
|
rprint(" [orange3]uvx[/] llamactl serve")
|
|
293
307
|
rprint("")
|
|
294
308
|
rprint("[bold]To deploy:[/]")
|
|
295
|
-
if
|
|
309
|
+
# Only show manual git init steps if repository failed to initialize earlier
|
|
310
|
+
if not git_initialized:
|
|
296
311
|
rprint(" [orange3]git[/] init")
|
|
297
312
|
rprint(" [orange3]git[/] add .")
|
|
298
313
|
rprint(" [orange3]git[/] commit -m 'Initial commit'")
|
|
@@ -303,7 +318,7 @@ def _create(template: str | None, dir: Path | None, force: bool) -> None:
|
|
|
303
318
|
rprint(" [orange3]git[/] push -u origin main")
|
|
304
319
|
rprint("")
|
|
305
320
|
# rprint(" [orange3]uvx[/] llamactl login")
|
|
306
|
-
rprint(" [orange3]uvx[/] llamactl deploy")
|
|
321
|
+
rprint(" [orange3]uvx[/] llamactl deploy create")
|
|
307
322
|
rprint("")
|
|
308
323
|
|
|
309
324
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import logging
|
|
2
3
|
import os
|
|
3
4
|
from pathlib import Path
|
|
@@ -5,15 +6,19 @@ from typing import Literal
|
|
|
5
6
|
|
|
6
7
|
import click
|
|
7
8
|
import questionary
|
|
9
|
+
from click.exceptions import Abort, Exit
|
|
8
10
|
from llama_deploy.cli.commands.auth import validate_authenticated_profile
|
|
9
11
|
from llama_deploy.cli.config.env_service import service
|
|
10
12
|
from llama_deploy.cli.config.schema import Auth
|
|
11
13
|
from llama_deploy.cli.options import interactive_option
|
|
12
14
|
from llama_deploy.cli.styles import WARNING
|
|
15
|
+
from llama_deploy.cli.utils.redact import redact_api_key
|
|
16
|
+
from llama_deploy.core.client.manage_client import ControlPlaneClient
|
|
13
17
|
from llama_deploy.core.config import DEFAULT_DEPLOYMENT_FILE_PATH
|
|
14
18
|
from llama_deploy.core.deployment_config import (
|
|
15
19
|
read_deployment_config_from_git_root_or_cwd,
|
|
16
20
|
)
|
|
21
|
+
from llama_deploy.core.schema.projects import ProjectSummary
|
|
17
22
|
from rich import print as rprint
|
|
18
23
|
|
|
19
24
|
from ..app import app
|
|
@@ -87,6 +92,16 @@ def serve(
|
|
|
87
92
|
rprint(f"[red]Deployment file '{deployment_file}' not found[/red]")
|
|
88
93
|
raise click.Abort()
|
|
89
94
|
|
|
95
|
+
# Early check: appserver requires a pyproject.toml in the config directory
|
|
96
|
+
config_dir = deployment_file if deployment_file.is_dir() else deployment_file.parent
|
|
97
|
+
if not (config_dir / "pyproject.toml").exists():
|
|
98
|
+
rprint(
|
|
99
|
+
"[red]No pyproject.toml found at[/red] "
|
|
100
|
+
f"[bold]{config_dir}[/bold].\n"
|
|
101
|
+
"Add a pyproject.toml to your project and re-run 'llamactl serve'."
|
|
102
|
+
)
|
|
103
|
+
raise click.Abort()
|
|
104
|
+
|
|
90
105
|
try:
|
|
91
106
|
# Pre-check: if the template requires llama cloud access, ensure credentials
|
|
92
107
|
_maybe_inject_llama_cloud_credentials(
|
|
@@ -108,6 +123,7 @@ def serve(
|
|
|
108
123
|
build=preview,
|
|
109
124
|
)
|
|
110
125
|
deployment_config = get_deployment_config()
|
|
126
|
+
_print_connection_summary()
|
|
111
127
|
start_server_in_target_venv(
|
|
112
128
|
cwd=Path.cwd(),
|
|
113
129
|
deployment_file=deployment_file,
|
|
@@ -127,6 +143,9 @@ def serve(
|
|
|
127
143
|
else None,
|
|
128
144
|
)
|
|
129
145
|
|
|
146
|
+
except (Exit, Abort):
|
|
147
|
+
raise
|
|
148
|
+
|
|
130
149
|
except KeyboardInterrupt:
|
|
131
150
|
logger.debug("Shutting down...")
|
|
132
151
|
|
|
@@ -207,6 +226,32 @@ def _maybe_inject_llama_cloud_credentials(
|
|
|
207
226
|
|
|
208
227
|
existing = os.environ.get("LLAMA_CLOUD_API_KEY") or vars.get("LLAMA_CLOUD_API_KEY")
|
|
209
228
|
if existing:
|
|
229
|
+
# If interactive, allow choosing between env var and configured profile
|
|
230
|
+
if interactive:
|
|
231
|
+
choice = questionary.select(
|
|
232
|
+
"LLAMA_CLOUD_API_KEY detected in environment. Which credentials do you want to use?",
|
|
233
|
+
choices=[
|
|
234
|
+
questionary.Choice(
|
|
235
|
+
title=f"Use environment variable - {redact_api_key(existing)}",
|
|
236
|
+
value="env",
|
|
237
|
+
),
|
|
238
|
+
questionary.Choice(title="Use configured profile", value="profile"),
|
|
239
|
+
],
|
|
240
|
+
).ask()
|
|
241
|
+
if choice is None:
|
|
242
|
+
raise Exit(0)
|
|
243
|
+
if choice == "profile":
|
|
244
|
+
# Ensure we have an authenticated profile and inject from it
|
|
245
|
+
authed = validate_authenticated_profile(True)
|
|
246
|
+
_set_env_vars_from_profile(authed)
|
|
247
|
+
return
|
|
248
|
+
# Default to env var path when cancelled or explicitly chosen
|
|
249
|
+
_set_env_vars_from_env({**os.environ, **vars})
|
|
250
|
+
# If no project id provided, try to detect and select one using the env API key
|
|
251
|
+
if not os.environ.get("LLAMA_DEPLOY_PROJECT_ID"):
|
|
252
|
+
_maybe_select_project_for_env_key()
|
|
253
|
+
return
|
|
254
|
+
# Non-interactive: trust current environment variables
|
|
210
255
|
_set_env_vars_from_env({**os.environ, **vars})
|
|
211
256
|
return
|
|
212
257
|
|
|
@@ -243,3 +288,56 @@ def _maybe_inject_llama_cloud_credentials(
|
|
|
243
288
|
rprint(
|
|
244
289
|
f"[{WARNING}]Warning: LLAMA_CLOUD_API_KEY is not set and no logged-in profile was found. The app may not work.[/]"
|
|
245
290
|
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _maybe_select_project_for_env_key() -> None:
|
|
294
|
+
"""When using an env API key, ensure LLAMA_DEPLOY_PROJECT_ID is set.
|
|
295
|
+
|
|
296
|
+
If more than one project exists, prompt the user to select one.
|
|
297
|
+
"""
|
|
298
|
+
api_key = os.environ.get("LLAMA_CLOUD_API_KEY")
|
|
299
|
+
base_url = os.environ.get("LLAMA_CLOUD_BASE_URL", "https://api.cloud.llamaindex.ai")
|
|
300
|
+
if not api_key:
|
|
301
|
+
return
|
|
302
|
+
try:
|
|
303
|
+
|
|
304
|
+
async def _run() -> list[ProjectSummary]:
|
|
305
|
+
async with ControlPlaneClient.ctx(base_url, api_key, None) as client:
|
|
306
|
+
return await client.list_projects()
|
|
307
|
+
|
|
308
|
+
projects = asyncio.run(_run())
|
|
309
|
+
if not projects:
|
|
310
|
+
return
|
|
311
|
+
if len(projects) == 1:
|
|
312
|
+
os.environ["LLAMA_DEPLOY_PROJECT_ID"] = projects[0].project_id
|
|
313
|
+
return
|
|
314
|
+
# Multiple: prompt selection
|
|
315
|
+
choice = questionary.select(
|
|
316
|
+
"Select a project",
|
|
317
|
+
choices=[
|
|
318
|
+
questionary.Choice(
|
|
319
|
+
title=f"{p.project_name} ({p.deployment_count} deployments)",
|
|
320
|
+
value=p.project_id,
|
|
321
|
+
)
|
|
322
|
+
for p in projects
|
|
323
|
+
],
|
|
324
|
+
).ask()
|
|
325
|
+
if choice:
|
|
326
|
+
os.environ["LLAMA_DEPLOY_PROJECT_ID"] = choice
|
|
327
|
+
except Exception:
|
|
328
|
+
# Best-effort; if we fail to list, do nothing
|
|
329
|
+
pass
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _print_connection_summary() -> None:
|
|
333
|
+
base_url = os.environ.get("LLAMA_CLOUD_BASE_URL")
|
|
334
|
+
project_id = os.environ.get("LLAMA_DEPLOY_PROJECT_ID")
|
|
335
|
+
api_key = os.environ.get("LLAMA_CLOUD_API_KEY")
|
|
336
|
+
if not base_url and not project_id and not api_key:
|
|
337
|
+
return
|
|
338
|
+
redacted = redact_api_key(api_key)
|
|
339
|
+
env_text = base_url or "-"
|
|
340
|
+
proj_text = project_id or "-"
|
|
341
|
+
rprint(
|
|
342
|
+
f"Connecting to environment: [bold]{env_text}[/], project: [bold]{proj_text}[/], api key: [bold]{redacted}[/]"
|
|
343
|
+
)
|
|
@@ -3,6 +3,7 @@ import asyncio
|
|
|
3
3
|
from llama_deploy.cli.auth.client import PlatformAuthClient, RefreshMiddleware
|
|
4
4
|
from llama_deploy.cli.config._config import Auth, ConfigManager, Environment
|
|
5
5
|
from llama_deploy.cli.config.schema import DeviceOIDC
|
|
6
|
+
from llama_deploy.cli.utils.redact import redact_api_key
|
|
6
7
|
from llama_deploy.core.client.manage_client import ControlPlaneClient, httpx
|
|
7
8
|
from llama_deploy.core.schema import VersionResponse
|
|
8
9
|
from llama_deploy.core.schema.projects import ProjectSummary
|
|
@@ -123,8 +124,4 @@ class AuthService:
|
|
|
123
124
|
|
|
124
125
|
def _auto_profile_name_from_token(api_key: str) -> str:
|
|
125
126
|
token = api_key or "token"
|
|
126
|
-
|
|
127
|
-
first = cleaned[:6]
|
|
128
|
-
last = cleaned[-4:] if len(cleaned) > 10 else cleaned[-2:]
|
|
129
|
-
base = f"{first}****{last}"
|
|
130
|
-
return base
|
|
127
|
+
return redact_api_key(token)
|
|
@@ -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,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def redact_api_key(
|
|
5
|
+
token: str | None,
|
|
6
|
+
visible_prefix: int = 6,
|
|
7
|
+
visible_suffix_long: int = 4,
|
|
8
|
+
visible_suffix_short: int = 2,
|
|
9
|
+
long_threshold: int = 10,
|
|
10
|
+
mask: str = "****",
|
|
11
|
+
) -> str:
|
|
12
|
+
"""Redact an API key for display.
|
|
13
|
+
|
|
14
|
+
Shows a prefix and suffix with a mask in the middle. If token is short,
|
|
15
|
+
reduces the suffix length to keep at least two trailing characters visible.
|
|
16
|
+
|
|
17
|
+
This mirrors the masking behavior used for profile names.
|
|
18
|
+
"""
|
|
19
|
+
if not token:
|
|
20
|
+
return "-"
|
|
21
|
+
cleaned = token.replace(" ", "")
|
|
22
|
+
if len(cleaned) <= 0:
|
|
23
|
+
return "-"
|
|
24
|
+
first = cleaned[:visible_prefix]
|
|
25
|
+
last_len = (
|
|
26
|
+
visible_suffix_long if len(cleaned) > long_threshold else visible_suffix_short
|
|
27
|
+
)
|
|
28
|
+
last = cleaned[-last_len:] if last_len > 0 else ""
|
|
29
|
+
return f"{first}{mask}{last}"
|
|
@@ -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.9 → llamactl-0.3.11}/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.9 → llamactl-0.3.11}/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
|