gateway: add privacy guard plus reminders and mentor relay commands
This commit is contained in:
100
gateway-bot/daarion_facade/reminder_worker.py
Normal file
100
gateway-bot/daarion_facade/reminder_worker.py
Normal 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
|
||||
154
gateway-bot/daarion_facade/reminders.py
Normal file
154
gateway-bot/daarion_facade/reminders.py
Normal 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
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user