mirror of
https://github.com/mudler/LocalAGI.git
synced 2026-07-23 10:45:41 -04:00
* feat: implement 'agent run' CLI command (#448) - Add 'local-agi agent run' command supporting agent name or config file - Support 'local-agi agent run <name>' to run agent from registry (pool.json) - Support 'local-agi agent run --config <file.json>' to run from JSON config - Extract web server into 'local-agi serve' subcommand - Implement agent name lookup from registry - Add JSON config file parsing and validation - Create standalone agent execution logic - Add proper error handling for invalid inputs - Reuse existing service factories (actions, connectors, filters, skills) - Environment variable fallback for config values - No web server - agent runs in foreground with clean SIGINT/SIGTERM handling References: https://github.com/mudler/LocalAGI/issues/448 * refactor(cmd): use pool to start agent instead of duplicating logic Replace ~220 lines of duplicated agent initialization code in startStandaloneAgent() with a call to pool.StartAgentStandalone(). The new pool method delegates to the existing startAgentWithConfig(), eliminating code duplication between the CLI and the pool. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add default help command to root * fix: default to serve command when no subcommand provided The e2e tests fail because when the container starts with no arguments, the root command shows help instead of starting the web server. Changed the root command to call serveCmd.RunE() directly when no subcommand is provided, ensuring the web server starts by default. This fixes the CI e2e test failure where the web server wasn't starting. * fix: add serve subcommand to Dockerfile ENTRYPOINT * refactor: remove unused GetRAGProvider function * refactor: centralize env var setup in cmd/env.go --------- Co-authored-by: localai-bot <localai-bot@noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -61,4 +61,4 @@ RUN apt-get update && apt-get install -y \
|
||||
COPY --from=builder /work/localagi /localagi
|
||||
|
||||
# Define the command that will be run when the container is started
|
||||
ENTRYPOINT ["/localagi"]
|
||||
ENTRYPOINT ["/localagi", "serve"]
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var agentCmd = &cobra.Command{
|
||||
Use: "agent",
|
||||
Short: "Manage agents",
|
||||
Long: "Commands for managing and running LocalAGI agents.",
|
||||
}
|
||||
|
||||
func init() {
|
||||
agentCmd.AddCommand(agentRunCmd)
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/state"
|
||||
"github.com/mudler/LocalAGI/services"
|
||||
"github.com/mudler/LocalAGI/services/skills"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
configFile string
|
||||
)
|
||||
|
||||
var agentRunCmd = &cobra.Command{
|
||||
Use: "run [agent_name]",
|
||||
Short: "Run an agent standalone",
|
||||
Long: `Run an agent without starting the web server.
|
||||
|
||||
Two modes are supported:
|
||||
1. Run an agent by name from the registry (pool.json):
|
||||
local-agi agent run my-agent
|
||||
|
||||
2. Run an agent from a JSON config file:
|
||||
local-agi agent run --config agent.json
|
||||
|
||||
The agent runs in the foreground until interrupted (Ctrl+C).`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: runAgent,
|
||||
}
|
||||
|
||||
func init() {
|
||||
agentRunCmd.Flags().StringVarP(&configFile, "config", "c", "", "path to agent JSON config file")
|
||||
}
|
||||
|
||||
func runAgent(cmd *cobra.Command, args []string) error {
|
||||
agentName, agentConfig, err := resolveAgentConfig(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return startStandaloneAgent(agentName, agentConfig)
|
||||
}
|
||||
|
||||
// resolveAgentConfig determines the agent name and config from either
|
||||
// a registry lookup or a JSON config file.
|
||||
func resolveAgentConfig(args []string) (string, *state.AgentConfig, error) {
|
||||
if configFile != "" && len(args) > 0 {
|
||||
return "", nil, fmt.Errorf("cannot specify both --config and agent name; use one or the other")
|
||||
}
|
||||
|
||||
if configFile == "" && len(args) == 0 {
|
||||
return "", nil, fmt.Errorf("either an agent name or --config <file> is required")
|
||||
}
|
||||
|
||||
if configFile != "" {
|
||||
return loadConfigFromFile(configFile)
|
||||
}
|
||||
|
||||
return loadConfigFromRegistry(args[0])
|
||||
}
|
||||
|
||||
// loadConfigFromFile reads and validates an agent config from a JSON file.
|
||||
func loadConfigFromFile(path string) (string, *state.AgentConfig, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to read config file %q: %w", path, err)
|
||||
}
|
||||
|
||||
var config state.AgentConfig
|
||||
if err := json.Unmarshal(data, &config); err != nil {
|
||||
return "", nil, fmt.Errorf("failed to parse config file %q: %w", path, err)
|
||||
}
|
||||
|
||||
if err := validateConfig(&config); err != nil {
|
||||
return "", nil, fmt.Errorf("invalid config in %q: %w", path, err)
|
||||
}
|
||||
|
||||
name := config.Name
|
||||
if name == "" {
|
||||
// Derive name from filename
|
||||
base := filepath.Base(path)
|
||||
name = base[:len(base)-len(filepath.Ext(base))]
|
||||
config.Name = name
|
||||
}
|
||||
|
||||
return name, &config, nil
|
||||
}
|
||||
|
||||
// loadConfigFromRegistry loads an agent config from the pool.json registry.
|
||||
func loadConfigFromRegistry(name string) (string, *state.AgentConfig, error) {
|
||||
stateDir := os.Getenv("LOCALAGI_STATE_DIR")
|
||||
if stateDir == "" {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to get working directory: %w", err)
|
||||
}
|
||||
stateDir = filepath.Join(cwd, "pool")
|
||||
}
|
||||
|
||||
poolFile := filepath.Join(stateDir, "pool.json")
|
||||
data, err := os.ReadFile(poolFile)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to read pool file %q: %w\nEnsure LOCALAGI_STATE_DIR is set or a pool/ directory exists", poolFile, err)
|
||||
}
|
||||
|
||||
var pool map[string]state.AgentConfig
|
||||
if err := json.Unmarshal(data, &pool); err != nil {
|
||||
return "", nil, fmt.Errorf("failed to parse pool file %q: %w", poolFile, err)
|
||||
}
|
||||
|
||||
config, exists := pool[name]
|
||||
if !exists {
|
||||
available := make([]string, 0, len(pool))
|
||||
for k := range pool {
|
||||
available = append(available, k)
|
||||
}
|
||||
return "", nil, fmt.Errorf("agent %q not found in registry\nAvailable agents: %v", name, available)
|
||||
}
|
||||
|
||||
return name, &config, nil
|
||||
}
|
||||
|
||||
// validateConfig checks that required fields are present in the config.
|
||||
func validateConfig(config *state.AgentConfig) error {
|
||||
// Model and API URL can come from env vars, so they're not strictly required in config.
|
||||
// But we validate that the config is at least parseable (already done by JSON unmarshal).
|
||||
return nil
|
||||
}
|
||||
|
||||
// startStandaloneAgent creates and runs a single agent using the pool,
|
||||
// without starting the web server.
|
||||
func startStandaloneAgent(name string, config *state.AgentConfig) error {
|
||||
// Load all environment variables
|
||||
env := LoadEnv()
|
||||
|
||||
if env.Model == "" {
|
||||
env.Model = config.Model
|
||||
}
|
||||
if env.LLMAPIURL == "" {
|
||||
env.LLMAPIURL = config.APIURL
|
||||
}
|
||||
if env.LLMAPIKey == "" {
|
||||
env.LLMAPIKey = config.APIKey
|
||||
}
|
||||
|
||||
if env.Model == "" {
|
||||
return fmt.Errorf("model not set: provide 'model' in config or set LOCALAGI_MODEL")
|
||||
}
|
||||
if env.LLMAPIURL == "" {
|
||||
return fmt.Errorf("API URL not set: provide 'api_url' in config or set LOCALAGI_LLM_API_URL")
|
||||
}
|
||||
|
||||
if env.StateDir == "" {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get working directory: %w", err)
|
||||
}
|
||||
env.StateDir = filepath.Join(cwd, "pool")
|
||||
}
|
||||
os.MkdirAll(env.StateDir, 0755)
|
||||
|
||||
// Override config with resolved values
|
||||
config.Model = env.Model
|
||||
config.APIURL = env.LLMAPIURL
|
||||
config.APIKey = env.LLMAPIKey
|
||||
config.MultimodalModel = env.MultimodalModel
|
||||
config.TranscriptionModel = env.TranscriptionModel
|
||||
config.TranscriptionLanguage = env.TranscriptionLanguage
|
||||
config.TTSModel = env.TTSModel
|
||||
|
||||
if config.PeriodicRuns == "" {
|
||||
config.PeriodicRuns = "10m"
|
||||
}
|
||||
if config.SchedulerPollInterval == "" {
|
||||
config.SchedulerPollInterval = "30s"
|
||||
}
|
||||
|
||||
// Initialize skills service
|
||||
skillsService, err := skills.NewService(env.StateDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize skills service: %w", err)
|
||||
}
|
||||
|
||||
// Build service factories
|
||||
actionsFactory := services.Actions(map[string]string{
|
||||
services.ActionConfigSSHBoxURL: env.SSHBoxURL,
|
||||
services.ConfigStateDir: env.StateDir,
|
||||
services.CustomActionsDir: env.CustomActionsDir,
|
||||
})
|
||||
dynamicPromptsFactory := services.DynamicPrompts(map[string]string{
|
||||
services.ConfigStateDir: env.StateDir,
|
||||
services.CustomActionsDir: env.CustomActionsDir,
|
||||
})
|
||||
|
||||
// Create the pool and use it to start the agent
|
||||
pool, err := state.NewAgentPool(
|
||||
env.Model, env.MultimodalModel, env.TranscriptionModel, env.TranscriptionLanguage, env.TTSModel,
|
||||
env.LLMAPIURL, env.LLMAPIKey, env.StateDir,
|
||||
actionsFactory, services.Connectors, dynamicPromptsFactory, services.Filters,
|
||||
env.Timeout, false, skillsService,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create agent pool: %w", err)
|
||||
}
|
||||
|
||||
if env.LocalRAGURL != "" {
|
||||
pool.SetRAGProvider(state.NewHTTPRAGProvider(env.LocalRAGURL, env.LLMAPIKey))
|
||||
}
|
||||
|
||||
// Start the agent via the pool (handles all option building, connectors, etc.)
|
||||
if err := pool.StartAgentStandalone(name, config); err != nil {
|
||||
return fmt.Errorf("failed to start agent: %w", err)
|
||||
}
|
||||
|
||||
a := pool.GetAgent(name)
|
||||
if a == nil {
|
||||
return fmt.Errorf("agent %q was not found after starting", name)
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "Starting agent %q (model: %s, api: %s)\n", name, env.Model, env.LLMAPIURL)
|
||||
fmt.Fprintf(os.Stderr, "Press Ctrl+C to stop\n")
|
||||
|
||||
// Wait for interrupt
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
sig := <-sigCh
|
||||
fmt.Fprintf(os.Stderr, "\nReceived %v, stopping agent...\n", sig)
|
||||
pool.Stop(name)
|
||||
|
||||
// Give agent a moment to clean up
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
return nil
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Env contains all environment variables used by LocalAGI
|
||||
type Env struct {
|
||||
// Model and API configuration
|
||||
Model string
|
||||
LLMAPIURL string
|
||||
LLMAPIKey string
|
||||
MultimodalModel string
|
||||
TranscriptionModel string
|
||||
TranscriptionLanguage string
|
||||
TTSModel string
|
||||
Timeout string
|
||||
|
||||
// Directories and paths
|
||||
StateDir string
|
||||
LocalRAGURL string
|
||||
CustomActionsDir string
|
||||
SSHBoxURL string
|
||||
CollectionDBPath string
|
||||
FileAssets string
|
||||
|
||||
// Conversation settings
|
||||
EnableConversationsLogging bool
|
||||
APIKeys []string
|
||||
ConversationDuration string
|
||||
|
||||
// RAG/Vector settings
|
||||
VectorEngine string
|
||||
EmbeddingModel string
|
||||
MaxChunkingSize int
|
||||
ChunkOverlap int
|
||||
DatabaseURL string
|
||||
}
|
||||
|
||||
// LoadEnv reads all environment variables and returns an Env struct
|
||||
func LoadEnv() Env {
|
||||
env := Env{
|
||||
Model: envOrDefault("LOCALAGI_MODEL", ""),
|
||||
LLMAPIURL: envOrDefault("LOCALAGI_LLM_API_URL", ""),
|
||||
LLMAPIKey: envOrDefault("LOCALAGI_LLM_API_KEY", ""),
|
||||
MultimodalModel: envOrDefault("LOCALAGI_MULTIMODAL_MODEL", ""),
|
||||
TranscriptionModel: envOrDefault("LOCALAGI_TRANSCRIPTION_MODEL", ""),
|
||||
TranscriptionLanguage: envOrDefault("LOCALAGI_TRANSCRIPTION_LANGUAGE", ""),
|
||||
TTSModel: envOrDefault("LOCALAGI_TTS_MODEL", ""),
|
||||
Timeout: envOrDefault("LOCALAGI_TIMEOUT", "5m"),
|
||||
StateDir: envOrDefault("LOCALAGI_STATE_DIR", ""),
|
||||
LocalRAGURL: os.Getenv("LOCALAGI_LOCALRAG_URL"),
|
||||
CustomActionsDir: os.Getenv("LOCALAGI_CUSTOM_ACTIONS_DIR"),
|
||||
SSHBoxURL: os.Getenv("LOCALAGI_SSHBOX_URL"),
|
||||
EnableConversationsLogging: os.Getenv("LOCALAGI_ENABLE_CONVERSATIONS_LOGGING") == "true",
|
||||
ConversationDuration: os.Getenv("LOCALAGI_CONVERSATION_DURATION"),
|
||||
CollectionDBPath: os.Getenv("COLLECTION_DB_PATH"),
|
||||
FileAssets: os.Getenv("FILE_ASSETS"),
|
||||
VectorEngine: os.Getenv("VECTOR_ENGINE"),
|
||||
EmbeddingModel: os.Getenv("EMBEDDING_MODEL"),
|
||||
DatabaseURL: os.Getenv("DATABASE_URL"),
|
||||
}
|
||||
|
||||
// Parse APIKeys from comma-separated string
|
||||
if apiKeysEnv := os.Getenv("LOCALAGI_API_KEYS"); apiKeysEnv != "" {
|
||||
env.APIKeys = strings.Split(apiKeysEnv, ",")
|
||||
}
|
||||
|
||||
// Parse numeric values
|
||||
if maxChunkingSizeEnv := os.Getenv("MAX_CHUNKING_SIZE"); maxChunkingSizeEnv != "" {
|
||||
if n, err := strconv.Atoi(maxChunkingSizeEnv); err == nil {
|
||||
env.MaxChunkingSize = n
|
||||
}
|
||||
}
|
||||
|
||||
if chunkOverlapEnv := os.Getenv("CHUNK_OVERLAP"); chunkOverlapEnv != "" {
|
||||
if n, err := strconv.Atoi(chunkOverlapEnv); err == nil {
|
||||
env.ChunkOverlap = n
|
||||
}
|
||||
}
|
||||
|
||||
// Set defaults for empty values
|
||||
if env.VectorEngine == "" {
|
||||
env.VectorEngine = "chromem"
|
||||
}
|
||||
if env.EmbeddingModel == "" {
|
||||
env.EmbeddingModel = "granite-embedding-107m-multilingual"
|
||||
}
|
||||
if env.MaxChunkingSize == 0 {
|
||||
env.MaxChunkingSize = 400
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
// envOrDefault returns the environment variable value if set, otherwise the fallback.
|
||||
func envOrDefault(envKey, fallback string) string {
|
||||
if v := os.Getenv(envKey); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "local-agi",
|
||||
Short: "LocalAGI - Self-hosted AI Agent platform",
|
||||
Long: "LocalAGI is a self-hosted AI Agent platform that allows running autonomous agents with various connectors, actions, and tools.",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// If no subcommand is provided, default to serving the web server
|
||||
// This ensures the container starts the web server by default
|
||||
return serveCmd.RunE(cmd, args)
|
||||
},
|
||||
}
|
||||
|
||||
// Execute runs the root command.
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(serveCmd)
|
||||
rootCmd.AddCommand(agentCmd)
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/agent"
|
||||
"github.com/mudler/LocalAGI/core/state"
|
||||
"github.com/mudler/LocalAGI/services"
|
||||
"github.com/mudler/LocalAGI/services/skills"
|
||||
"github.com/mudler/LocalAGI/webui"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var serveCmd = &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "Start the LocalAGI web server",
|
||||
Long: "Start the LocalAGI web server with the agent pool and web UI.",
|
||||
RunE: runServe,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(serveCmd)
|
||||
}
|
||||
|
||||
func runServe(cmd *cobra.Command, args []string) error {
|
||||
// Load all environment variables
|
||||
env := LoadEnv()
|
||||
|
||||
if env.Model == "" {
|
||||
return cmd.Help()
|
||||
}
|
||||
if env.LLMAPIURL == "" {
|
||||
return cmd.Help()
|
||||
}
|
||||
|
||||
if env.StateDir == "" {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env.StateDir = filepath.Join(cwd, "pool")
|
||||
}
|
||||
|
||||
os.MkdirAll(env.StateDir, 0755)
|
||||
|
||||
if env.CollectionDBPath == "" {
|
||||
env.CollectionDBPath = filepath.Join(env.StateDir, "collections")
|
||||
}
|
||||
if env.FileAssets == "" {
|
||||
env.FileAssets = filepath.Join(env.StateDir, "assets")
|
||||
}
|
||||
|
||||
apiKeys := env.APIKeys
|
||||
if len(apiKeys) == 0 {
|
||||
apiKeys = []string{}
|
||||
}
|
||||
|
||||
skillsService, err := skills.NewService(env.StateDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pool, err := state.NewAgentPool(
|
||||
env.Model,
|
||||
env.MultimodalModel,
|
||||
env.TranscriptionModel,
|
||||
env.TranscriptionLanguage,
|
||||
env.TTSModel,
|
||||
env.LLMAPIURL,
|
||||
env.LLMAPIKey,
|
||||
env.StateDir,
|
||||
services.Actions(map[string]string{
|
||||
services.ActionConfigSSHBoxURL: env.SSHBoxURL,
|
||||
services.ConfigStateDir: env.StateDir,
|
||||
services.CustomActionsDir: env.CustomActionsDir,
|
||||
}),
|
||||
services.Connectors,
|
||||
services.DynamicPrompts(map[string]string{
|
||||
services.ConfigStateDir: env.StateDir,
|
||||
services.CustomActionsDir: env.CustomActionsDir,
|
||||
}),
|
||||
services.Filters,
|
||||
env.Timeout,
|
||||
env.EnableConversationsLogging,
|
||||
skillsService,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
app := webui.NewApp(
|
||||
webui.WithPool(pool),
|
||||
webui.WithSkillsService(skillsService),
|
||||
webui.WithConversationStoreduration(env.ConversationDuration),
|
||||
webui.WithApiKeys(apiKeys...),
|
||||
webui.WithLLMAPIUrl(env.LLMAPIURL),
|
||||
webui.WithLLMAPIKey(env.LLMAPIKey),
|
||||
webui.WithLLMModel(env.Model),
|
||||
webui.WithCustomActionsDir(env.CustomActionsDir),
|
||||
webui.WithStateDir(env.StateDir),
|
||||
webui.WithCollectionDBPath(env.CollectionDBPath),
|
||||
webui.WithFileAssets(env.FileAssets),
|
||||
webui.WithVectorEngine(env.VectorEngine),
|
||||
webui.WithEmbeddingModel(env.EmbeddingModel),
|
||||
webui.WithMaxChunkingSize(env.MaxChunkingSize),
|
||||
webui.WithChunkOverlap(env.ChunkOverlap),
|
||||
webui.WithDatabaseURL(env.DatabaseURL),
|
||||
webui.WithLocalRAGURL(env.LocalRAGURL),
|
||||
)
|
||||
|
||||
if env.LocalRAGURL != "" {
|
||||
pool.SetRAGProvider(state.NewHTTPRAGProvider(env.LocalRAGURL, env.LLMAPIKey))
|
||||
} else {
|
||||
embedded := app.CollectionsRAGProvider()
|
||||
pool.SetRAGProvider(func(collectionName, _, _ string) (agent.RAGDB, state.KBCompactionClient, bool) {
|
||||
return embedded(collectionName)
|
||||
})
|
||||
}
|
||||
|
||||
if err := pool.StartAll(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Fatal(app.Listen(":3000"))
|
||||
return nil
|
||||
}
|
||||
@@ -193,6 +193,14 @@ func replaceInvalidChars(s string) string {
|
||||
return strings.ReplaceAll(s, " ", "_")
|
||||
}
|
||||
|
||||
// StartAgentStandalone starts an agent without saving it to the pool registry.
|
||||
// It is intended for running a single agent from the CLI without the web server.
|
||||
func (a *AgentPool) StartAgentStandalone(name string, agentConfig *AgentConfig) error {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
return a.startAgentWithConfig(name, a.pooldir, agentConfig, nil)
|
||||
}
|
||||
|
||||
// CreateAgent adds a new agent to the pool
|
||||
// and starts it.
|
||||
// It also saves the state to the file.
|
||||
@@ -797,3 +805,4 @@ func (a *AgentPool) GetManager(name string) sse.Manager {
|
||||
defer a.Unlock()
|
||||
return a.managers[name]
|
||||
}
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@ require (
|
||||
github.com/google/jsonschema-go v0.3.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.8.0 // indirect
|
||||
@@ -94,6 +95,8 @@ require (
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/spf13/cobra v1.10.2 // indirect
|
||||
github.com/spf13/pflag v1.0.9 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
go.etcd.io/bbolt v1.4.0 // indirect
|
||||
|
||||
@@ -96,6 +96,7 @@ github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7np
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
|
||||
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2 h1:flLYmnQFZNo04x2NPehMbf30m7Pli57xwZ0NFqR/hb0=
|
||||
@@ -217,6 +218,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
@@ -339,6 +342,7 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
||||
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
|
||||
@@ -361,6 +365,10 @@ github.com/slack-go/slack v0.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g
|
||||
github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk=
|
||||
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
|
||||
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
|
||||
@@ -1,170 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/agent"
|
||||
"github.com/mudler/LocalAGI/core/state"
|
||||
"github.com/mudler/LocalAGI/services"
|
||||
"github.com/mudler/LocalAGI/services/skills"
|
||||
"github.com/mudler/LocalAGI/webui"
|
||||
"github.com/mudler/LocalAGI/cmd"
|
||||
)
|
||||
|
||||
var baseModel = os.Getenv("LOCALAGI_MODEL")
|
||||
var multimodalModel = os.Getenv("LOCALAGI_MULTIMODAL_MODEL")
|
||||
var transcriptionModel = os.Getenv("LOCALAGI_TRANSCRIPTION_MODEL")
|
||||
var transcriptionLanguage = os.Getenv("LOCALAGI_TRANSCRIPTION_LANGUAGE")
|
||||
var ttsModel = os.Getenv("LOCALAGI_TTS_MODEL")
|
||||
var apiURL = os.Getenv("LOCALAGI_LLM_API_URL")
|
||||
var apiKey = os.Getenv("LOCALAGI_LLM_API_KEY")
|
||||
var timeout = os.Getenv("LOCALAGI_TIMEOUT")
|
||||
var stateDir = os.Getenv("LOCALAGI_STATE_DIR")
|
||||
var localRAG = os.Getenv("LOCALAGI_LOCALRAG_URL")
|
||||
var withLogs = os.Getenv("LOCALAGI_ENABLE_CONVERSATIONS_LOGGING") == "true"
|
||||
var apiKeysEnv = os.Getenv("LOCALAGI_API_KEYS")
|
||||
var conversationDuration = os.Getenv("LOCALAGI_CONVERSATION_DURATION")
|
||||
var customActionsDir = os.Getenv("LOCALAGI_CUSTOM_ACTIONS_DIR")
|
||||
var sshBoxURL = os.Getenv("LOCALAGI_SSHBOX_URL")
|
||||
|
||||
// Collection / knowledge base env
|
||||
var collectionDBPath = os.Getenv("COLLECTION_DB_PATH")
|
||||
var fileAssets = os.Getenv("FILE_ASSETS")
|
||||
var vectorEngine = os.Getenv("VECTOR_ENGINE")
|
||||
var embeddingModel = os.Getenv("EMBEDDING_MODEL")
|
||||
var maxChunkingSizeEnv = os.Getenv("MAX_CHUNKING_SIZE")
|
||||
var chunkOverlapEnv = os.Getenv("CHUNK_OVERLAP")
|
||||
var databaseURL = os.Getenv("DATABASE_URL")
|
||||
|
||||
func init() {
|
||||
if baseModel == "" {
|
||||
panic("LOCALAGI_MODEL not set")
|
||||
}
|
||||
if apiURL == "" {
|
||||
panic("LOCALAGI_LLM_API_URL not set")
|
||||
}
|
||||
if timeout == "" {
|
||||
timeout = "5m"
|
||||
}
|
||||
if stateDir == "" {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
stateDir = filepath.Join(cwd, "pool")
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
// make sure state dir exists
|
||||
os.MkdirAll(stateDir, 0755)
|
||||
|
||||
// Collection defaults when env unset
|
||||
if collectionDBPath == "" {
|
||||
collectionDBPath = filepath.Join(stateDir, "collections")
|
||||
}
|
||||
if fileAssets == "" {
|
||||
fileAssets = filepath.Join(stateDir, "assets")
|
||||
}
|
||||
if vectorEngine == "" {
|
||||
vectorEngine = "chromem"
|
||||
}
|
||||
if embeddingModel == "" {
|
||||
embeddingModel = "granite-embedding-107m-multilingual"
|
||||
}
|
||||
maxChunkingSize := 400
|
||||
if maxChunkingSizeEnv != "" {
|
||||
if n, err := strconv.Atoi(maxChunkingSizeEnv); err == nil {
|
||||
maxChunkingSize = n
|
||||
}
|
||||
}
|
||||
chunkOverlap := 0
|
||||
if chunkOverlapEnv != "" {
|
||||
if n, err := strconv.Atoi(chunkOverlapEnv); err == nil {
|
||||
chunkOverlap = n
|
||||
}
|
||||
}
|
||||
|
||||
apiKeys := []string{}
|
||||
if apiKeysEnv != "" {
|
||||
apiKeys = strings.Split(apiKeysEnv, ",")
|
||||
}
|
||||
|
||||
// Skills service (optional: provides skills prompt and MCP when agents have EnableSkills)
|
||||
skillsService, err := skills.NewService(stateDir)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Create the agent pool (RAG provider set below after app is created)
|
||||
pool, err := state.NewAgentPool(
|
||||
baseModel,
|
||||
multimodalModel,
|
||||
transcriptionModel,
|
||||
transcriptionLanguage,
|
||||
ttsModel,
|
||||
apiURL,
|
||||
apiKey,
|
||||
stateDir,
|
||||
services.Actions(map[string]string{
|
||||
services.ActionConfigSSHBoxURL: sshBoxURL,
|
||||
services.ConfigStateDir: stateDir,
|
||||
services.CustomActionsDir: customActionsDir,
|
||||
}),
|
||||
services.Connectors,
|
||||
services.DynamicPrompts(map[string]string{
|
||||
services.ConfigStateDir: stateDir,
|
||||
services.CustomActionsDir: customActionsDir,
|
||||
}),
|
||||
services.Filters,
|
||||
timeout,
|
||||
withLogs,
|
||||
skillsService,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Create the application (registers collection routes and sets up in-process RAG state)
|
||||
app := webui.NewApp(
|
||||
webui.WithPool(pool),
|
||||
webui.WithSkillsService(skillsService),
|
||||
webui.WithConversationStoreduration(conversationDuration),
|
||||
webui.WithApiKeys(apiKeys...),
|
||||
webui.WithLLMAPIUrl(apiURL),
|
||||
webui.WithLLMAPIKey(apiKey),
|
||||
webui.WithLLMModel(baseModel),
|
||||
webui.WithCustomActionsDir(customActionsDir),
|
||||
webui.WithStateDir(stateDir),
|
||||
webui.WithCollectionDBPath(collectionDBPath),
|
||||
webui.WithFileAssets(fileAssets),
|
||||
webui.WithVectorEngine(vectorEngine),
|
||||
webui.WithEmbeddingModel(embeddingModel),
|
||||
webui.WithMaxChunkingSize(maxChunkingSize),
|
||||
webui.WithChunkOverlap(chunkOverlap),
|
||||
webui.WithDatabaseURL(databaseURL),
|
||||
webui.WithLocalRAGURL(localRAG),
|
||||
)
|
||||
|
||||
// Single RAG provider: HTTP client when URL set, in-process when not
|
||||
if localRAG != "" {
|
||||
pool.SetRAGProvider(state.NewHTTPRAGProvider(localRAG, apiKey))
|
||||
} else {
|
||||
embedded := app.CollectionsRAGProvider()
|
||||
pool.SetRAGProvider(func(collectionName, _, _ string) (agent.RAGDB, state.KBCompactionClient, bool) {
|
||||
return embedded(collectionName)
|
||||
})
|
||||
}
|
||||
|
||||
// Start the agents
|
||||
if err := pool.StartAll(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Start the web server
|
||||
log.Fatal(app.Listen(":3000"))
|
||||
cmd.Execute()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user