e071084ebe
govulncheck / govulncheck (push) Waiting to run
Harness (E2E) / Harnesses (mock LLM) (push) Waiting to run
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Waiting to run
Lint / golangci-lint (push) Waiting to run
Run Tests / Unit Tests (push) Waiting to run
Run Tests / Etcd Integration Tests (push) Waiting to run
241 lines
8.8 KiB
Go
241 lines
8.8 KiB
Go
// Package ai provides abstraction for AI model providers
|
|
package ai
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
)
|
|
|
|
// Model provides an interface for interacting with AI model providers
|
|
type Model interface {
|
|
// Init initializes the model with options
|
|
Init(...Option) error
|
|
// Options returns the model options
|
|
Options() Options
|
|
// Generate generates a response from the model
|
|
Generate(ctx context.Context, req *Request, opts ...GenerateOption) (*Response, error)
|
|
// Stream generates a streaming response (for future implementation)
|
|
Stream(ctx context.Context, req *Request, opts ...GenerateOption) (Stream, error)
|
|
// String returns the name of the provider
|
|
String() string
|
|
}
|
|
|
|
// Tool represents a tool/function that can be called by the model
|
|
type Tool struct {
|
|
Name string // LLM-safe name (e.g., "greeter_Greeter_Hello")
|
|
OriginalName string // Original name (e.g., "greeter.Greeter.Hello")
|
|
Description string
|
|
Properties map[string]any // JSON schema for tool parameters
|
|
}
|
|
|
|
// Request represents a request to generate content from a model
|
|
type Request struct {
|
|
// Prompt is the user's message/prompt
|
|
Prompt string
|
|
// SystemPrompt is the system instruction for the model
|
|
SystemPrompt string
|
|
// Tools available for the model to use
|
|
Tools []Tool
|
|
// Messages for continuing a conversation (optional).
|
|
// Use ai.History to accumulate these across turns.
|
|
Messages []Message
|
|
}
|
|
|
|
// Message represents a conversation message
|
|
type Message struct {
|
|
Role string // "user", "assistant", "system", "tool"
|
|
Content any // Can be string or structured content
|
|
}
|
|
|
|
// Usage describes token counts returned by model providers.
|
|
type Usage struct {
|
|
InputTokens int `json:"input_tokens,omitempty"`
|
|
OutputTokens int `json:"output_tokens,omitempty"`
|
|
TotalTokens int `json:"total_tokens,omitempty"`
|
|
}
|
|
|
|
// Response represents the response from a model
|
|
type Response struct {
|
|
// Reply is the text response from the model
|
|
Reply string
|
|
// ToolCalls are tool calls requested by the model
|
|
ToolCalls []ToolCall
|
|
// Answer is the final answer after tool execution (if tools were used)
|
|
Answer string
|
|
// Usage contains provider token usage when available.
|
|
Usage Usage
|
|
}
|
|
|
|
// ToolCall represents a request to call a tool and its result
|
|
type ToolCall struct {
|
|
ID string // Tool call ID (for correlation)
|
|
Name string // Tool name
|
|
Input map[string]any // Tool input arguments
|
|
Result string // Tool execution result (populated after execution)
|
|
Error string // Tool execution error (populated after execution)
|
|
}
|
|
|
|
// Scan decodes the call's Input into v (a pointer to a struct or map),
|
|
// the same way a codec decodes an RPC request body. Use it when a tool
|
|
// wants typed arguments instead of the raw map:
|
|
//
|
|
// var args struct{ Query string `json:"query"` }
|
|
// if err := call.Scan(&args); err != nil { ... }
|
|
func (c ToolCall) Scan(v any) error {
|
|
b, err := json.Marshal(c.Input)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return json.Unmarshal(b, v)
|
|
}
|
|
|
|
// ToolResult represents the result of a tool execution
|
|
type ToolResult struct {
|
|
ID string // Tool call ID (for correlation)
|
|
Value any // Structured result (optional)
|
|
Content string // Tool execution result (JSON string), shown to the model
|
|
Attempts int `json:"attempts,omitempty"` // Tool execution attempts, set when retried.
|
|
// Refused names the reason a guardrail blocked the call before it ran
|
|
// ("max_steps", "loop", "approval"); empty when the call executed. A
|
|
// tool wrapper can switch on it to build reliability tooling — react to
|
|
// a detected loop, audit refusals — without parsing the message.
|
|
Refused string `json:"refused,omitempty"`
|
|
}
|
|
|
|
// Refusal reason codes set on ToolResult.Refused by the agent's guardrails.
|
|
const (
|
|
RefusedMaxSteps = "max_steps"
|
|
RefusedLoop = "loop"
|
|
RefusedApproval = "approval"
|
|
// RefusedSpendBudget means an agent refused a paid tool before execution
|
|
// because the configured per-run x402 spend budget would be exceeded.
|
|
RefusedSpendBudget = "spend_budget"
|
|
)
|
|
|
|
// RunInfo describes the agent run a tool call belongs to. The agent
|
|
// attaches it to the context passed to a ToolHandler, so a wrapper can
|
|
// correlate calls within a run and across delegation without coupling to
|
|
// the agent package. Flows also attach their name and current step so
|
|
// tools and agents called from a workflow can be tied back to the
|
|
// services → agents → workflows lifecycle that invoked them. Per-call
|
|
// detail (tool name, id) is on the ToolCall. Attempt and MaxAttempts are
|
|
// set while a model Generate call is in progress, so tools and wrappers can
|
|
// tell which provider attempt produced the call and whether it is part of a
|
|
// retry budget. They are zero when no model-attempt context is known.
|
|
type RunInfo struct {
|
|
RunID string // correlation id for this agent or flow run
|
|
ParentID string // the run that delegated to this one, if any
|
|
Agent string // the agent's name
|
|
Flow string // the flow's name, when the call is part of a workflow
|
|
Step string // the flow step currently executing, when known
|
|
Attempt int // current model Generate attempt, starting at 1 when known
|
|
MaxAttempts int // configured model Generate attempt budget when known
|
|
VerificationFeedback string // feedback from the previous failed verifier attempt, when retrying a flow step
|
|
Dispatch string // how the run was dispatched (direct, broker, schedule, resume) when known
|
|
Trigger string // external trigger or schedule label that started the run, when known
|
|
Spent int64 // cumulative paid x402 spend in this run, in the asset's smallest unit
|
|
ToolSpend int64 // paid x402 spend attributed to the current tool call, in the asset's smallest unit
|
|
}
|
|
|
|
type runInfoKey struct{}
|
|
|
|
// WithRunInfo attaches run info to ctx.
|
|
func WithRunInfo(ctx context.Context, r RunInfo) context.Context {
|
|
return context.WithValue(ctx, runInfoKey{}, r)
|
|
}
|
|
|
|
// RunInfoFrom returns the run info attached to ctx, and whether it was set.
|
|
func RunInfoFrom(ctx context.Context) (RunInfo, bool) {
|
|
r, ok := ctx.Value(runInfoKey{}).(RunInfo)
|
|
return r, ok
|
|
}
|
|
|
|
// ErrStreamingUnsupported is returned by providers that implement the Model
|
|
// interface but do not yet support token streaming. Use errors.Is so callers
|
|
// can distinguish an unsupported capability from transient provider failures.
|
|
var ErrStreamingUnsupported = errors.New("ai: streaming unsupported")
|
|
|
|
// Stream is the interface for streaming responses.
|
|
type Stream interface {
|
|
// Recv receives the next chunk of the response
|
|
Recv() (*Response, error)
|
|
// Close closes the stream
|
|
Close() error
|
|
}
|
|
|
|
// ToolHandler executes a tool call and returns its result. It mirrors a
|
|
// go-micro RPC handler — context first, a request in, a result out — so
|
|
// the same mental model carries over from services to tools.
|
|
type ToolHandler func(ctx context.Context, call ToolCall) ToolResult
|
|
|
|
// ToolWrapper wraps a ToolHandler to add behavior around execution —
|
|
// logging, metrics, retries, guardrails. It is the tool-side analog of
|
|
// client.CallWrapper and server.HandlerWrapper: a wrapper takes the next
|
|
// handler and returns a new one, and code before the next(...) call runs
|
|
// before the tool, code after runs after.
|
|
type ToolWrapper func(ToolHandler) ToolHandler
|
|
|
|
// NewFunc creates a new Model instance
|
|
type NewFunc func(...Option) Model
|
|
|
|
var providers = make(map[string]NewFunc)
|
|
|
|
// Register registers a model provider
|
|
func Register(name string, fn NewFunc) {
|
|
providers[name] = fn
|
|
}
|
|
|
|
// New creates a new Model instance based on the provider name
|
|
func New(provider string, opts ...Option) Model {
|
|
if fn, ok := providers[provider]; ok {
|
|
return fn(opts...)
|
|
}
|
|
|
|
// Default to first registered provider
|
|
if len(providers) > 0 {
|
|
for _, fn := range providers {
|
|
return fn(opts...)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AutoDetectProvider attempts to detect the provider from the base URL
|
|
func AutoDetectProvider(baseURL string) string {
|
|
if baseURL == "" {
|
|
return "openai"
|
|
}
|
|
switch {
|
|
case strings.Contains(baseURL, "anthropic"):
|
|
return "anthropic"
|
|
case strings.Contains(baseURL, "atlascloud"):
|
|
return "atlascloud"
|
|
case strings.Contains(baseURL, "googleapis.com"), strings.Contains(baseURL, "google"):
|
|
return "gemini"
|
|
case strings.Contains(baseURL, "groq"):
|
|
return "groq"
|
|
case strings.Contains(baseURL, "minimax"):
|
|
return "minimax"
|
|
case strings.Contains(baseURL, "mistral"):
|
|
return "mistral"
|
|
case strings.Contains(baseURL, "together"):
|
|
return "together"
|
|
default:
|
|
return "openai"
|
|
}
|
|
}
|
|
|
|
// DefaultModel is a default model instance
|
|
var DefaultModel Model
|
|
|
|
// Generate generates a response using the default model.
|
|
func Generate(ctx context.Context, req *Request, opts ...GenerateOption) (*Response, error) {
|
|
if DefaultModel == nil {
|
|
return nil, nil
|
|
}
|
|
return DefaultModel.Generate(ctx, req, opts...)
|
|
}
|