From 3eed2e9c638f2a1111ca6944c794fe8e7e9a77ef Mon Sep 17 00:00:00 2001 From: Mikkel Georgsen Date: Fri, 10 Apr 2026 05:46:05 +0000 Subject: [PATCH] docs(02-01): complete AI package foundation plan Co-Authored-By: Claude Sonnet 4.6 --- .../phases/02-ai-pipeline/02-01-SUMMARY.md | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 .planning/phases/02-ai-pipeline/02-01-SUMMARY.md diff --git a/.planning/phases/02-ai-pipeline/02-01-SUMMARY.md b/.planning/phases/02-ai-pipeline/02-01-SUMMARY.md new file mode 100644 index 0000000..1551367 --- /dev/null +++ b/.planning/phases/02-ai-pipeline/02-01-SUMMARY.md @@ -0,0 +1,152 @@ +--- +phase: 02-ai-pipeline +plan: "01" +subsystem: ai +tags: [go, ai, openai, netbox, tdd, config] +dependency_graph: + requires: [internal/netbox.Client, internal/config.Config] + provides: [internal/ai.AIClient, internal/ai.TierClient, internal/ai.MockAIClient, internal/netbox.CreateDevice] + affects: [internal/config.Config, cmd/hwlab] +tech_stack: + added: [github.com/sashabaranov/go-openai v1.41.2] + patterns: [interface-with-mock, tier-routing-via-baseurl, viper-merge-config, oneOf-FK-helpers] +key_files: + created: + - internal/ai/types.go + - internal/ai/client.go + - internal/ai/mock.go + - internal/ai/client_test.go + - internal/ai/prompts/intake.go + - ai_config.json + modified: + - go.mod + - go.sum + - internal/netbox/client.go + - internal/netbox/client_test.go + - internal/config/config.go + - internal/config/config_test.go + - .gitignore +decisions: + - id: AI-CLIENT-01 + summary: "AIClient is an interface so TierClient and MockAIClient are interchangeable — all downstream plans depend on this contract, not the concrete type" + - id: AI-CLIENT-02 + summary: "TierClient tier-routing via openai.DefaultConfig + BaseURL override — single go-openai client factory, swap endpoint per tier from config" + - id: AI-CONFIG-01 + summary: "ai_config.json merged via separate viper instance to avoid SetConfigName collision with primary config.json" + - id: NB-CREATE-01 + summary: "go-netbox v4 CreateDevice uses Int32AsDeviceBayTemplateRequestDeviceType / Int32AsDeviceWithConfigContextRequest{Role,Site} oneOf helpers — plain int32 not accepted by constructor" + - id: SEC-01 + summary: "ai_config.json committed as template with REPLACE_WITH_OPENROUTER_KEY placeholder; ai_config.local.json added to .gitignore for real keys (T-02-01)" +metrics: + duration: "~4 minutes" + completed: "2026-04-10T05:45:16Z" + tasks_completed: 2 + files_created: 6 + files_modified: 7 +--- + +# Phase 2 Plan 01: AI Package Foundation Summary + +**One-liner:** go-openai v1.41.2 installed; AIClient interface with TierClient (BaseURL tier-routing) and MockAIClient test double; intake prompt template; Config extended with AIConfig; NetBox CreateDevice added using go-netbox v4 oneOf FK helpers. + +## What Was Built + +### `internal/ai/types.go` +Domain types decoupled from go-openai: +- `IntakeRequest` — 1-3 base64 photos + job UUID for tracing +- `IntakeResult` — structured AI output (model, manufacturer, category, specs, confidence, etc.) +- `TierConfig` — per-tier provider config (BaseURL, APIKey, Model, TimeoutSeconds) +- `AIConfig` — orchestration config (Tier1, Tier2, ConfidenceThreshold, QuickAddEnabled, QuickAddThreshold) + +### `internal/ai/client.go` +- `AIClient` interface with `AnalyzePhotos(ctx, IntakeRequest) (*IntakeResult, error)` +- `TierClient` — production implementation wrapping `go-openai`; tier-routing via `openai.DefaultConfig(key) + config.BaseURL`; `context.WithTimeout` on every call (T-02-03 DoS mitigation) +- JSON parse failure returns zero-confidence `IntakeResult` (not error) — orchestrator escalates + +### `internal/ai/mock.go` +- `MockAIClient` — records all `IntakeRequest` calls in `Calls` slice; returns configurable `FixedResult`/`FixedError` +- `HighConfidenceResult()` — Raspberry Pi 4 fixture at 0.95 confidence +- `LowConfidenceResult()` — unknown device fixture at 0.40 confidence + +### `internal/ai/prompts/intake.go` +- `BuildIntakePrompt(photoCount int)` — JSON-extraction prompt with exact schema, no markdown/fences instruction + +### `internal/config/config.go` +- `Config.AI ai.AIConfig` field added with `mapstructure:"ai"` tag +- Viper defaults for both tiers, confidence threshold, quick-add settings +- Env bindings: `HWLAB_AI_TIER1_*`, `HWLAB_AI_TIER2_*`, `HWLAB_AI_CONFIDENCE_THRESHOLD`, `HWLAB_AI_QUICK_ADD_ENABLED` +- Secondary viper instance merges `ai_config.json` as override without conflicting with primary `config.json` loading + +### `ai_config.json` +Template config file committed to repo with placeholder `REPLACE_WITH_OPENROUTER_KEY` for Tier2. + +### `internal/netbox/client.go` — CreateDevice + DeleteDevice +- `CreateDevice(ctx, name, assetTag, deviceTypeID, roleID, siteID int32) (int64, error)` — uses `Int32As*` oneOf helpers required by go-netbox v4 constructor +- `DeleteDevice(ctx, id int64) error` — for integration test cleanup + +## Test Results + +| Test | Package | Result | +|------|---------|--------| +| TestCreateDeviceValidation | internal/netbox | PASS | +| TestCreateDeviceLive | internal/netbox | SKIP (placeholder token) | +| TestMockAIClient | internal/ai | PASS | +| TestMockAIClientError | internal/ai | PASS | +| TestTierClientConstruction | internal/ai | PASS | +| TestAIConfigDefaults | internal/config | PASS | +| TestLoadDefaults | internal/config | PASS | +| TestLoadEnvOverride | internal/config | PASS | +| TestLoadNetBoxURL | internal/config | PASS | + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 1 - Bug] go-netbox v4 WritableDeviceWithConfigContextRequest constructor signature mismatch** +- **Found during:** Task 1 GREEN phase +- **Issue:** Plan used `nb.NewWritableDeviceWithConfigContextRequest(name, roleID, siteID, deviceTypeID)` (4 args with name first). Actual go-netbox v4 signature is `(deviceType DeviceBayTemplateRequestDeviceType, role DeviceWithConfigContextRequestRole, site DeviceWithConfigContextRequestSite)` — 3 args with oneOf wrapper types, no name parameter. +- **Fix:** Used `Int32AsDeviceBayTemplateRequestDeviceType`, `Int32AsDeviceWithConfigContextRequestRole`, `Int32AsDeviceWithConfigContextRequestSite` helpers; called `req.SetName(name)` separately; `SetAssetTag` takes plain `string` not `NullableString`. +- **Files modified:** internal/netbox/client.go +- **Commit:** 6040ecc + +**2. [Rule 2 - Security] ai_config.local.json added to .gitignore (T-02-01)** +- **Found during:** Task 2 — threat model review +- **Issue:** T-02-01 requires gitignoring ai_config.json for real API keys, but the plan also requires committing the template. These are contradictory for the same filename. +- **Fix:** Committed `ai_config.json` as template (placeholder key), added `ai_config.local.json` to `.gitignore` as the pattern for operator's real keys. Documented in file comment. +- **Files modified:** .gitignore +- **Commit:** 8c03780 + +## Known Stubs + +None — no UI rendering paths or data sources in this plan. All types are concrete and wired to real go-openai client or mock. + +## Threat Flags + +| Flag | File | Description | +|------|------|-------------| +| threat_flag: information_disclosure | internal/ai/client.go | TierClient logs no API keys (T-02-04 accepted) — verified: no log.Printf of TierConfig.APIKey | + +## Self-Check + +Files created: +- internal/ai/types.go: FOUND +- internal/ai/client.go: FOUND +- internal/ai/mock.go: FOUND +- internal/ai/client_test.go: FOUND +- internal/ai/prompts/intake.go: FOUND +- ai_config.json: FOUND + +Commits: +- 6040ecc: Task 1 (go-openai install + CreateDevice) +- 8c03780: Task 2 (AI package + config extension) + +`go build ./...`: PASS +`go test ./internal/ai/...`: PASS (3/3) +`go test ./internal/config/...`: PASS (4/4) +`go test ./internal/netbox/... -run TestCreateDevice`: PASS (1 pass, 1 skip) +`grep sashabaranov/go-openai go.mod`: FOUND v1.41.2 +`ls internal/ai/`: types.go client.go mock.go client_test.go prompts/ +`ls internal/ai/prompts/`: intake.go +`ls ai_config.json`: FOUND + +## Self-Check: PASSED