Files
microdao-daarion/services/router/prompt_builder.py
Apple bca81dc719 feat: Node Self-Healing, DAGI Audit, Agent Prompts, Infra Invariants
### 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
2025-11-30 13:52:01 -08:00

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