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>
567 lines
20 KiB
Python
567 lines
20 KiB
Python
"""
|
||
Config Registry Service
|
||
=======================
|
||
Версійоване сховище конфігурацій для DAARION platform.
|
||
|
||
Зберігає:
|
||
- Системні промпти (версійовані)
|
||
- Routing rules
|
||
- Agent configurations
|
||
- Feature flags
|
||
- Quotas per user/team/agent
|
||
"""
|
||
|
||
import json
|
||
import hashlib
|
||
import logging
|
||
from datetime import datetime
|
||
from typing import Dict, Any, Optional, List
|
||
from dataclasses import dataclass, field
|
||
from pathlib import Path
|
||
import yaml
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
@dataclass
|
||
class ConfigVersion:
|
||
"""Версія конфігурації"""
|
||
version: str
|
||
content: Dict[str, Any]
|
||
content_hash: str
|
||
created_at: datetime
|
||
created_by: str
|
||
is_active: bool = False
|
||
description: str = ""
|
||
|
||
@dataclass
|
||
class PromptConfig:
|
||
"""Конфігурація системного промпту"""
|
||
agent_id: str
|
||
current_version: str
|
||
versions: Dict[str, ConfigVersion] = field(default_factory=dict)
|
||
|
||
def get_active_prompt(self) -> str:
|
||
"""Повертає активний промпт"""
|
||
if self.current_version in self.versions:
|
||
return self.versions[self.current_version].content.get("prompt", "")
|
||
return ""
|
||
|
||
@dataclass
|
||
class RoutingRule:
|
||
"""Правило маршрутизації"""
|
||
rule_id: str
|
||
name: str
|
||
conditions: Dict[str, Any] # {"intent": "energy", "confidence_min": 0.7}
|
||
target_agent: str
|
||
priority: int = 0
|
||
enabled: bool = True
|
||
|
||
@dataclass
|
||
class AgentQuota:
|
||
"""Квота для агента/користувача"""
|
||
entity_type: str # "user", "team", "agent"
|
||
entity_id: str
|
||
quota_type: str # "tokens", "requests", "storage"
|
||
limit: int
|
||
used: int = 0
|
||
reset_period: str = "daily" # "daily", "weekly", "monthly"
|
||
last_reset: Optional[datetime] = None
|
||
|
||
|
||
class ConfigRegistry:
|
||
"""
|
||
Централізований реєстр конфігурацій.
|
||
|
||
Підтримує:
|
||
- Версіонування промптів
|
||
- Routing rules
|
||
- Feature flags
|
||
- Quotas
|
||
"""
|
||
|
||
def __init__(self, config_dir: str = "/opt/microdao-daarion/config"):
|
||
self.config_dir = Path(config_dir)
|
||
self.config_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
# In-memory cache
|
||
self._prompts: Dict[str, PromptConfig] = {}
|
||
self._routing_rules: Dict[str, RoutingRule] = {}
|
||
self._feature_flags: Dict[str, bool] = {}
|
||
self._quotas: Dict[str, AgentQuota] = {}
|
||
|
||
# Load existing configs
|
||
self._load_all()
|
||
|
||
def _load_all(self):
|
||
"""Завантажує всі конфігурації з файлів"""
|
||
self._load_prompts()
|
||
self._load_routing_rules()
|
||
self._load_feature_flags()
|
||
self._load_quotas()
|
||
|
||
def _compute_hash(self, content: Any) -> str:
|
||
"""Обчислює SHA256 хеш контенту"""
|
||
content_str = json.dumps(content, sort_keys=True, default=str)
|
||
return hashlib.sha256(content_str.encode()).hexdigest()[:16]
|
||
|
||
# ==================== PROMPTS ====================
|
||
|
||
def _load_prompts(self):
|
||
"""Завантажує промпти з файлів"""
|
||
prompts_dir = self.config_dir / "prompts"
|
||
if not prompts_dir.exists():
|
||
prompts_dir.mkdir(parents=True, exist_ok=True)
|
||
return
|
||
|
||
for prompt_file in prompts_dir.glob("*.yaml"):
|
||
try:
|
||
with open(prompt_file, 'r', encoding='utf-8') as f:
|
||
data = yaml.safe_load(f)
|
||
if data:
|
||
agent_id = prompt_file.stem
|
||
self._prompts[agent_id] = self._parse_prompt_config(agent_id, data)
|
||
except Exception as e:
|
||
logger.error(f"Failed to load prompt {prompt_file}: {e}")
|
||
|
||
def _parse_prompt_config(self, agent_id: str, data: Dict) -> PromptConfig:
|
||
"""Парсить конфігурацію промпту"""
|
||
versions = {}
|
||
for ver_id, ver_data in data.get("versions", {}).items():
|
||
versions[ver_id] = ConfigVersion(
|
||
version=ver_id,
|
||
content=ver_data.get("content", {}),
|
||
content_hash=self._compute_hash(ver_data.get("content", {})),
|
||
created_at=datetime.fromisoformat(ver_data.get("created_at", datetime.now().isoformat())),
|
||
created_by=ver_data.get("created_by", "system"),
|
||
is_active=ver_data.get("is_active", False),
|
||
description=ver_data.get("description", "")
|
||
)
|
||
|
||
return PromptConfig(
|
||
agent_id=agent_id,
|
||
current_version=data.get("current_version", "v1"),
|
||
versions=versions
|
||
)
|
||
|
||
def get_prompt(self, agent_id: str, version: str = None) -> Optional[str]:
|
||
"""
|
||
Повертає промпт для агента.
|
||
|
||
Args:
|
||
agent_id: ID агента
|
||
version: Версія (або None для активної)
|
||
"""
|
||
if agent_id not in self._prompts:
|
||
return None
|
||
|
||
config = self._prompts[agent_id]
|
||
target_version = version or config.current_version
|
||
|
||
if target_version in config.versions:
|
||
return config.versions[target_version].content.get("prompt", "")
|
||
|
||
return None
|
||
|
||
def set_prompt(self, agent_id: str, prompt: str,
|
||
version: str = None, created_by: str = "system",
|
||
description: str = "", activate: bool = True) -> str:
|
||
"""
|
||
Створює нову версію промпту.
|
||
|
||
Returns:
|
||
Версія нового промпту
|
||
"""
|
||
if agent_id not in self._prompts:
|
||
self._prompts[agent_id] = PromptConfig(
|
||
agent_id=agent_id,
|
||
current_version="v1",
|
||
versions={}
|
||
)
|
||
|
||
config = self._prompts[agent_id]
|
||
|
||
# Generate version if not provided
|
||
if version is None:
|
||
version = f"v{len(config.versions) + 1}"
|
||
|
||
# Create new version
|
||
content = {"prompt": prompt}
|
||
config.versions[version] = ConfigVersion(
|
||
version=version,
|
||
content=content,
|
||
content_hash=self._compute_hash(content),
|
||
created_at=datetime.now(),
|
||
created_by=created_by,
|
||
is_active=activate,
|
||
description=description
|
||
)
|
||
|
||
if activate:
|
||
# Deactivate previous
|
||
for v in config.versions.values():
|
||
if v.version != version:
|
||
v.is_active = False
|
||
config.current_version = version
|
||
|
||
# Save to file
|
||
self._save_prompt(agent_id)
|
||
|
||
logger.info(f"Created prompt version {version} for {agent_id}")
|
||
return version
|
||
|
||
def _save_prompt(self, agent_id: str):
|
||
"""Зберігає промпт у файл"""
|
||
prompts_dir = self.config_dir / "prompts"
|
||
prompts_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
config = self._prompts[agent_id]
|
||
data = {
|
||
"agent_id": agent_id,
|
||
"current_version": config.current_version,
|
||
"versions": {
|
||
v.version: {
|
||
"content": v.content,
|
||
"content_hash": v.content_hash,
|
||
"created_at": v.created_at.isoformat(),
|
||
"created_by": v.created_by,
|
||
"is_active": v.is_active,
|
||
"description": v.description
|
||
}
|
||
for v in config.versions.values()
|
||
}
|
||
}
|
||
|
||
with open(prompts_dir / f"{agent_id}.yaml", 'w', encoding='utf-8') as f:
|
||
yaml.dump(data, f, allow_unicode=True, default_flow_style=False)
|
||
|
||
def list_prompt_versions(self, agent_id: str) -> List[Dict]:
|
||
"""Повертає список версій промпту"""
|
||
if agent_id not in self._prompts:
|
||
return []
|
||
|
||
return [
|
||
{
|
||
"version": v.version,
|
||
"content_hash": v.content_hash,
|
||
"created_at": v.created_at.isoformat(),
|
||
"created_by": v.created_by,
|
||
"is_active": v.is_active,
|
||
"description": v.description
|
||
}
|
||
for v in self._prompts[agent_id].versions.values()
|
||
]
|
||
|
||
def rollback_prompt(self, agent_id: str, version: str) -> bool:
|
||
"""Відкатує промпт до вказаної версії"""
|
||
if agent_id not in self._prompts:
|
||
return False
|
||
|
||
config = self._prompts[agent_id]
|
||
if version not in config.versions:
|
||
return False
|
||
|
||
# Deactivate all
|
||
for v in config.versions.values():
|
||
v.is_active = False
|
||
|
||
# Activate target
|
||
config.versions[version].is_active = True
|
||
config.current_version = version
|
||
|
||
self._save_prompt(agent_id)
|
||
logger.info(f"Rolled back {agent_id} prompt to {version}")
|
||
return True
|
||
|
||
# ==================== ROUTING RULES ====================
|
||
|
||
def _load_routing_rules(self):
|
||
"""Завантажує routing rules"""
|
||
rules_file = self.config_dir / "routing_rules.yaml"
|
||
if not rules_file.exists():
|
||
return
|
||
|
||
try:
|
||
with open(rules_file, 'r', encoding='utf-8') as f:
|
||
data = yaml.safe_load(f) or {}
|
||
for rule_id, rule_data in data.get("rules", {}).items():
|
||
self._routing_rules[rule_id] = RoutingRule(
|
||
rule_id=rule_id,
|
||
name=rule_data.get("name", rule_id),
|
||
conditions=rule_data.get("conditions", {}),
|
||
target_agent=rule_data.get("target_agent", ""),
|
||
priority=rule_data.get("priority", 0),
|
||
enabled=rule_data.get("enabled", True)
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Failed to load routing rules: {e}")
|
||
|
||
def get_routing_rules(self, enabled_only: bool = True) -> List[RoutingRule]:
|
||
"""Повертає routing rules, відсортовані за пріоритетом"""
|
||
rules = list(self._routing_rules.values())
|
||
if enabled_only:
|
||
rules = [r for r in rules if r.enabled]
|
||
return sorted(rules, key=lambda r: r.priority, reverse=True)
|
||
|
||
def add_routing_rule(self, rule: RoutingRule):
|
||
"""Додає routing rule"""
|
||
self._routing_rules[rule.rule_id] = rule
|
||
self._save_routing_rules()
|
||
|
||
def _save_routing_rules(self):
|
||
"""Зберігає routing rules"""
|
||
data = {
|
||
"rules": {
|
||
r.rule_id: {
|
||
"name": r.name,
|
||
"conditions": r.conditions,
|
||
"target_agent": r.target_agent,
|
||
"priority": r.priority,
|
||
"enabled": r.enabled
|
||
}
|
||
for r in self._routing_rules.values()
|
||
}
|
||
}
|
||
|
||
with open(self.config_dir / "routing_rules.yaml", 'w', encoding='utf-8') as f:
|
||
yaml.dump(data, f, allow_unicode=True, default_flow_style=False)
|
||
|
||
# ==================== FEATURE FLAGS ====================
|
||
|
||
def _load_feature_flags(self):
|
||
"""Завантажує feature flags"""
|
||
flags_file = self.config_dir / "feature_flags.yaml"
|
||
if not flags_file.exists():
|
||
# Default flags
|
||
self._feature_flags = {
|
||
"orchestration_enabled": True,
|
||
"soft_handoff_enabled": True,
|
||
"auto_handoff_enabled": False,
|
||
"privacy_gate_enabled": True,
|
||
"audit_logging_enabled": True,
|
||
"multi_agent_workflows": False,
|
||
}
|
||
self._save_feature_flags()
|
||
return
|
||
|
||
try:
|
||
with open(flags_file, 'r', encoding='utf-8') as f:
|
||
self._feature_flags = yaml.safe_load(f) or {}
|
||
except Exception as e:
|
||
logger.error(f"Failed to load feature flags: {e}")
|
||
|
||
def get_feature_flag(self, flag_name: str, default: bool = False) -> bool:
|
||
"""Повертає значення feature flag"""
|
||
return self._feature_flags.get(flag_name, default)
|
||
|
||
def set_feature_flag(self, flag_name: str, value: bool):
|
||
"""Встановлює feature flag"""
|
||
self._feature_flags[flag_name] = value
|
||
self._save_feature_flags()
|
||
logger.info(f"Feature flag {flag_name} set to {value}")
|
||
|
||
def _save_feature_flags(self):
|
||
"""Зберігає feature flags"""
|
||
with open(self.config_dir / "feature_flags.yaml", 'w', encoding='utf-8') as f:
|
||
yaml.dump(self._feature_flags, f, allow_unicode=True)
|
||
|
||
# ==================== QUOTAS ====================
|
||
|
||
def _load_quotas(self):
|
||
"""Завантажує квоти"""
|
||
quotas_file = self.config_dir / "quotas.yaml"
|
||
if not quotas_file.exists():
|
||
return
|
||
|
||
try:
|
||
with open(quotas_file, 'r', encoding='utf-8') as f:
|
||
data = yaml.safe_load(f) or {}
|
||
for quota_id, quota_data in data.get("quotas", {}).items():
|
||
self._quotas[quota_id] = AgentQuota(
|
||
entity_type=quota_data.get("entity_type", "user"),
|
||
entity_id=quota_data.get("entity_id", ""),
|
||
quota_type=quota_data.get("quota_type", "requests"),
|
||
limit=quota_data.get("limit", 1000),
|
||
used=quota_data.get("used", 0),
|
||
reset_period=quota_data.get("reset_period", "daily"),
|
||
last_reset=datetime.fromisoformat(quota_data["last_reset"]) if quota_data.get("last_reset") else None
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Failed to load quotas: {e}")
|
||
|
||
def get_quota(self, entity_type: str, entity_id: str, quota_type: str) -> Optional[AgentQuota]:
|
||
"""Повертає квоту"""
|
||
quota_id = f"{entity_type}:{entity_id}:{quota_type}"
|
||
return self._quotas.get(quota_id)
|
||
|
||
def check_quota(self, entity_type: str, entity_id: str,
|
||
quota_type: str, amount: int = 1) -> bool:
|
||
"""
|
||
Перевіряє, чи є достатньо квоти.
|
||
|
||
Returns:
|
||
True якщо квота дозволяє, False якщо ліміт перевищено
|
||
"""
|
||
quota = self.get_quota(entity_type, entity_id, quota_type)
|
||
if quota is None:
|
||
return True # No quota = unlimited
|
||
|
||
return (quota.used + amount) <= quota.limit
|
||
|
||
def consume_quota(self, entity_type: str, entity_id: str,
|
||
quota_type: str, amount: int = 1) -> bool:
|
||
"""
|
||
Споживає квоту.
|
||
|
||
Returns:
|
||
True якщо успішно, False якщо ліміт перевищено
|
||
"""
|
||
quota_id = f"{entity_type}:{entity_id}:{quota_type}"
|
||
|
||
if quota_id not in self._quotas:
|
||
# Create default quota
|
||
self._quotas[quota_id] = AgentQuota(
|
||
entity_type=entity_type,
|
||
entity_id=entity_id,
|
||
quota_type=quota_type,
|
||
limit=10000, # Default limit
|
||
used=0,
|
||
reset_period="daily"
|
||
)
|
||
|
||
quota = self._quotas[quota_id]
|
||
|
||
if (quota.used + amount) > quota.limit:
|
||
return False
|
||
|
||
quota.used += amount
|
||
self._save_quotas()
|
||
return True
|
||
|
||
def _save_quotas(self):
|
||
"""Зберігає квоти"""
|
||
data = {
|
||
"quotas": {
|
||
f"{q.entity_type}:{q.entity_id}:{q.quota_type}": {
|
||
"entity_type": q.entity_type,
|
||
"entity_id": q.entity_id,
|
||
"quota_type": q.quota_type,
|
||
"limit": q.limit,
|
||
"used": q.used,
|
||
"reset_period": q.reset_period,
|
||
"last_reset": q.last_reset.isoformat() if q.last_reset else None
|
||
}
|
||
for q in self._quotas.values()
|
||
}
|
||
}
|
||
|
||
with open(self.config_dir / "quotas.yaml", 'w', encoding='utf-8') as f:
|
||
yaml.dump(data, f, allow_unicode=True, default_flow_style=False)
|
||
|
||
|
||
# Global instance
|
||
_config_registry: Optional[ConfigRegistry] = None
|
||
|
||
def get_config_registry() -> ConfigRegistry:
|
||
"""Повертає глобальний екземпляр ConfigRegistry"""
|
||
global _config_registry
|
||
if _config_registry is None:
|
||
_config_registry = ConfigRegistry()
|
||
return _config_registry
|
||
|
||
|
||
# ==================== HTTP API ====================
|
||
|
||
from fastapi import APIRouter, HTTPException
|
||
from pydantic import BaseModel
|
||
|
||
router = APIRouter(prefix="/config", tags=["config"])
|
||
|
||
class PromptRequest(BaseModel):
|
||
prompt: str
|
||
version: Optional[str] = None
|
||
description: str = ""
|
||
activate: bool = True
|
||
|
||
class FeatureFlagRequest(BaseModel):
|
||
value: bool
|
||
|
||
@router.get("/prompts/{agent_id}")
|
||
async def get_prompt(agent_id: str, version: str = None):
|
||
"""Отримати промпт для агента"""
|
||
registry = get_config_registry()
|
||
prompt = registry.get_prompt(agent_id, version)
|
||
if prompt is None:
|
||
raise HTTPException(404, f"Prompt not found for {agent_id}")
|
||
return {"agent_id": agent_id, "prompt": prompt}
|
||
|
||
@router.get("/prompts/{agent_id}/versions")
|
||
async def list_prompt_versions(agent_id: str):
|
||
"""Список версій промпту"""
|
||
registry = get_config_registry()
|
||
versions = registry.list_prompt_versions(agent_id)
|
||
return {"agent_id": agent_id, "versions": versions}
|
||
|
||
@router.post("/prompts/{agent_id}")
|
||
async def set_prompt(agent_id: str, request: PromptRequest):
|
||
"""Створити нову версію промпту"""
|
||
registry = get_config_registry()
|
||
version = registry.set_prompt(
|
||
agent_id=agent_id,
|
||
prompt=request.prompt,
|
||
version=request.version,
|
||
description=request.description,
|
||
activate=request.activate
|
||
)
|
||
return {"agent_id": agent_id, "version": version, "activated": request.activate}
|
||
|
||
@router.post("/prompts/{agent_id}/rollback/{version}")
|
||
async def rollback_prompt(agent_id: str, version: str):
|
||
"""Відкатити промпт до версії"""
|
||
registry = get_config_registry()
|
||
success = registry.rollback_prompt(agent_id, version)
|
||
if not success:
|
||
raise HTTPException(404, f"Version {version} not found")
|
||
return {"agent_id": agent_id, "version": version, "status": "rolled_back"}
|
||
|
||
@router.get("/feature-flags")
|
||
async def get_feature_flags():
|
||
"""Отримати всі feature flags"""
|
||
registry = get_config_registry()
|
||
return {"flags": registry._feature_flags}
|
||
|
||
@router.get("/feature-flags/{flag_name}")
|
||
async def get_feature_flag(flag_name: str):
|
||
"""Отримати feature flag"""
|
||
registry = get_config_registry()
|
||
value = registry.get_feature_flag(flag_name)
|
||
return {"flag": flag_name, "value": value}
|
||
|
||
@router.put("/feature-flags/{flag_name}")
|
||
async def set_feature_flag(flag_name: str, request: FeatureFlagRequest):
|
||
"""Встановити feature flag"""
|
||
registry = get_config_registry()
|
||
registry.set_feature_flag(flag_name, request.value)
|
||
return {"flag": flag_name, "value": request.value}
|
||
|
||
@router.get("/routing-rules")
|
||
async def get_routing_rules(enabled_only: bool = True):
|
||
"""Отримати routing rules"""
|
||
registry = get_config_registry()
|
||
rules = registry.get_routing_rules(enabled_only)
|
||
return {"rules": [
|
||
{
|
||
"rule_id": r.rule_id,
|
||
"name": r.name,
|
||
"conditions": r.conditions,
|
||
"target_agent": r.target_agent,
|
||
"priority": r.priority,
|
||
"enabled": r.enabled
|
||
}
|
||
for r in rules
|
||
]}
|
||
|
||
@router.get("/health")
|
||
async def health():
|
||
"""Health check"""
|
||
return {"status": "ok", "service": "config-registry"}
|