mirror of
https://github.com/mudler/LocalAGI.git
synced 2026-08-26 18:47:04 -04:00
f6366af5e8
An agent with a task-creating tool re-creates the same reminder on every run. One deployment reached 6,399 tasks for a single agent, and 4,734 of them (74%) were byte-identical in both prompt and schedule. CreateTask now returns the task that is scheduled, which is the existing one when an equivalent task is already set. The caller sees a success either way. An error would invite the model to retry with different wording, and that agent already held seven phrasings of one reminder. Tasks match on agent, schedule type, schedule value and a normalised prompt: lowercased, with whitespace collapsed and punctuation dropped. Reworded prompts stay distinct. Merging prompts that differ in their words risks dropping a task somebody meant to schedule, so a per-agent ceiling bounds that case instead. At the ceiling, creation fails with a message naming list_tasks and remove_task so the agent can free room itself. A duplicate consumes no room and is never refused. Dedupe is on by default with a ceiling of 100 tasks per agent. LOCALAGI_SCHEDULER_DEDUPE_TASKS and LOCALAGI_SCHEDULER_MAX_TASKS_PER_AGENT tune them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZaoEXfGsjmkhXtVxtvPp4
201 lines
6.4 KiB
Go
201 lines
6.4 KiB
Go
package cmd
|
|
|
|
import (
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mudler/LocalAGI/core/conversations"
|
|
"github.com/mudler/LocalAGI/core/scheduler"
|
|
"github.com/mudler/LocalAGI/core/state"
|
|
"github.com/mudler/xlog"
|
|
)
|
|
|
|
// 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
|
|
LocalAGIURL string
|
|
LocalRAGURL string
|
|
CustomActionsDir string
|
|
SSHBoxURL string
|
|
CollectionDBPath string
|
|
FileAssets string
|
|
|
|
// Conversation settings
|
|
EnableConversationsLogging bool
|
|
APIKeys []string
|
|
ConversationDuration string
|
|
|
|
// Retention settings. A zero value disables the individual limit.
|
|
ConversationsMaxAge time.Duration
|
|
ConversationsMaxPerAgent int
|
|
ConversationsPruneInterval time.Duration
|
|
SchedulerMaxRunsPerTask int
|
|
SchedulerMaxRunAge time.Duration
|
|
SchedulerDedupeTasks bool
|
|
SchedulerMaxTasksPerAgent int
|
|
|
|
// 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", ""),
|
|
LocalAGIURL: envOrDefault("LOCALAGI_BASE_URL", ":3000"),
|
|
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"),
|
|
|
|
ConversationsMaxAge: envDuration("LOCALAGI_CONVERSATIONS_MAX_AGE", 720*time.Hour),
|
|
ConversationsMaxPerAgent: envInt("LOCALAGI_CONVERSATIONS_MAX_PER_AGENT", 200),
|
|
ConversationsPruneInterval: envDuration("LOCALAGI_CONVERSATIONS_PRUNE_INTERVAL", time.Hour),
|
|
SchedulerMaxRunsPerTask: envInt("LOCALAGI_SCHEDULER_MAX_RUNS_PER_TASK", 20),
|
|
SchedulerMaxRunAge: envDuration("LOCALAGI_SCHEDULER_MAX_RUN_AGE", 720*time.Hour),
|
|
SchedulerDedupeTasks: envBool("LOCALAGI_SCHEDULER_DEDUPE_TASKS", true),
|
|
SchedulerMaxTasksPerAgent: envInt("LOCALAGI_SCHEDULER_MAX_TASKS_PER_AGENT", 100),
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Limits builds the pool limits from the parsed environment.
|
|
func (e Env) Limits() state.PoolLimits {
|
|
return state.PoolLimits{
|
|
Conversations: conversations.RetentionPolicy{
|
|
MaxAge: e.ConversationsMaxAge,
|
|
MaxPerAgent: e.ConversationsMaxPerAgent,
|
|
},
|
|
ConversationSweep: e.ConversationsPruneInterval,
|
|
SchedulerRuns: scheduler.RetentionPolicy{
|
|
MaxRunsPerTask: e.SchedulerMaxRunsPerTask,
|
|
MaxRunAge: e.SchedulerMaxRunAge,
|
|
},
|
|
SchedulerCreation: scheduler.CreationPolicy{
|
|
Dedupe: e.SchedulerDedupeTasks,
|
|
MaxTasksPerAgent: e.SchedulerMaxTasksPerAgent,
|
|
},
|
|
}
|
|
}
|
|
|
|
// envDuration reads a duration, accepting a day suffix ("30d") that
|
|
// time.ParseDuration does not. An unset value takes the fallback; an
|
|
// unparseable one keeps the fallback rather than silently disabling a limit.
|
|
func envDuration(envKey string, fallback time.Duration) time.Duration {
|
|
v := os.Getenv(envKey)
|
|
if v == "" {
|
|
return fallback
|
|
}
|
|
|
|
d, err := scheduler.ParseDuration(v)
|
|
if err != nil {
|
|
xlog.Warn("Ignoring unparseable duration, using default", "env", envKey, "value", v, "default", fallback)
|
|
return fallback
|
|
}
|
|
|
|
return d
|
|
}
|
|
|
|
// envInt reads an integer, keeping the fallback when the value is unset or
|
|
// unparseable.
|
|
func envInt(envKey string, fallback int) int {
|
|
v := os.Getenv(envKey)
|
|
if v == "" {
|
|
return fallback
|
|
}
|
|
|
|
n, err := strconv.Atoi(v)
|
|
if err != nil {
|
|
xlog.Warn("Ignoring unparseable integer, using default", "env", envKey, "value", v, "default", fallback)
|
|
return fallback
|
|
}
|
|
|
|
return n
|
|
}
|
|
|
|
// envBool reads a boolean, keeping the fallback when the value is unset or
|
|
// unparseable.
|
|
func envBool(envKey string, fallback bool) bool {
|
|
v := os.Getenv(envKey)
|
|
if v == "" {
|
|
return fallback
|
|
}
|
|
|
|
b, err := strconv.ParseBool(v)
|
|
if err != nil {
|
|
xlog.Warn("Ignoring unparseable boolean, using default", "env", envKey, "value", v, "default", fallback)
|
|
return fallback
|
|
}
|
|
|
|
return b
|
|
}
|
|
|
|
// 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
|
|
}
|