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