05668e398f
Implementa el comando built-in !metrics que lee los JSONL logs del dia actual usando shell/logger/query.go y calcula agregados en memoria: - Mensajes recibidos (handling event) - Comandos ejecutados (command_received) - Llamadas LLM (count, tokens totales, latencia media) - Llamadas a tools (count, errores) - Errores totales (nivel ERROR) - Total de entradas de log El comando se registra como built-in disponible para todos los agentes. Recibe logDir via Option pattern (WithLogDir) para no romper la firma de agents.New(). El launcher pasa logDir al crear cada agente. Formatea la salida como tabla markdown para Matrix. Incluye tests unitarios con logs JSONL sinteticos. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
130 lines
4.3 KiB
Go
130 lines
4.3 KiB
Go
package agents
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/enmanuel/agents/internal/config"
|
|
"github.com/enmanuel/agents/pkg/decision"
|
|
"github.com/enmanuel/agents/tools"
|
|
|
|
"log/slog"
|
|
)
|
|
|
|
// newTestAgent creates a minimal Agent for testing commands.
|
|
// Does NOT connect to Matrix or LLM.
|
|
func newTestAgent(logDir string) *Agent {
|
|
cfg := &config.AgentConfig{
|
|
Agent: config.AgentMeta{
|
|
ID: "test-bot",
|
|
Name: "Test Bot",
|
|
},
|
|
}
|
|
return &Agent{
|
|
cfg: cfg,
|
|
logDir: logDir,
|
|
toolReg: tools.NewRegistry(slog.Default()),
|
|
logger: slog.Default(),
|
|
startTime: time.Now(),
|
|
commands: make(map[string]CommandHandler),
|
|
cmdAliases: make(map[string]string),
|
|
}
|
|
}
|
|
|
|
func TestCmdMetrics_NoLogDir(t *testing.T) {
|
|
a := newTestAgent("")
|
|
result := a.cmdMetrics(context.Background(), decision.MessageContext{})
|
|
if !strings.Contains(result, "no configurado") {
|
|
t.Errorf("expected 'no configurado' message, got: %s", result)
|
|
}
|
|
}
|
|
|
|
func TestCmdMetrics_NoLogsToday(t *testing.T) {
|
|
dir := t.TempDir()
|
|
// Create the agent subdirectory but with no log files
|
|
agentDir := filepath.Join(dir, "test-bot")
|
|
os.MkdirAll(agentDir, 0o755)
|
|
|
|
a := newTestAgent(dir)
|
|
result := a.cmdMetrics(context.Background(), decision.MessageContext{})
|
|
if !strings.Contains(result, "No hay logs") {
|
|
t.Errorf("expected 'No hay logs' message, got: %s", result)
|
|
}
|
|
}
|
|
|
|
func TestCmdMetrics_AggregatesCorrectly(t *testing.T) {
|
|
dir := t.TempDir()
|
|
agentDir := filepath.Join(dir, "test-bot")
|
|
os.MkdirAll(agentDir, 0o755)
|
|
|
|
// Create a JSONL log file for today
|
|
today := time.Now().UTC().Format("2006-01-02")
|
|
logFile := filepath.Join(agentDir, today+".jsonl")
|
|
|
|
lines := []string{
|
|
`{"time":"2026-04-09T10:00:00Z","level":"DEBUG","msg":"handling event","agent_id":"test-bot","sender":"@user:example.com"}`,
|
|
`{"time":"2026-04-09T10:00:01Z","level":"INFO","msg":"command_received","agent_id":"test-bot","command":"help"}`,
|
|
`{"time":"2026-04-09T10:00:02Z","level":"DEBUG","msg":"handling event","agent_id":"test-bot","sender":"@user2:example.com"}`,
|
|
`{"time":"2026-04-09T10:01:00Z","level":"DEBUG","msg":"LLM responded","agent_id":"test-bot","content_len":100,"duration_ms":500}`,
|
|
`{"time":"2026-04-09T10:01:01Z","level":"DEBUG","msg":"LLM responded","agent_id":"test-bot","content_len":200,"duration_ms":300,"tokens_used":150}`,
|
|
`{"time":"2026-04-09T10:02:00Z","level":"INFO","msg":"tool_exec_end","agent_id":"test-bot","tool":"current_time","duration_ms":5}`,
|
|
`{"time":"2026-04-09T10:02:01Z","level":"WARN","msg":"tool_exec_error","agent_id":"test-bot","tool":"ssh_command","err":"timeout"}`,
|
|
`{"time":"2026-04-09T10:03:00Z","level":"ERROR","msg":"something_failed","agent_id":"test-bot"}`,
|
|
}
|
|
|
|
content := strings.Join(lines, "\n") + "\n"
|
|
if err := os.WriteFile(logFile, []byte(content), 0o644); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
|
|
a := newTestAgent(dir)
|
|
result := a.cmdMetrics(context.Background(), decision.MessageContext{})
|
|
|
|
// Verify the output contains expected metrics
|
|
checks := map[string]string{
|
|
"messages": "| Mensajes recibidos | 2 |",
|
|
"commands": "| Comandos ejecutados | 1 |",
|
|
"llm_calls": "| Llamadas LLM | 2 |",
|
|
"llm_tokens": "| Tokens LLM (total) | 150 |",
|
|
"tool_calls": "| Llamadas a tools | 2 |",
|
|
"tool_errors": "| Errores de tools | 1 |",
|
|
"errors": "| Errores totales | 1 |",
|
|
"entries": "| Entradas de log | 8 |",
|
|
}
|
|
|
|
for name, expected := range checks {
|
|
if !strings.Contains(result, expected) {
|
|
t.Errorf("missing %s: expected %q in output:\n%s", name, expected, result)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCmdMetrics_LLMLatencyAverage(t *testing.T) {
|
|
dir := t.TempDir()
|
|
agentDir := filepath.Join(dir, "test-bot")
|
|
os.MkdirAll(agentDir, 0o755)
|
|
|
|
today := time.Now().UTC().Format("2006-01-02")
|
|
logFile := filepath.Join(agentDir, today+".jsonl")
|
|
|
|
lines := []string{
|
|
`{"time":"2026-04-09T10:01:00Z","level":"DEBUG","msg":"LLM responded","duration_ms":400}`,
|
|
`{"time":"2026-04-09T10:01:01Z","level":"DEBUG","msg":"LLM responded","duration_ms":600}`,
|
|
}
|
|
|
|
content := strings.Join(lines, "\n") + "\n"
|
|
os.WriteFile(logFile, []byte(content), 0o644)
|
|
|
|
a := newTestAgent(dir)
|
|
result := a.cmdMetrics(context.Background(), decision.MessageContext{})
|
|
|
|
// Average of 400 and 600 = 500
|
|
if !strings.Contains(result, "| Latencia LLM (media) | 500 ms |") {
|
|
t.Errorf("expected average latency 500 ms in output:\n%s", result)
|
|
}
|
|
}
|