feat: 推理引擎数据模型与异常层(types/exceptions)及 httpx/jinja2 依赖
This commit is contained in:
@@ -13,6 +13,8 @@ dependencies = [
|
||||
"pyyaml>=6.0",
|
||||
"openpyxl>=3.1",
|
||||
"python-docx>=1.0",
|
||||
"httpx>=0.28",
|
||||
"jinja2>=3.1",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Genesis 推理引擎(统一 LLM 调用入口)。"""
|
||||
|
||||
from .types import ChatMessage, ChatResult, Prompt, StructuredResult, TokenUsage
|
||||
from .exceptions import (
|
||||
LLMError,
|
||||
LLMNetworkError,
|
||||
LLMNotConfiguredError,
|
||||
LLMResponseError,
|
||||
LLMTimeoutError,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ChatMessage",
|
||||
"ChatResult",
|
||||
"Prompt",
|
||||
"StructuredResult",
|
||||
"TokenUsage",
|
||||
"LLMError",
|
||||
"LLMNetworkError",
|
||||
"LLMNotConfiguredError",
|
||||
"LLMResponseError",
|
||||
"LLMTimeoutError",
|
||||
]
|
||||
@@ -0,0 +1,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
class LLMError(Exception):
|
||||
"""LLM 调用相关的异常基类(api-design §7 映射基底)"""
|
||||
|
||||
|
||||
class LLMNetworkError(LLMError):
|
||||
"""网络失败 / 5xx 重试耗尽(可重试语义)"""
|
||||
|
||||
|
||||
class LLMTimeoutError(LLMError):
|
||||
"""LLM 调用超时(api-error: LLM_TIMEOUT 502)"""
|
||||
|
||||
|
||||
class LLMNotConfiguredError(LLMError):
|
||||
"""Key / 模型缺失(api-error: LLM_NOT_CONFIGURED 503)"""
|
||||
|
||||
|
||||
class LLMResponseError(LLMError):
|
||||
"""响应结构损坏(JSON 解析失败等)"""
|
||||
@@ -0,0 +1,52 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Literal
|
||||
|
||||
|
||||
@dataclass
|
||||
class TokenUsage:
|
||||
"""一次 LLM 调用的 token 用量(可观测性事件/统计用)"""
|
||||
input_tokens: int = 0
|
||||
output_tokens: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChatMessage:
|
||||
"""Chat Completions 消息"""
|
||||
role: Literal["system", "user", "assistant"]
|
||||
content: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChatResult:
|
||||
"""chat() 的返回值"""
|
||||
text: str
|
||||
model: str
|
||||
prompt_version: str
|
||||
usage: TokenUsage
|
||||
duration_ms: int
|
||||
status: Literal["ok", "fallback", "failed"]
|
||||
error: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class StructuredResult:
|
||||
"""chat_structured() 的返回值(补丁 1:含 status 字段)"""
|
||||
data: dict
|
||||
raw_text: str
|
||||
parse_attempts: int
|
||||
model: str
|
||||
prompt_version: str
|
||||
usage: TokenUsage
|
||||
duration_ms: int
|
||||
status: Literal["ok", "fallback", "parse_error", "failed"]
|
||||
error: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Prompt:
|
||||
"""Prompt 模板条目(name+version 唯一)"""
|
||||
name: str
|
||||
version: str
|
||||
template: str
|
||||
@@ -0,0 +1,21 @@
|
||||
import pytest
|
||||
|
||||
from genesis.inference.exceptions import (
|
||||
LLMError,
|
||||
LLMNetworkError,
|
||||
LLMNotConfiguredError,
|
||||
LLMResponseError,
|
||||
LLMTimeoutError,
|
||||
)
|
||||
|
||||
|
||||
def test_error_hierarchy():
|
||||
assert issubclass(LLMNetworkError, LLMError)
|
||||
assert issubclass(LLMTimeoutError, LLMError)
|
||||
assert issubclass(LLMNotConfiguredError, LLMError)
|
||||
assert issubclass(LLMResponseError, LLMError)
|
||||
|
||||
|
||||
def test_error_message_roundtrip():
|
||||
e = LLMTimeoutError("timeout!")
|
||||
assert str(e) == "timeout!"
|
||||
@@ -0,0 +1,40 @@
|
||||
from genesis.inference.types import ChatMessage, ChatResult, Prompt, StructuredResult, TokenUsage
|
||||
|
||||
|
||||
def test_token_usage_defaults():
|
||||
u = TokenUsage()
|
||||
assert u.input_tokens == 0 and u.output_tokens == 0
|
||||
|
||||
|
||||
def test_chat_message_roles():
|
||||
assert ChatMessage(role="system", content="x").content == "x"
|
||||
|
||||
|
||||
def test_chat_result_defaults():
|
||||
r = ChatResult(
|
||||
text="t", model="m", prompt_version="v1", usage=TokenUsage(),
|
||||
duration_ms=10, status="ok",
|
||||
)
|
||||
assert r.status == "ok" and r.error is None
|
||||
|
||||
|
||||
def test_structured_result_status_ok():
|
||||
r = StructuredResult(
|
||||
data={"a": 1}, raw_text='{"a":1}', parse_attempts=1, model="m",
|
||||
prompt_version="v1", usage=TokenUsage(), duration_ms=10, status="ok",
|
||||
)
|
||||
assert r.data == {"a": 1} and r.raw_text == '{"a":1}'
|
||||
|
||||
|
||||
def test_structured_result_status_parse_error():
|
||||
r = StructuredResult(
|
||||
data={}, raw_text="NOT JSON", parse_attempts=3, model="m",
|
||||
prompt_version="v1", usage=TokenUsage(), duration_ms=50,
|
||||
status="parse_error", error="bad json",
|
||||
)
|
||||
assert r.status == "parse_error" and r.error == "bad json"
|
||||
|
||||
|
||||
def test_prompt_fields():
|
||||
p = Prompt(name="writer", version="v2", template="章节 {{chapter}}")
|
||||
assert p.name == "writer" and p.version == "v2"
|
||||
Reference in New Issue
Block a user