gateway: add privacy guard plus reminders and mentor relay commands

This commit is contained in:
NODA1 System
2026-02-20 19:01:50 +01:00
parent 987ece5bac
commit c2f0b64604
4 changed files with 593 additions and 1 deletions

View File

@@ -263,6 +263,52 @@ services:
retries: 3
gateway-reminder-worker:
build:
context: ./gateway-bot
dockerfile: Dockerfile
container_name: dagi-gateway-reminder-worker-node1
command: ["python", "-m", "daarion_facade.reminder_worker"]
environment:
- REDIS_URL=redis://redis:6379/0
- DAARION_REMINDER_POLL_SECONDS=${DAARION_REMINDER_POLL_SECONDS:-2}
- DAARION_REMINDER_TTL_SECONDS=${DAARION_REMINDER_TTL_SECONDS:-2592000}
- DAARION_REMINDER_DEFAULT_TZ=${DAARION_REMINDER_DEFAULT_TZ:-Europe/Kyiv}
- GLOBAL_RELAY_ALLOWED_USER_IDS=${GLOBAL_RELAY_ALLOWED_USER_IDS:-}
- MENTOR_PRIVATE_HANDLES=${MENTOR_PRIVATE_HANDLES:-ivantytar,archenvis,olegarch88}
- MENTOR_PRIVATE_NAMES=${MENTOR_PRIVATE_NAMES:-Іван Титар,Александр Вертій,Олег Ковальчук}
- MENTOR_DISCLOSURE_ALLOWED_USER_IDS=${MENTOR_DISCLOSURE_ALLOWED_USER_IDS:-}
- HELION_MENTOR_CHAT_IDS=${HELION_MENTOR_CHAT_IDS:-}
- HELION_RELAY_ALLOWED_USER_IDS=${HELION_RELAY_ALLOWED_USER_IDS:-}
- DAARWIZZ_TELEGRAM_BOT_TOKEN=${DAARWIZZ_TELEGRAM_BOT_TOKEN:-8323412397:AAGZbAR22LuOiGD8xVC3OXMjahQ8rs2lJwo}
- HELION_TELEGRAM_BOT_TOKEN=${HELION_TELEGRAM_BOT_TOKEN:-8112062582:AAGS-HwRLEI269lDutLtAJTFArsIq31YNhE}
- GREENFOOD_TELEGRAM_BOT_TOKEN=${GREENFOOD_TELEGRAM_BOT_TOKEN:-7495165343:AAGR1XEOzg7DkPFPCzL_eYLCJfxJuonCxug}
- AGROMATRIX_TELEGRAM_BOT_TOKEN=${AGROMATRIX_TELEGRAM_BOT_TOKEN:-8580290441:AAFuDBmFJtpl-3I_WfkH7Hkb59X0fhYNMOE}
- ALATEYA_TELEGRAM_BOT_TOKEN=${ALATEYA_TELEGRAM_BOT_TOKEN:-8436880945:AAEi-HS6GEctddoqBUd37MHfweZQP-OjRlo}
- NUTRA_TELEGRAM_BOT_TOKEN=${NUTRA_TELEGRAM_BOT_TOKEN:-8517315428:AAGTLcKxBAZDsMgx28agKTvl1SqJGi0utH4}
- DRUID_TELEGRAM_BOT_TOKEN=${DRUID_TELEGRAM_BOT_TOKEN:-8145618489:AAFR714mBsNmiuF-rjCw-295iORBReJQZ70}
- CLAN_TELEGRAM_BOT_TOKEN=${CLAN_TELEGRAM_BOT_TOKEN:-8516872152:AAHH26wU8hJZJbSCJXb4vbmPmakTP77ok5E}
- EONARCH_TELEGRAM_BOT_TOKEN=${EONARCH_TELEGRAM_BOT_TOKEN:-7962391584:AAFYkelLRG3VR_Lxuu6pEGG76t4vZdANtz4}
- SENPAI_TELEGRAM_BOT_TOKEN=${SENPAI_TELEGRAM_BOT_TOKEN:-8510265026:AAGFrFBIIEihsLptZSxuKdmW2RoRPQDY9FE}
- ONEOK_TELEGRAM_BOT_TOKEN=${ONEOK_TELEGRAM_BOT_TOKEN}
- SOUL_TELEGRAM_BOT_TOKEN=${SOUL_TELEGRAM_BOT_TOKEN:-8041596416:AAHhpfCtY8paCm_9AD-4stJJg-Vw-CBf6Qk}
- YAROMIR_TELEGRAM_BOT_TOKEN=${YAROMIR_TELEGRAM_BOT_TOKEN:-8128180674:AAGNZdG3LwECI4z_803smsuRHsK3nPdjMLY}
- SOFIIA_TELEGRAM_BOT_TOKEN=${SOFIIA_TELEGRAM_BOT_TOKEN:-8589292566:AAEmPvS6nY9e-Y-TZm04CAHWlaFnWVxajE4}
volumes:
- ${DEPLOY_ROOT:-.}/gateway-bot:/app/gateway-bot:ro
- ${DEPLOY_ROOT:-.}/logs:/app/logs
depends_on:
- redis
networks:
- dagi-network
restart: unless-stopped
healthcheck:
test: ["CMD", "python", "-c", "print(\"ok\")"]
interval: 30s
timeout: 5s
retries: 3
metrics-poller-node1:
build:
context: ./gateway-bot

View File

@@ -0,0 +1,100 @@
import asyncio
import logging
import os
from typing import Dict
import httpx
from .reminders import close_redis, pop_due_reminders
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
logger = logging.getLogger("daarion-reminder-worker")
POLL_SECONDS = float(os.getenv("DAARION_REMINDER_POLL_SECONDS", "2"))
TELEGRAM_TIMEOUT = float(os.getenv("DAARION_REMINDER_TELEGRAM_TIMEOUT", "20"))
AGENT_TOKEN_ENV: Dict[str, str] = {
"daarwizz": "DAARWIZZ_TELEGRAM_BOT_TOKEN",
"helion": "HELION_TELEGRAM_BOT_TOKEN",
"greenfood": "GREENFOOD_TELEGRAM_BOT_TOKEN",
"agromatrix": "AGROMATRIX_TELEGRAM_BOT_TOKEN",
"alateya": "ALATEYA_TELEGRAM_BOT_TOKEN",
"nutra": "NUTRA_TELEGRAM_BOT_TOKEN",
"druid": "DRUID_TELEGRAM_BOT_TOKEN",
"clan": "CLAN_TELEGRAM_BOT_TOKEN",
"eonarch": "EONARCH_TELEGRAM_BOT_TOKEN",
"senpai": "SENPAI_TELEGRAM_BOT_TOKEN",
"oneok": "ONEOK_TELEGRAM_BOT_TOKEN",
"soul": "SOUL_TELEGRAM_BOT_TOKEN",
"yaromir": "YAROMIR_TELEGRAM_BOT_TOKEN",
"sofiia": "SOFIIA_TELEGRAM_BOT_TOKEN",
}
def _token_for_agent(agent_id: str) -> str:
env = AGENT_TOKEN_ENV.get((agent_id or "").lower(), "")
return os.getenv(env, "") if env else ""
async def _send_reminder(item: Dict[str, str]) -> bool:
agent_id = str(item.get("agent_id", ""))
chat_id = str(item.get("chat_id", ""))
reminder_text = str(item.get("text", "")).strip()
due_at = str(item.get("due_at", ""))
token = _token_for_agent(agent_id)
if not token:
logger.warning("reminder_skip_no_token agent=%s reminder_id=%s", agent_id, item.get("reminder_id"))
return False
if not chat_id or not reminder_text:
logger.warning("reminder_skip_invalid_payload reminder_id=%s", item.get("reminder_id"))
return False
body = {
"chat_id": chat_id,
"text": f"⏰ Нагадування ({agent_id})\n\n{reminder_text}\n\n🕒 {due_at}",
}
url = f"https://api.telegram.org/bot{token}/sendMessage"
async with httpx.AsyncClient(timeout=TELEGRAM_TIMEOUT) as client:
resp = await client.post(url, json=body)
if resp.status_code != 200:
logger.warning(
"reminder_send_failed reminder_id=%s status=%s body=%s",
item.get("reminder_id"),
resp.status_code,
resp.text[:300],
)
return False
logger.info("reminder_sent reminder_id=%s agent=%s chat=%s", item.get("reminder_id"), agent_id, chat_id)
return True
async def worker_loop() -> None:
logger.info("reminder_worker_started poll_seconds=%s", POLL_SECONDS)
while True:
try:
items = await pop_due_reminders(limit=20)
if items:
for item in items:
try:
await _send_reminder(item)
except Exception:
logger.exception("reminder_send_exception reminder_id=%s", item.get("reminder_id"))
except asyncio.CancelledError:
raise
except Exception:
logger.exception("reminder_worker_cycle_failed")
await asyncio.sleep(POLL_SECONDS)
if __name__ == "__main__":
try:
asyncio.run(worker_loop())
finally:
try:
asyncio.run(close_redis())
except Exception:
pass

View File

@@ -0,0 +1,154 @@
import json
import os
import time
import uuid
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional
from redis.asyncio import Redis
REDIS_URL = os.getenv("REDIS_URL", "redis://redis:6379/0")
REMINDER_PREFIX = "daarion:reminders"
REMINDER_BY_ID = f"{REMINDER_PREFIX}:by_id"
REMINDER_SCHEDULE = f"{REMINDER_PREFIX}:schedule"
REMINDER_TTL_SECONDS = int(os.getenv("DAARION_REMINDER_TTL_SECONDS", str(30 * 24 * 3600)))
_redis: Optional[Redis] = None
@dataclass
class Reminder:
reminder_id: str
agent_id: str
chat_id: str
user_id: str
text: str
due_ts: int
created_at: str
def to_dict(self) -> Dict[str, Any]:
return {
"reminder_id": self.reminder_id,
"agent_id": self.agent_id,
"chat_id": self.chat_id,
"user_id": self.user_id,
"text": self.text,
"due_ts": self.due_ts,
"created_at": self.created_at,
}
async def redis_client() -> Redis:
global _redis
if _redis is None:
_redis = Redis.from_url(REDIS_URL, decode_responses=True)
return _redis
async def close_redis() -> None:
global _redis
if _redis is not None:
await _redis.close()
_redis = None
def _iso_now() -> str:
return datetime.now(timezone.utc).isoformat()
def _iso_from_ts(ts: int) -> str:
return datetime.fromtimestamp(ts, tz=timezone.utc).isoformat()
async def create_reminder(agent_id: str, chat_id: str, user_id: str, text: str, due_ts: int) -> Dict[str, Any]:
reminder = Reminder(
reminder_id=f"rem_{uuid.uuid4().hex[:16]}",
agent_id=agent_id,
chat_id=str(chat_id),
user_id=str(user_id),
text=text.strip(),
due_ts=int(due_ts),
created_at=_iso_now(),
)
r = await redis_client()
key = f"{REMINDER_BY_ID}:{reminder.reminder_id}"
payload = json.dumps(reminder.to_dict(), ensure_ascii=False)
await r.set(key, payload, ex=REMINDER_TTL_SECONDS)
await r.zadd(REMINDER_SCHEDULE, {reminder.reminder_id: float(reminder.due_ts)})
result = reminder.to_dict()
result["due_at"] = _iso_from_ts(reminder.due_ts)
return result
async def list_reminders(agent_id: str, chat_id: str, user_id: str, limit: int = 10) -> List[Dict[str, Any]]:
r = await redis_client()
now_ts = int(time.time())
ids = await r.zrangebyscore(REMINDER_SCHEDULE, min=now_ts - 365 * 24 * 3600, max="+inf", start=0, num=max(1, limit * 5))
out: List[Dict[str, Any]] = []
for reminder_id in ids:
raw = await r.get(f"{REMINDER_BY_ID}:{reminder_id}")
if not raw:
continue
try:
item = json.loads(raw)
except json.JSONDecodeError:
continue
if item.get("agent_id") != agent_id:
continue
if str(item.get("chat_id")) != str(chat_id):
continue
if str(item.get("user_id")) != str(user_id):
continue
item["due_at"] = _iso_from_ts(int(item.get("due_ts", 0)))
out.append(item)
if len(out) >= limit:
break
return out
async def cancel_reminder(reminder_id: str, agent_id: str, chat_id: str, user_id: str) -> bool:
r = await redis_client()
key = f"{REMINDER_BY_ID}:{reminder_id}"
raw = await r.get(key)
if not raw:
return False
try:
item = json.loads(raw)
except json.JSONDecodeError:
return False
if item.get("agent_id") != agent_id or str(item.get("chat_id")) != str(chat_id) or str(item.get("user_id")) != str(user_id):
return False
await r.delete(key)
await r.zrem(REMINDER_SCHEDULE, reminder_id)
return True
async def pop_due_reminders(limit: int = 20) -> List[Dict[str, Any]]:
r = await redis_client()
now_ts = int(time.time())
ids = await r.zrangebyscore(REMINDER_SCHEDULE, min="-inf", max=now_ts, start=0, num=max(1, limit))
out: List[Dict[str, Any]] = []
for reminder_id in ids:
removed = await r.zrem(REMINDER_SCHEDULE, reminder_id)
if removed == 0:
continue
raw = await r.get(f"{REMINDER_BY_ID}:{reminder_id}")
if not raw:
continue
await r.delete(f"{REMINDER_BY_ID}:{reminder_id}")
try:
item = json.loads(raw)
item["due_at"] = _iso_from_ts(int(item.get("due_ts", now_ts)))
out.append(item)
except json.JSONDecodeError:
continue
return out

View File

@@ -14,9 +14,10 @@ import uuid
import httpx
from pathlib import Path
from typing import Dict, Any, Optional, List, Tuple
from datetime import datetime
from datetime import datetime, timedelta, timezone
from dataclasses import dataclass
from io import BytesIO
from zoneinfo import ZoneInfo
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
@@ -46,6 +47,11 @@ from behavior_policy import (
BehaviorDecision,
AGENT_NAME_VARIANTS,
)
from daarion_facade.reminders import (
create_reminder,
list_reminders,
cancel_reminder,
)
logger = logging.getLogger(__name__)
@@ -242,6 +248,279 @@ def preferred_language_label(lang: str) -> str:
}.get((lang or "").lower(), "Ukrainian")
def _csv_set(value: Optional[str]) -> set[str]:
if not value:
return set()
return {x.strip() for x in str(value).split(",") if x.strip()}
MENTOR_PRIVATE_HANDLES = _csv_set(os.getenv("MENTOR_PRIVATE_HANDLES", "ivantytar,archenvis,olegarch88"))
MENTOR_PRIVATE_NAMES = _csv_set(os.getenv("MENTOR_PRIVATE_NAMES", "Іван Титар,Александр Вертій,Олег Ковальчук"))
MENTOR_DISCLOSURE_ALLOWED_USER_IDS = _csv_set(os.getenv("MENTOR_DISCLOSURE_ALLOWED_USER_IDS", ""))
GLOBAL_RELAY_ALLOWED_USER_IDS = _csv_set(os.getenv("GLOBAL_RELAY_ALLOWED_USER_IDS", ""))
REMINDER_DEFAULT_TZ = os.getenv("DAARION_REMINDER_DEFAULT_TZ", "Europe/Kyiv")
def _agent_env_prefix(agent_config: "AgentConfig") -> str:
env = str(agent_config.telegram_token_env or "").strip()
if env.endswith("_TELEGRAM_BOT_TOKEN"):
return env[: -len("_TELEGRAM_BOT_TOKEN")]
return str(agent_config.agent_id or "").upper()
def _is_disclosure_allowed(user_id: str) -> bool:
return str(user_id) in MENTOR_DISCLOSURE_ALLOWED_USER_IDS
def _redact_private_mentions(answer_text: str, user_id: str) -> str:
if not answer_text or _is_disclosure_allowed(user_id):
return answer_text
out = answer_text
for handle in MENTOR_PRIVATE_HANDLES:
out = re.sub(rf"@{re.escape(handle)}\b", "@mentor", out, flags=re.IGNORECASE)
for name in MENTOR_PRIVATE_NAMES:
out = re.sub(re.escape(name), "ментор", out, flags=re.IGNORECASE)
return out
def _soften_unfulfillable_promises(answer_text: str) -> str:
if not answer_text:
return answer_text
lower = answer_text.lower()
has_dm_promise = any(
phrase in lower
for phrase in (
"особисті повідомлення",
"в dm",
"direct message",
"personal message",
"напишу менторам",
"надішлю менторам",
)
)
has_reminder_promise = any(
phrase in lower
for phrase in (
"нагадаю",
"напомню",
"i will remind",
)
)
hints: List[str] = []
if has_dm_promise:
hints.append("Для реальної передачі повідомлення менторам використай `/relay_mentors <текст>`.")
if has_reminder_promise:
hints.append("Для реального нагадування використай `/remind` або `/remind_in`.")
if not hints:
return answer_text
if "✅ Виконано дію:" in answer_text:
return answer_text
return f"{answer_text}\n\n {' '.join(hints)}"
def _sanitize_agent_answer(answer_text: str, user_id: str) -> str:
text = _redact_private_mentions(answer_text, user_id)
text = _soften_unfulfillable_promises(text)
return text
def _parse_duration_to_seconds(raw: str) -> Optional[int]:
m = re.match(r"^\s*(\d+)\s*([mhd])\s*$", raw.lower())
if not m:
return None
value = int(m.group(1))
unit = m.group(2)
if unit == "m":
return value * 60
if unit == "h":
return value * 3600
return value * 86400
def _parse_remind_datetime(raw: str) -> Optional[datetime]:
raw = raw.strip()
patterns = [
"%Y-%m-%d %H:%M",
"%Y-%m-%d %H:%M:%S",
]
parsed = None
for pattern in patterns:
try:
parsed = datetime.strptime(raw, pattern)
break
except ValueError:
continue
if parsed is None:
return None
try:
tz = ZoneInfo(REMINDER_DEFAULT_TZ)
except Exception:
tz = timezone.utc
return parsed.replace(tzinfo=tz).astimezone(timezone.utc)
def _relay_allowed_for_user(prefix: str, user_id: str) -> bool:
agent_allow = _csv_set(os.getenv(f"{prefix}_RELAY_ALLOWED_USER_IDS", ""))
allowed = GLOBAL_RELAY_ALLOWED_USER_IDS | agent_allow
return str(user_id) in allowed if allowed else False
def _mentor_chat_ids(prefix: str) -> List[str]:
return [x for x in os.getenv(f"{prefix}_MENTOR_CHAT_IDS", "").split(",") if x.strip()]
async def _handle_action_commands(
*,
agent_config: "AgentConfig",
text: str,
chat_id: str,
user_id: str,
username: str,
dao_id: str,
telegram_token: str,
) -> Optional[Dict[str, Any]]:
t = (text or "").strip()
if not t.startswith("/"):
return None
prefix = _agent_env_prefix(agent_config)
if t.startswith("/remind_in "):
parts = t.split(maxsplit=2)
if len(parts) < 3:
await send_telegram_message(chat_id, "Формат: `/remind_in 2h текст`", telegram_token)
return {"ok": True, "handled": "remind_in_help"}
seconds = _parse_duration_to_seconds(parts[1])
if not seconds or seconds <= 0:
await send_telegram_message(chat_id, "Некоректна тривалість. Приклад: `30m`, `2h`, `1d`.", telegram_token)
return {"ok": True, "handled": "remind_in_invalid"}
due_ts = int(time.time()) + seconds
item = await create_reminder(
agent_id=agent_config.agent_id,
chat_id=chat_id,
user_id=user_id,
text=parts[2],
due_ts=due_ts,
)
await send_telegram_message(
chat_id,
f"✅ Виконано дію: нагадування створено\nID: `{item['reminder_id']}`\nЧас: `{item['due_at']}`",
telegram_token,
)
await memory_client.save_chat_turn(
agent_id=agent_config.agent_id,
team_id=dao_id,
user_id=f"tg:{user_id}",
message=text,
response=f"✅ reminder_created {item['reminder_id']}",
channel_id=chat_id,
scope="short_term",
save_agent_response=True,
agent_metadata={"action": "reminder_create", "reminder_id": item["reminder_id"]},
username=username,
)
return {"ok": True, "handled": "remind_in", "reminder_id": item["reminder_id"]}
if t.startswith("/remind "):
payload = t[len("/remind ") :].strip()
m = re.match(r"^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})(?:\s+)(.+)$", payload)
if not m:
await send_telegram_message(chat_id, "Формат: `/remind 2026-03-18 09:00 текст`", telegram_token)
return {"ok": True, "handled": "remind_help"}
due = _parse_remind_datetime(m.group(1))
if due is None:
await send_telegram_message(chat_id, "Не зміг розпізнати дату/час. Формат: `YYYY-MM-DD HH:MM`", telegram_token)
return {"ok": True, "handled": "remind_invalid_date"}
item = await create_reminder(
agent_id=agent_config.agent_id,
chat_id=chat_id,
user_id=user_id,
text=m.group(2),
due_ts=int(due.timestamp()),
)
await send_telegram_message(
chat_id,
f"✅ Виконано дію: нагадування створено\nID: `{item['reminder_id']}`\nЧас: `{item['due_at']}`",
telegram_token,
)
await memory_client.save_chat_turn(
agent_id=agent_config.agent_id,
team_id=dao_id,
user_id=f"tg:{user_id}",
message=text,
response=f"✅ reminder_created {item['reminder_id']}",
channel_id=chat_id,
scope="short_term",
save_agent_response=True,
agent_metadata={"action": "reminder_create", "reminder_id": item["reminder_id"]},
username=username,
)
return {"ok": True, "handled": "remind", "reminder_id": item["reminder_id"]}
if t == "/reminders":
items = await list_reminders(agent_id=agent_config.agent_id, chat_id=chat_id, user_id=user_id, limit=10)
if not items:
await send_telegram_message(chat_id, "Активних нагадувань не знайдено.", telegram_token)
return {"ok": True, "handled": "reminders_empty"}
rows = [f"- `{x['reminder_id']}` → `{x['due_at']}` :: {x['text'][:80]}" for x in items]
await send_telegram_message(chat_id, "🗓 Твої нагадування:\n" + "\n".join(rows), telegram_token)
return {"ok": True, "handled": "reminders_list", "count": len(items)}
if t.startswith("/cancel_reminder "):
reminder_id = t[len("/cancel_reminder ") :].strip()
ok = await cancel_reminder(reminder_id, agent_id=agent_config.agent_id, chat_id=chat_id, user_id=user_id)
await send_telegram_message(
chat_id,
f"{'' if ok else ''} {'Скасовано.' if ok else 'Не знайдено або немає доступу.'}",
telegram_token,
)
return {"ok": True, "handled": "cancel_reminder", "canceled": ok}
if t.startswith("/relay_mentors "):
if not _relay_allowed_for_user(prefix, user_id):
await send_telegram_message(chat_id, "⛔ Немає доступу до relay. Звернись до адміністратора.", telegram_token)
return {"ok": True, "handled": "relay_denied"}
recipients = _mentor_chat_ids(prefix)
if not recipients:
await send_telegram_message(chat_id, "⚠️ Не налаштовано mentor recipients.", telegram_token)
return {"ok": True, "handled": "relay_not_configured"}
payload = t[len("/relay_mentors ") :].strip()
if not payload:
await send_telegram_message(chat_id, "Формат: `/relay_mentors текст повідомлення`", telegram_token)
return {"ok": True, "handled": "relay_help"}
delivered = 0
body = (
f"📨 Relay from {agent_config.name}\n"
f"From: @{username or user_id} (tg:{user_id})\n"
f"Source chat: {chat_id}\n\n"
f"{payload}"
)
for rid in recipients:
try:
ok = await send_telegram_message(rid.strip(), body, telegram_token)
if ok:
delivered += 1
except Exception:
logger.exception("mentor_relay_send_failed recipient=%s", rid)
await send_telegram_message(chat_id, f"✅ Виконано дію: relay sent to {delivered}/{len(recipients)} mentor(s).", telegram_token)
await memory_client.save_chat_turn(
agent_id=agent_config.agent_id,
team_id=dao_id,
user_id=f"tg:{user_id}",
message=text,
response=f"✅ mentor_relay delivered={delivered}",
channel_id=chat_id,
scope="short_term",
save_agent_response=True,
agent_metadata={"action": "mentor_relay", "delivered": delivered, "requested": len(recipients)},
username=username,
)
return {"ok": True, "handled": "relay_mentors", "delivered": delivered, "requested": len(recipients)}
return None
def _extract_preferred_language_from_profile_fact(fact: Optional[Dict[str, Any]]) -> Optional[str]:
if not isinstance(fact, dict):
return None
@@ -2405,6 +2684,18 @@ async def handle_telegram_webhook(
text = caption
logger.info(f"{agent_config.name} Telegram message from {username} (tg:{user_id}) in chat {chat_id}: {text[:50]}")
command_result = await _handle_action_commands(
agent_config=agent_config,
text=text,
chat_id=chat_id,
user_id=user_id,
username=username,
dao_id=dao_id,
telegram_token=telegram_token,
)
if command_result is not None:
return command_result
mentioned_bots = extract_bot_mentions(text)
needs_complex_reasoning = requires_complex_reasoning(text)
@@ -2780,6 +3071,7 @@ async def handle_telegram_webhook(
force_detailed=force_detailed_reply,
needs_complex_reasoning=needs_complex_reasoning,
)
answer_text = _sanitize_agent_answer(answer_text, user_id=user_id)
# Skip Telegram sending for prober requests (chat_id=0)
if is_prober: