Files
deerflow-code/offline-backend-20260512/backend/app/gateway/routers/_strategy_selection_seed.py
T

208 lines
7.1 KiB
Python

"""Seed the built-in functional agents used by strategy selection.
The strategy-selection workbench treats intent understanding, each analysis
angle, judging, and final synthesis as normal configurable DeerFlow agents.
Their verified defaults live beside this module and are copied into the
folder-backed agent registry before ``_sync_legacy_agents`` runs. Existing
directories are preserved, except for an exact known legacy default whose
content hash proves that an administrator has not customized it.
"""
from __future__ import annotations
import logging
import os
from collections.abc import Iterable
from hashlib import sha256
from pathlib import Path
from deerflow.config.paths import get_paths
logger = logging.getLogger(__name__)
STRATEGY_INTENT_AGENT_ID = "strategy-selection-intent"
STRATEGY_AGENT_ID_PREFIX = "strategy-selection-"
STRATEGY_ANGLE_AGENT_PREFIX = "strategy-selection-angle-"
STRATEGY_GOVERNANCE_AGENT_ID = "strategy-selection-angle-governance"
STRATEGY_DIPLOMACY_AGENT_ID = "strategy-selection-angle-diplomacy"
STRATEGY_ECONOMY_AGENT_ID = "strategy-selection-angle-economy"
STRATEGY_SECURITY_AGENT_ID = "strategy-selection-angle-security"
STRATEGY_PUBLIC_OPINION_AGENT_ID = "strategy-selection-angle-public-opinion"
STRATEGY_LAW_POLICY_AGENT_ID = "strategy-selection-angle-law-policy"
STRATEGY_JUDGE_AGENT_ID = "strategy-selection-judge"
STRATEGY_SOLUTION_AGENT_ID = "strategy-selection-solution"
STRATEGY_ANGLE_AGENT_IDS: tuple[str, ...] = (
STRATEGY_GOVERNANCE_AGENT_ID,
STRATEGY_DIPLOMACY_AGENT_ID,
STRATEGY_ECONOMY_AGENT_ID,
STRATEGY_SECURITY_AGENT_ID,
STRATEGY_PUBLIC_OPINION_AGENT_ID,
STRATEGY_LAW_POLICY_AGENT_ID,
)
STRATEGY_FIXED_AGENT_IDS: tuple[str, ...] = (
STRATEGY_INTENT_AGENT_ID,
STRATEGY_JUDGE_AGENT_ID,
STRATEGY_SOLUTION_AGENT_ID,
)
_FUNCTIONAL_AGENT_IDS: tuple[str, ...] = (
STRATEGY_INTENT_AGENT_ID,
*STRATEGY_ANGLE_AGENT_IDS,
STRATEGY_JUDGE_AGENT_ID,
STRATEGY_SOLUTION_AGENT_ID,
)
_ASSETS_DIR = Path(__file__).parent / "_strategy_selection_seed_assets"
_SEED_FILES: tuple[str, ...] = ("config.yaml", "SOUL.md")
# The first shipped intent SOUL embedded the six built-in angle ids. Upgrade
# only that byte-for-byte semantic default so existing installations adopt the
# administrator-driven angle catalog without overwriting customized SOUL.
_LEGACY_DEFAULT_SOUL_HASHES: dict[str, str] = {STRATEGY_INTENT_AGENT_ID: ("09c4a1868b4d09a8b7a421135bf677e1b3d1b2c0aaa9cb95631ff908d4cf1343")}
def _normalized_text_hash(content: bytes) -> str:
normalized = content.decode("utf-8").replace("\r\n", "\n").encode("utf-8")
return sha256(normalized).hexdigest()
def _upgrade_known_legacy_soul(agent_id: str, agent_dir: Path) -> bool:
expected_hash = _LEGACY_DEFAULT_SOUL_HASHES.get(agent_id)
target = agent_dir / "SOUL.md"
source = _ASSETS_DIR / agent_id / "SOUL.md"
if not expected_hash or not target.is_file() or not source.is_file():
return False
temporary: Path | None = None
try:
if _normalized_text_hash(target.read_bytes()) != expected_hash:
return False
replacement = source.read_bytes()
if _normalized_text_hash(replacement) == expected_hash:
return False
temporary = target.with_name(f".{target.name}.{os.getpid()}.seed-upgrade.tmp")
temporary.write_bytes(replacement)
os.replace(temporary, target)
logger.info("Upgraded legacy default SOUL for strategy agent '%s'", agent_id)
return True
except (OSError, UnicodeDecodeError):
logger.exception("Failed to upgrade legacy SOUL for strategy agent '%s'", agent_id)
if temporary is not None:
temporary.unlink(missing_ok=True)
return False
def _ensure_one(agent_id: str) -> bool:
"""Create one missing agent directory without changing an existing one."""
agent_dir = get_paths().agent_dir(agent_id)
if agent_dir.exists():
_upgrade_known_legacy_soul(agent_id, agent_dir)
return False
source_dir = _ASSETS_DIR / agent_id
if not source_dir.is_dir():
logger.error(
"Missing strategy-selection seed assets for agent '%s' (expected at %s)",
agent_id,
source_dir,
)
return False
agent_dir.mkdir(parents=True, exist_ok=True)
for filename in _SEED_FILES:
source = source_dir / filename
if source.exists():
(agent_dir / filename).write_bytes(source.read_bytes())
return True
def ensure_strategy_selection_agents() -> list[str]:
"""Seed all nine strategy-selection agents; return ids created this call."""
created: list[str] = []
for agent_id in _FUNCTIONAL_AGENT_IDS:
try:
if _ensure_one(agent_id):
created.append(agent_id)
except Exception:
logger.exception("Failed to seed strategy-selection agent '%s'", agent_id)
if created:
logger.info("Seeded missing strategy-selection agents: %s", ", ".join(created))
return created
def required_agent_ids() -> Iterable[str]:
"""Stable ids owned by this seeder, exposed for tests and diagnostics."""
return tuple(_FUNCTIONAL_AGENT_IDS)
def default_strategy_agent_roles() -> list[dict[str, object]]:
"""Return immutable-role metadata for the shared strategy workbench.
The registry is deliberately separate from the folder-backed agent record:
it owns role/category and lifecycle state, while model, SOUL and skills keep
using the common agent configuration path. ``ensure_defaults`` only
inserts missing rows, so an administrator disabling an angle is respected.
"""
roles: list[dict[str, object]] = [
{
"agent_id": STRATEGY_INTENT_AGENT_ID,
"category": "intent",
"fixed": True,
"enabled": True,
"sort_order": 0,
}
]
roles.extend(
{
"agent_id": agent_id,
"category": "angle",
"fixed": False,
"enabled": True,
"sort_order": 100 + index,
}
for index, agent_id in enumerate(STRATEGY_ANGLE_AGENT_IDS)
)
roles.extend(
(
{
"agent_id": STRATEGY_JUDGE_AGENT_ID,
"category": "judge",
"fixed": True,
"enabled": True,
"sort_order": 200,
},
{
"agent_id": STRATEGY_SOLUTION_AGENT_ID,
"category": "solution",
"fixed": True,
"enabled": True,
"sort_order": 300,
},
)
)
return roles
__all__ = [
"STRATEGY_AGENT_ID_PREFIX",
"STRATEGY_ANGLE_AGENT_IDS",
"STRATEGY_ANGLE_AGENT_PREFIX",
"STRATEGY_DIPLOMACY_AGENT_ID",
"STRATEGY_ECONOMY_AGENT_ID",
"STRATEGY_FIXED_AGENT_IDS",
"STRATEGY_GOVERNANCE_AGENT_ID",
"STRATEGY_INTENT_AGENT_ID",
"STRATEGY_JUDGE_AGENT_ID",
"STRATEGY_LAW_POLICY_AGENT_ID",
"STRATEGY_PUBLIC_OPINION_AGENT_ID",
"STRATEGY_SECURITY_AGENT_ID",
"STRATEGY_SOLUTION_AGENT_ID",
"default_strategy_agent_roles",
"ensure_strategy_selection_agents",
"required_agent_ids",
]