Coverage for src/local_deep_research/web_search_engines/engine_groups.py: 100%
27 statements
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-19 23:35 +0000
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-19 23:35 +0000
1"""Grouping and ordering of search engines in the selector UI.
3Single source of truth for which band a search engine falls into and the order
4the bands appear in. The available-search-engines API attaches each engine's
5band to the response and the frontend renders one header per band, so the
6bucketing logic lives here only and is never duplicated in JavaScript.
7"""
9from dataclasses import dataclass
12@dataclass(frozen=True)
13class SearchEngineGroup:
14 """A band in the search-engine selector dropdown."""
16 key: str # stable identifier, e.g. "academic"
17 label: str # header text shown in the dropdown
18 icon: str # emoji associated with the band
21# Bands in display order, top to bottom; the list index IS the sort order
22# (lower sorts higher). "favorites" is an overlay band: a starred engine is
23# shown here regardless of its category — see effective_group().
24SEARCH_ENGINE_GROUPS: tuple[SearchEngineGroup, ...] = (
25 SearchEngineGroup("favorites", "Favorites", "⭐"),
26 SearchEngineGroup("collections", "Collections", "📁"),
27 SearchEngineGroup("academic", "Academic", "🔬"),
28 SearchEngineGroup("local_rag", "Local RAG", "📂"),
29 SearchEngineGroup("books", "Books", "📚"),
30 SearchEngineGroup("code", "Code", "💻"),
31 SearchEngineGroup("news", "News", "📰"),
32 SearchEngineGroup("no_api_key", "No API key", "🌐"),
33 SearchEngineGroup("api_key", "API key", "🔑"),
34)
36# Not a secret — the band's identifier string; matches SEARCH_ENGINE_GROUPS[0].
37FAVORITES_GROUP_KEY = "favorites" # gitleaks:allow
39_GROUP_BY_KEY: dict[str, SearchEngineGroup] = {
40 group.key: group for group in SEARCH_ENGINE_GROUPS
41}
42_GROUP_ORDER: dict[str, int] = {
43 group.key: index for index, group in enumerate(SEARCH_ENGINE_GROUPS)
44}
46# Maps the category label produced by
47# settings_routes._get_engine_icon_and_category() to a band. Any category not
48# listed here (e.g. "Web Search", "Search") falls through to the API-key split.
49# Keep these strings in sync with that function.
50_CATEGORY_TO_GROUP: dict[str, str] = {
51 "Scientific": "academic",
52 "Local RAG": "local_rag",
53 "Books": "books",
54 "Code": "code",
55 "News": "news",
56}
59def is_collection_engine(engine_id: str) -> bool:
60 """True for the document-collection pseudo-engines: the aggregate
61 "Search All Collections" entry (``library``) and the per-collection
62 ``collection_<id>`` entries."""
63 return engine_id == "library" or engine_id.startswith("collection_")
66def classify_engine_group(
67 engine_id: str, category: str, requires_api_key: bool
68) -> str:
69 """Return the base band key for an engine, ignoring favorite status.
71 Document collections come first, then engines with a distinct category
72 (academic / local / books / code / news); everything else (generic web
73 search) is split only by whether it needs an API key.
74 """
75 if is_collection_engine(engine_id):
76 return "collections"
77 category_group = _CATEGORY_TO_GROUP.get(category)
78 if category_group is not None:
79 return category_group
80 return "api_key" if requires_api_key else "no_api_key"
83def effective_group(base_group_key: str, is_favorite: bool) -> str:
84 """The band an engine is actually shown in: the Favorites overlay wins over
85 the engine's base category so a starred engine floats to the top band."""
86 return FAVORITES_GROUP_KEY if is_favorite else base_group_key
89def group_order(group_key: str) -> int:
90 """Sort index for a band (lower sorts higher); unknown keys sort last."""
91 return _GROUP_ORDER.get(group_key, len(SEARCH_ENGINE_GROUPS))
94def group_label(group_key: str) -> str:
95 """Header text for a band; falls back to the key if unknown."""
96 group = _GROUP_BY_KEY.get(group_key)
97 return group.label if group is not None else group_key
100def group_icon(group_key: str) -> str:
101 """Emoji for a band; empty string if unknown."""
102 group = _GROUP_BY_KEY.get(group_key)
103 return group.icon if group is not None else ""