OpenAI API 的智能助手系统总结(附github源码)
init.py 文件是将一个目录标记为 Python 包的必要条件。当一个目录包含 init.py 文件时,Python 解释器会将该目录视为一个包,这样就可以使用 import 语句导入该目录下的模块。
一、核心代码
# main.py
# 导入 logging 模块,用于配置和记录日志信息
import logging
# 导入 asyncio 模块,用于支持异步编程
import asyncio
# 从 openai 库中导入 AsyncOpenAI 和 OpenAI 类,分别用于创建异步和同步的 OpenAI 客户端
from openai import AsyncOpenAI, OpenAI
# 从 server.assistant 模块中导入 OpenAIAssistant 类,用于管理 OpenAI 助手
from server.assistant import OpenAIAssistant
# 从 server.utils 模块中导入多个辅助函数,用于创建助手、线程,删除线程以及与助手进行对话
from server.utils import create_assistant, create_thread, delete_thread, chat_with_assistant
# 配置日志的基本设置,将日志级别设置为 INFO,即记录所有信息级别的日志
logging.basicConfig(level=logging.INFO)
# 创建一个名为 __name__ 的日志记录器对象,__name__ 通常是当前模块的名称
logger = logging.getLogger(__name__)
# 定义一个异步函数 main,作为程序的主函数
async def main():
# 初始化一个异步的 OpenAI 客户端,用于进行异步的 API 调用
client = AsyncOpenAI()
# 初始化一个同步的 OpenAI 客户端,用于进行同步的 API 调用
sync_client = OpenAI()
# 创建 OpenAIAssistant 类的一个实例,将异步客户端传递给它
assistant_instance = OpenAIAssistant(client=client)
# 调用 create_assistant 函数,异步创建一个 OpenAI 助手对象,并将其赋值给 assistant 变量
assistant = await create_assistant(assistant_instance)
# 调用 create_thread 函数,异步创建一个新的线程,并将其赋值给 thread 变量
thread = await create_thread(client=client)
# 进入一个无限循环,用于持续与用户进行交互
while True:
try:
# 提示用户输入问题,并将用户输入的内容赋值给 query 变量
query = input("请输入你的问题. (输入 '退出' 结束当前对话):")
# 检查用户输入的内容是否为 '退出',忽略大小写并去除前后空格
if query.lower().strip() == "退出":
# 如果用户输入 '退出',则跳出循环,结束对话
break
# 调用 chat_with_assistant 函数,与助手进行对话,异步获取响应的每个 token
async for token in chat_with_assistant(assistant=assistant, thread=thread, user_query=query, client=client):
# 打印每个 token,不换行
print(token, end='')
# 捕获所有异常
except Exception:
# 使用日志记录器记录异常信息,方便调试和排查问题
logger.exception("error in chat: ")
# 调用 delete_thread 函数,删除之前创建的线程,传入线程 ID 和同步客户端
delete_thread(thread_id=thread.id, sync_client=sync_client)
# 判断当前模块是否作为主程序运行
if __name__ == '__main__':
# 调用 asyncio.run 函数,运行异步的 main 函数
asyncio.run(main())
# AssistantStreaming/server/assistant.py
# 从 openai 库中导入 AsyncOpenAI 和 OpenAI 类,分别用于创建异步和同步的 OpenAI 客户端
from openai import AsyncOpenAI, OpenAI
# 导入 logging 模块,用于配置和记录日志信息
import logging
# 配置日志的基本设置,将日志级别设置为 INFO,即记录所有信息级别的日志
logging.basicConfig(level=logging.INFO)
# 创建一个名为 __name__ 的日志记录器对象,__name__ 通常是当前模块的名称
logger = logging.getLogger(__name__)
# 定义一个名为 OpenAIAssistant 的类,用于管理 OpenAI 助手的创建、更新等操作
class OpenAIAssistant:
# 类的初始化方法,当创建类的实例时会自动调用
def __init__(self, client):
# 将传入的客户端对象赋值给实例属性 client,用于后续的 API 调用
self.client = client
# 初始化助手的 ID 为 None,后续会根据操作进行更新
self.assistant_id = None
# 定义一个异步方法,用于获取现有的助手对象,如果不存在则创建一个新的助手对象
async def get_or_create_assistant(self, name, model):
try:
# 尝试使用客户端的 retrieve 方法根据名称获取现有的助手对象
self.assistant = await self.client.beta.assistants.retrieve(name=name)
# 如果成功获取到助手对象,将其 ID 赋值给实例属性 assistant_id
self.assistant_id = self.assistant.id
# 返回当前类的实例本身,以便进行链式调用
return self
# 如果在获取助手对象时出现异常,说明该助手可能不存在
except Exception:
# 使用客户端的 create 方法创建一个新的助手对象
self.assistant = await self.client.beta.assistants.create(
# 助手的名称,由传入的参数指定
name=name,
# 助手使用的模型,由传入的参数指定
model=model,
# 助手的指令,告诉助手如何回答用户的问题
instructions="You are a helpful AI assistant who is adept at using tools to answer questions posed by users",
)
# 将新创建的助手对象的 ID 赋值给实例属性 assistant_id
self.assistant_id = self.assistant.id
# 返回当前类的实例本身,以便进行链式调用
return self
# 定义一个异步方法,用于更新助手的描述和指令
async def set_description_and_instructions(self, instructions):
# 使用客户端的 update 方法更新助手的指令
self.assistant = await self.client.beta.assistants.update(
# 要更新的助手的 ID
assistant_id=self.assistant_id,
# 新的指令内容
instructions=instructions
)
# 返回当前类的实例本身,以便进行链式调用
return self
# 定义一个异步方法,用于设置助手可以使用的工具
async def set_tools(self, tools):
# 检查传入的工具列表中是否包含 {"type": "file_search"} 类型的工具
contains_file_search = any(tool['type'] == 'file_search' for tool in tools)
# 打开名为 vector_store_id.txt 的文件,以读取模式打开
with open('vector_store_id.txt', 'r') as file:
# 读取文件中的内容,并去除首尾的空白字符
vector_store_id = file.read().strip()
# 使用日志记录器记录读取到的向量存储 ID
logger.info(f"written vector_store_id:{vector_store_id}")
# 根据是否包含 file_search 工具进行不同的操作
if contains_file_search:
# 如果包含 file_search 工具,使用客户端的 update 方法更新助手的工具和工具资源
await self.client.beta.assistants.update(
# 要更新的助手的 ID
assistant_id=self.assistant_id,
# 新的工具列表
tools=tools,
# 工具资源,指定 file_search 工具使用的向量存储 ID
tool_resources={"file_search": {"vector_store_ids": [vector_store_id]}}
)
else:
# 如果不包含 file_search 工具,只更新助手的工具列表
await self.client.beta.assistants.update(
# 要更新的助手的 ID
assistant_id=self.assistant_id,
# 新的工具列表
tools=tools
)
# 返回当前类的实例本身,以便进行链式调用
return self
# 判断当前模块是否作为主程序运行
if __name__ == '__main__':
# 创建一个异步的 OpenAI 客户端对象
client = AsyncOpenAI()
# 创建一个同步的 OpenAI 客户端对象
sync_client = OpenAI()
# 创建 OpenAIAssistant 类的一个实例,传入异步客户端对象
assistant_instance = OpenAIAssistant(client=client)
# 打印创建的 OpenAIAssistant 类的实例
print(assistant_instance)
# AssistantStreaming/server/run.py
# 导入 NoneType 类型,用于后续类型判断
from types import NoneType
# 导入 Type 用于类型注解,表示类型本身;get_origin 用于获取泛型类型的原始类型;
# get_args 用于获取泛型类型的参数;Union 用于定义联合类型;Any 表示任意类型
from typing import Type, get_origin, get_args, Union, Any
# 定义 AsyncChain 类,用于对一个对象进行一系列的异步操作,并按顺序执行这些操作
class AsyncChain:
"""
AsyncChain 类的目的是允许对一个对象进行一系列的异步操作,并按顺序执行这些操作。
"""
# 类的初始化方法,当创建类的实例时会自动调用
def __init__(self, obj):
"""
初始化 AsyncChain,设置目标对象,方法将在此对象上被调用。
:param obj: 方法将被调用的对象。
"""
# 将传入的对象赋值给实例属性 _obj,作为后续操作的目标对象
self._obj = obj # obj 是任何一个 Python 对象
# 初始化一个空列表 _calls,用于存储所有待执行的异步方法调用
self._calls = [] # 初始化函数将传入的对象保存在 self._obj 中,并初始化一个空列表 self._calls,用于存储所有待执行的异步方法调用。
# 定义 Python 魔法方法,当尝试访问对象中不存在的属性(这里是方法)时会被调用
def __getattr__(self, name):
"""
这个方法是 Python 的魔法方法,它在你尝试访问对象的某个属性(这里是方法)时被调用,但这个属性在对象的常规属性列表中不存在。
返回一个方法,该方法会将它的异步调用添加到链中。
:param name: 对象上要调用的方法的名称。
:return: 一个可调用对象,将异步方法调用添加到链中。
"""
# 定义一个内部函数 method,用于处理方法调用和参数传递
def method(*args, **kwargs):
# 定义一个异步内部函数 async_call,用于实际执行异步方法调用
async def async_call():
# 从 _obj 对象中获取名称为 name 的方法
func = getattr(self._obj, name)
# 调用该方法并等待其执行完成,将结果赋值给 _obj
self._obj = await func(*args, **kwargs)
# 将 async_call 函数添加到 _calls 列表中,以便后续按顺序执行
self._calls.append(async_call)
# 返回当前类的实例本身,实现链式调用
return self
# 返回 method 函数,使其可以被调用
return method
# 定义一个异步方法 execute,用于按顺序执行 _calls 列表中的所有异步方法调用
async def execute(self):
"""
execute 方法是一个异步方法,它按照 self._calls 列表中的顺序依次执行所有的异步方法调用。
按添加的顺序执行所有链式的异步方法调用。
:return: 经过所有链式调用修改后的对象。
"""
# 遍历 _calls 列表中的每个异步方法调用
for call in self._calls:
# 等待并执行每个异步方法调用
await call()
# 返回经过所有链式调用修改后的对象
return self._obj
# AssistantStreaming/server/utils.py
# 从 openai.types.beta 模块导入 Assistant 和 Thread 类,用于处理 OpenAI 助手和线程相关操作
from openai.types.beta import Assistant, Thread
# 从 openai.types.beta.threads 模块导入 Run 和 RequiredActionFunctionToolCall 类,用于处理运行和工具调用相关操作
from openai.types.beta.threads import Run, RequiredActionFunctionToolCall
# 从 openai.types.beta.assistant_stream_event 模块导入多个事件类,用于处理助手流事件
from openai.types.beta.assistant_stream_event import (
ThreadRunRequiresAction, ThreadMessageDelta, ThreadRunCompleted,
ThreadRunFailed, ThreadRunCancelling, ThreadRunCancelled, ThreadRunExpired, ThreadRunStepFailed,
ThreadRunStepCancelled)
# 从 server.run 模块导入 AsyncChain 类,用于实现异步链式调用
from server.run import AsyncChain
# 从 tools.python_inter 模块导入 PythonInterpreterTool 类,这是一个自定义的 Python 代码执行工具
from tools.python_inter import PythonInterpreterTool
# 从 tools.utils 模块导入 generate_openai_function_spec 函数,用于生成符合 OpenAI API 函数调用格式的规格说明
from tools.utils import generate_openai_function_spec
# 导入 logging 模块,用于记录程序运行过程中的信息
import logging
# 导入 asyncio 模块,用于支持异步编程
import asyncio
# 导入 json 模块,用于处理 JSON 数据
import json
# 从 typing 模块导入 Dict 类型,用于类型注解
from typing import Dict
# 配置日志的基本设置,将日志级别设置为 INFO,即记录所有信息级别的日志
logging.basicConfig(level=logging.INFO)
# 创建一个名为 __name__ 的日志记录器对象,__name__ 通常是当前模块的名称
logger = logging.getLogger(__name__)
# 定义一个全局变量 tool_instances,用于存储工具实例的字典
tool_instances = {}
# 定义一个异步函数 create_assistant,用于创建 OpenAI 助手对象
async def create_assistant(assistant_instant) -> Assistant:
# 声明使用全局变量 tool_instances
global tool_instances
# 定义助手的名称
assistant_name = "Data Engineer"
# 定义助手使用的模型
assistant_model = "gpt-4o"
# 定义助手的指令,告诉助手如何回答用户的问题
assistant_instructions = "You're a senior data analyst. When asked for data information, write and run Python code to answer the question"
# 创建一个 AsyncChain 类的实例,用于管理异步链式调用
chain = AsyncChain(assistant_instant)
# 定义一个列表,包含内置的工具,这里只有 file_search 工具
default_tools = [
{"type": "file_search"}
]
# 定义一个列表,包含自定义工具,这里只有 PythonInterpreterTool 工具
tools = [PythonInterpreterTool]
# 创建一个字典,将工具类的名称映射到工具类的实例,同时传入日志记录器
tool_instances = {tool_cls.get_name(): tool_cls(logger=logger) for tool_cls in tools}
# 生成每个工具类的 OpenAI 函数规格说明列表
tools_spec = [generate_openai_function_spec(tool_cls) for tool_cls in tools]
# 将自定义工具的规格说明添加到内置工具列表中
default_tools.extend(tools_spec)
# 执行异步链式调用,依次调用 get_or_create_assistant、set_description_and_instructions 和 set_tools 方法,最后调用 execute 方法执行所有操作
openai_assistant_instance = await (
chain.get_or_create_assistant(name=assistant_name, model=assistant_model)
.set_description_and_instructions(instructions=assistant_instructions)
.set_tools(default_tools)
.execute()
)
# 从助手实例中获取 OpenAI 助手对象
openai_assistant = openai_assistant_instance.assistant
# 使用日志记录器记录创建的助手名称和 ID
logger.info(f"created assistant {openai_assistant.name} with id: {openai_assistant.id}")
# 返回创建的 OpenAI 助手对象
return openai_assistant
# 定义一个异步函数 create_thread,用于创建一个新的 OpenAI 线程
async def create_thread(client) -> Thread:
# 调用客户端的 beta.threads.create 方法创建一个新的线程
thread = await client.beta.threads.create()
# 使用日志记录器记录创建的新线程的 ID
logger.info(f"created new thread: {thread.id}")
# 返回创建的线程对象
return thread
# 定义一个函数 delete_thread,用于删除指定 ID 的 OpenAI 线程
def delete_thread(thread_id, sync_client):
# 调用同步客户端的 beta.threads.delete 方法删除指定 ID 的线程
thread_deleted = sync_client.beta.threads.delete(thread_id=thread_id)
# 使用日志记录器记录删除的线程 ID 和删除结果
logger.info(f"deleted thread {thread_id}: {thread_deleted.deleted}")
# 定义一个异步函数 kill_if_thread_is_running,用于检查并终止正在运行的线程
async def kill_if_thread_is_running(thread_id: str, client):
# 调用客户端的 beta.threads.runs.list 方法获取指定线程的所有运行记录
runs = client.beta.threads.runs.list(
thread_id=thread_id
)
# 初始化一个空列表,用于存储正在运行的线程记录
running_threads = []
# 异步遍历所有运行记录
async for run in runs:
# 检查运行记录的状态是否为正在运行、排队、需要操作或正在取消
if run.status in ["in_progress", "queued", "requires_action", "cancelling"]:
# 如果是,则将该运行记录添加到正在运行的线程记录列表中
running_threads.append(run)
# 定义一个异步内部函数 kill_run,用于终止指定的运行记录
async def kill_run(run_to_kill: Run):
# 初始化计数器
counter = 0
try:
# 进入无限循环,直到运行记录被终止
while True:
# 调用客户端的 beta.threads.runs.retrieve 方法获取指定运行记录的最新状态
run_obj = await client.beta.threads.runs.retrieve(run_id=run_to_kill.id, thread_id=thread_id)
# 获取运行记录的状态
run_status = run_obj.status
# 检查运行记录的状态是否为正在取消
if run_status == "cancelling":
# 如果是,则使用日志记录器记录信息,并等待 2 秒后继续检查
logger.info(f"run {run_to_kill.id} is being cancelled, waiting for it to get cancelled")
await asyncio.sleep(2)
continue
# 检查运行记录的状态是否为已取消、失败、完成或过期
if run_status in ["cancelled", "failed", "completed", "expired"]:
# 如果是,则使用日志记录器记录信息,并跳出循环
logger.info(f"run {run_to_kill.id} is cancelled")
break
# 调用客户端的 beta.threads.runs.cancel 方法尝试取消指定的运行记录
run_obj = await client.beta.threads.runs.cancel(
thread_id=thread_id,
run_id=run_to_kill.id
)
# 检查取消操作后的运行记录状态是否为已取消、失败或过期
if run_obj.status in ["cancelled", "failed", "expired"]:
# 如果是,则使用日志记录器记录信息,并跳出循环
logger.info(
f"run {run_obj.id} for thread {thread_id} is killed. status is {run_obj.status}")
break
else:
# 如果不是,则使用日志记录器记录信息,计数器加 1,并等待 2 秒后继续尝试取消
logger.info(
f"run {run_obj.id} for thread {thread_id} is not yet killed. status is {run_obj.status}")
counter += 1
await asyncio.sleep(2)
continue
except Exception:
# 如果在取消过程中出现异常,则使用日志记录器记录异常信息,并抛出异常
logger.exception(f"error in killing thread: {thread_id}")
raise Exception(f"error in killing thread: {thread_id}")
# 检查是否有正在运行的线程记录
if not running_threads:
# 如果没有,则使用日志记录器记录信息并返回
logger.info(f"no running threads for thread : {thread_id}")
return
if running_threads:
# 如果有,则使用日志记录器记录正在运行的线程记录数量
logger.info(f"total {len(running_threads)} running threads")
# 初始化一个空列表,用于存储异步任务
tasks = []
# 遍历所有正在运行的线程记录
for run_obj in running_threads:
# 创建一个异步任务,用于终止指定的运行记录
task = asyncio.create_task(kill_run(run_obj))
# 短暂暂停,让事件循环有机会处理其他任务
await asyncio.sleep(0)
# 将异步任务添加到任务列表中
tasks.append(task)
# 等待所有异步任务完成,设置超时时间为 120 秒
done, pending = await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED, timeout=120)
# 初始化异常计数器
no_of_exceptions = 0
# 遍历所有已完成的任务
for done_task in done:
# 检查任务是否有异常
if done_task.exception() is None:
# 如果没有异常,则获取任务的结果
task_result = done_task.result()
if task_result:
# 如果有结果,则使用日志记录器记录任务状态和结果
logger.info(f"status of run kill task: {done_task} is {task_result}")
else:
# 如果有异常,则使用日志记录器记录异常信息,并增加异常计数器
if logger:
logger.exception(f"error in run kill task: {done_task}, "
f"exception: {done_task.exception()}")
no_of_exceptions += 1
# 遍历所有未完成的任务
for pending_task in pending:
# 使用日志记录器记录取消未完成任务的信息
logger.info(f"cancelling run kill task: {pending_task}")
# 取消未完成的任务
pending_task.cancel()
# 检查是否有异常或未完成的任务
if no_of_exceptions > 0 or pending:
# 如果有,则抛出异常
raise Exception("failed to kill running threads")
# 定义一个异步函数 handle_function_call,用于处理单个工具调用
async def handle_function_call(tool_call: RequiredActionFunctionToolCall) -> (str, str):
# 检查工具调用的类型是否为函数调用
if tool_call.type != "function":
# 如果不是,则返回 None 和 None
return None, None
# 获取工具调用的 ID
tool_id = tool_call.id
# 获取工具调用的函数信息
function = tool_call.function
# 获取函数的名称
function_name = function.name
# 将函数的参数从 JSON 字符串解析为 Python 字典
function_args = json.loads(function.arguments)
try:
# 使用日志记录器记录正在调用的函数名称和参数
logger.info(f"calling function {function_name} with args: {function_args}")
# 调用工具实例的 arun 方法异步执行函数,并获取结果
function_result = await tool_instances[function_name].arun(**function_args)
# 使用日志记录器记录从函数获取的结果
logger.info(f"got result from {function_name}: {function_result}")
except Exception as e:
# 如果在执行过程中出现异常,则使用日志记录器记录异常信息,并将结果设置为 None
logger.exception(f"Error handling function call: {e}")
function_result = None
# 返回工具调用的 ID 和执行结果
return tool_id, function_result
# 定义一个异步函数 handle_function_calls,用于处理多个工具调用
async def handle_function_calls(run_obj: Run) -> Dict[str, str]:
# 获取运行记录的所需操作信息
required_action = run_obj.required_action
# 检查所需操作的类型是否为提交工具输出
if required_action.type != "submit_tool_outputs":
# 如果不是,则返回空字典
return {}
# 获取所需操作的工具调用列表
tool_calls = required_action.submit_tool_outputs.tool_calls
# 异步并发处理所有工具调用,并获取结果
results = await asyncio.gather(
*(handle_function_call(tool_call) for tool_call in tool_calls)
)
# 创建一个字典,将工具调用的 ID 映射到执行结果,过滤掉 ID 为 None 的结果
return {tool_id: result for tool_id, result in results if tool_id is not None}
# 定义一个异步函数 submit_tool_outputs,用于提交工具输出
async def submit_tool_outputs(thread_id: str, run_id: str, function_ids_to_result_map: Dict[str, str], client,
stream=False):
# 创建一个列表,包含每个工具调用的 ID 和输出结果
tool_outputs = [{"tool_call_id": tool_id, "output": result if result is not None else ""} for tool_id, result in
function_ids_to_result_map.items()]
# 使用日志记录器记录正在提交的工具输出信息
logger.info(f"submitting tool outputs: {tool_outputs}")
# 调用客户端的 beta.threads.runs.submit_tool_outputs 方法提交工具输出
run = await client.beta.threads.runs.submit_tool_outputs(thread_id=thread_id,
run_id=run_id,
tool_outputs=tool_outputs,
stream=stream)
# 返回提交工具输出后的运行记录
return run
# 定义一个异步生成器函数 process_event,用于处理助手流事件
async def process_event(event, thread: Thread, client, **kwargs):
# 检查事件是否为线程消息增量事件
if isinstance(event, ThreadMessageDelta):
# 获取事件数据中的消息内容
data = event.data.delta.content
# 遍历消息内容中的每个文本块
for text in data:
# 生成文本块的值
yield text.text.value
# 打印文本块的值,不换行
# print(text.text.value, end='', flush=True)
# 检查事件是否为线程运行需要操作事件
elif isinstance(event, ThreadRunRequiresAction):
# 获取事件数据中的运行记录
run_obj = event.data
# 处理运行记录中的工具调用,并获取结果
function_ids_to_result_map = await handle_function_calls(run_obj)
# 提交工具输出,并获取工具输出事件流
tool_output_events = await submit_tool_outputs(thread.id,
run_obj.id,
function_ids_to_result_map,
client=client,
stream=True)
# 异步遍历工具输出事件流
async for tool_event in tool_output_events:
# 递归处理每个工具输出事件,并生成每个事件的文本块值
async for token in process_event(tool_event, thread=thread, client=client, **kwargs):
yield token
# 检查事件是否为线程运行失败、正在取消、已取消、已过期、运行步骤失败或运行步骤已取消事件
elif any(isinstance(event, cls) for cls in [ThreadRunFailed, ThreadRunCancelling, ThreadRunCancelled,
ThreadRunExpired, ThreadRunStepFailed, ThreadRunStepCancelled]):
# 如果是,则抛出异常
raise Exception("Run failed")
# 检查事件是否为线程运行完成事件
elif isinstance(event, ThreadRunCompleted):
# 如果是,则打印运行完成信息
print("\nRun completed")
else:
# 其他情况,不做处理
pass
# print("\nRun in progress")
# 定义一个异步生成器函数 chat_with_assistant,用于与助手进行对话
async def chat_with_assistant(assistant: Assistant, thread: Thread, user_query: str, client, **kwargs):
# 检查并终止正在运行的线程
await kill_if_thread_is_running(thread_id=thread.id, client=client)
# 在指定线程中创建一条用户消息
message = await client.beta.threads.messages.create(thread_id=thread.id, role="user", content=user_query)
# 使用日志记录器记录创建的消息信息
logger.info(f"created message: {message}")
# 在指定线程中创建一个运行记录,并开启流式响应
stream = await client.beta.threads.runs.create(
thread_id=thread.id,
assistant_id=assistant.id,
stream=True
)
# 异步遍历流式响应中的每个事件
async for event in stream:
# 处理每个事件,并生成每个事件的文本块值
async for token in process_event(event, thread, client=client, **kwargs):
yield token
# 判断当前模块是否作为主程序运行
if __name__ == '__main__':
# 从 openai 模块导入 OpenAI 类
from openai import OpenAI
# 创建一个 OpenAI 客户端实例
client = OpenAI()
# 定义一个包含多个文件路径的列表,这些文件是要上传到 OpenAI 的文件
# 这些文件存储在项目目录下的 "../data/01_LLMs/" 文件夹中,包含了关于 AI 开发和模型介绍的 PDF 文件
file_paths = [
"../data/01_LLMs/AI Agent开发入门.pdf",
"../data/01_LLMs/ChatGLM3-6B零基础部署与使用指南.pdf",
"../data/01_LLMs/ChatGLM3模型介绍.pdf"
]
# 初始化一个空列表,用于存储上传文件后返回的文件 ID
uploaded_files = []
# 遍历文件路径列表中的每个文件路径
for path in file_paths:
# 以二进制只读模式打开当前文件路径对应的文件
with open(path, "rb") as file:
# 使用 OpenAI 客户端的 files.create 方法上传文件
# 参数 file 是打开的文件对象,purpose="assistants" 表示文件用途是用于辅助工具
new_file = client.files.create(
file=file,
purpose="assistants"
)
# 将上传文件后返回的文件 ID 添加到 uploaded_files 列表中
uploaded_files.append(new_file.id)
# 打印上传文件的文件 ID 列表,方便查看上传结果
print(f"uploaded_files:{uploaded_files}")
# 使用 OpenAI 客户端的 beta.vector_stores.create 方法创建一个向量存储
# 向量存储的名称设置为 "llms",并将之前上传文件的文件 ID 列表作为参数传入
vector_store = client.beta.vector_stores.create(
name="llms",
file_ids=uploaded_files
)
# 打印创建的向量存储的 ID,方便查看创建结果
print(f"vector_store:{vector_store.id}")
# 定义一个文件路径,用于存储向量存储的 ID
# 这里的路径可以根据项目实际情况进行修改
file_path = "../vector_store_id.txt"
# 以写入模式打开指定文件路径对应的文件
with open(file_path, 'w') as file:
# 将向量存储的 ID 写入文件中
file.write(vector_store.id)
# 打印提示信息,表明向量存储的 ID 已经成功写入指定文件
print(f"Vector Store ID '{vector_store.id}' has been written to {file_path}")
# base_tool.py
# 从 pydantic 库中导入 BaseModel、Field 和 Extra 类
# BaseModel 用于创建数据模型,Field 用于定义字段的元数据,Extra 用于配置模型对额外字段的处理方式
from pydantic import BaseModel, Field, Extra
# 从 abc 模块中导入 ABC 和 abstractmethod
# ABC 是抽象基类,用于创建抽象类,abstractmethod 用于定义抽象方法
from abc import ABC, abstractmethod
# 从 typing 模块中导入 Any 类型
# Any 表示任意类型,用于类型注解
from typing import Any
# 定义一个抽象基类 BaseTool,继承自 ABC 和 BaseModel
# ABC 使得该类成为抽象类,不能被实例化,BaseModel 使得该类可以使用 pydantic 的数据验证和序列化功能
class BaseTool(ABC, BaseModel):
# 使用 pydantic 的 Field 类定义字段 'name',并附带描述
# ... 表示该字段是必需的,description 用于说明该字段的含义
name: str = Field(..., description="The name of the tool")
# 使用 pydantic 的 Field 类定义字段 'description',并附带描述
# ... 表示该字段是必需的,description 用于说明该字段的含义
description: str = Field(..., description="A description of what the tool does")
# __init_subclass__ 是 Python 的特殊方法,在子类被创建时自动调用
# 用于在子类创建时进行检查,确保每个子类都有 'name' 和 'description' 属性
def __init_subclass__(cls, **kwargs):
# 调用父类的 __init_subclass__ 方法,确保父类的初始化逻辑正常执行
super().__init_subclass__(**kwargs)
# 检查子类是否定义了 'name' 和 'description' 属性
if not hasattr(cls, 'name') or not hasattr(cls, 'description'):
# 如果没有定义,抛出 TypeError 异常
raise TypeError("Subclasses must define 'name' and 'description'")
# Config 类是 pydantic 模型的配置类,用于提供模型的配置信息
class Config:
# 配置类的文档字符串,说明该类的作用
"""Configuration for this pydantic object."""
# 允许模型接受额外字段
# 'allow' 表示模型可以接受在定义之外的字段
extra='allow'
# 允许模型使用任意类型的字段
# True 表示不限制字段的类型
arbitrary_types_allowed = True
# 定义一个静态抽象方法 get_name
# 静态方法不需要实例化类就可以调用,抽象方法必须由子类实现
@staticmethod
@abstractmethod
def get_name() -> str:
# 方法的文档字符串,说明该方法的作用
# 该方法用于获取工具的名称
"""Retrieve the name of the tool."""
# 如果子类没有实现该方法,抛出 NotImplementedError 异常
raise NotImplementedError("Subclasses must implement 'get_name' method")
# 定义一个静态抽象方法 get_description
# 静态方法不需要实例化类就可以调用,抽象方法必须由子类实现
@staticmethod
@abstractmethod
def get_description() -> str:
# 方法的文档字符串,说明该方法的作用
# 该方法用于获取工具的描述
"""Retrieve a description of the tool."""
# 如果子类没有实现该方法,抛出 NotImplementedError 异常
raise NotImplementedError("Subclasses must implement 'get_description' method")
# 定义一个静态抽象方法 get_args_schema
# 静态方法不需要实例化类就可以调用,抽象方法必须由子类实现
@staticmethod
@abstractmethod
def get_args_schema() -> Any:
# 方法的文档字符串,说明该方法的作用
# 该方法用于定义工具运行所需的参数结构
"""Retrieve the argument schema for the tool."""
# 如果子类没有实现该方法,抛出 NotImplementedError 异常
raise NotImplementedError("Subclasses must implement 'get_args_schema' method")
# 定义一个抽象方法 run
# 抽象方法必须由子类实现,用于定义工具的同步执行逻辑
@abstractmethod
def run(self, *args, **kwargs) -> str:
# 方法的文档字符串,说明该方法的作用
# 该方法用于同步运行工具
"""Run the tool synchronously."""
# 如果子类没有实现该方法,抛出 NotImplementedError 异常
raise NotImplementedError("Subclasses must implement 'run' method")
# 定义一个异步抽象方法 arun
# 异步方法使用 async 关键字定义,抽象方法必须由子类实现,用于定义工具的异步执行逻辑
async def arun(self, *args, **kwargs) -> str:
# 方法的文档字符串,说明该方法的作用
# 该方法用于异步运行工具
"""Run the tool asynchronously."""
# 如果子类没有实现该方法,抛出 NotImplementedError 异常
raise NotImplementedError("Subclasses must implement 'arun' method if needed")
# python_inter.py
# 导入 asyncio 库,用于支持异步编程
import asyncio
# 从 pydantic 库中导入 BaseModel 类,用于创建数据模型
from pydantic import BaseModel
# 从 typing 模块中导入 Type 类型,用于类型注解
from typing import Type
# 从 tools.base_tool 模块中导入 BaseTool 类,作为自定义工具类的基类
from tools.base_tool import BaseTool
# 定义一个继承自 BaseModel 的数据模型类 PythonInterpreterInput
# 该类用于定义 Python 解释器工具所需的输入参数
class PythonInterpreterInput(BaseModel):
# 定义一个字符串类型的字段 py_code,用于存储要执行的 Python 代码
py_code: str # Python 代码作为字符串
# 定义一个继承自 BaseTool 的工具类 PythonInterpreterTool
# 该类用于执行 Python 代码并返回结果或错误信息
class PythonInterpreterTool(BaseTool):
# 定义工具的名称
name: str = "PythonInterpreterTool"
# 定义工具的描述信息
description: str = "Executes Python code and returns the result or error message."
# 定义工具所需的输入参数的数据模型类型
args_schema: Type[BaseModel] = PythonInterpreterInput
# 类的构造函数,用于初始化对象的属性
def __init__(self, logger=None):
# 调用父类的构造函数,确保父类的初始化逻辑正常执行
super().__init__()
# 初始化日志记录器
self.logger = logger
# 静态方法,用于获取工具的名称
@staticmethod
def get_name():
# 返回工具的名称
return "PythonInterpreterTool"
# 静态方法,用于获取工具的描述信息
@staticmethod
def get_description():
# 返回工具的描述信息
return "A tool to execute Python code and returns the result or error message."
# 静态方法,用于获取工具所需的输入参数的数据模型类型
@staticmethod
def get_args_schema():
# 返回输入参数的数据模型类型
return PythonInterpreterInput
# 同步方法,用于执行 Python 代码
def run(self, py_code: str) -> str:
try:
# 尝试使用 eval 函数执行代码,如果是表达式,则返回表达式运行结果
return str(eval(py_code))
except Exception as e:
# 如果 eval 函数执行失败,则尝试使用 exec 函数执行代码
try:
# 执行代码
exec(py_code)
# 返回代码执行成功的信息
return "代码已顺利执行"
except Exception as exec_error:
# 如果 exec 函数执行失败,检查日志记录器是否存在
if self.logger:
# 如果存在,则记录错误信息
self.logger.error(f"Error while executing code: {exec_error}")
# 返回代码执行报错的信息
return f"代码执行时报错: {exec_error}"
# 异步方法,用于异步执行 Python 代码
async def arun(self, py_code: str) -> str:
"""
Asynchronously executes Python code and returns the result or error message.
:param py_code: The Python code to execute.
:return: The result of the execution or an error message if an exception occurs.
"""
# 获取当前的事件循环
loop = asyncio.get_running_loop()
try:
# 将 eval 函数运行在执行器中,以便异步运行
result = await loop.run_in_executor(None, eval, py_code)
# 返回执行结果的字符串表示
return str(result)
except Exception as e:
# 如果 eval 函数执行失败,则尝试使用 exec 函数异步执行代码
try:
# 将 exec 函数运行在执行器中,以便异步运行
await loop.run_in_executor(None, exec, py_code)
# 返回代码执行成功的信息
return "代码已顺利执行"
except Exception as exec_error:
# 如果 exec 函数执行失败,检查日志记录器是否存在
if self.logger:
# 如果存在,则记录错误信息
self.logger.error(f"Error while executing code: {exec_error}")
# 返回代码执行报错的信息
return f"代码执行时报错: {exec_error}"
# tools/utils.py
# 导入 typing 模块中的 Type、get_origin、get_args、Union 和 Any 类型
# Type 用于表示类型,get_origin 用于获取泛型类型的原始类型,get_args 用于获取泛型类型的参数
# Union 用于表示联合类型,Any 表示任意类型
from typing import Type, get_origin, get_args, Union, Any
# 从 types 模块中导入 NoneType 类型,用于表示 None 的类型
from types import NoneType
# 从 tools.base_tool 模块中导入 BaseTool 类,作为工具类的基类
from tools.base_tool import BaseTool
# 定义一个函数 infer_field_type,用于根据给定的 Python 类型推断对应的 JSON schema 类型
def infer_field_type(field_type: Any) -> str:
"""
根据给定的 Python 类型确定 JSON schema 类型,特别是处理 Optional 类型。
:param field_type: 要从中推断的 Python 类型。
:return: 表示 JSON schema 类型的字符串。
"""
# 检查 field_type 是否为 Union 类型
if get_origin(field_type) is Union:
# 提取实际类型,如果字段是 Optional(与 NoneType 的联合)
# get_args(field_type) 会返回 Union 类型的所有参数
# 过滤掉 NoneType,得到非 None 的类型列表
non_none_types = [t for t in get_args(field_type) if t is not type(None)]
# 如果非 None 类型列表不为空,则递归调用 infer_field_type 函数处理第一个非 None 类型
# 否则返回 'null'
return infer_field_type(non_none_types[0]) if non_none_types else 'null'
# 定义 Python 类型到 JSON schema 类型的映射字典
type_mappings = {
str: 'string', # 字符串类型映射到 'string'
int: 'integer', # 整数类型映射到 'integer'
float: 'float', # 浮点数类型映射到 'float'
bool: 'boolean' # 布尔类型映射到 'boolean'
# 根据需要添加更多映射
}
# 根据 field_type 在 type_mappings 中查找对应的 JSON schema 类型
# 如果找不到,则默认为 'string'
return type_mappings.get(field_type, 'string')
# 定义一个函数 generate_openai_function_spec,用于根据给定的工具类生成符合 OpenAI API 函数调用格式的规格说明
def generate_openai_function_spec(tool_class: Type[BaseTool]) -> dict:
"""
根据给定的工具类生成符合 OpenAI API 函数调用格式的规格说明。
:param tool_class: 要生成函数规格说明的类。
:return: 格式化为 OpenAI API 函数规格的字典。
"""
# 调用工具类的 get_name 方法获取函数名称
function_name = tool_class.get_name()
# 调用工具类的 get_description 方法获取函数描述
description = tool_class.get_description()
# 调用工具类的 get_args_schema 方法获取函数参数的模式
args_schema = tool_class.get_args_schema()
# 初始化一个空字典,用于存储函数参数的属性
properties = {}
# 初始化一个空列表,用于存储函数的必填参数
required_fields = []
# 遍历工具类中定义的 Pydantic 模型字段
# args_schema.__annotations__.items() 可以获取模型字段的名称和类型注解
for field_name, field_model in args_schema.__annotations__.items():
# 获取字段的详细信息
field_info = args_schema.__fields__[field_name]
# 获取字段的描述,如果没有描述则为空字符串
field_description = field_info.description or ''
# 调用 infer_field_type 函数推断字段的 JSON schema 类型
field_type = infer_field_type(field_model)
# 将字段的类型和描述添加到 properties 字典中
properties[field_name] = {"type": field_type, "description": field_description}
# 处理枚举类型,如果字段模型是枚举类型
if hasattr(field_model, '__members__'):
# 将枚举类型的所有值添加到 properties 字典的 'enum' 键中
properties[field_name]['enum'] = [e.value for e in field_model]
# 从必填字段中排除 Optional 字段:检查字段类型是否包含 NoneType,以判断是否是 Optional
if get_origin(field_model) is Union:
# 获取 Union 类型的所有参数
type_args = get_args(field_model)
# 如果参数中不包含 NoneType,则将该字段添加到必填字段列表中
if NoneType not in type_args:
required_fields.append(field_name)
else:
# 如果不是 Union 类型,则将该字段添加到必填字段列表中
required_fields.append(field_name)
# 构建符合 OpenAI API 函数调用格式的规格说明字典
function_spec = {
"type": "function", # 表明这是一个函数类型
"function": {
"name": function_name, # 函数名称
"description": description, # 函数描述
"parameters": {
"type": "object", # 参数类型为对象
"properties": properties, # 参数的属性
"required": required_fields # 必填参数
}
}
}
# 返回生成的函数规格说明字典
return function_spec
# 主程序入口
if __name__ == '__main__':
# 从 tools.python_inter 模块中导入 PythonInterpreterTool 类
from tools.python_inter import PythonInterpreterTool
# 调用 generate_openai_function_spec 函数生成 PythonInterpreterTool 类的函数规格说明
function_spec = generate_openai_function_spec(PythonInterpreterTool)
# 打印生成的函数规格说明
print(function_spec)
二、关注:工具可扩展性
1. BaseTool
类
类概述
BaseTool
类是一个抽象基类,其主要作用是为具体的工具类提供一个统一的接口和基本的结构。通过定义抽象方法和属性,强制要求子类实现特定的功能,以保证工具类的一致性和规范性。
类属性
name
:该属性用于存储工具的名称,不过在基类中未进行具体赋值,需要子类自行定义。description
:此属性用于存储工具的描述信息,同样在基类中未赋值,由子类来定义。
方法
__init_subclass__
:- 功能:这是一个特殊方法,会在子类被创建时自动调用。它的作用是检查子类是否定义了
name
和description
属性,如果没有定义,就会抛出TypeError
异常,以此确保子类的完整性。 - 代码示例:
- 功能:这是一个特殊方法,会在子类被创建时自动调用。它的作用是检查子类是否定义了
from abc import ABC, abstractmethod
from pydantic import BaseModel
class BaseTool(ABC, BaseModel):
name: str = None
description: str = None
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if not hasattr(cls, 'name') or not hasattr(cls, 'description'):
raise TypeError("Subclasses must define 'name' and 'description'")
get_name
:- 功能:这是一个抽象方法,子类必须实现该方法,其作用是返回工具的名称。
- 示例实现:
@staticmethod
@abstractmethod
def get_name() -> str:
pass
get_description
:- 功能:同样是抽象方法,子类需要实现它来返回工具的描述信息。
- 示例实现:
@staticmethod
@abstractmethod
def get_description() -> str:
pass
get_args_schema
:- 功能:抽象方法,子类实现该方法以返回工具参数的模式,用于描述工具所需的参数。
- 示例实现:
@staticmethod
@abstractmethod
def get_args_schema() -> Any:
pass
run
:- 功能:抽象方法,用于执行工具的同步操作,子类需要实现具体的逻辑。
- 示例实现:
@abstractmethod
def run(self, *args, **kwargs) -> str:
pass
arun
:- 功能:抽象方法,用于执行工具的异步操作,子类可根据需求实现异步逻辑。
- 示例实现:
async def arun(self, *args, **kwargs) -> str:
pass
2. PythonInterpreterTool
类
类概述
PythonInterpreterTool
类继承自 BaseTool
类,它实现了一个可以执行 Python 代码的工具。通过实现基类的抽象方法,提供了执行 Python 代码并返回结果或错误信息的功能。
类属性
name
:定义为"python_interpreter"
,明确了该工具的名称。description
:定义为"A tool to execute Python code and returns the result or error message."
,清晰地描述了工具的功能。
方法
__init__
:- 功能:初始化方法,接收一个
logger
参数,将其赋值给实例属性self.logger
,用于后续的日志记录。 - 代码示例:
- 功能:初始化方法,接收一个
import logging
class PythonInterpreterTool(BaseTool):
name = "python_interpreter"
description = "A tool to execute Python code and returns the result or error message."
def __init__(self, logger):
self.logger = logger
get_name
:- 功能:实现了基类的抽象方法,返回工具的名称
"python_interpreter"
。 - 代码示例:
- 功能:实现了基类的抽象方法,返回工具的名称
@staticmethod
def get_name() -> str:
return "python_interpreter"
get_description
:- 功能:实现了基类的抽象方法,返回工具的描述信息。
- 代码示例:
@staticmethod
def get_description() -> str:
return "A tool to execute Python code and returns the result or error message."
get_args_schema
:- 功能:实现了基类的抽象方法,返回工具参数的模式,这里定义了一个包含
code
字段的字典,用于接收要执行的 Python 代码。 - 代码示例:
- 功能:实现了基类的抽象方法,返回工具参数的模式,这里定义了一个包含
@staticmethod
def get_args_schema() -> dict:
return {
"code": {
"type": "string",
"description": "The Python code to execute."
}
}
run
:- 功能:实现了基类的抽象方法,用于同步执行 Python 代码。使用
exec
函数执行代码,并捕获可能出现的异常,最后将执行结果或错误信息返回。 - 代码示例:
- 功能:实现了基类的抽象方法,用于同步执行 Python 代码。使用
def run(self, code: str) -> str:
try:
exec_globals = {}
exec(code, exec_globals)
result = exec_globals.get('result', None)
if result is None:
return "Code executed successfully, but no result was returned."
return str(result)
except Exception as e:
self.logger.error(f"Error executing Python code: {e}")
return f"Error: {str(e)}"
arun
:- 功能:实现了基类的抽象方法,用于异步执行 Python 代码。实际上是调用了
run
方法,因为 Python 的exec
本身不是异步的,所以这里只是将同步操作封装成异步形式。 - 代码示例:
- 功能:实现了基类的抽象方法,用于异步执行 Python 代码。实际上是调用了
async def arun(self, code: str) -> str:
return self.run(code)
总结
BaseTool
类为工具类提供了一个通用的框架,确保所有工具类具有一致的接口和基本属性。PythonInterpreterTool
类继承自 BaseTool
类,实现了执行 Python 代码的具体功能,通过实现基类的抽象方法,保证了与其他工具类的兼容性和规范性。