llmboost-hub 0.1.1__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.
- llmboost_hub/cli.py +47 -0
- llmboost_hub/commands/attach.py +74 -0
- llmboost_hub/commands/chat.py +62 -0
- llmboost_hub/commands/completions.py +238 -0
- llmboost_hub/commands/list.py +283 -0
- llmboost_hub/commands/login.py +72 -0
- llmboost_hub/commands/prep.py +559 -0
- llmboost_hub/commands/run.py +486 -0
- llmboost_hub/commands/search.py +182 -0
- llmboost_hub/commands/serve.py +303 -0
- llmboost_hub/commands/status.py +34 -0
- llmboost_hub/commands/stop.py +59 -0
- llmboost_hub/commands/test_cmd.py +45 -0
- llmboost_hub/commands/tune.py +372 -0
- llmboost_hub/utils/config.py +220 -0
- llmboost_hub/utils/container_utils.py +126 -0
- llmboost_hub/utils/fs_utils.py +42 -0
- llmboost_hub/utils/generate_sample_lookup.py +132 -0
- llmboost_hub/utils/gpu_info.py +244 -0
- llmboost_hub/utils/license_checker.py +3 -0
- llmboost_hub/utils/license_wrapper.py +91 -0
- llmboost_hub/utils/llmboost_version.py +1 -0
- llmboost_hub/utils/lookup_cache.py +123 -0
- llmboost_hub/utils/model_utils.py +76 -0
- llmboost_hub/utils/signature.py +3 -0
- llmboost_hub-0.1.1.dist-info/METADATA +203 -0
- llmboost_hub-0.1.1.dist-info/RECORD +31 -0
- llmboost_hub-0.1.1.dist-info/WHEEL +5 -0
- llmboost_hub-0.1.1.dist-info/entry_points.txt +3 -0
- llmboost_hub-0.1.1.dist-info/licenses/LICENSE +16 -0
- llmboost_hub-0.1.1.dist-info/top_level.txt +1 -0
llmboost_hub/cli.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
from llmboost_hub.commands import (
|
|
4
|
+
attach,
|
|
5
|
+
completions,
|
|
6
|
+
list as list_cmd,
|
|
7
|
+
login,
|
|
8
|
+
prep,
|
|
9
|
+
run,
|
|
10
|
+
search,
|
|
11
|
+
serve,
|
|
12
|
+
status as status_cmd,
|
|
13
|
+
stop,
|
|
14
|
+
test_cmd,
|
|
15
|
+
tune,
|
|
16
|
+
)
|
|
17
|
+
from importlib import metadata
|
|
18
|
+
|
|
19
|
+
pkg_name = __package__ or "llmboost_hub"
|
|
20
|
+
pkg_version = metadata.version(pkg_name)
|
|
21
|
+
|
|
22
|
+
@click.group(context_settings={"help_option_names": ["-h", "--help"]})
|
|
23
|
+
@click.version_option(version=pkg_version, package_name=pkg_name, message="llmboost_hub (lbh) version %(version)s")
|
|
24
|
+
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose mode.")
|
|
25
|
+
@click.pass_context
|
|
26
|
+
def main(ctx: click.Context, verbose):
|
|
27
|
+
"""LLMBoost Hub (lbh): Manage LLMBoost model containers and environments to run, serve, and tune large language models."""
|
|
28
|
+
ctx.ensure_object(dict)
|
|
29
|
+
ctx.obj["VERBOSE"] = verbose
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Register commands
|
|
33
|
+
main.add_command(attach.attach)
|
|
34
|
+
main.add_command(completions.completions)
|
|
35
|
+
main.add_command(list_cmd.list_models)
|
|
36
|
+
main.add_command(login.login)
|
|
37
|
+
main.add_command(prep.prep)
|
|
38
|
+
main.add_command(run.run)
|
|
39
|
+
main.add_command(search.search)
|
|
40
|
+
main.add_command(serve.serve)
|
|
41
|
+
main.add_command(status_cmd.status_cmd)
|
|
42
|
+
main.add_command(stop.stop)
|
|
43
|
+
main.add_command(test_cmd.test_cmd)
|
|
44
|
+
main.add_command(tune.tune)
|
|
45
|
+
|
|
46
|
+
if __name__ == "__main__":
|
|
47
|
+
main()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import subprocess
|
|
3
|
+
|
|
4
|
+
from llmboost_hub.utils.container_utils import (
|
|
5
|
+
container_name_for_model,
|
|
6
|
+
is_container_running,
|
|
7
|
+
)
|
|
8
|
+
from llmboost_hub.commands.completions import complete_model_names
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def do_attach(model: str | None, container: str | None, verbose: bool = False) -> dict:
|
|
12
|
+
"""
|
|
13
|
+
Attach to a running container and open an interactive shell (bash -> sh fallback).
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
model: Model identifier used to derive container name when 'container' is not provided.
|
|
17
|
+
container: Explicit container name to attach to.
|
|
18
|
+
verbose: If True, echo informative messages.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
Dict: {returncode: int, container_name: str, error: str|None}
|
|
22
|
+
"""
|
|
23
|
+
cname = container or (container_name_for_model(model) if model else None)
|
|
24
|
+
if not cname:
|
|
25
|
+
return {"returncode": 1, "container_name": "", "error": "No model or container specified."}
|
|
26
|
+
|
|
27
|
+
# Early return if target is not running
|
|
28
|
+
if not is_container_running(cname):
|
|
29
|
+
return {
|
|
30
|
+
"returncode": 1,
|
|
31
|
+
"container_name": cname,
|
|
32
|
+
"error": f"Container '{cname}' is not running.",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Prefer bash; fall back to sh on failure
|
|
36
|
+
exec_cmd = ["docker", "exec", "-it", cname, "bash"]
|
|
37
|
+
if verbose:
|
|
38
|
+
click.echo(f"[attach] Opening shell in container '{cname}'...")
|
|
39
|
+
try:
|
|
40
|
+
subprocess.run(exec_cmd, check=True)
|
|
41
|
+
return {"returncode": 0, "container_name": cname, "error": None}
|
|
42
|
+
except subprocess.CalledProcessError:
|
|
43
|
+
exec_cmd = ["docker", "exec", "-it", cname, "/bin/sh"]
|
|
44
|
+
try:
|
|
45
|
+
subprocess.run(exec_cmd, check=True)
|
|
46
|
+
return {"returncode": 0, "container_name": cname, "error": None}
|
|
47
|
+
except subprocess.CalledProcessError as e:
|
|
48
|
+
return {
|
|
49
|
+
"returncode": e.returncode,
|
|
50
|
+
"container_name": cname,
|
|
51
|
+
"error": f"Attach failed (exit {e.returncode})",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@click.command(context_settings={"help_option_names": ["-h", "--help"]})
|
|
56
|
+
@click.argument("model", required=False, shell_complete=complete_model_names)
|
|
57
|
+
@click.option(
|
|
58
|
+
"-c",
|
|
59
|
+
"--container",
|
|
60
|
+
"container",
|
|
61
|
+
type=str,
|
|
62
|
+
help="Container name to attach to (overrides model).",
|
|
63
|
+
)
|
|
64
|
+
@click.pass_context
|
|
65
|
+
def attach(ctx: click.Context, model, container):
|
|
66
|
+
"""
|
|
67
|
+
Attach to a running model container and open a shell.
|
|
68
|
+
|
|
69
|
+
Equivalent to 'docker exec -it <container> bash'.
|
|
70
|
+
"""
|
|
71
|
+
verbose = ctx.obj.get("VERBOSE", False)
|
|
72
|
+
res = do_attach(model, container, verbose=verbose)
|
|
73
|
+
if res["returncode"] != 0:
|
|
74
|
+
raise click.ClickException(res["error"] or "Attach failed")
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import subprocess
|
|
3
|
+
|
|
4
|
+
from llmboost_hub.utils.container_utils import (
|
|
5
|
+
container_name_for_model,
|
|
6
|
+
is_container_running,
|
|
7
|
+
)
|
|
8
|
+
from llmboost_hub.commands.run import do_run
|
|
9
|
+
from llmboost_hub.commands.completions import complete_model_names
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def do_chat(model: str, verbose: bool = False) -> dict:
|
|
13
|
+
"""
|
|
14
|
+
Ensure a container is running, then exec `llmboost chat --model_name <model>`.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
model: Target model identifier.
|
|
18
|
+
verbose: If True, echo docker exec command.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
Dict: {returncode: int, container_name: str, error: str|None}
|
|
22
|
+
"""
|
|
23
|
+
cname = container_name_for_model(model)
|
|
24
|
+
if not is_container_running(cname):
|
|
25
|
+
if verbose:
|
|
26
|
+
click.echo(f"[chat] No running container for {model}; starting via lbh run...")
|
|
27
|
+
res = do_run(model, lbh_workspace=None, docker_args=(), verbose=verbose)
|
|
28
|
+
if res["returncode"] != 0:
|
|
29
|
+
return {"returncode": res["returncode"], "container_name": "", "error": res["error"]}
|
|
30
|
+
# Re-check after attempting to start container
|
|
31
|
+
if not is_container_running(cname):
|
|
32
|
+
return {
|
|
33
|
+
"returncode": 1,
|
|
34
|
+
"container_name": "",
|
|
35
|
+
"error": "Failed to start container for chat.",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
exec_cmd = ["docker", "exec", "-it", cname, "llmboost", "chat", "--model_name", model]
|
|
39
|
+
if verbose:
|
|
40
|
+
click.echo("[chat] " + " ".join(exec_cmd))
|
|
41
|
+
try:
|
|
42
|
+
subprocess.run(exec_cmd, check=True)
|
|
43
|
+
return {"returncode": 0, "container_name": cname, "error": None}
|
|
44
|
+
except subprocess.CalledProcessError as e:
|
|
45
|
+
return {
|
|
46
|
+
"returncode": e.returncode,
|
|
47
|
+
"container_name": cname,
|
|
48
|
+
"error": f"Failed to start chat (exit {e.returncode})",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@click.command(context_settings={"help_option_names": ["-h", "--help"]})
|
|
53
|
+
@click.argument("model", required=True, shell_complete=complete_model_names)
|
|
54
|
+
@click.pass_context
|
|
55
|
+
def chat(ctx: click.Context, model):
|
|
56
|
+
"""
|
|
57
|
+
Start an interactive chat session inside the model container.
|
|
58
|
+
"""
|
|
59
|
+
verbose = ctx.obj.get("VERBOSE", False)
|
|
60
|
+
res = do_chat(model, verbose=verbose)
|
|
61
|
+
if res["returncode"] != 0:
|
|
62
|
+
raise click.ClickException(res["error"] or "Chat failed")
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from llmboost_hub.commands.search import do_search
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@click.group(context_settings={"help_option_names": ["-h", "--help"]})
|
|
8
|
+
@click.version_option("0.1.0")
|
|
9
|
+
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose mode.")
|
|
10
|
+
@click.pass_context
|
|
11
|
+
def main(ctx: click.Context, verbose):
|
|
12
|
+
"""LLMBoost Hub (lbh): Manage LLMBoost model containers and environments."""
|
|
13
|
+
ctx.ensure_object(dict)
|
|
14
|
+
ctx.obj["VERBOSE"] = verbose
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _detect_shell() -> str:
|
|
18
|
+
"""
|
|
19
|
+
Detect the current shell from environment variables.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
One of: 'bash', 'zsh', 'fish', or 'powershell' (best-effort; defaults to bash).
|
|
23
|
+
"""
|
|
24
|
+
sh = os.environ.get("SHELL", "")
|
|
25
|
+
base = os.path.basename(sh).lower()
|
|
26
|
+
if "zsh" in base:
|
|
27
|
+
return "zsh"
|
|
28
|
+
if "fish" in base:
|
|
29
|
+
return "fish"
|
|
30
|
+
if "bash" in base:
|
|
31
|
+
return "bash"
|
|
32
|
+
# Best-effort: allow powershell if on Windows session
|
|
33
|
+
if os.environ.get("PSModulePath"):
|
|
34
|
+
return "powershell"
|
|
35
|
+
return "bash"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _one_liner(shell: str, prog: str = "lbh") -> str:
|
|
39
|
+
"""
|
|
40
|
+
Return the eval one-liner to enable Click completions for a shell.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
shell: Target shell.
|
|
44
|
+
prog: CLI executable name (default: 'lbh').
|
|
45
|
+
"""
|
|
46
|
+
var = f"_{prog.upper()}_COMPLETE"
|
|
47
|
+
if shell == "bash":
|
|
48
|
+
return f'eval "$({var}=bash_source {prog})"'
|
|
49
|
+
if shell == "zsh":
|
|
50
|
+
return f'eval "$({var}=zsh_source {prog})"'
|
|
51
|
+
if shell == "fish":
|
|
52
|
+
return f'eval "$({var}=fish_source {prog})"'
|
|
53
|
+
if shell == "powershell":
|
|
54
|
+
return f'$Env:{var}="powershell_source"; {prog} | Out-String | Invoke-Expression'
|
|
55
|
+
# default bash
|
|
56
|
+
return f'eval "$({var}=bash_source {prog})"'
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _default_profile_path(shell: str) -> str:
|
|
60
|
+
"""
|
|
61
|
+
Return the default profile file path for a given shell.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
shell: Target shell.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Path to rc/profile file where completions should be persisted.
|
|
68
|
+
"""
|
|
69
|
+
home = os.path.expanduser("~")
|
|
70
|
+
if shell == "bash":
|
|
71
|
+
return os.path.join(home, ".bashrc")
|
|
72
|
+
if shell == "zsh":
|
|
73
|
+
return os.path.join(home, ".zshrc")
|
|
74
|
+
if shell == "fish":
|
|
75
|
+
return os.path.join(home, ".config", "fish", "config.fish")
|
|
76
|
+
if shell == "powershell":
|
|
77
|
+
# Prefer $PROFILE if available
|
|
78
|
+
return os.environ.get("PROFILE") or os.path.join(
|
|
79
|
+
home, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1"
|
|
80
|
+
)
|
|
81
|
+
return os.path.join(home, ".bashrc")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _venv_activation_targets(venv_root: str, shell: str | None = None) -> list[tuple[str, str]]:
|
|
85
|
+
"""
|
|
86
|
+
Return (shell_key, path) tuples for activation files in a virtualenv.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
venv_root: Root path of the active virtualenv.
|
|
90
|
+
shell: Optional single shell to target; when None, include all detected.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
List of (shell_key, file_path) pairs to update.
|
|
94
|
+
"""
|
|
95
|
+
targets = []
|
|
96
|
+
if shell in (None, "bash", "zsh"):
|
|
97
|
+
bash_path = os.path.join(venv_root, "bin", "activate")
|
|
98
|
+
zsh_path = os.path.join(venv_root, "bin", "activate.zsh")
|
|
99
|
+
targets.append(("bash/zsh", bash_path))
|
|
100
|
+
targets.append(("zsh", zsh_path))
|
|
101
|
+
if shell in (None, "fish"):
|
|
102
|
+
fish_path = os.path.join(venv_root, "bin", "activate.fish")
|
|
103
|
+
targets.append(("fish", fish_path))
|
|
104
|
+
if shell in (None, "powershell"):
|
|
105
|
+
ps_path = os.path.join(venv_root, "Scripts", "Activate.ps1")
|
|
106
|
+
targets.append(("powershell", ps_path))
|
|
107
|
+
return targets
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _write_block(file_path: str, content: str, start_tag: str, end_tag: str) -> None:
|
|
111
|
+
"""
|
|
112
|
+
Idempotently write a tagged content block to a file.
|
|
113
|
+
|
|
114
|
+
Behavior:
|
|
115
|
+
- Removes any existing block between start_tag and end_tag.
|
|
116
|
+
- Appends a fresh block to the end of the file (creating the file if needed).
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
file_path: Destination file path.
|
|
120
|
+
content: Text content to write between tags.
|
|
121
|
+
start_tag: Start marker line.
|
|
122
|
+
end_tag: End marker line.
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
ClickException: When writing fails.
|
|
126
|
+
"""
|
|
127
|
+
try:
|
|
128
|
+
existing = ""
|
|
129
|
+
if os.path.exists(file_path):
|
|
130
|
+
with open(file_path, "r", encoding="utf-8") as fh:
|
|
131
|
+
existing = fh.read()
|
|
132
|
+
# Remove existing block if present
|
|
133
|
+
import re as _re
|
|
134
|
+
|
|
135
|
+
pattern = _re.compile(
|
|
136
|
+
_re.escape(start_tag) + r".*?" + _re.escape(end_tag), flags=_re.DOTALL
|
|
137
|
+
)
|
|
138
|
+
cleaned = _re.sub(pattern, "", existing).rstrip()
|
|
139
|
+
# Append new block
|
|
140
|
+
new_text = (
|
|
141
|
+
(cleaned + "\n\n" if cleaned else "")
|
|
142
|
+
+ start_tag
|
|
143
|
+
+ "\n"
|
|
144
|
+
+ content
|
|
145
|
+
+ "\n"
|
|
146
|
+
+ end_tag
|
|
147
|
+
+ "\n"
|
|
148
|
+
)
|
|
149
|
+
os.makedirs(os.path.dirname(os.path.abspath(file_path)), exist_ok=True)
|
|
150
|
+
with open(file_path, "w", encoding="utf-8") as fh:
|
|
151
|
+
fh.write(new_text)
|
|
152
|
+
except Exception as e:
|
|
153
|
+
raise click.ClickException(f"Failed to update '{file_path}': {e}")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# completion helper for model names (uses the same source as `lbh list`)
|
|
157
|
+
def complete_model_names(ctx: click.Context, param: click.Parameter, query: str = r".*"):
|
|
158
|
+
"""
|
|
159
|
+
Shell completion callback returning model names discovered by `do_search` (local_only, names_only).
|
|
160
|
+
|
|
161
|
+
Behavior:
|
|
162
|
+
- Regex, case-insensitive search on `query`.
|
|
163
|
+
"""
|
|
164
|
+
try:
|
|
165
|
+
model_names = do_search(query=query, local_only=True, verbose=False, names_only=True)[
|
|
166
|
+
"model"
|
|
167
|
+
]
|
|
168
|
+
return model_names.dropna().astype(str).unique().tolist()
|
|
169
|
+
except Exception:
|
|
170
|
+
return []
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@click.command(name="completions", context_settings={"help_option_names": ["-h", "--help"]})
|
|
174
|
+
@click.option(
|
|
175
|
+
"--shell",
|
|
176
|
+
type=click.Choice(["bash", "zsh", "fish", "powershell"]),
|
|
177
|
+
default=None,
|
|
178
|
+
help="Target shell (default: auto-detect).",
|
|
179
|
+
)
|
|
180
|
+
@click.option(
|
|
181
|
+
"--profile",
|
|
182
|
+
is_flag=True,
|
|
183
|
+
help="Append completion to the detected shell profile (~/.bashrc, ~/.zshrc, fish config, or PowerShell profile).",
|
|
184
|
+
)
|
|
185
|
+
@click.option(
|
|
186
|
+
"--venv",
|
|
187
|
+
is_flag=True,
|
|
188
|
+
help="Append completion to the active virtualenv activation file(s). If shell not specified, updates all present.",
|
|
189
|
+
)
|
|
190
|
+
@click.pass_context
|
|
191
|
+
def completions(ctx: click.Context, shell, profile, venv):
|
|
192
|
+
"""
|
|
193
|
+
Manage shell completions.
|
|
194
|
+
|
|
195
|
+
Usage:
|
|
196
|
+
- Print a temporary one-liner for eval (no file modification).
|
|
197
|
+
- Optionally persist to shell profile and/or virtualenv activation files.
|
|
198
|
+
"""
|
|
199
|
+
sh = shell or _detect_shell()
|
|
200
|
+
|
|
201
|
+
# No persistence flags -> print the one-liner for eval in the current shell
|
|
202
|
+
if not profile and not venv:
|
|
203
|
+
click.echo(_one_liner(sh, "lbh"))
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
# Profile persistence
|
|
207
|
+
if profile:
|
|
208
|
+
profile_path = _default_profile_path(sh)
|
|
209
|
+
line = _one_liner(sh, "lbh")
|
|
210
|
+
start_tag = "# >>> lbh shell completion start"
|
|
211
|
+
end_tag = "# <<< lbh shell completion end"
|
|
212
|
+
_write_block(os.path.expanduser(profile_path), line, start_tag, end_tag)
|
|
213
|
+
click.echo(f"Updated profile: {os.path.expanduser(profile_path)}")
|
|
214
|
+
|
|
215
|
+
# Venv persistence
|
|
216
|
+
if venv:
|
|
217
|
+
venv_root = os.environ.get("VIRTUAL_ENV", "")
|
|
218
|
+
if not venv_root:
|
|
219
|
+
raise click.ClickException("No active virtualenv detected (VIRTUAL_ENV is not set).")
|
|
220
|
+
targets = _venv_activation_targets(venv_root, shell=sh if shell else None)
|
|
221
|
+
updated = []
|
|
222
|
+
start_tag = "# >>> lbh venv completion start"
|
|
223
|
+
end_tag = "# <<< lbh venv completion end"
|
|
224
|
+
for shell_key, path in targets:
|
|
225
|
+
if os.path.exists(path):
|
|
226
|
+
line = (
|
|
227
|
+
_one_liner("bash", "lbh")
|
|
228
|
+
if shell_key == "bash/zsh"
|
|
229
|
+
else _one_liner(shell_key, "lbh")
|
|
230
|
+
)
|
|
231
|
+
_write_block(path, line, start_tag, end_tag)
|
|
232
|
+
updated.append(path)
|
|
233
|
+
if not updated:
|
|
234
|
+
raise click.ClickException(
|
|
235
|
+
"No matching activation files found in the current virtualenv."
|
|
236
|
+
)
|
|
237
|
+
for p in updated:
|
|
238
|
+
click.echo(f"Updated venv activation: {p}")
|