Skip to content

Commit 1bf876c

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

4 files changed

Lines changed: 95 additions & 62 deletions

File tree

internal/api/aiinboxcat/doc.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Package aiinboxcat serves the LLM-backed inbox auto-categorization endpoint.
2+
package aiinboxcat
3+
4+
// Layer: handler

internal/api/ai_inbox_categorization_handler.go renamed to internal/api/aiinboxcat/handler.go

Lines changed: 45 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package api
1+
package aiinboxcat
22

33
// Phase-50 / 0035 — A2 Inbox auto-categorization.
44
//
@@ -65,40 +65,49 @@ import (
6565
"github.com/ev-dev-labs/teslasync/internal/ai/stream"
6666
"github.com/ev-dev-labs/teslasync/internal/ai/tools"
6767
"github.com/ev-dev-labs/teslasync/internal/ai/tools/nl"
68+
"github.com/ev-dev-labs/teslasync/internal/api/httpx"
6869
tsauth "github.com/ev-dev-labs/teslasync/internal/auth"
6970
dbalert "github.com/ev-dev-labs/teslasync/internal/database/alert"
7071
dbnotif "github.com/ev-dev-labs/teslasync/internal/database/notification"
7172
)
7273

73-
// aiInboxCategorizationMaxIterations bounds the dispatcher's
74+
// maxIterations bounds the dispatcher's
7475
// tool-loop. The strategy is at most draft_alert_categories ->
7576
// validate_alert_category(per label) -> answer (with optional
7677
// retries). A hard ceiling of 8 is generous and matches the
7778
// other A-tier propose-only handlers.
78-
const aiInboxCategorizationMaxIterations = 8
79+
const maxIterations = 8
7980

80-
// aiInboxCategorizationDefaultWindowDays is the trailing-window
81+
// defaultWindowDays is the trailing-window
8182
// length the production InboxCategorizationSource adapter
8283
// projects across when reading the recent notification_logs.
8384
// 7 mirrors the canonical "recent" window used elsewhere in
8485
// Phase-50.
85-
const aiInboxCategorizationDefaultWindowDays = 7
86+
const defaultWindowDays = 7
8687

87-
// aiInboxCategorizationMinEvents is the minimum total
88+
// minEvents is the minimum total
8889
// notification_logs row count (across the trailing window) the
8990
// adapter requires before it lets the narrator quote per-
9091
// category counts. Below this threshold has_enough_history flips
9192
// false and the narrator says so plainly. 10 is the minimum
9293
// sample for a meaningful descriptive tally — fewer notifications
9394
// makes any per-category breakdown statistically meaningless.
94-
const aiInboxCategorizationMinEvents = 10
95+
const minEvents = 10
9596

96-
// aiInboxCategorizationRequest is the JSON body shape this
97+
func writeError(w http.ResponseWriter, status int, msg string) {
98+
httpx.WriteError(w, status, msg)
99+
}
100+
101+
func denyAllConfirm(_ context.Context, _ dispatch.ConfirmRequest) (dispatch.ConfirmDecision, error) {
102+
return dispatch.ConfirmDenied, nil
103+
}
104+
105+
// request is the JSON body shape this
97106
// handler accepts. Body is OPTIONAL (an empty body is accepted;
98107
// the handler defaults to the entire inbox over the last
99-
// aiInboxCategorizationDefaultWindowDays days). Mirrors how the
108+
// defaultWindowDays days). Mirrors how the
100109
// canonical NotificationFilterBar URL params already work.
101-
type aiInboxCategorizationRequest struct {
110+
type request struct {
102111
// VehicleID restricts the recent window to a single
103112
// vehicle. Optional — when absent the handler returns
104113
// the per-category counts across every vehicle the
@@ -109,7 +118,7 @@ type aiInboxCategorizationRequest struct {
109118
VehicleID *int64 `json:"vehicle_id,omitempty"`
110119

111120
// WindowDays is the lookback in days. Defaults to
112-
// aiInboxCategorizationDefaultWindowDays when nil.
121+
// defaultWindowDays when nil.
113122
// Capped at 90 by the tool's input validator so a
114123
// runaway request cannot scan an unbounded range.
115124
WindowDays *int `json:"window_days,omitempty"`
@@ -124,22 +133,22 @@ type aiInboxCategorizationRequest struct {
124133
RuleIDs []int64 `json:"rule_ids,omitempty"`
125134
}
126135

127-
// AIInboxCategorizationHandler is the HTTP handler for
136+
// Handler is the HTTP handler for
128137
// POST /api/v1/ai/alerts/inbox/categorize.
129138
//
130139
// Stateless beyond its constructor inputs; safe for concurrent
131140
// use across requests. Construction is in router.go so the
132141
// dispatcher's tool registry + provider registry are wired once
133142
// at boot.
134-
type AIInboxCategorizationHandler struct {
143+
type Handler struct {
135144
registry *provider.Registry
136145
tools *tools.Registry
137146
strategy strategy.Strategy
138147
headerName string
139148
maxIters int
140149
}
141150

142-
// NewAIInboxCategorizationHandler constructs the handler. All
151+
// NewHandler constructs the handler. All
143152
// non-pointer arguments are required; the constructor panics on
144153
// a nil so the wiring bug surfaces at boot, not at first
145154
// request.
@@ -155,26 +164,26 @@ type AIInboxCategorizationHandler struct {
155164
// headerName: forward-auth header name; used to extract subject
156165
//
157166
// for audit.
158-
func NewAIInboxCategorizationHandler(
167+
func NewHandler(
159168
registry *provider.Registry,
160169
toolReg *tools.Registry,
161170
strat strategy.Strategy,
162171
headerName string,
163-
) *AIInboxCategorizationHandler {
172+
) *Handler {
164173
switch {
165174
case registry == nil:
166-
panic("api: NewAIInboxCategorizationHandler: nil provider.Registry")
175+
panic("aiinboxcat: NewHandler: nil provider.Registry")
167176
case toolReg == nil:
168-
panic("api: NewAIInboxCategorizationHandler: nil tools.Registry")
177+
panic("aiinboxcat: NewHandler: nil tools.Registry")
169178
case strat == nil:
170-
panic("api: NewAIInboxCategorizationHandler: nil strategy.Strategy")
179+
panic("aiinboxcat: NewHandler: nil strategy.Strategy")
171180
}
172-
return &AIInboxCategorizationHandler{
181+
return &Handler{
173182
registry: registry,
174183
tools: toolReg,
175184
strategy: strat,
176185
headerName: headerName,
177-
maxIters: aiInboxCategorizationMaxIterations,
186+
maxIters: maxIterations,
178187
}
179188
}
180189

@@ -187,8 +196,8 @@ func NewAIInboxCategorizationHandler(
187196
// Severity values are validated against the canonical {info,
188197
// warn, critical} set; vehicle_id and rule_id values must be
189198
// positive when provided; window_days must be in [1, 90].
190-
func parseInboxCategorizationBody(w http.ResponseWriter, r *http.Request) (*aiInboxCategorizationRequest, bool) {
191-
req := &aiInboxCategorizationRequest{}
199+
func parseInboxCategorizationBody(w http.ResponseWriter, r *http.Request) (*request, bool) {
200+
req := &request{}
192201
if r.Body == nil {
193202
return req, true
194203
}
@@ -237,7 +246,7 @@ func parseInboxCategorizationBody(w http.ResponseWriter, r *http.Request) (*aiIn
237246
// path either writes a structured frame onto the SSE stream
238247
// (when the writer has been opened) or a plain JSON 4xx/5xx
239248
// (before it has).
240-
func (h *AIInboxCategorizationHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
249+
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
241250
// 1) Parse + validate body. A malformed body fails fast
242251
// with a JSON 400 before any provider lookup.
243252
body, ok := parseInboxCategorizationBody(w, r)
@@ -301,7 +310,7 @@ func (h *AIInboxCategorizationHandler) ServeHTTP(w http.ResponseWriter, r *http.
301310
// body's optional vehicle_id is mentioned only as a hint
302311
// — the typed envelope returned by draft_alert_categories
303312
// is the source of truth.
304-
windowDays := aiInboxCategorizationDefaultWindowDays
313+
windowDays := defaultWindowDays
305314
if body.WindowDays != nil {
306315
windowDays = *body.WindowDays
307316
}
@@ -348,9 +357,9 @@ func (h *AIInboxCategorizationHandler) ServeHTTP(w http.ResponseWriter, r *http.
348357
}
349358
}
350359

351-
// Compile-time assertion: AIInboxCategorizationHandler satisfies
360+
// Compile-time assertion: Handler satisfies
352361
// http.Handler.
353-
var _ http.Handler = (*AIInboxCategorizationHandler)(nil)
362+
var _ http.Handler = (*Handler)(nil)
354363

355364
// ---------------------------------------------------------------------
356365
// Production wiring for the InboxCategorizationSource port declared by
@@ -359,7 +368,7 @@ var _ http.Handler = (*AIInboxCategorizationHandler)(nil)
359368
// the AIAlertTuningSource pattern from slice 0034.
360369
// ---------------------------------------------------------------------
361370

362-
// AIInboxCategorizationSource is the production
371+
// Source is the production
363372
// nl.InboxCategorizationSource. It composes the canonical
364373
// NotificationRepo (read) + AlertRuleRepo (read) so the AI
365374
// projection is grounded in the SAME notification_logs +
@@ -368,22 +377,22 @@ var _ http.Handler = (*AIInboxCategorizationHandler)(nil)
368377
//
369378
// The struct holds two narrow read interfaces; the constructor
370379
// panics on a nil so a wiring bug surfaces at boot.
371-
type AIInboxCategorizationSource struct {
380+
type Source struct {
372381
notifications *dbnotif.NotificationRepo
373382
rules *dbalert.AlertRuleRepo
374383
}
375384

376-
// NewAIInboxCategorizationSource constructs the adapter. Panics
385+
// NewSource constructs the adapter. Panics
377386
// on a nil repo so a wiring mistake surfaces at boot rather
378387
// than as a nil-deref on first AI request.
379-
func NewAIInboxCategorizationSource(notifications *dbnotif.NotificationRepo, rules *dbalert.AlertRuleRepo) *AIInboxCategorizationSource {
388+
func NewSource(notifications *dbnotif.NotificationRepo, rules *dbalert.AlertRuleRepo) *Source {
380389
if notifications == nil {
381-
panic("api: NewAIInboxCategorizationSource: nil *dbnotif.NotificationRepo")
390+
panic("aiinboxcat: NewSource: nil *dbnotif.NotificationRepo")
382391
}
383392
if rules == nil {
384-
panic("api: NewAIInboxCategorizationSource: nil *dbalert.AlertRuleRepo")
393+
panic("aiinboxcat: NewSource: nil *dbalert.AlertRuleRepo")
385394
}
386-
return &AIInboxCategorizationSource{notifications: notifications, rules: rules}
395+
return &Source{notifications: notifications, rules: rules}
387396
}
388397

389398
// LoadCategoryCounts implements
@@ -400,7 +409,7 @@ func NewAIInboxCategorizationSource(notifications *dbnotif.NotificationRepo, rul
400409
// CategoryCount.Count across the returned slice (NOT the raw
401410
// notification_logs row count — rows whose alert_id is missing
402411
// from the rules lookup bucket into "other").
403-
func (a *AIInboxCategorizationSource) LoadCategoryCounts(ctx context.Context, f dbnotif.NotificationLogFilters) ([]nl.CategoryCount, int, int, error) {
412+
func (a *Source) LoadCategoryCounts(ctx context.Context, f dbnotif.NotificationLogFilters) ([]nl.CategoryCount, int, int, error) {
404413
// Defence in depth: clamp the limit so a runaway caller
405414
// cannot blow past the canonical 1000-row cap. The tool
406415
// already sets Limit=1000; this is belt-and-suspenders.
@@ -463,5 +472,5 @@ func (a *AIInboxCategorizationSource) LoadCategoryCounts(ctx context.Context, f
463472
total += c.Count
464473
}
465474

466-
return counts, total, aiInboxCategorizationMinEvents, nil
475+
return counts, total, minEvents, nil
467476
}

0 commit comments

Comments
 (0)