Skip to content

Commit f7b62de

Browse files
atulmguptaCopilot
andcommitted
refactor(R2d.147): carve internal/api/ailogtrace subpackage
Move the log-trace summarization handler and tests into the ailogtrace package. Rename the exported handler surface to Handler/NewHandler and wire router.go through ailogtrace while keeping aiLogTraceSummarizationHandler and AIHandlers.LogTraceSummarization unchanged. Add package documentation so arch doc.go coverage remains green. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent edf40c1 commit f7b62de

4 files changed

Lines changed: 124 additions & 71 deletions

File tree

internal/api/ailogtrace/doc.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Package ailogtrace hosts the log and trace summarization AI handler.
2+
//
3+
// # Layer
4+
//
5+
// Layer: handler
6+
//
7+
// # Why a subpackage
8+
//
9+
// Carved in Phase R2d.147 from the flat internal/api parent to isolate the
10+
// log-trace-summarization HTTP surface. The package depends only on AI
11+
// orchestration primitives and shared API infrastructure; it MUST NOT import its
12+
// parent api package.
13+
//
14+
// # Scope
15+
//
16+
// In-scope (lives here):
17+
// - Handler for POST /api/v1/ai/system/logs/summarize.
18+
// - NewHandler constructor used by router wiring.
19+
// - TraceWindowSource adapter used by summary.RegisterLogTraceSummarizerTools.
20+
//
21+
// Out-of-scope (remains in parent api): AIHandlers aggregation and route
22+
// registration in ai_routes.go.
23+
package ailogtrace

internal/api/ai_log_trace_summarization_handler.go renamed to internal/api/ailogtrace/handler.go

Lines changed: 72 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package api
1+
package ailogtrace
22

33
// Phase-50 / 0045 — S4 Log and trace summarization.
44
//
@@ -89,35 +89,36 @@ import (
8989
"github.com/ev-dev-labs/teslasync/internal/ai/stream"
9090
"github.com/ev-dev-labs/teslasync/internal/ai/tools"
9191
"github.com/ev-dev-labs/teslasync/internal/ai/tools/summary"
92+
"github.com/ev-dev-labs/teslasync/internal/api/httpx"
9293
tsauth "github.com/ev-dev-labs/teslasync/internal/auth"
9394
)
9495

95-
// aiLogTraceSummarizationMaxIterations bounds the dispatcher's
96+
// maxIterations bounds the dispatcher's
9697
// tool-loop. The strategy is at most query_trace_window →
9798
// (optional) retrieve_log_chunks → answer (with optional retries
9899
// on transient tool error). A hard ceiling of 8 is generous,
99100
// matching the other narrator handlers.
100-
const aiLogTraceSummarizationMaxIterations = 8
101+
const maxIterations = 8
101102

102-
// aiLogTraceSummarizationMaxBodyBytes caps the request body. The
103+
// maxBodyBytes caps the request body. The
103104
// body is small (3 numeric fields); bound it cheaply. 16 KiB
104105
// matches the other body-driven AI handlers.
105-
const aiLogTraceSummarizationMaxBodyBytes = 16 * 1024
106+
const maxBodyBytes = 16 * 1024
106107

107-
// aiLogTraceSummarizationMaxWindowSeconds caps the window the
108+
// maxWindowSeconds caps the window the
108109
// caller may request. 24 hours is generous for an operator log-
109110
// triage workflow and bounds the size of the envelope the source
110111
// has to compute.
111-
const aiLogTraceSummarizationMaxWindowSeconds = 24 * 60 * 60
112+
const maxWindowSeconds = 24 * 60 * 60
112113

113-
// aiLogTraceSummarizationMaxFromUnix is a sanity upper bound on
114+
// maxFromUnix is a sanity upper bound on
114115
// from_unix to reject obvious garbage (e.g. epoch year 9999). Set
115116
// to year 2100 in Unix seconds.
116-
const aiLogTraceSummarizationMaxFromUnix = int64(4102444800)
117+
const maxFromUnix = int64(4102444800)
117118

118-
// aiLogTraceSummarizationRequest is the typed body shape. Only
119+
// summarizationRequest is the typed body shape. Only
119120
// from_unix / to_unix are required; vehicle_id is optional.
120-
type aiLogTraceSummarizationRequest struct {
121+
type summarizationRequest struct {
121122
// FromUnix is the inclusive start of the window in Unix
122123
// seconds. Required + positive.
123124
FromUnix int64 `json:"from_unix"`
@@ -133,13 +134,13 @@ type aiLogTraceSummarizationRequest struct {
133134
VehicleID int64 `json:"vehicle_id,omitempty"`
134135
}
135136

136-
// AILogTraceSummarizationHandler is the HTTP handler for
137+
// Handler is the HTTP handler for
137138
// POST /api/v1/ai/system/logs/summarize.
138139
//
139140
// Stateless beyond its constructor inputs; safe for concurrent use
140141
// across requests. Construction is in router.go so the dispatcher's
141142
// tool registry + provider registry are wired once at boot.
142-
type AILogTraceSummarizationHandler struct {
143+
type Handler struct {
143144
registry *provider.Registry
144145
tools *tools.Registry
145146
strategy strategy.Strategy
@@ -148,7 +149,7 @@ type AILogTraceSummarizationHandler struct {
148149
maxIters int
149150
}
150151

151-
// NewAILogTraceSummarizationHandler constructs the handler. All
152+
// NewHandler constructs the handler. All
152153
// non-pointer arguments are required; the constructor panics on a
153154
// nil so the wiring bug surfaces at boot, not at first request.
154155
//
@@ -168,98 +169,108 @@ type AILogTraceSummarizationHandler struct {
168169
//
169170
// source: the production summary.TraceWindowSource (currently
170171
//
171-
// AILogTraceWindowSource — a deterministic empty
172+
// TraceWindowSource — a deterministic empty
172173
// adapter; the operator-facing log surface is
173174
// stream-only and has no historical reader yet).
174175
//
175176
// headerName: forward-auth header name; used to extract subject
176177
//
177178
// for audit.
178-
func NewAILogTraceSummarizationHandler(
179+
func NewHandler(
179180
registry *provider.Registry,
180181
toolReg *tools.Registry,
181182
strat strategy.Strategy,
182183
source summary.TraceWindowSource,
183184
headerName string,
184-
) *AILogTraceSummarizationHandler {
185+
) *Handler {
185186
switch {
186187
case registry == nil:
187-
panic("api: NewAILogTraceSummarizationHandler: nil provider.Registry")
188+
panic("ailogtrace: NewHandler: nil provider.Registry")
188189
case toolReg == nil:
189-
panic("api: NewAILogTraceSummarizationHandler: nil tools.Registry")
190+
panic("ailogtrace: NewHandler: nil tools.Registry")
190191
case strat == nil:
191-
panic("api: NewAILogTraceSummarizationHandler: nil strategy.Strategy")
192+
panic("ailogtrace: NewHandler: nil strategy.Strategy")
192193
case source == nil:
193-
panic("api: NewAILogTraceSummarizationHandler: nil summary.TraceWindowSource")
194+
panic("ailogtrace: NewHandler: nil summary.TraceWindowSource")
194195
}
195-
return &AILogTraceSummarizationHandler{
196+
return &Handler{
196197
registry: registry,
197198
tools: toolReg,
198199
strategy: strat,
199200
source: source,
200201
headerName: headerName,
201-
maxIters: aiLogTraceSummarizationMaxIterations,
202+
maxIters: maxIterations,
202203
}
203204
}
204205

205-
// parseLogTraceSummarizationRequest drains the body. Both
206+
// parseRequest drains the body. Both
206207
// from_unix / to_unix are required; vehicle_id is optional.
207208
// Absence or invalid values surface as JSON 400 with a stable
208209
// error key the SPA can localise. Returns (req, true) when the
209210
// body is acceptable.
210-
func parseLogTraceSummarizationRequest(w http.ResponseWriter, r *http.Request) (aiLogTraceSummarizationRequest, bool) {
211-
var req aiLogTraceSummarizationRequest
211+
func parseRequest(w http.ResponseWriter, r *http.Request) (summarizationRequest, bool) {
212+
var req summarizationRequest
212213
if r.Body == nil {
213-
writeError(w, http.StatusBadRequest, "missing body")
214+
httpx.WriteError(w, http.StatusBadRequest, "missing body")
214215
return req, false
215216
}
216217
defer r.Body.Close()
217-
bodyBytes, readErr := io.ReadAll(io.LimitReader(r.Body, aiLogTraceSummarizationMaxBodyBytes))
218+
bodyBytes, readErr := io.ReadAll(io.LimitReader(r.Body, maxBodyBytes))
218219
if readErr != nil {
219-
writeError(w, http.StatusBadRequest, fmt.Sprintf("failed to read body: %v", readErr))
220+
httpx.WriteError(w, http.StatusBadRequest, fmt.Sprintf("failed to read body: %v", readErr))
220221
return req, false
221222
}
222-
if len(bytesTrim(bodyBytes)) == 0 {
223-
writeError(w, http.StatusBadRequest, "empty body")
223+
if len(trimSpace(bodyBytes)) == 0 {
224+
httpx.WriteError(w, http.StatusBadRequest, "empty body")
224225
return req, false
225226
}
226227
dec := json.NewDecoder(strings.NewReader(string(bodyBytes)))
227228
dec.DisallowUnknownFields()
228229
if err := dec.Decode(&req); err != nil {
229-
writeError(w, http.StatusBadRequest, fmt.Sprintf("invalid JSON body: %v", err))
230+
httpx.WriteError(w, http.StatusBadRequest, fmt.Sprintf("invalid JSON body: %v", err))
230231
return req, false
231232
}
232233
if req.FromUnix <= 0 {
233-
writeError(w, http.StatusBadRequest, "from_unix must be > 0")
234+
httpx.WriteError(w, http.StatusBadRequest, "from_unix must be > 0")
234235
return req, false
235236
}
236-
if req.FromUnix > aiLogTraceSummarizationMaxFromUnix {
237-
writeError(w, http.StatusBadRequest, fmt.Sprintf("from_unix exceeds upper bound %d", aiLogTraceSummarizationMaxFromUnix))
237+
if req.FromUnix > maxFromUnix {
238+
httpx.WriteError(w, http.StatusBadRequest, fmt.Sprintf("from_unix exceeds upper bound %d", maxFromUnix))
238239
return req, false
239240
}
240241
if req.ToUnix <= req.FromUnix {
241-
writeError(w, http.StatusBadRequest, "to_unix must be > from_unix")
242+
httpx.WriteError(w, http.StatusBadRequest, "to_unix must be > from_unix")
242243
return req, false
243244
}
244-
if req.ToUnix-req.FromUnix > aiLogTraceSummarizationMaxWindowSeconds {
245-
writeError(w, http.StatusBadRequest, fmt.Sprintf("window (%d s) exceeds cap %d s", req.ToUnix-req.FromUnix, aiLogTraceSummarizationMaxWindowSeconds))
245+
if req.ToUnix-req.FromUnix > maxWindowSeconds {
246+
httpx.WriteError(w, http.StatusBadRequest, fmt.Sprintf("window (%d s) exceeds cap %d s", req.ToUnix-req.FromUnix, maxWindowSeconds))
246247
return req, false
247248
}
248249
if req.VehicleID < 0 {
249-
writeError(w, http.StatusBadRequest, "vehicle_id must be >= 0")
250+
httpx.WriteError(w, http.StatusBadRequest, "vehicle_id must be >= 0")
250251
return req, false
251252
}
252253
return req, true
253254
}
254255

256+
func trimSpace(b []byte) []byte {
257+
for len(b) > 0 && (b[0] == ' ' || b[0] == '\t' || b[0] == '\r' || b[0] == '\n') {
258+
b = b[1:]
259+
}
260+
for len(b) > 0 && (b[len(b)-1] == ' ' || b[len(b)-1] == '\t' || b[len(b)-1] == '\r' || b[len(b)-1] == '\n') {
261+
b = b[:len(b)-1]
262+
}
263+
return b
264+
}
265+
255266
// ServeHTTP implements [http.Handler]. The body is parsed, the
256267
// dispatcher is invoked, and the SSE stream is closed via the
257268
// dispatcher's deferred WriteDone. Every error path either writes
258269
// a structured frame onto the SSE stream (when the writer has
259270
// been opened) or a plain JSON 4xx/5xx (before it has).
260-
func (h *AILogTraceSummarizationHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
271+
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
261272
// 1) Parse + validate the request body.
262-
req, ok := parseLogTraceSummarizationRequest(w, r)
273+
req, ok := parseRequest(w, r)
263274
if !ok {
264275
return
265276
}
@@ -270,7 +281,7 @@ func (h *AILogTraceSummarizationHandler) ServeHTTP(w http.ResponseWriter, r *htt
270281
// stream — emit JSON 502 so the frontend falls back gracefully.
271282
if _, err := h.registry.For(r.Context(), logtracesummarization.FeatureID); err != nil {
272283
log.Error().Err(err).Msg("ai log-trace-summarization: provider.For failed")
273-
writeError(w, http.StatusBadGateway, "ai provider unavailable")
284+
httpx.WriteError(w, http.StatusBadGateway, "ai provider unavailable")
274285
return
275286
}
276287

@@ -290,7 +301,7 @@ func (h *AILogTraceSummarizationHandler) ServeHTTP(w http.ResponseWriter, r *htt
290301
sseW, ctx, err := stream.New(ctx, w, stream.WithFeatureID(logtracesummarization.FeatureID))
291302
if err != nil {
292303
log.Error().Err(err).Msg("ai log-trace-summarization: stream.New failed (non-flushable writer)")
293-
writeError(w, http.StatusInternalServerError, "streaming not supported")
304+
httpx.WriteError(w, http.StatusInternalServerError, "streaming not supported")
294305
return
295306
}
296307

@@ -314,7 +325,7 @@ func (h *AILogTraceSummarizationHandler) ServeHTTP(w http.ResponseWriter, r *htt
314325
// window and instructs the tool sequence EXACTLY:
315326
// query_trace_window first, then OPTIONALLY
316327
// retrieve_log_chunks, then summary.
317-
userMsg := buildLogTraceSummarizationUserMessage(req.FromUnix, req.ToUnix, req.VehicleID)
328+
userMsg := buildUserMessage(req.FromUnix, req.ToUnix, req.VehicleID)
318329

319330
// 8) Run the dispatcher.
320331
in := strategy.StrategyInput{
@@ -330,11 +341,11 @@ func (h *AILogTraceSummarizationHandler) ServeHTTP(w http.ResponseWriter, r *htt
330341
}
331342
}
332343

333-
// buildLogTraceSummarizationUserMessage synthesises the window-
344+
// buildUserMessage synthesises the window-
334345
// scoped user message the LLM sees. The format is deterministic
335346
// (RFC3339 UTC time strings) so canned goldens and provider
336347
// prompt-hash caches stay stable across boots.
337-
func buildLogTraceSummarizationUserMessage(fromUnix, toUnix, vehicleID int64) string {
348+
func buildUserMessage(fromUnix, toUnix, vehicleID int64) string {
338349
fromStr := time.Unix(fromUnix, 0).UTC().Format(time.RFC3339)
339350
toStr := time.Unix(toUnix, 0).UTC().Format(time.RFC3339)
340351
var vehicleClause string
@@ -362,9 +373,14 @@ func buildLogTraceSummarizationUserMessage(fromUnix, toUnix, vehicleID int64) st
362373
)
363374
}
364375

365-
// Compile-time assertion: AILogTraceSummarizationHandler satisfies
376+
// denyAllConfirm is the dispatch confirm hook for this read-only AI surface.
377+
func denyAllConfirm(_ context.Context, _ dispatch.ConfirmRequest) (dispatch.ConfirmDecision, error) {
378+
return dispatch.ConfirmDenied, nil
379+
}
380+
381+
// Compile-time assertion: Handler satisfies
366382
// http.Handler.
367-
var _ http.Handler = (*AILogTraceSummarizationHandler)(nil)
383+
var _ http.Handler = (*Handler)(nil)
368384

369385
// ---------------------------------------------------------------------
370386
// Production wiring for the tool interface declared by
@@ -374,7 +390,7 @@ var _ http.Handler = (*AILogTraceSummarizationHandler)(nil)
374390
// pattern.
375391
// ---------------------------------------------------------------------
376392

377-
// AILogTraceWindowSource is the production
393+
// TraceWindowSource is the production
378394
// summary.TraceWindowSource. The operator-facing log surface is
379395
// stream-only — there is NO historical log persistence beyond
380396
// zerolog's stdout — so this adapter intentionally returns a
@@ -388,13 +404,13 @@ var _ http.Handler = (*AILogTraceSummarizationHandler)(nil)
388404
// handler installed and stringifies them so the LLM sees a
389405
// recognisable window without having to format Unix seconds
390406
// itself.
391-
type AILogTraceWindowSource struct{}
407+
type TraceWindowSource struct{}
392408

393-
// NewAILogTraceWindowSource constructs the deterministic empty
409+
// NewTraceWindowSource constructs the deterministic empty
394410
// adapter. No deps. Returned by-pointer for symmetry with the
395411
// other AI* source types.
396-
func NewAILogTraceWindowSource() *AILogTraceWindowSource {
397-
return &AILogTraceWindowSource{}
412+
func NewTraceWindowSource() *TraceWindowSource {
413+
return &TraceWindowSource{}
398414
}
399415

400416
// TraceWindow implements summary.TraceWindowSource. Returns a
@@ -404,7 +420,7 @@ func NewAILogTraceWindowSource() *AILogTraceWindowSource {
404420
// The envelope's slices are non-nil (empty-but-allocated) so JSON
405421
// marshalling renders [] rather than null — keeping the LLM's
406422
// tool-reply parsing predictable.
407-
func (a *AILogTraceWindowSource) TraceWindow(_ context.Context, fromUnix, toUnix, vehicleID int64) (*summary.TraceWindowEnvelope, error) {
423+
func (a *TraceWindowSource) TraceWindow(_ context.Context, fromUnix, toUnix, vehicleID int64) (*summary.TraceWindowEnvelope, error) {
408424
if fromUnix <= 0 {
409425
return nil, fmt.Errorf("api ai log-trace-summarization: from_unix must be > 0")
410426
}
@@ -428,6 +444,6 @@ func (a *AILogTraceWindowSource) TraceWindow(_ context.Context, fromUnix, toUnix
428444
}, nil
429445
}
430446

431-
// Compile-time assertion: AILogTraceWindowSource satisfies
447+
// Compile-time assertion: TraceWindowSource satisfies
432448
// summary.TraceWindowSource.
433-
var _ summary.TraceWindowSource = (*AILogTraceWindowSource)(nil)
449+
var _ summary.TraceWindowSource = (*TraceWindowSource)(nil)

0 commit comments

Comments
 (0)