Files
hermes-agent/tui_gateway/onboarding_personalization.py
alt-glitch 6ec517c475 feat: onboarding offers catalog plugins beside connectors and installs the picked ones before handoff
NS-960. One card, one group ("connectors"): the curated catalog plugins
this OS runs lead the hosted connectors (D1, D4). A plugin whose app is
absent is greyed with the reason and stays pickable (D5). Picks land in
answers.plugins and ride the existing [setup] note to the guide.

The guide's runbook gains the install beat as the last thing before the
handoff card (D2, D3): narrow the picks to what the chosen task needs, then
ONE manage_catalog install call. A new suggested first task sits beside the
existing ones and installs its plugins through that same beat: "Set up my
games and streaming" (NVIDIA App + Broadcast) on a Windows PC with an NVIDIA
GPU, "Help me make something in Blender" everywhere else.

When the guide's install card settles, each plugin row's outcome
(installed / failed / skipped, whatever the user did) is written into the
answers (D6). The build session's runbook names what is ready, what was
offered and not installed, and what was picked but not offered, and tells
the build agent never to install. profiles.remember_onboarding records the
picked plugin names in the default profile's memory, next to the connectors.
2026-09-23 09:26:07 +05:30

46 lines
2.5 KiB
Python

"""Writes the setup facts agreed during onboarding into the default profile's user memory."""
import json
from hermes_constants import reset_hermes_home_override, set_hermes_home_override
from hermes_cli.profiles import get_profile_dir
from tools.memory_tool import load_on_disk_store, memory_tool
def remember_onboarding(answers: dict) -> dict:
if not isinstance(answers, dict):
raise ValueError('Onboarding answers must be an object')
facts = []
for key, label in (('name', 'User prefers to be called'), ('context', 'Working on'),
('theme', 'Desktop theme'), ('accent', 'Desktop accent'), ('layout', 'Desktop layout')):
value = answers.get(key)
if value is not None and not isinstance(value, str):
raise ValueError(f'{key} must be text')
if value and value.strip():
facts.append(f'{label}: {value.strip()}')
for key, label in (('focus', 'Focus areas'), ('connectors', 'Tools the user uses (not connection status)'),
('plugins', 'Hermes plugins the user picked during onboarding (not install status)')):
values = answers.get(key, [])
if not isinstance(values, list) or not all(isinstance(value, str) for value in values):
raise ValueError(f'{key} must be a list of text')
if values := list(dict.fromkeys(value.strip() for value in values if value.strip())):
facts.append(f'{label}: {", ".join(values)}')
if not facts:
return {'saved': True, 'profile': 'default', 'target': 'user'}
content = 'Agreed during onboarding:\n' + '\n'.join(facts)
if len(content) > 2000:
raise ValueError('Onboarding facts are too long to remember')
# The entry must land in the 'default' profile directory even when this RPC arrives on the guide's
# backend or under a custom Hermes home.
token = set_hermes_home_override(get_profile_dir('default'))
try:
result = json.loads(memory_tool(action='add', target='user', content=content, store=load_on_disk_store()))
if not result.get('success') or result.get('staged'):
raise ValueError(result.get('error') or result.get('message') or 'Memory was not saved')
# memory_tool can report success without the entry reaching disk, so read it back from a fresh store.
if content not in load_on_disk_store().user_entries:
raise ValueError('Could not verify saved onboarding facts')
return {'saved': True, 'profile': 'default', 'target': 'user'}
finally:
reset_hermes_home_override(token)