Skip to content

feat(WP6): operator assistant - grounded in live state + docs, never acts, works without a key - #14

Merged
Neal006 merged 1 commit into
mainfrom
worktree-wp6-assistant
Sep 13, 2026
Merged

Neal006 merged 1 commit into
mainfrom
worktree-wp6-assistant

Conversation

@Neal006

@Neal006 Neal006 commented Sep 13, 2026

Copy link
Copy Markdown
Owner

What

The facilities manager can ask "why is bay c07 only getting 1.4 kW?" or "what happens if the grid API dies?" and get a short answer grounded in live site state + repo docs, with a link to the page that shows it. It explains and suggests; it never executes an action.

Backend

  • noonshift/assist.py
    • snapshot() — plain dict from S, built with the same REST builders the dashboard uses so numbers agree: sim time, mode + ladder, site limits, impact (incl. renewable share / health $), status, the 15 most relevant connector rows (at risk → urgent → charging → done → by slack) + counts, waiting, last 20 events, signal summary (min/max/mean MOER, zero-carbon hours, hourly), tariff bands + block. Capped at 24 k chars (~6 k tokens); events are the elastic part.
    • ask() — claude-opus-5, SYSTEM = [RULES, KNOWLEDGE] both cache_control: ephemeral, volatile state in the user turn, thinking={"type":"adaptive"}, output_config={"effort":"low"}, max_tokens=1024. Errors most-specific first: AuthenticationError/missing key → fallback; RateLimitError → 429 + Retry-After; APIConnectionError/APIStatusError → fallback with degraded: true; request ids logged.
    • fallback() — keyword-routed templates filled from the snapshot (bay by id, at-risk, ladder walk, safe share, CO₂/km, urgency bands, queue/move-by, done/idle, money §7b, load vs block/feed, signal window, 1.4 kW floor). Unknown → the six starters. Carries fallback: true.
    • parse() lifts trailing label -> /ops/<page> lines (max 2) and [sources: …]; allow() = 10 req/min per IP; suggestions() = 6 starters from state.
  • noonshift/assist_knowledge.md — ~4 k tokens hand-condensed from README, ARCHITECTURE §2–5, business §0b/§4b/§7b, metrics §2/§3/§6, hard-questions — updated to what the code does now (safe-share full rung, two OCPP profiles, first-hour floor, 0 kW = deferred slot / 1.4 kW = 6 A floor).
  • POST /assist {question, history?, page?} → {answer, sources, suggested_actions, fallback?, degraded?, usage?}; GET /assist/suggestions. ops_auth bearer dependency (OPS_TOKEN; open when unset) on /assist* and /demo/*. S["events"] ring buffer (20).
  • requirements.txt +anthropic==1.5.* (imported lazily inside ask(); CI needs neither key nor network).

Web (web/, no new deps)

  • AssistDrawer.jsx: floating Ask button in AppShell, right drawer 360 px (full width < sm), history in React state + sessionStorage (try/catch), six chips refreshed on open, sources as tags, actions as buttons → navigate(path), "offline answer" tag on fallback/degraded, loading, error + retry, 429 shows the retry-after. Sends page: location.pathname and the last 6 turns.
  • lib/api.js — single API base (was a private const in the context).

Found and fixed on the way

  • site_peaks() / hourly_ledger() dropped the start-minute offset of mid-slot plug-ins: two cars misaligned in opposite directions could report a slot peak above the feed (the smoke printed 171.6 kW on a 150 kW feed) although the instantaneous sum never exceeded it (the day test checks that and passes). One-line root fix in both; test_metered_peak_never_reports_above_the_feed fails on the old code.

Tests

  • tests/test_assist.py (22): snapshot keys/cap/row shape, JSON-serialisable, every starter gets a grounded fallback, 13 hard questions route to the right template, unknown → starters, parser (actions + sources), rate limiter, no-key ask() → fallback, ops_auth, prompt-cache reuse test skipped without ANTHROPIC_API_KEY.
  • scripts/smoke.py §8: /assist/suggestions = 6, /assist with history → 200, ladder answer, /ops/ actions, fallback (no key), 422 on empty.
  • Full suite: 92 passed, 1 skipped; smoke SMOKE OK; oxlint (1 pre-existing notice) + vite build ✓; contracts regenerated.

Assumptions / honest limits

  • No real model call was made in this session (no ANTHROPIC_API_KEY on this machine), so the "observed usage" the spec asks for is not in this PR; the cache test will report it on the first keyed run. Estimated per question: ~10 k cached + 1–2 k uncached input, ≤ 1 k output ≈ $0.03–0.05 with cache hits.
  • OPS_TOKEN guards /assist* and /demo/* only; driver/read endpoints stay open (they are what the driver app uses).
  • Rate limit is in-process (one worker); a shared store if the API is ever scaled out.
  • Drawer layout checked by lint/build only — no browser in this session.

🤖 Generated with Claude Code

https://claude.ai/code/session_017thvbGo7rTVnG2f46cLMzU

…r acts, works without a key

noonshift/assist.py: snapshot() (<= 15 relevant connector rows + counts, last 20 events,
signal/tariff summaries, capped ~6k tokens), ask() on claude-opus-5 with a cached SYSTEM
prefix (RULES + assist_knowledge.md), adaptive thinking, effort low; errors most-specific
first: bad/missing key -> fallback, RateLimitError -> 429 + Retry-After, connection/5xx ->
fallback flagged degraded. Deterministic keyword-routed fallback fills templates from the
snapshot so the demo needs no key or network. parse() lifts `label -> /ops/<page>` lines
and [sources: ...]. 10 req/min per IP. OPS_TOKEN bearer dependency (open when unset) on
/assist and /demo/*.

API: POST /assist, GET /assist/suggestions (6 starters from state); S["events"] ring buffer.
Web: AssistDrawer (floating Ask, right drawer, sessionStorage history, chips, sources,
action buttons, offline-answer tag); API base moved to lib/api.js. No new front-end deps.

Also fixes a reporting bug found by the smoke: site_peaks()/hourly_ledger() dropped the
start-minute offset of mid-slot plug-ins, so misaligned cars could sum into one slot and
show a peak above the feed that never happened. Test added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017thvbGo7rTVnG2f46cLMzU
Copilot AI lite review requested due to automatic review settings September 13, 2026 01:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@Neal006
Neal006 merged commit 7d80d07 into main Sep 13, 2026
3 checks passed
@Neal006
Neal006 deleted the worktree-wp6-assistant branch September 13, 2026 01:50
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