Files
Ettore Di Giacinto 9438d39f70 feat: support streaming mode for tool calls
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-03-15 22:33:51 +00:00

618 lines
15 KiB
Go

package agent
import (
"context"
"strings"
"time"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/cogito"
)
type Option func(*options) error
// ConversationStorageMode defines how conversations are stored in the knowledge base
type ConversationStorageMode string
const (
// StoreUserOnly stores only user messages (default)
StoreUserOnly ConversationStorageMode = "user_only"
// StoreUserAndAssistant stores both user and assistant messages separately
StoreUserAndAssistant ConversationStorageMode = "user_and_assistant"
// StoreWholeConversation stores the entire conversation as a single block
StoreWholeConversation ConversationStorageMode = "whole_conversation"
)
type llmOptions struct {
APIURL string
APIKey string
Model string
MultimodalModel string
ReviewerModel string
TranscriptionModel string
TranscriptionLanguage string
TTSModel string
}
type options struct {
LLMAPI llmOptions
character Character
randomIdentityGuidance string
randomIdentity bool
userActions types.Actions
jobFilters types.JobFilters
enableHUD, standaloneJob, showCharacter, enableKB, enableSummaryMemory, enableLongTermMemory bool
stripThinkingTags bool
kbAutoSearch bool
conversationStorageMode ConversationStorageMode
canStopItself bool
initiateConversations bool
forceReasoning bool
forceReasoningTool bool
enableGuidedTools bool
canPlan bool
disableSinkState bool
characterfile string
statefile string
schedulerStorePath string // Path to scheduler JSON storage file
context context.Context
permanentGoal string
timeout string
periodicRuns time.Duration
schedulerPollInterval time.Duration
kbResults int
ragdb RAGDB
// Evaluation settings
maxEvaluationLoops int
loopDetection int
enableEvaluation bool
prompts []DynamicPrompt
systemPrompt string
innerMonologueTemplate string
skillPromptTemplate string
schedulerTaskTemplate string
// callbacks
reasoningCallback func(types.ActionCurrentState) bool
resultCallback func(types.ActionState)
conversationsPath string
mcpServers []MCPServer
mcpStdioServers []MCPSTDIOServer
mcpPrepareScript string
extraMCPSessions []*mcp.ClientSession
newConversationsSubscribers []func(*types.ConversationMessage)
observer Observer
enableAutoCompaction bool
autoCompactionThreshold int
parallelJobs int
lastMessageDuration time.Duration
// cancelPreviousOnNewMessage: when true (or nil), Enqueue cancels the running job for the same conversation_id. When false, jobs are queued.
cancelPreviousOnNewMessage *bool
// maxAttempts: on ExecuteTools failure, retry up to this many times before surfacing the error to the user (1 = no retries).
maxAttempts int
// streamCallback receives streaming events from cogito during final answer generation.
streamCallback func(cogito.StreamEvent)
}
func (o *options) SeparatedMultimodalModel() bool {
return o.LLMAPI.MultimodalModel != "" && o.LLMAPI.Model != o.LLMAPI.MultimodalModel
}
func defaultOptions() *options {
return &options{
parallelJobs: 1,
maxAttempts: 1,
periodicRuns: 15 * time.Minute,
schedulerPollInterval: 30 * time.Second,
maxEvaluationLoops: 2,
enableEvaluation: false,
kbAutoSearch: true, // Default to true to maintain backward compatibility
conversationStorageMode: StoreUserOnly, // Default to user-only for backward compatibility
LLMAPI: llmOptions{
APIURL: "http://localhost:8080",
Model: "gpt-4",
TranscriptionModel: "whisper-1",
TranscriptionLanguage: "",
TTSModel: "tts-1",
},
character: Character{
Name: "",
Age: "",
Occupation: "",
Hobbies: []string{},
MusicTaste: []string{},
},
}
}
func newOptions(opts ...Option) (*options, error) {
options := defaultOptions()
for _, o := range opts {
if err := o(options); err != nil {
return nil, err
}
}
return options, nil
}
var EnableHUD = func(o *options) error {
o.enableHUD = true
return nil
}
var EnableForceReasoning = func(o *options) error {
o.forceReasoning = true
return nil
}
var EnableGuidedTools = func(o *options) error {
o.enableGuidedTools = true
return nil
}
var EnableForceReasoningTool = func(o *options) error {
o.forceReasoningTool = true
return nil
}
var EnableKnowledgeBase = func(o *options) error {
o.enableKB = true
o.kbResults = 5
return nil
}
var CanStopItself = func(o *options) error {
o.canStopItself = true
return nil
}
func WithTimeout(timeout string) Option {
return func(o *options) error {
o.timeout = timeout
return nil
}
}
func WithConversationsPath(path string) Option {
return func(o *options) error {
o.conversationsPath = path
return nil
}
}
func EnableKnowledgeBaseWithResults(results int) Option {
return func(o *options) error {
o.enableKB = true
o.kbResults = results
return nil
}
}
func WithLastMessageDuration(duration string) Option {
return func(o *options) error {
d, err := time.ParseDuration(duration)
if err != nil {
d = types.DefaultLastMessageDuration
}
o.lastMessageDuration = d
return nil
}
}
func WithParallelJobs(jobs int) Option {
return func(o *options) error {
o.parallelJobs = jobs
return nil
}
}
// WithCancelPreviousOnNewMessage sets whether a new job with the same conversation_id cancels the currently running job (true) or is queued (false). Nil/default means true.
func WithCancelPreviousOnNewMessage(cancel bool) Option {
return func(o *options) error {
o.cancelPreviousOnNewMessage = &cancel
return nil
}
}
// WithMaxAttempts sets how many times to attempt execution on failure before surfacing the error to the user (1 = no retries).
func WithMaxAttempts(attempts int) Option {
return func(o *options) error {
o.maxAttempts = attempts
return nil
}
}
func WithLoopDetection(loops int) Option {
return func(o *options) error {
o.loopDetection = loops
return nil
}
}
func WithNewConversationSubscriber(sub func(*types.ConversationMessage)) Option {
return func(o *options) error {
o.newConversationsSubscribers = append(o.newConversationsSubscribers, sub)
return nil
}
}
var EnableInitiateConversations = func(o *options) error {
o.initiateConversations = true
return nil
}
var EnablePlanning = func(o *options) error {
o.canPlan = true
return nil
}
var DisableSinkState = func(o *options) error {
o.disableSinkState = true
return nil
}
var WithPlanReviewerLLM = func(model string) Option {
return func(o *options) error {
o.LLMAPI.ReviewerModel = model
return nil
}
}
// EnableStandaloneJob is an option to enable the agent
// to run jobs in the background automatically
var EnableStandaloneJob = func(o *options) error {
o.standaloneJob = true
return nil
}
var EnablePersonality = func(o *options) error {
o.showCharacter = true
return nil
}
var EnableSummaryMemory = func(o *options) error {
o.enableSummaryMemory = true
return nil
}
var EnableLongTermMemory = func(o *options) error {
o.enableLongTermMemory = true
return nil
}
func WithRAGDB(db RAGDB) Option {
return func(o *options) error {
o.ragdb = db
return nil
}
}
// WithConversationStorageMode sets how conversations are stored in the knowledge base
func WithConversationStorageMode(mode ConversationStorageMode) Option {
return func(o *options) error {
switch mode {
case StoreUserOnly, StoreUserAndAssistant, StoreWholeConversation:
o.conversationStorageMode = mode
default:
o.conversationStorageMode = StoreUserOnly
}
return nil
}
}
func WithSystemPrompt(prompt string) Option {
return func(o *options) error {
o.systemPrompt = prompt
return nil
}
}
// WithInnerMonologueTemplate sets the prompt used for periodic/standalone runs. If empty, the default template is used.
func WithInnerMonologueTemplate(template string) Option {
return func(o *options) error {
o.innerMonologueTemplate = template
return nil
}
}
// WithSkillPromptTemplate sets the template for rendering skills in the prompt. If empty, the default template is used.
func WithSkillPromptTemplate(template string) Option {
return func(o *options) error {
o.skillPromptTemplate = template
return nil
}
}
func WithMCPServers(servers ...MCPServer) Option {
return func(o *options) error {
o.mcpServers = servers
return nil
}
}
func WithMCPSTDIOServers(servers ...MCPSTDIOServer) Option {
return func(o *options) error {
o.mcpStdioServers = servers
return nil
}
}
func WithMCPPrepareScript(script string) Option {
return func(o *options) error {
o.mcpPrepareScript = script
return nil
}
}
func WithLLMAPIURL(url string) Option {
return func(o *options) error {
o.LLMAPI.APIURL = url
return nil
}
}
func WithStateFile(path string) Option {
return func(o *options) error {
o.statefile = path
return nil
}
}
func WithCharacterFile(path string) Option {
return func(o *options) error {
o.characterfile = path
return nil
}
}
// WithPrompts adds additional block prompts to the agent
// to be rendered internally in the conversation
// when processing the conversation to the LLM
func WithPrompts(prompts ...DynamicPrompt) Option {
return func(o *options) error {
o.prompts = prompts
return nil
}
}
// WithMCPSession adds a pre-connected MCP client session (e.g. in-process skills MCP) to the agent.
func WithMCPSession(session *mcp.ClientSession) Option {
return func(o *options) error {
o.extraMCPSessions = append(o.extraMCPSessions, session)
return nil
}
}
// WithDynamicPrompts is a helper function to create dynamic prompts
// Dynamic prompts contains golang code which is executed dynamically
// // to render a prompt to the LLM
// func WithDynamicPrompts(prompts ...map[string]string) Option {
// return func(o *options) error {
// for _, p := range prompts {
// prompt, err := NewDynamicPrompt(p, "")
// if err != nil {
// return err
// }
// o.prompts = append(o.prompts, prompt)
// }
// return nil
// }
// }
func WithLLMAPIKey(key string) Option {
return func(o *options) error {
o.LLMAPI.APIKey = key
return nil
}
}
func WithMultimodalModel(model string) Option {
return func(o *options) error {
o.LLMAPI.MultimodalModel = model
return nil
}
}
func WithPermanentGoal(goal string) Option {
return func(o *options) error {
o.permanentGoal = goal
return nil
}
}
func WithPeriodicRuns(duration string) Option {
return func(o *options) error {
t, err := time.ParseDuration(duration)
if err != nil {
o.periodicRuns, _ = time.ParseDuration("10m")
}
o.periodicRuns = t
return nil
}
}
func WithSchedulerPollInterval(duration string) Option {
return func(o *options) error {
t, err := time.ParseDuration(duration)
if err != nil {
o.schedulerPollInterval = 30 * time.Second
return nil
}
o.schedulerPollInterval = t
return nil
}
}
func WithContext(ctx context.Context) Option {
return func(o *options) error {
o.context = ctx
return nil
}
}
func WithAgentReasoningCallback(cb func(types.ActionCurrentState) bool) Option {
return func(o *options) error {
o.reasoningCallback = cb
return nil
}
}
func WithAgentResultCallback(cb func(types.ActionState)) Option {
return func(o *options) error {
o.resultCallback = cb
return nil
}
}
func WithModel(model string) Option {
return func(o *options) error {
o.LLMAPI.Model = model
return nil
}
}
func WithCharacter(c Character) Option {
return func(o *options) error {
o.character = c
return nil
}
}
func FromFile(path string) Option {
return func(o *options) error {
c, err := Load(path)
if err != nil {
return err
}
o.character = *c
return nil
}
}
func WithRandomIdentity(guidance ...string) Option {
return func(o *options) error {
o.randomIdentityGuidance = strings.Join(guidance, "")
o.randomIdentity = true
o.showCharacter = true
return nil
}
}
func WithActions(actions ...types.Action) Option {
return func(o *options) error {
o.userActions = append(o.userActions, actions...)
return nil
}
}
func WithJobFilters(filters ...types.JobFilter) Option {
return func(o *options) error {
o.jobFilters = filters
return nil
}
}
func WithObserver(observer Observer) Option {
return func(o *options) error {
o.observer = observer
return nil
}
}
var EnableStripThinkingTags = func(o *options) error {
o.stripThinkingTags = true
return nil
}
func WithMaxEvaluationLoops(loops int) Option {
return func(o *options) error {
o.maxEvaluationLoops = loops
return nil
}
}
func EnableEvaluation() Option {
return func(o *options) error {
o.enableEvaluation = true
return nil
}
}
func WithTranscriptionModel(model string) Option {
return func(o *options) error {
o.LLMAPI.TranscriptionModel = model
return nil
}
}
func WithTranscriptionLanguage(language string) Option {
return func(o *options) error {
o.LLMAPI.TranscriptionLanguage = language
return nil
}
}
func WithTTSModel(model string) Option {
return func(o *options) error {
o.LLMAPI.TTSModel = model
return nil
}
}
func WithKBAutoSearch(enabled bool) Option {
return func(o *options) error {
o.kbAutoSearch = enabled
return nil
}
}
// WithSchedulerStorePath sets the path for the scheduler's JSON storage file
func WithSchedulerStorePath(path string) Option {
return func(o *options) error {
o.schedulerStorePath = path
return nil
}
}
// WithSchedulerTaskTemplate sets the prompt used for scheduled/recurring tasks run by the scheduler.
// If empty, the default inner monologue template is used with the task injected.
func WithSchedulerTaskTemplate(template string) Option {
return func(o *options) error {
o.schedulerTaskTemplate = template
return nil
}
}
var EnableAutoCompaction = func(o *options) error {
o.enableAutoCompaction = true
return nil
}
func WithAutoCompactionThreshold(threshold int) Option {
return func(o *options) error {
o.autoCompactionThreshold = threshold
return nil
}
}
// WithStreamCallback sets a callback to receive streaming events from cogito
// during final answer generation. This enables live token-by-token delivery.
func WithStreamCallback(fn func(cogito.StreamEvent)) Option {
return func(o *options) error {
o.streamCallback = fn
return nil
}
}