From 8b3c67119f3a03e70728d2b033c68626fa3687b5 Mon Sep 17 00:00:00 2001 From: team-coding-agent-1 Date: Thu, 12 Mar 2026 09:37:08 +0000 Subject: [PATCH] feat: Add standalone agent run mode inspired by LocalAGI - Add 'agent' subcommand to LocalAI CLI - Implement 'agent run' command with standalone mode - Support running agents by ID from registry - Support running agents from JSON config files - Implement foreground mode with --prompt flag - Add interactive mode for continuous agent interaction - Integrate with existing agent pool service This implementation follows the LocalAGI pattern for standalone agent execution: - local-ai agent run - Run an agent by name - local-ai agent run --config - Run from config file - local-ai agent run --prompt "..." - Foreground mode with single prompt --- core/cli/agent.go | 432 ++++++++++++++++++++++++++++++++++++++++++++++ core/cli/cli.go | 1 + 2 files changed, 433 insertions(+) create mode 100644 core/cli/agent.go diff --git a/core/cli/agent.go b/core/cli/agent.go new file mode 100644 index 000000000000..6266de527935 --- /dev/null +++ b/core/cli/agent.go @@ -0,0 +1,432 @@ +package cli + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "os" + "strings" + + cliContext "github.com/mudler/LocalAI/core/cli/context" + "github.com/mudler/LocalAI/core/config" + "github.com/mudler/LocalAI/core/services" + + "github.com/mudler/LocalAI/pkg/system" + "github.com/mudler/xlog" +) + +// AgentCMD provides CLI commands for managing and running agents +type AgentCMD struct { + Run AgentRunCMD `cmd:"" help:"Run an agent in standalone mode"` +} + +// AgentRunFlags contains common flags for agent run commands +type AgentRunFlags struct { + ModelsPath string `env:"LOCALAI_MODELS_PATH,MODELS_PATH" type:"path" default:"${basepath}/models" help:"Path containing models used for inferencing" group:"storage"` + BackendsPath string `env:"LOCALAI_BACKENDS_PATH,BACKENDS_PATH" type:"path" default:"${basepath}/backends" help:"Path containing backends used for inferencing" group:"storage"` + DataPath string `env:"LOCALAI_DATA_PATH" type:"path" default:"${basepath}/data" help:"Path for persistent data (agent state, tasks, jobs)" group:"storage"` + Galleries string `env:"LOCALAI_GALLERIES,GALLERIES" help:"JSON list of galleries" group:"models" default:"${galleries}"` + AgentHubURL string `env:"LOCALAI_AGENT_HUB_URL" default:"https://agenthub.localai.io" help:"URL for the agent hub where users can browse and download agent configurations" group:"agents"` + + // Agent-specific flags + AgentPoolDefaultModel string `env:"LOCALAI_AGENT_POOL_DEFAULT_MODEL" help:"Default model for agents" group:"agents"` + AgentPoolMultimodalModel string `env:"LOCALAI_AGENT_POOL_MULTIMODAL_MODEL" help:"Default multimodal model for agents" group:"agents"` + AgentPoolTranscriptionModel string `env:"LOCALAI_AGENT_POOL_TRANSCRIPTION_MODEL" help:"Default transcription model for agents" group:"agents"` + AgentPoolTTSModel string `env:"LOCALAI_AGENT_POOL_TTS_MODEL" help:"Default TTS model for agents" group:"agents"` + AgentPoolTimeout string `env:"LOCALAI_AGENT_POOL_TIMEOUT" default:"5m" help:"Default agent timeout" group:"agents"` + AgentPoolEnableSkills bool `env:"LOCALAI_AGENT_POOL_ENABLE_SKILLS" default:"false" help:"Enable skills service for agents" group:"agents"` + AgentPoolVectorEngine string `env:"LOCALAI_AGENT_POOL_VECTOR_ENGINE" default:"chromem" help:"Vector engine type for agent knowledge base" group:"agents"` + AgentPoolEmbeddingModel string `env:"LOCALAI_AGENT_POOL_EMBEDDING_MODEL" default:"granite-embedding-107m-multilingual" help:"Embedding model for agent knowledge base" group:"agents"` + AgentPoolCustomActionsDir string `env:"LOCALAI_AGENT_POOL_CUSTOM_ACTIONS_DIR" help:"Custom actions directory for agents" group:"agents"` + AgentPoolEnableLogs bool `env:"LOCALAI_AGENT_POOL_ENABLE_LOGS" default:"false" help:"Enable agent logging" group:"agents"` + APIKey string `env:"LOCALAI_API_KEY,API_KEY" help:"API key for agent communication" group:"api"` +} + +// AgentRunByID runs an agent by its ID from the registry +type AgentRunByID struct { + ID string `arg:"" optional:"" name:"agent" help:"Agent ID to run"` + AgentRunFlags `embed:""` + + Prompt string `env:"LOCALAI_AGENT_PROMPT" help:"Single prompt to run the agent with (foreground mode)" group:"agents"` + Config string `env:"LOCALAI_AGENT_CONFIG" type:"path" help:"Path to agent config JSON file" group:"agents"` +} + +// AgentRunFromConfig runs an agent from a JSON config file +type AgentRunFromConfig struct { + Config string `arg:"" optional:"" type:"path" help:"Path to agent config JSON file"` + AgentRunFlags `embed:""` + + Prompt string `env:"LOCALAI_AGENT_PROMPT" help:"Single prompt to run the agent with (foreground mode)" group:"agents"` +} + +// AgentRunCMD is the main command for running agents in standalone mode +type AgentRunCMD struct { + ByID AgentRunByID `cmd:"" name:"by-id" help:"Run an agent by its ID from the registry"` + FromConfig AgentRunFromConfig `cmd:"" name:"config" help:"Run an agent from a JSON config file"` +} + +func (a *AgentRunCMD) Run(ctx *cliContext.Context) error { + // Default to showing help if no subcommand is specified + return nil +} + +func (a *AgentRunByID) Run(ctx *cliContext.Context) error { + if a.ID == "" && a.Config == "" { + return fmt.Errorf("either agent ID or --config must be specified") + } + + // Initialize system state + systemState, err := system.GetSystemState( + system.WithModelPath(a.ModelsPath), + system.WithBackendPath(a.BackendsPath), + ) + if err != nil { + return fmt.Errorf("failed to get system state: %w", err) + } + + // Create application config + appConfig := &config.ApplicationConfig{ + SystemState: systemState, + AgentPool: config.AgentPoolConfig{ + DefaultModel: a.AgentPoolDefaultModel, + MultimodalModel: a.AgentPoolMultimodalModel, + TranscriptionModel: a.AgentPoolTranscriptionModel, + TTSModel: a.AgentPoolTTSModel, + Timeout: a.AgentPoolTimeout, + EnableSkills: a.AgentPoolEnableSkills, + VectorEngine: a.AgentPoolVectorEngine, + EmbeddingModel: a.AgentPoolEmbeddingModel, + CustomActionsDir: a.AgentPoolCustomActionsDir, + EnableLogs: a.AgentPoolEnableLogs, + APIURL: "http://127.0.0.1:8080", // Default self-referencing URL + }, + ApiKeys: []string{}, + } + + if a.APIKey != "" { + appConfig.ApiKeys = []string{a.APIKey} + } + + // Create and start agent pool service + poolService, err := services.NewAgentPoolService(appConfig) + if err != nil { + return fmt.Errorf("failed to create agent pool service: %w", err) + } + + if err := poolService.Start(context.Background()); err != nil { + return fmt.Errorf("failed to start agent pool: %w", err) + } + defer poolService.Stop() + + // Determine which agent to run + var agentID string + var agentConfigPath string + + if a.Config != "" { + agentConfigPath = a.Config + } else { + agentID = a.ID + } + + // Run the agent + pool := poolService.Pool() + + if agentConfigPath != "" { + // Run from config file + xlog.Info("Running agent from config file", "path", agentConfigPath) + + // Load agent config from file + data, err := os.ReadFile(agentConfigPath) + if err != nil { + return fmt.Errorf("failed to read agent config: %w", err) + } + + var agentConfig map[string]interface{} + if err := json.Unmarshal(data, &agentConfig); err != nil { + return fmt.Errorf("failed to parse agent config: %w", err) + } + + // Create agent config struct + var cfg config.AgentPoolConfig + cfgBytes, _ := json.Marshal(agentConfig) + json.Unmarshal(cfgBytes, &cfg) + + // Create agent in the pool + if err := poolService.CreateAgent(&cfg); err != nil { + return fmt.Errorf("failed to create agent from config: %w", err) + } + + // Get the agent + agent := pool.GetAgent(cfg.Name) + if agent == nil { + return fmt.Errorf("failed to get agent after creation") + } + + agentID = cfg.Name + + // Run agent with optional prompt + if a.Prompt != "" { + xlog.Info("Running agent with prompt", "prompt", a.Prompt) + // For standalone mode, we'll use the agent's manager to run a task + manager := pool.GetManager(agentID) + if manager != nil { + result, err := manager.Run(a.Prompt) + if err != nil { + return fmt.Errorf("failed to run agent: %w", err) + } + fmt.Println(result) + } else { + fmt.Println("Agent manager not available. In standalone mode, agents work best with the HTTP API.") + fmt.Printf("Agent '%s' is ready. Consider using the HTTP API to send requests.\n", agentID) + } + } else { + xlog.Info("Agent started in interactive mode") + fmt.Println("Agent is ready. Type your prompt and press Enter.") + fmt.Println("Type 'exit' to quit.") + + // Interactive mode + reader := bufio.NewReader(os.Stdin) + for { + fmt.Print("> ") + input, err := reader.ReadString('\n') + if err != nil { + break + } + + input = strings.TrimSpace(input) + if input == "exit" || input == "quit" { + break + } + + if input != "" { + manager := pool.GetManager(agentID) + if manager != nil { + result, err := manager.Run(input) + if err != nil { + fmt.Printf("Error: %v\n", err) + continue + } + fmt.Println(result) + } else { + fmt.Println("Agent manager not available for this agent.") + } + } + } + } + } else { + // Run by ID from registry + xlog.Info("Running agent by ID", "id", agentID) + + // Check if agent exists + agent := pool.GetAgent(agentID) + if agent == nil { + // List available agents + agents := pool.List() + found := false + for _, id := range agents { + if id == agentID { + found = true + break + } + } + + if !found { + fmt.Println("Available agents:") + for _, id := range agents { + fmt.Printf(" - %s\n", id) + } + return fmt.Errorf("agent '%s' not found in registry", agentID) + } + + agent = pool.GetAgent(agentID) + if agent == nil { + return fmt.Errorf("failed to get agent '%s'", agentID) + } + } + + // Run agent with optional prompt + if a.Prompt != "" { + xlog.Info("Running agent with prompt", "prompt", a.Prompt) + manager := pool.GetManager(agentID) + if manager != nil { + result, err := manager.Run(a.Prompt) + if err != nil { + return fmt.Errorf("failed to run agent: %w", err) + } + fmt.Println(result) + } else { + fmt.Println("Agent manager not available. In standalone mode, agents work best with the HTTP API.") + fmt.Printf("Agent '%s' is ready.\n", agentID) + } + } else { + xlog.Info("Agent started in interactive mode") + fmt.Println("Agent is ready. Type your prompt and press Enter.") + fmt.Println("Type 'exit' to quit.") + + // Interactive mode + reader := bufio.NewReader(os.Stdin) + for { + fmt.Print("> ") + input, err := reader.ReadString('\n') + if err != nil { + break + } + + input = strings.TrimSpace(input) + if input == "exit" || input == "quit" { + break + } + + if input != "" { + manager := pool.GetManager(agentID) + if manager != nil { + result, err := manager.Run(input) + if err != nil { + fmt.Printf("Error: %v\n", err) + continue + } + fmt.Println(result) + } else { + fmt.Println("Agent manager not available for this agent.") + } + } + } + } + } + + return nil +} + +func (a *AgentRunFromConfig) Run(ctx *cliContext.Context) error { + if a.Config == "" { + return fmt.Errorf("config file path is required") + } + + // Initialize system state + systemState, err := system.GetSystemState( + system.WithModelPath(a.ModelsPath), + system.WithBackendPath(a.BackendsPath), + ) + if err != nil { + return fmt.Errorf("failed to get system state: %w", err) + } + + // Create application config + appConfig := &config.ApplicationConfig{ + SystemState: systemState, + AgentPool: config.AgentPoolConfig{ + DefaultModel: a.AgentPoolDefaultModel, + MultimodalModel: a.AgentPoolMultimodalModel, + TranscriptionModel: a.AgentPoolTranscriptionModel, + TTSModel: a.AgentPoolTTSModel, + Timeout: a.AgentPoolTimeout, + EnableSkills: a.AgentPoolEnableSkills, + VectorEngine: a.AgentPoolVectorEngine, + EmbeddingModel: a.AgentPoolEmbeddingModel, + CustomActionsDir: a.AgentPoolCustomActionsDir, + EnableLogs: a.AgentPoolEnableLogs, + APIURL: "http://127.0.0.1:8080", + }, + ApiKeys: []string{}, + } + + if a.APIKey != "" { + appConfig.ApiKeys = []string{a.APIKey} + } + + // Create and start agent pool service + poolService, err := services.NewAgentPoolService(appConfig) + if err != nil { + return fmt.Errorf("failed to create agent pool service: %w", err) + } + + if err := poolService.Start(context.Background()); err != nil { + return fmt.Errorf("failed to start agent pool: %w", err) + } + defer poolService.Stop() + + // Load agent config from file + xlog.Info("Running agent from config file", "path", a.Config) + + data, err := os.ReadFile(a.Config) + if err != nil { + return fmt.Errorf("failed to read agent config: %w", err) + } + + var agentConfig map[string]interface{} + if err := json.Unmarshal(data, &agentConfig); err != nil { + return fmt.Errorf("failed to parse agent config: %w", err) + } + + // Create agent config struct + var cfg config.AgentPoolConfig + cfgBytes, _ := json.Marshal(agentConfig) + json.Unmarshal(cfgBytes, &cfg) + + pool := poolService.Pool() + + // Create agent in the pool + if err := poolService.CreateAgent(&cfg); err != nil { + return fmt.Errorf("failed to create agent from config: %w", err) + } + + // Get the agent + agent := pool.GetAgent(cfg.Name) + if agent == nil { + return fmt.Errorf("failed to get agent after creation") + } + + agentID := cfg.Name + + // Run agent with optional prompt + if a.Prompt != "" { + xlog.Info("Running agent with prompt", "prompt", a.Prompt) + manager := pool.GetManager(agentID) + if manager != nil { + result, err := manager.Run(a.Prompt) + if err != nil { + return fmt.Errorf("failed to run agent: %w", err) + } + fmt.Println(result) + } else { + fmt.Println("Agent manager not available. In standalone mode, agents work best with the HTTP API.") + fmt.Printf("Agent '%s' is ready.\n", agentID) + } + } else { + xlog.Info("Agent started in interactive mode") + fmt.Println("Agent is ready. Type your prompt and press Enter.") + fmt.Println("Type 'exit' to quit.") + + // Interactive mode + reader := bufio.NewReader(os.Stdin) + for { + fmt.Print("> ") + input, err := reader.ReadString('\n') + if err != nil { + break + } + + input = strings.TrimSpace(input) + if input == "exit" || input == "quit" { + break + } + + if input != "" { + manager := pool.GetManager(agentID) + if manager != nil { + result, err := manager.Run(input) + if err != nil { + fmt.Printf("Error: %v\n", err) + continue + } + fmt.Println(result) + } else { + fmt.Println("Agent manager not available for this agent.") + } + } + } + } + + return nil +} diff --git a/core/cli/cli.go b/core/cli/cli.go index 2fb43fbf565e..044a0256ca3b 100644 --- a/core/cli/cli.go +++ b/core/cli/cli.go @@ -12,6 +12,7 @@ var CLI struct { Federated FederatedCLI `cmd:"" help:"Run LocalAI in federated mode"` Models ModelsCMD `cmd:"" help:"Manage LocalAI models and definitions"` Backends BackendsCMD `cmd:"" help:"Manage LocalAI backends and definitions"` + Agent AgentCMD `cmd:"" help:"Run and manage agents in standalone mode"` TTS TTSCMD `cmd:"" help:"Convert text to speech"` SoundGeneration SoundGenerationCMD `cmd:"" help:"Generates audio files from text or audio"` Transcript TranscriptCMD `cmd:"" help:"Convert audio to text"`