feat: PromptRegistry 注册/版本/渲染(jinja2)

This commit is contained in:
lhl
2026-08-09 08:03:17 +08:00
parent 71d38b90bf
commit 59c1ea7abe
3 changed files with 91 additions and 0 deletions
+1
View File
@@ -44,3 +44,4 @@
| 2026-08-09 下午 | Agent 实现 | 里程碑3.1 实施计划生成(writing-plans):5 任务 TDD 计划落盘 docs/superpowers/plans/2026-08-09-inference-engine.md——Task1 types+exceptions+依赖(httpx/jinja2)、Task2 token(approximate/tiktoken可选)、Task3 PromptRegistry(jinja2)、Task4 HttpLLMClient(重试/退避/超时/鉴权)、Task5 InferenceEngine(chat/chat_structured全流程+注入Fake+文档补丁);修正 helper 导入、方法名(_call)、结构化状态语义(重试成功仍ok) | docs/superpowers/plans/2026-08-09-inference-engine.md, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
| 2026-08-09 05:53 | Agent 实现 | 里程碑3.1 Task1 实现:推理引擎数据模型与异常层。新建 src/genesis/inference/__init__/types/exceptionsTokenUsage、ChatMessage、ChatResult、StructuredResult、Prompt 五 dataclass 与 LLMError 体系五异常,__init__ 暂不导入 engine/client 防循环);pyproject.toml 增加 httpx>=0.28/jinja2>=3.1 依赖;tests/test_inference_types.py6 用例)与 tests/test_inference_errors.py2 用例);TDD 验证 REDModuleNotFoundError: No module named 'genesis.inference')→ GREEN(聚焦 8 passed);pytest 全量 79 passed 覆盖 100.00%608 stmts/140 br),fail_under=99 达标;提交见 git log | src/genesis/inference/__init__.py, src/genesis/inference/types.py, src/genesis/inference/exceptions.py, pyproject.toml, tests/test_inference_types.py, tests/test_inference_errors.py, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
| 2026-08-09 05:57 | Agent 实现 | 里程碑3.1 Task2 实现:推理引擎 token 估算模块。新建 src/genesis/inference/token.pyapproximate_token_count 每 4 字符≈1 token 最少 1、_tiktoken_estimator tiktoken 编码未安装返回 None、make_estimator backend="tiktoken" 默认优先 tiktoken 缺失回落 approximate);tests/test_inference_token.py 按 brief 5 用例 + 补充 2 用例(假 tiktoken 成功路径、encoding 抛异常回落)覆盖 tiktoken 成功/异常两分支,保证全量覆盖不回落;TDD 验证 REDModuleNotFoundError: No module named 'genesis.inference.token')→ 聚焦 7 passedtoken.py 100%pytest 全量 86 passed 覆盖 100.00%626 stmts/142 br);提交见 git log | src/genesis/inference/token.py, tests/test_inference_token.py, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
| 2026-08-09 08:02 | Agent 实现 | 里程碑3.1 Task3 实现:Prompt 注册表。新建 src/genesis/inference/prompt_registry.pyPromptRegistryregister/get/list_versions/renderversion=None 取最新,jinja2 渲染,name/name@version 缺失抛 KeyError);tests/test_inference_prompt_registry.py 按 brief 5 用例 + 补 1 用例(指定不存在的 version 抛 KeyError,覆盖 get 中 key not in templates 分支);TDD 验证 REDModuleNotFoundError: No module named genesis.inference.prompt_registry)→ GREEN(聚焦 6 passed);pytest 全量 92 passed 覆盖 100.00%653 stmts/150 br),fail_under=99 达标 | src/genesis/inference/prompt_registry.py, tests/test_inference_prompt_registry.py, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
+48
View File
@@ -0,0 +1,48 @@
from __future__ import annotations
from typing import Any
from jinja2 import Template
from .types import Prompt
class PromptRegistry:
"""Prompt 模板库:注册/取用/版本管理/渲染(集中管理待迁移 prompts/ 目录)。"""
def __init__(self) -> None:
self._templates: dict[tuple[str, str], str] = {}
def register(self, name: str, version: str, template: str) -> None:
"""注册(或覆盖)一个版本的模板。"""
self._templates[(name, version)] = template
def get(
self,
name: str,
version: str | None = None,
variables: dict[str, Any] | None = None,
) -> str:
"""取模板;version=None 返回该 name 最新注册版本;variables 非空时渲染。"""
if version is None:
versions = self.list_versions(name)
if not versions:
raise KeyError(f"prompt not found: {name}")
version = versions[-1]
key = (name, version)
if key not in self._templates:
raise KeyError(f"prompt version not found: {name}@{version}")
template = self._templates[key]
if variables:
return self.render(template, variables)
return template
def list_versions(self, name: str) -> list[str]:
"""返回某 name 的已注册版本(按注册顺序)。"""
return [v for (n, v) in self._templates if n == name]
def render(self, template: str, variables: dict[str, Any]) -> str:
"""用 jinja2 渲染模板。"""
from jinja2 import Template
return Template(template).render(**variables)
+42
View File
@@ -0,0 +1,42 @@
import pytest
from genesis.inference.prompt_registry import PromptRegistry
from genesis.inference.types import Prompt
def test_register_and_render():
reg = PromptRegistry()
reg.register("writer", "v1", "按规则撰写:{{ chapter }}")
assert reg.get("writer", "v1") == "按规则撰写:{{ chapter }}"
def test_get_with_variables_renders():
reg = PromptRegistry()
reg.register("writer", "v1", "按规则撰写:{{ chapter }}")
assert reg.get("writer", "v1", {"chapter": "帳票設計"}) == "按规则撰写:帳票設計"
def test_get_latest_version():
reg = PromptRegistry()
reg.register("writer", "v1", "t1")
reg.register("writer", "v2", "t2")
assert reg.get("writer") == "t2"
assert reg.list_versions("writer") == ["v1", "v2"]
def test_get_missing_raises_keyerror():
reg = PromptRegistry()
with pytest.raises(KeyError):
reg.get("dne")
def test_get_missing_version_raises_keyerror():
reg = PromptRegistry()
reg.register("writer", "v1", "t1")
with pytest.raises(KeyError):
reg.get("writer", "v9")
def test_render_raw():
reg = PromptRegistry()
assert reg.render("{{ a }} と {{ b }}", {"a": "x", "b": 1}) == "x と 1"