Files
cairnobs/api/ai/router/router.go
T
jcoffey-dev 7d316f92db Phase 7: AI-assisted query authoring (autocomplete, explain, fix, optimize, NL translation)
Adds a self-hosted (Ollama, qwen2.5-coder) model provider abstraction
with a pluggable opt-in cloud adapter, schema grounding, and a shared
cost/safety guard every AI-suggested query is assessed against --
compiling to and executing through the same unchanged Phase 2 IR/
compiler and Phase 4 tenant scoping as a hand-written query, no
parallel execution path.

Track A (built into the query bar): inline ghost-text autocomplete,
"Explain this query", "Fix this query" with a diff view, and a
rule-based "Optimize" suggestion. Track B: natural-language-to-query
translation, always a separate review step from execution, with
`sentryctl query --nl` requiring explicit confirmation to run.
Every accepted/dismissed translate-fix-optimize interaction is logged
into the same append-only audit_log table Phase 4 built.

Two real product bugs were found and fixed via live browser
verification (a Svelte effect re-running on every keystroke that
silently cancelled the ghost-text debounce; a ghost-text widget
positioned at document offset 0 instead of the cursor), and a real
costguard logic bug (unbounded-aggregation vs. raw-row) was caught by
its own test suite. New integration tests wire a real Ollama client
through the real HTTP handler against a mock server matching Ollama's
wire contract (hack/mock-ollama), keeping model-quality verification
out of CI as a disclosed, periodic human-run check instead.

See /docs/phase-7-ai-design.md and /docs/phase-7-runbook.md.
2026-08-16 18:06:27 -07:00

63 lines
2.4 KiB
Go

// Package router is the per-operation provider/model dispatch layer
// /docs/phase-7-ai-design.md's "per-operation provider/model
// configuration" section decided to build now rather than defer --
// Complete's tight latency budget and Translate/Fix's quality needs are
// already in tension under a single-model-for-everything design, not a
// hypothetical future conflict.
//
// Deliberately thin: a lookup table from Operation to whichever
// provider.Provider was configured for it, falling back to one default
// when an operation has no specific override. This package makes no
// decisions about *which* provider is good for an operation -- that's
// deployment configuration, resolved once at startup by whoever
// constructs a Router (main.go, once the AI HTTP handlers exist to
// consume it).
package router
import "github.com/sentry/sentry/api/ai/provider"
type Operation string
const (
OpTranslate Operation = "translate"
OpComplete Operation = "complete"
OpExplain Operation = "explain"
OpFix Operation = "fix"
)
// Router selects a provider.Provider per Operation. Not itself a
// provider.Provider -- callers ask For(op) and then call the operation
// they actually need on the result, rather than this type trying to
// implement all four methods and dispatch internally, which would just
// be an extra layer of indirection for no benefit.
type Router struct {
byOp map[Operation]provider.Provider
fallback provider.Provider
}
// New builds a Router. fallback must not be nil -- every operation
// resolves to *some* provider, even a deployment that never calls
// SetOperation and just wants one model for everything.
func New(fallback provider.Provider) *Router {
return &Router{byOp: make(map[Operation]provider.Provider), fallback: fallback}
}
// SetOperation overrides which provider handles op. Call once per
// operation that needs a non-default model at startup configuration
// time -- not intended to change at runtime (a Router isn't
// synchronized for concurrent SetOperation/For calls, matching every
// other "assembled once in main.go, read-only after that" config shape
// in this codebase, e.g. Handler's fields in queryapi).
func (r *Router) SetOperation(op Operation, p provider.Provider) {
r.byOp[op] = p
}
// For returns the provider configured for op, or the fallback if none
// was set specifically.
func (r *Router) For(op Operation) provider.Provider {
if p, ok := r.byOp[op]; ok {
return p
}
return r.fallback
}