merge: issue/0034-e2e-create-bot-skill — robot de prueba + E2E tests + scripts de automatización
Robot test-bot creado con pipeline completo (scaffold → register → verify → E2E). Scripts mejorados: create-full.sh --type robot, convert-to-robot.sh, notify-developer.sh. 14 E2E tests (7 funcionales + 7 pipeline) todos pasando. Fix: config loader ya no exige llm.provider para robots.
This commit is contained in:
+2
-1
@@ -89,7 +89,7 @@ cp e2e/.env.example e2e/.env # configurar credenciales
|
||||
```
|
||||
|
||||
- **Fixtures**: `e2e/fixtures/` — login E2EE (`element-auth.ts`), helpers de room (`matrix-room.ts`)
|
||||
- **Tests**: `e2e/tests/` — login, assistant-bot, asistente-2
|
||||
- **Tests**: `e2e/tests/` — login, assistant-bot, asistente-2, test-bot, create-bot-pipeline
|
||||
- **Assertions flexibles** para respuestas LLM (no-deterministicas), estrictas para commands (`!help`, `!ping`)
|
||||
- Documentacion completa: `e2e/README.md`
|
||||
|
||||
@@ -115,6 +115,7 @@ Templates: `agents/_template/` (agent) y `agents/_template_robot/` (robot).
|
||||
|----|------|-----|-------------|
|
||||
| assistant-bot | agent | GPT-4o | Asistente general, DMs |
|
||||
| asistente-2 | agent | GPT-4o | Asistente con tools |
|
||||
| test-bot | robot | — | Robot de prueba (E2E tests pipeline) |
|
||||
|
||||
## Build
|
||||
|
||||
|
||||
@@ -37,15 +37,23 @@ Si `$ARGUMENTS` contiene el agent-id, usarlo directamente: `$0` = agent-id, `$1`
|
||||
|
||||
### Paso 2: Ejecutar pipeline de scaffold
|
||||
|
||||
Para **agentes** (con LLM):
|
||||
```bash
|
||||
./dev-scripts/agent/create-full.sh <agent-id> "<display-name>"
|
||||
```
|
||||
|
||||
Este script ejecuta 4 etapas:
|
||||
Para **robots** (command-only, sin LLM):
|
||||
```bash
|
||||
./dev-scripts/agent/create-full.sh <agent-id> "<display-name>" --type robot
|
||||
```
|
||||
|
||||
El script ejecuta automaticamente:
|
||||
1. **Scaffold**: copia `_template/`, personaliza archivos, actualiza launcher
|
||||
2. **Build**: compila con `go build -tags goolm ./...`
|
||||
3. **Register**: crea usuario Matrix, genera token + password + pickle key
|
||||
4. **Verify E2EE**: genera cross-signing keys, recovery key
|
||||
5. **(robots)** **Convert**: convierte a robot (config minimo, sin prompts, `command_prefix: ""`)
|
||||
6. **Notify**: envia DM a los developers (`DEVELOPER_MATRIX_USERS` en `.env`) presentandose
|
||||
|
||||
Si alguna etapa falla, revisar el error y corregir antes de continuar.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name: create-bot
|
||||
description: >
|
||||
Crear un nuevo robot Matrix (command-only, sin LLM). Ejecuta el pipeline
|
||||
scaffold + build + register + verify, luego personaliza config.yaml y
|
||||
scaffold + build + register + verify + convert + notify, luego personaliza
|
||||
comandos custom segun los inputs del usuario.
|
||||
allowed-tools: Bash Read Write Edit Grep Glob Agent
|
||||
argument-hint: "<bot-id> [display-name]"
|
||||
@@ -34,60 +34,27 @@ Si `$ARGUMENTS` contiene el bot-id, usarlo directamente: `$0` = bot-id, `$1` = d
|
||||
2. Verificar que no existe `agents/<bot-id>/`
|
||||
3. Si faltan inputs, preguntar al usuario
|
||||
|
||||
### Paso 2: Ejecutar pipeline de scaffold
|
||||
### Paso 2: Ejecutar pipeline completo
|
||||
|
||||
```bash
|
||||
./dev-scripts/agent/create-full.sh <bot-id> "<display-name>"
|
||||
./dev-scripts/agent/create-full.sh <bot-id> "<display-name>" --type robot
|
||||
```
|
||||
|
||||
Este script ejecuta 4 etapas: scaffold → build → register Matrix → verify E2EE.
|
||||
Este script ejecuta 6 etapas automaticamente:
|
||||
1. **Scaffold**: crea agent.go, config.yaml, prompts/ desde template
|
||||
2. **Build**: verifica compilacion con `-tags goolm`
|
||||
3. **Register**: registra usuario Matrix, genera token + password + pickle key
|
||||
4. **Verify E2EE**: genera cross-signing keys + recovery key
|
||||
5. **Convert**: convierte a robot (config minimo, sin prompts, sin LLM, `command_prefix: ""`)
|
||||
6. **Notify**: envia DM a los developers (DEVELOPER_MATRIX_USERS) presentandose
|
||||
|
||||
Si alguna etapa falla, revisar el error y corregir antes de continuar.
|
||||
|
||||
### Paso 3: Convertir a robot
|
||||
### Paso 3: Personalizar config
|
||||
|
||||
El scaffold crea un agente por defecto. Convertirlo a robot:
|
||||
|
||||
#### 3.1 Reemplazar `agents/<bot-id>/agent.go`
|
||||
|
||||
```go
|
||||
package <pkgname> // sin guiones ni _bot: "ping-bot" → package ping
|
||||
|
||||
import (
|
||||
"github.com/enmanuel/agents/agents"
|
||||
"github.com/enmanuel/agents/pkg/decision"
|
||||
)
|
||||
|
||||
func init() {
|
||||
agents.Register("<bot-id>", Rules)
|
||||
}
|
||||
|
||||
// Rules returns nil — robots don't use decision rules.
|
||||
// All behavior is via RegisterCommand in the launcher.
|
||||
func Rules() []decision.Rule {
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
Package name = bot-id sin guiones ni `_bot` (ej: `ping-bot` → `package ping`).
|
||||
|
||||
#### 3.2 Reemplazar `agents/<bot-id>/config.yaml`
|
||||
|
||||
Consultar [templates/config.yaml.md](templates/config.yaml.md) para el template base.
|
||||
|
||||
Ajustes obligatorios:
|
||||
- `agent.id`: debe coincidir con el nombre del directorio
|
||||
- `agent.type: robot` (CRITICO — sin esto se lanza como agent completo)
|
||||
Editar `agents/<bot-id>/config.yaml`:
|
||||
- `agent.description`: la descripcion del usuario
|
||||
- `personality.prefix`: emoji representativo del bot
|
||||
- Env vars: normalizar bot-id → mayusculas, guiones → underscores (NUNCA eliminar sufijos)
|
||||
|
||||
#### 3.3 Eliminar `agents/<bot-id>/prompts/system.md`
|
||||
|
||||
Los robots no necesitan system prompt. Eliminar el directorio prompts/ completo:
|
||||
|
||||
```bash
|
||||
rm -rf agents/<bot-id>/prompts/
|
||||
```
|
||||
- `personality.prefix`: emoji representativo del bot (opcional)
|
||||
|
||||
### Paso 4: Crear comandos custom (si el usuario los pidio)
|
||||
|
||||
@@ -133,7 +100,7 @@ Luego registrar en `cmd/launcher/main.go` despues de `agents.NewRobot()`:
|
||||
```go
|
||||
if cfg.Agent.ID == "<bot-id>" {
|
||||
for _, cmd := range <pkg>.Commands() {
|
||||
r.RegisterCommand(cmd.Spec, cmd.Handler)
|
||||
robot.RegisterCommand(cmd.Spec, cmd.Handler)
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -153,7 +120,7 @@ Verificar y reportar al usuario:
|
||||
- [ ] `go build -tags goolm ./...` compila sin errores
|
||||
- [ ] `agents/<id>/agent.go` exporta `Rules()` que retorna `nil`
|
||||
- [ ] `agents/<id>/config.yaml` tiene `agent.type: robot` y `agent.id` coincide con directorio
|
||||
- [ ] `cmd/launcher/main.go` tiene blank import del paquete del bot
|
||||
- [ ] `cmd/launcher/main.go` tiene import del paquete del bot
|
||||
- [ ] `.env` contiene las 4 env vars: `MATRIX_TOKEN_<NORM>`, `MATRIX_PASSWORD_<NORM>`, `PICKLE_KEY_<NORM>`, `SSSS_RECOVERY_KEY_<NORM>`
|
||||
- [ ] No existe `agents/<id>/prompts/` (robots no necesitan system prompt)
|
||||
- [ ] Si tiene comandos custom, estan registrados en el launcher
|
||||
@@ -163,11 +130,11 @@ Informar al usuario:
|
||||
Robot <bot-id> creado. Para arrancar:
|
||||
./dev-scripts/server/start.sh
|
||||
|
||||
Comandos built-in: !help, !ping, !status, !info, !version
|
||||
Comandos built-in: help, ping, status, info, version
|
||||
Comandos custom: <lista si hay>
|
||||
(Sin prefijo ! — el robot acepta comandos directamente)
|
||||
|
||||
Archivos a revisar:
|
||||
agents/<bot-id>/agent.go — reglas (nil para robots)
|
||||
agents/<bot-id>/config.yaml — configuracion
|
||||
agents/<bot-id>/commands.go — comandos custom (si aplica)
|
||||
```
|
||||
@@ -183,6 +150,7 @@ Archivos a revisar:
|
||||
| Reglas | Si (agent.go con Rules) | nil |
|
||||
| Tools | Opcionales | No |
|
||||
| Memoria/Knowledge | Opcionales | No |
|
||||
| Prefijo comandos | `!` (obligatorio) | `""` (sin prefijo por defecto) |
|
||||
| Comandos built-in | help, ping, tools, tool, status, info, clear, prompts, version | help, ping, status, info, version |
|
||||
|
||||
## Notas importantes
|
||||
@@ -191,5 +159,5 @@ Archivos a revisar:
|
||||
- **Nunca commitear tokens ni passwords** — van en `.env`
|
||||
- **Homeserver**: `https://matrix-af2f3d.organic-machine.com`
|
||||
- **Server name**: `matrix-af2f3d.organic-machine.com`
|
||||
- Referencia de robot existente: `agents/_template_robot/`
|
||||
- Referencia de robot existente: `agents/test-bot/`
|
||||
- El bot-id DEBE coincidir entre directorio, config.yaml y `agents.Register()`
|
||||
|
||||
@@ -49,6 +49,10 @@ STAGING_HOST=10.0.2.10
|
||||
MONITORING_HOST=10.0.3.10
|
||||
BASTION_HOST=bastion.example.com
|
||||
|
||||
# ── Desarrolladores (notificación al crear bots/agentes) ─────
|
||||
# Lista separada por comas de usernames Matrix (sin @ ni :server)
|
||||
DEVELOPER_MATRIX_USERS=egutierrez
|
||||
|
||||
# ── Matrix rooms (opcionales — el assistant-bot opera en DMs) ─
|
||||
MATRIX_ROOM_DEVOPS=
|
||||
MATRIX_ROOM_ALERTS=
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// Package test es un agente plantilla (no lanzable).
|
||||
// Sirve como referencia canonica para crear nuevos agentes.
|
||||
// Al crear un nuevo agente, new-agent.sh reemplaza test y test-bot.
|
||||
package test
|
||||
|
||||
import (
|
||||
"github.com/enmanuel/agents/agents"
|
||||
"github.com/enmanuel/agents/pkg/decision"
|
||||
)
|
||||
|
||||
func init() {
|
||||
agents.Register("test-bot", Rules)
|
||||
}
|
||||
|
||||
// Rules devuelve las reglas de este agente (vacio para el template).
|
||||
func Rules() []decision.Rule {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
|
||||
"github.com/enmanuel/agents/pkg/command"
|
||||
"github.com/enmanuel/agents/pkg/decision"
|
||||
)
|
||||
|
||||
// CommandEntry pairs a spec with its handler.
|
||||
type CommandEntry struct {
|
||||
Spec command.Spec
|
||||
Handler func(ctx context.Context, msgCtx decision.MessageContext) string
|
||||
}
|
||||
|
||||
// Commands returns the custom command specs and handlers for test-bot.
|
||||
func Commands() []CommandEntry {
|
||||
return []CommandEntry{
|
||||
{
|
||||
Spec: command.Spec{
|
||||
Name: "echo",
|
||||
Description: "Repite el texto recibido",
|
||||
Usage: "!echo <texto>",
|
||||
},
|
||||
Handler: func(_ context.Context, msgCtx decision.MessageContext) string {
|
||||
if len(msgCtx.Args) == 0 {
|
||||
return "Uso: !echo <texto>"
|
||||
}
|
||||
return strings.Join(msgCtx.Args, " ")
|
||||
},
|
||||
},
|
||||
{
|
||||
Spec: command.Spec{
|
||||
Name: "dice",
|
||||
Aliases: []string{"dado"},
|
||||
Description: "Lanza un dado (1-6)",
|
||||
Usage: "!dice",
|
||||
},
|
||||
Handler: func(_ context.Context, _ decision.MessageContext) string {
|
||||
return fmt.Sprintf("%d", rand.Intn(6)+1)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
# ============================================
|
||||
# TEST-BOT — Robot de prueba (command-only, sin LLM)
|
||||
# ============================================
|
||||
# Robot para validar el pipeline de creacion de bots y E2E tests.
|
||||
# Solo responde a comandos (!xxx). Mensajes normales se ignoran.
|
||||
|
||||
agent:
|
||||
id: test-bot
|
||||
name: "Test Bot"
|
||||
version: "0.1.0"
|
||||
type: robot
|
||||
enabled: true
|
||||
template: false
|
||||
description: "Robot de prueba para validar el pipeline de creacion de bots"
|
||||
tags: [test, robot]
|
||||
|
||||
# ============================================
|
||||
# PERSONALIDAD (minima para robots)
|
||||
# ============================================
|
||||
personality:
|
||||
prefix: ""
|
||||
language: es
|
||||
|
||||
# ============================================
|
||||
# MATRIX
|
||||
# ============================================
|
||||
matrix:
|
||||
homeserver: "${MATRIX_HOMESERVER}"
|
||||
user_id: "@test-bot:${MATRIX_SERVER_NAME}"
|
||||
access_token_env: MATRIX_TOKEN_TEST_BOT
|
||||
device_id: "HXINOYBBUW"
|
||||
|
||||
encryption:
|
||||
enabled: true
|
||||
store_path: "./agents/test-bot/data/crypto/"
|
||||
pickle_key_env: PICKLE_KEY_TEST_BOT
|
||||
trust_mode: tofu
|
||||
recovery_key_env: SSSS_RECOVERY_KEY_TEST_BOT
|
||||
|
||||
rooms:
|
||||
listen: []
|
||||
respond: []
|
||||
admin: []
|
||||
|
||||
filters:
|
||||
command_prefix: "" # sin prefijo — todo mensaje es un posible comando
|
||||
mention_respond: false
|
||||
dm_respond: false
|
||||
ignore_bots: true
|
||||
ignore_users: []
|
||||
unauthorized_response: silent
|
||||
min_power_level: 0
|
||||
|
||||
threads:
|
||||
enabled: true
|
||||
auto_thread: false
|
||||
|
||||
# ============================================
|
||||
# SEGURIDAD
|
||||
# ============================================
|
||||
security:
|
||||
audit:
|
||||
enabled: false
|
||||
log_file: ""
|
||||
log_to_room: ""
|
||||
include: []
|
||||
|
||||
secrets:
|
||||
provider: env
|
||||
|
||||
sanitize:
|
||||
enabled: false
|
||||
mode: warn
|
||||
min_severity: medium
|
||||
disabled_patterns: []
|
||||
|
||||
tool_rate_limit:
|
||||
enabled: false
|
||||
max_calls_per_min: 10
|
||||
cleanup_interval_s: 60
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
_ "github.com/enmanuel/agents/agents/assistant-bot"
|
||||
_ "github.com/enmanuel/agents/agents/asistente-2"
|
||||
_ "github.com/enmanuel/agents/agents/meteorologo"
|
||||
testbot "github.com/enmanuel/agents/agents/test-bot"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -180,6 +181,13 @@ func main() {
|
||||
agentCleanup()
|
||||
continue
|
||||
}
|
||||
// Register agent-specific commands for robots
|
||||
if cfg.Agent.ID == "test-bot" {
|
||||
for _, cmd := range testbot.Commands() {
|
||||
robot.RegisterCommand(cmd.Spec, cmd.Handler)
|
||||
}
|
||||
}
|
||||
|
||||
runner = robot
|
||||
agentLogger.Info("created robot", "id", cfg.Agent.ID)
|
||||
} else {
|
||||
|
||||
Executable
+132
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env bash
|
||||
# convert-to-robot.sh — convierte un scaffold de agente a robot
|
||||
#
|
||||
# Uso:
|
||||
# ./dev-scripts/agent/convert-to-robot.sh <agent-id>
|
||||
#
|
||||
# Cambios:
|
||||
# 1. Reescribe config.yaml desde _template_robot (manteniendo Matrix/E2EE)
|
||||
# 2. Simplifica agent.go (Rules() retorna nil)
|
||||
# 3. Elimina prompts/ (robots no necesitan system prompt)
|
||||
# 4. Pone command_prefix: "" por defecto (sin prefijo)
|
||||
|
||||
source "$(dirname "$0")/../_common.sh"
|
||||
load_env
|
||||
|
||||
need_arg "${1:-}"
|
||||
|
||||
ID="$1"
|
||||
NORM="$(normalize_id "$ID")"
|
||||
PACKAGE="$(echo "$ID" | tr '-' '_' | sed 's/_bot//')"
|
||||
DIR="agents/$ID"
|
||||
DEVICE_ID=""
|
||||
|
||||
[[ -d "$DIR" ]] || fail "No existe agents/$ID — ejecuta create-full.sh primero"
|
||||
|
||||
info "Convirtiendo $ID a robot..."
|
||||
|
||||
# ── Extraer device_id del config actual ──────────────────────────────────
|
||||
if [[ -f "$DIR/config.yaml" ]]; then
|
||||
DEVICE_ID="$(grep -m1 'device_id:' "$DIR/config.yaml" | awk '{print $2}' | tr -d '"')"
|
||||
fi
|
||||
|
||||
# ── Reescribir config.yaml ───────────────────────────────────────────────
|
||||
cat > "$DIR/config.yaml" << YAML
|
||||
# ============================================
|
||||
# ${ID} — Robot (command-only, sin LLM)
|
||||
# ============================================
|
||||
agent:
|
||||
id: ${ID}
|
||||
name: "${2:-$ID}"
|
||||
version: "0.1.0"
|
||||
type: robot
|
||||
enabled: true
|
||||
template: false
|
||||
description: ""
|
||||
tags: [robot]
|
||||
|
||||
personality:
|
||||
prefix: ""
|
||||
language: es
|
||||
|
||||
matrix:
|
||||
homeserver: "\${MATRIX_HOMESERVER}"
|
||||
user_id: "@${ID}:\${MATRIX_SERVER_NAME}"
|
||||
access_token_env: MATRIX_TOKEN_${NORM}
|
||||
device_id: "${DEVICE_ID}"
|
||||
|
||||
encryption:
|
||||
enabled: true
|
||||
store_path: "./agents/${ID}/data/crypto/"
|
||||
pickle_key_env: PICKLE_KEY_${NORM}
|
||||
trust_mode: tofu
|
||||
recovery_key_env: SSSS_RECOVERY_KEY_${NORM}
|
||||
|
||||
rooms:
|
||||
listen: []
|
||||
respond: []
|
||||
admin: []
|
||||
|
||||
filters:
|
||||
command_prefix: ""
|
||||
mention_respond: false
|
||||
dm_respond: false
|
||||
ignore_bots: true
|
||||
ignore_users: []
|
||||
unauthorized_response: silent
|
||||
min_power_level: 0
|
||||
|
||||
threads:
|
||||
enabled: true
|
||||
auto_thread: false
|
||||
|
||||
security:
|
||||
audit:
|
||||
enabled: false
|
||||
log_file: ""
|
||||
log_to_room: ""
|
||||
include: []
|
||||
secrets:
|
||||
provider: env
|
||||
sanitize:
|
||||
enabled: false
|
||||
mode: warn
|
||||
min_severity: medium
|
||||
disabled_patterns: []
|
||||
tool_rate_limit:
|
||||
enabled: false
|
||||
max_calls_per_min: 10
|
||||
cleanup_interval_s: 60
|
||||
YAML
|
||||
|
||||
ok "config.yaml reescrito como robot (command_prefix: \"\", sin LLM)"
|
||||
|
||||
# ── Simplificar agent.go ─────────────────────────────────────────────────
|
||||
cat > "$DIR/agent.go" << GO
|
||||
package ${PACKAGE}
|
||||
|
||||
import (
|
||||
"github.com/enmanuel/agents/agents"
|
||||
"github.com/enmanuel/agents/pkg/decision"
|
||||
)
|
||||
|
||||
func init() {
|
||||
agents.Register("${ID}", Rules)
|
||||
}
|
||||
|
||||
// Rules returns nil — robots only respond to commands.
|
||||
func Rules() []decision.Rule {
|
||||
return nil
|
||||
}
|
||||
GO
|
||||
|
||||
ok "agent.go simplificado (Rules() retorna nil)"
|
||||
|
||||
# ── Eliminar prompts/ ────────────────────────────────────────────────────
|
||||
if [[ -d "$DIR/prompts" ]]; then
|
||||
rm -rf "$DIR/prompts"
|
||||
ok "prompts/ eliminado (robots no necesitan system prompt)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
ok "$ID convertido a robot"
|
||||
@@ -1,17 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
# create-full.sh — pipeline completo para crear un agente funcional
|
||||
# create-full.sh — pipeline completo para crear un agente o robot funcional
|
||||
#
|
||||
# Ejecuta en orden: scaffold → build → register → verify E2EE
|
||||
# Ejecuta en orden: scaffold → build → register → verify E2EE → [convert robot] → [notify dev]
|
||||
# NO arranca el agente — primero personalizar agent.go, config.yaml y prompts/system.md
|
||||
#
|
||||
# Uso:
|
||||
# ./dev-scripts/agent/create-full.sh <agent-id> "Display Name"
|
||||
#
|
||||
# Ejemplo:
|
||||
# ./dev-scripts/agent/create-full.sh monitor-bot "Monitor Agent"
|
||||
# ./dev-scripts/agent/create-full.sh <agent-id> "Display Name" # agente (default)
|
||||
# ./dev-scripts/agent/create-full.sh <agent-id> "Display Name" --type robot # robot
|
||||
#
|
||||
# Requisitos en .env:
|
||||
# MATRIX_ADMIN_TOKEN, MATRIX_HOMESERVER, MATRIX_SERVER_NAME
|
||||
# DEVELOPER_MATRIX_USERS (opcional, para notificación al developer)
|
||||
|
||||
source "$(dirname "$0")/../_common.sh"
|
||||
load_env
|
||||
@@ -20,17 +19,47 @@ need_arg "${1:-}"
|
||||
|
||||
ID="$1"
|
||||
DISPLAYNAME="${2:-$ID}"
|
||||
TYPE="agent"
|
||||
NORM="$(normalize_id "$ID")"
|
||||
SCRIPT_DIR="$(dirname "$0")"
|
||||
|
||||
# Parse --type flag
|
||||
shift 2 2>/dev/null || shift 1 2>/dev/null || true
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--type)
|
||||
TYPE="${2:-agent}"
|
||||
shift 2
|
||||
;;
|
||||
--type=*)
|
||||
TYPE="${1#--type=}"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$TYPE" == "robot" ]]; then
|
||||
TYPE_LABEL="robot"
|
||||
TYPE_EMOJI="🤖"
|
||||
else
|
||||
TYPE_LABEL="agente"
|
||||
TYPE_EMOJI="🧠"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${BLU}═══════════════════════════════════════════════════════${RST}"
|
||||
echo -e "${BLU} Creando agente: ${GRN}$ID${BLU} ($DISPLAYNAME)${RST}"
|
||||
echo -e "${BLU} Creando ${TYPE_LABEL}: ${GRN}$ID${BLU} ($DISPLAYNAME) ${TYPE_EMOJI}${RST}"
|
||||
echo -e "${BLU}═══════════════════════════════════════════════════════${RST}"
|
||||
echo ""
|
||||
|
||||
# ── Paso 1: Scaffold ─────────────────────────────────────────────────────
|
||||
info "Paso 1/4 — Scaffold (agent.go, config.yaml, prompts, launcher)"
|
||||
TOTAL_STEPS=5
|
||||
[[ "$TYPE" == "robot" ]] && TOTAL_STEPS=6
|
||||
|
||||
info "Paso 1/${TOTAL_STEPS} — Scaffold (agent.go, config.yaml, prompts, launcher)"
|
||||
echo ""
|
||||
|
||||
"$SCRIPT_DIR/new-agent.sh" "$ID" "$DISPLAYNAME"
|
||||
@@ -38,7 +67,7 @@ echo ""
|
||||
echo ""
|
||||
|
||||
# ── Paso 2: Verificar compilación ─────────────────────────────────────────
|
||||
info "Paso 2/4 — Verificando compilación..."
|
||||
info "Paso 2/${TOTAL_STEPS} — Verificando compilación..."
|
||||
|
||||
if "$GO" build -tags goolm ./... 2>&1; then
|
||||
ok "Compilación exitosa"
|
||||
@@ -49,7 +78,7 @@ fi
|
||||
echo ""
|
||||
|
||||
# ── Paso 3: Registrar en Matrix ──────────────────────────────────────────
|
||||
info "Paso 3/4 — Registrando en Matrix..."
|
||||
info "Paso 3/${TOTAL_STEPS} — Registrando en Matrix..."
|
||||
echo ""
|
||||
|
||||
# Reload .env in case new-agent.sh or previous steps changed it
|
||||
@@ -60,7 +89,7 @@ load_env
|
||||
echo ""
|
||||
|
||||
# ── Paso 4: Verificar E2EE ───────────────────────────────────────────────
|
||||
info "Paso 4/4 — Verificación E2EE (cross-signing + recovery key)..."
|
||||
info "Paso 4/${TOTAL_STEPS} — Verificación E2EE (cross-signing + recovery key)..."
|
||||
echo ""
|
||||
|
||||
# Reload .env to pick up token, password, pickle key from register.sh
|
||||
@@ -70,15 +99,44 @@ load_env
|
||||
|
||||
echo ""
|
||||
|
||||
# ── Paso 5 (robots): Convertir a robot ───────────────────────────────────
|
||||
if [[ "$TYPE" == "robot" ]]; then
|
||||
info "Paso 5/${TOTAL_STEPS} — Convirtiendo a robot..."
|
||||
echo ""
|
||||
|
||||
"$SCRIPT_DIR/convert-to-robot.sh" "$ID" "$DISPLAYNAME"
|
||||
|
||||
# Rebuild after conversion
|
||||
info "Recompilando tras conversión..."
|
||||
"$GO" build -tags goolm ./... 2>&1 || fail "Error de compilación tras conversión a robot"
|
||||
ok "Recompilación exitosa"
|
||||
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# ── Paso final: Notificar al developer ───────────────────────────────────
|
||||
NOTIFY_STEP=$TOTAL_STEPS
|
||||
info "Paso ${NOTIFY_STEP}/${TOTAL_STEPS} — Notificando a desarrolladores..."
|
||||
echo ""
|
||||
|
||||
# Reload .env (verify.sh may have added recovery key)
|
||||
load_env
|
||||
|
||||
"$SCRIPT_DIR/notify-developer.sh" "$ID" "$TYPE" "$DISPLAYNAME" || true
|
||||
|
||||
echo ""
|
||||
|
||||
# ── Resumen ──────────────────────────────────────────────────────────────
|
||||
echo -e "${GRN}═══════════════════════════════════════════════════════${RST}"
|
||||
echo -e "${GRN} ✓ Agente $ID creado exitosamente${RST}"
|
||||
echo -e "${GRN} ✓ ${TYPE_LABEL^} $ID creado exitosamente ${TYPE_EMOJI}${RST}"
|
||||
echo -e "${GRN}═══════════════════════════════════════════════════════${RST}"
|
||||
echo ""
|
||||
echo -e " ${BLU}Archivos creados:${RST}"
|
||||
echo -e " agents/$ID/agent.go"
|
||||
echo -e " agents/$ID/config.yaml"
|
||||
echo -e " agents/$ID/prompts/system.md"
|
||||
if [[ "$TYPE" != "robot" ]]; then
|
||||
echo -e " agents/$ID/prompts/system.md"
|
||||
fi
|
||||
echo ""
|
||||
echo -e " ${BLU}Variables en .env:${RST}"
|
||||
echo -e " MATRIX_TOKEN_${NORM}"
|
||||
@@ -87,15 +145,23 @@ echo -e " PICKLE_KEY_${NORM}"
|
||||
echo -e " SSSS_RECOVERY_KEY_${NORM}"
|
||||
echo ""
|
||||
echo -e " ${BLU}Launcher actualizado:${RST}"
|
||||
echo -e " cmd/launcher/main.go (import + rulesRegistry)"
|
||||
echo -e " cmd/launcher/main.go (import)"
|
||||
echo ""
|
||||
echo -e "${YLW}Siguiente paso:${RST}"
|
||||
echo ""
|
||||
echo -e " 1. Personalizar los archivos del agente:"
|
||||
echo -e " ${DIM}agents/$ID/agent.go${RST} — reglas de decisión"
|
||||
echo -e " ${DIM}agents/$ID/config.yaml${RST} — LLM, tools, personalidad"
|
||||
echo -e " ${DIM}agents/$ID/prompts/system.md${RST} — system prompt"
|
||||
if [[ "$TYPE" == "robot" ]]; then
|
||||
echo -e " 1. Añadir comandos custom:"
|
||||
echo -e " ${DIM}agents/$ID/commands.go${RST}"
|
||||
echo ""
|
||||
echo -e " 2. Registrar comandos en el launcher:"
|
||||
echo -e " ${DIM}cmd/launcher/main.go${RST}"
|
||||
else
|
||||
echo -e " 1. Personalizar los archivos del agente:"
|
||||
echo -e " ${DIM}agents/$ID/agent.go${RST} — reglas de decisión"
|
||||
echo -e " ${DIM}agents/$ID/config.yaml${RST} — LLM, tools, personalidad"
|
||||
echo -e " ${DIM}agents/$ID/prompts/system.md${RST} — system prompt"
|
||||
fi
|
||||
echo ""
|
||||
echo -e " 2. Arrancar:"
|
||||
echo -e " Arrancar:"
|
||||
echo -e " ${DIM}./dev-scripts/server/start.sh${RST}"
|
||||
echo ""
|
||||
|
||||
Executable
+95
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env bash
|
||||
# notify-developer.sh — envía DM a los desarrolladores al crear un bot/agente
|
||||
#
|
||||
# Uso:
|
||||
# ./dev-scripts/agent/notify-developer.sh <agent-id> <type> <display-name>
|
||||
#
|
||||
# Requisitos en .env:
|
||||
# DEVELOPER_MATRIX_USERS — lista separada por comas de usernames Matrix
|
||||
# Ejemplo: DEVELOPER_MATRIX_USERS=egutierrez,admin
|
||||
# MATRIX_TOKEN_<NORM> — token del bot recién creado
|
||||
# MATRIX_HOMESERVER, MATRIX_SERVER_NAME
|
||||
|
||||
source "$(dirname "$0")/../_common.sh"
|
||||
load_env
|
||||
|
||||
ID="${1:-}"
|
||||
TYPE="${2:-agent}"
|
||||
DISPLAYNAME="${3:-$ID}"
|
||||
NORM="$(normalize_id "$ID")"
|
||||
|
||||
[[ -z "$ID" ]] && { warn "notify-developer: se necesita agent-id"; exit 0; }
|
||||
|
||||
# ── Obtener token del bot ────────────────────────────────────────────────
|
||||
TOKEN_VAR="MATRIX_TOKEN_${NORM}"
|
||||
TOKEN="${!TOKEN_VAR:-}"
|
||||
|
||||
if [[ -z "$TOKEN" ]]; then
|
||||
warn "notify-developer: $TOKEN_VAR no encontrado en .env — saltando notificación"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Obtener lista de desarrolladores ─────────────────────────────────────
|
||||
if [[ -z "${DEVELOPER_MATRIX_USERS:-}" ]]; then
|
||||
warn "notify-developer: DEVELOPER_MATRIX_USERS no definido en .env — saltando"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Construir mensaje ────────────────────────────────────────────────────
|
||||
if [[ "$TYPE" == "robot" ]]; then
|
||||
EMOJI="🤖"
|
||||
TYPE_LABEL="Robot"
|
||||
COMMANDS_MSG="Mis comandos: help, ping, status, info, version"
|
||||
else
|
||||
EMOJI="🧠"
|
||||
TYPE_LABEL="Agente"
|
||||
COMMANDS_MSG="Escríbeme directamente o usa !help para ver mis comandos"
|
||||
fi
|
||||
|
||||
MSG="${EMOJI} ¡Hola! Soy **${DISPLAYNAME}** (${TYPE_LABEL}). Acabo de ser creado. ${COMMANDS_MSG}."
|
||||
|
||||
# ── Enviar DM a cada desarrollador ───────────────────────────────────────
|
||||
IFS=',' read -ra DEVS <<< "$DEVELOPER_MATRIX_USERS"
|
||||
|
||||
for dev in "${DEVS[@]}"; do
|
||||
dev="$(echo "$dev" | xargs)" # trim spaces
|
||||
[[ -z "$dev" ]] && continue
|
||||
|
||||
USER_ID="@${dev}:${MATRIX_SERVER_NAME}"
|
||||
info "Enviando DM de $ID a $USER_ID..."
|
||||
|
||||
# Crear DM room (o reutilizar existente)
|
||||
ROOM_RESP=$(curl -sf -X POST "${MATRIX_HOMESERVER}/_matrix/client/v3/createRoom" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"is_direct\": true,
|
||||
\"invite\": [\"${USER_ID}\"],
|
||||
\"preset\": \"trusted_private_chat\"
|
||||
}" 2>&1) || {
|
||||
warn " No se pudo crear DM room con $USER_ID"
|
||||
continue
|
||||
}
|
||||
|
||||
ROOM_ID=$(echo "$ROOM_RESP" | grep -o '"room_id":"[^"]*"' | cut -d'"' -f4)
|
||||
if [[ -z "$ROOM_ID" ]]; then
|
||||
warn " Respuesta inesperada al crear room: $ROOM_RESP"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Enviar mensaje
|
||||
TXN_ID="notify-$(date +%s%N)"
|
||||
SEND_RESP=$(curl -sf -X PUT \
|
||||
"${MATRIX_HOMESERVER}/_matrix/client/v3/rooms/${ROOM_ID}/send/m.room.message/${TXN_ID}" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"msgtype\": \"m.text\",
|
||||
\"body\": \"${MSG}\"
|
||||
}" 2>&1) || {
|
||||
warn " No se pudo enviar mensaje a $USER_ID"
|
||||
continue
|
||||
}
|
||||
|
||||
ok "DM enviado a $USER_ID"
|
||||
done
|
||||
@@ -44,5 +44,5 @@ afectados y notas de implementacion.
|
||||
| 31 | Expandir file tools (write, list, append, delete) | [0031-expand-file-tools.md](completed/0031-expand-file-tools.md) | completado |
|
||||
| 32 | E2E: verificar skill /create-agent | [0032-e2e-create-agent-skill.md](0032-e2e-create-agent-skill.md) | pendiente |
|
||||
| 33 | Comandos de robots sin prefijo ! | [0033-bot-commands-no-prefix.md](completed/0033-bot-commands-no-prefix.md) | completado |
|
||||
| 34 | E2E: verificar skill /create-bot | [0034-e2e-create-bot-skill.md](0034-e2e-create-bot-skill.md) | pendiente |
|
||||
| 34 | E2E: verificar skill /create-bot | [0034-e2e-create-bot-skill.md](completed/0034-e2e-create-bot-skill.md) | completado |
|
||||
| 35 | Audit trail + comando !metrics | [0035-audit-trail-metrics.md](completed/0035-audit-trail-metrics.md) | completado |
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
const REPO_ROOT = path.resolve(__dirname, "../..");
|
||||
const AGENT_DIR = path.join(REPO_ROOT, "agents/test-bot");
|
||||
const LAUNCHER = path.join(REPO_ROOT, "cmd/launcher/main.go");
|
||||
|
||||
test.describe("create-bot pipeline (validacion estructural)", () => {
|
||||
test("agents/test-bot/agent.go existe y exporta Rules()", () => {
|
||||
const agentGo = path.join(AGENT_DIR, "agent.go");
|
||||
expect(fs.existsSync(agentGo)).toBe(true);
|
||||
|
||||
const content = fs.readFileSync(agentGo, "utf-8");
|
||||
expect(content).toContain("func Rules()");
|
||||
expect(content).toContain('agents.Register("test-bot"');
|
||||
expect(content).toContain("return nil");
|
||||
});
|
||||
|
||||
test("agents/test-bot/config.yaml tiene type: robot", () => {
|
||||
const configYaml = path.join(AGENT_DIR, "config.yaml");
|
||||
expect(fs.existsSync(configYaml)).toBe(true);
|
||||
|
||||
const content = fs.readFileSync(configYaml, "utf-8");
|
||||
expect(content).toMatch(/type:\s*robot/);
|
||||
expect(content).toMatch(/id:\s*test-bot/);
|
||||
expect(content).toMatch(/enabled:\s*true/);
|
||||
});
|
||||
|
||||
test("agents/test-bot NO tiene prompts/system.md", () => {
|
||||
const systemPrompt = path.join(AGENT_DIR, "prompts/system.md");
|
||||
expect(fs.existsSync(systemPrompt)).toBe(false);
|
||||
});
|
||||
|
||||
test("agents/test-bot/commands.go existe con Commands()", () => {
|
||||
const commandsGo = path.join(AGENT_DIR, "commands.go");
|
||||
expect(fs.existsSync(commandsGo)).toBe(true);
|
||||
|
||||
const content = fs.readFileSync(commandsGo, "utf-8");
|
||||
expect(content).toContain("func Commands()");
|
||||
expect(content).toContain('"echo"');
|
||||
expect(content).toContain('"dice"');
|
||||
});
|
||||
|
||||
test("cmd/launcher/main.go tiene import de test-bot", () => {
|
||||
const content = fs.readFileSync(LAUNCHER, "utf-8");
|
||||
expect(content).toContain("agents/test-bot");
|
||||
});
|
||||
|
||||
test("config.yaml tiene command_prefix vacio (sin prefijo !)", () => {
|
||||
const configYaml = path.join(AGENT_DIR, "config.yaml");
|
||||
const content = fs.readFileSync(configYaml, "utf-8");
|
||||
expect(content).toMatch(/command_prefix:\s*""/);
|
||||
});
|
||||
|
||||
test("config.yaml tiene encryption habilitada", () => {
|
||||
const configYaml = path.join(AGENT_DIR, "config.yaml");
|
||||
const content = fs.readFileSync(configYaml, "utf-8");
|
||||
// encryption.enabled should be true
|
||||
expect(content).toMatch(/encryption:[\s\S]*?enabled:\s*true/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
import { test, expect, handleElementDialogs } from "../fixtures/persistent-context";
|
||||
import {
|
||||
goToRoom,
|
||||
sendMessage,
|
||||
waitForBotReply,
|
||||
assertNoDecryptionErrors,
|
||||
} from "../fixtures/matrix-room";
|
||||
|
||||
test.describe("test-bot (robot)", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await handleElementDialogs(page);
|
||||
await goToRoom(page, "Test Bot");
|
||||
});
|
||||
|
||||
test("help lista comandos built-in y custom", async ({ page }) => {
|
||||
await sendMessage(page, "help");
|
||||
|
||||
const reply = await waitForBotReply(page, {
|
||||
timeout: 10_000,
|
||||
sender: "Test Bot",
|
||||
});
|
||||
expect(reply).toBeTruthy();
|
||||
expect(reply.toLowerCase()).toContain("help");
|
||||
expect(reply.toLowerCase()).toContain("ping");
|
||||
expect(reply.toLowerCase()).toContain("echo");
|
||||
expect(reply.toLowerCase()).toContain("dice");
|
||||
});
|
||||
|
||||
test("ping responde sin prefijo !", async ({ page }) => {
|
||||
await sendMessage(page, "ping");
|
||||
|
||||
const reply = await waitForBotReply(page, {
|
||||
timeout: 10_000,
|
||||
sender: "Test Bot",
|
||||
});
|
||||
expect(reply).toBeTruthy();
|
||||
});
|
||||
|
||||
test("!ping tambien funciona (retrocompatible)", async ({ page }) => {
|
||||
await sendMessage(page, "!ping");
|
||||
|
||||
const reply = await waitForBotReply(page, {
|
||||
timeout: 10_000,
|
||||
sender: "Test Bot",
|
||||
});
|
||||
expect(reply).toBeTruthy();
|
||||
});
|
||||
|
||||
test("echo repite el texto exacto", async ({ page }) => {
|
||||
await sendMessage(page, "echo hello world");
|
||||
|
||||
const reply = await waitForBotReply(page, {
|
||||
timeout: 10_000,
|
||||
sender: "Test Bot",
|
||||
});
|
||||
expect(reply).toBe("hello world");
|
||||
});
|
||||
|
||||
test("dice devuelve un numero entre 1 y 6", async ({ page }) => {
|
||||
await sendMessage(page, "dice");
|
||||
|
||||
const reply = await waitForBotReply(page, {
|
||||
timeout: 10_000,
|
||||
sender: "Test Bot",
|
||||
});
|
||||
expect(reply).toBeTruthy();
|
||||
const num = parseInt(reply.trim(), 10);
|
||||
expect(num).toBeGreaterThanOrEqual(1);
|
||||
expect(num).toBeLessThanOrEqual(6);
|
||||
});
|
||||
|
||||
test("comando desconocido muestra error", async ({ page }) => {
|
||||
await sendMessage(page, "unknowncommand");
|
||||
|
||||
const reply = await waitForBotReply(page, {
|
||||
timeout: 10_000,
|
||||
sender: "Test Bot",
|
||||
});
|
||||
expect(reply).toBeTruthy();
|
||||
expect(reply.toLowerCase()).toMatch(/desconocido|unknown|no reconozco/);
|
||||
});
|
||||
|
||||
test("no hay errores de E2EE en el timeline", async ({ page }) => {
|
||||
await assertNoDecryptionErrors(page);
|
||||
});
|
||||
});
|
||||
@@ -94,7 +94,7 @@ func validate(cfg *AgentConfig) error {
|
||||
if cfg.Matrix.UserID == "" {
|
||||
return fmt.Errorf("matrix.user_id is required")
|
||||
}
|
||||
if cfg.LLM.Primary.Provider == "" {
|
||||
if cfg.Agent.Type != "robot" && cfg.LLM.Primary.Provider == "" {
|
||||
return fmt.Errorf("llm.primary.provider is required")
|
||||
}
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user