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

@@ -8,7 +8,9 @@ import json
import re
import logging
import os
import sys
import time
import uuid
import httpx
from pathlib import Path
from typing import Dict, Any, Optional, List, Tuple
@@ -31,9 +33,17 @@ from behavior_policy import (
should_respond,
analyze_message,
detect_media_question,
detect_explicit_request,
detect_url,
detect_agent_mention,
is_no_output_response,
record_interaction,
record_ack,
get_ack_text,
is_prober_request,
NO_OUTPUT,
BehaviorDecision,
AGENT_NAME_VARIANTS,
)
logger = logging.getLogger(__name__)
@@ -41,6 +51,56 @@ logger = logging.getLogger(__name__)
# Telegram message length limits
TELEGRAM_MAX_MESSAGE_LENGTH = 4096
TELEGRAM_SAFE_LENGTH = 3500 # Leave room for formatting
# Operator pending state cache (chat_id -> {ts, items})
LAST_PENDING_STATE: Dict[str, Dict[str, Any]] = {}
PENDING_STATE_TTL = 1800 # 30 minutes
def _pending_state_cleanup():
now = time.time()
expired = [cid for cid, rec in LAST_PENDING_STATE.items() if now - rec.get('ts', 0) > PENDING_STATE_TTL]
for cid in expired:
del LAST_PENDING_STATE[cid]
def _get_last_pending(chat_id: str) -> list | None:
_pending_state_cleanup()
rec = LAST_PENDING_STATE.get(str(chat_id))
if not rec:
return None
return rec.get('items')
def _set_last_pending(chat_id: str, items: list):
LAST_PENDING_STATE[str(chat_id)] = {"ts": time.time(), "items": items}
def _chunk_text(text: str, max_len: int = 4096):
if not text:
return [""]
chunks = []
current = []
current_len = 0
for line in text.split("\n"):
add_len = len(line) + (1 if current else 0)
if current_len + add_len <= max_len:
current.append(line)
current_len += add_len
continue
if current:
chunks.append("\n".join(current))
current = []
current_len = 0
while len(line) > max_len:
chunks.append(line[:max_len])
line = line[max_len:]
current.append(line)
current_len = len(line)
if current:
chunks.append("\n".join(current))
return chunks
# Training groups - agents respond to ALL messages without mention requirement
TRAINING_GROUP_IDS = {
@@ -230,6 +290,54 @@ EONARCH_CONFIG = load_agent_config(
default_prompt="Ти — EONARCH, провідник еволюції свідомості в екосистемі DAARION.city. Супроводжуєш людство на шляху трансформації свідомості до колективної мудрості.",
)
# SENPAI (Gordon Senpai) Configuration
SENPAI_CONFIG = load_agent_config(
agent_id="senpai",
name=os.getenv("SENPAI_NAME", "SENPAI"),
prompt_path=os.getenv(
"SENPAI_PROMPT_PATH",
str(Path(__file__).parent / "senpai_prompt.txt"),
),
telegram_token_env="SENPAI_TELEGRAM_BOT_TOKEN",
default_prompt="Ти — Гордон Сенпай (Gordon Senpai), радник з ринків капіталу та цифрових активів. Допомагаєш з трейдингом, ризик-менеджментом, аналізом ринків.",
)
# SOUL / Athena Configuration
SOUL_CONFIG = load_agent_config(
agent_id="soul",
name=os.getenv("SOUL_NAME", "Athena"),
prompt_path=os.getenv(
"SOUL_PROMPT_PATH",
str(Path(__file__).parent / "soul_prompt.txt"),
),
telegram_token_env="SOUL_TELEGRAM_BOT_TOKEN",
default_prompt="Ти — Athena, духовний гід та ментор спільноти DAARION.city. Підтримуєш місію, цінності та зв\'язки між учасниками.",
)
# YAROMIR Configuration
YAROMIR_CONFIG = load_agent_config(
agent_id="yaromir",
name=os.getenv("YAROMIR_NAME", "Yaromir"),
prompt_path=os.getenv(
"YAROMIR_PROMPT_PATH",
str(Path(__file__).parent / "yaromir_prompt.txt"),
),
telegram_token_env="YAROMIR_TELEGRAM_BOT_TOKEN",
default_prompt="Ти — Yaromir, стратег та наставник в екосистемі DAARION.city. Стратегія, наставництво, психологічна підтримка команди.",
)
# SOFIIA (Sophia) Configuration
SOFIIA_CONFIG = load_agent_config(
agent_id="sofiia",
name=os.getenv("SOFIIA_NAME", "Sophia"),
prompt_path=os.getenv(
"SOFIIA_PROMPT_PATH",
str(Path(__file__).parent / "sofiia_prompt.txt"),
),
telegram_token_env="SOFIIA_TELEGRAM_BOT_TOKEN",
default_prompt="Ти — Sophia (Софія), Chief AI Architect та Technical Sovereign екосистеми DAARION.city. Координуєш R&D, архітектуру, безпеку та еволюцію платформи.",
)
# Registry of all agents (для легкого додавання нових агентів)
AGENT_REGISTRY: Dict[str, AgentConfig] = {
"daarwizz": DAARWIZZ_CONFIG,
@@ -241,6 +349,10 @@ AGENT_REGISTRY: Dict[str, AgentConfig] = {
"druid": DRUID_CONFIG,
"clan": CLAN_CONFIG,
"eonarch": EONARCH_CONFIG,
"senpai": SENPAI_CONFIG,
"soul": SOUL_CONFIG,
"yaromir": YAROMIR_CONFIG,
"sofiia": SOFIIA_CONFIG,
}
# 3. Створіть endpoint (опціонально, якщо потрібен окремий webhook):
# @router.post("/new_agent/telegram/webhook")
@@ -284,8 +396,97 @@ async def druid_telegram_webhook(update: TelegramUpdate):
# AGROMATRIX webhook endpoint
async def handle_stepan_message(update: TelegramUpdate, agent_config: AgentConfig) -> Dict[str, Any]:
update_id = getattr(update, 'update_id', None) or update.update_id
if update_id:
if update_id in _PROCESSED_UPDATES:
return {"ok": True, "status": "duplicate"}
_PROCESSED_UPDATES[update_id] = _time.time()
if len(_PROCESSED_UPDATES) > _DEDUP_MAX_SIZE:
_dedup_cleanup()
message = update.message or update.channel_post or {}
text = message.get('text') or message.get('caption') or ''
if not text:
return {"ok": True, "status": "no_text"}
user = message.get('from', {}) or {}
chat = message.get('chat', {}) or {}
user_id = str(user.get('id', ''))
chat_id = str(chat.get('id', ''))
# ops mode if operator
ops_mode = False
op_ids = [s.strip() for s in os.getenv('AGX_OPERATOR_IDS', '').split(',') if s.strip()]
op_chat = os.getenv('AGX_OPERATOR_CHAT_ID', '').strip()
if op_chat and chat_id == op_chat:
ops_mode = True
if user_id and user_id in op_ids:
ops_mode = True
trace_id = str(uuid.uuid4())
# call Stepan directly
try:
sys.path.insert(0, str(Path('/opt/microdao-daarion')))
from crews.agromatrix_crew.run import handle_message
started = time.time()
last_pending = _get_last_pending(chat_id)
response_text = await asyncio.wait_for(
asyncio.to_thread(handle_message, text, user_id, chat_id, trace_id, ops_mode, last_pending),
timeout=25
)
duration_ms = int((time.time() - started) * 1000)
except Exception as e:
logger.error(f"Stepan handler error: {e}; trace_id={trace_id}")
response_text = f"Помилка обробки. trace_id={trace_id}"
duration_ms = 0
# If JSON, try to show summary
try:
parsed = json.loads(response_text)
summary = parsed.get('summary')
if summary:
response_text = summary
if parsed.get('details'):
response_text += "\n(details truncated)"
except Exception:
pass
# chunk and send
bot_token = agent_config.get_telegram_token()
for chunk in _chunk_text(response_text, max_len=4096):
await send_telegram_message(chat_id, chunk, bot_token=bot_token)
logger.info(f"Stepan reply sent: trace_id={trace_id}, user_id={user_id}, chat_id={chat_id}, update_id={update_id}, duration_ms={duration_ms}")
return {"ok": True}
@router.post("/agromatrix/telegram/webhook")
async def agromatrix_telegram_webhook(update: TelegramUpdate):
# Check if this is an operator request (slash command or NL operator intent)
message = (update.message or update.channel_post or {})
msg_text = message.get('text') or message.get('caption') or ''
user = message.get('from', {}) or {}
chat = message.get('chat', {}) or {}
user_id = str(user.get('id', ''))
chat_id = str(chat.get('id', ''))
is_slash = msg_text.strip().startswith('/')
is_ops = False
op_ids = [s.strip() for s in os.getenv('AGX_OPERATOR_IDS', '').split(',') if s.strip()]
op_chat = os.getenv('AGX_OPERATOR_CHAT_ID', '').strip()
if op_chat and chat_id == op_chat:
is_ops = True
if user_id and user_id in op_ids:
is_ops = True
# Operator NL or slash commands -> handle via Stepan handler
if is_slash or is_ops:
return await handle_stepan_message(update, AGROMATRIX_CONFIG)
# General conversation -> standard Router pipeline (like all other agents)
return await handle_telegram_webhook(AGROMATRIX_CONFIG, update)
@@ -307,6 +508,30 @@ async def eonarch_telegram_webhook(update: TelegramUpdate):
return await handle_telegram_webhook(EONARCH_CONFIG, update)
# SENPAI (Gordon Senpai) webhook endpoint
@router.post("/senpai/telegram/webhook")
async def senpai_telegram_webhook(update: TelegramUpdate):
return await handle_telegram_webhook(SENPAI_CONFIG, update)
# SOUL / Athena webhook endpoint
@router.post("/soul/telegram/webhook")
async def soul_telegram_webhook(update: TelegramUpdate):
return await handle_telegram_webhook(SOUL_CONFIG, update)
# YAROMIR webhook endpoint
@router.post("/yaromir/telegram/webhook")
async def yaromir_telegram_webhook(update: TelegramUpdate):
return await handle_telegram_webhook(YAROMIR_CONFIG, update)
# SOFIIA (Sophia) webhook endpoint
@router.post("/sofiia/telegram/webhook")
async def sofiia_telegram_webhook(update: TelegramUpdate):
return await handle_telegram_webhook(SOFIIA_CONFIG, update)
class DiscordMessage(BaseModel):
"""Simplified Discord message model"""
content: Optional[str] = None
@@ -319,16 +544,32 @@ class DiscordMessage(BaseModel):
# DAO Mapping (temporary)
# ========================================
# Map chat/channel ID to DAO ID
# TODO: Move to database or config
# Map agent_id to DAO ID
AGENT_TO_DAO = {
"helion": "helion-dao",
"greenfood": "greenfood-dao",
"agromatrix": "agromatrix-dao",
"nutra": "nutra-dao",
"druid": "druid-dao",
"daarwizz": "daarwizz-dao",
"clan": "clan-dao",
"alateya": "alateya-dao",
"eonarch": "eonarch-dao",
"senpai": "senpai-dao",
"soul": "soul-dao",
"yaromir": "yaromir-dao",
}
# Legacy: Map chat/channel ID to DAO ID
CHAT_TO_DAO = {
"default": "greenfood-dao",
# Add mappings: "telegram:12345": "specific-dao",
"default": "daarion-dao",
}
def get_dao_id(chat_id: str, source: str) -> str:
"""Get DAO ID from chat ID"""
def get_dao_id(chat_id: str, source: str, agent_id: str = None) -> str:
"""Get DAO ID from agent_id or chat ID"""
if agent_id and agent_id in AGENT_TO_DAO:
return AGENT_TO_DAO[agent_id]
key = f"{source}:{chat_id}"
return CHAT_TO_DAO.get(key, CHAT_TO_DAO["default"])
@@ -419,23 +660,20 @@ def store_response_cache(agent_id: str, chat_id: str, text: str, answer: str) ->
def _resolve_stt_upload_url() -> str:
"""
Повертає фінальний endpoint для STT upload, враховуючи налаштування.
Дозволяє передати або базовий URL сервісу, або повний шлях до /api/stt/upload.
Повертає фінальний endpoint для STT.
Swapper service використовує POST /stt з multipart file upload.
"""
upload_override = os.getenv("STT_SERVICE_UPLOAD_URL")
if upload_override:
return upload_override.rstrip("/")
base_url = os.getenv("STT_SERVICE_URL", "http://172.21.0.19:8895").rstrip("/")
base_url = os.getenv("STT_SERVICE_URL", "http://swapper-service:8890").rstrip("/")
if base_url.endswith("/api/stt/upload"):
# Swapper endpoint is /stt (not /api/stt/upload)
if base_url.endswith("/stt"):
return base_url
if base_url.endswith("/api/stt"):
return f"{base_url}/upload"
if base_url.endswith("/api"):
return f"{base_url}/stt/upload"
return f"{base_url}/api/stt/upload"
return f"{base_url}/stt"
# ========================================
@@ -613,6 +851,7 @@ async def process_photo(
"file_id": file_id,
"caption": caption,
},
username=username,
)
return {"ok": True, "skipped": True, "reason": "media_no_question"}
@@ -704,6 +943,7 @@ async def process_photo(
scope="short_term",
save_agent_response=not is_service_response(answer_text),
agent_metadata={"context": "photo"},
username=username,
)
return {"ok": True, "agent": agent_config.agent_id, "model": "specialist_vision_8b"}
@@ -858,6 +1098,52 @@ async def process_document(
answer_text = f"📄 Отримав документ **{file_name}**, але не вдалося прочитати текст. Можливо, це скановане зображення?"
logger.info(f"{agent_config.name}: Document processed: {file_name}, doc_id={result.doc_id}")
# === SAVE TO CHAT HISTORY (CRITICAL: so agent remembers the document) ===
user_msg = f"[Документ: {file_name}] Надіслано документ"
if update.message.get("caption"):
user_msg = f"[Документ: {file_name}] {update.message.get('caption')}"
await memory_client.save_chat_turn(
agent_id=agent_config.agent_id,
team_id=dao_id,
user_id=f"tg:{user_id}",
message=user_msg,
response=answer_text,
channel_id=chat_id,
scope="short_term",
save_agent_response=True,
agent_metadata={"context": "document", "file_name": file_name, "doc_id": result.doc_id},
username=username,
)
logger.info(f"{agent_config.name}: Document chat turn saved to memory: {file_name}")
# === END SAVE TO CHAT HISTORY ===
# === AUTO-INGEST: Store document in agent Qdrant _docs collection ===
if doc_text:
try:
import httpx as _httpx
router_url = os.getenv("ROUTER_URL", "http://router:8000")
async with _httpx.AsyncClient(timeout=60.0) as _client:
ingest_resp = await _client.post(
f"{router_url}/v1/documents/ingest",
json={
"agent_id": agent_config.agent_id,
"doc_id": result.doc_id,
"file_name": file_name,
"text": doc_text,
"dao_id": dao_id,
"user_id": f"tg:{user_id}"
}
)
ingest_data = ingest_resp.json()
if ingest_data.get("ok"):
logger.info(f"{agent_config.name}: Document ingested to Qdrant: {ingest_data.get('chunks_stored', 0)} chunks")
else:
logger.warning(f"{agent_config.name}: Document ingest failed: {ingest_data.get('error')}")
except Exception as ingest_err:
logger.warning(f"{agent_config.name}: Document auto-ingest error: {ingest_err}")
# === END AUTO-INGEST ===
await send_telegram_message(chat_id, answer_text, telegram_token)
return {"ok": True, "agent": "parser", "mode": "doc_parse", "doc_id": result.doc_id}
@@ -937,10 +1223,15 @@ async def process_voice(
mime_type or "audio/ogg",
)
}
# Swapper /stt expects: file (multipart), model (form), language (form)
form_data = {
"model": "whisper-small",
"task": "transcribe",
}
logger.info(f"{agent_config.name}: Sending voice to STT endpoint {stt_upload_url}")
async with httpx.AsyncClient(timeout=90.0) as client:
stt_resp = await client.post(stt_upload_url, files=files)
stt_resp = await client.post(stt_upload_url, files=files, data=form_data)
stt_resp.raise_for_status()
stt_data = stt_resp.json()
text = stt_data.get("text", "")
@@ -973,6 +1264,21 @@ async def process_voice(
# Universal Telegram Webhook Handler
# ========================================
# === UPDATE DEDUPLICATION ===
import time as _time
_PROCESSED_UPDATES: Dict[int, float] = {} # update_id -> timestamp
_DEDUP_MAX_SIZE = 2000
_DEDUP_TTL = 300 # 5 minutes
def _dedup_cleanup():
"""Remove old entries from dedup cache."""
now = _time.time()
expired = [uid for uid, ts in _PROCESSED_UPDATES.items() if now - ts > _DEDUP_TTL]
for uid in expired:
del _PROCESSED_UPDATES[uid]
# === END DEDUPLICATION ===
async def handle_telegram_webhook(
agent_config: AgentConfig,
update: TelegramUpdate
@@ -989,6 +1295,16 @@ async def handle_telegram_webhook(
"""
# Allow updates without message if they contain photo/voice
# The actual message validation happens after multimodal checks
# === DEDUP CHECK ===
if update.update_id:
if update.update_id in _PROCESSED_UPDATES:
logger.info(f"🔄 Skipping duplicate update_id={update.update_id} for {agent_config.name}")
return {"status": "ok", "skipped": "duplicate_update"}
_PROCESSED_UPDATES[update.update_id] = _time.time()
if len(_PROCESSED_UPDATES) > _DEDUP_MAX_SIZE:
_dedup_cleanup()
# === END DEDUP CHECK ===
if not update.message:
if update.channel_post:
update.message = update.channel_post
@@ -1009,7 +1325,7 @@ async def handle_telegram_webhook(
is_sender_bot = bool(from_user.get("is_bot") or (username and username.lower().endswith("bot")))
# Get DAO ID for this chat
dao_id = get_dao_id(chat_id, "telegram")
dao_id = get_dao_id(chat_id, "telegram", agent_id=agent_config.agent_id)
# Оновлюємо факти про користувача/агента для побудови графу пам'яті
asyncio.create_task(
@@ -1637,6 +1953,7 @@ async def handle_telegram_webhook(
"mentioned_bots": mentioned_bots,
"requires_complex_reasoning": needs_complex_reasoning,
},
username=username,
)
return {"ok": True, "agent": agent_config.agent_id, "cached": True}
@@ -1674,26 +1991,56 @@ async def handle_telegram_webhook(
# Fall through to regular chat if RAG query fails
# ========================================
# BEHAVIOR POLICY v1: Check if should respond
# BEHAVIOR POLICY v2.1: Check if should respond
# Gateway computes has_link and has_explicit_request (source of truth)
# ========================================
chat_type = chat.get("type", "private")
is_private_chat = chat_type == "private"
# Check if message has media (photo already handled above, check for links)
has_link = bool(re.search(r'https?://\S+', text)) if text else False
# Gateway: compute has_link (single source of truth)
has_link = detect_url(text) if text else False
respond_decision, respond_reason = should_respond(
# Gateway: detect mentioned agents
mentioned_agents = []
if text:
for aid, variants in AGENT_NAME_VARIANTS.items():
for v in variants:
if v.lower() in text.lower():
mentioned_agents.append(aid)
break
# Gateway: compute has_explicit_request (single source of truth)
# CONTRACT: imperative OR (? AND (dm OR reply OR mention OR thread))
has_explicit_request = detect_explicit_request(
text=text,
is_dm=is_private_chat,
is_reply_to_agent=False, # TODO: detect from Telegram reply_to_message
mentioned_agents=mentioned_agents,
thread_has_agent_participation=False, # REQUIRED, fail-closed default
)
# Check if this is a prober request (chat_id=0 or user_id=0)
is_prober = is_prober_request(chat_id, user_id)
# SOWA v2.2: 3-level decision (FULL / ACK / SILENT)
sowa_decision = analyze_message(
text=text,
agent_id=agent_config.agent_id,
chat_id=chat_id,
has_media=has_link, # Links treated as media
user_id=str(user_id),
has_media=has_link,
media_caption=text if has_link else "",
is_private_chat=is_private_chat,
payload_explicit_request=False,
payload_explicit_request=has_explicit_request,
payload_has_link=has_link,
is_reply_to_agent=False, # TODO: detect from Telegram reply_to_message
thread_has_agent_participation=False, # TODO: track per thread
)
respond_decision = sowa_decision.should_respond
respond_reason = sowa_decision.reason
if not respond_decision:
logger.info(f"🔇 SOWA: Agent {agent_config.agent_id} NOT responding. Reason: {respond_reason}")
if sowa_decision.action == "SILENT":
logger.info(f"\U0001f507 SOWA: Agent {agent_config.agent_id} NOT responding. Reason: {respond_reason}")
# Save to memory for context tracking, but don't respond
await memory_client.save_chat_turn(
agent_id=agent_config.agent_id,
@@ -1708,10 +2055,61 @@ async def handle_telegram_webhook(
"sowa_skipped": True,
"skip_reason": respond_reason,
},
username=username,
)
return {"ok": True, "skipped": True, "reason": respond_reason}
logger.info(f"✅ SOWA: Agent {agent_config.agent_id} WILL respond. Reason: {respond_reason}")
# ACK: send short presence message WITHOUT calling LLM/Router
if sowa_decision.action == "ACK":
ack_text = sowa_decision.ack_text or get_ack_text(agent_config.agent_id)
logger.info(f"\U0001f44b SOWA ACK: Agent {agent_config.agent_id} sending ACK. Reason: {respond_reason}")
# Send ACK to Telegram (no LLM call)
if not is_prober:
token = agent_config.get_telegram_token()
if token:
try:
url = f"https://api.telegram.org/bot{token}/sendMessage"
async with httpx.AsyncClient(timeout=30) as client:
resp = await client.post(url, json={
"chat_id": chat_id,
"text": ack_text,
})
if resp.status_code == 200:
logger.info(f"\U0001f44b ACK sent to chat {chat_id}: {ack_text}")
else:
logger.warning(f"ACK send failed: {resp.status_code} {resp.text[:200]}")
except Exception as e:
logger.error(f"ACK send error: {e}")
# Record ACK for cooldown and interaction tracking
record_ack(agent_config.agent_id, str(chat_id))
record_interaction(agent_config.agent_id, str(chat_id), str(user_id))
# Save to memory
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=ack_text,
channel_id=chat_id,
scope="short_term",
save_agent_response=True,
agent_metadata={
"sowa_ack": True,
"ack_reason": respond_reason,
},
username=username,
)
return {"ok": True, "ack": True, "reason": respond_reason}
# FULL: proceed with LLM/Router call
# For prober requests, respond but don't send to Telegram
if is_prober:
logger.info(f"\U0001f9ea PROBER: Agent {agent_config.agent_id} responding to prober request. Reason: {respond_reason}")
else:
logger.info(f"\u2705 SOWA: Agent {agent_config.agent_id} WILL respond (FULL). Reason: {respond_reason}")
# Regular chat mode
# Fetch memory context (includes local context as fallback)
@@ -1743,11 +2141,11 @@ async def handle_telegram_webhook(
# Build request to Router
system_prompt = agent_config.system_prompt
logger.info(f"📝 Helion system_prompt length: {len(system_prompt) if system_prompt else 0} chars")
logger.info(f"📝 {agent_config.name} system_prompt length: {len(system_prompt) if system_prompt else 0} chars")
if system_prompt:
logger.debug(f"System prompt preview: {system_prompt[:200]}...")
else:
logger.error(f"Helion system_prompt is EMPTY or None!")
logger.error(f"{agent_config.name} system_prompt is EMPTY or None!")
router_request = {
"message": message_with_context,
@@ -1776,8 +2174,10 @@ async def handle_telegram_webhook(
}
if should_force_concise_reply(text):
# IMPORTANT: preserve conversation context! Only append concise instruction
router_request["message"] = (
f"{text}\n\n(Інструкція: дай максимально коротку відповідь, якщо не просили деталей "
router_request["message"]
+ "\n\n(Інструкція: дай максимально коротку відповідь, якщо не просили деталей "
"і дочекайся додаткового питання.)"
)
@@ -1805,6 +2205,33 @@ async def handle_telegram_webhook(
# Check for NO_OUTPUT (LLM decided not to respond)
if is_no_output_response(answer_text):
logger.info(f"🔇 NO_OUTPUT: Agent {agent_config.agent_id} returned empty/NO_OUTPUT. Not sending to Telegram.")
# P4: Detect NO_OUTPUT contract violations (extra text after NO_OUTPUT marker)
_stripped = (answer_text or "").strip()
_has_extra = False
if _stripped and "__NO_OUTPUT__" in _stripped:
_after_marker = _stripped.split("__NO_OUTPUT__", 1)[-1].strip()
if _after_marker:
_has_extra = True
logger.warning(
f"🚨 policy_violation=no_output_extra_text "
f"agent={agent_config.agent_id} "
f"chat_id={chat_id} "
f"extra_text_len={len(_after_marker)} "
f"extra_preview={_after_marker[:80]!r}"
)
elif _stripped and _stripped.lower() not in ("", "no_output", "no output", "silent", "мовчу", "", ".", "..", "..."):
# LLM returned something that looks like NO_OUTPUT but has unexpected content
if len(_stripped) > 10:
_has_extra = True
logger.warning(
f"🚨 policy_violation=ambiguous_no_output "
f"agent={agent_config.agent_id} "
f"chat_id={chat_id} "
f"response_len={len(_stripped)} "
f"response_preview={_stripped[:80]!r}"
)
# Save to memory for context tracking
await memory_client.save_chat_turn(
agent_id=agent_config.agent_id,
@@ -1818,7 +2245,9 @@ async def handle_telegram_webhook(
agent_metadata={
"no_output": True,
"original_response": answer_text[:100] if answer_text else "",
"policy_violation": "no_output_extra_text" if _has_extra else None,
},
username=username,
)
return {"ok": True, "skipped": True, "reason": "no_output_from_llm"}
@@ -1826,6 +2255,11 @@ async def handle_telegram_webhook(
if len(answer_text) > TELEGRAM_SAFE_LENGTH:
answer_text = answer_text[:TELEGRAM_SAFE_LENGTH] + "\n\n_... (відповідь обрізано)_"
# Skip Telegram sending for prober requests (chat_id=0)
if is_prober:
logger.info(f"🧪 PROBER: Skipping Telegram send for prober request. Response: {answer_text[:100]}...")
return {"ok": True, "agent": agent_config.agent_id, "prober": True, "response_preview": answer_text[:100]}
# Send image if generated
if image_base64:
try:
@@ -1850,6 +2284,9 @@ async def handle_telegram_webhook(
# Send text response only
await send_telegram_message(chat_id, answer_text, telegram_token)
# Record successful interaction for conversation context
record_interaction(agent_config.agent_id, chat_id, str(user_id))
await memory_client.save_chat_turn(
agent_id=agent_config.agent_id,
team_id=dao_id,
@@ -1863,6 +2300,7 @@ async def handle_telegram_webhook(
"mentioned_bots": mentioned_bots,
"requires_complex_reasoning": needs_complex_reasoning,
},
username=username,
)
store_response_cache(agent_config.agent_id, chat_id, text, answer_text)
@@ -1880,6 +2318,15 @@ async def handle_telegram_webhook(
# ========================================
# DAARWIZZ webhook endpoints (both paths for compatibility)
@router.get("/healthz")
async def healthz():
try:
from crews.agromatrix_crew.run import handle_message # noqa: F401
return {"ok": True, "status": "healthy"}
except Exception as e:
return {"ok": False, "status": "error", "error": str(e)}
@router.post("/telegram/webhook")
async def telegram_webhook(update: TelegramUpdate):
"""Handle Telegram webhook for DAARWIZZ agent (default path)."""
@@ -1916,7 +2363,7 @@ async def _old_telegram_webhook(update: TelegramUpdate):
username = from_user.get("username", "")
# Get DAO ID for this chat
dao_id = get_dao_id(chat_id, "telegram")
dao_id = get_dao_id(chat_id, "telegram", agent_id=agent_config.agent_id)
# Check for /ingest command
text = update.message.get("text", "")
@@ -2133,6 +2580,7 @@ async def _old_telegram_webhook(update: TelegramUpdate):
scope="short_term",
save_agent_response=not is_service_response(answer_text),
agent_metadata={"context": "photo"},
username=username,
)
return {"ok": True, "agent": "daarwizz", "model": "specialist_vision_8b"}
@@ -2261,7 +2709,7 @@ async def _old_telegram_webhook(update: TelegramUpdate):
agent_id="daarwizz",
team_id=dao_id,
channel_id=chat_id,
limit=10
limit=80
)
# Build request to Router with DAARWIZZ context
@@ -2308,6 +2756,7 @@ async def _old_telegram_webhook(update: TelegramUpdate):
scope="short_term",
save_agent_response=not is_service_response(answer_text),
agent_metadata={"context": "legacy_daarwizz"},
username=username,
)
# Send response back to Telegram
@@ -2357,7 +2806,7 @@ async def discord_webhook(message: DiscordMessage):
agent_id="daarwizz",
team_id=dao_id,
channel_id=channel_id,
limit=10
limit=80
)
# Build request to Router with DAARWIZZ context
@@ -2403,6 +2852,7 @@ async def discord_webhook(message: DiscordMessage):
scope="short_term",
save_agent_response=not is_service_response(answer_text),
agent_metadata={"source": "discord"},
username=username,
)
# TODO: Send response back to Discord
@@ -2841,7 +3291,7 @@ async def _old_helion_telegram_webhook(update: TelegramUpdate):
username = from_user.get("username", "")
# Get DAO ID for this chat (Energy Union specific)
dao_id = get_dao_id(chat_id, "telegram")
dao_id = get_dao_id(chat_id, "telegram", agent_id=agent_config.agent_id)
# Check for /ingest command
text = update.message.get("text", "")
@@ -3054,6 +3504,7 @@ async def _old_helion_telegram_webhook(update: TelegramUpdate):
scope="short_term",
save_agent_response=not is_service_response(answer_text),
agent_metadata={"context": "photo"},
username=username,
)
return {"ok": True, "agent": "helion", "model": "specialist_vision_8b"}
@@ -3117,12 +3568,13 @@ async def _old_helion_telegram_webhook(update: TelegramUpdate):
# Regular chat mode
# Fetch memory context (includes local context as fallback)
# All agents use limit=80 for full conversation history
memory_context = await memory_client.get_context(
user_id=f"tg:{user_id}",
agent_id="helion",
team_id=dao_id,
channel_id=chat_id,
limit=10
limit=80
)
# Build message with conversation context
@@ -3190,6 +3642,7 @@ async def _old_helion_telegram_webhook(update: TelegramUpdate):
"mentioned_bots": mentioned_bots,
"requires_complex_reasoning": needs_complex_reasoning,
},
username=username,
)
# Send response back to Telegram