Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ tab-complete; `/help` opens the same catalog live.
| `/settings` | | View or change settings |
| `/set <key> <val>` | | Change a setting (e.g. `/set temperature 0.7`) |
| `/profile [name]` | | Switch the settings profile. No args opens the Profile tab in Settings |
| `/analyze [dir\|report\|off]` | | Recommend a profile for your documents or a folder. `report` opens the last report; `off` hides the analyze tip |
| `/theme <name>` | | Switch theme |
| `/status` | | Show indexed documents and config |
| `/login <token>` | | Log in to HuggingFace |
Expand Down Expand Up @@ -1008,10 +1009,18 @@ lilbee --json analyze # the report as one JSON object
```

`lilbee init` and `lilbee add` print a one-line tip before ingest
starts while the project is on the Default profile and was never
analyzed. Analyzing, applying any profile, or `lilbee analyze --off`
starts while the project is on the Default profile, was never
analyzed, and has no documents yet. Analyzing, applying any profile, or `lilbee analyze --off`
hides it. Press Ctrl+C to cancel a run; a cancelled run saves nothing.

In the TUI, `/analyze [dir]` (or "Analyze documents" in the command
palette) runs in the task bar. When it finishes, a notice says so and
`/analyze report` opens the report; it never opens over what you are
typing. Apply shows what the profile changes and asks before it
switches; Save only saves it without switching. While you type `/add`,
the argument hint shows the same tip until the project has documents.
`/analyze off` hides it.

### Sessions

See [Sessions](#sessions). Ids accept any unique prefix.
Expand Down
16 changes: 13 additions & 3 deletions src/lilbee/app/analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
save_recommended,
show,
)
from lilbee.core import settings
from lilbee.core.config import cfg
from lilbee.core.config.enums import FtsLanguage
from lilbee.core.config.resolve import read_layers, read_profile_table, resolve
Expand Down Expand Up @@ -415,12 +416,21 @@ def tip_state(root: Path) -> TipState:
analyzed = state.analyzed_at is not None
name = read_profile_table(root).name or DEFAULT_PROFILE_NAME
on_default = profile_key(name) == profile_key(DEFAULT_PROFILE_NAME)
shows = on_default and not analyzed and not state.tip_dismissed
return TipState(analyzed, state.tip_dismissed, shows)
fresh = on_default and not analyzed and not state.tip_dismissed
return TipState(analyzed, state.tip_dismissed, fresh and not _has_documents(root))


def _has_documents(root: Path) -> bool:
"""Whether the project at *root* indexed, or will index, anything: an added root or a file."""
if settings.load(root).get("linked_roots"):
return True
# a project other than the active one (a fresh init) keeps the default documents folder
documents = cfg.documents_dir if root == cfg.data_root else root / "documents"
return any(path.is_file() for path in documents.rglob("*"))


def tip_shows(root: Path) -> bool:
"""True when the project at *root* is on Default, never analyzed, and the tip is not hidden."""
"""True when the project at *root* is on Default, unanalyzed, empty, and not hiding the tip."""
return tip_state(root).tip_shows


Expand Down
22 changes: 20 additions & 2 deletions src/lilbee/app/profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,9 +247,13 @@ def apply(store: ProfileStore, name: str) -> ApplyResult:
profile = _usable(store, name)
planned = _diff(profile)
apply_profile_layer(profile.name, profile.values)
return _apply_result(planned)


def _apply_result(planned: ProfileDiff) -> ApplyResult:
effects = {row.key: row.effect for row in planned.changes}
return ApplyResult(
name=profile.name,
name=planned.name,
changes=planned.changes,
reindex_required=ProfileEffect.REINDEX in effects.values(),
new_files_only=tuple(k for k, e in effects.items() if e is ProfileEffect.NEW_FILES_ONLY),
Expand Down Expand Up @@ -458,13 +462,27 @@ def save_recommended(
"""
directory = _target_dir(target)
replacing = _analyze_file_to_replace(store, target, name)
text = profile_text(replace(_bare(name, values), description=ANALYZE_DESCRIPTION))
text = profile_text(recommended_file(name, values))
planned = plan_write(directory, target, text, stem=name, replacing=replacing)
if switch:
return _switch_to(planned, ())
return SaveResult(_written(planned), ())


def recommended_file(name: str, values: Mapping[str, Any]) -> ProfileFile:
"""Analyze's recommended profile *name* of *values*, as it is saved."""
return replace(_bare(name, values), description=ANALYZE_DESCRIPTION)


def apply_recommended(
store: ProfileStore, name: str, values: Mapping[str, Any], target: ProfileFolder
) -> ApplyResult:
"""Save analyze's recommended *values* as *name* and switch this project to it."""
planned = _diff(recommended_file(name, values))
save_recommended(store, name, values, target, switch=True)
return _apply_result(planned)


def discard() -> DiscardResult:
"""Remove your settings of profile keys from config.toml; environment variables stay."""
yours = _your_profile_keys(read_layers(cfg.data_root))
Expand Down
9 changes: 9 additions & 0 deletions src/lilbee/cli/tui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@

if TYPE_CHECKING:
from lilbee.app.services import Services
from lilbee.cli.tui.screens.analyze_report import FinishedAnalysis
from lilbee.cli.tui.screens.chat import ChatScreen
from lilbee.cli.tui.screens.startup_gate import StartupGate

Expand Down Expand Up @@ -258,6 +259,7 @@ def __init__(self, *, initial_view: str | None = None) -> None:
from lilbee.cli.tui.widgets.task_bar_controller import TaskBarController

self.task_bar = TaskBarController(self)
self.last_analysis: FinishedAnalysis | None = None

def notify(
self,
Expand Down Expand Up @@ -1018,6 +1020,13 @@ def open_profile_tab(self) -> None:
if isinstance(screen, SettingsScreen):
screen.call_after_refresh(screen.show_profile_tab)

def start_analyze(self) -> None:
"""Queue analyze of the indexed documents; its report opens when it finishes."""
# circular: screens import the app module
from lilbee.cli.tui.screens.analyze_report import start_analysis

start_analysis(self, None)

def start_rebuild(self) -> None:
"""Queue a full reindex on the chat screen's task bar without leaving this view."""
chat = self.chat_screen()
Expand Down
12 changes: 12 additions & 0 deletions src/lilbee/cli/tui/command_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ class SlashCommand:
allowed_while_streaming: bool = False


ANALYZE_OFF_ARG = "off"
ANALYZE_REPORT_ARG = "report"

COMMANDS: tuple[SlashCommand, ...] = (
SlashCommand(
"/model",
Expand Down Expand Up @@ -82,6 +85,15 @@ class SlashCommand:
args_hint="[name]",
help_text="Switch profile (no arg opens the Profile tab in Settings)",
),
SlashCommand(
"/analyze",
"_cmd_analyze",
args_hint=f"[dir|{ANALYZE_REPORT_ARG}|{ANALYZE_OFF_ARG}]",
help_text=(
f"Recommend a profile for your documents ({ANALYZE_REPORT_ARG} opens the last "
f"report, {ANALYZE_OFF_ARG} hides the tip)"
),
),
SlashCommand(
"/reset",
"_cmd_reset",
Expand Down
1 change: 1 addition & 0 deletions src/lilbee/cli/tui/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def _get_commands(self) -> list[tuple[str, str, Any]]:
("Open status", "Knowledge base status", lambda: app.switch_view("Status")),
("Open settings", "View and change settings", lambda: app.switch_view("Settings")),
("Choose profile", "Pick the settings profile for this project", app.open_profile_tab),
("Analyze documents", "Recommend a profile for your documents", app.start_analyze),
("Open task center", "Monitor background tasks", lambda: app.switch_view("Tasks")),
("Help", "Show keybinding reference", app.action_push_help),
("Cycle theme", "Switch to next color theme", app.action_cycle_theme),
Expand Down
56 changes: 56 additions & 0 deletions src/lilbee/cli/tui/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ def prune_ignored_message(count: int) -> str:
TASK_NAME_REBUILD = "Rebuild index"
TASK_NAME_IMPORT = "Import {file}"
TASK_NAME_EXPORT = "Export {file}"
TASK_NAME_ANALYZE = "Analyze {folder}"
IMPORT_STATUS_LOADING = "Loading dataset..."
EXPORT_STATUS_RUNNING = "Exporting..."
CMD_SET_UNKNOWN = "Unknown setting: {key}"
Expand Down Expand Up @@ -933,6 +934,61 @@ def get_nav_views() -> list[str]:
NameProblem.TAKEN: "A profile in that folder already has this name.",
}

# /analyze, the analyze report, and the analyze tip on the /add hint
ANALYZE_STATUS_FINDING = "Finding files..."
ANALYZE_STATUS_READING = "Reading {done} of {total}: {file}"
ANALYZE_NOT_A_FOLDER = "{path} is not a folder."
ANALYZE_FAILED = "Analyze failed: {error}"
ANALYZE_TIP_HIDDEN = "The analyze tip is hidden for this project."
ANALYZE_DONE = "Analyze of {folder} finished. Type /analyze report to see it."
ANALYZE_NO_REPORT = "No analyze report yet. Run /analyze first."
ANALYZE_TITLE = "What's in {folder}"
ANALYZE_YOUR_DOCUMENTS = "your documents"
ANALYZE_READ = "Read {read} of {total} documents without OCR. No AI model runs."
ANALYZE_READ_COUNTED = (
"Read {read} of {total} documents without OCR and counted {counted} code, image and "
"archive files. No AI model runs."
)
ANALYZE_SAMPLED = "The documents are sampled evenly; analyze_max_files is {cap}."
ANALYZE_FOUND = "Found"
ANALYZE_FACT_TYPES = "File types"
ANALYZE_FACT_CODE = "Code"
ANALYZE_FACT_SCANS = "Scanned pages"
ANALYZE_FACT_TABLES = "Tables"
ANALYZE_FACT_LANGUAGES = "Languages"
ANALYZE_FACT_PDF_LENGTH = "Typical PDF length"
ANALYZE_FACT_TEXT_LENGTH = "Typical length"
ANALYZE_TYPE_COUNT = "{count} {kind}"
ANALYZE_SHARE_OF_FILES = "{share:.0%} of files"
ANALYZE_SHARE_OF_PAGES = "{share:.0%} of pages"
ANALYZE_TABLES_VALUE = "in {tables} of {files} PDFs"
ANALYZE_LANGUAGE_SHARE = "{name} {share:.0%}"
ANALYZE_LANGUAGES_TEXT = "{languages} (text files)"
ANALYZE_LANGUAGES_NONE = "none detected"
ANALYZE_PAGES_VALUE = "{pages:g} pages"
ANALYZE_CHARS_VALUE = "{chars:,.0f} characters"
ANALYZE_FAILED_TITLE = "Could not read"
ANALYZE_COL_FILE = "File"
ANALYZE_COL_REASON = "Reason"
ANALYZE_COL_WHY = "Why"
ANALYZE_WHY_BUILTIN = "{builtin} sets it"
ANALYZE_RECOMMEND_TITLE = "Recommendation: {name}"
ANALYZE_RECOMMEND_DERIVED = (
"{builtin}, with these changes. Apply saves it to your profiles and switches this "
"project to it."
)
ANALYZE_RECOMMEND_BUILTIN = "{builtin} fits these files. Apply switches this project to it."
ANALYZE_PICKED = "Picked because {reason}."
ANALYZE_NO_CHANGES = "Applying it changes nothing."
ANALYZE_KEEPS = "Keeps your values of {keys}."
ANALYZE_NOTES_TITLE = "Notes"
ANALYZE_SAVE_LABEL = "Save only"
ANALYZE_TIP_LABEL = "tip"
ANALYZE_TIP = (
"This project uses the Default profile. Run /analyze on the same path first to pick "
"settings for it, or /analyze off to hide this tip."
)


def profile_count_text(count: int) -> str:
"""The profile line's count of values set by you."""
Expand Down
Loading
Loading