snapshot: NODE1 production state 2026-02-09

Complete snapshot of /opt/microdao-daarion/ from NODE1 (144.76.224.179).
This represents the actual running production code that has diverged
significantly from the previous main branch.

Key changes from old main:
- Gateway (http_api.py): expanded from ~40KB to 164KB with full agent support
- Router: new /v1/agents/{id}/infer endpoint with vision + DeepSeek routing
- Behavior Policy: SOWA v2.2 (3-level: FULL/ACK/SILENT)
- Agent Registry: config/agent_registry.yml as single source of truth
- 13 agents configured (was 3)
- Memory service integration
- CrewAI teams and roles

Excluded from snapshot: venv/, .env, data/, backups, .tgz archives

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Apple
2026-02-09 08:46:46 -08:00
parent 134c044c21
commit ef3473db21
9473 changed files with 408933 additions and 2769877 deletions

View File

@@ -1,42 +0,0 @@
# Environment variables
.env
# Data directory
data/
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Docker
docker-compose.override.yml
telegram-gateway/bots.yaml

View File

@@ -1,412 +0,0 @@
# 🔧 Завдання для Cursor: Виправити telegram-gateway (DAARWIZZ і Helion не відповідають)
## 🎯 Мета
Зробити так, щоб обидва боти (DAARWIZZ і Helion) автоматично ініціалізувались при старті та відповідали на повідомлення через polling.
---
## 📋 Проблеми зараз
1. ❌ Боти не ініціалізуються автоматично при старті
2.`bots.yaml` не монтується в контейнер
3. ❌ Polling не запускається для ботів з конфігурації
4. ❌ Логування недостатнє для діагностики
---
## ✅ Що потрібно зробити
### 1. Виправити `docker-compose.yml`
**Файл:** `/Users/apple/github-projects/microdao-daarion/telegram-infrastructure/docker-compose.yml`
**Додати volume для bots.yaml:**
```yaml
telegram-gateway:
build: ./telegram-gateway
container_name: telegram-gateway
restart: unless-stopped
env_file:
- .env
environment:
- TELEGRAM_API_BASE=http://telegram-bot-api:8081
- NATS_URL=nats://nats:4222
- ROUTER_BASE_URL=http://router:9102
- DEBUG=true
depends_on:
- telegram-bot-api
- nats
ports:
- "127.0.0.1:8000:8000"
volumes:
- ./telegram-gateway/bots.yaml:/app/bots.yaml:ro # ← ДОДАТИ ЦЕЙ РЯДОК
networks:
- telegram-net
- dagi-network
```
**Чому:** Контейнер має читати `bots.yaml` при старті, але файл не копіюється в образ і не монтується як volume.
---
### 2. Виправити `app/main.py` — автоматична ініціалізація ботів
**Файл:** `/Users/apple/github-projects/microdao-daarion/telegram-infrastructure/telegram-gateway/app/main.py`
**Поточний код `on_startup()`:**
```python
@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.
# За потреби сюди можна додати завантаження конфігів з БД.
```
**Новий код `on_startup()` (ЗАМІНИТИ):**
```python
@app.on_event("startup")
async def on_startup():
# 1. Підключаємося до NATS
await nats_client.connect()
logger.info("✅ Connected to NATS at %s", settings.NATS_URL)
# 2. Завантажити конфігурацію ботів з bots.yaml або env
from .config import load_bots_config
try:
bot_configs = load_bots_config()
logger.info("📋 Loaded %d bot(s) from config", len(bot_configs))
except Exception as e:
logger.warning("⚠️ Failed to load bots config: %s", e)
bot_configs = []
# 3. Зареєструвати всі боти в реєстрі
if bot_configs:
bots_registry.register_from_config(bot_configs)
logger.info("📝 Registered %d bot(s) in registry", len(bot_configs))
# 4. Запустити polling для кожного бота
for bot_config in bot_configs:
agent_id = bot_config.agent_id
bot_token = bot_config.bot_token
# Запускаємо polling в фоновій задачі
asyncio.create_task(telegram_listener.add_bot(bot_token))
logger.info("🚀 Started polling for agent=%s (token=%s...)", agent_id, bot_token[:16])
if not bot_configs:
logger.warning("⚠️ No bots configured. Use /bots/register to add bots manually.")
```
**Чому:** Зараз боти не запускаються автоматично — треба викликати `/bots/register` вручну. Цей код автоматично завантажує конфіг і запускає polling при старті.
---
### 3. Перевірити `app/config.py` — функція `load_bots_config()`
**Файл:** `/Users/apple/github-projects/microdao-daarion/telegram-infrastructure/telegram-gateway/app/config.py`
**Переконайся, що функція існує і виглядає приблизно так:**
```python
from pathlib import Path
from typing import List
import yaml
import os
from pydantic import BaseModel
class BotConfig(BaseModel):
agent_id: str
bot_token: str
def load_bots_config() -> List[BotConfig]:
"""
Завантажити конфігурацію ботів з bots.yaml або env variables.
Пріоритет:
1. bots.yaml (якщо існує)
2. Environment variables: BOT_<AGENT_ID>_TOKEN
"""
bots = []
# Спробувати завантажити з bots.yaml
config_path = Path("/app/bots.yaml") # Шлях в контейнері
if config_path.exists():
try:
with open(config_path, "r") as f:
data = yaml.safe_load(f)
if data and "bots" in data:
for bot_data in data["bots"]:
bots.append(BotConfig(**bot_data))
except Exception as e:
# Fallback до env variables
pass
# Fallback: завантажити з env variables
if not bots:
for key, value in os.environ.items():
if key.startswith("BOT_") and key.endswith("_TOKEN"):
agent_id = key[4:-6].lower() # BOT_DAARWIZZ_TOKEN -> daarwizz
bots.append(BotConfig(agent_id=agent_id, bot_token=value))
return bots
```
**Якщо функція відсутня або неповна — додай/виправ її.**
---
### 4. Перевірити `app/bots_registry.py` — метод `register_from_config()`
**Файл:** `/Users/apple/github-projects/microdao-daarion/telegram-infrastructure/telegram-gateway/app/bots_registry.py`
**Переконайся, що метод існує:**
```python
from typing import List
from .config import BotConfig
class BotsRegistry:
# ... інші методи ...
def register_from_config(self, configs: List[BotConfig]) -> None:
"""Масова реєстрація ботів з конфігурації"""
for config in configs:
self.register(config)
# або напряму:
# self._agent_to_token[config.agent_id] = config.bot_token
# self._token_to_agent[config.bot_token] = config.agent_id
```
**Якщо метод відсутній — додай його.**
---
### 5. Додати детальне логування в `app/telegram_listener.py`
**Файл:** `/Users/apple/github-projects/microdao-daarion/telegram-infrastructure/telegram-gateway/app/telegram_listener.py`
**Оновити метод `add_bot()` для кращого логування:**
```python
async def add_bot(self, bot_token: str) -> None:
if bot_token in self._bots:
logger.info("🔄 Bot already registered: %s...", bot_token[:16])
return
logger.info("🤖 Creating bot: %s...", bot_token[:16])
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[:16])
return
logger.info("📨 Received message: agent=%s, chat=%s, user=%s, len=%d",
agent_id, message.chat.id, message.from_user.id if message.from_user else 0, len(message.text or ""))
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()
)
logger.info("📤 Publishing to NATS: subject=agent.telegram.update, agent=%s", agent_id)
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[:16])
await dp.start_polling(bot)
except asyncio.CancelledError:
logger.info("🛑 Polling cancelled for bot %s...", bot_token[:16])
except Exception as e:
logger.exception("💥 Polling error for bot %s...: %s", bot_token[:16], e)
raise
task = asyncio.create_task(_polling())
self._bots[bot_token] = bot
self._dispatchers[bot_token] = dp
self._tasks[bot_token] = task
logger.info("✅ Bot registered and polling started: %s...", bot_token[:16])
```
**Чому:** Детальніше логування допоможе побачити, що відбувається при отриманні повідомлень.
---
### 6. Створити `bots.yaml` локально (якщо ще не створений)
**Файл:** `/Users/apple/github-projects/microdao-daarion/telegram-infrastructure/telegram-gateway/bots.yaml`
**Вміст:**
```yaml
bots:
- agent_id: daarwizz
bot_token: 8323412397:AAFxaru-hHRl08A3T6TC02uHLvO5wAB0m3M
- agent_id: helion
bot_token: 8112062582:AAGI7tPFo4gvZ6bfbkFu9miq5GdAH2_LvcM
```
---
## 🧪 Як перевірити після змін
### 1. Деплой на сервер (виконати з Mac)
```bash
cd /Users/apple/github-projects/microdao-daarion/telegram-infrastructure
# Синхронізація коду
rsync -avz --exclude='.git' --exclude='__pycache__' --exclude='*.pyc' --exclude='data/' ./ root@144.76.224.179:/opt/telegram-infrastructure/
# Перезапуск на сервері
ssh root@144.76.224.179 "cd /opt/telegram-infrastructure && docker compose down telegram-gateway && docker compose up -d --build telegram-gateway"
```
### 2. Перевірка логів (на сервері)
```bash
ssh root@144.76.224.179
docker logs -f telegram-gateway
```
**Очікувані рядки в логах:**
```
✅ Connected to NATS at nats://nats:4222
📋 Loaded 2 bot(s) from config
📝 Registered 2 bot(s) in registry
🚀 Started polling for agent=daarwizz (token=8323412397:AAFxa...)
🚀 Started polling for agent=helion (token=8112062582:AAGI7...)
🔁 Start polling for bot 8323412397:AAFxa...
🔁 Start polling for bot 8112062582:AAGI7...
```
### 3. Перевірка списку ботів
```bash
curl -s http://127.0.0.1:8000/bots/list | jq .
```
**Очікувана відповідь:**
```json
{
"bots": ["daarwizz", "helion"],
"count": 2
}
```
### 4. Перевірка polling tasks
```bash
curl -s http://127.0.0.1:8000/debug/bots | jq .
```
**Очікувана відповідь:**
```json
{
"registered_bots": 2,
"bot_tokens": ["8323412397:AAFxa...", "8112062582:AAGI7..."],
"registry_mappings": 2,
"active_tasks": 2
}
```
```bash
curl -s http://127.0.0.1:8000/debug/bots/tasks | jq .
```
**Очікувана відповідь:**
```json
{
"8323412397:AAFxa...": {
"done": false,
"cancelled": false
},
"8112062582:AAGI7...": {
"done": false,
"cancelled": false
}
}
```
### 5. Надіслати тестове повідомлення
**В Telegram:**
1. Надішліть "Привіт" в DAARWIZZ бот
2. Надішліть "Привіт" в Helion бот
**В логах має з'явитись:**
```
📨 Received message: agent=daarwizz, chat=123456, user=789, len=6
📤 Publishing to NATS: subject=agent.telegram.update, agent=daarwizz
📨 Received message: agent=helion, chat=123456, user=789, len=6
📤 Publishing to NATS: subject=agent.telegram.update, agent=helion
```
---
## ⚠️ Troubleshooting
### Якщо боти не ініціалізуються:
```bash
# Перевірити, чи bots.yaml є в контейнері
docker exec telegram-gateway cat /app/bots.yaml
# Має показати вміст файлу. Якщо "No such file" — volume не працює
```
### Якщо polling не запускається:
```bash
# Перевірити логи aiogram
docker logs telegram-gateway 2>&1 | grep -i "polling\|error\|exception"
```
### Якщо повідомлення не приходять:
```bash
# Перевірити, чи webhooks видалені
curl -s "https://api.telegram.org/bot8323412397:AAFxaru-hHRl08A3T6TC02uHLvO5wAB0m3M/getWebhookInfo" | jq .result.url
curl -s "https://api.telegram.org/bot8112062582:AAGI7tPFo4gvZ6bfbkFu9miq5GdAH2_LvcM/getWebhookInfo" | jq .result.url
# Обидва мають повертати: "" (порожній рядок = webhook видалений)
```
---
## 📝 Checklist для Cursor
- [ ] Додати `volumes:` в `docker-compose.yml` для `telegram-gateway`
- [ ] Оновити `on_startup()` в `app/main.py` з автоматичною ініціалізацією
- [ ] Перевірити/додати `load_bots_config()` в `app/config.py`
- [ ] Перевірити/додати `register_from_config()` в `app/bots_registry.py`
- [ ] Додати детальне логування в `app/telegram_listener.py`
- [ ] Створити `telegram-gateway/bots.yaml` з реальними токенами
---
## 🎯 Очікуваний результат
Після виконання всіх кроків:
- ✅ Обидва боти (DAARWIZZ і Helion) автоматично стартують при запуску контейнера
- ✅ Polling працює для обох ботів
- ✅ Повідомлення отримуються і публікуються в NATS
- ✅ Детальні логи для діагностики
-`/bots/list` показує обидва боти
**Після цього агенти мають відповідати на повідомлення!** 🎉

View File

@@ -1,556 +0,0 @@
# 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.
---

View File

@@ -1,33 +0,0 @@
# Environment Variables Template
Скопіюй цей файл як `.env` і заповни реальні значення.
```env
# Telegram Bot API (Local Bot API)
TELEGRAM_API_ID=XXXXXXX
TELEGRAM_API_HASH=XXXXXXXXXXXXXXXXXXXXXXXXXXXX
# Telegram Gateway Service
TELEGRAM_API_BASE=http://telegram-bot-api:8081
NATS_URL=nats://nats:4222
ROUTER_BASE_URL=http://router:9102
DEBUG=false
# Bot Tokens (опційно, якщо не використовується bots.yaml)
# BOT_DAARWIZZ_TOKEN=8323412397:AAFxaru-hHRl08A3T6TC02uHLvO5wAB0m3M
# BOT_HELION_TOKEN=8112062582:AAGI7tPFo4gvZ6bfbkFu9miq5GdAH2_LvcM
```
## Опис змінних
- `TELEGRAM_API_ID` / `TELEGRAM_API_HASH` — отримай з https://my.telegram.org/apps
- `TELEGRAM_API_BASE` — URL Local Telegram Bot API (за замовчуванням: `http://telegram-bot-api:8081`)
- `NATS_URL` — URL NATS event bus (за замовчуванням: `nats://nats:4222`)
- `ROUTER_BASE_URL` — URL DAGI Router (за замовчуванням: `http://router:9102`)
- `DEBUG` — увімкнути детальне логування (`true`/`false`)
## Примітки
- Токени ботів краще зберігати в `bots.yaml` (не комітити в Git!)
- `.env` файл не повинен потрапляти в Git (додай до `.gitignore`)

View File

@@ -1,214 +0,0 @@
# Telegram Infrastructure
Мінімальний стек для інтеграції Telegram-ботів з системою агентів DAGI/microDAO.
## 🏗️ Архітектура
Цей стек складається з трьох сервісів:
### 1. **telegram-bot-api** (Local Telegram Bot API)
- Офіційний Local Telegram Bot API від Telegram
- Працює в режимі `--local` (HTTP only, без SSL)
- Доступний тільки на `127.0.0.1:8081` (не публічний)
- Дозволяє використовувати Bot API без зовнішніх запитів до Telegram серверів
### 2. **nats** (Message Bus)
- NATS як шина подій між сервісами
- Простий режим без кластера
- Доступний на `127.0.0.1:4222`
### 3. **telegram-gateway** (Python FastAPI)
- Міст між Telegram Bot API і NATS
- Базовий FastAPI сервіс з `/healthz` endpoint
- Буде розширений через Cursor для:
- Читання апдейтів з Telegram
- Публікації подій у NATS
- Прийому HTTP команд від інших сервісів
## 📋 Передумови
- Docker + Docker Compose
- Telegram API credentials (API ID, API Hash) з https://my.telegram.org/apps
- Домен, що вказує на ваш сервер (опціонально для майбутнього HTTPS)
## 🚀 Швидкий старт
### 1. Налаштування змінних середовища
Створіть `.env` файл у кореневій директорії:
```bash
cp .env.example .env
```
Заповніть `.env` вашими значеннями:
```env
TELEGRAM_API_ID=YOUR_API_ID
TELEGRAM_API_HASH=YOUR_API_HASH
GATEWAY_DOMAIN=gateway.daarion.city
```
### 2. Запуск стеку
```bash
docker compose up -d
```
### 3. Перевірка статусу
Перевірте, що всі контейнери запущені:
```bash
docker compose ps
```
Очікуваний вивід:
```
NAME STATUS PORTS
telegram-bot-api running 127.0.0.1:8081->8081/tcp
nats running 127.0.0.1:4222->4222/tcp
telegram-gateway running 127.0.0.1:8000->8000/tcp
```
## 🧪 Тестування
### 1. Перевірка telegram-gateway
```bash
curl http://127.0.0.1:8000/healthz
```
Очікувана відповідь:
```json
{
"status": "ok",
"service": "telegram-gateway",
"telegram_api": "http://telegram-bot-api:8081",
"nats": "nats://nats:4222"
}
```
### 2. Перевірка Local Telegram Bot API
Замініть `<YOUR_BOT_TOKEN>` на ваш реальний токен бота:
```bash
curl http://127.0.0.1:8081/bot<YOUR_BOT_TOKEN>/getMe
```
Очікувана відповідь (JSON з інформацією про бота):
```json
{
"ok": true,
"result": {
"id": 123456789,
"is_bot": true,
"first_name": "YourBot",
"username": "your_bot"
}
}
```
### 3. Перевірка NATS
```bash
docker logs nats
```
Має показувати успішний запуск NATS сервера.
## 📁 Структура проєкту
```
telegram-infrastructure/
├── docker-compose.yml # Конфігурація Docker Compose
├── .env # Змінні середовища (не в git)
├── .env.example # Приклад змінних
├── README.md # Ця документація
├── data/ # Дані (автоматично створюється)
│ └── telegram-bot-api/ # Дані Local Telegram Bot API
└── telegram-gateway/ # Telegram Gateway сервіс
├── Dockerfile
├── requirements.txt
└── app/
├── __init__.py
└── main.py # FastAPI додаток
```
## 🔒 Безпека
- **telegram-bot-api** доступний тільки на localhost (`127.0.0.1:8081`)
- **nats** доступний тільки на localhost (`127.0.0.1:4222`)
- **telegram-gateway** доступний тільки на localhost (`127.0.0.1:8000`)
- Для публічного доступу додайте reverse proxy (Caddy/Traefik/nginx) з HTTPS
## 🔄 Управління
### Зупинка сервісів
```bash
docker compose down
```
### Перезапуск сервісу
```bash
docker compose restart telegram-gateway
```
### Перегляд логів
```bash
# Всі сервіси
docker compose logs -f
# Конкретний сервіс
docker compose logs -f telegram-gateway
```
### Rebuild після змін коду
```bash
docker compose up -d --build telegram-gateway
```
## 📝 Наступні кроки
Після того, як інфраструктура запрацює, використовуйте **Cursor** для розробки логіки `telegram-gateway`:
1. **Інтеграція aiogram** — для роботи з Telegram Bot API
2. **NATS publisher** — публікація подій у NATS
3. **Webhook endpoints** — прийом команд від інших сервісів
4. **Message routing** — маршрутизація повідомлень між агентами
## 🐛 Troubleshooting
### Контейнери не запускаються
```bash
docker compose logs telegram-bot-api
```
### Порти зайняті
Переконайтесь, що порти 8081, 4222, 8000 вільні:
```bash
lsof -i :8081
lsof -i :4222
lsof -i :8000
```
### Проблеми з правами доступу
```bash
chmod -R 755 data/
```
## 📚 Посилання
- [Local Telegram Bot API](https://github.com/tdlib/telegram-bot-api)
- [NATS](https://nats.io/)
- [FastAPI](https://fastapi.tiangolo.com/)
- [aiogram](https://docs.aiogram.dev/) (для майбутньої інтеграції)

View File

@@ -1,54 +0,0 @@
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"
networks:
- telegram-net
nats:
image: nats:2
container_name: nats
restart: unless-stopped
ports:
- "127.0.0.1:4222:4222"
networks:
- telegram-net
telegram-gateway:
build: ./telegram-gateway
container_name: telegram-gateway
restart: unless-stopped
env_file:
- .env
environment:
- TELEGRAM_API_BASE=http://telegram-bot-api:8081
- NATS_URL=nats://nats:4222
- ROUTER_BASE_URL=http://dagi-router:9102
- DEBUG=true
depends_on:
- telegram-bot-api
- nats
ports:
- "127.0.0.1:8000:8000"
volumes:
- ./telegram-gateway/bots.yaml:/app/bots.yaml:ro
networks:
- telegram-net
- dagi-network
networks:
telegram-net:
driver: bridge
dagi-network:
external: true

View File

@@ -1,44 +0,0 @@
#!/bin/bash
# Health check script for telegram-gateway
# Перевіряє статус всіх сервісів
set -e
echo "🏥 Health Check for Telegram Infrastructure"
echo "=========================================="
# Check telegram-gateway
echo -n "📡 telegram-gateway: "
if curl -s http://localhost:8000/healthz > /dev/null; then
echo "✅ OK"
curl -s http://localhost:8000/bots/list | jq .
else
echo "❌ FAILED"
fi
# Check NATS
echo -n "📨 NATS: "
if docker ps | grep -q nats; then
echo "✅ Running"
else
echo "❌ Not running"
fi
# Check telegram-bot-api
echo -n "🤖 telegram-bot-api: "
if docker ps | grep -q telegram-bot-api; then
echo "✅ Running"
else
echo "❌ Not running"
fi
# Check registered bots
echo ""
echo "📋 Registered Bots:"
curl -s http://localhost:8000/bots/list | jq -r '.bots[]' | while read bot; do
echo " - $bot"
done
echo ""
echo "✅ Health check complete!"

View File

@@ -1,44 +0,0 @@
#!/bin/bash
# Deployment script for telegram-gateway
# Використання: ./scripts/deploy.sh [production|development]
set -e
ENVIRONMENT=${1:-production}
PROJECT_ROOT="/opt/telegram-infrastructure"
REMOTE_HOST="root@144.76.224.179"
LOCAL_ROOT="/Users/apple/github-projects/microdao-daarion/telegram-infrastructure"
echo "🚀 Deploying telegram-gateway to $ENVIRONMENT..."
if [ "$ENVIRONMENT" = "production" ]; then
echo "📦 Syncing files to production server..."
rsync -avz \
--exclude='.git' \
--exclude='__pycache__' \
--exclude='*.pyc' \
--exclude='data/' \
--exclude='.env' \
"$LOCAL_ROOT/" "$REMOTE_HOST:$PROJECT_ROOT/"
echo "🔄 Restarting services on production server..."
ssh "$REMOTE_HOST" "cd $PROJECT_ROOT && \
docker compose down telegram-gateway && \
docker compose up -d --build telegram-gateway"
echo "✅ Deployment complete!"
echo "📋 Check logs: ssh $REMOTE_HOST 'docker logs -f telegram-gateway'"
elif [ "$ENVIRONMENT" = "development" ]; then
echo "🔄 Restarting services locally..."
cd "$LOCAL_ROOT"
docker compose down telegram-gateway
docker compose up -d --build telegram-gateway
echo "✅ Local deployment complete!"
echo "📋 Check logs: docker compose logs -f telegram-gateway"
else
echo "❌ Unknown environment: $ENVIRONMENT"
echo "Usage: ./scripts/deploy.sh [production|development]"
exit 1
fi

View File

@@ -1,19 +0,0 @@
FROM python:3.11-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Install dependencies
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY app ./app
# Expose port
EXPOSE 8000
# Run uvicorn
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--log-level", "info"]

View File

@@ -1,373 +0,0 @@
# Telegram Gateway Service
Сервіс для інтеграції Telegram-ботів з DAGI/microDAO через NATS message bus.
**Частина DAGI Stack** — див. [INFRASTRUCTURE.md](../../INFRASTRUCTURE.md) для повної інформації про інфраструктуру.
## Інтеграція з DAGI Stack
- **Router** (порт 9102): маршрутизація повідомлень до агентів
- **NATS** (порт 4222): event bus для подій `agent.telegram.update`
- **Local Telegram Bot API** (порт 8081): long polling без SSL/webhook
**Network Nodes:**
- **Node #1 (Production):** `144.76.224.179` — Hetzner GEX44
- **Node #2 (Development):** `192.168.1.244` — MacBook Pro M4 Max
## Архітектура
```
Telegram Bot → Local Bot API → telegram-gateway (polling) → NATS → Router/microDAO
Telegram Bot ← Local Bot API ← telegram-gateway (/send) ← HTTP API ← Router/microDAO
```
## Особливості
- **Long Polling** через Local Telegram Bot API (без SSL/webhook)
- Автоматична ініціалізація ботів з конфігурації при старті
- Публікація подій `agent.telegram.update` у NATS
- HTTP API для відправки повідомлень (`/send`)
- Підтримка кількох ботів одночасно (DAARWIZZ, Helion, тощо)
## Швидкий старт
### 1. Конфігурація ботів
Створи файл `bots.yaml` в корені `telegram-gateway/`:
```yaml
bots:
- agent_id: "daarwizz"
bot_token: "YOUR_DAARWIZZ_BOT_TOKEN"
enabled: true
description: "DAARWIZZ agent bot"
- agent_id: "helion"
bot_token: "YOUR_HELION_BOT_TOKEN"
enabled: true
description: "Helion agent bot"
```
Або використовуй environment variables:
```bash
export BOT_DAARWIZZ_TOKEN="your_token_here"
export BOT_HELION_TOKEN="your_token_here"
```
### 2. Environment variables
У `.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
DEBUG=false
```
### 3. Запуск
```bash
cd telegram-infrastructure
# Збірка
docker compose build telegram-gateway
# Запуск
docker compose up -d telegram-bot-api nats telegram-gateway
# Перевірка
curl http://localhost:8000/healthz
curl http://localhost:8000/bots/list
```
## API Endpoints
### `GET /healthz`
Health check endpoint.
**Відповідь:**
```json
{"status": "ok"}
```
### `GET /bots/list`
Список зареєстрованих ботів.
**Відповідь:**
```json
{
"bots": ["daarwizz", "helion"],
"count": 2
}
```
### `POST /bots/register`
Реєстрація нового бота (якщо не використовується `bots.yaml`).
**Request:**
```json
{
"agent_id": "helion",
"bot_token": "YOUR_BOT_TOKEN"
}
```
**Відповідь:**
```json
{
"status": "registered",
"agent_id": "helion"
}
```
### `POST /send`
Відправка повідомлення в Telegram від імені агента.
**Request:**
```json
{
"agent_id": "helion",
"chat_id": 123456789,
"text": "Привіт від Helion!",
"reply_to_message_id": null
}
```
**Відповідь:**
```json
{
"status": "sent"
}
```
## NATS Events
### `agent.telegram.update`
Подія, яка публікується при отриманні повідомлення з Telegram.
**Payload:**
```json
{
"agent_id": "helion",
"bot_id": "bot:12345678",
"chat_id": 123456789,
"user_id": 987654321,
"text": "Привіт!",
"raw_update": { ... }
}
```
### `bot.registered`
Подія, яка публікується при реєстрації нового бота.
**Payload:**
```json
{
"agent_id": "helion",
"bot_token": "12345678..."
}
```
## Логування
Логи містять:
- Отримання повідомлень: `agent_id`, `chat_id`, `user_id`, довжина тексту
- Публікацію в NATS: subject, agent_id
- Відправку повідомлень: agent_id, chat_id, довжина тексту
Для детального логування встанови `DEBUG=true` в `.env`.
## Діагностика
### Перевірка webhook статусу
```bash
# DAARWIZZ
curl -s "https://api.telegram.org/bot<DAARWIZZ_TOKEN>/getWebhookInfo"
# Helion
curl -s "https://api.telegram.org/bot<HELION_TOKEN>/getWebhookInfo"
```
Якщо є проблеми з SSL/webhook, видали webhook:
```bash
curl -s "https://api.telegram.org/bot<HELION_TOKEN>/deleteWebhook"
```
### Перевірка сервісу
```bash
# Health check
curl http://localhost:8000/healthz
# Список ботів
curl http://localhost:8000/bots/list
# Логи
docker compose logs -f telegram-gateway
```
## Додавання нового бота
### Варіант 1: Через `bots.yaml`
Додай запис у `bots.yaml`:
```yaml
bots:
- agent_id: "new_agent"
bot_token: "NEW_BOT_TOKEN"
enabled: true
```
Перезапусти сервіс:
```bash
docker compose restart telegram-gateway
```
### Варіант 2: Через HTTP API
```bash
curl -X POST http://localhost:8000/bots/register \
-H "Content-Type: application/json" \
-d '{
"agent_id": "new_agent",
"bot_token": "NEW_BOT_TOKEN"
}'
```
### Варіант 3: Через environment variable
```bash
export BOT_NEW_AGENT_TOKEN="NEW_BOT_TOKEN"
docker compose restart telegram-gateway
```
## Troubleshooting
### Бот не отримує повідомлення
1. Перевір, чи бот зареєстрований: `curl http://localhost:8000/bots/list`
2. Перевір логи: `docker compose logs telegram-gateway`
3. Перевір, чи видалено webhook: `curl "https://api.telegram.org/bot<TOKEN>/getWebhookInfo"`
4. Перевір, чи працює Local Bot API: `curl http://localhost:8081/bot<TOKEN>/getMe`
### Події не досягають NATS
1. Перевір підключення до NATS: `docker compose logs telegram-gateway | grep NATS`
2. Перевір, чи працює NATS: `docker compose ps nats`
3. Перевір логи на помилки публікації
### Повідомлення не відправляються
1. Перевір, чи бот зареєстрований
2. Перевір логи на помилки відправки
3. Перевір формат `agent_id` (має збігатися з тим, що в NATS подіях)
## Розробка
### Локальний запуск (без Docker)
```bash
cd telegram-gateway
# Встановити залежності
pip install -r requirements.txt
# Запустити
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
```
### Структура проєкту
```
telegram-gateway/
├── Dockerfile
├── requirements.txt
├── bots.yaml.example
├── README.md
└── app/
├── __init__.py
├── config.py # Налаштування та завантаження конфігурації ботів
├── models.py # Pydantic моделі
├── nats_client.py # Клієнт NATS
├── bots_registry.py # Реєстр ботів
├── telegram_listener.py # Long polling та обробка повідомлень
└── main.py # FastAPI додаток
```
## Deployment
### Production Deployment
```bash
# З локальної машини
cd /Users/apple/github-projects/microdao-daarion/telegram-infrastructure
./scripts/deploy.sh production
```
### Development Deployment
```bash
# Локально
cd telegram-infrastructure
./scripts/deploy.sh development
```
### Health Check
```bash
./scripts/check-health.sh
```
## Інтеграція з DAGI Stack
### Підключення до Router
Router отримує події через NATS:
- Subject: `agent.telegram.update`
- Payload: `TelegramUpdateEvent` (див. `app/models.py`)
### Підключення до microDAO
microDAO може відправляти повідомлення через HTTP API:
- Endpoint: `POST /send`
- Payload: `TelegramSendCommand` (див. `app/models.py`)
### Network Configuration
Для інтеграції з DAGI Stack додай в `docker-compose.yml`:
```yaml
telegram-gateway:
networks:
- telegram-net
- dagi-network # Підключення до DAGI Stack
```
І створи external network:
```bash
docker network create dagi-network
```
## Майбутні покращення
- [ ] Персистентне сховище ботів (PostgreSQL/microDAO DB)
- [ ] Підписка на NATS `agent.telegram.send` для відправки через події
- [ ] Rate limiting
- [ ] Метрики (Prometheus)
- [ ] Підтримка інших типів повідомлень (документи, фото, тощо)
- [ ] Інтеграція з Grafana для моніторингу
## Документація
- [INFRASTRUCTURE.md](../../INFRASTRUCTURE.md) — повна інформація про інфраструктуру
- [docs/infrastructure_quick_ref.ipynb](../../docs/infrastructure_quick_ref.ipynb) — швидкий довідник
- [CURSOR_INSTRUCTIONS.md](../CURSOR_INSTRUCTIONS.md) — інструкції для розробки

View File

@@ -1 +0,0 @@
"""Telegram Gateway Application"""

View File

@@ -1,59 +0,0 @@
import logging
from typing import Dict, Optional, List
from .models import BotRegistration
from .config import BotConfig
logger = logging.getLogger(__name__)
class BotsRegistry:
"""
Реєстр ботів з підтримкою ініціалізації з конфігурації.
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:
"""Реєстрація бота через BotRegistration (HTTP API)"""
self._agent_to_token[reg.agent_id] = reg.bot_token
self._token_to_agent[reg.bot_token] = reg.agent_id
logger.info(f"Registered bot: agent_id={reg.agent_id}, token={reg.bot_token[:8]}...")
def register_from_config(self, bot_config: BotConfig) -> None:
"""Реєстрація бота з BotConfig (конфігурація)"""
if not bot_config.enabled:
logger.debug(f"Skipping disabled bot: agent_id={bot_config.agent_id}")
return
self._agent_to_token[bot_config.agent_id] = bot_config.bot_token
self._token_to_agent[bot_config.bot_token] = bot_config.agent_id
logger.info(f"Registered bot from config: agent_id={bot_config.agent_id}, token={bot_config.bot_token[:8]}...")
def register_batch(self, bot_configs: List[BotConfig]) -> None:
"""Масове реєстрування ботів з конфігурації"""
for bot_config in bot_configs:
self.register_from_config(bot_config)
def unregister(self, agent_id: str) -> Optional[str]:
"""Видалити бота за agent_id та повернути bot_token"""
bot_token = self._agent_to_token.pop(agent_id, None)
if bot_token:
self._token_to_agent.pop(bot_token, None)
logger.info(f"Unregistered bot: agent_id={agent_id}, token={bot_token[:8]}...")
return bot_token
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)
def list_agents(self) -> List[str]:
"""Повернути список всіх зареєстрованих agent_id"""
return list(self._agent_to_token.keys())
bots_registry = BotsRegistry()

View File

@@ -1,112 +0,0 @@
import os
import yaml
import logging
from pathlib import Path
from typing import List, Optional
from pydantic import BaseModel
from pydantic_settings import BaseSettings
logger = logging.getLogger(__name__)
class BotConfig(BaseModel):
"""Конфігурація одного бота"""
agent_id: str
bot_token: str
# Опційно: додаткові параметри
enabled: bool = True
description: Optional[str] = None
class Settings(BaseSettings):
"""
Налаштування telegram-gateway сервісу.
Інтеграція з DAGI Stack:
- Router: порт 9102 (http://router:9102)
- NATS: порт 4222 (nats://nats:4222)
- Local Telegram Bot API: порт 8081 (http://telegram-bot-api:8081)
Див. INFRASTRUCTURE.md для повної інформації про інфраструктуру.
"""
# Local Telegram Bot API (Docker service)
# Використовується для long polling без SSL/webhook
TELEGRAM_API_BASE: str = "http://telegram-bot-api:8081"
# NATS event bus (DAGI Stack)
# Публікація подій agent.telegram.update для Router/microDAO
NATS_URL: str = "nats://nats:4222"
# DAGI Router (опційно, для майбутньої інтеграції)
# Використовується для маршрутизації повідомлень до агентів
ROUTER_BASE_URL: str = "http://router:9102"
# Debug логування (true для детальних логів)
DEBUG: bool = False
# Шлях до файлу конфігурації ботів (опційно)
# За замовчуванням: /app/bots.yaml (в контейнері) або ./bots.yaml (локально)
BOTS_CONFIG_FILE: Optional[str] = None
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
settings = Settings()
def load_bots_config() -> List[BotConfig]:
"""
Завантажити конфігурацію ботів з:
1. YAML файлу (якщо вказано BOTS_CONFIG_FILE або /app/bots.yaml в контейнері)
2. Environment variables (BOT_<AGENT_ID>_TOKEN)
3. Повернути порожній список, якщо нічого не знайдено
Пріоритет:
1. bots.yaml (якщо існує)
2. Environment variables: BOT_<AGENT_ID>_TOKEN
"""
bots: List[BotConfig] = []
# Спробувати завантажити з YAML
config_file = settings.BOTS_CONFIG_FILE
if not config_file:
# Спочатку спробувати /app/bots.yaml (шлях в контейнері)
container_path = Path("/app/bots.yaml")
if container_path.exists():
config_file = container_path
else:
# Fallback: шукати bots.yaml в корені telegram-gateway (для локальної розробки)
gateway_root = Path(__file__).parent.parent
config_file = gateway_root / "bots.yaml"
if config_file and Path(config_file).exists():
try:
with open(config_file, "r", encoding="utf-8") as f:
data = yaml.safe_load(f)
if data and "bots" in data:
for bot_data in data["bots"]:
bots.append(BotConfig(**bot_data))
logger.info(f"✅ Loaded {len(bots)} bot(s) from {config_file}")
except Exception as e:
logger.warning(f"⚠️ Failed to load bots from {config_file}: {e}")
# Fallback: завантажити з env variables
if not bots:
env_prefix = "BOT_"
for key, value in os.environ.items():
if key.startswith(env_prefix) and key.endswith("_TOKEN"):
# Витягти agent_id з ключа: BOT_DAARWIZZ_TOKEN -> daarwizz
agent_id = key[len(env_prefix):-len("_TOKEN")].lower()
# Перевірити, чи вже не додано з YAML
if not any(b.agent_id == agent_id for b in bots):
bots.append(BotConfig(
agent_id=agent_id,
bot_token=value,
enabled=True
))
logger.info(f"✅ Loaded bot '{agent_id}' from environment variable {key}")
return bots

View File

@@ -1,34 +0,0 @@
from fastapi import APIRouter
from .bots_registry import bots_registry
from .telegram_listener import telegram_listener
router = APIRouter(prefix="/debug", tags=["debug"])
@router.get("/bots")
async def list_bots():
"""Список зареєстрованих ботів"""
return {
"registered_bots": len(telegram_listener._bots),
"bot_tokens": [token[:16] + "..." for token in telegram_listener._bots.keys()],
"registry_mappings": len(bots_registry._agent_to_token),
"active_tasks": len(telegram_listener._tasks)
}
@router.get("/bots/tasks")
async def list_tasks():
"""Статус polling tasks"""
tasks_status = {}
for token, task in telegram_listener._tasks.items():
tasks_status[token[:16] + "..."] = {
"done": task.done(),
"cancelled": task.cancelled(),
}
if task.done() and not task.cancelled():
try:
task.result()
except Exception as e:
tasks_status[token[:16] + "..."]["error"] = str(e)
return tasks_status

View File

@@ -1,182 +0,0 @@
import asyncio
import logging
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from .config import settings, load_bots_config
from .models import BotRegistration, TelegramSendCommand
from .bots_registry import bots_registry
from .nats_client import nats_client
from .telegram_listener import telegram_listener
from .router_handler import router_handler
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO if settings.DEBUG else logging.WARNING)
app = FastAPI(title="telegram-gateway", version="0.1.0")
ALLOWED_ORIGINS = [
"http://localhost:8899",
"http://127.0.0.1:8899",
"http://localhost:5173",
"http://127.0.0.1:5173",
]
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.on_event("startup")
async def on_startup():
# 1. Підключаємося до NATS
await nats_client.connect()
logger.info("✅ Connected to NATS at %s", settings.NATS_URL)
# 2. Завантажити конфігурацію ботів з bots.yaml або env
try:
bot_configs = load_bots_config()
logger.info("📋 Loaded %d bot(s) from config", len(bot_configs))
except Exception as e:
logger.warning("⚠️ Failed to load bots config: %s", e)
bot_configs = []
# 3. Зареєструвати всі боти в реєстрі
if bot_configs:
bots_registry.register_batch(bot_configs)
logger.info("📝 Registered %d bot(s) in registry", len(bot_configs))
# 4. Запустити polling для кожного бота
for bot_config in bot_configs:
if not bot_config.enabled:
logger.debug("⏭️ Skipping disabled bot: agent_id=%s", bot_config.agent_id)
continue
agent_id = bot_config.agent_id
bot_token = bot_config.bot_token
# Запускаємо polling в фоновій задачі
asyncio.create_task(telegram_listener.add_bot(bot_token))
logger.info("🚀 Started polling for agent=%s (token=%s...)", agent_id, bot_token[:16])
# Публікувати подію реєстрації
await nats_client.publish_json(
subject="bot.registered",
data={"agent_id": agent_id, "bot_token": bot_token[:8] + "..."}
)
enabled_count = len([b for b in bot_configs if b.enabled])
logger.info("✅ Initialized %d bot(s)", enabled_count)
else:
logger.warning("⚠️ No bots configured. Use /bots/register to add bots manually.")
# 5. Запустити NATS subscriber для обробки подій та виклику Router
try:
await router_handler.start_subscription()
logger.info("✅ RouterHandler subscription started")
except Exception as e:
logger.warning(f"⚠️ Failed to start RouterHandler subscription: {e}")
@app.on_event("shutdown")
async def on_shutdown():
await router_handler.close()
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.
"""
logger.info(f"Registering bot via API: agent_id={reg.agent_id}")
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[:8] + "..."}
)
return {"status": "registered", "agent_id": reg.agent_id}
@app.delete("/bots/{agent_id}")
async def unregister_bot(agent_id: str):
"""Відключити бота та зупинити polling"""
bot_token = bots_registry.unregister(agent_id)
if not bot_token:
raise HTTPException(status_code=404, detail="Bot not registered")
await telegram_listener.remove_bot(bot_token)
await nats_client.publish_json(
subject="bot.unregistered",
data={"agent_id": agent_id, "bot_token": bot_token[:8] + "..."},
)
return {"status": "unregistered", "agent_id": agent_id}
@app.get("/bots/list")
async def list_bots():
"""Повернути список зареєстрованих ботів"""
agents = bots_registry.list_agents()
return {"bots": agents, "count": len(agents)}
@app.get("/bots/status/{agent_id}")
async def bot_status(agent_id: str):
"""Повернути статус конкретного бота"""
status = telegram_listener.get_status(agent_id)
return status
@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"}
@app.get("/")
async def root():
"""Root endpoint"""
return {
"service": "Telegram Gateway",
"version": "0.1.0",
"docs": "/docs",
"endpoints": [
"GET /healthz",
"POST /bots/register",
"POST /send"
]
}

View File

@@ -1,29 +0,0 @@
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
metadata: Optional[Dict[str, Any]] = 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, ім'я, тощо

View File

@@ -1,30 +0,0 @@
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)

View File

@@ -1,501 +0,0 @@
"""
NATS subscriber для обробки подій agent.telegram.update
Викликає Router через HTTP API та відправляє відповідь назад в Telegram
"""
import asyncio
import json
import logging
from typing import Dict, Any
import httpx
import nats
from .config import settings
from .models import TelegramUpdateEvent, TelegramSendCommand
from .telegram_listener import telegram_listener
logger = logging.getLogger(__name__)
class RouterHandler:
"""Обробник подій з NATS, який викликає Router та відправляє відповіді"""
def __init__(self):
self._nc = None
self._sub = None
self._router_url = settings.ROUTER_BASE_URL
self._running = False
async def connect(self):
"""Підключитися до NATS"""
if self._nc is None or self._nc.is_closed:
self._nc = await nats.connect(settings.NATS_URL)
logger.info(f"✅ RouterHandler connected to NATS at {settings.NATS_URL}")
async def start_subscription(self):
"""Підписатися на події agent.telegram.update"""
await self.connect()
async def message_handler(msg):
"""Обробка повідомлення з NATS"""
try:
data = json.loads(msg.data.decode())
event = TelegramUpdateEvent(**data)
logger.info(
f"📥 Received NATS event: agent={event.agent_id}, "
f"chat={event.chat_id}, text_len={len(event.text or '')}"
)
# Обробити подію асинхронно
asyncio.create_task(self._handle_telegram_event(event))
# NOTE: No ack() for core NATS (non-JetStream) subscriptions
# JetStream ack is only needed when using JetStream consumers
except Exception as e:
logger.error(f"❌ Error processing NATS message: {e}", exc_info=True)
# Підписатися на subject
self._sub = await self._nc.subscribe("agent.telegram.update", cb=message_handler)
self._running = True
logger.info("✅ Subscribed to NATS subject: agent.telegram.update")
async def _handle_telegram_event(self, event: TelegramUpdateEvent):
"""Обробити подію Telegram та викликати Router"""
try:
metadata = event.metadata or {}
# Обробка фото (Vision Encoder)
if "photo" in metadata:
await self._handle_photo(event, metadata)
return
# Обробка документів (Parser Service)
if "document" in metadata:
await self._handle_document(event, metadata)
return
# Звичайні текстові повідомлення
if not event.text:
logger.debug(f"Skipping event without text: agent={event.agent_id}")
return
# Отримати системний промпт для агента
system_prompt = self._get_system_prompt(event.agent_id)
# Викликати Router через HTTP API
# Структура: payload.context.system_prompt (як очікує Router)
router_request = {
"message": event.text,
"mode": "chat",
"agent": event.agent_id,
"source": "telegram",
"user_id": f"tg:{event.user_id}",
"session_id": f"telegram:{event.chat_id}",
"payload": {
"context": {
"agent_name": event.agent_id.upper(),
"system_prompt": system_prompt, # Системний промпт для агента
}
}
}
# Детальне логування перед відправкою
payload_keys = list(router_request.get('payload', {}).keys())
context_keys = list(router_request.get('payload', {}).get('context', {}).keys())
sp_len = len(system_prompt) if system_prompt else 0
logger.info(
f"📞 Calling Router: agent={event.agent_id}, chat={event.chat_id}"
)
logger.info(
f" payload.keys={payload_keys}, "
f"context.keys={context_keys}, "
f"system_prompt_len={sp_len}"
)
if system_prompt:
logger.info(f" system_prompt preview: {system_prompt[:80]}...")
# Логування JSON перед відправкою (з повним system_prompt для діагностики)
import json
full_json = json.dumps(router_request, ensure_ascii=False)
logger.info(f"📤 Full request JSON length: {len(full_json)} bytes")
logger.info(f"📤 Payload.context.system_prompt in request: {router_request.get('payload', {}).get('context', {}).get('system_prompt', '')[:100]}...")
async with httpx.AsyncClient(timeout=120.0) as client: # Збільшено timeout до 120 сек
logger.info(f"📡 Sending HTTP POST to {self._router_url}/route")
response = await client.post(
f"{self._router_url}/route",
json=router_request
)
logger.info(f"📡 Router response status: {response.status_code}")
# Перевірка на 502 Bad Gateway
if response.status_code == 502:
logger.error(f"❌ Router returned 502 Bad Gateway for agent={event.agent_id}")
await telegram_listener.send_message(
agent_id=event.agent_id,
chat_id=event.chat_id,
text="⚠️ Вибач, зараз велике навантаження. Спробуй через хвилину."
)
return
response.raise_for_status()
result = response.json()
# Отримати відповідь
answer = None
if isinstance(result, dict):
answer = (
result.get("data", {}).get("text") or
result.get("data", {}).get("answer") or
result.get("response") or
result.get("text")
)
if not answer:
logger.warning(f"⚠️ No answer from Router for agent={event.agent_id}")
answer = "Вибач, зараз не можу відповісти."
logger.info(f"📤 Sending response: agent={event.agent_id}, chat={event.chat_id}, len={len(answer)}")
# Перевірити чи треба відповідати голосом (якщо користувач надіслав voice)
raw_update = event.raw_update or {}
should_reply_voice = raw_update.get("voice") or raw_update.get("audio") or raw_update.get("video_note")
if should_reply_voice:
# Синтезувати голос
audio_bytes = await self._text_to_speech(answer)
if audio_bytes:
logger.info(f"🔊 Sending voice response: agent={event.agent_id}, audio_size={len(audio_bytes)}")
await telegram_listener.send_voice(
agent_id=event.agent_id,
chat_id=event.chat_id,
audio_bytes=audio_bytes
)
else:
# Fallback to text
await telegram_listener.send_message(
agent_id=event.agent_id,
chat_id=event.chat_id,
text=answer
)
else:
# Звичайна текстова відповідь
await telegram_listener.send_message(
agent_id=event.agent_id,
chat_id=event.chat_id,
text=answer
)
logger.info(f"✅ Response sent: agent={event.agent_id}, chat={event.chat_id}")
except httpx.HTTPError as e:
logger.error(f"❌ HTTP error calling Router: {e}")
# Відправити повідомлення про помилку користувачу
try:
await telegram_listener.send_message(
agent_id=event.agent_id,
chat_id=event.chat_id,
text="❌ Помилка зв'язку з сервером. Спробуй ще раз."
)
except:
pass
except Exception as e:
logger.error(f"❌ Error handling Telegram event: {e}", exc_info=True)
async def _handle_photo(self, event: TelegramUpdateEvent, metadata: Dict[str, Any]):
"""Обробити фото через Swapper vision-8b модель"""
try:
photo_info = metadata.get("photo", {})
file_url = photo_info.get("file_url", "")
caption = event.text or ""
logger.info(f"🖼️ Processing photo: agent={event.agent_id}, url={file_url[:50]}...")
# Відправити до Router з specialist_vision_8b через Swapper
router_request = {
"message": f"Опиши це зображення детально: {file_url}",
"mode": "chat",
"agent": event.agent_id,
"metadata": {
"source": "telegram",
"chat_id": event.chat_id,
"file_url": file_url,
"has_image": True,
},
}
# Override LLM to use specialist_vision_8b for image understanding
router_request["metadata"]["use_llm"] = "specialist_vision_8b"
try:
async with httpx.AsyncClient(timeout=90.0) as client:
response = await client.post(f"{self._router_url}/route", json=router_request)
response.raise_for_status()
result = response.json()
if result.get("ok"):
answer_text = result.get("data", {}).get("text") or result.get("response", "")
if answer_text:
await telegram_listener.send_message(
agent_id=event.agent_id,
chat_id=event.chat_id,
text=f"✅ **Фото оброблено**\n\n{answer_text}"
)
return
# Якщо помилка
error_msg = result.get("error", "Unknown error")
logger.error(f"Router error: {error_msg}")
await telegram_listener.send_message(
agent_id=event.agent_id,
chat_id=event.chat_id,
text=f"Вибач, не вдалося обробити фото: {error_msg}"
)
except Exception as e:
logger.error(f"Error calling Router: {e}", exc_info=True)
await telegram_listener.send_message(
agent_id=event.agent_id,
chat_id=event.chat_id,
text="Вибач, не вдалося обробити фото. Переконайся, що Swapper Service з vision-8b моделлю запущений."
)
logger.info(f"✅ Photo response sent: agent={event.agent_id}, chat={event.chat_id}")
except Exception as e:
logger.error(f"❌ Error handling photo: {e}", exc_info=True)
try:
await telegram_listener.send_message(
agent_id=event.agent_id,
chat_id=event.chat_id,
text="❌ Помилка обробки зображення."
)
except:
pass
async def _handle_document(self, event: TelegramUpdateEvent, metadata: Dict[str, Any]):
"""Обробити PDF через Parser Service"""
try:
doc_info = metadata.get("document", {})
file_url = doc_info.get("file_url", "")
file_name = doc_info.get("file_name", "document.pdf")
logger.info(f"📄 Processing document: agent={event.agent_id}, file={file_name}")
# Викликати Parser Service через DAGI Router
parsed_content = await self._parse_document(file_url, file_name)
# Якщо є питання в caption/text - відповісти на основі parsed content
user_question = event.text
if user_question and user_question != f"[DOCUMENT] {file_name}":
# Додати parsed content до контексту
system_prompt = self._get_system_prompt(event.agent_id)
enhanced_text = f"Користувач запитує про документ '{file_name}':\n{user_question}\n\n[DOCUMENT_CONTENT]:\n{parsed_content[:2000]}"
# Викликати Router для відповіді
router_request = {
"message": enhanced_text,
"mode": "chat",
"agent": event.agent_id,
"source": "telegram",
"user_id": f"tg:{event.user_id}",
"session_id": f"telegram:{event.chat_id}",
"payload": {
"context": {
"agent_name": event.agent_id.upper(),
"system_prompt": system_prompt,
}
}
}
async with httpx.AsyncClient(timeout=120.0) as client:
response = await client.post(
f"{self._router_url}/route",
json=router_request
)
if response.status_code == 502:
await telegram_listener.send_message(
agent_id=event.agent_id,
chat_id=event.chat_id,
text="⚠️ Вибач, зараз велике навантаження. Спробуй через хвилину."
)
return
response.raise_for_status()
result = response.json()
# Отримати відповідь
answer = (
result.get("data", {}).get("text") or
result.get("data", {}).get("answer") or
result.get("response") or
result.get("text") or
"Вибач, не зміг проаналізувати документ."
)
await telegram_listener.send_message(
agent_id=event.agent_id,
chat_id=event.chat_id,
text=answer
)
else:
# Просто парсинг без питання
summary = parsed_content[:500] if parsed_content else "Документ оброблено"
await telegram_listener.send_message(
agent_id=event.agent_id,
chat_id=event.chat_id,
text=f"✅ Документ '{file_name}' оброблено.\n\n{summary}...\n\nЗадай питання про нього!"
)
logger.info(f"✅ Document response sent: agent={event.agent_id}, chat={event.chat_id}")
except Exception as e:
logger.error(f"❌ Error handling document: {e}", exc_info=True)
try:
await telegram_listener.send_message(
agent_id=event.agent_id,
chat_id=event.chat_id,
text="❌ Помилка обробки документу. Спробуй ще раз."
)
except:
pass
async def _parse_document(self, doc_url: str, file_name: str) -> str:
"""Викликати Parser Service для PDF"""
try:
logger.info(f"📡 Calling Parser Service: url={doc_url[:50]}..., file={file_name}")
async with httpx.AsyncClient(timeout=90.0) as client:
# Виклик DAGI Router з mode: "doc_parse"
response = await client.post(
f"{self._router_url}/route",
json={
"mode": "doc_parse",
"agent": "parser",
"payload": {
"context": {
"doc_url": doc_url,
"file_name": file_name,
"output_mode": "markdown"
}
}
}
)
response.raise_for_status()
result = response.json()
# Витягнути parsed content
if "data" in result:
markdown = result["data"].get("markdown", "")
if markdown:
return markdown
# Fallback
return result.get("text", "") or result.get("response", "") or "Документ оброблено"
except Exception as e:
logger.error(f"❌ Parser Service error: {e}")
return "[Не вдалося прочитати документ]"
async def _text_to_speech(self, text: str) -> bytes:
"""Синтезувати голос через TTS Service"""
try:
logger.info(f"🔊 Calling TTS Service: text_len={len(text)}")
async with httpx.AsyncClient(timeout=60.0) as client:
response = await client.post(
"http://dagi-tts:9100/tts",
json={
"text": text[:500], # Обмежуємо довжину для TTS
"lang": "uk"
}
)
response.raise_for_status()
audio_bytes = response.content
logger.info(f"✅ TTS response: {len(audio_bytes)} bytes")
return audio_bytes
except Exception as e:
logger.error(f"❌ TTS Service error: {e}")
return b"" # Fallback to text
def _get_system_prompt(self, agent_id: str) -> str:
"""Отримати системний промпт для агента"""
# Системні промпти для агентів
prompts = {
"helion": """Ти - Helion, AI-агент платформи Energy Union екосистеми DAARION.city.
Допомагай користувачам з технологіями EcoMiner/BioMiner, токеномікою та DAO governance.
Твої основні функції:
- Консультації з енергетичними технологіями (сонячні панелі, вітряки, біогаз)
- Пояснення токеноміки Energy Union (ENERGY токен, стейкінг, винагороди)
- Допомога з onboarding в DAO
- Відповіді на питання про EcoMiner/BioMiner устаткування
Стиль спілкування:
- професійний, технічний, але зрозумілий
- точний у цифрах та даних
- конструктивний у рекомендаціях
Важливо:
- Не вигадуй дані, яких немає в системі
- Якщо дані недоступні — чесно скажи про це
- Не давай фінансових порад без консультації з експертами""",
"daarwizz": """Ти — DAARWIZZ, офіційний AI-агент екосистеми DAARION.city.
Допомагай учасникам з microDAO, ролями та процесами.
Відповідай коротко, практично, враховуй RBAC контекст користувача.""",
"greenfood": """Ти — GREENFOOD Assistant, фронтовий оркестратор ERP-системи для крафтових виробників, хабів та покупців.
Твоя місія: зрозуміти, хто з тобою говорить (комітент, менеджер складу, логіст, бухгалтер, маркетолог, покупець), виявити намір і делегувати завдання спеціалізованим агентам GREENFOOD.
У твоєму розпорядженні 12 спеціалізованих агентів:
- Product & Catalog (каталог товарів)
- Batch & Quality (партії та якість)
- Vendor Success (успіх комітентів)
- Warehouse (склад)
- Logistics & Delivery (доставка)
- Seller (продажі)
- Customer Care (підтримка)
- Finance & Pricing (фінанси)
- SMM & Campaigns (маркетинг)
- SEO & Web (SEO)
- Analytics & BI (аналітика)
- Compliance & Audit (аудит)
Правила роботи:
- Спочатку уточнюй роль і контекст
- Перетворюй запит на чітку дію
- Не вигадуй дані - якщо чогось немає, чесно кажи
- Завжди давай коротке резюме: що зроблено, наступні кроки
Відповідай українською, чітко та по-діловому.""",
}
prompt = prompts.get(agent_id.lower(), "")
if prompt:
logger.debug(f"Using system prompt for agent={agent_id}, len={len(prompt)}")
else:
logger.warning(f"No system prompt found for agent={agent_id}")
return prompt
async def close(self):
"""Закрити підписку та з'єднання"""
self._running = False
if self._sub:
await self._sub.unsubscribe()
if self._nc and not self._nc.is_closed:
await self._nc.drain()
await self._nc.close()
logger.info("RouterHandler closed")
router_handler = RouterHandler()

View File

@@ -1,322 +0,0 @@
import asyncio
import logging
from typing import Any, Dict, Optional
from aiogram import Bot, Dispatcher, F
from aiogram.client.session.aiohttp import AiohttpSession
from aiogram.client.telegram import TelegramAPIServer
from aiogram.types import Message
from .config import settings
from .models import TelegramUpdateEvent
from .nats_client import nats_client
from .bots_registry import bots_registry
from .voice_handler import handle_voice_message, handle_document_message
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:
logger.info("🔄 Bot already registered: %s...", bot_token[:16])
return
logger.info("🤖 Creating bot: %s...", bot_token[:16])
bot = await self._create_bot(bot_token)
dp = Dispatcher()
# TEXT
@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[:16])
return
logger.info(
"📨 Received TEXT: agent=%s, chat=%s, user=%s, len=%d",
agent_id,
message.chat.id,
message.from_user.id if message.from_user else 0,
len(message.text or ""),
)
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(),
)
logger.debug("✅ Published TEXT to NATS: agent=%s, chat_id=%s", agent_id, message.chat.id)
# VOICE / AUDIO / VIDEO NOTE
@dp.message(F.voice | F.audio | F.video_note)
async def on_voice(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[:16])
return
logger.info(
"🎤 Received VOICE: agent=%s, chat=%s, user=%s",
agent_id,
message.chat.id,
message.from_user.id if message.from_user else 0,
)
await message.answer("🎤 Обробляю голосове повідомлення...")
transcribed_text = await handle_voice_message(message, bot)
if not transcribed_text:
await message.answer("Не вдалося розпізнати голос. Спробуйте ще раз.")
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=transcribed_text,
raw_update=message.model_dump(),
)
await nats_client.publish_json(
subject="agent.telegram.update",
data=event.model_dump(),
)
logger.info("✅ Published STT text to NATS: agent=%s", agent_id)
# DOCUMENTS (PDF)
@dp.message(F.document)
async def on_document(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[:16])
return
logger.info(
"📄 Received DOCUMENT: agent=%s, chat=%s, user=%s, file=%s",
agent_id,
message.chat.id,
message.from_user.id if message.from_user else 0,
message.document.file_name if message.document else "unknown",
)
doc_info = await handle_document_message(message, bot)
if not doc_info:
logger.info("⏭️ Not a PDF or processing skipped")
return
await message.answer(f"📄 Обробляю документ: {doc_info.get('file_name')}...")
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.caption or f"[DOCUMENT] {doc_info.get('file_name')}",
raw_update=message.model_dump(),
metadata={"document": doc_info},
)
await nats_client.publish_json(
subject="agent.telegram.update",
data=event.model_dump(),
)
logger.info("✅ Published DOCUMENT to NATS: agent=%s", agent_id)
# PHOTOS
@dp.message(F.photo)
async def on_photo(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[:16])
return
logger.info(
"🖼️ Received PHOTO: agent=%s, chat=%s, user=%s",
agent_id,
message.chat.id,
message.from_user.id if message.from_user else 0,
)
photo = message.photo[-1] if message.photo else None
if not photo:
logger.warning("⏭️ Photo payload missing")
return
await message.answer("🖼️ Обробляю зображення...")
try:
file = await bot.get_file(photo.file_id)
file_path = file.file_path
file_url = f"https://api.telegram.org/file/bot{bot.token}/{file_path}"
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.caption or "[IMAGE]",
raw_update=message.model_dump(),
metadata={
"photo": {
"file_url": file_url,
"file_id": photo.file_id,
"file_size": photo.file_size,
"width": photo.width,
"height": photo.height,
}
},
)
await nats_client.publish_json(
subject="agent.telegram.update",
data=event.model_dump(),
)
logger.info("✅ Published PHOTO to NATS: agent=%s", agent_id)
except Exception as e:
logger.error("❌ Error processing photo: %s", e, exc_info=True)
await message.answer("❌ Помилка обробки зображення. Спробуйте ще раз.")
async def _polling():
try:
logger.info("🔁 Start polling for bot %s...", bot_token[:16])
await dp.start_polling(bot)
except asyncio.CancelledError:
logger.info("🛑 Polling cancelled for bot %s...", bot_token[:16])
except Exception as e:
logger.exception("💥 Polling error for bot %s...: %s", bot_token[:16], e)
raise
task = asyncio.create_task(_polling())
self._bots[bot_token] = bot
self._dispatchers[bot_token] = dp
self._tasks[bot_token] = task
logger.info("✅ Bot registered and polling started: %s...", bot_token[:16])
async def send_message(
self,
agent_id: str,
chat_id: int,
text: str,
reply_to_message_id: Optional[int] = None,
):
logger.info(
"Sending message: agent_id=%s, chat_id=%s, text_length=%d, reply_to=%s",
agent_id,
chat_id,
len(text),
reply_to_message_id,
)
bot_token = bots_registry.get_token_by_agent(agent_id)
if not bot_token:
logger.error("No bot token for agent_id=%s", agent_id)
raise RuntimeError(f"No bot token for agent_id={agent_id}")
bot = await self._ensure_bot(bot_token)
await bot.send_message(
chat_id=chat_id,
text=text,
reply_to_message_id=reply_to_message_id,
)
logger.info("✅ Message sent: agent_id=%s, chat_id=%s", agent_id, chat_id)
async def send_voice(
self,
agent_id: str,
chat_id: int,
audio_bytes: bytes,
reply_to_message_id: Optional[int] = None,
):
if not audio_bytes:
logger.warning("Empty audio_bytes for agent_id=%s, skip", agent_id)
return
bot_token = bots_registry.get_token_by_agent(agent_id)
if not bot_token:
logger.error("No bot token for agent_id=%s", agent_id)
raise RuntimeError(f"No bot token for agent_id={agent_id}")
bot = await self._ensure_bot(bot_token)
from aiogram.types import BufferedInputFile
audio_file = BufferedInputFile(audio_bytes, filename="voice.mp3")
await bot.send_voice(
chat_id=chat_id,
voice=audio_file,
reply_to_message_id=reply_to_message_id,
)
logger.info("✅ Voice message sent: agent_id=%s, chat_id=%s", agent_id, chat_id)
async def remove_bot(self, bot_token: str) -> None:
"""Зупинити polling та видалити бота"""
task = self._tasks.pop(bot_token, None)
if task:
task.cancel()
await asyncio.gather(task, return_exceptions=True)
bot = self._bots.pop(bot_token, None)
if bot:
await bot.session.close()
self._dispatchers.pop(bot_token, None)
logger.info("🗑️ Bot stopped and removed: %s...", bot_token[:16])
async def _ensure_bot(self, bot_token: str) -> Bot:
bot = self._bots.get(bot_token)
if bot:
return bot
await self.add_bot(bot_token)
return self._bots[bot_token]
def get_status(self, agent_id: str) -> Dict[str, any]:
"""Повернути статус бота (для UI)"""
bot_token = bots_registry.get_token_by_agent(agent_id)
if not bot_token:
return {"agent_id": agent_id, "registered": False}
task = self._tasks.get(bot_token)
status = {
"agent_id": agent_id,
"registered": True,
"token_prefix": bot_token[:8],
"polling": bool(task) and not task.done(),
"task_cancelled": bool(task) and task.cancelled(),
}
return status
async def shutdown(self):
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()

View File

@@ -1,303 +0,0 @@
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
from .voice_handler import handle_voice_message, handle_document_message
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:
logger.info("🔄 Bot already registered: %s...", bot_token[:16])
return
logger.info("🤖 Creating bot: %s...", bot_token[:16])
bot = await self._create_bot(bot_token)
dp = Dispatcher()
# Handler for text messages
@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[:16])
return
logger.info("📨 Received TEXT: agent=%s, chat=%s, user=%s, len=%d",
agent_id, message.chat.id, message.from_user.id if message.from_user else 0,
len(message.text or ""))
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()
)
logger.info("📤 Publishing to NATS: subject=agent.telegram.update, agent=%s", agent_id)
await nats_client.publish_json(
subject="agent.telegram.update",
data=event.model_dump()
)
logger.debug("✅ Published to NATS: agent=%s, chat_id=%s", agent_id, message.chat.id)
# Handler for voice messages
@dp.message(F.voice | F.audio | F.video_note)
async def on_voice(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[:16])
return
logger.info("🎤 Received VOICE: agent=%s, chat=%s, user=%s",
agent_id, message.chat.id, message.from_user.id if message.from_user else 0)
# Send "processing" message
await message.answer("🎤 Обробляю голосове повідомлення...")
# Process voice through STT
transcribed_text = await handle_voice_message(message, bot_token)
if not transcribed_text:
await message.answer("Не вдалося розпізнати голос. Спробуйте ще раз.")
return
logger.info("📝 Transcribed (%d chars): %s...", len(transcribed_text), transcribed_text[:50])
# Publish transcribed text as regular message to NATS
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=transcribed_text,
raw_update=message.model_dump()
)
logger.info("📤 Publishing transcribed text to NATS: agent=%s", agent_id)
await nats_client.publish_json(
subject="agent.telegram.update",
data=event.model_dump()
)
# Handler for documents (PDF)
@dp.message(F.document)
async def on_document(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[:16])
return
logger.info("📄 Received DOCUMENT: agent=%s, chat=%s, user=%s, file=%s",
agent_id, message.chat.id, message.from_user.id if message.from_user else 0,
message.document.file_name if message.document else "unknown")
# Process document
doc_info = await handle_document_message(message, bot_token)
if not doc_info:
logger.info("⏭️ Not a PDF or processing skipped")
return
# Send "processing" message
await message.answer(f"📄 Обробляю документ: {doc_info.get('file_name')}...")
# Publish document info to NATS
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=f"[DOCUMENT] {doc_info.get('file_name')}",
raw_update=message.model_dump(),
metadata={"document": doc_info}
)
logger.info("📤 Publishing document to NATS: agent=%s, file=%s", agent_id, doc_info.get('file_name'))
await nats_client.publish_json(
subject="agent.telegram.update",
data=event.model_dump()
)
# Handler for photos/images
@dp.message(F.photo)
async def on_photo(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[:16])
return
logger.info("🖼️ Received PHOTO: agent=%s, chat=%s, user=%s",
agent_id, message.chat.id, message.from_user.id if message.from_user else 0)
# Get largest photo
photo = message.photo[-1] if message.photo else None
if not photo:
return
# Send "processing" message
await message.answer("🖼️ Обробляю зображення...")
# Get file info
file_id = photo.file_id
file_size = photo.file_size
try:
# Get file from bot
file = await bot.get_file(file_id)
file_path = file.file_path
# Download file URL (через офіційний Telegram API для файлів)
file_url = f"https://api.telegram.org/file/bot{bot_token}/{file_path}"
logger.info("📥 Photo URL: %s", file_url)
# Prepare caption or default text
caption = message.caption or "[IMAGE]"
# Publish photo info to NATS
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=caption,
raw_update=message.model_dump(),
metadata={
"photo": {
"file_url": file_url,
"file_id": file_id,
"file_size": file_size,
"width": photo.width,
"height": photo.height,
}
}
)
logger.info("📤 Publishing photo to NATS: agent=%s", agent_id)
await nats_client.publish_json(
subject="agent.telegram.update",
data=event.model_dump()
)
except Exception as e:
logger.error(f"❌ Error processing photo: {e}", exc_info=True)
await message.answer("❌ Помилка обробки зображення. Спробуйте ще раз.")
# Запускаємо polling у фоні
async def _polling():
try:
logger.info("🔁 Start polling for bot %s...", bot_token[:16])
await dp.start_polling(bot)
except asyncio.CancelledError:
logger.info("🛑 Polling cancelled for bot %s...", bot_token[:16])
except Exception as e:
logger.exception("💥 Polling error for bot %s...: %s", bot_token[:16], e)
raise
task = asyncio.create_task(_polling())
self._bots[bot_token] = bot
self._dispatchers[bot_token] = dp
self._tasks[bot_token] = task
logger.info("✅ Bot registered and polling started: %s...", bot_token[:16])
async def send_message(self, agent_id: str, chat_id: int, text: str, reply_to_message_id: int | None = None):
# Логування перед відправкою
logger.info(
f"Sending message: agent_id={agent_id}, chat_id={chat_id}, "
f"text_length={len(text)}, reply_to={reply_to_message_id}"
)
bot_token = bots_registry.get_token_by_agent(agent_id)
if not bot_token:
logger.error(f"No bot token for agent_id={agent_id}")
raise RuntimeError(f"No bot token for agent_id={agent_id}")
bot = self._bots.get(bot_token)
if not bot:
# Якщо бот ще не запущений (наприклад, перший виклик через /send)
logger.info(f"Bot not started yet, initializing: agent_id={agent_id}")
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
)
logger.info(f"Message sent successfully: agent_id={agent_id}, chat_id={chat_id}")
async def send_voice(self, agent_id: str, chat_id: int, audio_bytes: bytes, reply_to_message_id: int | None = None):
"""Відправити голосове повідомлення"""
logger.info(
f"Sending voice: agent_id={agent_id}, chat_id={chat_id}, "
f"audio_size={len(audio_bytes)} bytes, reply_to={reply_to_message_id}"
)
bot_token = bots_registry.get_token_by_agent(agent_id)
if not bot_token:
logger.error(f"No bot token for agent_id={agent_id}")
raise RuntimeError(f"No bot token for agent_id={agent_id}")
bot = self._bots.get(bot_token)
if not bot:
logger.info(f"Bot not started yet, initializing: agent_id={agent_id}")
await self.add_bot(bot_token)
bot = self._bots[bot_token]
if not audio_bytes:
logger.warning(f"Empty audio_bytes for agent_id={agent_id}, skipping")
return
# Створити BytesIO об'єкт для aiogram
from io import BytesIO
from aiogram.types import BufferedInputFile
audio_file = BufferedInputFile(audio_bytes, filename="voice.mp3")
await bot.send_voice(
chat_id=chat_id,
voice=audio_file,
reply_to_message_id=reply_to_message_id
)
logger.info(f"Voice sent successfully: agent_id={agent_id}, chat_id={chat_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()

View File

@@ -1,135 +0,0 @@
"""
Voice and Document Handler for Telegram Gateway
Handles STT (Speech-to-Text) and document processing
"""
import logging
import httpx
from aiogram import Bot
from aiogram.types import Message
logger = logging.getLogger(__name__)
STT_SERVICE_URL = "http://dagi-stt:9000/stt"
PARSER_SERVICE_URL = "http://dagi-parser:9400"
async def handle_voice_message(message: Message, bot: Bot) -> str:
"""
Process voice/audio message through STT
Args:
message: Telegram message with voice/audio
bot_token: Bot token for file download
Returns:
Transcribed text
"""
# Get file_id from different message types
file_id = None
if message.voice:
file_id = message.voice.file_id
duration = message.voice.duration
elif message.audio:
file_id = message.audio.file_id
duration = message.audio.duration
elif message.video_note:
file_id = message.video_note.file_id
duration = message.video_note.duration
if not file_id:
logger.error("No file_id found in voice message")
return ""
logger.info(f"🎤 Processing voice: file_id={file_id}, duration={duration}s")
try:
# Get file path from Telegram
file = await bot.get_file(file_id)
file_path = file.file_path
# Download file URL (через офіційний Telegram API для файлів)
file_url = f"https://api.telegram.org/file/bot{bot.token}/{file_path}"
logger.info(f"📥 Downloading audio: {file_url}")
# Download audio file
async with httpx.AsyncClient(timeout=30.0) as client:
audio_response = await client.get(file_url)
audio_response.raise_for_status()
audio_bytes = audio_response.content
logger.info(f"✅ Downloaded {len(audio_bytes)} bytes")
# Send to STT service
logger.info(f"🔊 Sending to STT: {STT_SERVICE_URL}")
async with httpx.AsyncClient(timeout=60.0) as client:
files = {"file": ("audio.ogg", audio_bytes, "audio/ogg")}
stt_response = await client.post(STT_SERVICE_URL, files=files)
stt_response.raise_for_status()
result = stt_response.json()
transcribed_text = result.get("text", "")
logger.info(f"📝 Transcribed: {transcribed_text[:100]}...")
return transcribed_text
except httpx.HTTPError as e:
logger.error(f"❌ HTTP error in voice processing: {e}")
return ""
except Exception as e:
logger.error(f"❌ Error in voice processing: {e}", exc_info=True)
return ""
async def handle_document_message(message: Message, bot: Bot) -> dict:
"""
Process document (PDF) message through Parser
Args:
message: Telegram message with document
bot_token: Bot token for file download
Returns:
Dict with document info or empty dict
"""
if not message.document:
return {}
file_name = message.document.file_name or "document"
mime_type = message.document.mime_type
file_id = message.document.file_id
file_size = message.document.file_size
# Check if it's a PDF
is_pdf = mime_type == "application/pdf" or file_name.lower().endswith(".pdf")
if not is_pdf:
logger.info(f"⏭️ Skipping non-PDF document: {file_name} ({mime_type})")
return {}
logger.info(f"📄 Processing PDF: {file_name}, size={file_size} bytes")
try:
# Get file path from Telegram
file = await bot.get_file(file_id)
file_path = file.file_path
# Download file URL (через офіційний Telegram API для файлів)
file_url = f"https://api.telegram.org/file/bot{bot.token}/{file_path}"
logger.info(f"📥 PDF URL: {file_url}")
# Return document info (processing will happen through Router)
return {
"file_url": file_url,
"file_name": file_name,
"file_size": file_size,
"mime_type": mime_type,
"file_id": file_id,
}
except Exception as e:
logger.error(f"❌ Error processing document: {e}", exc_info=True)
return {}

View File

@@ -1,19 +0,0 @@
# Приклад конфігурації ботів для telegram-gateway
# Скопіюй цей файл як bots.yaml і заповни реальні токени
bots:
- agent_id: "daarwizz"
bot_token: "YOUR_DAARWIZZ_BOT_TOKEN"
enabled: true
description: "DAARWIZZ agent bot"
- agent_id: "helion"
bot_token: "YOUR_HELION_BOT_TOKEN"
enabled: true
description: "Helion agent bot"
# Додаткові боти можна додати тут
# - agent_id: "another_agent"
# bot_token: "ANOTHER_BOT_TOKEN"
# enabled: true

View File

@@ -1,8 +0,0 @@
fastapi==0.109.0
uvicorn[standard]==0.27.0
pydantic==2.5.3
aiogram==3.*
nats-py
pydantic-settings
httpx
pyyaml

View File

@@ -1,20 +0,0 @@
#!/usr/bin/env python3
"""Test Router connectivity"""
import asyncio
import httpx
import sys
async def test():
try:
async with httpx.AsyncClient(timeout=5.0) as client:
r = await client.get("http://dagi-router:9102/health")
print(f"Router health: {r.json()}")
return True
except Exception as e:
print(f"Error: {e}")
return False
if __name__ == "__main__":
result = asyncio.run(test())
sys.exit(0 if result else 1)