""" DAGI Router Client Sends requests to DAGI Router from Bot Gateway """ import logging import os import time import httpx from typing import Dict, Any logger = logging.getLogger(__name__) # Import metrics try: from metrics import ROUTER_CALLS_TOTAL, ROUTER_LATENCY, ERRORS_TOTAL METRICS_AVAILABLE = True except ImportError: METRICS_AVAILABLE = False # Router configuration from environment ROUTER_BASE_URL = os.getenv("ROUTER_URL", "http://127.0.0.1:9102") ROUTER_TIMEOUT = float(os.getenv("ROUTER_TIMEOUT", "180.0")) GATEWAY_MAX_TOKENS_DEFAULT = int(os.getenv("GATEWAY_MAX_TOKENS_DEFAULT", "700")) GATEWAY_MAX_TOKENS_CONCISE = int(os.getenv("GATEWAY_MAX_TOKENS_CONCISE", "220")) GATEWAY_MAX_TOKENS_TRAINING = int(os.getenv("GATEWAY_MAX_TOKENS_TRAINING", "900")) GATEWAY_TEMPERATURE_DEFAULT = float(os.getenv("GATEWAY_TEMPERATURE_DEFAULT", "0.4")) GATEWAY_MAX_TOKENS_SENPAI_DEFAULT = int(os.getenv("GATEWAY_MAX_TOKENS_SENPAI_DEFAULT", "320")) GATEWAY_MAX_TOKENS_HELION_DEFAULT = int(os.getenv("GATEWAY_MAX_TOKENS_HELION_DEFAULT", "240")) GATEWAY_MAX_TOKENS_DETAILED = int(os.getenv("GATEWAY_MAX_TOKENS_DETAILED", "900")) def _apply_runtime_communication_guardrails(system_prompt: str, metadata: Dict[str, Any]) -> str: """Apply global communication constraints for all agents in Telegram flows.""" if not system_prompt: return system_prompt lang_label = (metadata or {}).get("preferred_response_language_label") or "user language" guardrail = ( "\n\n[GLOBAL COMMUNICATION POLICY]\n" "1) Do not introduce yourself by name in every message.\n" "2) Do not add repetitive generic closers like 'how can I help' unless user explicitly asks.\n" "3) Continue the dialog naturally from context.\n" f"4) Respond in {lang_label}, matching the user's latest language.\n" ) return system_prompt + guardrail def _apply_agent_style_guardrails(agent_id: str, system_prompt: str) -> str: """Apply lightweight runtime style constraints for specific agents.""" if not system_prompt: return system_prompt if agent_id == "nutra": nutra_guardrail = ( "\n\n[STYLE LOCK - NUTRA]\n" "Always write in first-person singular and feminine form.\n" "Use feminine wording in Ukrainian/Russian (e.g., 'я підготувала', 'я готова', " "'я зрозуміла').\n" "Never switch to masculine forms (e.g., 'понял', 'готов').\n" ) return system_prompt + nutra_guardrail return system_prompt async def send_to_router(body: Dict[str, Any]) -> Dict[str, Any]: """Send request to DAGI Router.""" _start = time.time() agent_id = body.get("agent", "devtools") message = body.get("message", "") metadata = body.get("metadata", {}) context = body.get("context", {}) system_prompt = body.get("system_prompt") or context.get("system_prompt") system_prompt = _apply_agent_style_guardrails(agent_id, system_prompt) system_prompt = _apply_runtime_communication_guardrails(system_prompt, metadata) if system_prompt: logger.info(f"Using system prompt ({len(system_prompt)} chars) for agent {agent_id}") infer_url = f"{ROUTER_BASE_URL}/v1/agents/{agent_id}/infer" metadata["agent_id"] = agent_id # Keep defaults moderate to avoid overly long replies while preserving flexibility. max_tokens = GATEWAY_MAX_TOKENS_DEFAULT # Senpai tends to over-verbose responses in Telegram; use lower default unless user asked details. if agent_id == "senpai": max_tokens = GATEWAY_MAX_TOKENS_SENPAI_DEFAULT elif agent_id == "helion": max_tokens = min(max_tokens, GATEWAY_MAX_TOKENS_HELION_DEFAULT) if metadata.get("is_training_group"): max_tokens = GATEWAY_MAX_TOKENS_TRAINING if metadata.get("force_detailed"): max_tokens = max(max_tokens, GATEWAY_MAX_TOKENS_DETAILED) if metadata.get("force_concise"): max_tokens = min(max_tokens, GATEWAY_MAX_TOKENS_CONCISE) infer_body = { "prompt": message, "system_prompt": system_prompt, "metadata": metadata, "max_tokens": max_tokens, "temperature": float(metadata.get("temperature_override", GATEWAY_TEMPERATURE_DEFAULT)), } images = context.get("images", []) if images: infer_body["images"] = images logger.info(f"Including {len(images)} image(s) in request") if metadata.get("provider"): infer_body["provider_override"] = metadata["provider"] prov = metadata.get("provider", "default") logger.info( f"Sending to Router ({infer_url}): agent={agent_id}, provider={prov}, " f"has_images={bool(images)}, prompt_len={len(message)}, max_tokens={max_tokens}" ) try: async with httpx.AsyncClient(timeout=ROUTER_TIMEOUT) as client: response = await client.post(infer_url, json=infer_body) response.raise_for_status() result = response.json() latency = time.time() - _start if METRICS_AVAILABLE: ROUTER_CALLS_TOTAL.labels(status="success").inc() ROUTER_LATENCY.observe(latency) logger.info(f"Router response in {latency:.2f}s") return { "ok": True, "data": { "text": result.get("response", result.get("text", "")), "image_base64": result.get("image_base64"), "file_base64": result.get("file_base64"), "file_name": result.get("file_name"), "file_mime": result.get("file_mime"), }, "response": result.get("response", result.get("text", "")), "model": result.get("model"), "backend": result.get("backend"), "image_base64": result.get("image_base64"), "file_base64": result.get("file_base64"), "file_name": result.get("file_name"), "file_mime": result.get("file_mime"), } except httpx.TimeoutException as e: if METRICS_AVAILABLE: ROUTER_CALLS_TOTAL.labels(status="timeout").inc() ERRORS_TOTAL.labels(type="timeout", source="router").inc() logger.error(f"Router request timeout after {time.time() - _start:.2f}s: {e}") raise except httpx.HTTPError as e: if METRICS_AVAILABLE: ROUTER_CALLS_TOTAL.labels(status="error").inc() ERRORS_TOTAL.labels(type="http_error", source="router").inc() logger.error(f"Router request failed: {e}") raise