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.
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
module github.com/sentry/sentry/hack/mock-ollama
|
||||
|
||||
go 1.25.0
|
||||
@@ -0,0 +1,100 @@
|
||||
// Command mock-ollama stands in for a real Ollama server (see
|
||||
// /docs/phase-7-ai-design.md) when manually verifying Phase 7's AI
|
||||
// features against a live docker-compose stack without needing model
|
||||
// weights or a GPU. Matches Ollama's real POST /api/chat wire contract
|
||||
// (see api/ai/provider/ollama/ollama.go's chatRequest/chatResponse)
|
||||
// closely enough that api/cmd/api and enterprise/cmd/enterprise-api
|
||||
// can't tell the difference -- picks a canned, deterministic response by
|
||||
// inspecting the system prompt's distinctive opening line (see
|
||||
// api/ai/provider/ollama/prompts.go), the same technique the
|
||||
// integration tests in api/ai/aiapi/integration_test.go use for the
|
||||
// same reason: no live model, deterministic output, fast.
|
||||
//
|
||||
// Not part of any docker-compose service by default -- run it
|
||||
// standalone (or as a throwaway container on the sentry_default
|
||||
// network) and point OLLAMA_BASE_URL at it. See
|
||||
// /docs/phase-7-runbook.md for the exact recipe.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type chatMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type chatRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []chatMessage `json:"messages"`
|
||||
}
|
||||
|
||||
type chatResponse struct {
|
||||
Message chatMessage `json:"message"`
|
||||
}
|
||||
|
||||
// canned responses, keyed by a distinctive substring of each
|
||||
// operation's system prompt (see prompts.go's opening sentence for
|
||||
// each). Content is deliberately valid, uninteresting pipe syntax --
|
||||
// this tool exists to verify plumbing, not to simulate model quality.
|
||||
var canned = []struct {
|
||||
systemPromptContains string
|
||||
response string
|
||||
}{
|
||||
{"translate a plain-English question", `{"query":"earliest=-1h severity=ERROR","confidence":"high"}`},
|
||||
{"suggest how to continue", `{"suggestion":" severity=ERROR"}`},
|
||||
{"phrase those findings", "Add a time range (e.g. earliest=-1h) to avoid scanning the entire table."},
|
||||
{"fix a broken", `{"suggested_query":"earliest=-1h severity=ERROR","explanation":"added a missing time bound","confidence":"high"}`},
|
||||
// Plain explain (no findings, no original-intent framing) falls
|
||||
// through to this last, broadest match.
|
||||
{"", "This query filters logs where severity equals ERROR from the last hour."},
|
||||
}
|
||||
|
||||
func respond(system string) string {
|
||||
for _, c := range canned {
|
||||
if strings.Contains(system, c.systemPromptContains) {
|
||||
return c.response
|
||||
}
|
||||
}
|
||||
return canned[len(canned)-1].response
|
||||
}
|
||||
|
||||
func main() {
|
||||
addr := flag.String("addr", ":11434", "listen address")
|
||||
flag.Parse()
|
||||
|
||||
http.HandleFunc("/api/chat", func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "reading body: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var req chatRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
http.Error(w, "decoding request: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var system string
|
||||
for _, m := range req.Messages {
|
||||
if m.Role == "system" {
|
||||
system = m.Content
|
||||
break
|
||||
}
|
||||
}
|
||||
content := respond(system)
|
||||
fmt.Printf("chat: model=%s -> %s\n", req.Model, content)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(chatResponse{Message: chatMessage{Role: "assistant", Content: content}})
|
||||
})
|
||||
|
||||
log.Printf("mock-ollama listening on %s", *addr)
|
||||
log.Fatal(http.ListenAndServe(*addr, nil))
|
||||
}
|
||||
Reference in New Issue
Block a user