test: 清理 backlog——chat_structured 超限检测 + client 上下文管理器
This commit is contained in:
@@ -50,3 +50,4 @@
|
||||
| 2026-08-09 10:40 | 反馈迭代 | 里程碑3.1 最终全分支评审(base 2fbff07..8bc5e5e)修正:评审查到 client 两处缺陷并修复——① 2xx 但响应结构损坏(非 JSON/缺 choices 字段)从裸 KeyError/JSONDecodeError 逃逸出 engine 的 except LLMError,改为捕获 (json.JSONDecodeError, KeyError, IndexError, TypeError) 后抛 LLMResponseError(spec §3.8 既有类型首次被触发,封闭 api-design §7 LLM_PARSE_ERROR 来源列虚指);② 3xx 重定向被当作成功(httpx 不自动跟随),补显式 2xx 判定,非 2xx 一律 LLMNetworkError。TDD:新增 2 用例(test_chat_3xx_no_retry、test_chat_malformed_response_raises_llm_response_error 含 not-json/missing-key 两种 handler)先 RED→实现→GREEN;聚焦 9 passed;pytest 全量 117 passed 覆盖 100.00%(785 stmts/186 br),fail_under=99 达标 | src/genesis/inference/client.py, tests/test_inference_client.py, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
|
||||
|
||||
(End of file - total 50 lines)
|
||||
| 2026-08-09 10:50 | 反馈迭代 | 里程碑3.1 backlog 清理(retro 3 项):① chat_structured 增加 token 超限检测(渲染后估算超限→truncate_cb→重渲染,与 chat 流程一致)TDD RED→GREEN 新增 test_chat_structured_truncation_callback_triggered;② 清理 test_inference_engine.py 未用导入(pytest/json/ChatMessage);③ HttpLLMClient 增加上下文管理器(__enter__/__exit__ 退出时关闭底层 httpx.Client 释放连接)TDD 新增 test_client_context_manager_closes_transport;pytest 全量 119 passed 覆盖 100.00%,fail_under=99 达标;提交见 git log | src/genesis/inference/engine.py, src/genesis/inference/client.py, tests/test_inference_engine.py, tests/test_inference_client.py, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
|
||||
|
||||
@@ -48,6 +48,13 @@ class HttpLLMClient:
|
||||
self._retry_backoff = retry_backoff
|
||||
self._client = httpx.Client(timeout=timeout_sec, transport=transport)
|
||||
|
||||
def __enter__(self) -> HttpLLMClient:
|
||||
"""支持 with 块:退出时自动关闭底层连接。"""
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
||||
self._client.close()
|
||||
|
||||
def chat(
|
||||
self,
|
||||
*,
|
||||
|
||||
@@ -133,6 +133,9 @@ class InferenceEngine:
|
||||
retry_count: int = 2,
|
||||
) -> StructuredResult:
|
||||
rendered = self._render_prompt(prompt, variables)
|
||||
if self._estimator(rendered) > self._max_context_tokens:
|
||||
variables = self._apply_truncation(rendered, variables)
|
||||
rendered = self._render_prompt(prompt, variables)
|
||||
# 追加 schema 约束说明(不强制模板支持)
|
||||
schema_hint = json.dumps(schema, ensure_ascii=False) if schema else ""
|
||||
base_rendered = rendered + (f'\n\n请输出符合以下 JSON Schema 的 JSON:{schema_hint}' if schema_hint else "")
|
||||
|
||||
@@ -54,6 +54,24 @@ def test_chat_requires_api_key():
|
||||
HttpLLMClient(base_url="https://x", api_key="")
|
||||
|
||||
|
||||
def test_client_context_manager_closes_transport():
|
||||
# with 块退出后底层 httpx.Client 应被关闭(释放连接)
|
||||
with HttpLLMClient(
|
||||
base_url="https://api.test.local",
|
||||
api_key="sk-test",
|
||||
transport=httpx.MockTransport(_ok_handler),
|
||||
) as client:
|
||||
assert isinstance(client, HttpLLMClient)
|
||||
text, _ = client.chat(
|
||||
model="deepseek-chat",
|
||||
messages=[ChatMessage(role="user", content="hi")],
|
||||
temperature=0.2,
|
||||
max_tokens=100,
|
||||
)
|
||||
assert text == "Hello"
|
||||
assert client._client.is_closed is True
|
||||
|
||||
|
||||
def test_chat_timeout():
|
||||
def slow(request):
|
||||
raise httpx.ReadTimeout("slow")
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from genesis.inference.engine import InferenceEngine
|
||||
from genesis.inference.exceptions import LLMError
|
||||
from genesis.inference.prompt_registry import PromptRegistry
|
||||
from genesis.inference.token import approximate_token_count
|
||||
from genesis.inference.types import ChatMessage, Prompt
|
||||
from genesis.inference.types import Prompt
|
||||
from tests.inference_helpers import FakeLLMClient
|
||||
|
||||
|
||||
@@ -161,6 +157,32 @@ def test_chat_structured_failed_on_network():
|
||||
assert r.status == "failed"
|
||||
|
||||
|
||||
def test_chat_structured_truncation_callback_triggered():
|
||||
# chat_structured 超限时同样触发 truncate_cb(与 chat 流程一致)
|
||||
seen = {}
|
||||
|
||||
def truncate_cb(prompt_text, variables):
|
||||
seen["called"] = True
|
||||
return {**variables, "chapter": "裁剪版"}
|
||||
|
||||
eng = InferenceEngine(
|
||||
client=FakeLLMClient([("ok", '{"a": 1}')]),
|
||||
models=Models(),
|
||||
registry=PromptRegistry(),
|
||||
estimator=approximate_token_count,
|
||||
truncate_cb=truncate_cb,
|
||||
)
|
||||
eng._max_context_tokens = 2 # 强制超限(渲染约 3 token > 2)
|
||||
r = eng.chat_structured(
|
||||
session_id="s1",
|
||||
prompt=Prompt(name="p", version="v1", template="abcd{{ chapter }}"),
|
||||
variables={"chapter": "很长很长的标题"},
|
||||
schema={"type": "object", "properties": {"a": {"type": "number"}}},
|
||||
)
|
||||
assert seen["called"] is True
|
||||
assert r.status == "ok"
|
||||
|
||||
|
||||
# ---------- 补充分支覆盖(defensive / 缺失配置) ----------
|
||||
|
||||
def test_chat_fallback_all_failed_when_only_primary():
|
||||
|
||||
Reference in New Issue
Block a user