# -*- coding: utf-8 -*- """PromptBuilder for constructing system prompts from workspace markdown files. Based on CoPaw design - loads AGENTS.md, SOUL.md, PROFILE.md, etc. from agent workspace directories with YAML frontmatter support. """ from __future__ import annotations import logging from pathlib import Path from typing import Any, Dict, List, Optional import yaml logger = logging.getLogger(__name__) DEFAULT_SYS_PROMPT = """You are a helpful trading analysis assistant.""" class PromptBuilder: """Builder for constructing system prompts from markdown files. Loads markdown configuration files from agent workspace directories, supporting YAML frontmatter for metadata extraction. """ DEFAULT_FILES = [ "AGENTS.md", "SOUL.md", "PROFILE.md", "ROLE.md", "POLICY.md", "MEMORY.md", "HEARTBEAT.md", "STYLE.md", ] TITLE_MAP: Dict[str, str] = { "AGENTS.md": "Agent Guide", "SOUL.md": "Soul", "PROFILE.md": "Profile", "ROLE.md": "Role", "POLICY.md": "Policy", "MEMORY.md": "Memory", "HEARTBEAT.md": "Heartbeat", "STYLE.md": "Style", "BOOTSTRAP.md": "Bootstrap", } def __init__( self, workspace_dir: Path, enabled_files: Optional[List[str]] = None, ): """Initialize prompt builder. Args: workspace_dir: Directory containing markdown configuration files enabled_files: List of filenames to load (if None, uses defaults) """ self.workspace_dir = Path(workspace_dir) self.enabled_files = enabled_files or self.DEFAULT_FILES.copy() self._prompt_parts: List[str] = [] self._metadata: Dict[str, Any] = {} self.loaded_count = 0 def _load_file(self, filename: str) -> tuple[str, Optional[Dict[str, Any]]]: """Load a single markdown file with YAML frontmatter support. Args: filename: Name of the file to load Returns: Tuple of (content, metadata dict or None) """ file_path = self.workspace_dir / filename if not file_path.exists(): logger.debug("File %s not found in %s, skipping", filename, self.workspace_dir) return "", None try: raw_content = file_path.read_text(encoding="utf-8").strip() if not raw_content: logger.debug("Skipped empty file: %s", filename) return "", None content, metadata = self._parse_frontmatter(raw_content) if content: self.loaded_count += 1 logger.debug("Loaded %s (metadata: %s)", filename, bool(metadata)) return content, metadata except Exception as e: logger.warning("Failed to read file %s: %s, skipping", filename, e) return "", None def _parse_frontmatter(self, raw_content: str) -> tuple[str, Optional[Dict[str, Any]]]: """Parse YAML frontmatter from markdown content. Args: raw_content: Raw file content Returns: Tuple of (content without frontmatter, metadata dict or None) """ if not raw_content.startswith("---"): return raw_content, None parts = raw_content.split("---", 2) if len(parts) < 3: return raw_content, None frontmatter = parts[1].strip() content = parts[2].strip() try: metadata = yaml.safe_load(frontmatter) or {} if not isinstance(metadata, dict): metadata = {} return content, metadata except yaml.YAMLError as e: logger.warning("Failed to parse YAML frontmatter: %s", e) return content, None def _append_section(self, title: str, content: str) -> None: """Append a section to the prompt parts. Args: title: Section title content: Section content """ content = content.strip() if not content: return if self._prompt_parts: self._prompt_parts.append("") self._prompt_parts.append(f"## {title}") self._prompt_parts.append("") self._prompt_parts.append(content) def build(self) -> str: """Build the system prompt from markdown files. Returns: Constructed system prompt string """ self._prompt_parts = [] self._metadata = {} self.loaded_count = 0 for filename in self.enabled_files: content, metadata = self._load_file(filename) if metadata: self._metadata[filename] = metadata if content: title = self.TITLE_MAP.get(filename, filename.replace(".md", "")) self._append_section(title, content) if not self._prompt_parts: logger.warning("No content loaded from workspace: %s", self.workspace_dir) return DEFAULT_SYS_PROMPT final_prompt = "\n".join(self._prompt_parts) logger.debug( "System prompt built from %d file(s), total length: %d chars", self.loaded_count, len(final_prompt), ) return final_prompt def get_metadata(self) -> Dict[str, Any]: """Get metadata collected from YAML frontmatter. Returns: Dictionary mapping filenames to their metadata """ return self._metadata.copy() def get_agent_identity(self) -> Optional[Dict[str, Any]]: """Extract agent identity from PROFILE.md metadata. Returns: Identity dict with name, role, etc. or None """ profile_meta = self._metadata.get("PROFILE.md", {}) if not profile_meta: return None return { "name": profile_meta.get("name", "Unknown"), "role": profile_meta.get("role", ""), "expertise": profile_meta.get("expertise", []), "style": profile_meta.get("style", ""), } def build_system_prompt_from_workspace( workspace_dir: Path, enabled_files: Optional[List[str]] = None, agent_id: Optional[str] = None, extra_context: Optional[str] = None, ) -> str: """Build system prompt from workspace markdown files. This is the main entry point for building system prompts from agent workspace directories. Args: workspace_dir: Directory containing markdown configuration files enabled_files: List of filenames to load (if None, uses defaults) agent_id: Agent identifier to include in system prompt extra_context: Additional context to append to the prompt Returns: Constructed system prompt string """ builder = PromptBuilder( workspace_dir=workspace_dir, enabled_files=enabled_files, ) prompt = builder.build() # Add agent identity header if agent_id provided if agent_id and agent_id != "default": identity_header = ( f"# Agent Identity\n\n" f"Your agent ID is `{agent_id}`. " f"This is your unique identifier in the multi-agent system.\n\n" ) prompt = identity_header + prompt # Append extra context if provided if extra_context: prompt = prompt + "\n\n" + extra_context return prompt def build_bootstrap_guidance(language: str = "zh") -> str: """Build bootstrap guidance message for first-time setup. Args: language: Language code (zh/en) Returns: Formatted bootstrap guidance message """ if language == "zh": return ( "# 引导模式\n" "\n" "工作目录中存在 `BOOTSTRAP.md` — 首次设置。\n" "\n" "1. 阅读 BOOTSTRAP.md,友好地表示初次见面," "引导用户完成设置。\n" "2. 按照 BOOTSTRAP.md 的指示," "帮助用户定义你的身份和偏好。\n" "3. 按指南创建/更新必要文件" "(PROFILE.md、MEMORY.md 等)。\n" "4. 完成后删除 BOOTSTRAP.md。\n" "\n" "如果用户希望跳过,直接回答下面的问题即可。\n" "\n" "---\n" "\n" ) return ( "# BOOTSTRAP MODE\n" "\n" "`BOOTSTRAP.md` exists — first-time setup.\n" "\n" "1. Read BOOTSTRAP.md, greet the user, " "and guide them through setup.\n" "2. Follow BOOTSTRAP.md instructions " "to define identity and preferences.\n" "3. Create/update files " "(PROFILE.md, MEMORY.md, etc.) as described.\n" "4. Delete BOOTSTRAP.md when done.\n" "\n" "If the user wants to skip, answer their " "question directly instead.\n" "\n" "---\n" "\n" ) __all__ = [ "PromptBuilder", "build_system_prompt_from_workspace", "build_bootstrap_guidance", "DEFAULT_SYS_PROMPT", ]