feat: додано Node Registry, GreenFood, Monitoring та Utils

This commit is contained in:
Apple
2025-11-21 00:35:41 -08:00
parent 31f3602047
commit e018b9ab68
74 changed files with 13948 additions and 0 deletions

View File

@@ -0,0 +1,556 @@
# CURSOR_INSTRUCTIONS.md
## telegram-gateway для DAARION / microdao
Мета: реалізувати сервіс `telegram-gateway`, який:
- працює через **Local Telegram Bot API** (`telegram-bot-api`), без SSL і без публічних вебхуків;
- отримує апдейти від **декількох Telegram-ботів** через **long polling**;
- публікує вхідні повідомлення в **NATS** як події `agent.telegram.update`;
- приймає HTTP-запити від DAGI/microdao для надсилання повідомлень назад у Telegram (`agent.telegram.send`);
- інтегрується з існуючим **Router** (HTTP API `http://router:9102`), але не залежить від нього жорстко.
Інфраструктура (вже піднята Warp-ом):
- `telegram-bot-api` (Local Bot API, `http://telegram-bot-api:8081`, всередині Docker-мережі).
- `nats` (`nats://nats:4222`).
- `telegram-gateway` (цей сервіс, FastAPI, порт `8000` всередині контейнера).
Все це описано в `docker-compose.yml` у каталозі `telegram-infrastructure/`.
---
## 0. Структура проєкту
Працюємо в каталозі:
`telegram-infrastructure/telegram-gateway/`
Очікувана структура:
```text
telegram-infrastructure/
docker-compose.yml
.env
telegram-gateway/
Dockerfile
requirements.txt (або pyproject.toml — див. нижче)
app/
__init__.py
main.py
config.py
nats_client.py
telegram_listener.py
models.py
bots_registry.py
```
**Завдання для Cursor:** створити/оновити ці файли згідно інструкції нижче.
---
## 1. Залежності (requirements.txt)
Онови `telegram-gateway/requirements.txt`, додавши:
```txt
fastapi
uvicorn[standard]
aiogram==3.*
nats-py
pydantic-settings
httpx
```
Якщо вже існують інші залежності — збережи їх.
---
## 2. Конфігурація (app/config.py)
Створи файл `app/config.py`:
```python
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
# Local Telegram Bot API (Docker service)
TELEGRAM_API_BASE: str = "http://telegram-bot-api:8081"
# NATS event bus
NATS_URL: str = "nats://nats:4222"
# Опційно: URL Router-а DAGI/microdao
ROUTER_BASE_URL: str = "http://router:9102"
# Через це поле можна включити debug-логування
DEBUG: bool = False
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
settings = Settings()
```
---
## 3. Моделі подій і DTO (app/models.py)
Створи `app/models.py` з Pydantic-схемами:
```python
from typing import Optional, Any, Dict
from pydantic import BaseModel
class TelegramUpdateEvent(BaseModel):
"""Подія 'agent.telegram.update' для NATS."""
agent_id: str
bot_id: str
chat_id: int
user_id: int
text: Optional[str] = None
raw_update: Dict[str, Any]
class TelegramSendCommand(BaseModel):
"""Команда від DAGI/microdao для надсилання повідомлення в Telegram."""
agent_id: str
chat_id: int
text: str
reply_to_message_id: Optional[int] = None
class BotRegistration(BaseModel):
"""HTTP payload для реєстрації нового бота/агента."""
agent_id: str
bot_token: str
# опційно: allowed_chat_id, ім'я, тощо
```
---
## 4. Клієнт NATS (app/nats_client.py)
Створи `app/nats_client.py` з асинхронним клієнтом:
```python
import json
from typing import Optional
import nats
from .config import settings
class NatsClient:
def __init__(self, url: str):
self._url = url
self._nc: Optional[nats.NATS] = None
async def connect(self) -> None:
if self._nc is None or self._nc.is_closed:
self._nc = await nats.connect(self._url)
async def close(self) -> None:
if self._nc and not self._nc.is_closed:
await self._nc.drain()
await self._nc.close()
async def publish_json(self, subject: str, data: dict) -> None:
if self._nc is None or self._nc.is_closed:
await self.connect()
await self._nc.publish(subject, json.dumps(data).encode("utf-8"))
nats_client = NatsClient(settings.NATS_URL)
```
На цьому етапі **підписки** NATS не потрібні — тільки `publish`. Підписуватися на `agent.telegram.send` можна пізніше, якщо буде потрібно. Зараз відправлення в Telegram робимо через HTTP `/send`.
---
## 5. Реєстр ботів (app/bots_registry.py)
Нам потрібна мапа `agent_id → bot_token` (і навпаки) для маршрутизації.
Створи `app/bots_registry.py`:
```python
from typing import Dict, Optional
from .models import BotRegistration
class BotsRegistry:
"""
Простий in-memory реєстр.
TODO: замінити на персистентне сховище (PostgreSQL/microdao DB).
"""
def __init__(self) -> None:
self._agent_to_token: Dict[str, str] = {}
self._token_to_agent: Dict[str, str] = {}
def register(self, reg: BotRegistration) -> None:
self._agent_to_token[reg.agent_id] = reg.bot_token
self._token_to_agent[reg.bot_token] = reg.agent_id
def get_token_by_agent(self, agent_id: str) -> Optional[str]:
return self._agent_to_token.get(agent_id)
def get_agent_by_token(self, bot_token: str) -> Optional[str]:
return self._token_to_agent.get(bot_token)
bots_registry = BotsRegistry()
```
На MVP-дроті достатньо in-memory. Потім можна буде підключити БД microdao (таблиця `telegram_bots`).
---
## 6. Telegram listener (app/telegram_listener.py)
Задача: запускати **long polling** для кількох ботів через Local Bot API і на кожне вхідне повідомлення публікувати `agent.telegram.update` у NATS.
Створи `app/telegram_listener.py`:
```python
import asyncio
import logging
from typing import Dict
from aiogram import Bot, Dispatcher, F
from aiogram.client.session.aiohttp import AiohttpSession
from aiogram.client.telegram import TelegramAPIServer
from aiogram.types import Message, Update
from .config import settings
from .models import TelegramUpdateEvent
from .nats_client import nats_client
from .bots_registry import bots_registry
logger = logging.getLogger(__name__)
class TelegramListener:
def __init__(self) -> None:
self._bots: Dict[str, Bot] = {} # bot_token -> Bot
self._dispatchers: Dict[str, Dispatcher] = {}
self._tasks: Dict[str, asyncio.Task] = {}
self._server = TelegramAPIServer.from_base(settings.TELEGRAM_API_BASE)
async def _create_bot(self, bot_token: str) -> Bot:
session = AiohttpSession(api=self._server)
bot = Bot(token=bot_token, session=session)
return bot
async def add_bot(self, bot_token: str) -> None:
if bot_token in self._bots:
return
bot = await self._create_bot(bot_token)
dp = Dispatcher()
@dp.message(F.text)
async def on_message(message: Message) -> None:
agent_id = bots_registry.get_agent_by_token(bot_token)
if not agent_id:
logger.warning("No agent_id for bot_token=%s", bot_token)
return
event = TelegramUpdateEvent(
agent_id=agent_id,
bot_id=f"bot:{bot_token[:8]}",
chat_id=message.chat.id,
user_id=message.from_user.id if message.from_user else 0,
text=message.text,
raw_update=message.model_dump()
)
await nats_client.publish_json(
subject="agent.telegram.update",
data=event.model_dump()
)
# Запускаємо polling у фоні
async def _polling():
try:
logger.info("Start polling for bot %s", bot_token)
await dp.start_polling(bot)
except asyncio.CancelledError:
logger.info("Polling cancelled for bot %s", bot_token)
except Exception as e:
logger.exception("Polling error for bot %s: %s", bot_token, e)
raise
task = asyncio.create_task(_polling())
self._bots[bot_token] = bot
self._dispatchers[bot_token] = dp
self._tasks[bot_token] = task
async def send_message(self, agent_id: str, chat_id: int, text: str, reply_to_message_id: int | None = None):
bot_token = bots_registry.get_token_by_agent(agent_id)
if not bot_token:
raise RuntimeError(f"No bot token for agent_id={agent_id}")
bot = self._bots.get(bot_token)
if not bot:
# Якщо бот ще не запущений (наприклад, перший виклик через /send)
await self.add_bot(bot_token)
bot = self._bots[bot_token]
await bot.send_message(
chat_id=chat_id,
text=text,
reply_to_message_id=reply_to_message_id
)
async def shutdown(self):
# Завершити polling задачі
for task in self._tasks.values():
task.cancel()
await asyncio.gather(*self._tasks.values(), return_exceptions=True)
# Закрити бот-сесії
for bot in self._bots.values():
await bot.session.close()
telegram_listener = TelegramListener()
```
---
## 7. FastAPI: main + HTTP-ендпоінти (app/main.py)
Онови `app/main.py` так, щоб:
* при старті сервісу:
* підключатися до NATS;
* за потреби — попередньо запускати polling для вже відомих ботів (MVP можна пропустити);
* надавати HTTP-ендпоінти:
* `GET /healthz`
* `POST /bots/register` — реєстрація нового бота для агента
* `POST /send` — надсилання повідомлення в Telegram від агента
```python
import asyncio
import logging
from fastapi import FastAPI, HTTPException
from .config import settings
from .models import BotRegistration, TelegramSendCommand
from .bots_registry import bots_registry
from .nats_client import nats_client
from .telegram_listener import telegram_listener
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO if settings.DEBUG else logging.WARNING)
app = FastAPI(title="telegram-gateway", version="0.1.0")
@app.on_event("startup")
async def on_startup():
# Підключаємося до NATS
await nats_client.connect()
logger.info("Connected to NATS at %s", settings.NATS_URL)
# На цьому етапі список ботів пустий; їх додаватимуть через /bots/register.
# За потреби сюди можна додати завантаження конфігів з БД.
@app.on_event("shutdown")
async def on_shutdown():
await telegram_listener.shutdown()
await nats_client.close()
@app.get("/healthz")
async def healthz():
return {"status": "ok"}
@app.post("/bots/register")
async def register_bot(reg: BotRegistration):
"""
Прив'язати Telegram-бота до agent_id.
1) Зберегти в реєстрі (in-memory);
2) Запустити polling для цього bot_token.
3) Опційно: опублікувати подію bot.registered у NATS.
"""
bots_registry.register(reg)
# Запускаємо polling
asyncio.create_task(telegram_listener.add_bot(reg.bot_token))
# Публікуємо подію реєстрації (може ловити Router або інший сервіс)
await nats_client.publish_json(
subject="bot.registered",
data={"agent_id": reg.agent_id, "bot_token": reg.bot_token}
)
return {"status": "registered"}
@app.post("/send")
async def send_message(cmd: TelegramSendCommand):
"""
Відправити повідомлення в Telegram від імені агента.
Викликається DAGI Router / microdao.
"""
try:
await telegram_listener.send_message(
agent_id=cmd.agent_id,
chat_id=cmd.chat_id,
text=cmd.text,
reply_to_message_id=cmd.reply_to_message_id,
)
except RuntimeError as e:
raise HTTPException(status_code=400, detail=str(e)) from e
return {"status": "sent"}
```
---
## 8. Dockerfile (telegram-gateway/Dockerfile)
Переконайся, що `Dockerfile` відповідає цим вимогам:
```dockerfile
FROM python:3.11-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
```
---
## 9. docker-compose.yml (корінь telegram-infrastructure)
Переконайся, що сервіс `telegram-gateway` описаний приблизно так:
```yaml
services:
telegram-bot-api:
image: ghcr.io/tdlib/telegram-bot-api:latest
container_name: telegram-bot-api
restart: unless-stopped
env_file:
- .env
command:
- --local
- --http-port=8081
- --dir=/var/lib/telegram-bot-api
volumes:
- ./data/telegram-bot-api:/var/lib/telegram-bot-api
ports:
- "127.0.0.1:8081:8081"
nats:
image: nats:2
container_name: nats
restart: unless-stopped
ports:
- "127.0.0.1:4222:4222"
telegram-gateway:
build: ./telegram-gateway
container_name: telegram-gateway
restart: unless-stopped
env_file:
- .env
depends_on:
- telegram-bot-api
- nats
ports:
- "127.0.0.1:8000:8000"
```
---
## 10. Послідовність дій (для запуску й тесту)
1. Переконайся, що `.env` у корені `telegram-infrastructure` містить:
```env
TELEGRAM_API_ID=XXXXXXX
TELEGRAM_API_HASH=XXXXXXXXXXXXXXXXXXXXXXXXXXXX
NATS_URL=nats://nats:4222
TELEGRAM_API_BASE=http://telegram-bot-api:8081
```
2. Запусти стек:
```bash
docker compose build telegram-gateway
docker compose up -d telegram-bot-api nats telegram-gateway
```
3. Перевір `telegram-gateway`:
```bash
curl http://127.0.0.1:8000/healthz
# очікується: {"status":"ok"}
```
4. Перевір Local Telegram Bot API (з реальним BOT_TOKEN):
```bash
curl http://127.0.0.1:8081/bot<YOUR_BOT_TOKEN>/getMe
```
5. Зареєструй бота для агента:
```bash
curl -X POST http://127.0.0.1:8000/bots/register \
-H "Content-Type: application/json" \
-d '{
"agent_id": "ag_helion",
"bot_token": "<YOUR_BOT_TOKEN>"
}'
```
6. Надішли тестове повідомлення з агента:
```bash
curl -X POST http://127.0.0.1:8000/send \
-H "Content-Type: application/json" \
-d '{
"agent_id": "ag_helion",
"chat_id": <YOUR_CHAT_ID>,
"text": "Привіт від агента Helion!",
"reply_to_message_id": null
}'
```
7. Перевір у логах `nats` або через окремий subscriber, що події `agent.telegram.update` публікуються при вхідних повідомленнях.
---
## 11. Що далі (опційно)
Після того, як MVP працює:
* підключити реальну БД microdao для таблиці `telegram_bots` замість in-memory `BotsRegistry`;
* додати підписника NATS на `agent.telegram.send`, щоб можна було слати повідомлення не тільки через HTTP `/send`, а й через події;
* розширити payload подій (`mode`, `team_id`, `channel_id`) під існуючий Event Catalog microdao;
* додати обмеження / rate limiting / логування в стилі microdao.
---