mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-14 14:12:44 +00:00
Main's rewritten test_tui_npm_install.py tests call _make_tui_argv expecting the Ink/npm flow unconditionally; with the dual-engine dispatch merged in, _resolve_tui_engine() auto-selects opentui whenever ui-opentui/dist is built in the repo, routing the call away from the path under test (first subprocess became 'node --version' instead of 'npm run build'). Pin the engine to ink via an autouse fixture, mirroring the existing pinning precedent in test_tui_resume_flow.py.
154 lines
6.2 KiB
Python
154 lines
6.2 KiB
Python
"""Curated catalog of starter cron-job suggestions.
|
|
|
|
These are the built-in automations Hermes can offer a new user out of the box —
|
|
the ``catalog`` source of the unified suggestion surface. Each entry is a
|
|
ready-to-run ``cron.jobs.create_job`` spec wrapped as a suggestion; the user
|
|
accepts via ``/suggestions``. Nothing here auto-schedules.
|
|
|
|
The "important-mail monitor" entry is where the old proactive-monitor engine
|
|
lives now: its ``classify_items.py`` (poll a source -> LLM-score urgency ->
|
|
surface only above-threshold) is ONE catalog automation, not a standalone
|
|
feature.
|
|
|
|
Adding a catalog entry: append a CatalogEntry. Keep prompts self-contained
|
|
(cron jobs run with no chat context) and schedules sensible. The ``job_spec``
|
|
is passed verbatim to ``create_job`` on accept.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any, Callable, Dict, List, Optional
|
|
|
|
__all__ = ["CatalogEntry", "CATALOG", "seed_catalog_suggestions", "classify_items_script_path"]
|
|
|
|
|
|
def classify_items_script_path() -> str:
|
|
"""Absolute path to the urgency classifier script shipped with cron/."""
|
|
return str((Path(__file__).resolve().parent / "scripts" / "classify_items.py"))
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CatalogEntry:
|
|
"""A curated starter automation offered as a suggestion."""
|
|
|
|
key: str # stable dedup key (never re-offered once dismissed)
|
|
title: str
|
|
description: str
|
|
job_spec: Dict[str, Any] # kwargs for cron.jobs.create_job
|
|
|
|
|
|
# The curated set. Schedules use the cron/interval syntax create_job accepts.
|
|
CATALOG: List[CatalogEntry] = [
|
|
CatalogEntry(
|
|
key="catalog:daily-briefing",
|
|
title="Daily briefing",
|
|
description="Every morning at 8am, a short briefing: today's calendar, "
|
|
"weather, and anything urgent waiting on you.",
|
|
job_spec={
|
|
"prompt": (
|
|
"Produce a concise morning briefing for the user: today's "
|
|
"calendar events, the local weather, and any urgent items "
|
|
"(unread important email, due tasks). Keep it short and "
|
|
"scannable. If you have no connected data sources, give a brief "
|
|
"general good-morning with the date and offer to connect "
|
|
"calendar/email."
|
|
),
|
|
"schedule": "0 8 * * *",
|
|
"name": "Daily briefing",
|
|
"deliver": "origin",
|
|
},
|
|
),
|
|
CatalogEntry(
|
|
key="catalog:important-mail-monitor",
|
|
title="Important-mail monitor",
|
|
description="Check your inbox periodically and ping you ONLY about mail "
|
|
"that actually needs attention — never the newsletters.",
|
|
job_spec={
|
|
"prompt": (
|
|
"Check the user's inbox for new messages since the last run. "
|
|
"For each candidate, judge urgency against this rule: surface "
|
|
"only mail that needs a reply today, is from a manager/family "
|
|
"member, or mentions a deadline. Pipe candidates through the "
|
|
"urgency classifier (run `python3 -m cron.scripts.classify_items "
|
|
"--threshold 7 --criteria ...` from the hermes-agent install — "
|
|
"resolve the script path at run time, do not assume a fixed "
|
|
"location) and deliver ONLY what it returns. If nothing "
|
|
"clears the bar, respond with [SILENT] so the user is not "
|
|
"pinged. Requires a connected mail source; if none is "
|
|
"configured, explain how to connect one and then stop."
|
|
),
|
|
"schedule": "every 30m",
|
|
"name": "Important-mail monitor",
|
|
"deliver": "origin",
|
|
},
|
|
),
|
|
CatalogEntry(
|
|
key="catalog:weekly-review",
|
|
title="Weekly review",
|
|
description="Every Sunday evening, a recap of the week: what got done, "
|
|
"what's still open, and what's coming up next week.",
|
|
job_spec={
|
|
"prompt": (
|
|
"Produce a weekly review for the user: summarize what was "
|
|
"accomplished this week, list still-open items, and preview "
|
|
"next week's calendar. Pull from whatever sources are connected "
|
|
"(calendar, task tools, recent conversations). Keep it tight."
|
|
),
|
|
"schedule": "0 18 * * 0",
|
|
"name": "Weekly review",
|
|
"deliver": "origin",
|
|
},
|
|
),
|
|
CatalogEntry(
|
|
key="catalog:standup-reminder",
|
|
title="Workday start reminder",
|
|
description="A weekday nudge at 9am with your day's agenda and top "
|
|
"priorities, so you start focused.",
|
|
job_spec={
|
|
"prompt": (
|
|
"Give the user a brief weekday start-of-day nudge: their "
|
|
"calendar for today and the 1-3 highest-priority things to "
|
|
"focus on, inferred from recent context and any task tools. "
|
|
"Encouraging, short, one message."
|
|
),
|
|
"schedule": "0 9 * * 1-5",
|
|
"name": "Workday start reminder",
|
|
"deliver": "origin",
|
|
},
|
|
),
|
|
]
|
|
|
|
|
|
def seed_catalog_suggestions(
|
|
*,
|
|
add_fn: Optional[Callable[..., Optional[Dict[str, Any]]]] = None,
|
|
keys: Optional[List[str]] = None,
|
|
) -> List[Dict[str, Any]]:
|
|
"""Register catalog entries as pending suggestions.
|
|
|
|
``add_fn`` defaults to ``cron.suggestions.add_suggestion`` (injectable for
|
|
tests). ``keys`` restricts to specific catalog entries; omit to seed all.
|
|
Entries already dismissed/accepted (by dedup key) or beyond the pending cap
|
|
are skipped by the store, so re-seeding is safe and idempotent. Returns the
|
|
list of suggestion records actually created.
|
|
"""
|
|
if add_fn is None:
|
|
from cron.suggestions import add_suggestion as add_fn # type: ignore[assignment]
|
|
|
|
wanted = set(keys) if keys else None
|
|
created: List[Dict[str, Any]] = []
|
|
for entry in CATALOG:
|
|
if wanted is not None and entry.key not in wanted:
|
|
continue
|
|
rec = add_fn(
|
|
title=entry.title,
|
|
description=entry.description,
|
|
source="catalog",
|
|
job_spec=dict(entry.job_spec),
|
|
dedup_key=entry.key,
|
|
)
|
|
if rec is not None:
|
|
created.append(rec)
|
|
return created
|