异步构建集成外部函数的Assistant对象完整工程代码(附github)

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__
    • 功能:这是一个特殊方法,会在子类被创建时自动调用。它的作用是检查子类是否定义了 namedescription 属性,如果没有定义,就会抛出 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 函数执行代码,并捕获可能出现的异常,最后将执行结果或错误信息返回。
    • 代码示例
    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 本身不是异步的,所以这里只是将同步操作封装成异步形式。
    • 代码示例
    async def arun(self, code: str) -> str:
        return self.run(code)

总结

BaseTool 类为工具类提供了一个通用的框架,确保所有工具类具有一致的接口和基本属性。PythonInterpreterTool 类继承自 BaseTool 类,实现了执行 Python 代码的具体功能,通过实现基类的抽象方法,保证了与其他工具类的兼容性和规范性。

源码地址:https://github.com/fufankeji/AssistantStreaming
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值