homelabby/.planning/phases/07-research-agent-search/07-02-SUMMARY.md
Mikkel Georgsen 712a0a39b8 docs(07-02): complete natural language search plan summary
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 07:56:51 +00:00

5.2 KiB

phase plan subsystem tags dependency_graph tech_stack key_files decisions metrics
07-research-agent-search 02 search
search
nlp
ai
frontend
go
react
requires provides affects
07-01
search-endpoint
nl-search-ui
DashboardPage
FilterBar
api-router
added patterns
NL query sanitize → LLM extraction → in-memory filter (no raw text to NetBox)
TDD
failing tests written first, handler implemented to green
Debounced NL input (400ms) with TanStack Query (enabled guard on length > 2)
created modified
internal/api/handlers/search.go
internal/api/handlers/search_test.go
internal/api/router.go
cmd/hwlab/main.go
web/src/lib/api.ts
web/src/components/inventory/FilterBar.tsx
web/src/pages/DashboardPage.tsx
Used SearchNetBoxClient and SearchAIClient narrow interfaces in search.go for testability without importing ai package in tests
extractJSON helper strips markdown code fences from LLM response before JSON parse
NL search row placed below existing filter row (additive, not replacing) to keep local filter intact
displayLoading/displayItems derived state avoids duplicating isLoading/searchLoading logic
duration completed tasks_completed files_changed
~12 minutes 2026-04-10 2 7

Phase 07 Plan 02: Natural Language Search Summary

Natural language inventory search via Tier 1 LLM (Gemma 4): user query sanitized, translated to structured filter params (catalog_status + name_contains), applied in-memory against ListDevices(200), returned as InventoryItem JSON; dashboard gains a debounced NL search input with volt accent styling and fallback to substring match on LLM parse failure.

Tasks Completed

Task Name Commit Files
1 SearchHandler — NL query to NetBox filter 9db7707 search.go, search_test.go, router.go, main.go
2 Frontend NL search bar + api.ts wiring 7db093c api.ts, FilterBar.tsx, DashboardPage.tsx

What Was Built

Backend (Task 1)

internal/api/handlers/search.go provides SearchHandler with SearchDevices(w, r):

  1. Validates q param — returns 400 if empty
  2. Sanitizes query: strips non-printable chars, truncates to 200 runes (T-07-05)
  3. Calls tier1.TextComplete with an extraction prompt requesting {"catalog_status","name_contains","tag"} JSON
  4. Calls extractJSON to strip markdown fences before parse (LLM sometimes wraps in code blocks)
  5. On parse failure: logs warning, falls back to NameContains = rawQuery (never 500)
  6. tag field logged as not implemented and ignored (MVP)
  7. Fetches ListDevices(ctx, 200), applies in-memory filter, encodes result

Router wired: GET /api/search with nil-guard 503 fallback. main.go constructs handlers.NewSearchHandler(nbClient, tier1) and passes to NewRouter.

TDD: 4 tests written before implementation — TestSearch_EmptyQ, TestSearch_LLMParseFallback, TestSearch_CatalogStatusFilter, TestSearch_NameContainsAndStatus — all pass.

Frontend (Task 2)

web/src/lib/api.ts: fetchSearch(q) added — calls GET /api/search?q=<encoded>.

FilterBar.tsx restructured to two rows:

  • Row 1: existing local text search + status select + item count + view toggle (unchanged)
  • Row 2: NL input with Sparkles icon (volt/60 tint), border-volt/20focus:border-volt, Loader2 spinner when nlSearchLoading
  • 400ms debounce via useEffect + setTimeout pattern; local nlInputValue state, propagates via onNlQueryChange

DashboardPage.tsx:

  • nlQuery state + useQuery({ queryKey: ['search', nlQuery], enabled: nlQuery.trim().length > 2 })
  • displayItems = nlQuery > 2 ? searchResults : filtered — local filter fully preserved when NL empty
  • Loading/empty state messages adapt to NL vs local mode

Threat Mitigations Applied

Threat Mitigation
T-07-05 sanitizeQuery: non-printable chars stripped, truncated to 200 runes
T-07-06 Only catalog_status, name_contains, tag extracted; unknown keys ignored; fallback on parse failure

Deviations from Plan

Auto-added: extractJSON helper

Found during: Task 1 implementation Issue: LLMs commonly wrap JSON responses in markdown code fences (```json ... ```), which breaks json.Unmarshal directly Fix: Added extractJSON(s string) string that finds first { and last } to extract the JSON object before parsing Files modified: internal/api/handlers/search.go Rule: Rule 2 — missing critical functionality (robustness of LLM output parsing)

No other deviations — plan executed as specified.

Known Stubs

None — all data paths are wired end-to-end.

Threat Flags

None — no new network endpoints or trust boundaries beyond what the plan specified.

Self-Check: PASSED

  • internal/api/handlers/search.go: exists
  • internal/api/handlers/search_test.go: exists, 4 tests pass
  • internal/api/router.go: GET /api/search wired
  • web/src/lib/api.ts: fetchSearch exported
  • web/src/pages/DashboardPage.tsx: nlQuery state + useQuery present
  • web/src/components/inventory/FilterBar.tsx: NL input with debounce present
  • go build ./...: clean
  • npm run build: clean (built in 3.14s)
  • Commits 9db7707 and 7db093c: verified in git log