Coverage for src / lilbee / cli / app.py: 100%
77 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-16 08:27 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-16 08:27 +0000
1"""App creation, console, and global callback."""
3import logging
4import os
5import sys
6from pathlib import Path
8import typer
9from rich.console import Console
11from lilbee.cli.helpers import auto_sync, get_version
12from lilbee.cli.helpers import json_output as json_out
13from lilbee.config import cfg
14from lilbee.models import ensure_tag
16app = typer.Typer(help="lilbee — Local RAG knowledge base", invoke_without_command=True)
17console = Console()
19data_dir_option = typer.Option(
20 None,
21 "--data-dir",
22 "-d",
23 help="Override data directory (default: platform-specific, see 'lilbee status')",
24)
26model_option = typer.Option(
27 None,
28 "--model",
29 "-m",
30 help="Override chat model (default: $LILBEE_CHAT_MODEL or 'qwen3:8b')",
31)
33_json_option = typer.Option(
34 False,
35 "--json",
36 "-j",
37 help="Emit structured JSON output (for agent/script consumption).",
38)
40_global_option = typer.Option(
41 False,
42 "--global",
43 "-g",
44 help="Use the global database, ignoring any local .lilbee/ directory.",
45)
47_log_level_option = typer.Option(
48 None,
49 "--log-level",
50 help="Set log level (DEBUG, INFO, WARNING, ERROR). Overrides LILBEE_LOG_LEVEL.",
51)
53temperature_option = typer.Option(None, "--temperature", "-t", help="Sampling temperature.")
54top_p_option = typer.Option(None, "--top-p", help="Top-p (nucleus) sampling threshold.")
55top_k_sampling_option = typer.Option(None, "--top-k-sampling", help="Top-k sampling count.")
56repeat_penalty_option = typer.Option(None, "--repeat-penalty", help="Repeat penalty factor.")
57num_ctx_option = typer.Option(None, "--num-ctx", help="Context window size (tokens).")
58seed_option = typer.Option(None, "--seed", help="Random seed for reproducibility.")
61def _apply_data_root(root: Path) -> None:
62 """Point all cfg data paths at *root*."""
63 cfg.data_root = root
64 cfg.documents_dir = root / "documents"
65 cfg.data_dir = root / "data"
66 cfg.lancedb_dir = root / "data" / "lancedb"
69def apply_overrides(
70 data_dir: Path | None = None,
71 model: str | None = None,
72 use_global: bool = False,
73 temperature: float | None = None,
74 top_p: float | None = None,
75 top_k_sampling: int | None = None,
76 repeat_penalty: float | None = None,
77 num_ctx: int | None = None,
78 seed: int | None = None,
79) -> None:
80 """Apply CLI overrides to config before any work begins.
82 Precedence (highest first):
83 --data-dir / LILBEE_DATA > .lilbee/ (local walk-up) > global platform default
84 """
85 if data_dir is not None and use_global:
86 raise typer.BadParameter("Cannot use --global with --data-dir")
88 if use_global:
89 from lilbee.platform import default_data_dir
91 _apply_data_root(default_data_dir())
92 elif data_dir is not None:
93 _apply_data_root(data_dir)
94 else:
95 data_env = os.environ.get("LILBEE_DATA", "")
96 if data_env:
97 _apply_data_root(Path(data_env))
99 if model is not None:
100 cfg.chat_model = ensure_tag(model)
101 if temperature is not None:
102 cfg.temperature = temperature
103 if top_p is not None:
104 cfg.top_p = top_p
105 if top_k_sampling is not None:
106 cfg.top_k_sampling = top_k_sampling
107 if repeat_penalty is not None:
108 cfg.repeat_penalty = repeat_penalty
109 if num_ctx is not None:
110 cfg.num_ctx = num_ctx
111 if seed is not None:
112 cfg.seed = seed
115@app.callback()
116def _default(
117 ctx: typer.Context,
118 data_dir: Path | None = data_dir_option,
119 model: str | None = model_option,
120 json_output: bool = _json_option,
121 use_global: bool = _global_option,
122 log_level: str | None = _log_level_option,
123 show_version: bool = typer.Option(
124 False,
125 "--version",
126 "-V",
127 help="Show version and exit.",
128 is_eager=True,
129 ),
130) -> None:
131 """Start interactive chat when no command is given."""
132 if show_version:
133 typer.echo(f"lilbee {get_version()}")
134 raise SystemExit(0)
136 level_str = os.environ.get("LILBEE_LOG_LEVEL", "WARNING").upper()
137 if log_level is not None:
138 level_str = log_level.upper()
139 level = getattr(logging, level_str, logging.WARNING)
140 logging.basicConfig(
141 level=level, format="%(levelname)s %(name)s: %(message)s", stream=sys.stderr
142 )
143 # basicConfig is a no-op when handlers already exist, so always set level explicitly
144 logging.getLogger().setLevel(level)
146 cfg.json_mode = json_output
147 if ctx.invoked_subcommand is None:
148 apply_overrides(data_dir=data_dir, model=model, use_global=use_global)
149 if cfg.json_mode:
150 json_out({"error": "Interactive chat requires a terminal, not --json"})
151 raise SystemExit(1)
152 from lilbee.embedder import validate_model
153 from lilbee.models import ensure_chat_model
155 ensure_chat_model()
156 validate_model()
157 auto_sync(console)
158 from lilbee.cli.chat import chat_loop
160 chat_loop(console)