docs(06-lab-advisor-02): complete advisor backend plan

- 06-02-SUMMARY.md: SSE streaming chat, InventoryContextBuilder, 3 endpoints
This commit is contained in:
Mikkel Georgsen 2026-04-10 07:37:15 +00:00
parent 0190e8583c
commit ae34bc73fe

View file

@ -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*