feat: add assistant bot with LLM integration and configuration

- Implemented the assistant bot with basic command handling and LLM routing.
- Created configuration file for the assistant bot with personality, behavior, and LLM settings.
- Added system prompt for the assistant bot to define its capabilities and limitations.
- Developed registration script for creating Matrix bot users via Synapse admin API.
- Introduced common development scripts for agent management (start, stop, list, logs).
- Scaffolded new agent creation script to streamline the addition of new agents.
- Implemented agent removal script to disable agents without deleting data.
This commit is contained in:
2026-03-03 23:57:13 +00:00
parent c126187c5a
commit bd8e1432e5
19 changed files with 2142 additions and 93 deletions
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env bash
# _common.sh — sourced by all dev-scripts. Do not run directly.
set -euo pipefail
# ── Colores ────────────────────────────────────────────────────────────────
RED='\033[0;31m'
GRN='\033[0;32m'
YLW='\033[0;33m'
BLU='\033[0;34m'
DIM='\033[2m'
RST='\033[0m'
ok() { echo -e "${GRN}${RST} $*"; }
info() { echo -e "${BLU}${RST} $*"; }
warn() { echo -e "${YLW}!${RST} $*"; }
fail() { echo -e "${RED}${RST} $*" >&2; exit 1; }
dim() { echo -e "${DIM}$*${RST}"; }
# ── Repo root ──────────────────────────────────────────────────────────────
# Scripts can be called from any directory; we always operate from repo root.
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT"
# ── .env loader ───────────────────────────────────────────────────────────
load_env() {
local env_file="${1:-.env}"
[[ -f "$env_file" ]] || fail ".env not found at $REPO_ROOT/$env_file — copy .env.example and fill it in"
# Export only lines that are KEY=VALUE (skip comments and blanks)
set -o allexport
# shellcheck disable=SC1090
source <(grep -E '^[A-Z_]+=.+' "$env_file")
set +o allexport
}
# ── Go tooling ─────────────────────────────────────────────────────────────
GO=${GO_BIN:-go}
export PATH="$PATH:/usr/local/go/bin"
command -v "$GO" &>/dev/null || fail "go not found — install Go or set GO_BIN"
# ── Process helpers ────────────────────────────────────────────────────────
RUN_DIR="$REPO_ROOT/run"
mkdir -p "$RUN_DIR"
pid_file() { echo "$RUN_DIR/$1.pid"; }
log_file() { echo "$RUN_DIR/$1.log"; }
read_pid() {
local f; f="$(pid_file "$1")"
[[ -f "$f" ]] && cat "$f" || echo 0
}
is_running() {
local pid; pid="$(read_pid "$1")"
[[ "$pid" -gt 0 ]] && kill -0 "$pid" 2>/dev/null
}
agent_status() {
local id="$1" enabled="$2"
if [[ "$enabled" != "true" ]]; then
echo "disabled"
elif is_running "$id"; then
echo "running"
else
echo "stopped"
fi
}
# ── Agent discovery ────────────────────────────────────────────────────────
# Prints: id|version|enabled|description (one line per agent)
list_agents_raw() {
for cfg in agents/*/config.yaml; do
[[ -f "$cfg" ]] || continue
local id version enabled desc
id=$(grep -m1 '^ id:' "$cfg" | awk '{print $2}')
version=$(grep -m1 '^ version:' "$cfg" | awk '{print $2}' | tr -d '"')
enabled=$(grep -m1 '^ enabled:' "$cfg" | awk '{print $2}')
desc=$(grep -m1 '^ description:' "$cfg" | cut -d'"' -f2)
[[ -n "$id" ]] && echo "${id}|${version}|${enabled}|${desc}|${cfg}"
done
}
# ── Usage helper ──────────────────────────────────────────────────────────
need_arg() {
[[ -n "${1:-}" ]] || { echo "Usage: $0 <agent-id>"; exit 1; }
}
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# list.sh — muestra todos los agentes y su estado actual
# Uso: ./dev-scripts/list.sh
source "$(dirname "$0")/_common.sh"
printf "%-22s %-12s %-8s %s\n" "ID" "STATUS" "VERSION" "DESCRIPTION"
printf '%s\n' "$(printf '─%.0s' {1..70})"
while IFS='|' read -r id version enabled desc _cfg; do
status=$(agent_status "$id" "$enabled")
case "$status" in
running) label="${GRN}● running${RST}" ;;
stopped) label="${DIM}○ stopped${RST}" ;;
disabled) label="${YLW} disabled${RST}" ;;
*) label="$status" ;;
esac
# Truncate description
[[ ${#desc} -gt 38 ]] && desc="${desc:0:37}"
printf "%-22s " "$id"
printf "${label}"
printf " %-8s %s\n" "$version" "$desc"
done < <(list_agents_raw)
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# logs.sh — sigue los logs de uno o todos los agentes
#
# Uso:
# ./dev-scripts/logs.sh # tail -f de todos los logs activos
# ./dev-scripts/logs.sh assistant-bot # solo ese agente
# ./dev-scripts/logs.sh assistant-bot 100 # últimas 100 líneas
source "$(dirname "$0")/_common.sh"
TARGET="${1:-}"
LINES="${2:-50}"
log_files=()
while IFS='|' read -r id _version _enabled _desc _cfg; do
[[ -n "$TARGET" && "$id" != "$TARGET" ]] && continue
local_log="$(log_file "$id")"
[[ -f "$local_log" ]] && log_files+=("$local_log")
done < <(list_agents_raw)
if [[ "${#log_files[@]}" -eq 0 ]]; then
[[ -n "$TARGET" ]] && fail "No hay logs para '$TARGET' (¿ha sido iniciado alguna vez?)"
fail "No hay logs todavía — inicia algún agente primero"
fi
info "Siguiendo logs: ${log_files[*]}"
dim " Ctrl+C para salir"
echo ""
tail -n "$LINES" -f "${log_files[@]}"
+358
View File
@@ -0,0 +1,358 @@
#!/usr/bin/env bash
# new-agent.sh — genera el scaffold de un nuevo agente
#
# Uso:
# ./dev-scripts/new-agent.sh <agent-id> [displayname]
#
# Ejemplo:
# ./dev-scripts/new-agent.sh monitor-bot "Monitor Agent"
#
# Crea:
# agents/<agent-id>/config.yaml (basado en el assistant como plantilla)
# agents/<agent-id>/agent.go (reglas puras vacías, listo para extender)
# agents/<agent-id>/prompts/ (directorio para system prompt)
# agents/<agent-id>/data/ (directorio de datos, en .gitignore)
#
# También te recuerda los dos pasos manuales que quedan.
source "$(dirname "$0")/_common.sh"
load_env
need_arg "${1:-}"
ID="$1"
DISPLAYNAME="${2:-$ID}"
PACKAGE="$(echo "$ID" | tr '-' '_' | sed 's/_bot//')" # "monitor-bot" → "monitor"
DIR="agents/$ID"
[[ -d "$DIR" ]] && fail "Ya existe agents/$ID — ¿ya fue creado?"
info "Creando scaffold para $ID..."
mkdir -p "$DIR/prompts" "$DIR/data"
# ── config.yaml ────────────────────────────────────────────────────────────
cat > "$DIR/config.yaml" <<YAML
# ============================================
# IDENTIDAD
# ============================================
agent:
id: $ID
name: "$DISPLAYNAME"
version: "1.0.0"
enabled: true
description: "Descripción del agente $DISPLAYNAME"
tags: [$(echo "$ID" | tr '-' ',')]
# ============================================
# PERSONALIDAD Y COMPORTAMIENTO
# ============================================
personality:
tone: friendly
verbosity: concise
language: es
languages_supported: [es, en]
emoji_style: minimal
prefix: "🤖"
error_style: helpful
templates:
greeting: "Hola, soy $DISPLAYNAME. ¿En qué puedo ayudarte?"
unknown_command: "No reconozco ese comando. Escríbeme directamente."
permission_denied: "No tengo permiso para hacer eso."
error: "Algo salió mal: {{.Error}}"
success: "{{.Summary}}"
busy: "Procesando, dame un momento..."
behavior:
proactive: false
ask_confirmation: false
show_reasoning: false
thread_replies: true
typing_indicator: true
acknowledge_receipt: false
# ============================================
# LLM
# ============================================
llm:
primary:
provider: openai
model: gpt-4o
api_key_env: OPENAI_API_KEY
base_url: ""
max_tokens: 4096
temperature: 0.7
fallback:
provider: ""
model: ""
api_key_env: ""
base_url: ""
max_tokens: 0
temperature: 0
reasoning:
system_prompt_file: "prompts/system.md"
context_window: 16384
memory_messages: 20
tool_use:
enabled: false
max_iterations: 3
parallel_calls: false
rate_limit:
requests_per_minute: 30
tokens_per_minute: 100000
concurrent_requests: 3
# ============================================
# TOOLS — ajustar según necesidades del agente
# ============================================
tools:
ssh:
enabled: false
allowed_targets: []
forbidden_commands: []
timeout: 0s
max_concurrent: 0
require_confirmation: []
http:
enabled: false
allowed_domains: []
timeout: 0s
max_retries: 0
scripts:
enabled: false
scripts_dir: ""
allowed: []
timeout: 0s
sandbox: false
file_ops:
enabled: false
allowed_paths: []
read_only: true
mcp:
enabled: false
servers: []
expose:
port: 0
tools: []
# ============================================
# MATRIX
# ============================================
matrix:
homeserver: "${MATRIX_HOMESERVER}"
user_id: "@$ID:${MATRIX_SERVER_NAME}"
access_token_env: MATRIX_TOKEN_$(echo "$ID" | tr '[:lower:]-' '[:upper:]_')
device_id: "$(echo "$ID" | tr '[:lower:]-' '[:upper:]_')01"
encryption:
enabled: false
store_path: "./data/crypto/"
trust_mode: tofu
rooms:
listen: []
respond: []
admin: []
filters:
command_prefix: "!"
mention_respond: true
dm_respond: true
ignore_bots: true
ignore_users: []
min_power_level: 0
# ============================================
# INTER-AGENTES
# ============================================
agents:
peers: []
delegation:
enabled: false
can_delegate_to: []
can_receive_from: []
max_delegation_depth: 1
timeout: 30s
protocol:
format: json
channel: matrix
heartbeat_interval: 60s
# ============================================
# SSH
# ============================================
ssh:
defaults:
user: ""
port: 22
key_file_env: ""
known_hosts: ""
keepalive_interval: 0s
timeout: 0s
targets: {}
# ============================================
# SEGURIDAD
# ============================================
security:
roles:
admin:
users: ["@admin:\${MATRIX_SERVER_NAME}"]
actions: ["*"]
user:
users: ["*"]
actions: ["help"]
audit:
enabled: false
log_file: "./data/audit.log"
log_to_room: ""
include: []
secrets:
provider: env
# ============================================
# SCHEDULING
# ============================================
schedules: []
# ============================================
# OBSERVABILIDAD
# ============================================
observability:
logging:
level: info
format: json
output: stdout
file: "./data/$ID.log"
metrics:
enabled: false
port: 0
path: /metrics
export: prometheus
health:
enabled: true
port: 0
path: /healthz
tracing:
enabled: false
provider: ""
endpoint: ""
# ============================================
# RESILIENCIA
# ============================================
resilience:
circuit_breaker:
failure_threshold: 5
timeout: 30s
half_open_max: 2
retry:
max_attempts: 2
backoff: exponential
initial_delay: 1s
max_delay: 10s
shutdown:
timeout: 10s
drain_messages: true
save_state: false
state_file: ""
queue:
enabled: true
max_size: 50
priority_users: ["@admin:\${MATRIX_SERVER_NAME}"]
# ============================================
# ALMACENAMIENTO
# ============================================
storage:
state:
backend: sqlite
path: "./data/$ID.db"
cache:
enabled: true
backend: memory
ttl: 5m
max_entries: 200
history:
backend: sqlite
path: "./data/history.db"
retention: 168h
YAML
# ── agent.go ───────────────────────────────────────────────────────────────
cat > "$DIR/agent.go" <<GO
// Package $PACKAGE defines the pure rules for the $DISPLAYNAME.
package $PACKAGE
import "github.com/enmanuel/agents/pkg/decision"
// Rules returns the decision rules for the $ID.
func Rules() []decision.Rule {
return []decision.Rule{
{
Name: "help",
Match: decision.MatchCommand("help"),
Actions: []decision.Action{{
Kind: decision.ActionKindReply,
Reply: &decision.ReplyAction{
Content: "Soy $DISPLAYNAME. Escríbeme lo que necesitas.",
},
}},
},
// Catch-all: DMs y menciones van al LLM
{
Name: "llm-fallback",
Match: func(ctx decision.MessageContext) bool {
return ctx.IsDirectMsg || ctx.IsMention
},
Actions: []decision.Action{{
Kind: decision.ActionKindLLM,
LLM: &decision.LLMAction{},
}},
},
}
}
GO
# ── system prompt ──────────────────────────────────────────────────────────
cat > "$DIR/prompts/system.md" <<MD
# $DISPLAYNAME — System Prompt
Eres $DISPLAYNAME. Describe aquí el rol, capacidades y restricciones del agente.
## Rol
...
## Capacidades
...
## Restricciones
...
MD
ok "Scaffold creado en $DIR/"
echo ""
# ── Pasos siguientes ──────────────────────────────────────────────────────
echo -e "${YLW}Quedan 2 pasos manuales:${RST}"
echo ""
echo -e " ${BLU}1.${RST} Añade una línea en ${BLU}cmd/launcher/main.go${RST}:"
echo ""
echo -e ' import ('
echo -e " ${GRN}${PACKAGE}agent \"github.com/enmanuel/agents/agents/$ID\"${RST}"
echo -e ' )'
echo ""
echo -e ' var rulesRegistry = map[string]func() []decision.Rule{'
echo -e " ${GRN}\"$ID\": ${PACKAGE}agent.Rules,${RST}"
echo -e ' ...'
echo -e ' }'
echo ""
echo -e " ${BLU}2.${RST} Registra el bot en Matrix y añade el token a .env:"
echo ""
echo -e " ${DIM}./dev-scripts/register.sh $ID \"$DISPLAYNAME\"${RST}"
echo ""
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# register.sh — registra un nuevo bot en el servidor Matrix via Synapse admin API
#
# Uso:
# ./dev-scripts/register.sh <username> [displayname] [env-var-name]
#
# Ejemplos:
# ./dev-scripts/register.sh assistant-bot "Assistant" MATRIX_TOKEN_ASSISTANT
# ./dev-scripts/register.sh devops-bot "DevOps Agent" MATRIX_TOKEN_DEVOPS
#
# Requiere en .env:
# MATRIX_ADMIN_TOKEN=syt_...
# MATRIX_HOMESERVER=https://...
source "$(dirname "$0")/_common.sh"
load_env
need_arg "${1:-}"
USERNAME="$1"
DISPLAYNAME="${2:-$USERNAME}"
ENV_VAR="${3:-MATRIX_TOKEN_$(echo "$USERNAME" | tr '[:lower:]-' '[:upper:]_')}"
[[ -n "${MATRIX_ADMIN_TOKEN:-}" ]] || fail "MATRIX_ADMIN_TOKEN no está en .env"
[[ -n "${MATRIX_HOMESERVER:-}" ]] || fail "MATRIX_HOMESERVER no está en .env"
info "Registrando @${USERNAME}:${MATRIX_SERVER_NAME:-$MATRIX_HOMESERVER}..."
echo ""
"$GO" run ./cmd/register \
--homeserver "$MATRIX_HOMESERVER" \
--username "$USERNAME" \
--displayname "$DISPLAYNAME" \
--env-var "$ENV_VAR"
echo ""
dim " Copia las líneas de arriba a tu .env y luego corre:"
dim " ./dev-scripts/start.sh $USERNAME"
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# remove.sh — deshabilita un agente (enabled: false). No borra datos.
#
# Uso:
# ./dev-scripts/remove.sh assistant-bot
source "$(dirname "$0")/_common.sh"
need_arg "${1:-}"
TARGET="$1"
found=false
while IFS='|' read -r id _version _enabled _desc cfg; do
[[ "$id" != "$TARGET" ]] && continue
found=true
# Detener si está corriendo
if is_running "$id"; then
local_pid="$(read_pid "$id")"
info "Deteniendo $id (PID $local_pid)..."
kill -TERM "$local_pid" 2>/dev/null || true
sleep 1
kill -0 "$local_pid" 2>/dev/null && kill -9 "$local_pid" 2>/dev/null || true
rm -f "$(pid_file "$id")"
ok "$id detenido"
fi
# Marcar como disabled en el config (reemplaza solo la primera ocurrencia)
if grep -q 'enabled: true' "$cfg"; then
# sed compatible con Linux y macOS
sed -i 's/enabled: true/enabled: false/' "$cfg"
ok "$id marcado como disabled en $cfg"
else
warn "$id ya estaba marcado como disabled"
fi
dim " Datos preservados en agents/$id/data/"
done < <(list_agents_raw)
"$found" || fail "Agente '$TARGET' no encontrado"
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
# start.sh — inicia uno o todos los agentes habilitados en background
#
# Uso:
# ./dev-scripts/start.sh # inicia todos los habilitados
# ./dev-scripts/start.sh assistant-bot # inicia uno específico
source "$(dirname "$0")/_common.sh"
load_env
TARGET="${1:-}"
start_agent() {
local id="$1" cfg="$2"
local log; log="$(log_file "$id")"
local pid_f; pid_f="$(pid_file "$id")"
info "Iniciando $id..."
# Lanza el launcher en background, desacoplado del terminal
nohup "$GO" run ./cmd/launcher -c "$cfg" \
>> "$log" 2>&1 &
local pid=$!
echo "$pid" > "$pid_f"
# Espera un momento y verifica que el proceso siga vivo
sleep 1
if kill -0 "$pid" 2>/dev/null; then
ok "$id PID $pid → logs: $log"
else
rm -f "$pid_f"
fail "$id arrancó pero murió — revisa: tail -f $log"
fi
}
started=0
while IFS='|' read -r id version enabled desc cfg; do
# Filtrar por TARGET si se especificó uno
[[ -n "$TARGET" && "$id" != "$TARGET" ]] && continue
if [[ "$enabled" != "true" ]]; then
warn "$id (disabled en config, saltar)"
continue
fi
if is_running "$id"; then
warn "$id (ya corriendo, PID $(read_pid "$id"))"
continue
fi
start_agent "$id" "$cfg"
((started++)) || true
done < <(list_agents_raw)
[[ "$started" -eq 0 && -z "$TARGET" ]] && warn "Ningún agente iniciado."
[[ -n "$TARGET" && "$started" -eq 0 ]] && fail "Agente '$TARGET' no encontrado o ya está corriendo."
true
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# stop.sh — detiene uno o todos los agentes en ejecución
#
# Uso:
# ./dev-scripts/stop.sh # detiene todos los que estén corriendo
# ./dev-scripts/stop.sh assistant-bot # detiene uno específico
source "$(dirname "$0")/_common.sh"
TARGET="${1:-}"
stopped=0
while IFS='|' read -r id _version _enabled _desc _cfg; do
[[ -n "$TARGET" && "$id" != "$TARGET" ]] && continue
if ! is_running "$id"; then
dim " $id (no está corriendo)"
continue
fi
local_pid="$(read_pid "$id")"
kill -TERM "$local_pid" 2>/dev/null || true
# Espera hasta 5s a que muera limpiamente
for _ in {1..10}; do
kill -0 "$local_pid" 2>/dev/null || break
sleep 0.5
done
# SIGKILL si todavía sigue vivo
if kill -0 "$local_pid" 2>/dev/null; then
warn "$id no respondió a SIGTERM, enviando SIGKILL..."
kill -9 "$local_pid" 2>/dev/null || true
fi
rm -f "$(pid_file "$id")"
ok "$id detenido (PID $local_pid)"
((stopped++)) || true
done < <(list_agents_raw)
[[ "$stopped" -eq 0 && -z "$TARGET" ]] && dim "Ningún agente estaba corriendo."
[[ -n "$TARGET" && "$stopped" -eq 0 ]] && fail "Agente '$TARGET' no encontrado o no estaba corriendo."
true