feat:简化部分代码
This commit is contained in:
+16
-54
@@ -1,8 +1,6 @@
|
||||
"""可复用的 Jev 决策客户端;重试和 HTTP 连接管理交给官方 SDK"""
|
||||
|
||||
from functools import cache
|
||||
from math import isclose
|
||||
from typing import Self
|
||||
|
||||
from pydantic import ValidationError
|
||||
from typesafe_sdk import (
|
||||
@@ -29,24 +27,11 @@ from app.schemas.decision import (
|
||||
|
||||
|
||||
class DecisionEngine:
|
||||
def __init__(
|
||||
self,
|
||||
config: Settings | None = None,
|
||||
*,
|
||||
client: AsyncTypeSafeClient | None = None,
|
||||
):
|
||||
def __init__(self, config: Settings | None = None):
|
||||
self._config = config if config is not None else settings
|
||||
self._client = client
|
||||
self._owns_client = client is None
|
||||
self._closed = False
|
||||
|
||||
@property
|
||||
def closed(self) -> bool:
|
||||
return self._closed
|
||||
self._client: AsyncTypeSafeClient | None = None
|
||||
|
||||
def _get_client(self) -> AsyncTypeSafeClient:
|
||||
if self._closed:
|
||||
raise DecisionError("决策客户端已关闭", kind="closed")
|
||||
if self._client is None:
|
||||
config = self._config
|
||||
if not config.typesafe_api_key.strip():
|
||||
@@ -69,8 +54,6 @@ class DecisionEngine:
|
||||
return self._client
|
||||
|
||||
async def run(self, request: DecisionRequest) -> DecisionResponse:
|
||||
# 即便模型实例在创建后被修改,也必须重新经过请求校验
|
||||
request = DecisionRequest.model_validate(request.model_dump(mode="json"))
|
||||
client = self._get_client()
|
||||
questions = {
|
||||
name: question.model_dump(mode="json", exclude_unset=True)
|
||||
@@ -136,27 +119,18 @@ class DecisionEngine:
|
||||
answer = result.results[name]
|
||||
if answer.type != question.type:
|
||||
raise ValueError("返回的决策类型与请求不一致")
|
||||
if isinstance(question, ChoiceDecision) and isinstance(
|
||||
answer, ChoiceResult
|
||||
if (
|
||||
isinstance(question, ChoiceDecision)
|
||||
and isinstance(answer, ChoiceResult)
|
||||
and answer.choice not in question.criteria
|
||||
):
|
||||
if answer.choice not in question.criteria:
|
||||
raise ValueError("返回了候选集合之外的选项")
|
||||
if answer.probabilities.keys() != question.criteria.keys():
|
||||
raise ValueError("选项概率不完整")
|
||||
if isinstance(question, ScoreDecision) and isinstance(
|
||||
answer, ScoreResult
|
||||
raise ValueError("返回了候选集合之外的选项")
|
||||
if (
|
||||
isinstance(question, ScoreDecision)
|
||||
and isinstance(answer, ScoreResult)
|
||||
and not 0 <= answer.score <= len(question.criteria) - 1
|
||||
):
|
||||
levels = {str(index) for index in range(len(question.criteria))}
|
||||
if (
|
||||
answer.probabilities.keys() != levels
|
||||
or answer.legend.keys() != levels
|
||||
):
|
||||
raise ValueError("评分等级不完整")
|
||||
if not 0 <= answer.score <= len(question.criteria) - 1:
|
||||
raise ValueError("评分超出等级范围")
|
||||
if isinstance(answer, (ChoiceResult, ScoreResult)):
|
||||
if not isclose(sum(answer.probabilities.values()), 1, abs_tol=1e-4):
|
||||
raise ValueError("概率之和不为 1")
|
||||
raise ValueError("评分超出等级范围")
|
||||
return result
|
||||
except (ValidationError, ValueError) as error:
|
||||
raise DecisionError(
|
||||
@@ -166,17 +140,9 @@ class DecisionEngine:
|
||||
) from error
|
||||
|
||||
async def aclose(self) -> None:
|
||||
if not self._closed:
|
||||
if self._owns_client and self._client is not None:
|
||||
await self._client.aclose()
|
||||
self._closed = True
|
||||
|
||||
async def __aenter__(self) -> Self:
|
||||
self._get_client()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_value, traceback) -> None:
|
||||
await self.aclose()
|
||||
if self._client is not None:
|
||||
await self._client.aclose()
|
||||
self._client = None
|
||||
|
||||
|
||||
@cache
|
||||
@@ -186,11 +152,7 @@ def _cached_engine() -> DecisionEngine:
|
||||
|
||||
def get_decision_engine() -> DecisionEngine:
|
||||
"""与 get_llm 类似的共享入口;适用于单个服务事件循环"""
|
||||
engine = _cached_engine()
|
||||
if engine.closed:
|
||||
_cached_engine.cache_clear()
|
||||
engine = _cached_engine()
|
||||
return engine
|
||||
return _cached_engine()
|
||||
|
||||
|
||||
async def close_decision_engine() -> None:
|
||||
|
||||
+12
-57
@@ -1,10 +1,8 @@
|
||||
"""统一获取普通模型或绑定结构化输出的模型"""
|
||||
|
||||
from functools import cache
|
||||
from typing import Literal, TypeVar, cast, overload
|
||||
from typing import Any, TypeVar, overload
|
||||
|
||||
from langchain_core.language_models import LanguageModelInput
|
||||
from langchain_core.language_models.chat_models import BaseChatModel
|
||||
from langchain_core.runnables import Runnable
|
||||
from langchain_openai import ChatOpenAI
|
||||
from pydantic import BaseModel
|
||||
@@ -12,7 +10,6 @@ from pydantic import BaseModel
|
||||
from app.config import settings
|
||||
|
||||
OutputModel = TypeVar("OutputModel", bound=BaseModel)
|
||||
StructuredMethod = Literal["function_calling", "json_schema", "json_mode"]
|
||||
|
||||
|
||||
@cache
|
||||
@@ -29,64 +26,22 @@ def _get_base_llm() -> ChatOpenAI:
|
||||
)
|
||||
|
||||
|
||||
# 下面两个 @overload 只给类型检查器和编辑器看,运行时不会执行
|
||||
# 不传 schema:返回普通聊天模型,输出是自由文本
|
||||
@overload
|
||||
def get_llm(
|
||||
schema: type[OutputModel],
|
||||
*,
|
||||
method: StructuredMethod = "function_calling",
|
||||
strict: bool | None = False,
|
||||
client: BaseChatModel | None = None,
|
||||
) -> Runnable[LanguageModelInput, OutputModel]: ...
|
||||
def get_llm() -> ChatOpenAI: ...
|
||||
|
||||
|
||||
# 传入 Pydantic 类:返回结构化调用器,ainvoke 的结果就是该类的实例
|
||||
# OutputModel 让返回类型跟着传入的类走,例如 get_llm(Requirement) 得到 Runnable[..., Requirement]
|
||||
@overload
|
||||
def get_llm(
|
||||
schema: None = None,
|
||||
*,
|
||||
method: StructuredMethod = "function_calling",
|
||||
strict: bool | None = False,
|
||||
client: None = None,
|
||||
) -> ChatOpenAI: ...
|
||||
def get_llm(schema: type[OutputModel]) -> Runnable[Any, OutputModel]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def get_llm(
|
||||
schema: None = None,
|
||||
*,
|
||||
method: StructuredMethod = "function_calling",
|
||||
strict: bool | None = False,
|
||||
client: BaseChatModel,
|
||||
) -> BaseChatModel: ...
|
||||
|
||||
|
||||
def get_llm(
|
||||
schema: type[OutputModel] | None = None,
|
||||
*,
|
||||
method: StructuredMethod = "function_calling",
|
||||
strict: bool | None = False,
|
||||
client: BaseChatModel | None = None,
|
||||
) -> BaseChatModel | Runnable[LanguageModelInput, OutputModel]:
|
||||
"""不传 schema 返回普通模型,传入 Pydantic 类则返回结构化调用器
|
||||
|
||||
client 用于注入模型,省略时使用缓存客户端
|
||||
strict 控制服务端 schema 约束,本地仍由 Pydantic 校验
|
||||
"""
|
||||
if schema is not None and (
|
||||
not isinstance(schema, type) or not issubclass(schema, BaseModel)
|
||||
):
|
||||
raise TypeError("schema 必须是 Pydantic BaseModel 类")
|
||||
if method not in ("function_calling", "json_schema", "json_mode"):
|
||||
raise ValueError(f"不支持的结构化输出方式: {method}")
|
||||
if schema is None and (method != "function_calling" or strict not in (False, None)):
|
||||
raise ValueError("设置结构化输出参数时必须提供 schema")
|
||||
if method == "json_mode" and strict is True:
|
||||
raise ValueError("json_mode 不支持 strict=True")
|
||||
|
||||
llm = client if client is not None else _get_base_llm()
|
||||
# 真正的实现:schema 用 type[BaseModel] | None 覆盖上面两种调用
|
||||
# 返回类型由 overload 决定,这里不再重复标注
|
||||
def get_llm(schema: type[BaseModel] | None = None) -> Any:
|
||||
llm = _get_base_llm()
|
||||
if schema is None:
|
||||
return llm
|
||||
# json_mode 不接受 strict 参数,提示词还需要明确要求 JSON 及其结构
|
||||
structured = llm.with_structured_output(
|
||||
schema, method=method, strict=None if method == "json_mode" else strict
|
||||
)
|
||||
return cast(Runnable[LanguageModelInput, OutputModel], structured)
|
||||
return llm.with_structured_output(schema, method="function_calling", strict=False)
|
||||
|
||||
Reference in New Issue
Block a user