Skip to content

feat(ai): Helix Alert Studio message template suggestion - #117

Merged
atulmgupta merged 3 commits into
mainfrom
helix-alerts
Sep 18, 2026
Merged

atulmgupta merged 3 commits into
mainfrom
helix-alerts

Conversation

@atulmgupta

Copy link
Copy Markdown
Contributor

Summary

Adds an opt-in Helix advisor on /notifications/studio next to Pick a preset. When the user clicks Ask Helix, it proposes one notification body grounded in the currently selected alert dimensions (kind, signal or computed metric, operator, severity, thresholds).

How it works

  • Feature ID: alert-message-template-suggestion (default off, ADR-015)
  • Route: POST /api/v1/ai/alerts/message-template/draft (404 when AI is off)
  • Tools (propose-only, no DB writes):
    • draft_alert_message_template — same placeholder catalog + related presets as the deterministic editor
    • validate_alert_message_template — rejects unknown {{tokens}} and over-long bodies
  • Apply to editor copies the validated template into the existing Message Template field
  • Canonical Save on Alert Studio remains the only write path

Test plan

  • AI off: Helix card is absent; presets/autocomplete still work
  • AI on + feature toggle: card appears above the template field
  • Suggest disabled until signal+op (or metric_id) are selected
  • Suggest a BrakePedal = rule → template names the signal and uses allowed placeholders
  • Apply copies into the textarea; Save still uses PUT/POST /api/v1/alerts/rules

Verification

  • go test for tools, strategy, handler, features, internal/api
  • go run ./tools/aigen + go run ./tools/aivet
  • vitest for the new card + AlertMessageEditor
  • npx tsc --noEmit
  • docker compose build teslasync-api web

Add a propose-only Helix advisor on Alert Studio next to the message
template editor. Suggestions are grounded in the selected alert
dimensions via draft/validate tools against the existing alertmsg
catalog. Apply copies the draft into the editor; Save remains the
canonical write path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e
Copilot AI lite review requested due to automatic review settings September 18, 2026 06:42
The Phase-48 identifier gate treats json:"value_min" as a duration-in-minutes
field. Keep range bounds in the raw request JSON for the Helix prompt without
a ValueMin struct tag on the handler DTO.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e

Copilot AI commented Sep 18, 2026

Copy link
Copy Markdown

One or more custom setup steps configured for this repository failed during this Copilot code review run:

Verify TypeScript build

Setup steps run before each review. If the review above is missing context, or no review was posted at all, the failing step above may be the cause. See the workflow run for failure details, fix your setup steps configuration, and re-request a review.

Note

You can configure setup steps for Copilot code review separately from Copilot cloud agent with a copilot-code-review.yml file. Read the docs for details.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved moderate findings affect route and wiring correctness, validation, request binding, and stale UI proposals.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds an opt-in Helix assistant for Alert Studio that proposes validated notification templates while preserving the existing Save flow.

Changes:

  • Adds feature-gated UI, translations, streaming, and Apply behavior.
  • Adds guarded API routing, tools, strategy, feature wiring, and registry metadata.
  • Adds placeholder extraction, tests, goldens, and canned responses.
File summaries
File Summary
web/src/i18n/en/locale-detail-notifications.json Generated notification translations.
web/src/i18n/en.json English source translations.
web/src/features/notifications/components/AlertMessageEditor.tsx Embeds the suggestion card.
web/src/features/notifications/components/AlertMessageEditor.test.tsx Tests editor integration with the AI card.
web/src/components/ai/AIAlertMessageTemplateSuggestion.tsx Implements streaming suggestions and Apply handoff. Moderate (3 votes): scope key omits dimensions. Moderate (1 vote): stale proposals are not reset on body changes. Moderate (1 vote): Apply ignores the disabled state.
web/src/components/ai/AIAlertMessageTemplateSuggestion.test.tsx Tests card behavior and streaming.
web/src/ai/spaWiring.ts Generated frontend wiring metadata.
web/src/ai/features.ts Frontend feature registration.
internal/api/router.go Backend dependency wiring.
internal/api/aialertmsg/handler.go Implements the suggestion SSE handler. Moderate (2 votes): request dimensions are not bound to tool execution. Nit (3 votes): API tracing, error recording, and trace-ID logging are missing.
internal/api/aialertmsg/handler_test.go Tests parsing, gating, and construction.
internal/api/aialertmsg/doc.go Documents the handler package.
internal/api/ai_routes.go Mounts the guarded route.
internal/alertmsg/formatter.go Adds placeholder extraction.
internal/alertmsg/formatter_test.go Tests placeholder extraction.
internal/ai/tools/alert/message_template.go Adds draft and validation tools. Moderate (1 vote): signal operators are not checked against the canonical allowlist. Moderate (1 vote): metric IDs are not checked against the registry. Moderate (1 vote): metric windows and operators are not validated.
internal/ai/tools/alert/message_template_test.go Tests tool behavior and registration.
internal/ai/tools/alert/doc.go Documents the alert tool package.
internal/ai/strategies/alert-message-template-suggestion/strategy.go Defines prompts and allowed tools.
internal/ai/strategies/alert-message-template-suggestion/strategy_test.go Tests strategy contracts.
internal/ai/strategies/alert-message-template-suggestion/goldens.yaml Defines evaluation scenarios.
internal/ai/strategies/alert-message-template-suggestion/doc.go Documents the strategy package.
internal/ai/strategies/alert-message-template-suggestion/canned/validation_failure_unknown_placeholder.yaml Canned validation-failure response.
internal/ai/strategies/alert-message-template-suggestion/canned/signal_threshold_template.yaml Canned signal response.
internal/ai/strategies/alert-message-template-suggestion/canned/computed_metric_template.yaml Canned computed-metric response.
internal/ai/features/spa_wiring.go Backend SPA wiring metadata. Moderate (2 votes): classify the feature as RenderSuggestion and populate the canonical handoff.
internal/ai/features/registry.go Backend feature metadata. Moderate (1 vote): route metadata points to /alerts/studio instead of /notifications/studio.
Review details

Suppressed comments (6)

internal/ai/features/registry.go:1607

  • The registry points this feature at /alerts/studio, but the SPA registers /notifications/studio and only has the singular /alert-studio legacy redirect (web/src/App.tsx:586-597). The PR's own registry comment identifies /notifications/studio; leaving this value stale makes the feature's route metadata point to a nonexistent path.
			Frontend:  []string{"/alerts/studio"},

internal/ai/tools/alert/message_template.go:277

  • The signal branch only checks that op is non-empty, so values such as bogus pass both tool schemas and produce a status=ok proposal even though the canonical AlertRule validator rejects them (internal/api/alerts/alert_rules.go:991-1026). Validate the operator against the canonical allowlist before building the rule; otherwise this propose-only endpoint can spend an AI request on dimensions that Alert Studio cannot save.
		if rule.Op == "" {
			return nil, fmt.Errorf("op is required for kind=signal")

internal/ai/tools/alert/message_template.go:285

  • A computed-metric request with an unknown metric_id is accepted here and then exposed as an allowed placeholder, so the tool can return status=ok for a metric that the canonical validator rejects (validateComputedMetricRule checks the registry at internal/api/alerts/alert_rules.go:912-937). Check the selected ID against the computed-metric registry before returning a grounded catalog.
		metricID := strings.TrimSpace(in.MetricID)
		if metricID == "" {
			return nil, fmt.Errorf("metric_id is required for kind=computed_metric")
		}
		rule.MetricID = &metricID

internal/ai/tools/alert/message_template.go:290

  • When metric_window or metric_op is supplied, this code accepts any non-empty string instead of checking the metric-specific window and supported computed-metric operators. That lets validation succeed for dimensions the canonical Alert Studio rule path rejects; validate these optional fields whenever present before constructing the rule.
		if w := strings.TrimSpace(in.MetricWindow); w != "" {
			rule.MetricWindow = &w
		}
		if op := strings.TrimSpace(in.MetricOp); op != "" {
			rule.MetricOp = &op

web/src/components/ai/AIAlertMessageTemplateSuggestion.tsx:162

  • This cleanup only runs on unmount because cancelStream is stable; it does not clear the locally captured proposal when any draft dimension changes. Even after the stream scope is corrected, a template validated for the previous threshold/signal remains available through Apply in the new editor state. Reset the proposal when the memoized request body changes, while keeping a separate unmount cleanup for cancellation.
  useEffect(() => {
    return () => {
      cancelStream()
      setProposal(null)
    }

web/src/components/ai/AIAlertMessageTemplateSuggestion.tsx:217

  • The parent passes disabled while Alert Studio is saving, but this Apply button ignores it and remains able to call onApplyTemplate during that mutation. That lets the AI surface modify the editor while the rest of the editor is intentionally locked. Include disabled in both the button's disabled expression and its aria-disabled value.
              disabled={proposal == null || isBusy}
              aria-disabled={proposal == null || isBusy ? 'true' : 'false'}
  • Files reviewed: 27/27 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +152 to +153
Render: RenderNarrative,
},
Comment on lines +155 to +163
d := dispatch.New(h.tools, prov, denyAllConfirm, h.maxIters)
userMsg := fmt.Sprintf(
"Suggest a message template for this alert. "+
"kind=%s signal_name=%q op=%q severity=%q metric_id=%q metric_op=%q. "+
"Caller JSON (copy numeric operands and range bounds exactly): %s. "+
"Call draft_alert_message_template FIRST with these exact dimensions, "+
"then compose a template using only allowed_placeholders, "+
"then call validate_alert_message_template. "+
"Do NOT save the template; the user applies it in Alert Studio.",
}
}, [])

const scopeKey = `${draft.kind ?? ''}:${draft.signal_name ?? ''}:${draft.op ?? ''}:${draft.metric_id ?? ''}`
Comment on lines +124 to +125
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
body, raw, err := parseRequest(r)
TestHandlerV1Thinness forbids internal/api in handler/v1. Keep the
unwrapped JSON payload (request() does not unwrap {data}) using
encoding/json instead of httpx.WriteJSON.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e
Copilot AI review requested due to automatic review settings September 18, 2026 07:09

Copilot AI commented Sep 18, 2026

Copy link
Copy Markdown

One or more custom setup steps configured for this repository failed during this Copilot code review run:

Verify TypeScript build

Setup steps run before each review. If the review above is missing context, or no review was posted at all, the failing step above may be the cause. See the workflow run for failure details, fix your setup steps configuration, and re-request a review.

Note

You can configure setup steps for Copilot code review separately from Copilot cloud agent with a copilot-code-review.yml file. Read the docs for details.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Unresolved issues affect route coverage, request-dimension binding, observability, prompt validation, and frontend state safety.

Review details

Suppressed comments (7)

Previously missed (2) — in code that hasn't changed since the last review.

internal/ai/tools/alert/message_template.go:258

  • Both tool paths build the rule entirely from model-supplied dimensions here, while the HTTP handler only places the selected dimensions in prompt text. A model or prompt-injected field can call these tools for a different signal/metric/operator, receive status=ok, and hand the SPA a template for dimensions the user did not select. Bind the request dimensions in the dispatcher context and reject mismatches (and missing scope) in this shared path, as the scoped AI tools do; prompts alone are not an enforcement boundary.
    web/src/components/ai/AIAlertMessageTemplateSuggestion.tsx:217
  • The disabled prop is included in canStart, so Suggest is blocked, but Apply only checks proposal and isBusy. When this editor is rendered disabled, an already-captured proposal can still mutate the form through onApplyTemplate; include disabled in the Apply button's disabled and aria-disabled conditions (and guard handleApply).

internal/ai/features/registry.go:1607

  • Routes.Frontend is consumed by the AI-off walker, but /alerts/studio is not an SPA route: App.tsx mounts /notifications/studio, while the legacy redirect is /alert-studio. The new feature's coverage visit will therefore land on the NotFound route and contradict the advertised host path; point this entry at /notifications/studio.
			Frontend:  []string{"/alerts/studio"},

internal/api/aialertmsg/handler.go:128

  • This new HTTP handler has no otel.Tracer("api") span, so its parse/provider/stream/dispatcher failures cannot be recorded on a span or correlated with a trace_id in error logs. Start a span at entry, propagate its context through the stream and dispatcher, record each error, and include the span trace ID in the error logs to match the API observability contract.
// ServeHTTP streams a propose-only template suggestion.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	body, raw, err := parseRequest(r)
	if err != nil {
		httpx.WriteError(w, http.StatusBadRequest, err.Error())
		return

internal/api/aialertmsg/handler.go:163

  • The parser decodes into a typed struct but then appends the entire raw request, including arbitrary unknown keys, to the LLM user message. A caller can add instruction-like fields outside this endpoint's contract and steer the model; reject unknown fields or serialize only the validated body before building the prompt.
			"Caller JSON (copy numeric operands and range bounds exactly): %s. "+
			"Call draft_alert_message_template FIRST with these exact dimensions, "+
			"then compose a template using only allowed_placeholders, "+
			"then call validate_alert_message_template. "+
			"Do NOT save the template; the user applies it in Alert Studio.",

web/src/components/ai/AIAlertMessageTemplateSuggestion.tsx:148

  • scopeKey only includes kind, signal_name, op, and metric_id, so changing severity, name, thresholds, value operands, or metric window/op does not reset the in-flight stream or the captured proposal. A user can therefore change the selected alert dimensions and still apply a template generated for the previous draft. Include the full request body (or otherwise all template dimensions) in the scope identity and clear/cancel the local proposal when it changes.
  const scopeKey = `${draft.kind ?? ''}:${draft.signal_name ?? ''}:${draft.op ?? ''}:${draft.metric_id ?? ''}`

web/src/components/ai/AIAlertMessageTemplateSuggestion.tsx:261

  • This new component is 261 lines, exceeding the frontend component limit of 200 lines. Extract the proposal preview/apply block or the stream/body helpers into sibling files so the feature remains a thin orchestrator and stays within the repository's decomposition rule.
export const AIAlertMessageTemplateSuggestion = withAiFeature(
  'alert-message-template-suggestion',
  InnerSection,
)
AIAlertMessageTemplateSuggestion.displayName = 'AIAlertMessageTemplateSuggestion'
  • Files reviewed: 28/28 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@atulmgupta
atulmgupta merged commit 40d95f1 into main Sep 18, 2026
26 of 27 checks passed
@atulmgupta
atulmgupta deleted the helix-alerts branch September 18, 2026 15:01
atulmgupta added a commit that referenced this pull request Sep 18, 2026
Resolve analysis_handler.go comment conflict; both sides already wrote
unwrapped JSON via encoding/json (no httpx).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants