""" Prompt Builder for DAGI Router Цей модуль відповідає за побудову system prompts для агентів, використовуючи дані з БД через city-service API. Частина Agent System Prompts MVP v2 """ import httpx import logging import os import time from typing import Dict, Any, Optional from dataclasses import dataclass logger = logging.getLogger(__name__) @dataclass class AgentSystemPrompt: """Результат побудови system prompt""" agent_id: str agent_name: Optional[str] has_prompts: bool system_prompt: str source: str # "database", "fallback", "config" class PromptBuilder: """ Будує system prompts для агентів. Порядок пріоритетів: 1. Промти з БД (через city-service API) 2. Промти з router-config.yml 3. Fallback default prompt """ def __init__( self, city_service_url: str = "http://daarion-city-service:7001", router_config: Optional[Dict[str, Any]] = None ): self.city_service_url = city_service_url.rstrip("/") self.router_config = router_config or {} self._http_client: Optional[httpx.AsyncClient] = None self._city_service_unavailable_until = 0.0 self._city_service_cooldown_s = float(os.getenv("CITY_SERVICE_FAILURE_COOLDOWN_S", "120") or "120") async def _get_http_client(self) -> httpx.AsyncClient: """Lazy initialization of HTTP client""" if self._http_client is None: self._http_client = httpx.AsyncClient(timeout=10.0) return self._http_client async def close(self): """Close HTTP client""" if self._http_client: await self._http_client.aclose() self._http_client = None async def get_system_prompt(self, agent_id: str) -> AgentSystemPrompt: """ Отримати system prompt для агента. Спочатку пробує отримати з БД, потім з конфігу, потім fallback. """ # Try database first db_prompt = await self._fetch_from_database(agent_id) if db_prompt and db_prompt.has_prompts: logger.info(f"Using database prompt for agent {agent_id}") return db_prompt # Try config config_prompt = self._get_from_config(agent_id) if config_prompt: logger.info(f"Using config prompt for agent {agent_id}") return config_prompt # Fallback logger.warning(f"No prompts found for agent {agent_id}, using fallback") return self._get_fallback_prompt(agent_id) async def _fetch_from_database(self, agent_id: str) -> Optional[AgentSystemPrompt]: """Fetch system prompt from city-service API""" now = time.monotonic() if now < self._city_service_unavailable_until: return None try: client = await self._get_http_client() url = f"{self.city_service_url}/internal/agents/{agent_id}/system-prompt" response = await client.get(url) if response.status_code == 200: data = response.json() return AgentSystemPrompt( agent_id=data.get("agent_id", agent_id), agent_name=data.get("agent_name"), has_prompts=data.get("has_prompts", False), system_prompt=data.get("system_prompt", ""), source="database" ) else: logger.warning(f"City service returned {response.status_code} for agent {agent_id}") return None except httpx.RequestError as e: self._city_service_unavailable_until = time.monotonic() + max(0.0, self._city_service_cooldown_s) logger.warning( "Error fetching prompt from city-service: %s; suppressing retries for %.0fs", e, self._city_service_cooldown_s, ) return None except Exception as e: self._city_service_unavailable_until = time.monotonic() + max(0.0, self._city_service_cooldown_s) logger.warning( "Unexpected error fetching prompt: %s; suppressing retries for %.0fs", e, self._city_service_cooldown_s, ) return None def _get_from_config(self, agent_id: str) -> Optional[AgentSystemPrompt]: """Get system prompt from router config""" agents = self.router_config.get("agents", {}) agent_config = agents.get(agent_id) if not agent_config: return None system_prompt = agent_config.get("system_prompt") if not system_prompt: return None return AgentSystemPrompt( agent_id=agent_id, agent_name=agent_config.get("description"), has_prompts=True, system_prompt=system_prompt.strip(), source="config" ) def _get_fallback_prompt(self, agent_id: str) -> AgentSystemPrompt: """Generate fallback prompt for unknown agent""" fallback_prompt = ( f"You are an AI agent (ID: {agent_id}) in the DAARION.city ecosystem.\n\n" "Guidelines:\n" "- Be helpful, accurate, and professional\n" "- Follow ethical guidelines and safety protocols\n" "- Respect user privacy and data protection\n" "- Ask for clarification when uncertain\n" "- Never execute harmful or unauthorized actions\n" ) return AgentSystemPrompt( agent_id=agent_id, agent_name=None, has_prompts=False, system_prompt=fallback_prompt, source="fallback" ) async def check_prompts_available(self, agent_ids: list[str]) -> Dict[str, bool]: """ Check if prompts are available for multiple agents. Returns dict mapping agent_id to has_prompts boolean. """ result = {} try: client = await self._get_http_client() url = f"{self.city_service_url}/internal/agents/prompts/status" response = await client.post(url, json={"agent_ids": agent_ids}) if response.status_code == 200: data = response.json() result = data.get("status", {}) except Exception as e: logger.error(f"Error checking prompts status: {e}") # Fill missing with config check for agent_id in agent_ids: if agent_id not in result: config_prompt = self._get_from_config(agent_id) result[agent_id] = config_prompt is not None return result def build_system_prompt_from_parts( prompts: Dict[str, Optional[str]], agent_info: Optional[Dict[str, Any]] = None, context: Optional[Dict[str, Any]] = None ) -> str: """ Build system prompt from individual parts. This is a standalone function that can be used without PromptBuilder class. Args: prompts: Dict with keys "core", "safety", "governance", "tools" agent_info: Optional dict with agent metadata (name, kind, etc.) context: Optional dict with runtime context (node, microdao, etc.) Returns: Assembled system prompt string """ parts = [] # Core prompt (required) if prompts.get("core"): parts.append(prompts["core"]) elif agent_info: agent_name = agent_info.get("display_name") or agent_info.get("name") or "Agent" agent_kind = agent_info.get("kind") or "assistant" parts.append( f"You are {agent_name}, an AI {agent_kind} in DAARION.city ecosystem. " f"Be helpful, accurate, and follow ethical guidelines." ) else: parts.append("You are an AI assistant. Be helpful and accurate.") # Governance rules if prompts.get("governance"): parts.append("\n\n## Governance\n" + prompts["governance"]) # Safety guidelines if prompts.get("safety"): parts.append("\n\n## Safety Guidelines\n" + prompts["safety"]) # Tools instructions if prompts.get("tools"): parts.append("\n\n## Tools & Capabilities\n" + prompts["tools"]) # Context additions if context: context_lines = [] if context.get("node"): node = context["node"] context_lines.append(f"- **Node**: {node.get('name', 'Unknown')}") if context.get("district"): district = context["district"] context_lines.append(f"- **District**: {district.get('name', 'Unknown')}") if context.get("microdao"): microdao = context["microdao"] context_lines.append(f"- **MicroDAO**: {microdao.get('name', 'Unknown')}") if context.get("user_role"): context_lines.append(f"- **User Role**: {context['user_role']}") if context_lines: parts.append("\n\n## Current Context\n" + "\n".join(context_lines)) return "\n".join(parts) # Singleton instance for convenience _prompt_builder: Optional[PromptBuilder] = None async def get_prompt_builder( city_service_url: str = "http://daarion-city-service:7001", router_config: Optional[Dict[str, Any]] = None ) -> PromptBuilder: """Get or create singleton PromptBuilder instance""" global _prompt_builder if _prompt_builder is None: _prompt_builder = PromptBuilder(city_service_url, router_config) return _prompt_builder async def get_agent_system_prompt( agent_id: str, city_service_url: str = "http://daarion-city-service:7001", router_config: Optional[Dict[str, Any]] = None ) -> str: """ Convenience function to get system prompt for an agent. Usage in DAGI Router: system_prompt = await get_agent_system_prompt("daarwizz") """ builder = await get_prompt_builder(city_service_url, router_config) result = await builder.get_system_prompt(agent_id) return result.system_prompt