Pulled from /opt/teleo-eval/telegram/ on VPS. Includes: - bot.py (92K), kb_retrieval.py, kb_tools.py (agentic retrieval) - retrieval.py (RRF merge, query decomposition, entity traversal) - response.py (system prompt builder, response parser) - agent_config.py, agent_runner.py (multi-agent template unit support) - approval_stages.py, approvals.py, digest.py (approval workflow) - eval_checks.py, eval.py (response quality checks) - output_gate.py, x_publisher.py, x_client.py, x_search.py (X pipeline) - market_data.py, worktree_lock.py (utilities) - rio.yaml, theseus.yaml (agent configs) These files were deployed to VPS but never committed to the repo. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
160 lines
5.2 KiB
Python
160 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Agent config loader and validator.
|
|
|
|
Loads YAML config files from telegram/agents/*.yaml, validates required fields,
|
|
resolves file paths. Used by bot.py and future agent_runner.py.
|
|
|
|
Epimetheus owns this module.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
import re
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
logger = logging.getLogger("tg.agent_config")
|
|
|
|
SECRETS_DIR = "/opt/teleo-eval/secrets"
|
|
WORKTREE_DIR = "/opt/teleo-eval/workspaces/main"
|
|
|
|
REQUIRED_FIELDS = ["name", "handle", "bot_token_file", "pentagon_agent_id", "domain"]
|
|
REQUIRED_VOICE_FIELDS = ["voice_summary", "voice_definition"]
|
|
REQUIRED_KB_FIELDS = ["kb_scope"]
|
|
|
|
|
|
@dataclass
|
|
class AgentConfig:
|
|
"""Validated agent configuration loaded from YAML."""
|
|
name: str
|
|
handle: str
|
|
x_handle: Optional[str]
|
|
bot_token_file: str
|
|
pentagon_agent_id: str
|
|
domain: str
|
|
kb_scope_primary: list[str]
|
|
voice_summary: str
|
|
voice_definition: str
|
|
domain_expertise: str
|
|
learnings_file: str
|
|
opsec_additional_patterns: list[str] = field(default_factory=list)
|
|
response_model: str = "anthropic/claude-opus-4-6"
|
|
triage_model: str = "anthropic/claude-haiku-4.5"
|
|
max_tokens: int = 1024
|
|
max_response_per_user_per_hour: int = 30
|
|
|
|
def to_dict(self) -> dict:
|
|
"""Convert to dict for passing to build_system_prompt."""
|
|
return {
|
|
"name": self.name,
|
|
"handle": self.handle,
|
|
"x_handle": self.x_handle,
|
|
"domain": self.domain,
|
|
"voice_definition": self.voice_definition,
|
|
"voice_summary": self.voice_summary,
|
|
"domain_expertise": self.domain_expertise,
|
|
"pentagon_agent_id": self.pentagon_agent_id,
|
|
}
|
|
|
|
@property
|
|
def bot_token_path(self) -> str:
|
|
return os.path.join(SECRETS_DIR, self.bot_token_file)
|
|
|
|
@property
|
|
def learnings_path(self) -> str:
|
|
return os.path.join(WORKTREE_DIR, self.learnings_file)
|
|
|
|
@property
|
|
def handle_regex(self) -> re.Pattern:
|
|
"""Regex matching this agent's @handle with optional @botname suffix."""
|
|
clean = self.handle.lstrip("@")
|
|
return re.compile(rf"@{re.escape(clean)}(?:@\w+)?", re.IGNORECASE)
|
|
|
|
|
|
def load_agent_config(config_path: str) -> AgentConfig:
|
|
"""Load and validate an agent YAML config file.
|
|
|
|
Raises ValueError on validation failure.
|
|
"""
|
|
import yaml
|
|
|
|
with open(config_path) as f:
|
|
raw = yaml.safe_load(f)
|
|
|
|
errors = []
|
|
|
|
# Required fields
|
|
for fld in REQUIRED_FIELDS + REQUIRED_VOICE_FIELDS:
|
|
if fld not in raw or not raw[fld]:
|
|
errors.append(f"Missing required field: {fld}")
|
|
|
|
# KB scope
|
|
kb_scope = raw.get("kb_scope", {})
|
|
if not isinstance(kb_scope, dict) or "primary" not in kb_scope:
|
|
errors.append("Missing kb_scope.primary (list of primary domain dirs)")
|
|
elif not isinstance(kb_scope["primary"], list) or len(kb_scope["primary"]) == 0:
|
|
errors.append("kb_scope.primary must be a non-empty list")
|
|
|
|
# Learnings file
|
|
if "learnings_file" not in raw:
|
|
errors.append("Missing required field: learnings_file")
|
|
|
|
if errors:
|
|
raise ValueError(
|
|
f"Agent config validation failed ({config_path}):\n"
|
|
+ "\n".join(f" - {e}" for e in errors)
|
|
)
|
|
|
|
return AgentConfig(
|
|
name=raw["name"],
|
|
handle=raw["handle"],
|
|
x_handle=raw.get("x_handle"),
|
|
bot_token_file=raw["bot_token_file"],
|
|
pentagon_agent_id=raw["pentagon_agent_id"],
|
|
domain=raw["domain"],
|
|
kb_scope_primary=kb_scope["primary"],
|
|
voice_summary=raw["voice_summary"],
|
|
voice_definition=raw["voice_definition"],
|
|
domain_expertise=raw.get("domain_expertise", ""),
|
|
learnings_file=raw["learnings_file"],
|
|
opsec_additional_patterns=raw.get("opsec_additional_patterns", []),
|
|
response_model=raw.get("response_model", "anthropic/claude-opus-4-6"),
|
|
triage_model=raw.get("triage_model", "anthropic/claude-haiku-4.5"),
|
|
max_tokens=raw.get("max_tokens", 1024),
|
|
max_response_per_user_per_hour=raw.get("max_response_per_user_per_hour", 30),
|
|
)
|
|
|
|
|
|
def validate_agent_config(config_path: str) -> list[str]:
|
|
"""Validate config file and check runtime dependencies.
|
|
|
|
Returns list of warnings (empty = all good).
|
|
Raises ValueError on hard failures.
|
|
"""
|
|
config = load_agent_config(config_path)
|
|
warnings = []
|
|
|
|
# Check bot token file exists
|
|
if not os.path.exists(config.bot_token_path):
|
|
warnings.append(f"Bot token file not found: {config.bot_token_path}")
|
|
|
|
# Check primary KB dirs exist
|
|
for d in config.kb_scope_primary:
|
|
full = os.path.join(WORKTREE_DIR, d)
|
|
if not os.path.isdir(full):
|
|
warnings.append(f"KB scope dir not found: {full}")
|
|
|
|
# Check learnings file parent dir exists
|
|
learnings_dir = os.path.dirname(config.learnings_path)
|
|
if not os.path.isdir(learnings_dir):
|
|
warnings.append(f"Learnings dir not found: {learnings_dir}")
|
|
|
|
# Validate OPSEC patterns compile
|
|
for i, pattern in enumerate(config.opsec_additional_patterns):
|
|
try:
|
|
re.compile(pattern, re.IGNORECASE)
|
|
except re.error as e:
|
|
warnings.append(f"Invalid OPSEC regex pattern [{i}]: {e}")
|
|
|
|
return warnings
|