Files
agents_and_robots/dev/issues/0015-multi-platform-telegram.md
T
egutierrez 2756557498 chore: renombrar issues a formato 4 dígitos (NNNN)
Se estandariza la numeración de todos los issues de 3 dígitos a 4 dígitos
(e.g. 005 → 0005, 010 → 0010) para mantener consistencia con la convención
definida en create_issue.md. Se actualiza el README con los nuevos nombres
y links. No hay cambios de contenido en los issues, solo renombrado.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 18:39:33 +00:00

212 lines
10 KiB
Markdown

# 015 — Soporte multi-plataforma: Telegram como segunda plataforma
## Objetivo
Desacoplar el runtime de agentes de Matrix e introducir abstracciones de plataforma que permitan conectar un mismo agente a multiples servicios de mensajeria. Implementar Telegram como primera plataforma adicional para validar el diseno.
## Contexto
- Actualmente `agents/runtime.go` depende directamente de `*matrix.Client` y `*matrix.Listener`
- `shell/effects/runner.go` ya define `MatrixSender` como interfaz, pero con nombre acoplado
- `decision.MessageContext` es **generico** — no tiene nada de Matrix
- Las reglas, LLM, tools y memoria son independientes de la plataforma
- El acoplamiento esta en: runtime.go, effects/runner.go, listener, y algunos tools (matrix_send)
## Prerequisitos
- Ninguno estricto. Se puede hacer de forma incremental sin romper Matrix.
---
## Tareas
### Fase 1: Abstracciones de plataforma en `pkg/platform/`
- [ ] **1.1** Crear `pkg/platform/types.go` con las interfaces puras:
```go
// Messenger envía mensajes a una plataforma de chat.
type Messenger interface {
SendText(ctx context.Context, roomID, text string) error
SendMarkdown(ctx context.Context, roomID, markdown string) error
SendReplyMarkdown(ctx context.Context, roomID, inReplyTo, markdown string) error
SendTyping(ctx context.Context, roomID string, typing bool) error
}
// EventSource escucha eventos de una plataforma y los entrega como MessageContext.
type EventSource interface {
Run(ctx context.Context) error
}
// Platform agrupa Messenger + EventSource para una plataforma concreta.
type Platform interface {
Messenger
EventSource
Name() string // "matrix", "telegram", "slack", etc.
}
```
Nota: estas interfaces van en `pkg/` porque son tipos puros (no ejecutan I/O, solo los definen).
- [ ] **1.2** Definir `PlatformID` como prefijo para room IDs multi-plataforma:
- Formato: `matrix:!abc123:server.com`, `telegram:chat_456`
- Crear helpers `PrefixRoomID(platform, rawID) string` y `ParseRoomID(prefixed) (platform, rawID)`
- Esto permite que la memoria y windows no mezclen contextos entre plataformas
### Fase 2: Adaptar shell/matrix/ a las interfaces
- [ ] **2.1** Verificar que `shell/matrix/Client` ya satisface `platform.Messenger` (deberia, con los metodos actuales). Anadir metodo `Name() string` que retorne `"matrix"`.
- [ ] **2.2** Refactorizar `shell/matrix/Listener` para que implemente `platform.EventSource`:
- El `EventHandler` callback ya recibe `decision.MessageContext` — solo necesita ajustar la firma de `Run(ctx)` si difiere
- Internamente sigue usando mautrix syncer, pero externamente expone la interfaz generica
- [ ] **2.3** Crear wrapper `shell/matrix/Platform` que componga Client + Listener e implemente `platform.Platform`
### Fase 3: Desacoplar runtime.go
- [ ] **3.1** Cambiar el campo `matrix *matrix.Client` en `Agent` struct por `messenger platform.Messenger`
- [ ] **3.2** Cambiar `listener *matrix.Listener` por `sources []platform.EventSource`
- [ ] **3.3** Actualizar `Run()` para arrancar multiples EventSources en goroutines:
```go
for _, src := range a.sources {
go src.Run(ctx)
}
```
- [ ] **3.4** Actualizar `handleEvent` para que no reciba `*event.Event` — actualmente solo usa `evt.RoomID` que ya esta en `MessageContext.RoomID`. Eliminar la dependencia de `mautrix/event`.
- [ ] **3.5** Actualizar todas las llamadas directas a `a.matrix.SendXxx()` y `a.matrix.SendTyping()` para usar `a.messenger.SendXxx()`. Puntos clave:
- `handleEvent` — typing indicator, command replies, unknown command
- `executeActions` — ya pasa por el runner, OK
- `handleTaskEvent` — typing indicator, send reply
- `runLLM` — tool call notices
- [ ] **3.6** Actualizar `shell/effects/runner.go`:
- Renombrar interfaz `MatrixSender` a `Messenger` (o importar `platform.Messenger`)
- El Runner ya recibe la interfaz, solo cambia el nombre
- [ ] **3.7** Actualizar `New()` constructor para recibir `[]platform.Platform` en vez de construir matrix.Client internamente. Mover la creacion de clientes de plataforma al launcher.
### Fase 4: Implementar shell/telegram/
- [ ] **4.1** Elegir libreria de Telegram Bot API para Go. Opciones:
- (A) `github.com/go-telegram-bot-api/telegram-bot-api/v5` — la mas popular, estable
- (B) `github.com/gotd/td` — cliente completo (MTProto), mas complejo
- (C) HTTP directo contra Bot API — minimo, sin dependencias extra
- **Recomendacion**: opcion (A) por madurez y simplicidad
- [ ] **4.2** Crear `shell/telegram/client.go`:
- Struct `Client` con el bot API client interno
- Constructor `New(cfg config.TelegramCfg) (*Client, error)`
- Implementar `platform.Messenger`:
- `SendText` — `tgbotapi.NewMessage(chatID, text)`
- `SendMarkdown` — `tgbotapi.NewMessage` con `ParseMode: "MarkdownV2"`
- `SendReplyMarkdown` — `ReplyToMessageID` en el message config
- `SendTyping` — `tgbotapi.NewChatAction(chatID, "typing")`
- [ ] **4.3** Crear `shell/telegram/listener.go`:
- Implementar `platform.EventSource`
- Modo long-polling con `GetUpdatesChan()` (webhook es mas complejo y requiere dominio publico)
- Convertir cada `tgbotapi.Update` a `decision.MessageContext`:
- `SenderID` = user ID de Telegram (string)
- `SenderName` = username o first_name
- `RoomID` = `telegram:<chat_id>` (con prefijo de plataforma)
- `Content` = texto del mensaje
- `IsDirectMsg` = true si chat.Type == "private"
- `IsMention` = true si el mensaje contiene @botname
- `Command` = parsear si empieza con `!` (o `/` que es la convencion Telegram)
- Llamar al mismo `handleEvent(ctx, msgCtx)` del Agent
- [ ] **4.4** Crear `shell/telegram/platform.go` que componga Client + Listener e implemente `platform.Platform`
### Fase 5: Configuracion
- [ ] **5.1** Anadir tipos de config en `internal/config/schema.go`:
```yaml
telegram:
enabled: false
bot_token_env: "TELEGRAM_TOKEN_BOT"
allowed_chats: [] # lista de chat IDs permitidos (vacio = todos)
command_prefix: "/" # convencion Telegram, ademas de "!"
```
Struct: `TelegramCfg` con campos `Enabled`, `BotTokenEnv`, `AllowedChats`, `CommandPrefix`
- [ ] **5.2** Anadir `TelegramCfg` al config principal del agente (junto a `MatrixCfg`)
- [ ] **5.3** Actualizar `internal/config/loader.go` para parsear la nueva seccion
- [ ] **5.4** Actualizar `cmd/launcher/main.go` para instanciar plataformas segun config:
```go
var platforms []platform.Platform
if cfg.Matrix.Enabled { platforms = append(platforms, matrixPlatform) }
if cfg.Telegram.Enabled { platforms = append(platforms, telegramPlatform) }
```
- [ ] **5.5** Anadir `TELEGRAM_TOKEN_<BOT>` a `.env.example`
### Fase 6: Tool matrix_send → platform_send
- [ ] **6.1** Evaluar si `matrix_send` tool debe ser generico o especifico:
- Opcion A: renombrar a `send_message` con parametro `platform` — mas flexible
- Opcion B: mantener `matrix_send` y anadir `telegram_send` — mas simple
- **Recomendacion**: opcion A a largo plazo, pero para esta task basta con que el LLM
responda por la misma plataforma que recibio el mensaje (ya lo hace via `handleEvent` → runner)
- `matrix_send` como tool explícita solo se usa para enviar a rooms arbitrarios; si no se necesita eso en Telegram, no hace falta `telegram_send` ahora
- [ ] **6.2** Si se opta por generalizar: crear `tools/send.go` con `NewPlatformSend(messenger platform.Messenger)` que el LLM pueda usar para enviar a cualquier plataforma
### Fase 7: Tests
- [ ] **7.1** Unit tests para `pkg/platform/types.go` — verificar que las interfaces compilan y los helpers de PlatformID funcionan
- [ ] **7.2** Unit tests para `shell/telegram/client.go` — mock del bot API, verificar conversion de mensajes
- [ ] **7.3** Unit tests para `shell/telegram/listener.go` — mock de updates, verificar conversion a MessageContext
- [ ] **7.4** Integration test: verificar que un Agent con dos plataformas (matrix mock + telegram mock) recibe y responde correctamente por ambas
- [ ] **7.5** Verificar que todos los agentes existentes siguen funcionando solo con Matrix (backward compat)
### Fase 8: Documentacion
- [ ] **8.1** Actualizar `CLAUDE.md` — anadir `shell/telegram/` a la estructura de directorios, actualizar diagrama de flujo
- [ ] **8.2** Actualizar `docs/creating-agents.md` con la seccion de configuracion multi-plataforma
- [ ] **8.3** Actualizar `.claude/rules/create_agent.md` para mencionar la seccion `telegram:` en config
- [ ] **8.4** Anadir a `README.md` la seccion de soporte Telegram
---
## Orden de ejecucion recomendado
1. **Fase 1** (interfaces) — base para todo lo demas
2. **Fase 2** (adaptar matrix) — asegurar que Matrix sigue funcionando con las nuevas interfaces
3. **Fase 3** (desacoplar runtime) — el refactor central; debe compilar y pasar tests con solo Matrix
4. **Fase 5** (config) — preparar el config antes de implementar Telegram
5. **Fase 4** (implementar telegram) — el codigo nuevo
6. **Fase 6** (tools) — ajustar si es necesario
7. **Fase 7** (tests) — validar todo
8. **Fase 8** (docs) — ultima, cuando todo este estable
## Decisiones de diseno pendientes
- **Memoria compartida vs separada**: Si un usuario habla por Matrix y por Telegram, son windows separadas (por el prefijo de plataforma en roomID). Podria unificarse en el futuro con un "user identity" cross-platform, pero no es necesario ahora.
- **Comandos `/` vs `!`**: Telegram usa `/` como convencion para comandos de bot. Soportar ambos prefijos (`/` y `!`) en el parser de comandos, configurable por plataforma.
- **Webhooks vs long-polling**: Empezar con long-polling por simplicidad. Webhook requiere HTTPS publico y es una optimizacion posterior.
- **Presence**: Matrix tiene presence (online/offline). Telegram no tiene equivalente nativo para bots. Abstraer como opcional en la interfaz.
- **Reactions**: Matrix tiene reactions (`m.reaction`). Telegram tiene limitaciones. No incluir en `Messenger` por ahora; dejarlo como extension especifica de plataforma.
## Dependencias nuevas
```
github.com/go-telegram-bot-api/telegram-bot-api/v5 # Telegram Bot API client
```
## Riesgos
- El refactor de runtime.go (Fase 3) es el mas delicado — cambia el corazon del sistema. Hacer commits atomicos despues de cada sub-tarea.
- Asegurar backward compatibility: un agente sin `telegram:` en su config debe funcionar exactamente como antes.