初步重构llmrequest
This commit is contained in:
104
src/llm_models/payload_content/message.py
Normal file
104
src/llm_models/payload_content/message.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
# 设计这系列类的目的是为未来可能的扩展做准备
|
||||
|
||||
|
||||
class RoleType(Enum):
|
||||
System = "system"
|
||||
User = "user"
|
||||
Assistant = "assistant"
|
||||
Tool = "tool"
|
||||
|
||||
|
||||
SUPPORTED_IMAGE_FORMATS = ["jpg", "jpeg", "png", "webp", "gif"]
|
||||
|
||||
|
||||
class Message:
|
||||
def __init__(
|
||||
self,
|
||||
role: RoleType,
|
||||
content: str | list[tuple[str, str] | str],
|
||||
tool_call_id: str | None = None,
|
||||
):
|
||||
"""
|
||||
初始化消息对象
|
||||
(不应直接修改Message类,而应使用MessageBuilder类来构建对象)
|
||||
"""
|
||||
self.role: RoleType = role
|
||||
self.content: str | list[tuple[str, str] | str] = content
|
||||
self.tool_call_id: str | None = tool_call_id
|
||||
|
||||
|
||||
class MessageBuilder:
|
||||
def __init__(self):
|
||||
self.__role: RoleType = RoleType.User
|
||||
self.__content: list[tuple[str, str] | str] = []
|
||||
self.__tool_call_id: str | None = None
|
||||
|
||||
def set_role(self, role: RoleType = RoleType.User) -> "MessageBuilder":
|
||||
"""
|
||||
设置角色(默认为User)
|
||||
:param role: 角色
|
||||
:return: MessageBuilder对象
|
||||
"""
|
||||
self.__role = role
|
||||
return self
|
||||
|
||||
def add_text_content(self, text: str) -> "MessageBuilder":
|
||||
"""
|
||||
添加文本内容
|
||||
:param text: 文本内容
|
||||
:return: MessageBuilder对象
|
||||
"""
|
||||
self.__content.append(text)
|
||||
return self
|
||||
|
||||
def add_image_content(
|
||||
self, image_format: str, image_base64: str
|
||||
) -> "MessageBuilder":
|
||||
"""
|
||||
添加图片内容
|
||||
:param image_format: 图片格式
|
||||
:param image_base64: 图片的base64编码
|
||||
:return: MessageBuilder对象
|
||||
"""
|
||||
if image_format.lower() not in SUPPORTED_IMAGE_FORMATS:
|
||||
raise ValueError("不受支持的图片格式")
|
||||
if not image_base64:
|
||||
raise ValueError("图片的base64编码不能为空")
|
||||
self.__content.append((image_format, image_base64))
|
||||
return self
|
||||
|
||||
def add_tool_call(self, tool_call_id: str) -> "MessageBuilder":
|
||||
"""
|
||||
添加工具调用指令(调用时请确保已设置为Tool角色)
|
||||
:param tool_call_id: 工具调用指令的id
|
||||
:return: MessageBuilder对象
|
||||
"""
|
||||
if self.__role != RoleType.Tool:
|
||||
raise ValueError("仅当角色为Tool时才能添加工具调用ID")
|
||||
if not tool_call_id:
|
||||
raise ValueError("工具调用ID不能为空")
|
||||
self.__tool_call_id = tool_call_id
|
||||
return self
|
||||
|
||||
def build(self) -> Message:
|
||||
"""
|
||||
构建消息对象
|
||||
:return: Message对象
|
||||
"""
|
||||
if len(self.__content) == 0:
|
||||
raise ValueError("内容不能为空")
|
||||
if self.__role == RoleType.Tool and self.__tool_call_id is None:
|
||||
raise ValueError("Tool角色的工具调用ID不能为空")
|
||||
|
||||
return Message(
|
||||
role=self.__role,
|
||||
content=(
|
||||
self.__content[0]
|
||||
if (len(self.__content) == 1 and isinstance(self.__content[0], str))
|
||||
else self.__content
|
||||
),
|
||||
tool_call_id=self.__tool_call_id,
|
||||
)
|
||||
223
src/llm_models/payload_content/resp_format.py
Normal file
223
src/llm_models/payload_content/resp_format.py
Normal file
@@ -0,0 +1,223 @@
|
||||
from enum import Enum
|
||||
from typing import Optional, Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import TypedDict, Required
|
||||
|
||||
|
||||
class RespFormatType(Enum):
|
||||
TEXT = "text" # 文本
|
||||
JSON_OBJ = "json_object" # JSON
|
||||
JSON_SCHEMA = "json_schema" # JSON Schema
|
||||
|
||||
|
||||
class JsonSchema(TypedDict, total=False):
|
||||
name: Required[str]
|
||||
"""
|
||||
The name of the response format.
|
||||
|
||||
Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length
|
||||
of 64.
|
||||
"""
|
||||
|
||||
description: Optional[str]
|
||||
"""
|
||||
A description of what the response format is for, used by the model to determine
|
||||
how to respond in the format.
|
||||
"""
|
||||
|
||||
schema: dict[str, object]
|
||||
"""
|
||||
The schema for the response format, described as a JSON Schema object. Learn how
|
||||
to build JSON schemas [here](https://json-schema.org/).
|
||||
"""
|
||||
|
||||
strict: Optional[bool]
|
||||
"""
|
||||
Whether to enable strict schema adherence when generating the output. If set to
|
||||
true, the model will always follow the exact schema defined in the `schema`
|
||||
field. Only a subset of JSON Schema is supported when `strict` is `true`. To
|
||||
learn more, read the
|
||||
[Structured Outputs guide](https://platform.openai.com/docs/guides/structured-outputs).
|
||||
"""
|
||||
|
||||
|
||||
def _json_schema_type_check(instance) -> str | None:
|
||||
if "name" not in instance:
|
||||
return "schema必须包含'name'字段"
|
||||
elif not isinstance(instance["name"], str) or instance["name"].strip() == "":
|
||||
return "schema的'name'字段必须是非空字符串"
|
||||
if "description" in instance and (
|
||||
not isinstance(instance["description"], str)
|
||||
or instance["description"].strip() == ""
|
||||
):
|
||||
return "schema的'description'字段只能填入非空字符串"
|
||||
if "schema" not in instance:
|
||||
return "schema必须包含'schema'字段"
|
||||
elif not isinstance(instance["schema"], dict):
|
||||
return "schema的'schema'字段必须是字典,详见https://json-schema.org/"
|
||||
if "strict" in instance and not isinstance(instance["strict"], bool):
|
||||
return "schema的'strict'字段只能填入布尔值"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _remove_title(schema: dict[str, Any] | list[Any]) -> dict[str, Any] | list[Any]:
|
||||
"""
|
||||
递归移除JSON Schema中的title字段
|
||||
"""
|
||||
if isinstance(schema, list):
|
||||
# 如果当前Schema是列表,则对所有dict/list子元素递归调用
|
||||
for idx, item in enumerate(schema):
|
||||
if isinstance(item, (dict, list)):
|
||||
schema[idx] = _remove_title(item)
|
||||
elif isinstance(schema, dict):
|
||||
# 是字典,移除title字段,并对所有dict/list子元素递归调用
|
||||
if "title" in schema:
|
||||
del schema["title"]
|
||||
for key, value in schema.items():
|
||||
if isinstance(value, (dict, list)):
|
||||
schema[key] = _remove_title(value)
|
||||
|
||||
return schema
|
||||
|
||||
|
||||
def _link_definitions(schema: dict[str, Any]) -> dict[str, Any]:
|
||||
"""
|
||||
链接JSON Schema中的definitions字段
|
||||
"""
|
||||
|
||||
def link_definitions_recursive(
|
||||
path: str, sub_schema: list[Any] | dict[str, Any], defs: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
递归链接JSON Schema中的definitions字段
|
||||
:param path: 当前路径
|
||||
:param sub_schema: 子Schema
|
||||
:param defs: Schema定义集
|
||||
:return:
|
||||
"""
|
||||
if isinstance(sub_schema, list):
|
||||
# 如果当前Schema是列表,则遍历每个元素
|
||||
for i in range(len(sub_schema)):
|
||||
if isinstance(sub_schema[i], dict):
|
||||
sub_schema[i] = link_definitions_recursive(
|
||||
f"{path}/{str(i)}", sub_schema[i], defs
|
||||
)
|
||||
else:
|
||||
# 否则为字典
|
||||
if "$defs" in sub_schema:
|
||||
# 如果当前Schema有$def字段,则将其添加到defs中
|
||||
key_prefix = f"{path}/$defs/"
|
||||
for key, value in sub_schema["$defs"].items():
|
||||
def_key = key_prefix + key
|
||||
if def_key not in defs:
|
||||
defs[def_key] = value
|
||||
del sub_schema["$defs"]
|
||||
if "$ref" in sub_schema:
|
||||
# 如果当前Schema有$ref字段,则将其替换为defs中的定义
|
||||
def_key = sub_schema["$ref"]
|
||||
if def_key in defs:
|
||||
sub_schema = defs[def_key]
|
||||
else:
|
||||
raise ValueError(f"Schema中引用的定义'{def_key}'不存在")
|
||||
# 遍历键值对
|
||||
for key, value in sub_schema.items():
|
||||
if isinstance(value, (dict, list)):
|
||||
# 如果当前值是字典或列表,则递归调用
|
||||
sub_schema[key] = link_definitions_recursive(
|
||||
f"{path}/{key}", value, defs
|
||||
)
|
||||
|
||||
return sub_schema
|
||||
|
||||
return link_definitions_recursive("#", schema, {})
|
||||
|
||||
|
||||
def _remove_defs(schema: dict[str, Any]) -> dict[str, Any]:
|
||||
"""
|
||||
递归移除JSON Schema中的$defs字段
|
||||
"""
|
||||
if isinstance(schema, list):
|
||||
# 如果当前Schema是列表,则对所有dict/list子元素递归调用
|
||||
for idx, item in enumerate(schema):
|
||||
if isinstance(item, (dict, list)):
|
||||
schema[idx] = _remove_title(item)
|
||||
elif isinstance(schema, dict):
|
||||
# 是字典,移除title字段,并对所有dict/list子元素递归调用
|
||||
if "$defs" in schema:
|
||||
del schema["$defs"]
|
||||
for key, value in schema.items():
|
||||
if isinstance(value, (dict, list)):
|
||||
schema[key] = _remove_title(value)
|
||||
|
||||
return schema
|
||||
|
||||
|
||||
class RespFormat:
|
||||
"""
|
||||
响应格式
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _generate_schema_from_model(schema):
|
||||
json_schema = {
|
||||
"name": schema.__name__,
|
||||
"schema": _remove_defs(
|
||||
_link_definitions(_remove_title(schema.model_json_schema()))
|
||||
),
|
||||
"strict": False,
|
||||
}
|
||||
if schema.__doc__:
|
||||
json_schema["description"] = schema.__doc__
|
||||
return json_schema
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
format_type: RespFormatType = RespFormatType.TEXT,
|
||||
schema: type | JsonSchema | None = None,
|
||||
):
|
||||
"""
|
||||
响应格式
|
||||
:param format_type: 响应格式类型(默认为文本)
|
||||
:param schema: 模板类或JsonSchema(仅当format_type为JSON Schema时有效)
|
||||
"""
|
||||
self.format_type: RespFormatType = format_type
|
||||
|
||||
if format_type == RespFormatType.JSON_SCHEMA:
|
||||
if schema is None:
|
||||
raise ValueError("当format_type为'JSON_SCHEMA'时,schema不能为空")
|
||||
if isinstance(schema, dict):
|
||||
if check_msg := _json_schema_type_check(schema):
|
||||
raise ValueError(f"schema格式不正确,{check_msg}")
|
||||
|
||||
self.schema = schema
|
||||
elif issubclass(schema, BaseModel):
|
||||
try:
|
||||
json_schema = self._generate_schema_from_model(schema)
|
||||
|
||||
self.schema = json_schema
|
||||
except Exception as e:
|
||||
raise ValueError(
|
||||
f"自动生成JSON Schema时发生异常,请检查模型类{schema.__name__}的定义,详细信息:\n"
|
||||
f"{schema.__name__}:\n"
|
||||
) from e
|
||||
else:
|
||||
raise ValueError("schema必须是BaseModel的子类或JsonSchema")
|
||||
else:
|
||||
self.schema = None
|
||||
|
||||
def to_dict(self):
|
||||
"""
|
||||
将响应格式转换为字典
|
||||
:return: 字典
|
||||
"""
|
||||
if self.schema:
|
||||
return {
|
||||
"format_type": self.format_type.value,
|
||||
"schema": self.schema,
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"format_type": self.format_type.value,
|
||||
}
|
||||
155
src/llm_models/payload_content/tool_option.py
Normal file
155
src/llm_models/payload_content/tool_option.py
Normal file
@@ -0,0 +1,155 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ToolParamType(Enum):
|
||||
"""
|
||||
工具调用参数类型
|
||||
"""
|
||||
|
||||
String = "string" # 字符串
|
||||
Int = "integer" # 整型
|
||||
Float = "float" # 浮点型
|
||||
Boolean = "bool" # 布尔型
|
||||
|
||||
|
||||
class ToolParam:
|
||||
"""
|
||||
工具调用参数
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, name: str, param_type: ToolParamType, description: str, required: bool
|
||||
):
|
||||
"""
|
||||
初始化工具调用参数
|
||||
(不应直接修改ToolParam类,而应使用ToolOptionBuilder类来构建对象)
|
||||
:param name: 参数名称
|
||||
:param param_type: 参数类型
|
||||
:param description: 参数描述
|
||||
:param required: 是否必填
|
||||
"""
|
||||
self.name: str = name
|
||||
self.param_type: ToolParamType = param_type
|
||||
self.description: str = description
|
||||
self.required: bool = required
|
||||
|
||||
|
||||
class ToolOption:
|
||||
"""
|
||||
工具调用项
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
description: str,
|
||||
params: list[ToolParam] | None = None,
|
||||
):
|
||||
"""
|
||||
初始化工具调用项
|
||||
(不应直接修改ToolOption类,而应使用ToolOptionBuilder类来构建对象)
|
||||
:param name: 工具名称
|
||||
:param description: 工具描述
|
||||
:param params: 工具参数列表
|
||||
"""
|
||||
self.name: str = name
|
||||
self.description: str = description
|
||||
self.params: list[ToolParam] | None = params
|
||||
|
||||
|
||||
class ToolOptionBuilder:
|
||||
"""
|
||||
工具调用项构建器
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.__name: str = ""
|
||||
self.__description: str = ""
|
||||
self.__params: list[ToolParam] = []
|
||||
|
||||
def set_name(self, name: str) -> "ToolOptionBuilder":
|
||||
"""
|
||||
设置工具名称
|
||||
:param name: 工具名称
|
||||
:return: ToolBuilder实例
|
||||
"""
|
||||
if not name:
|
||||
raise ValueError("工具名称不能为空")
|
||||
self.__name = name
|
||||
return self
|
||||
|
||||
def set_description(self, description: str) -> "ToolOptionBuilder":
|
||||
"""
|
||||
设置工具描述
|
||||
:param description: 工具描述
|
||||
:return: ToolBuilder实例
|
||||
"""
|
||||
if not description:
|
||||
raise ValueError("工具描述不能为空")
|
||||
self.__description = description
|
||||
return self
|
||||
|
||||
def add_param(
|
||||
self,
|
||||
name: str,
|
||||
param_type: ToolParamType,
|
||||
description: str,
|
||||
required: bool = False,
|
||||
) -> "ToolOptionBuilder":
|
||||
"""
|
||||
添加工具参数
|
||||
:param name: 参数名称
|
||||
:param param_type: 参数类型
|
||||
:param description: 参数描述
|
||||
:param required: 是否必填(默认为False)
|
||||
:return: ToolBuilder实例
|
||||
"""
|
||||
if not name or not description:
|
||||
raise ValueError("参数名称/描述不能为空")
|
||||
|
||||
self.__params.append(
|
||||
ToolParam(
|
||||
name=name,
|
||||
param_type=param_type,
|
||||
description=description,
|
||||
required=required,
|
||||
)
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
def build(self):
|
||||
"""
|
||||
构建工具调用项
|
||||
:return: 工具调用项
|
||||
"""
|
||||
if self.__name == "" or self.__description == "":
|
||||
raise ValueError("工具名称/描述不能为空")
|
||||
|
||||
return ToolOption(
|
||||
name=self.__name,
|
||||
description=self.__description,
|
||||
params=None if len(self.__params) == 0 else self.__params,
|
||||
)
|
||||
|
||||
|
||||
class ToolCall:
|
||||
"""
|
||||
来自模型反馈的工具调用
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
call_id: str,
|
||||
func_name: str,
|
||||
args: dict | None = None,
|
||||
):
|
||||
"""
|
||||
初始化工具调用
|
||||
:param call_id: 工具调用ID
|
||||
:param func_name: 要调用的函数名称
|
||||
:param args: 工具调用参数
|
||||
"""
|
||||
self.call_id: str = call_id
|
||||
self.func_name: str = func_name
|
||||
self.args: dict | None = args
|
||||
Reference in New Issue
Block a user