feat: PromptRegistry 注册/版本/渲染(jinja2)
This commit is contained in:
@@ -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/exceptions:TokenUsage、ChatMessage、ChatResult、StructuredResult、Prompt 五 dataclass 与 LLMError 体系五异常,__init__ 暂不导入 engine/client 防循环);pyproject.toml 增加 httpx>=0.28/jinja2>=3.1 依赖;tests/test_inference_types.py(6 用例)与 tests/test_inference_errors.py(2 用例);TDD 验证 RED(ModuleNotFoundError: 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.py(approximate_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 验证 RED(ModuleNotFoundError: No module named 'genesis.inference.token')→ 聚焦 7 passed,token.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.py(PromptRegistry:register/get/list_versions/render,version=None 取最新,jinja2 渲染,name/name@version 缺失抛 KeyError);tests/test_inference_prompt_registry.py 按 brief 5 用例 + 补 1 用例(指定不存在的 version 抛 KeyError,覆盖 get 中 key not in templates 分支);TDD 验证 RED(ModuleNotFoundError: 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 |
|
||||
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user