mirror of
https://github.com/mudler/LocalAGI.git
synced 2026-07-23 18:55:52 -04:00
9438d39f70
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
618 lines
15 KiB
Go
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
|
|
}
|
|
}
|