Coverage for integrations / ui_actions / page_registry.py: 100.0%
53 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
1"""page_registry — single source of truth for Nunba/Hevolve app pages.
3Each PageEntry names one destination that the LiquidUI action bar can
4surface, plus the phrases that should route to it. The backend owns this
5registry so the LangChain agent can see the full set of destinations as
6part of its tool context — saying 'take me to the model manager' lands
7on /admin/models deterministically instead of devolving into chitchat.
9Adding a new page:
10 1. Add a PageEntry below with a stable `id` (used as the external key).
11 2. List the route + descriptive label + short description.
12 3. Fill `keywords` with the verbs/nouns a user might speak when
13 trying to reach this page. Keep them lowercase and distinctive.
15The frontend mirrors this registry (same ids + routes) in
16`landing-page/src/config/pageRegistry.js`. Keep the two in sync when
17you add or rename entries.
18"""
19from __future__ import annotations
21from dataclasses import dataclass, field
22from typing import Iterable, List, Optional
25@dataclass(frozen=True)
26class PageEntry:
27 """One destination the LiquidUI action bar can surface."""
29 id: str # stable key shared with frontend
30 label: str # short button label (≤20 chars)
31 route: str # SPA route path
32 description: str # one-line description for the LLM + tooltip
33 keywords: frozenset # lowercase tokens that trigger the match
34 icon: str = 'open_in_new' # MUI icon name, optional
35 requires_role: Optional[str] = None # 'central'|'regional'|None
36 category: str = 'general'
39def _ks(*words: str) -> frozenset:
40 """frozenset of lowercase keywords for cleaner literal tables."""
41 return frozenset(w.lower() for w in words)
44PAGE_REGISTRY: tuple[PageEntry, ...] = (
45 # ── Social ──────────────────────────────────────────────────────
46 PageEntry(
47 id='social_feed',
48 label='Social Hub',
49 route='/social',
50 description='Main social feed — posts, comments, discussions, votes.',
51 keywords=_ks('social', 'feed', 'posts', 'discussions', 'community',
52 'home', 'timeline'),
53 icon='forum',
54 category='social',
55 ),
56 PageEntry(
57 id='agent_chat',
58 label='Agent Chat',
59 route='/local',
60 description='Chat with the local AI agent (this screen).',
61 keywords=_ks('chat', 'agent', 'talk', 'assistant', 'local'),
62 icon='chat',
63 category='chat',
64 ),
65 PageEntry(
66 id='games',
67 label='Games',
68 route='/social/games',
69 description='Play kids learning games and co-op puzzles.',
70 keywords=_ks('game', 'games', 'play', 'puzzle', 'learning'),
71 icon='sports_esports',
72 category='play',
73 ),
74 PageEntry(
75 id='kids',
76 label='Kids Learning',
77 route='/social/kids',
78 description='Kids learning activities and educational content.',
79 keywords=_ks('kids', 'children', 'learning', 'education', 'learn'),
80 icon='school',
81 category='play',
82 ),
83 PageEntry(
84 id='marketplace',
85 label='Marketplace',
86 route='/social/marketplace',
87 description='Browse and install agents, skills, and experiences.',
88 keywords=_ks('marketplace', 'market', 'store', 'shop', 'browse',
89 'install', 'discover'),
90 icon='storefront',
91 category='discover',
92 ),
93 PageEntry(
94 id='mcp_tools',
95 label='MCP Tools',
96 route='/social/tools',
97 description='Browse and configure MCP (Model Context Protocol) tools.',
98 keywords=_ks('mcp', 'tools', 'integrations', 'plugins'),
99 icon='extension',
100 category='discover',
101 ),
102 PageEntry(
103 id='autopilot',
104 label='Autopilot',
105 route='/social/autopilot',
106 description='Configure autonomous agent workflows.',
107 keywords=_ks('autopilot', 'workflow', 'automation', 'autonomous',
108 'pipeline'),
109 icon='precision_manufacturing',
110 category='agents',
111 ),
112 PageEntry(
113 id='hive_contest',
114 label='Hive Contest',
115 route='/hive_contest',
116 description=(
117 'Open hive contest — submit ideas, watch the live floating '
118 'ideas wall, plug Claude Code into HARTOS via MCP onramp. '
119 'Three tracks: digital, embodied, human-wellness.'
120 ),
121 keywords=_ks('contest', 'hive contest', 'hive_contest',
122 'submit idea', 'hackathon', 'leaderboard',
123 'claude code', 'mcp', 'onramp', 'spark', 'prize'),
124 icon='leaderboard',
125 category='discover',
126 ),
127 # ── Admin ───────────────────────────────────────────────────────
128 PageEntry(
129 id='admin_models',
130 label='Model Management',
131 route='/admin/models',
132 description='Load, unload, download, and swap LLM/TTS/STT/VLM models.',
133 keywords=_ks('models', 'model', 'management', 'llm', 'tts', 'stt',
134 'vlm', 'download', 'load', 'unload'),
135 icon='memory',
136 requires_role='central',
137 category='admin',
138 ),
139 PageEntry(
140 id='admin_channels',
141 label='Channels',
142 route='/admin/channels',
143 description=(
144 'Connect / manage messaging channels (WhatsApp, Telegram, '
145 'Slack, Discord, etc.).'
146 ),
147 keywords=_ks('channels', 'channel', 'whatsapp', 'telegram', 'slack',
148 'discord', 'messaging', 'connect', 'integrations'),
149 icon='hub',
150 requires_role='central',
151 category='admin',
152 ),
153 PageEntry(
154 id='admin_providers',
155 label='AI Providers',
156 route='/admin/providers',
157 description='Configure AI providers (OpenAI, Anthropic, local, etc.).',
158 keywords=_ks('providers', 'provider', 'api', 'openai', 'anthropic',
159 'gateway', 'keys'),
160 icon='cloud',
161 requires_role='central',
162 category='admin',
163 ),
164 PageEntry(
165 id='admin_users',
166 label='Users',
167 route='/admin/users',
168 description='Manage users, roles, permissions.',
169 keywords=_ks('users', 'user', 'members', 'permissions', 'roles',
170 'admin'),
171 icon='people',
172 requires_role='central',
173 category='admin',
174 ),
175 PageEntry(
176 id='admin_home',
177 label='Admin',
178 route='/admin',
179 description='Admin dashboard overview.',
180 keywords=_ks('admin', 'dashboard', 'settings', 'configuration'),
181 icon='admin_panel_settings',
182 requires_role='central',
183 category='admin',
184 ),
185)
187_BY_ID: dict[str, PageEntry] = {p.id: p for p in PAGE_REGISTRY}
190def list_pages(
191 user_role: str = 'flat',
192 category: Optional[str] = None,
193) -> list[PageEntry]:
194 """Return pages visible to the given user role, optionally filtered.
196 Role gate: 'central' > 'regional' > 'flat' > 'guest'. A page that
197 requires 'central' is hidden from 'flat'/'guest' users so we don't
198 surface actions the frontend can't execute.
199 """
200 _order = {'guest': 0, 'flat': 1, 'regional': 2, 'central': 3}
201 user_rank = _order.get(user_role, 1)
202 out: list[PageEntry] = []
203 for p in PAGE_REGISTRY:
204 if p.requires_role:
205 needed = _order.get(p.requires_role, 3)
206 if user_rank < needed:
207 continue
208 if category and p.category != category:
209 continue
210 out.append(p)
211 return out
214def resolve_page(
215 query: str,
216 user_role: str = 'flat',
217 top_k: int = 3,
218) -> list[tuple[PageEntry, int]]:
219 """Score pages against a natural-language query.
221 Lightweight lexical ranker: +3 for each keyword hit, +5 if the
222 page label (lowercased) is a substring of the query, +2 for id
223 match. No ML, no embedding calls — this runs on every message so
224 it has to be cheap and deterministic.
226 Returns up to `top_k` (PageEntry, score) pairs with score > 0,
227 sorted by score descending. Pages the user can't access are filtered.
228 """
229 if not query:
230 return []
231 q = query.lower()
232 visible = {p.id for p in list_pages(user_role=user_role)}
233 scored: list[tuple[PageEntry, int]] = []
234 for page in PAGE_REGISTRY:
235 if page.id not in visible:
236 continue
237 score = 0
238 if page.label.lower() in q:
239 score += 5
240 if page.id.lower() in q:
241 score += 2
242 for kw in page.keywords:
243 if kw in q:
244 score += 3
245 if score > 0:
246 scored.append((page, score))
247 scored.sort(key=lambda item: item[1], reverse=True)
248 return scored[:top_k]
251def page_to_ui_action(page: PageEntry) -> dict:
252 """Serialize a PageEntry as a ui_action dict for the chat response.
254 The frontend's LiquidActionBar consumes this exact shape, so keep
255 the keys stable or update both sides together.
256 """
257 return {
258 'id': page.id,
259 'type': 'navigate',
260 'label': page.label,
261 'route': page.route,
262 'icon': page.icon,
263 'description': page.description,
264 'category': page.category,
265 }