""" Agentic QE Fleet — Agent 加载和上下文注入模块 负责: 1. 加载 Agent prompt 文件 2. 注入战区上下文(前序 manifest 数据) 3. 渲染最终可执行的 Agent prompt """ from __future__ import annotations import re from pathlib import Path from typing import Any REPO_ROOT = Path(__file__).resolve().parent.parent AGENTS_DIR = REPO_ROOT / "agents" # Agent 注册表: agent_id → (战区, 文件路径, 描述) AGENT_REGISTRY: dict[str, dict[str, Any]] = { # ── Prepare ── "document-parser": { "zone": "prepare", "path": "prepare/document_parser.md", "name": "文档解析专家", "description": "解析原始需求文档为标准 Markdown,自动识别技术方案", }, "knowledge-activator": { "zone": "prepare", "path": "prepare/knowledge_activator.md", "name": "知识激活专家", "description": "按需求内容自动激活术语/规则/历史缺陷/最佳实践", }, # ── Analyze ── "requirement-analyzer": { "zone": "analyze", "path": "analyze/requirement_analyzer.md", "name": "需求分析专家", "description": "结构化需求模型 + 歧义标注", }, "conflict-detector": { "zone": "analyze", "path": "analyze/conflict_detector.md", "name": "冲突检测专家", "description": "语义级历史需求规则冲突检测", }, "risk-assessor": { "zone": "analyze", "path": "analyze/risk_assessor.md", "name": "风险评估专家", "description": "多维度风险矩阵量化", }, # ── Design ── "test-strategist": { "zone": "design", "path": "design/test_strategist.md", "name": "测试策略师", "description": "分层测试策略 + 优先级矩阵", }, "testpoint-designer": { "zone": "design", "path": "design/testpoint_designer.md", "name": "测试点设计师", "description": "全面测试点矩阵 + 来源标注", }, "case-designer": { "zone": "design", "path": "design/case_designer.md", "name": "用例设计师", "description": "可执行测试用例 + 双验证预期结果", }, "data-builder": { "zone": "design", "path": "design/data_builder.md", "name": "数据构造师", "description": "精确测试数据集构造", }, # ── Execute ── "web-executor": { "zone": "execute", "path": "execute/web_executor.md", "name": "Web 自动化执行师", "description": "PC Web 端 Playwright 自动化测试执行 + 截图采集", }, "mobile-executor": { "zone": "execute", "path": "execute/mobile_executor.md", "name": "移动端自动化执行师", "description": "Android/iOS APP Appium 自动化测试执行 + 截图采集", }, "result-reporter": { "zone": "execute", "path": "execute/result_reporter.md", "name": "测试结果报告师", "description": "截图对比 + AI 视觉验证 + 测试结论报告", }, # ── Review ── "case-reviewer": { "zone": "review", "path": "review/case_reviewer.md", "name": "用例评审师", "description": "用例质量/规范性/可执行性逐项评审", }, "coverage-auditor": { "zone": "review", "path": "review/coverage_auditor.md", "name": "覆盖率审计师", "description": "需求→测试点→用例三级追溯覆盖审计", }, "quality-gatekeeper": { "zone": "review", "path": "review/quality_gatekeeper.md", "name": "质量门禁裁决官", "description": "三级裁决: PASS / PASS_WITH_FIX / BLOCKED", }, # ── Monitor ── "execution-analyst": { "zone": "monitor", "path": "monitor/execution_analyst.md", "name": "执行结果分析师", "description": "失败归类 + 模式识别 + 根因推测", }, "knowledge-curator": { "zone": "monitor", "path": "monitor/knowledge_curator.md", "name": "知识沉淀师", "description": "自动回写知识库 + 去重保护", }, } def get_agent_path(agent_id: str) -> Path: """返回 Agent prompt 文件的绝对路径。""" if agent_id not in AGENT_REGISTRY: raise ValueError(f"未知 Agent: {agent_id},可用: {list(AGENT_REGISTRY)}") return AGENTS_DIR / AGENT_REGISTRY[agent_id]["path"] def load_agent_prompt(agent_id: str) -> str: """加载 Agent prompt 原始内容。""" path = get_agent_path(agent_id) if not path.exists(): raise FileNotFoundError(f"Agent prompt 不存在: {path}") return path.read_text(encoding="utf-8") def list_zone_agents(zone: str) -> list[str]: """列出指定战区的所有 Agent ID。""" return sorted([ agent_id for agent_id, info in AGENT_REGISTRY.items() if info["zone"] == zone ]) def list_all_agents() -> dict[str, list[str]]: """按战区分组列出所有 Agent。""" result: dict[str, list[str]] = {} for zone in ["prepare", "analyze", "design", "execute", "review", "monitor"]: result[zone] = list_zone_agents(zone) return result def inject_context(prompt: str, context: dict[str, Any]) -> str: """向 prompt 注入运行时上下文变量。 支持的占位符: {{BASE_NAME}} → 需求基础名 {{MANIFEST_DIR}} → manifest 目录 {{PREPARE_MANIFEST}} → prepare manifest 路径 {{ANALYZE_MANIFEST}} → analyze manifest 路径 {{DESIGN_MANIFEST}} → design manifest 路径 {{PROJECT_PROFILE}} → 项目画像路径 {{FLEET_CONFIG}} → fleet_config.yml 路径 """ replacements = { "{{BASE_NAME}}": context.get("base_name", ""), "{{MANIFEST_DIR}}": str(REPO_ROOT / "output" / "manifests"), "{{PREPARE_MANIFEST}}": str(REPO_ROOT / "output" / "manifests" / f"{context.get('base_name', '')}_prepare.json"), "{{ANALYZE_MANIFEST}}": str(REPO_ROOT / "output" / "manifests" / f"{context.get('base_name', '')}_analyze.json"), "{{DESIGN_MANIFEST}}": str(REPO_ROOT / "output" / "manifests" / f"{context.get('base_name', '')}_design.json"), "{{PROJECT_PROFILE}}": str(REPO_ROOT / "knowledge_base" / "00_project" / "project_profile.md"), "{{FLEET_CONFIG}}": str(REPO_ROOT / "fleet_config.yml"), "{{OUTPUT_DIR}}": str(REPO_ROOT / "output"), "{{KNOWLEDGE_BASE_DIR}}": str(REPO_ROOT / "knowledge_base"), "{{REQUIREMENT_FILE}}": context.get("requirement_file", ""), } result = prompt for placeholder, value in replacements.items(): result = result.replace(placeholder, value) return result def render_agent_prompt(agent_id: str, context: dict[str, Any]) -> str: """加载并渲染 Agent prompt(注入上下文)。""" raw = load_agent_prompt(agent_id) return inject_context(raw, context) def get_agent_frontmatter(agent_id: str) -> dict[str, Any]: """提取 Agent prompt 的 YAML frontmatter。""" content = load_agent_prompt(agent_id) frontmatter: dict[str, Any] = {} if content.startswith("---"): parts = content.split("---", 2) if len(parts) >= 3: for line in parts[1].strip().split("\n"): line = line.strip() if ":" in line: key, value = line.split(":", 1) frontmatter[key.strip()] = value.strip() return frontmatter def get_agent_short_description(agent_id: str) -> str: """返回 Agent 的一句话描述。""" info = AGENT_REGISTRY.get(agent_id, {}) return info.get("description", agent_id) def validate_all_agents() -> dict[str, Any]: """验证所有 Agent prompt 文件是否存在且非空。""" result: dict[str, Any] = {"valid": True, "missing": [], "empty": [], "total": len(AGENT_REGISTRY)} for agent_id in AGENT_REGISTRY: path = get_agent_path(agent_id) if not path.exists(): result["missing"].append(agent_id) result["valid"] = False elif path.stat().st_size == 0: result["empty"].append(agent_id) result["valid"] = False return result def build_agent_invocation_context(base_name: str, agent_id: str) -> dict[str, Any]: """为 AI Agent 调用构建完整的上下文注入字典。""" from fleet_manifest import ZONE_ORDER, load_manifest_safe info = AGENT_REGISTRY.get(agent_id, {}) zone = info.get("zone", "") context: dict[str, Any] = { "base_name": base_name, "agent_id": agent_id, "agent_zone": zone, "repo_root": str(REPO_ROOT), "output_dir": str(REPO_ROOT / "output"), "manifests_dir": str(REPO_ROOT / "output" / "manifests"), "knowledge_base_dir": str(REPO_ROOT / "knowledge_base"), "fleet_config_path": str(REPO_ROOT / "fleet_config.yml"), "PROJECT_PROFILE": str(REPO_ROOT / "knowledge_base" / "00_project" / "project_profile.md"), "FLEET_CONFIG": str(REPO_ROOT / "fleet_config.yml"), } for prev_zone in ZONE_ORDER: manifest = load_manifest_safe(base_name, prev_zone) if manifest is None: continue context[f"manifest_{prev_zone}"] = manifest for key, value in manifest.items(): if key.endswith("_file") or key.endswith("_files"): context[key] = value if prev_zone == zone: break return context def get_agent_input_files(base_name: str, agent_id: str) -> list[str]: """返回 Agent 需要的所有输入文件路径列表。""" from fleet_manifest import ZONE_ORDER, load_manifest_safe info = AGENT_REGISTRY.get(agent_id, {}) zone = info.get("zone", "") input_files: list[str] = [] for prev_zone in ZONE_ORDER: if prev_zone == zone: break manifest = load_manifest_safe(base_name, prev_zone) if manifest is None: continue for key in manifest: if key.endswith("_file") or key.endswith("_files"): value = manifest[key] if isinstance(value, str): input_files.append(value) elif isinstance(value, list): input_files.extend(v for v in value if isinstance(v, str)) kb_files = [ str(REPO_ROOT / "knowledge_base" / "00_project" / "project_profile.md"), str(REPO_ROOT / "knowledge_base" / "01_standards" / "test_case_template.md"), str(REPO_ROOT / "knowledge_base" / "01_standards" / "review_checklist.md"), str(REPO_ROOT / "knowledge_base" / "01_standards" / "definition_of_done.md"), str(REPO_ROOT / "fleet_config.yml"), ] input_files.extend(kb_files) return sorted(set(input_files))