assistant

package
v0.60.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 12, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package assistant is the server half of the AI assistant built into the explorer (task #269): one conversation shape every provider speaks, the three drivers (OpenAI and every server that speaks its Chat Completions API, Anthropic's Messages API, Ollama), and the loop that runs a model's tool calls until it answers.

What the tools ARE is not this package's: the handlers hand the loop a Toolbox that runs filex's own MCP tools as the person who asked (handlers/assistant_tools.go). This package knows nothing about files, permissions or encryption - it moves words and tool calls between a model and that toolbox, within the budget an administrator set.

⚠ The provider's API key is held in memory by the driver and nowhere else here: it is never part of an error, a log line or an answer (redact).

Index

Constants

View Source
const (
	ReasonEnd       = "end"
	ReasonPending   = "pending"
	ReasonRounds    = "rounds"
	ReasonBudget    = "budget"
	ReasonMaxTokens = "max_tokens"
	ReasonRefusal   = "refusal"
	ReasonOther     = "other"
)

Why a run ended, as Result.Reason says it.

View Source
const (
	// StateOn: switched on and set up.
	StateOn = "on"
	// StateOff: switched off.
	StateOff = "off"
	// StateNotConfigured: switched on, but a provider, a model or a key is
	// missing - "the administrator has not set it up".
	StateNotConfigured = "not_configured"
)

The assistant's states, as the explorer and the user settings say them.

Variables

View Source
var (
	BoundsToolRounds    = [2]int{1, 50}
	BoundsOutputTokens  = [2]int{256, 128000}
	BoundsRequestTokens = [2]int{2000, 10000000}
	BoundsTimeout       = [2]int{10, 900}
	BoundsPerMinute     = [2]int{1, 600}
	BoundsRetention     = [2]int{1, 3650}
)

Bounds of the numbers, each [min, max].

View Source
var ErrInvalid = errors.New("invalid assistant setting")

ErrInvalid wraps every refusal of Validate.

Functions

func ArgsFromText

func ArgsFromText(s string) json.RawMessage

ArgsFromText makes the arguments a model wrote storable: valid JSON as it is, `{}` for nothing, and anything else as a JSON string holding the text - a reply cut off mid-call, or a model that wrote broken JSON. The loop refuses arguments that are not an object (ArgsObject) and tells the model; they are kept so the conversation still says what it asked.

func ArgsObject

func ArgsObject(a json.RawMessage) map[string]any

ArgsObject is a call's arguments when they are a JSON object; nil when they are anything else.

func ArgsText

func ArgsText(a json.RawMessage) string

ArgsText is the text a model wrote for a call's arguments (ArgsFromText read back).

func DefaultBaseURL

func DefaultBaseURL(k Kind) string

DefaultBaseURL is the address a provider answers on when the administrator names none.

func SuggestedModels

func SuggestedModels(k Kind) []string

SuggestedModels are the models the settings page offers before the provider has been asked for its list (Models). Only Anthropic's are known ahead: an OpenAI-compatible server or an Ollama install has whatever its operator put on it, so those come from the provider itself.

func ValidKind

func ValidKind(k Kind) bool

ValidKind reports whether k is a provider this build has a driver for.

Types

type Batch

type Batch struct {
	Calls []ToolCall `json:"calls"`
	// Results are the answers so far, in the calls' order: Results[i]
	// answers Calls[i]; the first call without one is the one waiting.
	Results []Part `json:"results"`
}

Batch is the tool calls of one model turn, as far as they were answered: what a conversation keeps while a call waits for the person.

func (*Batch) Waiting

func (b *Batch) Waiting() *ToolCall

Waiting is the call the batch waits on; nil when every call is answered.

type Decision

type Decision string

Decision is the person's answer to a call that waited for it.

const (
	// DecisionNone: not asked, or not answered yet.
	DecisionNone Decision = ""
	// DecisionApprove: go ahead.
	DecisionApprove Decision = "approve"
	// DecisionDeny: do not.
	DecisionDeny Decision = "deny"
)

type Invalid

type Invalid struct {
	Field string
	Min   int
	Max   int
}

Invalid is one refused field: what the settings page puts beside it.

func (*Invalid) Error

func (e *Invalid) Error() string

func (*Invalid) Unwrap

func (e *Invalid) Unwrap() error

type Kind

type Kind string

Kind names a provider.

const (
	// KindOpenAI is OpenAI's Chat Completions API - and every server that
	// speaks it: OpenRouter, vLLM, LM Studio, a company gateway.
	KindOpenAI Kind = "openai"
	// KindAnthropic is Anthropic's Messages API (Claude).
	KindAnthropic Kind = "anthropic"
	// KindOllama is a local Ollama server's own chat API.
	KindOllama Kind = "ollama"
)

func Kinds

func Kinds() []Kind

Kinds is every provider, in the order the settings page offers them.

type Limiter

type Limiter struct {

	// Now is the clock (time.Now when nil).
	Now func() time.Time
	// contains filtered or unexported fields
}

Limiter counts the requests each person started in the last minute.

func (*Limiter) Allow

func (l *Limiter) Allow(key int64, perMinute int) (ok bool, retry time.Duration)

Allow records a request of key and reports whether it is within perMinute; when it is not, retry is how long until it would be.

type Loop

type Loop struct {
	Provider Provider
	Model    string
	System   string
	Tools    Toolbox
	// MaxRounds is how many batches of tool calls one run may answer and go
	// back to the model with.
	MaxRounds int
	// MaxOutputTokens is one reply's limit; Budget the whole run's, input and
	// output of every call together.
	MaxOutputTokens int
	Budget          int
	// OnText receives the reply's text as it streams (nil: dropped).
	OnText func(string)
	// OnMessage receives every message the run appends, as it appends it -
	// before the toolbox runs that message's calls (nil: none).
	OnMessage func(Message)
}

Loop runs a conversation with a model and its tools, within a budget.

func (*Loop) Run

func (l *Loop) Run(ctx context.Context, history []Message, resume *Batch) (Result, error)

Run goes on with the conversation history: answers resume's calls first when a batch was waiting (the waiting call carries the person's Decision), then asks the model until it answers, waits for the person, or reaches a limit. A provider failure ends the run with the error and what was done before it.

type Message

type Message struct {
	Role  Role   `json:"role"`
	Parts []Part `json:"parts"`
}

Message is one turn of the conversation.

func (Message) Text

func (m Message) Text() string

Text is the message's words, its text parts joined.

func (Message) ToolCalls

func (m Message) ToolCalls() []ToolCall

ToolCalls are the message's tool calls, in order.

type Part

type Part struct {
	Kind     PartKind        `json:"kind"`
	Text     string          `json:"text,omitempty"`
	CallID   string          `json:"call_id,omitempty"`
	Tool     string          `json:"tool,omitempty"`
	Args     json.RawMessage `json:"args,omitempty"`
	IsError  bool            `json:"is_error,omitempty"`
	Provider string          `json:"provider,omitempty"`
	Raw      json.RawMessage `json:"raw,omitempty"`
	// Context marks a text part the server wrote into the person's message:
	// what they had selected (its paths in Args). Sent to the model like any
	// text; the panel shows it as the selection, not as their words.
	Context bool `json:"context,omitempty"`
}

Part is one piece of a message.

type PartKind

type PartKind string

PartKind is what one piece of a message is.

const (
	// PartText is words.
	PartText PartKind = "text"
	// PartToolCall is the model asking for a tool: CallID, Tool, Args.
	PartToolCall PartKind = "tool_call"
	// PartToolResult is a tool's answer to the call CallID: Text, IsError.
	PartToolResult PartKind = "tool_result"
	// PartOpaque is a block a provider asked to be sent back exactly as it
	// came (Anthropic's thinking blocks, with their signatures): Raw, and the
	// Provider it is for. Every other provider leaves it out.
	PartOpaque PartKind = "opaque"
)

type Provider

type Provider interface {
	// Kind is the provider's name: KindOpenAI, KindAnthropic, KindOllama.
	Kind() Kind
	// Chat sends req and streams the reply: onText is called with every
	// piece of text as it arrives (from this goroutine; never nil-checked by
	// the caller - pass a no-op).
	Chat(ctx context.Context, req Request, onText func(string)) (*Turn, error)
	// Models lists the models the address offers, for the settings page.
	Models(ctx context.Context) ([]string, error)
}

Provider is one model API.

func New

func New(s Settings, key string, client *http.Client) (Provider, error)

New builds the driver for s. key is the API key (empty for none); client is the HTTP client to use (nil: one with s's timeout).

type ProviderError

type ProviderError struct {
	Provider Kind
	// Status is the HTTP status the provider answered; 0 when it never
	// answered (the address did not answer, the stream broke).
	Status int
	// Message is the provider's own words (English, as it sent them).
	Message string
}

ProviderError is a provider's refusal or failure, said without its key.

func (*ProviderError) Error

func (e *ProviderError) Error() string

func (*ProviderError) Unauthorized

func (e *ProviderError) Unauthorized() bool

Unauthorized reports whether the provider refused the key.

type Request

type Request struct {
	Model     string
	System    string
	Messages  []Message
	Tools     []ToolDef
	MaxTokens int
}

Request is one call to a model.

type Result

type Result struct {
	// Added are the messages the run appended to the conversation: the
	// model's turns and the tool answers, in order.
	Added []Message
	// Pending is the batch that waits for the person (Reason "pending").
	Pending *Batch
	Reason  string
	Usage   Usage
	// Model is the model that answered last.
	Model string
	// RefusalCategory is the provider's reason for a refusal, when it gave
	// one.
	RefusalCategory string
}

Result is what one run of the loop did.

type Role

type Role string

Role is who said a message.

const (
	// RoleUser is the person - and the tool results the server sends back
	// for them, which every API files under the asking side.
	RoleUser Role = "user"
	// RoleAssistant is the model.
	RoleAssistant Role = "assistant"
)

type Settings

type Settings struct {
	// Enabled turns the assistant on. Off by default: nothing leaves the
	// server for a model until an administrator says so.
	Enabled bool `json:"enabled"`
	// HideButton keeps the assistant's button out of the explorer's toolbar
	// for everybody, whatever else is set - the switch for an installation
	// that wants no trace of it.
	HideButton bool   `json:"hide_button"`
	Provider   Kind   `json:"provider"`
	BaseURL    string `json:"base_url"`
	Model      string `json:"model"`
	// MaxToolRounds is how many times one request may go back to the model
	// with tool answers before it is stopped.
	MaxToolRounds int `json:"max_tool_rounds"`
	// MaxOutputTokens is the longest single reply the model may write.
	MaxOutputTokens int `json:"max_output_tokens"`
	// MaxRequestTokens is one request's budget: input and output of every
	// round together. Reaching it stops the request.
	MaxRequestTokens int `json:"max_request_tokens"`
	// TimeoutSeconds is how long one call to the provider may take, retries
	// included.
	TimeoutSeconds int `json:"timeout_seconds"`
	// RequestsPerMinute is how many requests one person may start a minute.
	RequestsPerMinute int `json:"requests_per_minute"`
	// RetentionDays is how long a conversation is kept after it was last
	// used.
	RetentionDays int `json:"retention_days"`
}

Settings is what an administrator sets on Admin → Settings → AI assistant. The API key is not here: it is kept sealed beside it (handlers assistant_settings.go) and handed to New on its own.

func Defaults

func Defaults() Settings

Defaults is a fresh install's settings: off, nothing chosen.

func (Settings) EffectiveBaseURL

func (s Settings) EffectiveBaseURL() string

EffectiveBaseURL is the address the driver calls: the administrator's, or the provider's own.

func (Settings) NeedsKey

func (s Settings) NeedsKey() bool

NeedsKey reports whether this provider at this address cannot work without an API key: Anthropic always, OpenAI on OpenAI's own address. An OpenAI-compatible server of one's own (vLLM, LM Studio) and Ollama may run without one.

func (Settings) Normalized

func (s Settings) Normalized() Settings

Normalized fills the numbers a stored document left out (a row written by an older version) with the defaults, and trims the strings.

func (Settings) State

func (s Settings) State(hasKey bool) string

State is the assistant's state given whether a key is stored.

func (Settings) Timeout

func (s Settings) Timeout() time.Duration

Timeout is TimeoutSeconds as a duration.

func (Settings) Validate

func (s Settings) Validate() error

Validate refuses a setting out of its bounds, a provider this build does not know and an address that is not http(s). An incomplete one (no model yet) is valid: it is State that says it is not ready.

type Stop

type Stop string

Stop is why a model stopped.

const (
	// StopEnd: it answered.
	StopEnd Stop = "end"
	// StopToolUse: it asked for tools and waits for their answers.
	StopToolUse Stop = "tool_use"
	// StopMaxTokens: the reply hit the output limit (MaxTokens).
	StopMaxTokens Stop = "max_tokens"
	// StopRefusal: the provider declined the request (a safety classifier,
	// a content filter).
	StopRefusal Stop = "refusal"
	// StopOther: anything the drivers do not map.
	StopOther Stop = "other"
)

type ToolCall

type ToolCall struct {
	ID       string          `json:"id"`
	Name     string          `json:"name"`
	Args     json.RawMessage `json:"args"`
	Decision Decision        `json:"decision,omitempty"`
}

ToolCall is one call the loop hands the toolbox.

type ToolDef

type ToolDef struct {
	Name        string
	Description string
	Schema      json.RawMessage
}

ToolDef is one tool as a model is told about it: its name, what it does and the JSON schema of its arguments.

type ToolOutcome

type ToolOutcome struct {
	// Text is what the model is told: the tool's answer, or why it did not
	// run.
	Text    string
	IsError bool
	// Pending: the call waits for the person (it cannot be undone and they
	// have not answered). The loop stops here; Text is not used.
	Pending bool
}

ToolOutcome is what the toolbox made of a call.

type Toolbox

type Toolbox interface {
	// Defs are the tools the model is offered - the same list, in the same
	// order, for every call of a conversation (a provider ties what it
	// cached, and Anthropic its thinking blocks, to the tools).
	Defs() []ToolDef
	// Run runs one call (or decides it must wait for the person).
	Run(ctx context.Context, call ToolCall) ToolOutcome
}

Toolbox runs the tools a model calls.

type Turn

type Turn struct {
	// Parts is the model's message: text, tool calls and the blocks its
	// provider wants back (PartOpaque), in the order they came.
	Parts []Part
	Stop  Stop
	Usage Usage
	// Model is the model that answered (a provider may name the exact
	// version, or the model it fell back to).
	Model string
	// RefusalCategory is the provider's word for why it declined, when it
	// said one.
	RefusalCategory string
}

Turn is what one call to a model answered.

type Usage

type Usage struct {
	InputTokens  int `json:"input_tokens"`
	OutputTokens int `json:"output_tokens"`
}

Usage is what a call (or a run of calls) cost, in tokens.

func (Usage) Add

func (u Usage) Add(o Usage) Usage

Add sums two usages.

func (Usage) Total

func (u Usage) Total() int

Total is input and output together.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL