Documentation
¶
Overview ¶
Package copilot wraps an LLM Messages/Chat API to produce natural-language explanations of telemetry artifacts — trace flame, open Problem, exception group.
Three providers supported:
- "anthropic": Anthropic Messages API (api.anthropic.com).
- "github": GitHub Copilot Chat (api.githubcopilot.com). The caller's API key is a GitHub OAuth token (`ghu_…`) which we exchange for a short-lived Copilot session token (cached + auto-refreshed).
- "openai": Any OpenAI-compatible /v1/chat/completions endpoint. Drives self-hosted local LLMs (Ollama, LM Studio, vLLM, llama.cpp server, LocalAI, OpenWebUI) AND the real OpenAI API. Banks running Coremetry air-gapped want this so traces / problems never leave the perimeter for explanation. APIKey is optional for local endpoints that don't gate on it (Ollama default).
The Service is configurable at runtime — admins can flip provider or rotate keys via the Settings UI without restarting Coremetry.
stream.go (v0.8.404) — token streaming for the one-shot narration call, with a TRANSPARENT runtime fallback to the buffered path.
StreamText is the streaming twin of Explain: same answer contract (full text + error return, one self-recorded ai_calls row), plus an onDelta callback fired per content chunk so the API layer can relay live tokens over its SSE stream. It covers the GUIDED chat path's single tool-less call — the clean streaming case.
Deliberately OUT of scope this slice:
- The free tool loop (ChatWithTools) stays buffered. Tool-call streaming is a different beast: deltas interleave partial tool_call JSON fragments that must be reassembled per index before anything is executable, the answer text may arrive in multiple assistant turns, and provider extras (Gemini thought_signature, v0.8.373) ride on the reassembled call. None of that buys the operator visible latency wins — the loop's time goes to tool execution rounds, not narration.
- GitHub Copilot: the session-token exchange + integration-header dance has no verified streaming contract; it uses the buffered call (zero deltas — the caller's final answer event still lands).
FALLBACK (the critical part — vLLM stream support is UNVERIFIED on the primary target, so the code adapts instead of assuming): when the stream:true request fails at CONNECT/first-byte — non-200, non-SSE content-type, immediate EOF before any event, JSON error body — we transparently retry ONCE with the existing buffered call and log "copilot stream unsupported, buffered fallback". A deterministic rejection additionally caches an "unsupported" verdict per (provider,baseURL,model) so subsequent guided calls skip the probe; Configure resets the cache. Mid-stream failures (after data has flowed) do NOT fall back — deltas already reached the client.
Index ¶
- Constants
- func SystemPromptAnomaly() string
- func SystemPromptCHQueryOptimize() string
- func SystemPromptCompareTraces() string
- func SystemPromptDeployImpact() string
- func SystemPromptException() string
- func SystemPromptIncident() string
- func SystemPromptNLToQuery() string
- func SystemPromptProblem() string
- func SystemPromptRootCauseNarration() string
- func SystemPromptRunbook() string
- func SystemPromptSLOBurn() string
- func SystemPromptServiceHealth() string
- func SystemPromptServiceTags() string
- func SystemPromptSlowQuery() string
- func SystemPromptSpan() string
- func SystemPromptTrace() string
- func WithMeta(ctx context.Context, m CallMeta) context.Context
- type CallMeta
- type CallRecord
- type ChatMessage
- type ChatTurn
- type Recorder
- type Service
- func (s *Service) Active() bool
- func (s *Service) ChatWithTools(ctx context.Context, system string, msgs []ChatMessage, tools []ToolSpec) (ChatTurn, error)
- func (s *Service) Configure(provider, apiKey, model, baseURL string, skipTLS, enabled bool)
- func (s *Service) Configured() bool
- func (s *Service) Explain(ctx context.Context, systemPrompt, userPrompt string) (string, error)
- func (s *Service) LoadPersisted(ctx context.Context, store SettingsStore) error
- func (s *Service) RecordUsage(ctx context.Context, inTok, outTok uint32, ...)
- func (s *Service) SavePersisted(ctx context.Context, store SettingsStore, ...) error
- func (s *Service) SetRecorder(r Recorder)
- func (s *Service) Snapshot() (provider, model, baseURL string, hasKey, skipTLS, enabled bool)
- func (s *Service) StartConfigRefresh(ctx context.Context, store SettingsStore, interval time.Duration)
- func (s *Service) StreamText(ctx context.Context, systemPrompt, userPrompt string, onDelta func(string)) (string, error)
- type SettingsStore
- type ToolCall
- type ToolResult
- type ToolSpec
Constants ¶
const ( ProviderAnthropic = "anthropic" ProviderGitHub = "github" ProviderOpenAI = "openai" )
const AnswerInTurkish = "\n\nHer zaman Türkçe yanıt ver."
AnswerInTurkish is appended to every PROSE copilot surface (v0.8.374, operator decision: "hepsi Türkçe" — the AI-analysis panel was already Turkish while Explain answered in English). Strict-JSON surfaces (systemNLToQuery, systemCHQueryOptimize, systemServiceTags) deliberately do NOT get it: a language directive invites prose around machine-parsed output. Exported so the api package's chat prompt shares the exact same line. Pinned by TestProsePromptsAnswerInTurkish.
Variables ¶
This section is empty.
Functions ¶
func SystemPromptAnomaly ¶ added in v0.5.23
func SystemPromptAnomaly() string
func SystemPromptCHQueryOptimize ¶ added in v0.6.8
func SystemPromptCHQueryOptimize() string
func SystemPromptCompareTraces ¶ added in v0.5.74
func SystemPromptCompareTraces() string
func SystemPromptDeployImpact ¶ added in v0.5.78
func SystemPromptDeployImpact() string
func SystemPromptException ¶
func SystemPromptException() string
func SystemPromptIncident ¶ added in v0.5.23
func SystemPromptIncident() string
func SystemPromptNLToQuery ¶ added in v0.5.255
func SystemPromptNLToQuery() string
func SystemPromptProblem ¶
func SystemPromptProblem() string
func SystemPromptRootCauseNarration ¶ added in v0.8.170
func SystemPromptRootCauseNarration() string
func SystemPromptRunbook ¶ added in v0.5.35
func SystemPromptRunbook() string
func SystemPromptSLOBurn ¶ added in v0.5.82
func SystemPromptSLOBurn() string
func SystemPromptServiceHealth ¶ added in v0.5.31
func SystemPromptServiceHealth() string
func SystemPromptServiceTags ¶ added in v0.5.49
func SystemPromptServiceTags() string
func SystemPromptSlowQuery ¶ added in v0.5.171
func SystemPromptSlowQuery() string
func SystemPromptSpan ¶ added in v0.5.144
func SystemPromptSpan() string
func SystemPromptTrace ¶
func SystemPromptTrace() string
Types ¶
type CallMeta ¶ added in v0.5.164
type CallMeta struct {
Surface string
UserID string
UserEmail string
// ExchangeID — see CallRecord.ExchangeID (v0.8.399). Carried in
// ctx so both the RecordUsage path (free chat loop) and the
// Explain self-recording path (guided chat) stamp the same id
// without new parameters on either call chain.
ExchangeID string
}
CallMeta is attribution data the API layer stashes in ctx before calling Explain — surface (which Copilot endpoint), userID/email for "who triggered this call" filtering on the /ai page.
func MetaFromContext ¶ added in v0.5.164
MetaFromContext is the read side. Returns the zero CallMeta when no tag is present so callers can treat it as "unknown".
type CallRecord ¶ added in v0.5.164
type CallRecord struct {
CreatedAt time.Time
Surface string
// ExchangeID (v0.8.399) — correlation key for operator feedback:
// the chat handler mints one id per exchange, emits it to the UI
// in the SSE answer event, and threads it here via CallMeta so a
// thumbs up/down (ai_feedback row) can be joined back to the
// ai_calls row it rates. Empty for surfaces that don't emit one.
// Provider-agnostic — pure correlation plumbing, no LLM coupling.
ExchangeID string
Provider string
Model string
BaseURL string
DurationMs uint32
InputTokens uint32
OutputTokens uint32
Status string
ErrorMsg string
PromptChars uint32
ResponseChars uint32
UserID string
UserEmail string
PromptSample string
ResponseSample string
}
CallRecord captures one LLM round-trip. CreatedAt is set by the Explain wrapper at call start; DurationMs measured at return. Token counts come from the provider response when available (OpenAI + Anthropic both ship usage data; some Ollama versions don't — those stay 0).
type ChatMessage ¶ added in v0.6.53
type ChatMessage struct {
Role string // "user" | "assistant"
Text string `json:",omitempty"`
ToolCalls []ToolCall `json:",omitempty"`
ToolResults []ToolResult `json:",omitempty"`
}
ChatMessage is one provider-neutral conversation turn. A user turn carries Text (the question) OR ToolResults (function outputs fed back). An assistant turn carries Text (prose) and/or ToolCalls (functions it wants run).
type ChatTurn ¶ added in v0.6.53
ChatTurn is one model response. When ToolCalls is non-empty the caller must execute them and loop; otherwise Text is the final answer.
type Recorder ¶ added in v0.5.164
type Recorder interface {
RecordCall(ctx context.Context, c CallRecord)
}
Recorder is the sink for the Coremetry-native AI observability pipeline. Implemented by a thin adapter around chstore.Store (kept in package api to avoid copilot→chstore import dependency). Every Explain call emits exactly one CallRecord regardless of success — errors show up in /ai with status="error" so the operator sees broken provider configs without grepping logs.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service is the small surface other packages call into.
Internals are guarded by mu so PUT /api/settings/ai can swap creds while Explain calls are in flight.
func New ¶
New always returns a Service. When apiKey is empty Configured() reports false and callers branch off — that's the dormant state before the operator pastes a key in Settings.
func (*Service) Active ¶ added in v0.8.189
Active reports whether the Copilot is BOTH enabled AND configured. This is the gate for any path that actually calls the provider — the background ProblemExplainer, Explain/ChatWithTools, the AI-usage HTTP endpoints, and the UI feature-flag (/api/copilot/config).
Distinct from Configured(): when the operator flips "Enable AI Copilot" OFF in Settings we KEEP the stored creds (Configured() stays true so the Settings form still renders) but Active() goes false — the background explainer stops hammering the provider, the AI affordances hide, and the AI endpoints 503. Re-enabling is one click (no key to re-paste). wf.
Inlines Configured()'s logic under a single RLock so the enabled gate and the cred check read a consistent snapshot.
func (*Service) ChatWithTools ¶ added in v0.6.53
func (s *Service) ChatWithTools(ctx context.Context, system string, msgs []ChatMessage, tools []ToolSpec) (ChatTurn, error)
ChatWithTools runs ONE model turn over the conversation with the given tools available. Branches on the configured provider. No ai_calls recording here — the handler records once per user message after the agentic loop settles (RecordUsage), summing the per-turn token usage, so one chat exchange = one ai_calls row.
func (*Service) Configure ¶
Configure swaps live credentials. Used by PUT /api/settings/ai. Empty apiKey legitimately disables the feature — Configured() flips to false and the UI hides the buttons. baseURL is only consulted by the "openai" provider; ignored for anthropic/github so a stale value persisted from a previous selection doesn't leak. v0.5.360: skipTLS rebuilds the http.Client transport when it flips; otherwise the existing client is kept (its 180s timeout matches the local-LLM use case). wf: enabled is the master on/off switch — set here so it lives behind the same lock as the creds and can't tear with an in-flight Active()/Explain.
func (*Service) Configured ¶
Configured reports whether the service has credentials. The "openai" provider with an empty key is allowed when baseURL points at a local endpoint that doesn't gate on auth (Ollama default config) — the caller's request just goes through with no Authorization header.
func (*Service) Explain ¶
Explain runs a single Messages/Chat call with the given system + user prompt. Branches on the configured provider. v0.5.162 wraps the dispatch with the AI-observability recorder so every call emits an ai_calls row regardless of success — recording happens on a goroutine so the user doesn't pay ingest cost in their request path.
func (*Service) LoadPersisted ¶
func (s *Service) LoadPersisted(ctx context.Context, store SettingsStore) error
LoadPersisted reads any DB-saved override and applies it. Silently skips when nothing's saved — env defaults stay in effect.
func (*Service) RecordUsage ¶ added in v0.6.53
func (s *Service) RecordUsage(ctx context.Context, inTok, outTok uint32, status, errMsg, promptSample, respSample string)
RecordUsage writes a single ai_calls row for a completed chat exchange. Mirrors the recording block in Explain so the /ai page attributes chat usage alongside the ✨ Explain surfaces. Surface comes from MetaFromContext (the handler sets it to "chat").
func (*Service) SavePersisted ¶
func (s *Service) SavePersisted(ctx context.Context, store SettingsStore, provider, apiKey, model, baseURL string, skipTLS, enabled bool) error
SavePersisted writes new credentials to system_settings AND updates the live Service. Called by PUT /api/settings/ai. v0.5.360 — skipTLS plumbed through end-to-end. wf — enabled persisted as a pointer (&enabled) so the round-trip is explicit; once SavePersisted has run the blob always carries the field. The disable-without-clearing-creds path is just enabled=false with the apiKey left untouched.
func (*Service) SetRecorder ¶ added in v0.5.164
SetRecorder wires the observability sink. Nil disables it. Safe to call before the Service is in use (single goroutine at boot).
func (*Service) Snapshot ¶
Snapshot returns the current configuration. The apiKey is masked (only "set" / "unset" matters to the UI) — full key is never echoed. baseURL is non-secret (operators put it in their Helm values), so we echo it back so the Settings page can show what's wired up. v0.5.360 — skipTLS surfaced so the UI checkbox reflects what's actually live. wf — enabled surfaced so getAISettings can drive the Settings toggle independently of whether a key is stored.
func (*Service) StartConfigRefresh ¶ added in v0.5.324
func (s *Service) StartConfigRefresh(ctx context.Context, store SettingsStore, interval time.Duration)
StartConfigRefresh — v0.5.324. Background poll: keeps the in-memory Copilot config in sync with the shared persisted blob across pods. interval ≤ 0 → 30s.
func (*Service) StreamText ¶ added in v0.8.404
func (s *Service) StreamText(ctx context.Context, systemPrompt, userPrompt string, onDelta func(string)) (string, error)
StreamText runs a single system+user narration call, streaming answer tokens through onDelta as they arrive. Returns the FULL final text — identical to what Explain would have returned — so the caller keeps its existing "answer is the source of truth" contract; the deltas are a pure progressive-rendering bonus. onDelta may be nil. Reasoning output (delta.reasoning_content / delta.reasoning / inline <think> blocks / Anthropic thinking_delta) is buffered silently and never streamed; if the model emits ONLY reasoning, the salvaged answer (v0.8.384 chain) is emitted as one final delta.
type SettingsStore ¶
type SettingsStore interface {
GetSetting(ctx context.Context, key string) ([]byte, error)
PutSetting(ctx context.Context, key string, value []byte) error
}
SettingsStore is the small slice of *chstore.Store we need — declared as an interface here so this package doesn't import chstore (which would cycle through callers).
type ToolCall ¶ added in v0.6.53
type ToolCall struct {
ID string // provider-issued id, echoed back in the result
Name string // tool name
Input json.RawMessage // arguments as JSON
// Raw is the COMPLETE tool_call object exactly as the openai-compat
// provider returned it (v0.8.373, operator-reported). Gemini's
// compat endpoint attaches extra fields (extra_content →
// thought_signature) and REJECTS the next turn with 400
// INVALID_ARGUMENT when the replayed functionCall lacks them —
// rebuilding the object from the trimmed fields above silently
// dropped everything unknown. When set, the replay encoder sends
// Raw verbatim; ID/Name/Input remain the parsed view for the
// executor. Nil for the Anthropic path and legacy messages.
Raw json.RawMessage `json:",omitempty"`
}
ToolCall is one function invocation the model requested.
type ToolResult ¶ added in v0.6.53
type ToolResult struct {
CallID string
Name string
Content string // JSON-stringified tool output (or an error string)
IsError bool
}
ToolResult is the output of executing a ToolCall, fed back into the next turn so the model can read it.