feat: implement 'agent run' CLI command (#448) (#449)

* 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:
LocalAI [bot]
2026-03-10 22:09:02 +01:00
committed by GitHub
parent 9b744d2bac
commit dc21ee83bc
10 changed files with 545 additions and 164 deletions
+1 -1
View File
@@ -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"]
+15
View File
@@ -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)
}
+243
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+9
View File
@@ -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]
}
+3
View File
@@ -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
+8
View File
@@ -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=
+2 -163
View File
@@ -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()
}