feat: import agents_and_robots platform as unibots (Matrix-out, unibus transport)
Reemplaza el scaffold del echobot por la plataforma completa de bots traida desde ~/DataProyects/Github/agents_and_robots tras la operacion Matrix-out: los bots ya no hablan por Matrix sino por el bus unibus (modelo todo-rooms + E2E via shell/transportunibus sobre github.com/enmanuel/unibus/pkg/client). - go.mod: replace de unibus -> ../unibus y de fn-registry -> ../../../.. (paths relativos reajustados a la nueva ubicacion dentro de fn_registry). - app.md: bump a 0.2.0, descripcion + arquitectura + comandos + gotchas reales. - modulo Go conservado como github.com/enmanuel/agents (sin reescribir imports). agents_and_robots queda archivado como museo de la era Matrix.
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Load reads and parses an agent config file from the given path.
|
||||
func Load(path string) (*AgentConfig, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read config %s: %w", path, err)
|
||||
}
|
||||
|
||||
// Expand environment variables in the raw YAML bytes.
|
||||
expanded := os.ExpandEnv(string(data))
|
||||
|
||||
var cfg AgentConfig
|
||||
if err := yaml.Unmarshal([]byte(expanded), &cfg); err != nil {
|
||||
return nil, fmt.Errorf("parse config %s: %w", path, err)
|
||||
}
|
||||
|
||||
if err := validate(&cfg); err != nil {
|
||||
return nil, fmt.Errorf("invalid config %s: %w", path, err)
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// LoadMeta reads only the `agent:` block from a config file without expanding
|
||||
// env vars or running full validation. Used by agentctl list to show all
|
||||
// agents regardless of whether their env vars are configured.
|
||||
func LoadMeta(path string) (*AgentConfig, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read config %s: %w", path, err)
|
||||
}
|
||||
var cfg AgentConfig
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("parse config %s: %w", path, err)
|
||||
}
|
||||
if cfg.Agent.ID == "" {
|
||||
return nil, fmt.Errorf("agent.id is required")
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// LoadSpecial reads and parses a special agent config file.
|
||||
// Special agents have no Matrix identity so validation is lighter.
|
||||
func LoadSpecial(path string) (*SpecialConfig, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read special config %s: %w", path, err)
|
||||
}
|
||||
|
||||
expanded := os.ExpandEnv(string(data))
|
||||
|
||||
var cfg SpecialConfig
|
||||
if err := yaml.Unmarshal([]byte(expanded), &cfg); err != nil {
|
||||
return nil, fmt.Errorf("parse special config %s: %w", path, err)
|
||||
}
|
||||
|
||||
if err := validateSpecial(&cfg); err != nil {
|
||||
return nil, fmt.Errorf("invalid special config %s: %w", path, err)
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// validateSpecial applies sanity checks for special agent configs.
|
||||
func validateSpecial(cfg *SpecialConfig) error {
|
||||
if cfg.Special.ID == "" {
|
||||
return fmt.Errorf("special.id is required")
|
||||
}
|
||||
if cfg.Special.Type == "" {
|
||||
return fmt.Errorf("special.type is required")
|
||||
}
|
||||
if cfg.LLM.Primary.Provider == "" {
|
||||
return fmt.Errorf("llm.primary.provider is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validate applies basic sanity checks.
|
||||
func validate(cfg *AgentConfig) error {
|
||||
if cfg.Agent.ID == "" {
|
||||
return fmt.Errorf("agent.id is required")
|
||||
}
|
||||
if cfg.Bus.NatsURL == "" {
|
||||
return fmt.Errorf("bus.nats_url is required")
|
||||
}
|
||||
if cfg.Bus.CtrlURL == "" {
|
||||
return fmt.Errorf("bus.ctrl_url is required")
|
||||
}
|
||||
if cfg.LLM.Primary.Provider == "" {
|
||||
return fmt.Errorf("llm.primary.provider is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
// Package config provides the configuration schema and loader for agents.
|
||||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
// AgentConfig is the root configuration for a single agent.
|
||||
type AgentConfig struct {
|
||||
Agent AgentMeta `yaml:"agent"`
|
||||
Personality PersonalityCfg `yaml:"personality"`
|
||||
LLM LLMCfg `yaml:"llm"`
|
||||
Tools ToolsCfg `yaml:"tools"`
|
||||
Bus BusCfg `yaml:"bus"`
|
||||
SSH SSHCfg `yaml:"ssh"`
|
||||
Security SecurityCfg `yaml:"security"`
|
||||
Schedules []ScheduleCfg `yaml:"schedules"`
|
||||
Storage StorageCfg `yaml:"storage"`
|
||||
Memory MemoryCfg `yaml:"memory"`
|
||||
Skills SkillsCfg `yaml:"skills"`
|
||||
}
|
||||
|
||||
// ── Identity ──────────────────────────────────────────────────────────────
|
||||
|
||||
type AgentMeta struct {
|
||||
ID string `yaml:"id"`
|
||||
Name string `yaml:"name"`
|
||||
Version string `yaml:"version"`
|
||||
Type string `yaml:"type"` // "agent" (default) or "robot" (command-only, no LLM)
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Template bool `yaml:"template"` // if true, launcher will skip this agent
|
||||
Description string `yaml:"description"`
|
||||
Tags []string `yaml:"tags"`
|
||||
}
|
||||
|
||||
// ── Personality ───────────────────────────────────────────────────────────
|
||||
|
||||
type PersonalityCfg struct {
|
||||
// --- campos existentes (sin cambios) ---
|
||||
Tone string `yaml:"tone"`
|
||||
Verbosity string `yaml:"verbosity"`
|
||||
Language string `yaml:"language"`
|
||||
LanguagesSupported []string `yaml:"languages_supported"`
|
||||
EmojiStyle string `yaml:"emoji_style"`
|
||||
Prefix string `yaml:"prefix"`
|
||||
ErrorStyle string `yaml:"error_style"`
|
||||
Templates TemplatesCfg `yaml:"templates"`
|
||||
Behavior BehaviorCfg `yaml:"behavior"`
|
||||
|
||||
// --- NUEVOS campos ---
|
||||
// Identidad narrativa
|
||||
Role string `yaml:"role"` // rol principal: "asistente general", "devops engineer", "analista de datos"
|
||||
Backstory string `yaml:"backstory"` // breve historia/contexto del personaje (1-3 frases)
|
||||
Expertise []string `yaml:"expertise"` // areas de experiencia: ["linux", "docker", "monitoring"]
|
||||
Limitations []string `yaml:"limitations"` // que NO sabe o no debe intentar
|
||||
|
||||
// Estilo de comunicacion
|
||||
Communication CommunicationCfg `yaml:"communication"`
|
||||
|
||||
// Directivas de comportamiento en texto libre
|
||||
CustomDirectives []string `yaml:"custom_directives"` // instrucciones adicionales para el system prompt
|
||||
}
|
||||
|
||||
type TemplatesCfg struct {
|
||||
Greeting string `yaml:"greeting"`
|
||||
UnknownCommand string `yaml:"unknown_command"`
|
||||
PermissionDenied string `yaml:"permission_denied"`
|
||||
Error string `yaml:"error"`
|
||||
Success string `yaml:"success"`
|
||||
Busy string `yaml:"busy"`
|
||||
}
|
||||
|
||||
type BehaviorCfg struct {
|
||||
Proactive bool `yaml:"proactive"`
|
||||
AskConfirmation bool `yaml:"ask_confirmation"`
|
||||
ShowReasoning bool `yaml:"show_reasoning"`
|
||||
ThreadReplies bool `yaml:"thread_replies"`
|
||||
TypingIndicator bool `yaml:"typing_indicator"`
|
||||
AcknowledgeReceipt bool `yaml:"acknowledge_receipt"`
|
||||
}
|
||||
|
||||
// CommunicationCfg define como se expresa el agente mas alla del tone basico.
|
||||
type CommunicationCfg struct {
|
||||
Formality string `yaml:"formality"` // formal | semiformal | casual | coloquial
|
||||
Humor string `yaml:"humor"` // none | subtle | moderate | frequent
|
||||
Personality string `yaml:"personality"` // analytical | creative | pragmatic | empathetic | assertive
|
||||
ResponseStyle string `yaml:"response_style"` // structured | conversational | bullet_points | narrative
|
||||
Quirks []string `yaml:"quirks"` // rasgos unicos: ["usa analogias de cocina", "cita a Linus Torvalds"]
|
||||
AvoidTopics []string `yaml:"avoid_topics"` // temas que evita o redirige
|
||||
Catchphrases []string `yaml:"catchphrases"` // frases tipicas que usa ocasionalmente
|
||||
}
|
||||
|
||||
// ── LLM ───────────────────────────────────────────────────────────────────
|
||||
|
||||
type LLMCfg struct {
|
||||
Primary LLMProviderCfg `yaml:"primary"`
|
||||
Fallback LLMProviderCfg `yaml:"fallback"`
|
||||
Reasoning LLMReasoningCfg `yaml:"reasoning"`
|
||||
ToolUse LLMToolUseCfg `yaml:"tool_use"`
|
||||
RateLimit LLMRateLimitCfg `yaml:"rate_limit"`
|
||||
}
|
||||
|
||||
type LLMProviderCfg struct {
|
||||
Provider string `yaml:"provider"`
|
||||
Model string `yaml:"model"`
|
||||
APIKeyEnv string `yaml:"api_key_env"`
|
||||
BaseURL string `yaml:"base_url"`
|
||||
MaxTokens int `yaml:"max_tokens"`
|
||||
Temperature float64 `yaml:"temperature"`
|
||||
|
||||
// ClaudeCode holds configuration for the claude-code provider (claude -p).
|
||||
ClaudeCode ClaudeCodeCfg `yaml:"claude_code"`
|
||||
}
|
||||
|
||||
// ClaudeCodeCfg configures the claude -p subprocess provider.
|
||||
type ClaudeCodeCfg struct {
|
||||
Binary string `yaml:"binary"` // path to claude binary (default: "claude")
|
||||
Timeout time.Duration `yaml:"timeout"` // subprocess timeout (default: 5m)
|
||||
DisableTools bool `yaml:"disable_tools"` // pass --tools "" to disable all internal tools
|
||||
AllowedTools []string `yaml:"allowed_tools"` // tools claude -p can use internally (e.g. Bash, Read, Edit)
|
||||
DisallowedTools []string `yaml:"disallowed_tools"` // tools to block
|
||||
WorkingDir string `yaml:"working_dir"` // working directory for claude -p
|
||||
PermissionMode string `yaml:"permission_mode"` // default, acceptEdits, bypassPermissions, plan
|
||||
Model string `yaml:"model"` // inner model: sonnet, opus, haiku, or full name
|
||||
FallbackModel string `yaml:"fallback_model"` // fallback model if primary is overloaded
|
||||
SessionID string `yaml:"session_id"` // fixed session ID for continuity
|
||||
AddDirs []string `yaml:"add_dirs"` // additional directories accessible
|
||||
}
|
||||
|
||||
type LLMReasoningCfg struct {
|
||||
SystemPromptFile string `yaml:"system_prompt_file"`
|
||||
ContextWindow int `yaml:"context_window"`
|
||||
MemoryMessages int `yaml:"memory_messages"`
|
||||
}
|
||||
|
||||
type LLMToolUseCfg struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
MaxIterations int `yaml:"max_iterations"`
|
||||
ParallelCalls bool `yaml:"parallel_calls"`
|
||||
}
|
||||
|
||||
type LLMRateLimitCfg struct {
|
||||
RequestsPerMinute int `yaml:"requests_per_minute"`
|
||||
TokensPerMinute int `yaml:"tokens_per_minute"`
|
||||
ConcurrentRequests int `yaml:"concurrent_requests"`
|
||||
}
|
||||
|
||||
// ── Tools ─────────────────────────────────────────────────────────────────
|
||||
|
||||
type ToolsCfg struct {
|
||||
SSH SSHToolCfg `yaml:"ssh"`
|
||||
HTTP HTTPToolCfg `yaml:"http"`
|
||||
Scripts ScriptsCfg `yaml:"scripts"`
|
||||
FileOps FileOpsCfg `yaml:"file_ops"`
|
||||
Bus BusToolCfg `yaml:"bus_send"`
|
||||
MCP MCPToolCfg `yaml:"mcp"`
|
||||
Memory MemoryToolCfg `yaml:"memory"`
|
||||
Knowledge KnowledgeToolCfg `yaml:"knowledge"`
|
||||
SharedKnowledge SharedKnowledgeToolCfg `yaml:"shared_knowledge"`
|
||||
Skills SkillsToolCfg `yaml:"skills"`
|
||||
IMDb IMDbToolCfg `yaml:"imdb"`
|
||||
}
|
||||
|
||||
// BusToolCfg configures the bus_send tool, which lets the LLM post a message to
|
||||
// an arbitrary unibus room. AllowedRooms restricts which room IDs can be
|
||||
// targeted (empty = any room the bot belongs to).
|
||||
type BusToolCfg struct {
|
||||
AllowedRooms []string `yaml:"allowed_rooms"` // if non-empty, only these room IDs can be targeted
|
||||
}
|
||||
|
||||
type KnowledgeToolCfg struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Dir string `yaml:"dir"` // default: "./knowledge" (relative to agent dir)
|
||||
}
|
||||
|
||||
type SharedKnowledgeToolCfg struct {
|
||||
Enabled bool `yaml:"enabled"` // default false
|
||||
Dir string `yaml:"dir"` // default "knowledges" (relative to project root)
|
||||
DBPath string `yaml:"db_path"` // default "knowledges/data/knowledge.db"
|
||||
}
|
||||
|
||||
type SSHToolCfg struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
AllowedTargets []string `yaml:"allowed_targets"`
|
||||
AllowedCommands []string `yaml:"allowed_commands"` // allowlist: if non-empty, only these command prefixes are permitted
|
||||
ForbiddenCommands []string `yaml:"forbidden_commands"`
|
||||
Timeout time.Duration `yaml:"timeout"`
|
||||
MaxConcurrent int `yaml:"max_concurrent"`
|
||||
RequireConfirmation []string `yaml:"require_confirmation"`
|
||||
}
|
||||
|
||||
type HTTPToolCfg struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
AllowedDomains []string `yaml:"allowed_domains"`
|
||||
Timeout time.Duration `yaml:"timeout"`
|
||||
MaxRetries int `yaml:"max_retries"`
|
||||
}
|
||||
|
||||
type ScriptsCfg struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
ScriptsDir string `yaml:"scripts_dir"`
|
||||
Allowed []string `yaml:"allowed"`
|
||||
Timeout time.Duration `yaml:"timeout"`
|
||||
Sandbox bool `yaml:"sandbox"`
|
||||
}
|
||||
|
||||
type FileOpsCfg struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
AllowedPaths []string `yaml:"allowed_paths"`
|
||||
ReadOnly bool `yaml:"read_only"`
|
||||
}
|
||||
|
||||
type MCPToolCfg struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Servers []MCPServerCfg `yaml:"servers"`
|
||||
Expose MCPExposeCfg `yaml:"expose"`
|
||||
}
|
||||
|
||||
type MCPServerCfg struct {
|
||||
Name string `yaml:"name"` // nombre logico del servidor
|
||||
Transport string `yaml:"transport"` // "stdio" | "sse" (default: auto-detect)
|
||||
Command string `yaml:"command"` // stdio: comando a ejecutar
|
||||
Args []string `yaml:"args"` // stdio: argumentos del comando
|
||||
Env map[string]string `yaml:"env"` // stdio: variables de entorno extra
|
||||
URL string `yaml:"url"` // sse: URL del servidor
|
||||
Headers map[string]string `yaml:"headers"` // sse: headers HTTP extra (auth, etc.)
|
||||
Tools []string `yaml:"tools"` // filtro: solo exponer estas tools (vacio = todas)
|
||||
Prefix string `yaml:"prefix"` // prefijo para nombres de tools (evitar colisiones)
|
||||
Timeout time.Duration `yaml:"timeout"` // timeout por llamada (default: 30s)
|
||||
}
|
||||
|
||||
type MCPExposeCfg struct {
|
||||
Port int `yaml:"port"`
|
||||
Tools []string `yaml:"tools"`
|
||||
}
|
||||
|
||||
// ── Bus ───────────────────────────────────────────────────────────────────
|
||||
|
||||
// BusCfg configures the unibus transport: where to reach the NATS data plane
|
||||
// and the membershipd control plane, the bot's long-term identity, and how it
|
||||
// detects mentions and commands. It replaces the former MatrixCfg now that the
|
||||
// ecosystem speaks only over unibus.
|
||||
type BusCfg struct {
|
||||
NatsURL string `yaml:"nats_url"` // NATS data plane, e.g. nats://host:4250
|
||||
CtrlURL string `yaml:"ctrl_url"` // membershipd control plane, e.g. http://host:8470
|
||||
IdentityPath string `yaml:"identity_path"` // path to the bot's long-term identity file (created if absent)
|
||||
Handle string `yaml:"handle"` // bot handle used for mention detection (e.g. "meteorologo")
|
||||
CommandPrefix string `yaml:"command_prefix"` // command marker, default "!"
|
||||
Threads ThreadsCfg `yaml:"threads"` // thread/reply behavior (transport-neutral)
|
||||
}
|
||||
|
||||
// ThreadsCfg controls threaded-reply behavior (transport-neutral: applies to
|
||||
// any fabric that carries a thread root id).
|
||||
type ThreadsCfg struct {
|
||||
Enabled bool `yaml:"enabled"` // respond in threads when message is in a thread (default true)
|
||||
AutoThread bool `yaml:"auto_thread"` // auto-create a thread for each new conversation (default false)
|
||||
}
|
||||
|
||||
// ── SSH Inventory ─────────────────────────────────────────────────────────
|
||||
|
||||
type SSHCfg struct {
|
||||
Defaults SSHDefaultsCfg `yaml:"defaults"`
|
||||
Targets map[string]SSHTargetCfg `yaml:"targets"`
|
||||
}
|
||||
|
||||
type SSHDefaultsCfg struct {
|
||||
User string `yaml:"user"`
|
||||
Port int `yaml:"port"`
|
||||
KeyFileEnv string `yaml:"key_file_env"`
|
||||
KnownHosts string `yaml:"known_hosts"`
|
||||
KeepaliveInterval time.Duration `yaml:"keepalive_interval"`
|
||||
Timeout time.Duration `yaml:"timeout"`
|
||||
}
|
||||
|
||||
type SSHTargetCfg struct {
|
||||
Hosts []string `yaml:"hosts"`
|
||||
User string `yaml:"user"`
|
||||
Port int `yaml:"port"`
|
||||
JumpHost string `yaml:"jump_host"`
|
||||
KeyFileEnv string `yaml:"key_file_env"`
|
||||
}
|
||||
|
||||
// ── Security ──────────────────────────────────────────────────────────────
|
||||
|
||||
type SecurityCfg struct {
|
||||
// Deprecated: use security/ centralized groups instead (see security/user-groups.yaml, permissions.yaml).
|
||||
// Kept for backward compatibility; will be removed in a future issue.
|
||||
Roles map[string]RoleCfg `yaml:"roles"`
|
||||
Audit AuditCfg `yaml:"audit"`
|
||||
Secrets SecretsCfg `yaml:"secrets"`
|
||||
Sanitize SanitizeCfg `yaml:"sanitize"`
|
||||
ToolRateLimit ToolRateLimitCfg `yaml:"tool_rate_limit"`
|
||||
}
|
||||
|
||||
// ToolRateLimitCfg controls per-room rate limiting of tool executions.
|
||||
type ToolRateLimitCfg struct {
|
||||
Enabled bool `yaml:"enabled"` // enable tool rate limiting (default false)
|
||||
MaxCallsPerMin int `yaml:"max_calls_per_min"` // max tool calls per room per minute (default 10)
|
||||
CleanupIntervalS int `yaml:"cleanup_interval_s"` // seconds between stale entry cleanup (default 60)
|
||||
}
|
||||
|
||||
// SanitizeCfg controls prompt injection detection on incoming messages.
|
||||
type SanitizeCfg struct {
|
||||
Enabled bool `yaml:"enabled"` // enable sanitization (default false)
|
||||
Mode string `yaml:"mode"` // warn | strip | reject (default warn)
|
||||
MinSeverity string `yaml:"min_severity"` // low | medium | high (default medium)
|
||||
DisabledPatterns []string `yaml:"disabled_patterns"` // pattern names to skip
|
||||
}
|
||||
|
||||
type RoleCfg struct {
|
||||
Users []string `yaml:"users"`
|
||||
Actions []string `yaml:"actions"`
|
||||
}
|
||||
|
||||
type AuditCfg struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
LogFile string `yaml:"log_file"`
|
||||
LogToRoom string `yaml:"log_to_room"`
|
||||
Include []string `yaml:"include"`
|
||||
}
|
||||
|
||||
type SecretsCfg struct {
|
||||
Provider string `yaml:"provider"` // env | vault | sops
|
||||
}
|
||||
|
||||
// ── Scheduling ────────────────────────────────────────────────────────────
|
||||
|
||||
type ScheduleCfg struct {
|
||||
Name string `yaml:"name"`
|
||||
Cron string `yaml:"cron"`
|
||||
Action ScheduledAction `yaml:"action"`
|
||||
OnFailure FailureAction `yaml:"on_failure"`
|
||||
OutputRoom string `yaml:"output_room"`
|
||||
}
|
||||
|
||||
type ScheduledAction struct {
|
||||
Kind string `yaml:"kind"`
|
||||
Target string `yaml:"target"`
|
||||
Command string `yaml:"command"`
|
||||
Script string `yaml:"script"`
|
||||
|
||||
// Phase 1: send_message and llm_prompt fields
|
||||
Message string `yaml:"message"` // inline text for send_message
|
||||
Template string `yaml:"template"` // path to .md file for send_message
|
||||
Prompt string `yaml:"prompt"` // inline prompt text for llm_prompt
|
||||
}
|
||||
|
||||
type FailureAction struct {
|
||||
NotifyRoom string `yaml:"notify_room"`
|
||||
EscalateTo string `yaml:"escalate_to"`
|
||||
}
|
||||
|
||||
// ── Storage ───────────────────────────────────────────────────────────────
|
||||
|
||||
type StorageCfg struct {
|
||||
BasePath string `yaml:"base_path"` // root for all data; default $AGENTS_DATA_DIR/<id> or agents/<id>/data
|
||||
State StateStorageCfg `yaml:"state"`
|
||||
Cache CacheStorageCfg `yaml:"cache"`
|
||||
History HistoryStorageCfg `yaml:"history"`
|
||||
}
|
||||
|
||||
type StateStorageCfg struct {
|
||||
Backend string `yaml:"backend"` // sqlite | redis | file
|
||||
Path string `yaml:"path"`
|
||||
}
|
||||
|
||||
type CacheStorageCfg struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Backend string `yaml:"backend"` // memory | redis
|
||||
TTL time.Duration `yaml:"ttl"`
|
||||
MaxEntries int `yaml:"max_entries"`
|
||||
}
|
||||
|
||||
type HistoryStorageCfg struct {
|
||||
Backend string `yaml:"backend"`
|
||||
Path string `yaml:"path"`
|
||||
Retention time.Duration `yaml:"retention"`
|
||||
}
|
||||
|
||||
// ── Memory ────────────────────────────────────────────────────────────────
|
||||
|
||||
type MemoryCfg struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
WindowSize int `yaml:"window_size"` // sliding window size per room (default 20)
|
||||
DBPath string `yaml:"db_path"` // SQLite path (default agents/<id>/data/memory.db)
|
||||
}
|
||||
|
||||
type MemoryToolCfg struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
}
|
||||
|
||||
// ── Skills ────────────────────────────────────────────────────────────────
|
||||
|
||||
type SkillsCfg struct {
|
||||
Enabled bool `yaml:"enabled"` // enable skills system (default false)
|
||||
SkillsPath string `yaml:"path"` // path to skills directory (default: "skills/")
|
||||
Categories []string `yaml:"categories"` // filter: only load skills from these categories (empty = all)
|
||||
Timeout time.Duration `yaml:"timeout"` // timeout for script execution (default: 60s)
|
||||
}
|
||||
|
||||
type SkillsToolCfg struct {
|
||||
AllowedInterpreters []string `yaml:"allowed_interpreters"` // allowlist for skill script execution (default: ["bash", "sh"])
|
||||
}
|
||||
|
||||
type IMDbToolCfg struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
APIKey string `yaml:"api_key"` // OMDb API key (get from http://www.omdbapi.com/)
|
||||
APIKeyEnv string `yaml:"api_key_env"` // env var name for API key (e.g., "OMDB_API_KEY")
|
||||
Timeout time.Duration `yaml:"timeout"` // timeout for API requests (default: 10s)
|
||||
}
|
||||
|
||||
// ── Special Agents ────────────────────────────────────────────────────────
|
||||
|
||||
// SpecialConfig is the root configuration for a special agent (no Matrix identity).
|
||||
type SpecialConfig struct {
|
||||
Special SpecialMeta `yaml:"special"`
|
||||
LLM LLMCfg `yaml:"llm"`
|
||||
Orchestration OrchestrationCfg `yaml:"orchestration"`
|
||||
}
|
||||
|
||||
// SpecialMeta identifies a special agent.
|
||||
type SpecialMeta struct {
|
||||
ID string `yaml:"id"`
|
||||
Type string `yaml:"type"` // "orchestrator", "scheduler", etc.
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Description string `yaml:"description"`
|
||||
}
|
||||
|
||||
// OrchestrationCfg configures the multi-bot orchestrator.
|
||||
type OrchestrationCfg struct {
|
||||
MaxIterations int `yaml:"max_iterations"`
|
||||
QualityThreshold float64 `yaml:"quality_threshold"`
|
||||
DelegationTimeout time.Duration `yaml:"delegation_timeout"`
|
||||
RepetitionThreshold float64 `yaml:"repetition_threshold"` // 0-1: similarity ratio to detect circular conversations
|
||||
Rooms []OrchestratedRoomCfg `yaml:"rooms"`
|
||||
}
|
||||
|
||||
// OrchestratedRoomCfg defines a room managed by the orchestrator.
|
||||
type OrchestratedRoomCfg struct {
|
||||
RoomID string `yaml:"room_id"`
|
||||
Participants []string `yaml:"participants"` // bot IDs that participate in this room
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// TestAgentConfigParseMinimal verifies that a minimal config YAML (with only
|
||||
// required fields) parses into AgentConfig without error.
|
||||
func TestAgentConfigParseMinimal(t *testing.T) {
|
||||
const minimalYAML = `
|
||||
agent:
|
||||
id: test-bot
|
||||
name: Test Bot
|
||||
enabled: true
|
||||
bus:
|
||||
nats_url: "nats://127.0.0.1:4250"
|
||||
ctrl_url: "http://127.0.0.1:8470"
|
||||
llm:
|
||||
primary:
|
||||
provider: openai
|
||||
model: gpt-4o
|
||||
`
|
||||
var cfg AgentConfig
|
||||
if err := yaml.Unmarshal([]byte(minimalYAML), &cfg); err != nil {
|
||||
t.Fatalf("failed to parse minimal config: %v", err)
|
||||
}
|
||||
if cfg.Agent.ID != "test-bot" {
|
||||
t.Errorf("expected agent.id=test-bot, got %q", cfg.Agent.ID)
|
||||
}
|
||||
if cfg.Bus.NatsURL != "nats://127.0.0.1:4250" {
|
||||
t.Errorf("expected bus.nats_url, got %q", cfg.Bus.NatsURL)
|
||||
}
|
||||
if cfg.LLM.Primary.Provider != "openai" {
|
||||
t.Errorf("expected provider=openai, got %q", cfg.LLM.Primary.Provider)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAgentConfigIgnoresRemovedSections verifies that YAML containing the
|
||||
// removed sections (agents, observability, resilience) still parses without
|
||||
// error. yaml.v3 silently ignores unknown keys.
|
||||
func TestAgentConfigIgnoresRemovedSections(t *testing.T) {
|
||||
const yamlWithRemoved = `
|
||||
agent:
|
||||
id: legacy-bot
|
||||
name: Legacy Bot
|
||||
enabled: true
|
||||
bus:
|
||||
nats_url: "nats://127.0.0.1:4250"
|
||||
ctrl_url: "http://127.0.0.1:8470"
|
||||
llm:
|
||||
primary:
|
||||
provider: openai
|
||||
model: gpt-4o
|
||||
|
||||
# These sections were removed from the schema but may still exist in old YAMLs.
|
||||
agents:
|
||||
peers:
|
||||
- id: other-bot
|
||||
capabilities: [general]
|
||||
room: "!abc:server.com"
|
||||
delegation:
|
||||
enabled: false
|
||||
protocol:
|
||||
format: json
|
||||
channel: matrix
|
||||
|
||||
observability:
|
||||
logging:
|
||||
level: info
|
||||
format: json
|
||||
metrics:
|
||||
enabled: false
|
||||
health:
|
||||
enabled: true
|
||||
port: 8080
|
||||
tracing:
|
||||
enabled: false
|
||||
|
||||
resilience:
|
||||
circuit_breaker:
|
||||
failure_threshold: 5
|
||||
timeout: 30s
|
||||
retry:
|
||||
max_attempts: 2
|
||||
backoff: exponential
|
||||
shutdown:
|
||||
timeout: 10s
|
||||
queue:
|
||||
enabled: true
|
||||
max_size: 100
|
||||
`
|
||||
var cfg AgentConfig
|
||||
if err := yaml.Unmarshal([]byte(yamlWithRemoved), &cfg); err != nil {
|
||||
t.Fatalf("parsing config with removed sections should succeed, got: %v", err)
|
||||
}
|
||||
if cfg.Agent.ID != "legacy-bot" {
|
||||
t.Errorf("expected agent.id=legacy-bot, got %q", cfg.Agent.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAgentConfigParseFull verifies that a config YAML with all active sections
|
||||
// parses correctly, including personality with communication.
|
||||
func TestAgentConfigParseFull(t *testing.T) {
|
||||
const fullYAML = `
|
||||
agent:
|
||||
id: full-bot
|
||||
name: Full Bot
|
||||
version: "1.0.0"
|
||||
enabled: true
|
||||
description: "A fully configured bot"
|
||||
tags: [test, full]
|
||||
|
||||
personality:
|
||||
tone: friendly
|
||||
verbosity: concise
|
||||
language: es
|
||||
role: "asistente general"
|
||||
communication:
|
||||
formality: semiformal
|
||||
humor: subtle
|
||||
personality: pragmatic
|
||||
response_style: structured
|
||||
quirks: ["usa analogias"]
|
||||
avoid_topics: ["politica"]
|
||||
catchphrases: ["interesante"]
|
||||
|
||||
llm:
|
||||
primary:
|
||||
provider: openai
|
||||
model: gpt-4o
|
||||
api_key_env: OPENAI_API_KEY
|
||||
max_tokens: 4096
|
||||
temperature: 0.7
|
||||
tool_use:
|
||||
enabled: true
|
||||
max_iterations: 5
|
||||
|
||||
bus:
|
||||
nats_url: "nats://127.0.0.1:4250"
|
||||
ctrl_url: "http://127.0.0.1:8470"
|
||||
handle: full
|
||||
threads:
|
||||
enabled: true
|
||||
auto_thread: false
|
||||
|
||||
tools:
|
||||
ssh:
|
||||
enabled: false
|
||||
http:
|
||||
enabled: true
|
||||
allowed_domains: ["api.example.com"]
|
||||
timeout: 10s
|
||||
|
||||
security:
|
||||
sanitize:
|
||||
enabled: true
|
||||
mode: warn
|
||||
min_severity: medium
|
||||
tool_rate_limit:
|
||||
enabled: true
|
||||
max_calls_per_min: 10
|
||||
|
||||
storage:
|
||||
base_path: "/data/full-bot"
|
||||
|
||||
memory:
|
||||
enabled: true
|
||||
window_size: 30
|
||||
|
||||
skills:
|
||||
enabled: true
|
||||
path: "skills/"
|
||||
categories: ["devops"]
|
||||
timeout: 60s
|
||||
`
|
||||
var cfg AgentConfig
|
||||
if err := yaml.Unmarshal([]byte(fullYAML), &cfg); err != nil {
|
||||
t.Fatalf("failed to parse full config: %v", err)
|
||||
}
|
||||
|
||||
// Verify key fields
|
||||
if cfg.Agent.ID != "full-bot" {
|
||||
t.Errorf("agent.id: got %q", cfg.Agent.ID)
|
||||
}
|
||||
if cfg.Personality.Communication.Humor != "subtle" {
|
||||
t.Errorf("personality.communication.humor: got %q", cfg.Personality.Communication.Humor)
|
||||
}
|
||||
if len(cfg.Personality.Communication.Quirks) != 1 {
|
||||
t.Errorf("personality.communication.quirks: expected 1, got %d", len(cfg.Personality.Communication.Quirks))
|
||||
}
|
||||
if !cfg.LLM.ToolUse.Enabled {
|
||||
t.Error("llm.tool_use.enabled should be true")
|
||||
}
|
||||
if !cfg.Tools.HTTP.Enabled {
|
||||
t.Error("tools.http.enabled should be true")
|
||||
}
|
||||
if cfg.Storage.BasePath != "/data/full-bot" {
|
||||
t.Errorf("storage.base_path: got %q", cfg.Storage.BasePath)
|
||||
}
|
||||
if !cfg.Memory.Enabled {
|
||||
t.Error("memory.enabled should be true")
|
||||
}
|
||||
if !cfg.Skills.Enabled {
|
||||
t.Error("skills.enabled should be true")
|
||||
}
|
||||
if !cfg.Security.Sanitize.Enabled {
|
||||
t.Error("security.sanitize.enabled should be true")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user