### Backend (city-service) - Node Registry + Self-Healing API (migration 039) - Improved get_all_nodes() with robust fallback for node_registry/node_cache - Agent Prompts Runtime API for DAGI Router integration - DAGI Router Audit endpoints (phantom/stale detection) - Node Agents API (Guardian/Steward) - Node metrics extended (CPU/GPU/RAM/Disk) ### Frontend (apps/web) - Node Directory with improved error handling - Node Cabinet with metrics cards - DAGI Router Card component - Node Metrics Card component - useDAGIAudit hook ### Scripts - check-invariants.py - deploy verification - node-bootstrap.sh - node self-registration - node-guardian-loop.py - continuous self-healing - dagi_agent_audit.py - DAGI audit utility ### Migrations - 034: Agent prompts seed - 035: Agent DAGI audit - 036: Node metrics extended - 037: Node agents complete - 038: Agent prompts full coverage - 039: Node registry self-healing ### Tests - test_infra_smoke.py - test_agent_prompts_runtime.py - test_dagi_router_api.py ### Documentation - DEPLOY_CHECKLIST_2024_11_30.md - Multiple TASK_PHASE docs
279 lines
9.5 KiB
Python
279 lines
9.5 KiB
Python
"""
|
|
Prompt Builder for DAGI Router
|
|
|
|
Цей модуль відповідає за побудову system prompts для агентів,
|
|
використовуючи дані з БД через city-service API.
|
|
|
|
Частина Agent System Prompts MVP v2
|
|
"""
|
|
|
|
import httpx
|
|
import logging
|
|
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
|
|
|
|
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"""
|
|
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:
|
|
logger.error(f"Error fetching prompt from city-service: {e}")
|
|
return None
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error fetching prompt: {e}")
|
|
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
|
|
|