diff --git a/Dockerfile.webui b/Dockerfile.webui index 2e8cb3b..489627f 100644 --- a/Dockerfile.webui +++ b/Dockerfile.webui @@ -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"] diff --git a/cmd/agent.go b/cmd/agent.go new file mode 100644 index 0000000..b0af9ce --- /dev/null +++ b/cmd/agent.go @@ -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) +} diff --git a/cmd/agent_run.go b/cmd/agent_run.go new file mode 100644 index 0000000..71fb4e0 --- /dev/null +++ b/cmd/agent_run.go @@ -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 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 +} diff --git a/cmd/env.go b/cmd/env.go new file mode 100644 index 0000000..683ad1f --- /dev/null +++ b/cmd/env.go @@ -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 +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..e26e721 --- /dev/null +++ b/cmd/root.go @@ -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) +} diff --git a/cmd/serve.go b/cmd/serve.go new file mode 100644 index 0000000..0e68980 --- /dev/null +++ b/cmd/serve.go @@ -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 +} diff --git a/core/state/pool.go b/core/state/pool.go index 3159f5f..4541617 100644 --- a/core/state/pool.go +++ b/core/state/pool.go @@ -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] } + diff --git a/go.mod b/go.mod index e7a3f7b..1179836 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 6ea9b57..d562b5f 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/main.go b/main.go index 8d202d6..068a55a 100644 --- a/main.go +++ b/main.go @@ -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() }