1- package api
1+ package aiautomation
22
33// Phase-50 / 0016 — N2 Natural-language automation builder.
44//
5- // ai_automation_handler .go implements the LLM-backed handler at
5+ // handler .go implements the LLM-backed handler at
66// POST /api/v1/ai/automations/draft. The flow mirrors the
77// nl-alert-builder handler from slice 0015 — same dispatch+stream
88// loop, same propose-only contract, no persistence (one-shot
@@ -53,6 +53,7 @@ package api
5353
5454import (
5555 "bytes"
56+ "context"
5657 "encoding/json"
5758 "fmt"
5859 "net/http"
@@ -67,39 +68,40 @@ import (
6768 "github.com/ev-dev-labs/teslasync/internal/ai/stream"
6869 "github.com/ev-dev-labs/teslasync/internal/ai/tools"
6970 apiautomation "github.com/ev-dev-labs/teslasync/internal/api/automation"
71+ apihttpx "github.com/ev-dev-labs/teslasync/internal/api/httpx"
7072 tsauth "github.com/ev-dev-labs/teslasync/internal/auth"
7173)
7274
73- // aiAutomationBuilderMaxIterations bounds the dispatcher's tool-loop.
75+ // builderMaxIterations bounds the dispatcher's tool-loop.
7476// The nl-automation-builder strategy is a two-tool sequence (draft,
7577// then validate), with at most one retry if validate rejects the
7678// first draft — a hard ceiling of 6 is generous for an LLM that
7779// occasionally re-drafts twice before settling. Mirrors
7880// aiAlertBuilderMaxIterations from slice 0015.
79- const aiAutomationBuilderMaxIterations = 6
81+ const builderMaxIterations = 6
8082
81- // aiAutomationBuilderMaxPromptChars bounds the user-supplied
83+ // builderMaxPromptChars bounds the user-supplied
8284// natural-language prompt at the HTTP boundary. Generous for a
8385// multi-sentence rule description; defensive against an enormous
8486// payload that would inflate the LLM's context window cost without
8587// any plausible legitimate use.
86- const aiAutomationBuilderMaxPromptChars = 4096
88+ const builderMaxPromptChars = 4096
8789
88- // AIAutomationHandler is the HTTP handler for
90+ // Handler is the HTTP handler for
8991// POST /api/v1/ai/automations/draft.
9092//
9193// Stateless beyond its constructor inputs; safe for concurrent use
9294// across requests. Construction is in router.go so the dispatcher's
9395// tool registry + provider registry are wired once at boot.
94- type AIAutomationHandler struct {
96+ type Handler struct {
9597 registry * provider.Registry
9698 tools * tools.Registry
9799 strategy strategy.Strategy
98100 headerName string
99101 maxIters int
100102}
101103
102- // NewAIAutomationHandler constructs the handler. All non-pointer
104+ // NewHandler constructs the handler. All non-pointer
103105// arguments are required; the constructor panics on a nil so the
104106// wiring bug surfaces at boot, not at first request.
105107//
@@ -111,37 +113,37 @@ type AIAutomationHandler struct {
111113//
112114// strat: the nl-automation-builder Strategy (one per process).
113115// headerName: forward-auth header name; used to extract subject for audit.
114- func NewAIAutomationHandler (
116+ func NewHandler (
115117 registry * provider.Registry ,
116118 toolReg * tools.Registry ,
117119 strat strategy.Strategy ,
118120 headerName string ,
119- ) * AIAutomationHandler {
121+ ) * Handler {
120122 switch {
121123 case registry == nil :
122- panic ("api: NewAIAutomationHandler : nil provider.Registry" )
124+ panic ("aiautomation: NewHandler : nil provider.Registry" )
123125 case toolReg == nil :
124- panic ("api: NewAIAutomationHandler : nil tools.Registry" )
126+ panic ("aiautomation: NewHandler : nil tools.Registry" )
125127 case strat == nil :
126- panic ("api: NewAIAutomationHandler : nil strategy.Strategy" )
128+ panic ("aiautomation: NewHandler : nil strategy.Strategy" )
127129 }
128- return & AIAutomationHandler {
130+ return & Handler {
129131 registry : registry ,
130132 tools : toolReg ,
131133 strategy : strat ,
132134 headerName : headerName ,
133- maxIters : aiAutomationBuilderMaxIterations ,
135+ maxIters : builderMaxIterations ,
134136 }
135137}
136138
137- // aiAutomationBuilderRequest is the wire shape for
139+ // builderRequest is the wire shape for
138140// POST /api/v1/ai/automations/draft.
139141//
140142// VehicleID is required and must be > 0 — the AI handler scopes the
141143// drafting to a single vehicle. Prompt is the user's plain-language
142144// description of the automation they want, capped at
143- // aiAutomationBuilderMaxPromptChars .
144- type aiAutomationBuilderRequest struct {
145+ // builderMaxPromptChars .
146+ type builderRequest struct {
145147 VehicleID int64 `json:"vehicle_id"`
146148 Prompt string `json:"prompt"`
147149}
@@ -151,9 +153,9 @@ type aiAutomationBuilderRequest struct {
151153// dispatcher's deferred WriteDone. Every error path either writes a
152154// structured frame onto the SSE stream (when the writer has been
153155// opened) or a plain JSON 4xx/5xx (before it has).
154- func (h * AIAutomationHandler ) ServeHTTP (w http.ResponseWriter , r * http.Request ) {
156+ func (h * Handler ) ServeHTTP (w http.ResponseWriter , r * http.Request ) {
155157 // 1) Decode + validate request body.
156- var body aiAutomationBuilderRequest
158+ var body builderRequest
157159 if err := json .NewDecoder (r .Body ).Decode (& body ); err != nil {
158160 writeError (w , http .StatusBadRequest , "invalid request body" )
159161 return
@@ -167,8 +169,8 @@ func (h *AIAutomationHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
167169 writeError (w , http .StatusBadRequest , "prompt is required" )
168170 return
169171 }
170- if len (prompt ) > aiAutomationBuilderMaxPromptChars {
171- writeError (w , http .StatusBadRequest , fmt .Sprintf ("prompt must be at most %d characters" , aiAutomationBuilderMaxPromptChars ))
172+ if len (prompt ) > builderMaxPromptChars {
173+ writeError (w , http .StatusBadRequest , fmt .Sprintf ("prompt must be at most %d characters" , builderMaxPromptChars ))
172174 return
173175 }
174176
@@ -255,31 +257,40 @@ func (h *AIAutomationHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
255257 }
256258}
257259
258- // Compile-time assertion: AIAutomationHandler satisfies http.Handler.
259- var _ http.Handler = (* AIAutomationHandler )(nil )
260+ // Compile-time assertion: Handler satisfies http.Handler.
261+ var _ http.Handler = (* Handler )(nil )
260262
261- // AIAutomationGraphValidator is the production implementation of
263+ func writeError (w http.ResponseWriter , status int , msg string ) {
264+ apihttpx .WriteError (w , status , msg )
265+ }
266+
267+ // denyAllConfirm rejects every mutating tool as defence-in-depth.
268+ func denyAllConfirm (_ context.Context , _ dispatch.ConfirmRequest ) (dispatch.ConfirmDecision , error ) {
269+ return dispatch .ConfirmDenied , nil
270+ }
271+
272+ // GraphValidator is the production implementation of
262273// automationtool.AutomationGraphValidator. It is a thin wrapper around the
263274// automation subpackage's canonical typed payload validator so the AI tool
264275// registration path can wire the same validation path used by the manual
265276// save endpoint.
266277//
267278// One per process; stateless.
268- type AIAutomationGraphValidator struct {}
279+ type GraphValidator struct {}
269280
270- // NewAIAutomationGraphValidator returns a ready-to-use validator
281+ // NewGraphValidator returns a ready-to-use validator
271282// wrapper. Exists as a constructor (rather than a value) so a future
272283// change that adds wiring (e.g. a custom signal allowlist) does not
273284// force every call site to update.
274- func NewAIAutomationGraphValidator () * AIAutomationGraphValidator {
275- return & AIAutomationGraphValidator {}
285+ func NewGraphValidator () * GraphValidator {
286+ return & GraphValidator {}
276287}
277288
278289// ValidateAutomationWire implements [automationtool.AutomationGraphValidator].
279290// Delegates to the canonical automation input validator — same code path the
280291// POST /api/v1/automations handler runs, so a draft accepted here is
281292// byte-equivalent to a draft accepted by the canonical handler.
282- func (v * AIAutomationGraphValidator ) ValidateAutomationWire (wireJSON json.RawMessage ) error {
293+ func (v * GraphValidator ) ValidateAutomationWire (wireJSON json.RawMessage ) error {
283294 if len (wireJSON ) == 0 {
284295 return fmt .Errorf ("empty wire payload" )
285296 }
0 commit comments