Skip to content

feat(dashboard): /skills/ema endpoint reading SessionDB.query_skill_ema#9

Open
Brecht-H wants to merge 1 commit intostainlu:mainfrom
Brecht-H:feat/skills-ema-endpoint-2026-05-04
Open

feat(dashboard): /skills/ema endpoint reading SessionDB.query_skill_ema#9
Brecht-H wants to merge 1 commit intostainlu:mainfrom
Brecht-H:feat/skills-ema-endpoint-2026-05-04

Conversation

@Brecht-H
Copy link
Copy Markdown

@Brecht-H Brecht-H commented May 4, 2026

Summary

Adds a single HTTP endpoint GET /api/plugins/hermes-labyrinth/skills/ema that wraps hermes_state.SessionDB.query_skill_ema() from upstream NousResearch/hermes-agent PR #19508.

That PR adds a skill_invocations table + skill_stats_daily view + a calendar-aware exponentially-weighted moving-average query for per-(skill_name, model) success rate / cost / duration. The endpoint surfaces those EMAs to the dashboard so operators can A/B local Qwen against external models (DeepSeek, Kimi-via-OpenRouter, etc.) without flying blind.

Endpoint contract

GET /api/plugins/hermes-labyrinth/skills/ema?window_days=14&alpha=0.3

Returns a JSON list of dicts (one per (skill_name, model) bucket) with:

  • skill_name, model, provider
  • sample_count, success_count, failure_count
  • ema_success_rate, ema_duration_s, ema_cost_per_call
  • days_with_data, last_invoked_at

Sorted by last_invoked_at descending.

Defensive on older hermes-agent installs

  • state.db missing → []
  • SessionDB has no query_skill_ema attribute (hermes-agent < schema v12) → []
  • 500 only on unexpected exceptions

This means installations on hermes-agent versions predating #19508 continue to render the dashboard normally (just with an empty /skills/ema panel).

Smoke test (live, this machine)

$ systemctl restart hermes-dashboard.service
$ curl http://localhost:9119/api/plugins/hermes-labyrinth/skills/ema
[]    # HTTP 200 — empty until cron-fired skills populate skill_invocations
$ curl http://localhost:9119/api/plugins/hermes-labyrinth/journeys?source=cron&limit=1
{"journeys":...}    # HTTP 200 — no regression

Frontend follow-up

Deliberately a separate PR — the wiring shape (tabular vs sparkline-per-skill vs scatter-plot-cost-vs-quality) is a UX decision worth its own review cycle. Happy to ship a frontend PR once we converge on the panel design.

Test plan

  • Endpoint registers and returns [] on empty skill_invocations table
  • Existing endpoints (/skills, /journeys, /cron, /health) still respond
  • Defensive fall-through for hermes-agent < schema v12
  • Endpoint returns non-empty data once cron-fired skills populate skill_invocations (pending Mac/Allaert flipping the analyzer crons to enabled: true)
  • Frontend panel (separate PR)

🤖 Generated with Claude Code


View in Codesmith
Need help on this PR? Tag @codesmith with what you need.

  • Let Codesmith autofix CI failures and bot reviews

Adds an HTTP wrapper around hermes-agent's
SessionDB.query_skill_ema() so the labyrinth dashboard can render
per-skill exponentially-weighted moving averages of success rate,
cost-per-call, and duration.

Endpoint:
  GET /api/plugins/hermes-labyrinth/skills/ema
       ?window_days=14   (calendar-day window)
       &alpha=0.3        (0 < alpha < 1; α=0.3 ≈ 1.94d half-life)

Returns a JSON list of dicts with the same shape as
query_skill_ema():
  skill_name, model, provider,
  sample_count, success_count, failure_count,
  ema_success_rate, ema_duration_s, ema_cost_per_call,
  days_with_data, last_invoked_at

Defensive on older hermes-agent installs:
- Returns [] if state.db is missing
- Returns [] if SessionDB has no query_skill_ema attribute
  (hermes-agent < schema v12 / NousResearch/hermes-agent#19508)
- 500 only on unexpected exceptions

This is the F1 follow-up flagged in NousResearch/hermes-agent#19508
PR review (R1: Mac 3-lens consensus, 2026-05-04).

Smoke test (live):
  systemctl restart hermes-dashboard.service
  curl http://localhost:9119/api/plugins/hermes-labyrinth/skills/ema
  -> 200 []   # empty until cron-fired skills populate skill_invocations
  curl http://localhost:9119/api/plugins/hermes-labyrinth/journeys?...
  -> 200 (no regression)

Frontend panel deliberately deferred to a follow-up PR — wiring
shape (tabular vs sparkline-per-skill) is a UX decision worth its
own review cycle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 4, 2026 17:46
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new dashboard API endpoint that exposes skill-level EMA metrics from hermes_state.SessionDB.query_skill_ema(), so the Hermes Labyrinth dashboard can surface recent per-model/per-skill performance data when the upstream agent schema supports it.

Changes:

  • Adds GET /api/plugins/hermes-labyrinth/skills/ema to proxy SessionDB.query_skill_ema(window_days, alpha).
  • Returns [] when state.db is missing or the installed SessionDB does not provide query_skill_ema, preserving compatibility with older hermes-agent installs.
  • Converts database initialization/query failures into HTTP 500 responses for the new endpoint.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread dashboard/plugin_api.py
Comment on lines +785 to +792
query = getattr(db, "query_skill_ema", None)
if not callable(query):
# hermes-agent older than PR #19508 — graceful empty response.
return []
try:
return query(window_days=window_days, alpha=alpha)
except Exception as e:
raise HTTPException(status_code=500, detail=f"query_skill_ema failed: {e}")
Comment thread dashboard/plugin_api.py
Comment on lines +760 to +790
async def skills_ema(window_days: int = 14, alpha: float = 0.3):
"""Per-(skill_name, model) exponentially-weighted moving averages.

Thin wrapper over ``hermes_state.SessionDB.query_skill_ema(...)``.
Requires hermes-agent ≥ schema v12 (PR
`NousResearch/hermes-agent#19508`). On older installs (no
``skill_invocations`` table or no ``query_skill_ema`` method) we
return ``[]`` so the dashboard renders an empty panel rather than
500ing.

Query params:
- ``window_days`` (default 14) — calendar-day window
- ``alpha`` (default 0.3) — exponential smoothing factor.
``α=0.3`` ≈ 1.94-day half-life. Use ``α≈0.129`` for a true
5-day half-life. ``0 < alpha < 1``.

Returns a JSON list of dicts. See ``query_skill_ema`` docstring
for field semantics.
"""
if not _state_db_exists():
return []
try:
db = _db()
except Exception as e:
raise HTTPException(status_code=500, detail=f"state.db unavailable: {e}")
query = getattr(db, "query_skill_ema", None)
if not callable(query):
# hermes-agent older than PR #19508 — graceful empty response.
return []
try:
return query(window_days=window_days, alpha=alpha)
Comment thread dashboard/plugin_api.py
Comment on lines +783 to +792
except Exception as e:
raise HTTPException(status_code=500, detail=f"state.db unavailable: {e}")
query = getattr(db, "query_skill_ema", None)
if not callable(query):
# hermes-agent older than PR #19508 — graceful empty response.
return []
try:
return query(window_days=window_days, alpha=alpha)
except Exception as e:
raise HTTPException(status_code=500, detail=f"query_skill_ema failed: {e}")
Comment thread dashboard/plugin_api.py
Comment on lines +759 to +792
@router.get("/skills/ema")
async def skills_ema(window_days: int = 14, alpha: float = 0.3):
"""Per-(skill_name, model) exponentially-weighted moving averages.

Thin wrapper over ``hermes_state.SessionDB.query_skill_ema(...)``.
Requires hermes-agent ≥ schema v12 (PR
`NousResearch/hermes-agent#19508`). On older installs (no
``skill_invocations`` table or no ``query_skill_ema`` method) we
return ``[]`` so the dashboard renders an empty panel rather than
500ing.

Query params:
- ``window_days`` (default 14) — calendar-day window
- ``alpha`` (default 0.3) — exponential smoothing factor.
``α=0.3`` ≈ 1.94-day half-life. Use ``α≈0.129`` for a true
5-day half-life. ``0 < alpha < 1``.

Returns a JSON list of dicts. See ``query_skill_ema`` docstring
for field semantics.
"""
if not _state_db_exists():
return []
try:
db = _db()
except Exception as e:
raise HTTPException(status_code=500, detail=f"state.db unavailable: {e}")
query = getattr(db, "query_skill_ema", None)
if not callable(query):
# hermes-agent older than PR #19508 — graceful empty response.
return []
try:
return query(window_days=window_days, alpha=alpha)
except Exception as e:
raise HTTPException(status_code=500, detail=f"query_skill_ema failed: {e}")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants