Files
microdao-daarion/gateway-bot/chat_isolation.py
Apple ef3473db21 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>
2026-02-09 08:46:46 -08:00

202 lines
7.4 KiB
Python

"""
DAARION Platform - Chat Isolation Module
=========================================
Determines agent_id ONLY from chat_id / bot_token.
NO cross-agent routing!
"""
import yaml
import logging
from pathlib import Path
from typing import Dict, Optional, Tuple, Any
from dataclasses import dataclass
logger = logging.getLogger(__name__)
@dataclass
class ChatResolution:
"""Result of chat -> agent resolution"""
agent_id: str
agent_name: str
nats_invoke: str
nats_response: str
is_private: bool
chat_name: Optional[str] = None
out_of_domain_response: Optional[str] = None
class ChatIsolation:
"""
Manages strict chat -> agent isolation.
Each chat belongs to exactly ONE agent.
"""
def __init__(self, config_path: str = None):
if config_path is None:
config_path = Path(__file__).parent / "agents_chat_map.yaml"
self.config_path = Path(config_path)
self.config: Dict[str, Any] = {}
self.chat_map: Dict[int, str] = {} # chat_id -> agent_id
self.agents: Dict[str, Dict] = {} # agent_id -> agent config
self.bot_tokens: Dict[str, str] = {} # env_var -> agent_id
self._load_config()
def _load_config(self):
"""Load and validate configuration"""
if not self.config_path.exists():
logger.error(f"Chat isolation config not found: {self.config_path}")
return
try:
with open(self.config_path, 'r', encoding='utf-8') as f:
self.config = yaml.safe_load(f)
# Build chat_id -> agent_id mapping
for agent_id, agent_data in self.config.get("agents", {}).items():
self.agents[agent_id] = agent_data
for chat in agent_data.get("telegram_chats", []):
if chat.get("type") == "private":
# Private chats handled via bot token
continue
chat_id = chat.get("chat_id")
if chat_id and chat.get("enabled", True):
self.chat_map[chat_id] = agent_id
# Build bot_token -> agent_id mapping
for agent_id, env_var in self.config.get("bot_tokens", {}).items():
self.bot_tokens[env_var] = agent_id
logger.info(f"✅ Chat isolation loaded: {len(self.chat_map)} group chats, {len(self.agents)} agents")
except Exception as e:
logger.error(f"Failed to load chat isolation config: {e}")
def resolve_agent(
self,
chat_id: int,
chat_type: str = "private",
bot_token_env: str = None
) -> Optional[ChatResolution]:
"""
Resolve chat to agent.
Priority:
1. For private chats: use bot_token to determine agent
2. For group chats: use chat_id mapping
Returns:
ChatResolution or None if chat is not configured
"""
agent_id = None
chat_name = None
is_private = chat_type == "private"
# 1. Try to resolve by bot token (for private chats)
if is_private and bot_token_env:
agent_id = self.bot_tokens.get(bot_token_env)
logger.debug(f"Private chat: resolved to {agent_id} via bot token")
# 2. Try to resolve by chat_id (for groups)
if not agent_id and chat_id in self.chat_map:
agent_id = self.chat_map[chat_id]
# Find chat name
agent_data = self.agents.get(agent_id, {})
for chat in agent_data.get("telegram_chats", []):
if chat.get("chat_id") == chat_id:
chat_name = chat.get("name")
break
logger.debug(f"Group chat {chat_id}: resolved to {agent_id}")
# 3. For private chats without specific mapping, try to infer from bot token
if not agent_id and is_private:
# Default: try to match webhook path or use fallback
logger.warning(f"Private chat {chat_id}: no specific mapping, need bot_token_env")
if not agent_id:
logger.warning(f"Chat {chat_id} ({chat_type}): NO AGENT CONFIGURED")
return None
agent_data = self.agents.get(agent_id, {})
out_of_domain = agent_data.get("out_of_domain", {}).get("response_uk", "")
return ChatResolution(
agent_id=agent_id,
agent_name=agent_data.get("name", agent_id),
nats_invoke=agent_data.get("nats_invoke", f"agent.{agent_id}.invoke"),
nats_response=agent_data.get("nats_response", f"agent.{agent_id}.response"),
is_private=is_private,
chat_name=chat_name,
out_of_domain_response=out_of_domain
)
def get_unknown_chat_response(self, lang: str = "uk") -> str:
"""Get response for unknown/unconfigured chats"""
policy = self.config.get("unknown_chat_policy", {})
key = f"message_{lang}"
return policy.get(key, policy.get("message_uk", "Чат не налаштований."))
def get_agent_domain(self, agent_id: str) -> list:
"""Get agent's domain keywords"""
return self.agents.get(agent_id, {}).get("domain", [])
def is_out_of_domain(self, agent_id: str, message: str) -> bool:
"""
Check if message is clearly outside agent's domain.
NOTE: This is a simple heuristic, not for routing!
Used only to generate polite out-of-domain responses.
"""
# Get other agents' domains
other_domains = []
for aid, adata in self.agents.items():
if aid != agent_id:
other_domains.extend(adata.get("domain", []))
# Simple keyword check
message_lower = message.lower()
# Check if message contains keywords from OTHER domains
# AND does NOT contain keywords from THIS agent's domain
own_domain = self.get_agent_domain(agent_id)
has_own_keyword = any(kw in message_lower for kw in own_domain)
has_other_keyword = any(kw in message_lower for kw in other_domains)
# Only flag as out-of-domain if clearly about another domain
return has_other_keyword and not has_own_keyword
def get_out_of_domain_response(self, agent_id: str, lang: str = "uk") -> str:
"""Get polite out-of-domain response for agent"""
agent_data = self.agents.get(agent_id, {})
key = f"response_{lang}"
return agent_data.get("out_of_domain", {}).get(key, "")
# Global instance
_chat_isolation: Optional[ChatIsolation] = None
def get_chat_isolation() -> ChatIsolation:
"""Get or create chat isolation instance"""
global _chat_isolation
if _chat_isolation is None:
_chat_isolation = ChatIsolation()
return _chat_isolation
def resolve_agent_for_chat(
chat_id: int,
chat_type: str = "private",
bot_token_env: str = None
) -> Optional[ChatResolution]:
"""
Convenience function to resolve agent for a chat.
Usage:
resolution = resolve_agent_for_chat(chat_id, "group")
if resolution:
agent_id = resolution.agent_id
nats_subject = resolution.nats_invoke
"""
return get_chat_isolation().resolve_agent(chat_id, chat_type, bot_token_env)