registries.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. """Prompt、Skill 与 Persona 注册中心。"""
  2. from dataclasses import dataclass
  3. from pathlib import Path
  4. from app.core.errors import AppError
  5. ASSET_ROOT = Path(__file__).resolve().parent / "assets"
  6. @dataclass(frozen=True)
  7. class SkillDefinition:
  8. id: str
  9. name: str
  10. personas: tuple[str, ...]
  11. tools: tuple[str, ...]
  12. instructions: str
  13. source: Path
  14. @dataclass(frozen=True)
  15. class PersonaDefinition:
  16. id: str
  17. name: str
  18. prompt_id: str
  19. skill_ids: tuple[str, ...]
  20. tool_names: tuple[str, ...]
  21. class PromptRegistry:
  22. def __init__(self, root: Path | None = None) -> None:
  23. self._root = root or ASSET_ROOT / "prompts"
  24. self._items = {
  25. path.stem: path.read_text(encoding="utf-8").strip() for path in self._root.glob("*.md")
  26. }
  27. def get(self, prompt_id: str) -> str:
  28. prompt = self._items.get(prompt_id)
  29. if prompt is None:
  30. raise AppError(
  31. "HARNESS_PROMPT_NOT_FOUND",
  32. f"未找到Prompt:{prompt_id}",
  33. 500,
  34. )
  35. return prompt
  36. def ids(self) -> tuple[str, ...]:
  37. return tuple(sorted(self._items))
  38. class SkillRegistry:
  39. """发现标准 ``SKILL.md``,并解析本项目使用的最小前置元数据。"""
  40. def __init__(self, root: Path | None = None) -> None:
  41. self._root = root or ASSET_ROOT / "skills"
  42. self._items: dict[str, SkillDefinition] = {}
  43. for path in self._root.glob("*/SKILL.md"):
  44. skill = self._parse(path)
  45. if skill.id in self._items:
  46. raise AppError(
  47. "HARNESS_SKILL_DUPLICATED",
  48. f"Skill标识重复:{skill.id}",
  49. 500,
  50. )
  51. self._items[skill.id] = skill
  52. def get(self, skill_id: str) -> SkillDefinition:
  53. skill = self._items.get(skill_id)
  54. if skill is None:
  55. raise AppError(
  56. "HARNESS_SKILL_NOT_FOUND",
  57. f"未找到Skill:{skill_id}",
  58. 500,
  59. )
  60. return skill
  61. def ids(self) -> tuple[str, ...]:
  62. return tuple(sorted(self._items))
  63. @staticmethod
  64. def _parse(path: Path) -> SkillDefinition:
  65. raw = path.read_text(encoding="utf-8").strip()
  66. if not raw.startswith("---\n"):
  67. raise AppError(
  68. "HARNESS_SKILL_INVALID",
  69. f"Skill缺少元数据:{path.parent.name}",
  70. 500,
  71. )
  72. _, metadata_text, instructions = raw.split("---", maxsplit=2)
  73. metadata: dict[str, str] = {}
  74. for line in metadata_text.strip().splitlines():
  75. key, separator, value = line.partition(":")
  76. if separator:
  77. metadata[key.strip()] = value.strip()
  78. required = {"id", "name", "personas", "tools"}
  79. if not required.issubset(metadata):
  80. raise AppError(
  81. "HARNESS_SKILL_INVALID",
  82. f"Skill元数据不完整:{path.parent.name}",
  83. 500,
  84. )
  85. return SkillDefinition(
  86. id=metadata["id"],
  87. name=metadata["name"],
  88. personas=tuple(_csv(metadata["personas"])),
  89. tools=tuple(_csv(metadata["tools"])),
  90. instructions=instructions.strip(),
  91. source=path,
  92. )
  93. class PersonaRegistry:
  94. def __init__(self) -> None:
  95. self._items = {
  96. "customer": PersonaDefinition(
  97. id="customer",
  98. name="客户保障顾问",
  99. prompt_id="customer",
  100. skill_ids=("insurance-consultation", "enrollment-guidance"),
  101. tool_names=(
  102. "list_available_products",
  103. "calculate_insurance_quote",
  104. "prepare_enrollment",
  105. "list_my_orders",
  106. "list_my_policies",
  107. ),
  108. ),
  109. "operation": PersonaDefinition(
  110. id="operation",
  111. name="运营数据助手",
  112. prompt_id="operation",
  113. skill_ids=("operation-insights",),
  114. tool_names=(
  115. "get_operation_overview",
  116. "list_recent_orders",
  117. "list_recent_policies",
  118. "get_attribution_performance",
  119. "list_available_products",
  120. ),
  121. ),
  122. }
  123. def get(self, persona_id: str) -> PersonaDefinition:
  124. persona = self._items.get(persona_id)
  125. if persona is None:
  126. raise AppError(
  127. "HARNESS_PERSONA_NOT_FOUND",
  128. f"未找到Agent人格:{persona_id}",
  129. 500,
  130. )
  131. return persona
  132. def _csv(value: str) -> list[str]:
  133. return [item.strip() for item in value.split(",") if item.strip()]