Skip to content

Commit 090c3f9

Browse files
atulmguptaCopilot
andcommitted
refactor(R2d.142): carve internal/api/ainldash subpackage
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent edf40c1 commit 090c3f9

4 files changed

Lines changed: 102 additions & 70 deletions

File tree

internal/api/ainldash/doc.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Package ainldash provides the natural-language dashboard composer HTTP handler.
2+
package ainldash
3+
4+
// Layer: handler

internal/api/ai_nl_dashboard_composer_handler.go renamed to internal/api/ainldash/handler.go

Lines changed: 57 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package api
1+
package ainldash
22

33
// Phase-50 / 0059 — PU3 Natural-language dashboard composer.
44
//
@@ -72,6 +72,7 @@ package api
7272
// slice.
7373

7474
import (
75+
"bytes"
7576
"context"
7677
"encoding/json"
7778
"errors"
@@ -90,6 +91,7 @@ import (
9091
"github.com/ev-dev-labs/teslasync/internal/ai/stream"
9192
"github.com/ev-dev-labs/teslasync/internal/ai/tools"
9293
"github.com/ev-dev-labs/teslasync/internal/ai/tools/nlq"
94+
apihttpx "github.com/ev-dev-labs/teslasync/internal/api/httpx"
9395
tsauth "github.com/ev-dev-labs/teslasync/internal/auth"
9496
)
9597

@@ -111,27 +113,27 @@ const aiNLDashboardComposerMaxBodyBytes = 16 * 1024
111113
// message out of the window.
112114
const aiNLDashboardComposerMaxPromptChars = 1200
113115

114-
// AINLDashboardComposerCatalogSource is the narrow read
116+
// CatalogSource is the narrow read
115117
// interface the handler consumes to load the curated install-
116118
// wide dashboard panel catalog. Production wiring satisfies it
117-
// via AINLDashboardComposerCatalogSourceImpl, which returns a
119+
// via CatalogSourceImpl, which returns a
118120
// hardcoded whitelist of pre-validated panel templates so the
119121
// AI can never name a panel outside the curated set.
120122
//
121123
// The interface is intentionally narrow (one method) so test
122124
// fakes stay small and the production implementation cannot
123125
// accidentally widen the surface.
124-
type AINLDashboardComposerCatalogSource interface {
126+
type CatalogSource interface {
125127
// DashboardComposerCatalog returns the curated install-wide
126128
// catalog at the time of the call. The returned slice MUST
127129
// be safe for the caller to retain.
128-
DashboardComposerCatalog(ctx context.Context) ([]AINLDashboardComposerPanelEntry, error)
130+
DashboardComposerCatalog(ctx context.Context) ([]PanelEntry, error)
129131
}
130132

131-
// AINLDashboardComposerPanelEntry describes one curated
133+
// PanelEntry describes one curated
132134
// dashboard panel the LLM is allowed to use as a slot.
133135
// Mirrors the SPA's CuratedDashboardPanel type 1:1.
134-
type AINLDashboardComposerPanelEntry struct {
136+
type PanelEntry struct {
135137
// Name is the canonical panel slug used as the
136138
// slot.panel_name when the LLM proposes a layout. Lower-
137139
// case to match Grafana folding.
@@ -151,23 +153,23 @@ type aiNLDashboardComposerRequest struct {
151153
Prompt string `json:"prompt"`
152154
}
153155

154-
// AINLDashboardComposerHandler is the HTTP handler for
156+
// Handler is the HTTP handler for
155157
// POST /api/v1/ai/power/dashboard/draft.
156158
//
157159
// Stateless beyond its constructor inputs; safe for concurrent
158160
// use across requests. Construction is in router.go so the
159161
// dispatcher's tool registry + provider registry are wired once
160162
// at boot.
161-
type AINLDashboardComposerHandler struct {
163+
type Handler struct {
162164
registry *provider.Registry
163165
tools *tools.Registry
164166
strategy strategy.Strategy
165-
source AINLDashboardComposerCatalogSource
167+
source CatalogSource
166168
headerName string
167169
maxIters int
168170
}
169171

170-
// NewAINLDashboardComposerHandler constructs the handler. All
172+
// NewHandler constructs the handler. All
171173
// non-pointer arguments are required; the constructor panics on
172174
// a nil so the wiring bug surfaces at boot, not at first
173175
// request.
@@ -186,31 +188,31 @@ type AINLDashboardComposerHandler struct {
186188
//
187189
// process).
188190
//
189-
// source: the production AINLDashboardComposerCatalogSource
191+
// source: the production CatalogSource
190192
//
191-
// (AINLDashboardComposerCatalogSourceImpl in router.go).
193+
// (CatalogSourceImpl in router.go).
192194
//
193195
// headerName: forward-auth header name; used to extract subject
194196
//
195197
// for audit.
196-
func NewAINLDashboardComposerHandler(
198+
func NewHandler(
197199
registry *provider.Registry,
198200
toolReg *tools.Registry,
199201
strat strategy.Strategy,
200-
source AINLDashboardComposerCatalogSource,
202+
source CatalogSource,
201203
headerName string,
202-
) *AINLDashboardComposerHandler {
204+
) *Handler {
203205
switch {
204206
case registry == nil:
205-
panic("api: NewAINLDashboardComposerHandler: nil provider.Registry")
207+
panic("ainldash: NewHandler: nil provider.Registry")
206208
case toolReg == nil:
207-
panic("api: NewAINLDashboardComposerHandler: nil tools.Registry")
209+
panic("ainldash: NewHandler: nil tools.Registry")
208210
case strat == nil:
209-
panic("api: NewAINLDashboardComposerHandler: nil strategy.Strategy")
211+
panic("ainldash: NewHandler: nil strategy.Strategy")
210212
case source == nil:
211-
panic("api: NewAINLDashboardComposerHandler: nil AINLDashboardComposerCatalogSource")
213+
panic("ainldash: NewHandler: nil CatalogSource")
212214
}
213-
return &AINLDashboardComposerHandler{
215+
return &Handler{
214216
registry: registry,
215217
tools: toolReg,
216218
strategy: strat,
@@ -236,7 +238,7 @@ func parseNLDashboardComposerRequest(w http.ResponseWriter, r *http.Request) (ai
236238
writeError(w, http.StatusBadRequest, fmt.Sprintf("failed to read body: %v", readErr))
237239
return req, false
238240
}
239-
if len(bytesTrim(bodyBytes)) == 0 {
241+
if len(bytes.TrimSpace(bodyBytes)) == 0 {
240242
writeError(w, http.StatusBadRequest, "empty body")
241243
return req, false
242244
}
@@ -265,7 +267,7 @@ func parseNLDashboardComposerRequest(w http.ResponseWriter, r *http.Request) (ai
265267
// Every error path either writes a structured frame onto the
266268
// SSE stream (when the writer has been opened) or a plain JSON
267269
// 4xx/5xx (before it has).
268-
func (h *AINLDashboardComposerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
270+
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
269271
// 1) Parse + validate the request body.
270272
req, ok := parseNLDashboardComposerRequest(w, r)
271273
if !ok {
@@ -365,7 +367,7 @@ func (h *AINLDashboardComposerHandler) ServeHTTP(w http.ResponseWriter, r *http.
365367
// only the bare ground-truth metadata keeps the transcript
366368
// volume minimal AND makes the goldens stable across catalog
367369
// churn.
368-
func buildNLDashboardComposerUserMessage(prompt string, catalog []AINLDashboardComposerPanelEntry) string {
370+
func buildNLDashboardComposerUserMessage(prompt string, catalog []PanelEntry) string {
369371
var b strings.Builder
370372

371373
b.WriteString("Suggest a single typed DashboardLayoutDraft that satisfies the user's request below. ")
@@ -377,7 +379,7 @@ func buildNLDashboardComposerUserMessage(prompt string, catalog []AINLDashboardC
377379
b.WriteString("Do NOT claim the dashboard was created, applied, exported, or pushed — the user reviews the proposal in the AI side panel and clicks the Apply to editor button to copy the draft into the manual dashboard composer on /power/dashboards, then clicks Copy to clipboard to paste it into their existing Grafana dashboard editor.")
378380

379381
// Sort panels by name for deterministic prompt hashing.
380-
panels := append([]AINLDashboardComposerPanelEntry(nil), catalog...)
382+
panels := append([]PanelEntry(nil), catalog...)
381383
sort.Slice(panels, func(i, j int) bool { return panels[i].Name < panels[j].Name })
382384
if len(panels) == 0 {
383385
b.WriteString("\n\nIn-scope curated panel catalog: NONE.\n")
@@ -396,9 +398,9 @@ func buildNLDashboardComposerUserMessage(prompt string, catalog []AINLDashboardC
396398
return b.String()
397399
}
398400

399-
// Compile-time assertion: AINLDashboardComposerHandler satisfies
401+
// Compile-time assertion: Handler satisfies
400402
// http.Handler.
401-
var _ http.Handler = (*AINLDashboardComposerHandler)(nil)
403+
var _ http.Handler = (*Handler)(nil)
402404

403405
// ---------------------------------------------------------------------
404406
// Production wiring for the source + validator interfaces declared
@@ -419,7 +421,7 @@ var _ http.Handler = (*AINLDashboardComposerHandler)(nil)
419421
// generated via nl-grafana-panel (slice 0058) OR a stock starter
420422
// the install ships with; the dashboard composer just picks
421423
// among them and arranges them on the grid.
422-
var nlDashboardComposerCuratedPanels = []AINLDashboardComposerPanelEntry{
424+
var nlDashboardComposerCuratedPanels = []PanelEntry{
423425
{
424426
Name: "drives_per_day_timeseries",
425427
Description: "Timeseries panel: SUM(distance_m)/day from the drives table",
@@ -446,41 +448,41 @@ var nlDashboardComposerCuratedPanels = []AINLDashboardComposerPanelEntry{
446448
},
447449
}
448450

449-
// AINLDashboardComposerCatalogSourceImpl is the production
450-
// AINLDashboardComposerCatalogSource. It returns the hardcoded
451+
// CatalogSourceImpl is the production
452+
// CatalogSource. It returns the hardcoded
451453
// curated whitelist so the AI can never name a panel outside
452454
// the curated set.
453455
//
454456
// No DB query — the catalog is hand-maintained. A future slice
455457
// that needs per-tenant catalog gating can swap this out without
456458
// churning the handler.
457-
type AINLDashboardComposerCatalogSourceImpl struct{}
459+
type CatalogSourceImpl struct{}
458460

459-
// NewAINLDashboardComposerCatalogSource constructs the adapter.
461+
// NewCatalogSource constructs the adapter.
460462
// No deps. Returned by-pointer for symmetry with the other AI*
461463
// source types.
462-
func NewAINLDashboardComposerCatalogSource() *AINLDashboardComposerCatalogSourceImpl {
463-
return &AINLDashboardComposerCatalogSourceImpl{}
464+
func NewCatalogSource() *CatalogSourceImpl {
465+
return &CatalogSourceImpl{}
464466
}
465467

466468
// DashboardComposerCatalog implements
467-
// AINLDashboardComposerCatalogSource. Returns a defensive copy
469+
// CatalogSource. Returns a defensive copy
468470
// of the curated whitelist so a caller cannot retroactively
469471
// mutate the source-of-truth slice.
470-
func (a *AINLDashboardComposerCatalogSourceImpl) DashboardComposerCatalog(_ context.Context) ([]AINLDashboardComposerPanelEntry, error) {
471-
out := make([]AINLDashboardComposerPanelEntry, len(nlDashboardComposerCuratedPanels))
472+
func (a *CatalogSourceImpl) DashboardComposerCatalog(_ context.Context) ([]PanelEntry, error) {
473+
out := make([]PanelEntry, len(nlDashboardComposerCuratedPanels))
472474
copy(out, nlDashboardComposerCuratedPanels)
473475
return out, nil
474476
}
475477

476478
// Compile-time assertion.
477-
var _ AINLDashboardComposerCatalogSource = (*AINLDashboardComposerCatalogSourceImpl)(nil)
479+
var _ CatalogSource = (*CatalogSourceImpl)(nil)
478480

479481
// ---------------------------------------------------------------------
480482
// Production wiring for the nlq.DashboardLayoutValidator interface.
481483
// ---------------------------------------------------------------------
482484

483-
// AINLDashboardComposerValidator is the production
485+
// Validator is the production
484486
// nlq.DashboardLayoutValidator. The shape checks (panel-name
485487
// catalog, slot count, per-slot grid bounds, duplicate-panel
486488
// detector, overlap detector) are already enforced by the
@@ -501,13 +503,13 @@ var _ AINLDashboardComposerCatalogSource = (*AINLDashboardComposerCatalogSourceI
501503
// the user reviews the typed proposal before clicking Apply.
502504
//
503505
// Stateless. Held by value; safe for concurrent use.
504-
type AINLDashboardComposerValidator struct{}
506+
type Validator struct{}
505507

506-
// NewAINLDashboardComposerValidator constructs the validator.
508+
// NewValidator constructs the validator.
507509
// No deps. Returned by-pointer for symmetry with the other AI*
508510
// validator types.
509-
func NewAINLDashboardComposerValidator() *AINLDashboardComposerValidator {
510-
return &AINLDashboardComposerValidator{}
511+
func NewValidator() *Validator {
512+
return &Validator{}
511513
}
512514

513515
// ValidateDashboardLayout implements
@@ -517,12 +519,21 @@ func NewAINLDashboardComposerValidator() *AINLDashboardComposerValidator {
517519
// slices need them. Keeping the body intentionally minimal so
518520
// the slice's mandate ("propose-only, no semantic surprises")
519521
// is locally legible.
520-
func (v *AINLDashboardComposerValidator) ValidateDashboardLayout(draft *nlq.DashboardLayoutDraft) error {
522+
func (v *Validator) ValidateDashboardLayout(draft *nlq.DashboardLayoutDraft) error {
521523
if draft == nil {
522-
return errors.New("api ai nl-dashboard-composer: nil DashboardLayoutDraft")
524+
return errors.New("ainldash: nil DashboardLayoutDraft")
523525
}
524526
return nil
525527
}
526528

527529
// Compile-time assertion.
528-
var _ nlq.DashboardLayoutValidator = (*AINLDashboardComposerValidator)(nil)
530+
var _ nlq.DashboardLayoutValidator = (*Validator)(nil)
531+
532+
func writeError(w http.ResponseWriter, status int, msg string) {
533+
apihttpx.WriteError(w, status, msg)
534+
}
535+
536+
// denyAllConfirm rejects every mutating tool as defence-in-depth.
537+
func denyAllConfirm(_ context.Context, _ dispatch.ConfirmRequest) (dispatch.ConfirmDecision, error) {
538+
return dispatch.ConfirmDenied, nil
539+
}

0 commit comments

Comments
 (0)