diff --git a/.planning/phases/06-lab-advisor/06-02-SUMMARY.md b/.planning/phases/06-lab-advisor/06-02-SUMMARY.md new file mode 100644 index 0000000..8277005 --- /dev/null +++ b/.planning/phases/06-lab-advisor/06-02-SUMMARY.md @@ -0,0 +1,143 @@ +--- +phase: 06-lab-advisor +plan: "02" +subsystem: api +tags: [go, sse, openrouter, claude, advisor, streaming, postgresql, chi] + +# Dependency graph +requires: + - phase: 06-lab-advisor-01 + provides: store.Store, CreateConversation, AddMessage, GetConversation, ListConversations, RunMigrations + - phase: 04-usb-events + provides: SSE pattern (text/event-stream headers, Flush loop) +provides: + - POST /api/advisor/chat — SSE streaming chat backed by Claude Opus via OpenRouter + - GET /api/advisor/conversations — list past conversations from PostgreSQL + - GET /api/advisor/conversations/{id} — full message thread + - InventoryContextBuilder — 60s-cached compact NetBox inventory summary for system prompt + - Tier3 config field in AIConfig for OpenRouter lab-advisor model +affects: [07-frontend-advisor, future-ai-tiers] + +# Tech tracking +tech-stack: + added: [] + patterns: + - "SSE streaming via go-openai CreateChatCompletionStream + http.Flusher.Flush per token" + - "Nil-guard in router: advisorHandler==nil returns 503 instead of panic" + - "Stale cache on NetBox error: return cached context rather than failing the chat request" + - "Body size guard: http.MaxBytesReader(64KB) + message truncation at 8000 chars" + +key-files: + created: + - internal/advisor/context.go + - internal/advisor/handler.go + modified: + - internal/ai/types.go + - internal/config/config.go + - internal/api/router.go + - cmd/hwlab/main.go + +key-decisions: + - "Tier3 config added to AIConfig struct (not a separate config) for consistency with Tier1/Tier2" + - "advisorHandler nil-guard in router returns 503 — graceful degradation when DB not configured" + - "Stale InventoryContext served on NetBox error rather than failing chat — availability over freshness" + - "HWLAB_DATABASE_URL absence is non-fatal; advisor endpoints return 503 rather than crashing server" + +patterns-established: + - "SSE pattern: Content-Type: text/event-stream + Cache-Control: no-cache + X-Accel-Buffering: no + Flush per write" + - "Per-request openai.Client construction from Tier3 config allows model override without restart" + +requirements-completed: [ADV-01, ADV-02, ADV-03, ADV-05] + +# Metrics +duration: 25min +completed: 2026-04-10 +--- + +# Phase 06 Plan 02: Lab Advisor Backend Summary + +**SSE streaming advisor chat backed by Claude Opus via OpenRouter, with 60s-cached NetBox inventory context injected into every system prompt and full PostgreSQL persistence of conversations and messages.** + +## Performance + +- **Duration:** 25 min +- **Started:** 2026-04-10T07:11:00Z +- **Completed:** 2026-04-10T07:36:19Z +- **Tasks:** 2 +- **Files modified:** 6 (2 created, 4 modified) + +## Accomplishments + +- InventoryContextBuilder: fetches up to 200 devices from NetBox, builds a compact text summary (total count, category breakdown, recent 20 items), caches 60s under sync.Mutex, falls back to stale cache on NetBox error +- AdvisorHandler: StreamChat streams tokens from OpenRouter via go-openai CreateChatCompletionStream; each token delivered as `data: {"conversation_id":"...","token":"..."}` SSE event; body limited to 64KB, message truncated to 8000 chars; API key never echoed +- Full persistence: CreateConversation + AddMessage(user) before stream; AddMessage(assistant) after stream completes +- Three endpoints registered under /api/advisor/ with nil-guard for DB-unavailable graceful degradation +- Tier3 TierConfig added to AIConfig with HWLAB_AI_TIER3_* env bindings and OpenRouter/claude-opus-4 defaults + +## Task Commits + +1. **Task 1: InventoryContextBuilder with 60s cache** - `7b02e67` (feat) +2. **Task 2: AdvisorHandler SSE streaming + router wiring** - `0190e85` (feat) + +## Files Created/Modified + +- `internal/advisor/context.go` - InventoryContextBuilder with 60s cache and compact NetBox summary +- `internal/advisor/handler.go` - AdvisorHandler: StreamChat, GetConversations, GetConversation +- `internal/ai/types.go` - Added Tier3 TierConfig field to AIConfig +- `internal/config/config.go` - Tier3 defaults (OpenRouter, claude-opus-4, 120s timeout) + env bindings +- `internal/api/router.go` - /api/advisor routes with nil-guard; import advisor package +- `cmd/hwlab/main.go` - Store init from HWLAB_DATABASE_URL, RunMigrations, AdvisorHandler wired + +## Decisions Made + +- Tier3 config follows the same TierConfig struct as Tier1/Tier2 — no special advisor config type needed +- Per-request openai.Client construction (not cached) allows model field in ChatRequest to override model without server restart, satisfying ADV-05 +- advisorHandler is nil when HWLAB_DATABASE_URL is absent; router nil-guard returns 503 on all three endpoints rather than panicking +- Stale inventory context returned on NetBox error (availability over freshness — homelab context) + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 2 - Missing Critical] Added Tier3 to AIConfig struct** +- **Found during:** Task 2 (AdvisorHandler implementation) +- **Issue:** Plan referenced `aiCfg.Tier3` but AIConfig only had Tier1 and Tier2 fields — handler would not compile +- **Fix:** Added `Tier3 TierConfig` to AIConfig in internal/ai/types.go; added defaults and env bindings in config.go +- **Files modified:** internal/ai/types.go, internal/config/config.go +- **Verification:** go build ./... passes; Tier3 config loads from HWLAB_AI_TIER3_* env vars +- **Committed in:** 0190e85 (Task 2 commit) + +**2. [Rule 2 - Missing Critical] Nil-guard in router for missing DB** +- **Found during:** Task 2 (router wiring) +- **Issue:** advisorHandler can be nil when HWLAB_DATABASE_URL is not set; registering nil method handlers would panic at request time +- **Fix:** Added nil check in router; returns 503 with descriptive message when advisor is disabled +- **Files modified:** internal/api/router.go +- **Verification:** go build ./... passes; server starts without DB configured +- **Committed in:** 0190e85 (Task 2 commit) + +--- + +**Total deviations:** 2 auto-fixed (2 missing critical) +**Impact on plan:** Both fixes required for correctness and safe server startup. No scope creep. + +## Issues Encountered + +- `.gitignore` entry `hwlab` matched the directory `cmd/hwlab/` — `git add cmd/hwlab/main.go` failed with "ignored file" warning. The file was already staged from a prior add, so commit proceeded normally. The gitignore entry targets the compiled binary, not the source directory; no fix needed. + +## Known Stubs + +None — all three endpoints are fully wired. StreamChat requires HWLAB_AI_TIER3_API_KEY to be set with a valid OpenRouter key for actual AI responses; without it, OpenRouter returns 401 which is surfaced as a 502 to the client. This is expected behavior for a homelab tool. + +## Threat Flags + +No new threat surface beyond what the plan's threat model covers. All mitigations from T-06-02-01 through T-06-02-03 are implemented (parameterized store calls, API key isolation, 64KB/8000-char input guards). + +## Next Phase Readiness + +- All three /api/advisor/* endpoints ready for frontend integration +- Set HWLAB_DATABASE_URL + HWLAB_AI_TIER3_API_KEY to enable advisor +- Frontend advisor chat UI (Phase 07 or equivalent) can connect to POST /api/advisor/chat and consume the SSE token stream + +--- +*Phase: 06-lab-advisor* +*Completed: 2026-04-10*