Files
2026Technology-Competition/tests/test_inference_engine.py
T

342 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
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 Prompt
from tests.inference_helpers import FakeLLMClient
class Models:
"""模拟 config.InferenceModelspydantic 结构,测试中直接构建)"""
def __init__(self):
import types as t
self.primary = t.SimpleNamespace(name="deepseek-chat", provider="deepseek")
self.fallback = t.SimpleNamespace(name="qwen-max", provider="qwen")
class ModelsNoFallback:
"""无 fallback 的模型配置(覆盖 getattr(fallback) 假值分支)"""
def __init__(self):
import types as t
self.primary = t.SimpleNamespace(name="deepseek-chat", provider="deepseek")
self.fallback = None
class ModelsEmpty:
"""主/备用模型均为空(覆盖 names 为空 → 返回内置默认)"""
def __init__(self):
self.primary = None
self.fallback = None
def make_engine(client=None, *, constants=None):
eng = InferenceEngine(
client=client or FakeLLMClient(),
models=Models(),
registry=PromptRegistry(),
estimator=approximate_token_count,
)
if constants:
eng._max_context_tokens = constants # 内部测试钩子
return eng
# ---------- chat 主路径 ----------
def test_chat_ok():
client = FakeLLMClient([("ok", "正文")])
eng = make_engine(client)
r = eng.chat(
session_id="s1",
prompt=Prompt(name="writer", version="v1", template="章节:{{ chapter }}"),
variables={"chapter": "DB設計"},
)
assert r.status == "ok" and r.text == "正文"
assert client.calls[0]["model"] == "deepseek-chat"
def test_chat_fallback_after_primary_failure():
client = FakeLLMClient([("raise_timeout", ""), ("ok", "备用输出")])
eng = make_engine(client)
r = eng.chat(
session_id="s1",
prompt=Prompt(name="p", version="v1", template="t:{{ x }}"),
variables={"x": "1"},
)
assert r.status == "fallback"
# 先主模型,后备模型(修正:断言首次调用为主模型)
assert client.calls[0]["model"] == "deepseek-chat"
def test_chat_all_failed_returns_failed():
client = FakeLLMClient([("raise_network", ""), ("raise_network", "")])
eng = make_engine(client)
r = eng.chat(
session_id="s1",
prompt=Prompt(name="p", version="v1", template="t"),
variables={},
)
assert r.status == "failed" and r.error
# ---------- error_code 透传(与 error 同源取最后一次异常) ----------
def test_chat_failed_error_code_timeout():
# 主模型超时 + 备用也失败:error_code 与 error 同源(取最后一次异常)
client = FakeLLMClient([("raise_timeout", ""), ("raise_timeout", "")])
eng = InferenceEngine(client=client, models=Models())
r = eng.chat(session_id="s1", prompt=Prompt(name="p", version="v1", template="t"), variables={})
assert r.status == "failed"
assert r.error_code == "LLM_TIMEOUT"
def test_chat_failed_error_code_network_last():
# 主模型超时(第一次)、备用网络失败(最后一次)→ error_code 取最后一次 = LLM_NETWORK_ERROR
client = FakeLLMClient([("raise_timeout", ""), ("raise_network", "")])
eng = InferenceEngine(client=client, models=Models())
r = eng.chat(session_id="s1", prompt=Prompt(name="p", version="v1", template="t"), variables={})
assert r.status == "failed"
assert r.error_code == "LLM_NETWORK_ERROR"
def test_chat_failed_error_code_not_configured():
# 备用模型 Key 缺失(最后一次)→ LLM_NOT_CONFIGURED
# 用自定义异常客户端模拟 NotConfigured
class NotConfiguredClient:
def __init__(self):
self.calls = []
def chat(self, *, model, messages, temperature, max_tokens):
self.calls.append(model)
from genesis.inference.exceptions import LLMNotConfiguredError
raise LLMNotConfiguredError("no key")
eng = InferenceEngine(client=NotConfiguredClient(), models=Models())
r = eng.chat(session_id="s1", prompt=Prompt(name="p", version="v1", template="t"), variables={})
assert r.status == "failed"
assert r.error_code == "LLM_NOT_CONFIGURED"
def test_chat_structured_parse_error_code():
client = FakeLLMClient([("parse_fail", ""), ("parse_fail", "")])
eng = InferenceEngine(client=client, models=Models())
r = eng.chat_structured(
session_id="s1", prompt=Prompt(name="p", version="v1", template="提取"),
variables={}, schema={}, retry_count=1,
)
assert r.status == "parse_error"
assert r.error_code == "LLM_PARSE_ERROR"
def test_chat_structured_failed_error_code_network():
client = FakeLLMClient([("raise_network", "")])
eng = InferenceEngine(client=client, models=Models())
r = eng.chat_structured(
session_id="s1", prompt=Prompt(name="p", version="v1", template="提取"),
variables={}, schema={},
)
assert r.status == "failed"
assert r.error_code == "LLM_NETWORK_ERROR"
def test_chat_plain_string_prompt():
eng = make_engine(FakeLLMClient([("ok", "hi")]))
r = eng.chat(session_id="s1", prompt="直接文本", variables={})
assert r.text == "hi" and r.status == "ok"
def test_chat_truncation_callback_triggered():
seen = {}
def truncate_cb(prompt_text, variables):
seen["called"] = True
seen["len"] = len(prompt_text)
return {**variables, "chapter": "裁剪版"}
eng = InferenceEngine(
client=FakeLLMClient([("ok", "x")]),
models=Models(),
registry=PromptRegistry(),
estimator=approximate_token_count,
truncate_cb=truncate_cb,
)
eng._max_context_tokens = 2 # 强制超限(复习:'abcd很长很长的标题' 约 3 token > 2
r = eng.chat(
session_id="s1",
prompt=Prompt(name="p", version="v1", template="abcd{{ chapter }}"),
variables={"chapter": "很长很长的标题"},
)
assert seen["called"] is True
assert r.status == "ok"
# ---------- chat_structured 主路径 ----------
def test_chat_structured_ok():
client = FakeLLMClient([("ok", '{"a": 1}')])
eng = make_engine(client)
r = eng.chat_structured(
session_id="s1", prompt=Prompt(name="p", version="v1", template="提取"),
variables={"text": "内容"},
schema={"type": "object", "properties": {"a": {"type": "number"}}},
)
assert r.status == "ok" and r.data == {"a": 1}
def test_chat_structured_retry_parse():
client = FakeLLMClient([("parse_fail", ""), ("ok", '{"a": 2}')])
eng = make_engine(client)
r = eng.chat_structured(
session_id="s1", prompt=Prompt(name="p", version="v1", template="提取"),
variables={}, schema={},
)
assert r.status == "ok" and r.data == {"a": 2} and r.parse_attempts == 2
def test_chat_structured_parse_error_returns_raw():
client = FakeLLMClient([("parse_fail", ""), ("parse_fail", "")])
eng = make_engine(client)
r = eng.chat_structured(
session_id="s1", prompt=Prompt(name="p", version="v1", template="提取"),
variables={}, schema={}, retry_count=1,
)
assert r.status == "parse_error"
assert r.raw_text == "NOT JSON"
assert r.parse_attempts == 2
def test_chat_structured_failed_on_network():
client = FakeLLMClient([("raise_network", "")])
eng = make_engine(client)
r = eng.chat_structured(
session_id="s1", prompt=Prompt(name="p", version="v1", template="提取"),
variables={}, schema={},
)
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():
"""models 无 fallback:仅尝试主模型,失败后直接 failed。"""
client = FakeLLMClient([("raise_network", "")])
eng = InferenceEngine(
client=client,
models=ModelsNoFallback(),
registry=PromptRegistry(),
estimator=approximate_token_count,
)
r = eng.chat(session_id="s1", prompt="t", variables={})
assert r.status == "failed"
assert len(client.calls) == 1
assert client.calls[0]["model"] == "deepseek-chat"
def test_chat_empty_models_uses_default():
"""主/备用均为空配置时回退内置默认模型 deepseek-chat。"""
eng = InferenceEngine(
client=FakeLLMClient([("ok", "x")]),
models=ModelsEmpty(),
registry=PromptRegistry(),
estimator=approximate_token_count,
)
r = eng.chat(session_id="s1", prompt="t", variables={})
assert r.status == "ok"
assert eng._model_names(None) == ["deepseek-chat"]
def test_chat_defaults_without_registry_estimator():
"""registry/estimator/models 均未配置时使用内置默认(PromptRegistry + approximate + deepseek-chat)。"""
client = FakeLLMClient([("ok", "默认")])
eng = InferenceEngine(client=client, models=None)
r = eng.chat(
session_id="s1",
prompt=Prompt(name="p", version="v1", template="tt"),
variables={},
)
assert r.status == "ok" and r.text == "默认"
assert eng._model_names(None) == ["deepseek-chat"]
def test_chat_explicit_model_param():
"""显式指定 model,跳过主/备选择,仅调用该模型。"""
client = FakeLLMClient([("ok", "c")])
eng = make_engine(client)
r = eng.chat(session_id="s1", prompt="t", variables={}, model="qwen-custom")
assert r.status == "ok"
assert client.calls[0]["model"] == "qwen-custom"
assert len(client.calls) == 1
def test_chat_truncation_without_callback_keeps_variables():
"""超限但无 truncate_cbvariables 原样保留(裁剪分支不触发)。"""
client = FakeLLMClient([("ok", "ok")])
eng = make_engine(client)
eng._max_context_tokens = 2 # 强制超限(渲染后约 11 token)
r = eng.chat(
session_id="s1",
prompt=Prompt(name="p", version="v1", template="abcd{{ chapter }}"),
variables={"chapter": "很长很长的标题"},
)
assert r.status == "ok"
assert client.calls[0]["model"] == "deepseek-chat"
def test_chat_structured_empty_schema_no_hint():
"""schema 为空时不追加 schema 提示,仍可正常解析。"""
client = FakeLLMClient([("ok", '{"v": true}')])
eng = make_engine(client)
r = eng.chat_structured(
session_id="s1", prompt="直接文本", variables={}, schema={},
)
assert r.status == "ok" and r.data == {"v": True}
def test_chat_truncation_callback_returns_none_keeps_variables():
"""truncate_cb 返回 None 时回退原 variables(覆盖 new_vars is None 分支)。"""
def truncate_cb(prompt_text, variables):
return None
client = FakeLLMClient([("ok", "x")])
eng = InferenceEngine(
client=client,
models=Models(),
registry=PromptRegistry(),
estimator=approximate_token_count,
truncate_cb=truncate_cb,
)
eng._max_context_tokens = 2
r = eng.chat(
session_id="s1",
prompt=Prompt(name="p", version="v1", template="abcd{{ chapter }}"),
variables={"chapter": "很长很长的标题"},
)
assert r.status == "ok"
# 未替换变量:发送内容仍为原样渲染
assert "很长很长的标题" in client.calls[0]["messages"][0]