docs: mover tasks 08-09 a completed y añadir tasks 10-11
Se mueven las tareas completadas (08-knowledge_por_agente, 09-command_system) al directorio .claude/tasks/completed/ para mantener organizado el backlog. Se añaden nuevas tareas planificadas: - 10-access-control.md: control de acceso por roles - 11-markdown-rendering.md: renderizado de markdown en mensajes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,305 @@
|
||||
# Tarea 08 — Knowledge por agente
|
||||
|
||||
## Objetivo
|
||||
|
||||
Cada agente tiene una carpeta `knowledge/` donde almacena documentos de conocimiento (markdown).
|
||||
El agente puede buscar, leer, escribir y mejorar su propio conocimiento usando tools siempre disponibles.
|
||||
El conocimiento es archivos reales — inspeccionables por humanos, editables, y se pueden sembrar con contenido inicial.
|
||||
|
||||
## Diseño
|
||||
|
||||
### Almacenamiento híbrido: archivos + índice FTS5
|
||||
|
||||
```
|
||||
agents/<id>/knowledge/ ← archivos .md reales (human-readable)
|
||||
├── go-patterns.md
|
||||
├── user-preferences.md
|
||||
└── matrix-tips.md
|
||||
|
||||
agents/<id>/data/knowledge.db ← índice SQLite FTS5 (búsqueda rápida)
|
||||
```
|
||||
|
||||
- Los documentos viven como archivos `.md` en `knowledge/`.
|
||||
- Un índice FTS5 en SQLite permite búsqueda full-text instantánea.
|
||||
- Al iniciar, se sincroniza: archivos → índice (detecta nuevos, modificados, eliminados).
|
||||
- Al escribir via tool, se actualiza archivo + índice atómicamente.
|
||||
|
||||
### Por qué archivos y no solo SQLite
|
||||
|
||||
1. **Sembrables**: se puede crear `knowledge/` con documentos iniciales antes de arrancar
|
||||
2. **Inspeccionables**: un humano puede leer/editar el conocimiento del agente
|
||||
3. **Git-friendly**: opcionalmente trackeable en el repo
|
||||
4. **Naturales**: el agente "escribe documentos", no inserta rows
|
||||
|
||||
---
|
||||
|
||||
## Arquitectura (pure core / impure shell)
|
||||
|
||||
### 1. Pure core: `pkg/knowledge/`
|
||||
|
||||
```go
|
||||
// pkg/knowledge/types.go
|
||||
package knowledge
|
||||
|
||||
import "time"
|
||||
|
||||
// Document represents a knowledge document.
|
||||
type Document struct {
|
||||
Slug string // filename sin extensión, e.g. "go-patterns"
|
||||
Title string // primera línea H1 del markdown, o slug humanizado
|
||||
Content string // contenido completo del archivo
|
||||
UpdatedAt time.Time // mtime del archivo
|
||||
}
|
||||
|
||||
// SearchResult is a document matched by a search query.
|
||||
type SearchResult struct {
|
||||
Slug string
|
||||
Title string
|
||||
Snippet string // fragmento relevante con match highlights
|
||||
Rank float64 // relevancia FTS5
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// pkg/knowledge/store.go
|
||||
package knowledge
|
||||
|
||||
import "context"
|
||||
|
||||
// Store is the pure interface for knowledge operations.
|
||||
// Implemented by shell/knowledge.
|
||||
type Store interface {
|
||||
// Search performs full-text search across all documents.
|
||||
Search(ctx context.Context, query string, limit int) ([]SearchResult, error)
|
||||
|
||||
// Get retrieves a document by slug.
|
||||
Get(ctx context.Context, slug string) (*Document, error)
|
||||
|
||||
// Put creates or updates a document (file + index).
|
||||
Put(ctx context.Context, doc Document) error
|
||||
|
||||
// Delete removes a document (file + index).
|
||||
Delete(ctx context.Context, slug string) error
|
||||
|
||||
// List returns all document slugs with titles.
|
||||
List(ctx context.Context) ([]Document, error)
|
||||
|
||||
// Sync re-indexes all files from disk. Called on startup.
|
||||
Sync(ctx context.Context) error
|
||||
|
||||
// Close releases resources.
|
||||
Close() error
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Impure shell: `shell/knowledge/`
|
||||
|
||||
```go
|
||||
// shell/knowledge/store.go
|
||||
package knowledge
|
||||
|
||||
// FileStore implements knowledge.Store using files + SQLite FTS5.
|
||||
type FileStore struct {
|
||||
dir string // path a agents/<id>/knowledge/
|
||||
dbPath string // path a agents/<id>/data/knowledge.db
|
||||
db *sql.DB
|
||||
logger *slog.Logger
|
||||
}
|
||||
```
|
||||
|
||||
**Schema SQLite:**
|
||||
|
||||
```sql
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS documents USING fts5(
|
||||
slug,
|
||||
title,
|
||||
content,
|
||||
updated_at UNINDEXED
|
||||
);
|
||||
```
|
||||
|
||||
**Operaciones:**
|
||||
|
||||
| Método | Archivos | SQLite FTS5 |
|
||||
|--------|----------|-------------|
|
||||
| `Sync()` | Lee todos los `.md` del dir | Reconstruye índice completo |
|
||||
| `Search()` | — | `SELECT slug, title, snippet(...) FROM documents WHERE documents MATCH ?` |
|
||||
| `Get()` | Lee `{slug}.md` | — |
|
||||
| `Put()` | Escribe `{slug}.md` | Upsert en FTS5 |
|
||||
| `Delete()` | Borra `{slug}.md` | Delete en FTS5 |
|
||||
| `List()` | — | `SELECT slug, title FROM documents` |
|
||||
|
||||
**Sync al startup:**
|
||||
1. Listar `*.md` en el directorio
|
||||
2. Para cada archivo: leer contenido, extraer título (primer `# ...`), calcular mtime
|
||||
3. `DELETE FROM documents` + re-insertar todo (rebuild completo, simple y correcto)
|
||||
4. Log: `knowledge_sync count=N`
|
||||
|
||||
**Slug rules:**
|
||||
- Solo `[a-z0-9-]`, máximo 64 chars
|
||||
- Derivado del nombre de archivo sin `.md`
|
||||
- El tool valida antes de escribir
|
||||
|
||||
### 3. Tools: `tools/knowledge.go`
|
||||
|
||||
Cuatro tools que el agente siempre tiene disponibles cuando knowledge está habilitado:
|
||||
|
||||
#### `knowledge_search`
|
||||
```
|
||||
Nombre: knowledge_search
|
||||
Descripción: Search your knowledge base for relevant documents. Returns matching snippets ranked by relevance.
|
||||
Parámetros:
|
||||
- query (string, required): Search terms or phrase
|
||||
- limit (integer, optional): Max results, default 5
|
||||
Retorna: Lista de resultados con slug, título y snippet
|
||||
```
|
||||
|
||||
#### `knowledge_read`
|
||||
```
|
||||
Nombre: knowledge_read
|
||||
Descripción: Read the full content of a knowledge document by its slug.
|
||||
Parámetros:
|
||||
- slug (string, required): Document slug (e.g. "go-patterns")
|
||||
Retorna: Contenido completo del documento
|
||||
```
|
||||
|
||||
#### `knowledge_write`
|
||||
```
|
||||
Nombre: knowledge_write
|
||||
Descripción: Create or update a knowledge document. Use this to save new knowledge or improve existing documents.
|
||||
Parámetros:
|
||||
- slug (string, required): Document slug (lowercase, hyphens, e.g. "matrix-tips")
|
||||
- content (string, required): Full markdown content of the document
|
||||
Retorna: Confirmación con slug y tamaño
|
||||
```
|
||||
|
||||
#### `knowledge_list`
|
||||
```
|
||||
Nombre: knowledge_list
|
||||
Descripción: List all documents in your knowledge base with their titles.
|
||||
Parámetros: ninguno
|
||||
Retorna: Lista de slugs con títulos y fecha de última actualización
|
||||
```
|
||||
|
||||
> **Nota:** No incluyo `knowledge_delete` por ahora. Los agentes deberían mejorar y ampliar, no borrar. Si se necesita, se añade después.
|
||||
|
||||
### 4. Config: `internal/config/schema.go`
|
||||
|
||||
```go
|
||||
type KnowledgeCfg struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Dir string `yaml:"dir"` // default: "./knowledge" (relativo al dir del agente)
|
||||
}
|
||||
```
|
||||
|
||||
Añadir a `ToolsCfg`:
|
||||
```go
|
||||
type ToolsCfg struct {
|
||||
// ... existentes ...
|
||||
Knowledge KnowledgeCfg `yaml:"knowledge"`
|
||||
}
|
||||
```
|
||||
|
||||
Config de ejemplo en `config.yaml`:
|
||||
```yaml
|
||||
tools:
|
||||
knowledge:
|
||||
enabled: true
|
||||
dir: "./knowledge" # opcional, default relativo al agente
|
||||
```
|
||||
|
||||
### 5. Registro en runtime: `agents/runtime.go`
|
||||
|
||||
En `buildToolRegistry()`, después de los memory tools:
|
||||
|
||||
```go
|
||||
if cfg.Tools.Knowledge.Enabled {
|
||||
knowledgeDir := resolveKnowledgeDir(cfg) // resolve relative to agent dir
|
||||
knowledgeDBPath := filepath.Join(cfg.Storage.DataDir, "knowledge.db")
|
||||
kStore, err := shellknowledge.New(knowledgeDir, knowledgeDBPath, logger)
|
||||
if err != nil {
|
||||
logger.Error("knowledge_store_init_failed", "err", err)
|
||||
} else {
|
||||
// Sync on startup
|
||||
if err := kStore.Sync(ctx); err != nil {
|
||||
logger.Error("knowledge_sync_failed", "err", err)
|
||||
}
|
||||
reg.Register(tools.NewKnowledgeSearch(kStore))
|
||||
reg.Register(tools.NewKnowledgeRead(kStore))
|
||||
reg.Register(tools.NewKnowledgeWrite(kStore))
|
||||
reg.Register(tools.NewKnowledgeList(kStore))
|
||||
logger.Debug("registered knowledge tools")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Plan de implementación (orden)
|
||||
|
||||
### Paso 1 — Pure types (`pkg/knowledge/`)
|
||||
- [ ] `pkg/knowledge/types.go` — Document, SearchResult
|
||||
- [ ] `pkg/knowledge/store.go` — Store interface
|
||||
|
||||
### Paso 2 — Config
|
||||
- [ ] Añadir `KnowledgeCfg` a `internal/config/schema.go` dentro de `ToolsCfg`
|
||||
|
||||
### Paso 3 — Shell store (`shell/knowledge/`)
|
||||
- [ ] `shell/knowledge/store.go` — FileStore con FTS5
|
||||
- Constructor `New(dir, dbPath, logger)`
|
||||
- Sync(), Search(), Get(), Put(), Delete(), List(), Close()
|
||||
- Validación de slugs
|
||||
- Extracción de título del markdown (primer `# `)
|
||||
|
||||
### Paso 4 — Tools (`tools/knowledge.go`)
|
||||
- [ ] `tools/knowledge.go` — NewKnowledgeSearch, NewKnowledgeRead, NewKnowledgeWrite, NewKnowledgeList
|
||||
- [ ] Interface `KnowledgeStore` en tools (subset de knowledge.Store, como se hizo con MemoryStore)
|
||||
|
||||
### Paso 5 — Registro en runtime
|
||||
- [ ] Modificar `buildToolRegistry()` en `agents/runtime.go`
|
||||
- [ ] Resolver directorio de knowledge relativo al agente
|
||||
|
||||
### Paso 6 — Activar en agentes existentes
|
||||
- [ ] Crear `agents/assistant-bot/knowledge/` con un documento semilla
|
||||
- [ ] Crear `agents/asistente-2/knowledge/` con un documento semilla
|
||||
- [ ] Actualizar `config.yaml` de ambos agentes: `tools.knowledge.enabled: true`
|
||||
- [ ] Actualizar system prompts para que el agente sepa que tiene knowledge tools
|
||||
|
||||
### Paso 7 — Tests
|
||||
- [ ] Test de `shell/knowledge/` — sync, search, put, get, list
|
||||
- [ ] Test de `tools/knowledge.go` — validación de slugs, parámetros
|
||||
- [ ] Build completo: `go build -tags goolm ./...`
|
||||
|
||||
---
|
||||
|
||||
## Ejemplo de uso por el agente
|
||||
|
||||
Un usuario le dice al bot: "¿Cómo configuro un webhook en Gitea?"
|
||||
|
||||
1. El agente llama `knowledge_search(query="gitea webhook")`
|
||||
2. Encuentra `gitea-admin.md` con snippet relevante
|
||||
3. Llama `knowledge_read(slug="gitea-admin")` para leer el documento completo
|
||||
4. Responde al usuario con la info
|
||||
5. Si descubre info nueva en la conversación, llama `knowledge_write(slug="gitea-webhooks", content="# Gitea Webhooks\n\n...")` para ampliar su base
|
||||
|
||||
## Diferencia con memory tools
|
||||
|
||||
| Aspecto | Memory (facts) | Knowledge (documents) |
|
||||
|---------|----------------|----------------------|
|
||||
| Granularidad | Key-value individual | Documentos completos |
|
||||
| Búsqueda | Por subject exacto | Full-text search (FTS5) |
|
||||
| Formato | Tripla (subject, key, value) | Markdown libre |
|
||||
| Propósito | Datos puntuales sobre users/temas | Base de conocimiento estructurada |
|
||||
| Persistencia | SQLite rows | Archivos .md + índice FTS5 |
|
||||
| Editable por humanos | No (solo via SQL) | Sí (archivos normales) |
|
||||
|
||||
---
|
||||
|
||||
## Notas de implementación
|
||||
|
||||
- **FTS5 y modernc/sqlite**: modernc.org/sqlite soporta FTS5 nativamente, no necesita CGO.
|
||||
- **Slugs**: validar con regexp `^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$` (min 2 chars).
|
||||
- **Título**: extraer primera línea que empiece con `# `. Si no hay, usar slug humanizado.
|
||||
- **Tamaño máximo por documento**: 64 KB (consistente con read_file tool).
|
||||
- **Directorio knowledge/ en .gitignore**: decisión del usuario. Se puede trackear o no.
|
||||
- **No embeddings**: FTS5 keyword search es suficiente para v1. Embeddings es extensión futura.
|
||||
@@ -0,0 +1,205 @@
|
||||
# Task 09 — Sistema de comandos directos (!command)
|
||||
|
||||
## Objetivo
|
||||
|
||||
Implementar un sistema de comandos que permita a los usuarios ejecutar acciones directamente via `!comando` sin depender del LLM. Soportar agentes "simple_bot" que no tienen LLM y solo responden a comandos.
|
||||
|
||||
## Contexto actual
|
||||
|
||||
- `message.Parse` ya detecta `CommandPrefix` (!) y extrae `Command` + `Args` en `MessageContext`
|
||||
- `decision.MatchCommand()` ya existe para matchear comandos en reglas
|
||||
- `tools.Registry` ya tiene `Execute(ctx, name, argsJSON)` para ejecutar tools
|
||||
- Cada agente define sus reglas en `agent.go` con `Rules() []decision.Rule`
|
||||
- El flujo actual: solo `!help` existe como comando hardcodeado en cada agente
|
||||
|
||||
## Problema
|
||||
|
||||
- Los comandos estan hardcodeados en cada `agent.go` como reglas individuales
|
||||
- No hay forma de ejecutar tools directamente sin pasar por el LLM
|
||||
- No hay comandos built-in compartidos entre agentes
|
||||
- No se puede crear un bot sin LLM (simple_bot)
|
||||
- El `!help` es estatico y no refleja las tools reales del agente
|
||||
|
||||
## Diseno
|
||||
|
||||
### Arquitectura (pure core / impure shell)
|
||||
|
||||
```
|
||||
pkg/command/ -> PURE: tipos Command, parser de args, specs built-in
|
||||
agents/runtime.go -> composicion: conecta commands con tools y shell
|
||||
```
|
||||
|
||||
### Tipos de comandos
|
||||
|
||||
1. **Built-in commands** (disponibles en todos los agentes):
|
||||
|
||||
| Comando | Descripcion |
|
||||
|------------|----------------------------------------------------|
|
||||
| `!help` | Lista comandos disponibles (built-in + custom) |
|
||||
| `!tools` | Lista tools registradas con descripcion |
|
||||
| `!ping` | Alive check, responde "pong" con timestamp |
|
||||
| `!status` | Info del agente: uptime, rooms activos, window sizes |
|
||||
| `!info` | Nombre, version, descripcion del agente |
|
||||
| `!clear` | Limpia ventana de conversacion del room actual |
|
||||
| `!version` | Version del agente |
|
||||
|
||||
2. **Tool commands** — ejecutar tools directas:
|
||||
```
|
||||
!tool <nombre> -> sin args
|
||||
!tool <nombre> key=value -> arg simple
|
||||
!tool <nombre> key="valor con espacios" -> arg con espacios
|
||||
!tool <nombre> key=value key2=value2 -> multiples args
|
||||
```
|
||||
Ejemplos:
|
||||
- `!tool ssh_command host=server1 command="uptime"`
|
||||
- `!tool current_time`
|
||||
- `!tool knowledge_search query="como configurar"`
|
||||
|
||||
3. **Custom commands** — definidos por cada agente en su `agent.go` via Rules con MatchCommand (como ahora, pero mejor integrados)
|
||||
|
||||
### Flujo de ejecucion
|
||||
|
||||
```
|
||||
Matrix event
|
||||
-> message.Parse (ya extrae Command + Args)
|
||||
-> handleEvent:
|
||||
1. Si hay Command (empieza con !prefix):
|
||||
a. Custom command del agente (rules con MatchCommand)? -> ejecutar regla
|
||||
b. Built-in command? -> ejecutar handler, responder
|
||||
c. "tool" command? -> parsear args, ejecutar via tools.Registry, responder
|
||||
d. No encontrado? -> responder "comando desconocido, usa !help"
|
||||
2. Si NO es comando: flujo actual (rules -> LLM fallback si hay LLM)
|
||||
3. Si NO es comando y NO hay LLM: ignorar (solo responde a comandos)
|
||||
```
|
||||
|
||||
**Nota**: las reglas custom del agente tienen prioridad sobre built-ins. Si un agente define una regla `MatchCommand("help")` propia, esa gana sobre el built-in.
|
||||
|
||||
### Nuevo paquete `pkg/command/` (puro)
|
||||
|
||||
```go
|
||||
// pkg/command/types.go
|
||||
|
||||
// Spec es la spec pura de un comando. Solo datos.
|
||||
type Spec struct {
|
||||
Name string
|
||||
Aliases []string // e.g. ["h"] para help
|
||||
Description string // descripcion corta para !help
|
||||
Usage string // e.g. "!tool <name> [key=value ...]"
|
||||
Hidden bool // no mostrar en !help
|
||||
}
|
||||
|
||||
// ParsedArgs resultado de parsear "key=value key2=value2"
|
||||
type ParsedArgs struct {
|
||||
Positional []string // args sin key=
|
||||
Named map[string]string // args con key=value
|
||||
Raw []string // args originales
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// pkg/command/parse.go
|
||||
|
||||
// ParseArgs convierte []string{"host=server1", "command=uptime"} en ParsedArgs. Puro.
|
||||
func ParseArgs(args []string) ParsedArgs { ... }
|
||||
|
||||
// ArgsToJSON convierte ParsedArgs.Named a JSON string para tools.Registry.Execute. Puro.
|
||||
func ArgsToJSON(named map[string]string) string { ... }
|
||||
```
|
||||
|
||||
```go
|
||||
// pkg/command/builtins.go
|
||||
|
||||
// Builtins retorna las specs de todos los comandos built-in. Puro.
|
||||
func Builtins() []Spec { ... }
|
||||
```
|
||||
|
||||
### Cambios en `agents/runtime.go`
|
||||
|
||||
```go
|
||||
// CommandHandler ejecuta un comando built-in y devuelve la respuesta texto.
|
||||
type CommandHandler func(ctx context.Context, msgCtx decision.MessageContext) string
|
||||
|
||||
// Nuevos campos en Agent:
|
||||
type Agent struct {
|
||||
// ... existente ...
|
||||
commands map[string]CommandHandler // built-in command handlers
|
||||
startTime time.Time // para !status
|
||||
}
|
||||
```
|
||||
|
||||
En `handleEvent`, el flujo cambia a:
|
||||
```go
|
||||
// 1. Evaluar reglas custom primero (pueden overridear built-ins)
|
||||
if msgCtx.Command != "" {
|
||||
actions := decision.Evaluate(msgCtx, a.rules)
|
||||
if len(actions) > 0 {
|
||||
// ejecutar como ahora (expand LLM actions, runner.Execute)
|
||||
return
|
||||
}
|
||||
// 2. Buscar en built-ins
|
||||
if handler, ok := a.commands[msgCtx.Command]; ok {
|
||||
reply := handler(ctx, msgCtx)
|
||||
a.matrix.SendText(ctx, roomID, reply)
|
||||
return
|
||||
}
|
||||
// 3. Comando desconocido
|
||||
a.matrix.SendText(ctx, roomID, "Comando desconocido. Usa !help")
|
||||
return
|
||||
}
|
||||
|
||||
// 4. Sin comando: LLM fallback (si hay LLM) o ignorar
|
||||
if a.llm == nil {
|
||||
return // simple_bot: solo responde a comandos
|
||||
}
|
||||
// ... flujo LLM actual (DM/mention -> LLM) ...
|
||||
```
|
||||
|
||||
### Simple bots (sin LLM)
|
||||
|
||||
Un simple_bot se configura sin seccion `llm` o con `llm.primary.provider: ""`:
|
||||
|
||||
```yaml
|
||||
agent:
|
||||
id: monitor-bot
|
||||
name: Monitor Bot
|
||||
enabled: true
|
||||
description: "Bot de monitoreo, solo comandos"
|
||||
|
||||
tools:
|
||||
ssh:
|
||||
enabled: true
|
||||
allowed_targets: ["webserver"]
|
||||
```
|
||||
|
||||
En `New()`, si no hay LLM configurado, `a.llm` queda nil. El bot solo responde a comandos.
|
||||
|
||||
## Tareas de implementacion
|
||||
|
||||
### Fase 1 — Core puro (`pkg/command/`)
|
||||
- [x] Crear `pkg/command/types.go` — tipos Spec, ParsedArgs
|
||||
- [x] Crear `pkg/command/parse.go` — ParseArgs, ArgsToJSON
|
||||
- [x] Crear `pkg/command/parse_test.go` — tests del parser
|
||||
- [x] Crear `pkg/command/builtins.go` — specs de los 7 comandos built-in + BuiltinNames()
|
||||
|
||||
### Fase 2 — Handlers en runtime (`agents/`)
|
||||
- [x] Agregar campos `commands`, `cmdAliases`, `startTime` al Agent struct
|
||||
- [x] Implementar handlers: help, tools, ping, info, version, clear, status
|
||||
- [x] Implementar handler `tool` — parsea args key=value, ejecuta via Registry, formatea respuesta
|
||||
- [x] Registrar todos los handlers en `New()` via `registerBuiltinCommands()`
|
||||
- [x] Modificar `handleEvent` — nuevo flujo: rules custom -> built-in -> comando desconocido -> LLM fallback
|
||||
- [x] Extraer `executeActions()` helper para reutilizar en ambos flujos
|
||||
|
||||
### Fase 3 — Simple bot support
|
||||
- [x] Hacer LLM opcional en `New()` (no fallar si no hay provider)
|
||||
- [x] Si `a.llm == nil` y no hay comando, ignorar mensaje
|
||||
- [ ] Verificar que un agente sin LLM arranca y responde a !help, !tool, !ping
|
||||
|
||||
### Fase 4 — Integracion con agentes existentes
|
||||
- [x] Eliminar regla `!help` hardcodeada de assistant-bot/agent.go
|
||||
- [x] Eliminar regla `!help` hardcodeada de asistente-2/agent.go
|
||||
- [x] Verificar que reglas custom (llm-all, etc.) siguen funcionando (build OK)
|
||||
- [ ] Test manual: !help, !tools, !tool current_time, !ping, !status, !clear, !info, !version
|
||||
|
||||
### Fase 5 (futura) — Simple bot de ejemplo
|
||||
- [ ] Crear agente simple_bot de ejemplo sin LLM
|
||||
- [ ] Documentar patron simple_bot
|
||||
Reference in New Issue
Block a user