feat: Token 估算(approximate 内置 / tiktoken 可选回落)
This commit is contained in:
+5
-1
@@ -39,4 +39,8 @@
|
|||||||
| 2026-08-09 04:40 | 反馈迭代 | 里程碑2.5 Task13 遗留清理:① extraction_method 枚举同源(free_text_extractor→ExtractionMethod.LLM_FROM_FREE_TEXT.value、table_extractor 空/非空两分支→ExtractionMethod.OPENPYXL.value,值不变既有用例兼容);② table_extractor header_row 越界/负值防御(<0 或 >=len(matrix) 回落 0,keep if not matrix 早返回);③ merge_fill.forward_fill 完整替换(min_row<1/min_col<1 非法值跳过 + 越界防御);④ data_models.Provenance.row 语义注释;⑤ 补测试 6 个(sheet_nature MIXED ・/■ 正向 x2、formatting_detector 字体色正向、table_extractor 枚举锁 + forward_fill 防御、free_text_extractor 枚举锁)。TDD:先加测试确认 RED(仅 forward_fill 防御用例失败,其余为已通过回归锁)→ 实现 → GREEN;pytest 全量 58 passed(52 baseline + 6) | src/genesis/parsers/free_text_extractor.py, src/genesis/parsers/table_extractor.py, src/genesis/parsers/merge_fill.py, src/genesis/data_models.py, tests/test_sheet_nature.py, tests/test_formatting_detector.py, tests/test_free_text_extractor.py, tests/test_table_extractor.py, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
|
| 2026-08-09 04:40 | 反馈迭代 | 里程碑2.5 Task13 遗留清理:① extraction_method 枚举同源(free_text_extractor→ExtractionMethod.LLM_FROM_FREE_TEXT.value、table_extractor 空/非空两分支→ExtractionMethod.OPENPYXL.value,值不变既有用例兼容);② table_extractor header_row 越界/负值防御(<0 或 >=len(matrix) 回落 0,keep if not matrix 早返回);③ merge_fill.forward_fill 完整替换(min_row<1/min_col<1 非法值跳过 + 越界防御);④ data_models.Provenance.row 语义注释;⑤ 补测试 6 个(sheet_nature MIXED ・/■ 正向 x2、formatting_detector 字体色正向、table_extractor 枚举锁 + forward_fill 防御、free_text_extractor 枚举锁)。TDD:先加测试确认 RED(仅 forward_fill 防御用例失败,其余为已通过回归锁)→ 实现 → GREEN;pytest 全量 58 passed(52 baseline + 6) | src/genesis/parsers/free_text_extractor.py, src/genesis/parsers/table_extractor.py, src/genesis/parsers/merge_fill.py, src/genesis/data_models.py, tests/test_sheet_nature.py, tests/test_formatting_detector.py, tests/test_free_text_extractor.py, tests/test_table_extractor.py, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
|
||||||
| 2026-08-09 10:05 | 反馈迭代 | 里程碑2.5 最终评审与交付:整体评审通过(base 81c6b6c..d93cc77,58 passed);清理 progress.md Task11 重复/编码行;修正 _AI_USAGE_LOG.md Task13 范式步骤用词;产出交付报告 docs/milestone2.5-mixed-parser-review.md(MIXED 遗留#1 闭环) | docs/milestone2.5-mixed-parser-review.md, .superpowers/sdd/progress.md, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
|
| 2026-08-09 10:05 | 反馈迭代 | 里程碑2.5 最终评审与交付:整体评审通过(base 81c6b6c..d93cc77,58 passed);清理 progress.md Task11 重复/编码行;修正 _AI_USAGE_LOG.md Task13 范式步骤用词;产出交付报告 docs/milestone2.5-mixed-parser-review.md(MIXED 遗留#1 闭环) | docs/milestone2.5-mixed-parser-review.md, .superpowers/sdd/progress.md, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
|
||||||
| 2026-08-09 10:12 | 测试验证 | 补齐覆盖率工具:安装 pytest-cov 7.1.0/coverage 7.15.4,pyproject.toml 固化 dev 依赖+addopts(--cov=genesis --cov-report=term-missing)+[tool.coverage.run/report](source=genesis、branch=true、fail_under=90、skip_empty、sort=miss);验证 fail_under=99 拦截 exit 1;产出 docs/coverage-report.md 与 htmlcov(已 ignore);基线覆盖 96.67%(571 stmts,12 miss) | pyproject.toml, docs/coverage-report.md | deepseek-v4-flash-free |
|
| 2026-08-09 10:12 | 测试验证 | 补齐覆盖率工具:安装 pytest-cov 7.1.0/coverage 7.15.4,pyproject.toml 固化 dev 依赖+addopts(--cov=genesis --cov-report=term-missing)+[tool.coverage.run/report](source=genesis、branch=true、fail_under=90、skip_empty、sort=miss);验证 fail_under=99 拦截 exit 1;产出 docs/coverage-report.md 与 htmlcov(已 ignore);基线覆盖 96.67%(571 stmts,12 miss) | pyproject.toml, docs/coverage-report.md | deepseek-v4-flash-free |
|
||||||
| 2026-08-09 10:20 | 测试验证 | 覆盖率提升至 100%:新增 13 个防御/边界用例(空矩阵/稀疏多列/>表头越界/非法色/RGB访问异常/fill=None/list分值/空sh山 skipped/MXED 段外格式隔离);pragma 标注 3 处不可达防御分支(classify 恒有表头);fail_under 90→99;全量 71 passed,覆盖率 100.00%(563 stmts/140 br 全绿);更新 docs/coverage-report.md | tests/test_sheet_nature.py, tests/test_table_extractor.py, tests/test_formatting_detector.py, tests/test_config.py, tests/test_excel_parser.py, src/genesis/parsers/excel_parser.py, src/genesis/parsers/sheet_nature.py, pyproject.toml, docs/coverage-report.md | deepseek-v4-flash-free |
|
| 2026-08-09 10:20 | 测试验证 | 覆盖率提升至 100%:新增 13 个防御/边界用例(空矩阵/稀疏多列/表头越界/非法色/RGB访问异常/fill=None/列表类型/空sheet skipped/MIXED 段外格式隔离);pragma 标注 3 处不可达防御分支(classify 恒有表头);fail_under 90→99;全量 71 passed,覆盖率 100.00%(563 stmts/140 br 全绿);更新 docs/coverage-report.md | tests/test_sheet_nature.py, tests/test_table_extractor.py, tests/test_formatting_detector.py, tests/test_config.py, tests/test_excel_parser.py, src/genesis/parsers/excel_parser.py, src/genesis/parsers/sheet_nature.py, pyproject.toml, docs/coverage-report.md | deepseek-v4-flash-free |
|
||||||
|
| 2026-08-09 下午 | 架构设计 | 里程碑3.1 InferenceEngine 设计完成并批准:头脑风暴三轮定稿(动机/关键决策/架构与关键行为),执行 plan-design-review(无 UI scope,一致性审查全对齐),审查产出 2 项补丁(StructuredResult.status 字段、api-design LLM_NOT_CONFIGURED 错误码补齐);确定 httpx 客户端选型(pyproject 新增依赖);落盘 spec 至 docs/superpowers/specs/2026-08-09-inference-engine-design.md(含 3.8 补丁 2/3.9 补丁 1、异常模型、测试策略) | docs/superpowers/specs/2026-08-09-inference-engine-design.md, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
|
||||||
|
| 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 |
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
|
||||||
|
def approximate_token_count(text: str) -> int:
|
||||||
|
"""内置近似估算:每 4 字符 ≈ 1 token(无外部依赖,可离线)。"""
|
||||||
|
return max(1, (len(text) + 3) // 4)
|
||||||
|
|
||||||
|
|
||||||
|
def _tiktoken_estimator(text: str) -> int | None:
|
||||||
|
"""tiktoken 编码估算;tiktoken 未安装时返回 None。"""
|
||||||
|
try:
|
||||||
|
import tiktoken
|
||||||
|
except ImportError:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
enc = tiktoken.get_encoding("cl100k_base")
|
||||||
|
return len(enc.encode(text))
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def make_estimator(backend: str = "tiktoken") -> Callable[[str], int]:
|
||||||
|
"""按配置选择估算器:backend="tiktoken"(默认)优先 tiktoken,
|
||||||
|
缺失或异常回落内置 approximate;backend="approximate" 直接用近似。"""
|
||||||
|
if backend == "approximate":
|
||||||
|
return approximate_token_count
|
||||||
|
return lambda text: _tiktoken_estimator(text) or approximate_token_count(text)
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from genesis.inference.token import (
|
||||||
|
_tiktoken_estimator,
|
||||||
|
approximate_token_count,
|
||||||
|
make_estimator,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_approximate_count_minimum():
|
||||||
|
assert approximate_token_count("") >= 1
|
||||||
|
assert approximate_token_count("a") == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_approximate_count_linear():
|
||||||
|
# 每 4 字符约 1 token(向上取整)
|
||||||
|
assert approximate_token_count("abcd") == 1
|
||||||
|
assert approximate_token_count("abcdefgh") == 2
|
||||||
|
assert approximate_token_count("abcdefghi") == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_tiktoken_estimator_missing_falls_back():
|
||||||
|
# 未安装 tiktoken 或不可用时返回 None(由 make_estimator 回落 approximate)
|
||||||
|
r = _tiktoken_estimator("hello")
|
||||||
|
assert r is None or isinstance(r, int)
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_estimator_approximate_backend():
|
||||||
|
est = make_estimator("approximate")
|
||||||
|
assert est("abcd") == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_estimator_default_without_tiktoken(monkeypatch):
|
||||||
|
# 强制模拟 tiktoken 缺失:make_estimator 必须回落 approximate
|
||||||
|
import builtins
|
||||||
|
real_import = builtins.__import__
|
||||||
|
|
||||||
|
def fake_import(name, *args, **kwargs):
|
||||||
|
if name == "tiktoken":
|
||||||
|
raise ImportError("no tiktoken")
|
||||||
|
return real_import(name, *args, **kwargs)
|
||||||
|
|
||||||
|
monkeypatch.setattr(builtins, "__import__", fake_import)
|
||||||
|
est = make_estimator("tiktoken")
|
||||||
|
assert est("abcd") == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_tiktoken_estimator_success_path(monkeypatch):
|
||||||
|
# 注入假 tiktoken 模块:验证编码成功路径(len(encode(text)))
|
||||||
|
import sys
|
||||||
|
|
||||||
|
class _FakeEncoding:
|
||||||
|
def encode(self, s: str):
|
||||||
|
return ["t"] * len(s)
|
||||||
|
|
||||||
|
class _FakeTiktoken:
|
||||||
|
@staticmethod
|
||||||
|
def get_encoding(name):
|
||||||
|
assert name == "cl100k_base"
|
||||||
|
return _FakeEncoding()
|
||||||
|
|
||||||
|
monkeypatch.setitem(sys.modules, "tiktoken", _FakeTiktoken)
|
||||||
|
assert _tiktoken_estimator("hello") == 5
|
||||||
|
|
||||||
|
|
||||||
|
def test_tiktoken_estimator_encoding_error(monkeypatch):
|
||||||
|
# tiktoken 可用但编码抛异常:返回 None(由 make_estimator 回落 approximate)
|
||||||
|
import sys
|
||||||
|
|
||||||
|
class _FakeTiktoken:
|
||||||
|
@staticmethod
|
||||||
|
def get_encoding(name):
|
||||||
|
raise RuntimeError("boom")
|
||||||
|
|
||||||
|
monkeypatch.setitem(sys.modules, "tiktoken", _FakeTiktoken)
|
||||||
|
assert _tiktoken_estimator("hello") is None
|
||||||
Reference in New Issue
Block a user