"""多智能体角色存储与解析。 角色定义文件格式(Markdown + Frontmatter): --- id: ui-operator name: UI Operator description: 界面设计与前端实现 model: "" thinking_mode: fast skills: - frontend-design --- 自定义 prompt body... 角色目录: - ~/.astrion/astrion/host/mutiagents/agents/.md 存储约定见 .astrion/memory/multi_agent_mode_design.md。 """ from __future__ import annotations import json import re from pathlib import Path from typing import Any, Dict, List, Optional try: from config.paths import IS_HOST_MODE, RUNTIME_ROOT except ImportError: # 防止直接 import 时 config 尚未加载,提供回退值 IS_HOST_MODE = True RUNTIME_ROOT = str(Path.home() / ".astrion" / "astrion") # 复用项目的 MUTIAGENTS 数据目录(保留原拼写 "mutiagents",见项目记忆) DEFAULT_MUTIAGENTS_DIR = Path(RUNTIME_ROOT) / ("host" if IS_HOST_MODE else "web") / "mutiagents" AGENTS_DIR_NAME = "agents" FRONTMATTER_RE = re.compile(r"^---\s*\n(?P.*?)\n---\s*\n(?P.*)$", re.S) def _agents_dir() -> Path: """返回角色根目录(按运行模式自动选择 host/web)。""" base = Path(DEFAULT_MUTIAGENTS_DIR) out = base / AGENTS_DIR_NAME out.mkdir(parents=True, exist_ok=True) return out def _parse_frontmatter(text: str) -> tuple[Dict[str, Any], str]: """解析 Markdown frontmatter,返回 (元数据 dict, body)。""" m = FRONTMATTER_RE.match(text) if not m: return {}, text raw_meta = m.group("body") or "" body = m.group("rest") or "" meta: Dict[str, Any] = {} # 简易解析(k: v 与 k: 列表项,不引入 yaml 依赖) current_key: Optional[str] = None for line in raw_meta.splitlines(): if not line.strip(): continue if line.startswith(" - ") or line.startswith("- "): value = line.lstrip(" ").lstrip("- ").strip() if current_key and isinstance(meta.get(current_key), list): meta[current_key].append(value) continue if ":" in line: k, v = line.split(":", 1) k = k.strip() v = v.strip().strip('"').strip("'") if v: meta[k] = v current_key = None else: # 可能是列表块 meta[k] = [] current_key = k return meta, body.strip() def load_role_from_file(file_path: Path) -> Optional["RoleConfig"]: """从 Markdown 文件加载角色定义。""" if not file_path.exists() or not file_path.is_file(): return None text = file_path.read_text(encoding="utf-8") meta, body = _parse_frontmatter(text) if not meta.get("id") or not meta.get("name"): return None return RoleConfig( role_id=str(meta["id"]), name=str(meta["name"]), description=str(meta.get("description") or ""), body_prompt=body, model_key=(str(meta["model"]) if meta.get("model") else None) or None, thinking_mode=str(meta.get("thinking_mode") or "fast"), skills=list(meta.get("skills") or []), source_file=str(file_path), ) def load_preset_role(role_id: str) -> Optional["RoleConfig"]: """从预设/自定义角色目录加载一个角色。""" f = _agents_dir() / f"{role_id}.md" return load_role_from_file(f) def list_roles() -> List["RoleConfig"]: """列出全部角色(预设 + 用户自定义)。""" agents_dir = _agents_dir() if not agents_dir.exists(): return [] roles: List[RoleConfig] = [] for p in sorted(agents_dir.glob("*.md")): r = load_role_from_file(p) if r and r.role_id: roles.append(r) return roles def save_custom_role(role: "RoleConfig") -> Path: """把自定义角色保存到 agents 目录。""" f = _agents_dir() / f"{role.role_id}.md" f.parent.mkdir(parents=True, exist_ok=True) f.write_text(_serialize_role(role), encoding="utf-8") return f def _serialize_role(role: "RoleConfig") -> str: """把 RoleConfig 序列化为 Markdown frontmatter 字符串。""" lines = ["---"] lines.append(f"id: {role.role_id}") lines.append(f"name: {role.name}") if role.description: # 单行描述转义 desc = role.description.replace('"', '\\"') lines.append(f'description: "{desc}"') if role.model_key: lines.append(f"model: {role.model_key}") lines.append(f"thinking_mode: {role.thinking_mode or 'fast'}") if role.skills: lines.append("skills:") for s in role.skills: lines.append(f" - {s}") lines.append("---") lines.append("") lines.append(role.body_prompt or "") return "\n".join(lines) def build_role_system_prompt(role: "RoleConfig") -> str: """生成子智能体的「专属 prompt」部分(frontmatter 之后的 body)。""" return role.body_prompt or "" class RoleConfig: """一个多智能体角色配置。""" __slots__ = ( "role_id", "name", "description", "body_prompt", "model_key", "thinking_mode", "skills", "source_file", ) def __init__( self, *, role_id: str, name: str, description: str = "", body_prompt: str = "", model_key: Optional[str] = None, thinking_mode: str = "fast", skills: Optional[List[str]] = None, source_file: str = "", ): self.role_id = role_id self.name = name self.description = description self.body_prompt = body_prompt self.model_key = model_key self.thinking_mode = thinking_mode or "fast" self.skills = list(skills or []) self.source_file = source_file def display_name(self, agent_id: int) -> str: """根据实例 id 生成显示名。""" return f"{self.name}_{agent_id}" def to_dict(self) -> Dict[str, Any]: return { "role_id": self.role_id, "name": self.name, "description": self.description, "model_key": self.model_key, "thinking_mode": self.thinking_mode, "skills": self.skills, "source_file": self.source_file, } def __repr__(self) -> str: return f""