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>
202 lines
7.4 KiB
Python
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)
|