feat: 推理引擎数据模型与异常层(types/exceptions)及 httpx/jinja2 依赖
This commit is contained in:
@@ -13,6 +13,8 @@ dependencies = [
|
|||||||
"pyyaml>=6.0",
|
"pyyaml>=6.0",
|
||||||
"openpyxl>=3.1",
|
"openpyxl>=3.1",
|
||||||
"python-docx>=1.0",
|
"python-docx>=1.0",
|
||||||
|
"httpx>=0.28",
|
||||||
|
"jinja2>=3.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[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