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 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}")