ChatManager学习

概述

这段代码主要实现了一个基于WebSocket的聊天管理系统,使用FastAPI框架构建。主要功能包括管理聊天历史、处理客户端连接、消息传递、缓存管理以及与后端服务的交互。

主要组件

  1. ChatHistory
    • 功能:管理每个客户端的聊天历史记录。
    • 方法:
      • add_message:将消息添加到聊天历史中,并将其存储到数据库。
      • empty_history:清空指定客户端的聊天历史。
  2. ChatManager
    • 功能:管理WebSocket连接、处理消息传递、管理缓存和任务调度。
    • 属性:
      • active_connections:存储当前活跃的WebSocket连接。
      • chat_history:实例化的ChatHistory对象,用于管理聊天记录。
      • cache_managerin_memory_cache:用于缓存管理。
      • task_manager:管理异步任务。
      • active_clients:记录已连接的客户端信息。
      • stream_queue:记录流式输出结果的队列。
    • 主要方法:
      • connectreuse_connect:处理新的WebSocket连接或复用现有连接。
      • disconnect:断开WebSocket连接。
      • send_messagesend_json:向客户端发送消息或JSON数据。
      • close_connectionclose_client:关闭连接并进行清理。
      • ping:发送心跳消息。
      • set_cache:设置缓存。
      • accept_clientclear_client:接受新客户端连接并清理客户端信息。
      • dispatch_client:处理客户端的消息接收和业务逻辑分发。
      • handle_websocket:处理WebSocket的整体连接生命周期,包括接收消息、处理负载和管理任务。
      • _process_when_payload:处理接收到的消息负载,并根据业务逻辑发送响应。
      • preper_reuse_connectionpreper_payloadpreper_action:辅助方法,用于准备复用连接、处理负载和准备动作。
      • init_langchain_object_task:初始化LangChain对象的任务,涉及图数据的构建和缓存设置。
      • refresh_graph_data:刷新图数据,处理节点数据的调整。

关键功能点

  • WebSocket管理:通过handle_websocket方法管理WebSocket的连接生命周期,包括接收和发送消息、处理断开连接等。
  • 消息处理:接收到客户端消息后,利用异步任务和线程池处理业务逻辑,确保不会阻塞主事件循环。
  • 缓存管理:使用cache_managerin_memory_cache进行数据缓存,加快数据访问速度,并减少数据库查询。
  • 任务调度:利用ThreadPoolManagerthread_pool管理并发任务,确保高效的任务执行和资源利用。
  • 数据库交互:通过session_getter和相关DAO类(如ChatMessageDaoUserDao)与数据库进行交互,管理用户信息和聊天记录。
  • 日志记录:使用loguru进行详细的日志记录,便于调试和监控系统运行状态。

依赖与模块

  • 外部库asyncio, concurrent.futures, json, time, uuid, collections, queue, typing, fastapi, loguru等。
  • 内部模块:涉及多个自定义模块和服务,如bisheng.api.services, bisheng.cache, bisheng.chat.client, bisheng.database.models, bisheng.processing.process, bisheng.utils, bisheng_langchain等。

总结

总体而言,这段代码实现了一个功能全面的聊天管理系统,具备高并发处理能力、健壮的错误处理机制和高效的资源管理策略。系统通过模块化设计,将不同的功能分离到各个类和方法中,增强了代码的可维护性和扩展性。

请告诉我您的具体需求或问题,我将竭诚为您提供进一步的帮助!

ChatHistory

ChatHistory 的详细解析

1. 类定义与继承

class ChatHistory(Subject):
  • 继承自 Subject:
    • ChatHistory 类继承自 Subject,这表明它采用了观察者模式。在观察者模式中,Subject 充当被观察者(Observable),而其他对象可以作为观察者(Observers)注册到它上面,以便在其状态发生变化时得到通知。
    • 这种设计允许 ChatHistory 在聊天历史发生变化(如添加新消息)时,自动通知所有注册的观察者,从而实现事件驱动的反应。

2. 初始化方法 __init__

def __init__(self):
    super().__init__()
    self.history: Dict[str, List[ChatMessage]] = defaultdict(list)
  • super().__init__():

    • 调用父类 Subject 的初始化方法,确保 Subject 的初始化逻辑得以执行(如初始化观察者列表等)。
  • self.history 属性:

    • 类型注解:

      Dict[str, List[ChatMessage]]
      
      • 键(str):通常是由 client_idchat_id 组合而成的唯一标识符,用于区分不同的聊天会话。
      • 值(List[ChatMessage]):存储对应聊天会话的所有消息列表。
    • defaultdict(list):

      • 使用 defaultdict 可以自动为新键创建一个空列表,避免在添加消息时需要先检查键是否存在。
      • 例如,当一个新的 client_idchat_id 组合首次出现时,不需要手动初始化其对应的消息列表。

3. 方法 add_message

def add_message(
        self,
        client_id: str,
        chat_id: str,
        message: ChatMessage,
):
    """Add a message to the chat history."""
    t1 = time.time()
    from bisheng.database.models.message import ChatMessage
    message.flow_id = client_id
    message.chat_id = chat_id
    if chat_id and (message.message or message.intermediate_steps
                    or message.files) and message.type != 'stream':
        msg = message.copy()
        msg.message = json.dumps(msg.message) if isinstance(msg.message, dict) else msg.message
        files = json.dumps(msg.files) if msg.files else ''
        msg.__dict__.pop('files')
        db_message = ChatMessage(files=files, **msg.__dict__)
        logger.info(f'chat={db_message} time={time.time() - t1}')
        with session_getter() as seesion:
            seesion.add(db_message)
            seesion.commit()
            seesion.refresh(db_message)
            message.message_id = db_message.id

    if not isinstance(message, FileResponse):
        self.notify()
参数说明
  • client_id: str:
    • 表示客户端的唯一标识符。
  • chat_id: str:
    • 表示特定聊天会话的唯一标识符。
  • message: ChatMessage:
    • 表示要添加到聊天历史中的消息对象。
方法逻辑详解
  1. 记录开始时间

    t1 = time.time()
    
    • 用于测量消息添加到数据库所花费的时间,便于性能监控和日志记录。
  2. 动态导入 ChatMessage 模型

    from bisheng.database.models.message import ChatMessage
    
    • 延迟导入(在方法内部导入)可以减少模块间的耦合,避免在类初始化时引发不必要的依赖加载。
  3. 设置消息的 flow_idchat_id

    message.flow_id = client_id
    message.chat_id = chat_id
    
    • client_idchat_id 赋值给消息对象,确保消息与正确的会话关联。
  4. 条件判断:决定是否将消息保存到数据库

    if chat_id and (message.message or message.intermediate_steps
                    or message.files) and message.type != 'stream':
    
    • 条件:
      • chat_id 存在:确保消息属于某个有效的聊天会话。
      • message.messagemessage.intermediate_stepsmessage.files 存在:确保消息中有实际内容需要保存。
      • message.type != 'stream':排除类型为 ‘stream’ 的消息,这些消息可能是流式传输的中间结果,不需要单独保存。
  5. 消息复制与处理

    msg = message.copy()
    msg.message = json.dumps(msg.message) if isinstance(msg.message, dict) else msg.message
    files = json.dumps(msg.files) if msg.files else ''
    msg.__dict__.pop('files')
    db_message = ChatMessage(files=files, **msg.__dict__)
    
    • 复制消息对象:
      • 使用 copy() 方法创建消息的副本,避免直接修改原始消息对象。
    • 序列化 message 字段:
      • 如果 message.message 是字典类型,则将其序列化为 JSON 字符串。
      • 这样做可以确保在数据库中以可存储的格式保存复杂的数据结构。
    • 处理 files 字段:
      • message.files 序列化为 JSON 字符串,如果存在文件信息。
      • 从消息副本中移除 files 字段,因为文件信息已经被单独处理。
    • 创建数据库消息对象:
      • 使用处理后的 msg 数据创建一个新的 ChatMessage 实例(数据库模型)。
      • 这将用于将消息存储到数据库中。
  6. 日志记录

    logger.info(f'chat={db_message} time={time.time() - t1}')
    
    • 记录消息保存到数据库的详细信息和所花费的时间,便于监控和调试。
  7. 数据库会话管理与消息保存

    with session_getter() as seesion:
        seesion.add(db_message)
        seesion.commit()
        seesion.refresh(db_message)
        message.message_id = db_message.id
    
    • session_getter():
      • 这是一个上下文管理器,用于获取数据库会话(通常基于 SQLAlchemy 会话)。
      • 确保数据库操作的原子性和事务管理。
    • seesion.add(db_message):
      • db_message 添加到当前会话中,准备插入数据库。
    • seesion.commit():
      • 提交事务,将所有挂起的更改(如新增消息)持久化到数据库。
    • seesion.refresh(db_message):
      • 刷新 db_message 对象,以确保它包含最新的数据库状态(如自动生成的主键 ID)。
    • message.message_id = db_message.id:
      • 将数据库生成的 id 赋值给原始消息对象,确保消息对象在内存中也包含数据库中的主键。
  8. 通知观察者

    if not isinstance(message, FileResponse):
        self.notify()
    
    • 条件:
      • 如果消息不是 FileResponse 类型,则调用 notify() 方法。
      • FileResponse 可能代表文件传输相关的消息,这些消息不需要触发观察者通知。
    • self.notify():
      • 触发 Subject 类中的 notify 方法,通知所有注册的观察者当前 ChatHistory 状态已发生变化(如添加了新消息)。
      • 这使得系统中的其他组件(如前端、日志系统等)能够对聊天历史的更新做出响应。

4. 方法 empty_history

def empty_history(self, client_id: str, chat_id: str):
    """Empty the chat history for a client."""
    self.history[get_cache_key(client_id, chat_id)] = []
参数说明
  • client_id: str:
    • 客户端的唯一标识符。
  • chat_id: str:
    • 特定聊天会话的唯一标识符。
方法逻辑详解
  1. 获取唯一键

    get_cache_key(client_id, chat_id)
    
    • 使用 get_cache_key 函数将 client_idchat_id 组合成一个唯一的字符串键。
    • 这种方式确保不同的聊天会话在 history 字典中有独立的条目。
  2. 清空聊天历史

    self.history[get_cache_key(client_id, chat_id)] = []
    
    • 将指定 client_idchat_id 的聊天历史列表重置为空列表,意味着清除了所有历史消息。
作用
  • 用途:
    • 当用户选择清除聊天历史记录时,调用此方法即可快速清空相关的消息记录。
  • 影响:
    • 不仅在内存中清除了聊天历史,还需要确保其他相关的数据存储(如数据库中的记录)也得到同步清理(这部分可能在其他地方处理)。

综合分析与系统中的作用

1. 聊天历史管理

  • 存储机制:
    • ChatHistory 类通过 self.history 字典管理不同客户端和聊天会话的消息记录。
    • 使用 defaultdict(list) 确保每个新的聊天会话自动初始化一个空的消息列表,简化了消息添加的逻辑。
  • 消息添加:
    • add_message 方法不仅在内存中添加消息,还将消息持久化到数据库中,确保消息的持久性和可恢复性。
    • 对于需要持久化的消息类型(非 FileResponse),通过 notify() 方法通知系统中的其他组件(如前端)有新的消息可用。
  • 消息清空:
    • empty_history 方法允许系统在需要时(如用户请求清除历史记录)快速清空指定聊天会话的消息历史。

2. 观察者模式的应用

  • Subject 类的角色:
    • 通过继承 SubjectChatHistory 成为了一个被观察者。它可以有多个观察者注册在其上,接收关于聊天历史变化的通知。
  • 通知机制:
    • add_message 方法添加新消息时,调用 self.notify() 触发通知机制,使得所有观察者可以感知到聊天历史的变化,并作出相应的反应(如更新前端界面、日志记录等)。

3. 数据持久化与一致性

  • 数据库交互:
    • 通过 session_getter 管理数据库会话,确保消息的持久化操作安全、原子且高效。
    • 将消息对象转换为数据库模型 ChatMessage,并处理可能的复杂字段(如序列化的 JSON 数据),确保数据在数据库中的一致性和完整性。
  • 消息 ID 的同步:
    • 在数据库保存消息后,将数据库生成的 id 赋值给内存中的消息对象,保持内存数据和数据库数据的一致性。

4. 性能与效率

  • 延迟导入:
    • add_message 方法内部导入数据库模型,减少了模块加载时的依赖和开销,提高了系统启动效率。
  • 条件判断优化:
    • 通过条件判断仅对需要持久化的消息进行数据库操作,避免了不必要的资源消耗。
  • 日志记录:
    • 记录消息保存的时间和内容,有助于性能监控、调试和故障排查。

5. 灵活性与扩展性

  • 支持多种消息类型:
    • 通过检查消息类型(如排除 stream 类型),ChatHistory 可以灵活处理不同类型的消息,适应多样化的业务需求。
  • 观察者的扩展:
    • 由于采用了观察者模式,未来可以方便地添加新的观察者(如新的日志记录器、实时监控系统等),无需修改 ChatHistory 类的核心逻辑。

潜在改进与优化建议

虽然 ChatHistory 类功能完备,但在实际应用中可能还存在一些优化空间:

  1. 错误处理与异常管理:

    • 当前的 add_message 方法假设所有操作都能成功完成。然而,实际中数据库操作可能会失败(如连接中断、约束冲突等)。
    • 建议在数据库操作部分添加异常捕获,确保在失败时能够适当处理(如重试机制、错误日志记录、通知观察者等)。
    try:
        with session_getter() as seesion:
            seesion.add(db_message)
            seesion.commit()
            seesion.refresh(db_message)
            message.message_id = db_message.id
    except Exception as e:
        logger.error(f'Failed to add message to database: {e}')
        # 可能的处理措施
    
  2. 异步支持:

    • 当前的 add_message 方法是同步的,可能会阻塞主线程,尤其是在高并发情况下。
    • 如果系统使用异步框架(如 asyncio),可以考虑将数据库操作改为异步执行,以提高性能。
    async def add_message(...):
        ...
        async with session_getter() as session:
            session.add(db_message)
            await session.commit()
            await session.refresh(db_message)
            message.message_id = db_message.id
        ...
    
  3. 批量操作优化:

    • 在高频消息场景下,频繁的数据库提交可能会成为瓶颈。
    • 可以考虑批量保存消息,减少数据库事务的频率,提升整体性能。
  4. 消息过期与清理策略:

    • 长时间运行的系统中,聊天历史可能会变得非常庞大,占用大量内存和数据库空间。
    • 需要设计适当的消息过期与清理策略,如基于时间或数量的历史记录限制。
  5. 数据索引与查询优化:

    • 确保数据库中的 ChatMessage 表对 flow_idchat_id 等常用查询字段建立索引,以加快查询速度。
  6. 安全性考虑:

    • 确保消息内容在存储和传输过程中得到适当的加密和保护,防止敏感信息泄露。

总结

ChatHistory 类在整个聊天系统中扮演着关键角色,负责管理和持久化聊天消息记录。通过继承 Subject 并采用观察者模式,它不仅能够有效地管理聊天历史,还能灵活地通知其他系统组件进行相应的更新和反应。类的设计充分考虑了数据的一致性、系统的性能以及未来的扩展性。然而,实际应用中还需根据具体需求和场景,进一步优化和增强其功能。

如果您有更多关于 ChatHistory 类的问题,或需要了解如何在具体场景中应用和扩展该类,请随时告知!

ChatManager

1. 类概述

ChatManager 类主要负责以下任务:

  • 管理多个客户端和聊天会话的 WebSocket 连接。
  • 维护聊天历史记录,并将其持久化到数据库中。
  • 处理缓存管理,包括与 cache_manager 的交互。
  • 管理并调度后台任务,确保系统的高效运行。
  • 处理和分发接收到的消息,确保消息能够正确地传递到目标客户端。
  • 实现观察者模式,以响应缓存管理器的更新。

2. 属性解析

让我们逐一解析 ChatManager 类中的各个属性及其用途:

class ChatManager:
    def __init__(self):
        self.active_connections: Dict[str, WebSocket] = {}
        self.chat_history = ChatHistory()
        self.cache_manager = cache_manager
        self.cache_manager.attach(self.update)
        self.in_memory_cache = InMemoryCache()
        self.task_manager: List[asyncio.Task] = []
        self.active_clients: Dict[str, ChatClient] = {}
        self.stream_queue: Dict[str, Queue] = {}

2.1. active_connections: Dict[str, WebSocket]

  • 用途
    • 存储当前所有活跃的 WebSocket 连接。
    • 键(str)通常是通过 client_idchat_id 组合生成的唯一标识符。
    • 值(WebSocket)是与客户端建立的 WebSocket 连接对象。
  • 作用
    • 允许 ChatManager 通过唯一键快速查找和管理特定客户端的 WebSocket 连接。
    • 用于发送消息、关闭连接等操作。

2.2. chat_history = ChatHistory()

  • 用途
    • 实例化 ChatHistory 类,用于管理和存储聊天记录。
    • ChatHistory 可能实现了观察者模式,能够记录每次消息的添加,并通知相关观察者(如前端界面或日志系统)。
  • 作用
    • 保持所有聊天会话的历史记录,支持查询、回溯等功能。
    • 确保聊天记录的持久化和可恢复性。

2.3. cache_manager = cache_manager

  • 用途
    • 引用外部的 cache_manager 对象,负责管理缓存。
    • 通过观察者模式,ChatManager 可以响应 cache_manager 的更新。
  • 作用
    • 缓存常用数据,提升系统性能。
    • ChatManager 通过附加自身的 update 方法,能够在缓存发生变化时执行相应的操作。

2.4. self.cache_manager.attach(self.update)

  • 用途
    • ChatManagerupdate 方法注册为 cache_manager 的观察者。
  • 作用
    • 确保当 cache_manager 更新时,ChatManager 能够接收到通知,并执行相应的更新逻辑。

2.5. in_memory_cache = InMemoryCache()

  • 用途
    • 实例化 InMemoryCache 类,用于在内存中存储临时数据。
  • 作用
    • 提供快速的数据访问,减少对持久化存储的依赖。
    • 可能用于存储当前会话的状态、临时计算结果等。

2.6. task_manager: List[asyncio.Task] = []

  • 用途
    • 存储正在运行的 asyncio 任务列表。
  • 作用
    • 允许 ChatManager 跟踪和管理后台异步任务,如处理消息、执行长时间运行的操作等。
    • 有助于在需要时取消或监控任务的状态。

2.7. active_clients: Dict[str, ChatClient] = {}

  • 用途
    • 存储当前活跃的客户端对象。
  • 作用
    • ChatClient 可能是一个封装了客户端状态、权限、会话信息等的类实例。
    • 允许 ChatManager 通过唯一键(client_key)快速访问和管理特定客户端的信息。

2.8. stream_queue: Dict[str, Queue] = {}

  • 用途
    • 为每个聊天会话维护一个消息队列,用于记录和处理流式输出结果。
  • 作用
    • 确保消息的有序处理和传递,支持实时数据流的管理。
    • 可能用于处理需要逐步响应的长时间运行的操作,如生成大型报告或处理复杂的请求。

3. 方法解析

ChatManager 类中包含多个方法,下面我们将逐一解析每个方法的功能和实现细节。

3.1. update 方法

def update(self):
    if self.cache_manager.current_client_id in self.active_connections:
        self.last_cached_object_dict = self.cache_manager.get_last()
        chat_response = FileResponse(
            message=None,
            type='file',
            data=self.last_cached_object_dict['obj'],
            data_type=self.last_cached_object_dict['type'],
        )
        self.chat_history.add_message(
            self.cache_manager.current_client_id,
            self.cache_manager.current_chat_id,
            chat_response
        )
  • 用途
    • 作为观察者模式中的回调方法,当 cache_manager 更新时被调用。
  • 功能
    1. 检查当前的 client_id 是否在活跃连接中。
    2. 获取 cache_manager 的最新缓存对象。
    3. 创建一个 FileResponse 对象,包含从缓存中获取的数据。
    4. FileResponse 消息添加到聊天历史中。
  • 作用
    • 当缓存发生变化(如接收到新的文件数据)时,自动将相关信息记录到聊天历史中,并可能通知前端界面或其他观察者。

3.2. connect 方法

async def connect(self, client_id: str, chat_id: str, websocket: WebSocket):
    await websocket.accept()
    self.active_connections[get_cache_key(client_id, chat_id)] = websocket
    self.stream_queue[get_cache_key(client_id, chat_id)] = Queue()
  • 用途
    • 处理新的 WebSocket 连接请求。
  • 功能
    1. 接受 WebSocket 连接。
    2. 将连接对象存储到 active_connections 字典中,键为 client_idchat_id 组合的唯一键。
    3. 为该会话创建一个新的消息队列,并存储到 stream_queue 字典中。
  • 作用
    • 确保每个客户端的连接和消息队列得到正确的初始化和管理。

3.3. reuse_connect 方法

def reuse_connect(self, client_id: str, chat_id: str, websocket: WebSocket):
    self.active_connections[get_cache_key(client_id, chat_id)] = websocket
    self.stream_queue[get_cache_key(client_id, chat_id)] = Queue()
  • 用途
    • 重新使用现有的 WebSocket 连接,通常在某些特定情况下需要复用连接时调用。
  • 功能
    1. 更新 active_connectionsstream_queue 中对应的连接和队列。
  • 作用
    • 允许系统在需要时复用现有的连接,避免重复创建连接对象。

3.4. disconnect 方法

def disconnect(self, client_id: str, chat_id: str, key: str = None):
    if key:
        logger.debug('disconnect_ws key={}', key)
        self.active_connections.pop(key, None)
    else:
        logger.info('disconnect_ws key={}', get_cache_key(client_id, chat_id))
        self.active_connections.pop(get_cache_key(client_id, chat_id), None)
  • 用途
    • 断开与客户端的 WebSocket 连接。
  • 功能
    1. 根据传入的 key(如果有)或通过 client_idchat_id 生成的唯一键,移除对应的 WebSocket 连接对象。
  • 作用
    • 确保在客户端断开连接时,相关的 WebSocket 连接和消息队列得到正确清理,释放资源。

3.5. send_message 方法

async def send_message(self, client_id: str, chat_id: str, message: str):
    websocket = self.active_connections[get_cache_key(client_id, chat_id)]
    await websocket.send_text(message)
  • 用途
    • 向特定客户端发送文本消息。
  • 功能
    1. 根据 client_idchat_id 获取对应的 WebSocket 连接。
    2. 通过 WebSocket 发送文本消息。
  • 作用
    • 实现消息的实时传输,确保客户端能够及时收到来自服务器的文本信息。

3.6. send_json 方法

async def send_json(self, client_id: str, chat_id: str, message: ChatMessage, add=True):
    message.flow_id = client_id
    message.chat_id = chat_id
    websocket = self.active_connections[get_cache_key(client_id, chat_id)]
    if add:
        self.chat_history.add_message(client_id, chat_id, message)
    await websocket.send_json(message.dict())
  • 用途
    • 向特定客户端发送 JSON 格式的消息。
  • 功能
    1. 设置消息的 flow_idchat_id 属性。
    2. 根据唯一键获取对应的 WebSocket 连接。
    3. 如果 add 参数为 True,将消息添加到聊天历史中。
    4. 通过 WebSocket 发送 JSON 格式的消息。
  • 作用
    • 提供了一种更结构化的消息传递方式,支持更复杂的数据结构和协议。

3.7. close_connection 方法

async def close_connection(
    self,
    flow_id: str,
    chat_id: str,
    code: int,
    reason: str,
    key_list: List[str] = None
):
    """close and clean ws"""
    if websocket := self.active_connections[get_cache_key(flow_id, chat_id)]:
        try:
            await websocket.close(code=code, reason=reason)
            self.disconnect(flow_id, chat_id)
            if key_list:
                for key in key_list:
                    self.disconnect(flow_id, chat_id, key)
        except RuntimeError as exc:
            if 'after sending' in str(exc):
                logger.error(exc)
  • 用途
    • 关闭特定客户端的 WebSocket 连接,并进行必要的清理工作。
  • 功能
    1. 获取对应的 WebSocket 连接。
    2. 尝试关闭 WebSocket 连接,传递关闭码和原因。
    3. 调用 disconnect 方法移除连接对象。
    4. 如果提供了 key_list,则遍历并移除这些额外的连接。
    5. 捕获并记录可能的运行时错误,避免程序崩溃。
  • 作用
    • 确保在连接关闭时,资源得到正确释放,系统保持稳定。

3.8. ping 方法

async def ping(self, client_id: str, chat_id: str):
    ping_pong = ChatMessage(
        is_bot=True,
        message='pong',
        intermediate_steps='',
    )
    await self.send_json(client_id, chat_id, ping_pong, False)
  • 用途
    • 向客户端发送一个 pong 消息,通常用于保持连接活跃或响应客户端的 ping 请求。
  • 功能
    1. 创建一个 ChatMessage 对象,内容为 'pong'
    2. 使用 send_json 方法发送该消息,但不将其添加到聊天历史中(add=False)。
  • 作用
    • 保持 WebSocket 连接的活跃状态,防止连接因长时间无数据传输而被关闭。
    • 响应客户端的健康检查或心跳机制。

3.9. set_cache 方法

def set_cache(self, client_id: str, langchain_object: Any) -> bool:
    """
    Set the cache for a client.
    """
    self.in_memory_cache.set(client_id, langchain_object)
    return client_id in self.in_memory_cache
  • 用途
    • 设置特定客户端的缓存数据。
  • 功能
    1. 使用 in_memory_cacheset 方法,将 langchain_object 存储在客户端的缓存中。
    2. 返回该 client_id 是否成功存储在缓存中。
  • 作用
    • 管理客户端特定的缓存数据,提升数据访问速度,减少对持久化存储的依赖。

3.10. accept_client 方法

async def accept_client(self, client_key: str, chat_client: ChatClient, websocket: WebSocket):
    await websocket.accept()
    self.active_clients[client_key] = chat_client
  • 用途
    • 接受并注册一个新的客户端连接。
  • 功能
    1. 接受 WebSocket 连接。
    2. chat_client 对象存储在 active_clients 字典中,键为 client_key
  • 作用
    • 确保新连接的客户端得到正确注册和管理,便于后续的消息处理和连接管理。

3.11. clear_client 方法

def clear_client(self, client_key: str):
    if client_key not in self.active_clients:
        logger.warning('close_client client_key={} not in active_clients', client_key)
        return
    logger.info('close_client client_key={}', client_key)
    self.active_clients.pop(client_key, None)
  • 用途
    • 清除特定客户端的注册信息。
  • 功能
    1. 检查 client_key 是否存在于 active_clients 字典中。
    2. 如果存在,移除该客户端的记录。
    3. 如果不存在,记录警告信息。
  • 作用
    • 确保在客户端断开连接或需要移除时,相关的客户端信息得到正确清理,防止内存泄漏或资源浪费。

3.12. close_client 方法

async def close_client(self, client_key: str, code: int, reason: str):
    if chat_client := self.active_clients.get(client_key):
        try:
            await chat_client.websocket.close(code=code, reason=reason)
            self.clear_client(client_key)
        except RuntimeError as exc:
            if isinstance(exc, concurrent.futures.CancelledError):
                continue
            logger.exception('feature_key={} {}', client_key, e)
            erro_resp = ChatResponse(**base_param)
            context = context_dict.get(future_key)
            if context.get('status') == 'init':
                erro_resp.intermediate_steps = f'LLM 技能执行错误. error={str(e)}'
            elif context.get('has_file'):
                erro_resp.intermediate_steps = f'文档解析失败,点击输入框上传按钮重新上传\n\n{str(e)}'
            else:
                erro_resp.intermediate_steps = f'Input data is parsed fail. error={str(e)}'
            context['status'] = 'init'
            await self.send_json(context.get('flow_id'), context.get('chat_id'), erro_resp)
            erro_resp.type = 'close'
            await self.send_json(context.get('flow_id'), context.get('chat_id'), erro_resp)
  • 用途
    • 关闭特定客户端的 WebSocket 连接,并处理可能出现的异常。
  • 功能
    1. 获取对应的 ChatClient 对象。
    2. 尝试关闭其 WebSocket 连接,传递关闭码和原因。
    3. 调用 clear_client 方法移除客户端记录。
    4. 捕获并处理可能的运行时错误,记录异常并发送错误响应给客户端。
  • 作用
    • 确保在关闭客户端连接时,能够正确处理异常情况,保持系统的稳定性和一致性。

3.13. dispatch_client 方法

async def dispatch_client(
    self,
    request: Request,
    client_id: str,
    chat_id: str,
    login_user: UserPayload,
    work_type: WorkType,
    websocket: WebSocket,
    graph_data: dict = None
):
    client_key = uuid.uuid4().hex
    chat_client = ChatClient(
        request,
        client_key,
        client_id,
        chat_id,
        login_user.user_id,
        login_user,
        work_type,
        websocket,
        graph_data=graph_data
    )
    await self.accept_client(client_key, chat_client, websocket)
    logger.debug(
        f'act=accept_client client_key={client_key} client_id={client_id} chat_id={chat_id}'
    )
    try:
        while True:
            try:
                json_payload_receive = await asyncio.wait_for(websocket.receive_json(), timeout=2.0)
            except asyncio.TimeoutError:
                continue
            try:
                payload = json.loads(json_payload_receive) if json_payload_receive else {}
            except TypeError:
                payload = json_payload_receive
            await chat_client.handle_message(payload)
    except WebSocketDisconnect as e:
        logger.info('act=rcv_client_disconnect {}', str(e))
    except IgnoreException:
        pass
    except Exception as e:
        logger.exception(str(e))
        await self.close_client(
            client_key,
            code=status.WS_1011_INTERNAL_ERROR,
            reason='后端未知错误类型'
        )
    finally:
        try:
            await self.close_client(
                client_key,
                code=status.WS_1000_NORMAL_CLOSURE,
                reason='Client disconnected'
            )
        except Exception as e:
            logger.exception(e)
        self.clear_client(client_key)
  • 用途
    • 分发和处理特定客户端的消息,管理整个消息接收和处理的生命周期。
  • 功能
    1. 生成一个唯一的 client_key
    2. 创建一个新的 ChatClient 对象,封装客户端的各种信息和状态。
    3. 调用 accept_client 方法注册新的客户端。
    4. 进入一个无限循环,持续接收和处理来自客户端的消息。
    5. 处理不同类型的异常,如 WebSocket 断开、忽略异常、以及其他未知异常。
    6. 在最终块中,确保客户端连接得到正确关闭和清理。
  • 作用
    • 确保每个客户端的消息能够被持续接收和正确处理,同时在断开连接或出现异常时,能够优雅地关闭连接并清理资源。

3.14. handle_websocket 方法

async def handle_websocket(
    self,
    flow_id: str,
    chat_id: str,
    websocket: WebSocket,
    user_id: int,
    gragh_data: dict = None,
):
    # 建立连接,并存储映射,兼容不复用ws 场景
    key_list = set([get_cache_key(flow_id, chat_id)])
    await self.connect(flow_id, chat_id, websocket)
    context_dict = {
        get_cache_key(flow_id, chat_id): {
            'status': 'init',
            'has_file': False,
            'flow_id': flow_id,
            'chat_id': chat_id
        }
    }
    payload = {}
    base_param = {
        'user_id': user_id,
        'flow_id': flow_id,
        'chat_id': chat_id,
        'type': 'end',
        'category': 'system'
    }
    try:
        while True:
            try:
                json_payload_receive = await asyncio.wait_for(websocket.receive_json(), timeout=2.0)
            except asyncio.TimeoutError:
                json_payload_receive = ''
            try:
                payload = json.loads(json_payload_receive) if json_payload_receive else {}
            except TypeError:
                payload = json_payload_receive

            # websocket multi use
            if payload and 'flow_id' in payload:
                chat_id = payload.get('chat_id')
                flow_id = payload.get('flow_id')
                key = get_cache_key(flow_id, chat_id)
                if key not in key_list:
                    gragh_data, message = self.preper_reuse_connection(flow_id, chat_id, websocket)
                    context_dict.update({
                        key: {
                            'status': 'init',
                            'has_file': False,
                            'flow_id': flow_id,
                            'chat_id': chat_id
                        }
                    })
                    if message:
                        logger.info('act=new_chat message={}', message)
                        erro_resp = ChatResponse(intermediate_steps=message, **base_param)
                        erro_resp.category = 'error'
                        await self.send_json(flow_id, chat_id, erro_resp, add=False)
                        continue
                    logger.info('act=new_chat_init_success key={}', key)
                    key_list.add(key)
                if not payload.get('inputs'):
                    continue

            # 判断当前是否是空循环
            process_param = {
                'autogen_pool': thread_pool,
                'user_id': user_id,
                'payload': payload,
                'graph_data': gragh_data,
                'context_dict': context_dict
            }
            if payload:
                await self._process_when_payload(flow_id, chat_id, **process_param)
            else:
                for v in context_dict.values():
                    if v['status'] != 'init':
                        await self._process_when_payload(v['flow_id'], v['chat_id'], **process_param)

            # 处理任务状态
            complete_normal = await thread_pool.as_completed(key_list)
            complete = complete_normal
            if complete:
                for future_key, future in complete:
                    try:
                        future.result()
                        logger.debug('task_complete key={}', future_key)
                    except Exception as e:
                        if isinstance(e, concurrent.futures.CancelledError):
                            continue
                        logger.exception('feature_key={} {}', future_key, e)
                        erro_resp = ChatResponse(**base_param)
                        context = context_dict.get(future_key)
                        if context.get('status') == 'init':
                            erro_resp.intermediate_steps = f'LLM 技能执行错误. error={str(e)}'
                        elif context.get('has_file'):
                            erro_resp.intermediate_steps = f'文档解析失败,点击输入框上传按钮重新上传\n\n{str(e)}'
                        else:
                            erro_resp.intermediate_steps = f'Input data is parsed fail. error={str(e)}'
                        context['status'] = 'init'
                        await self.send_json(context.get('flow_id'), context.get('chat_id'), erro_resp)
                        erro_resp.type = 'close'
                        await self.send_json(context.get('flow_id'), context.get('chat_id'), erro_resp)
    except WebSocketDisconnect as e:
        logger.info('act=rcv_client_disconnect {}', str(e))
    except Exception as e:
        logger.exception(str(e))
        await self.close_connection(
            flow_id=flow_id,
            chat_id=chat_id,
            code=status.WS_1011_INTERNAL_ERROR,
            reason='后端未知错误类型',
            key_list=key_list
        )
    finally:
        thread_pool.cancel_task(key_list)  # 将进行中的任务进行cancel
        try:
            await self.close_connection(
                flow_id=flow_id,
                chat_id=chat_id,
                code=status.WS_1000_NORMAL_CLOSURE,
                reason='Client disconnected',
                key_list=key_list
            )
        except Exception as e:
            logger.exception(e)
        self.disconnect(flow_id, chat_id)
  • 用途
    • 处理与客户端的 WebSocket 连接,负责接收和处理来自客户端的消息。
  • 功能
    1. 通过 flow_idchat_id 生成唯一键,并建立连接。
    2. 初始化 context_dict,用于跟踪每个会话的状态。
    3. 进入消息接收循环,持续接收来自客户端的 JSON 消息。
    4. 根据消息内容决定是否复用连接或创建新的会话。
    5. 调用 _process_when_payload 方法处理具体的消息负载。
    6. 监控后台任务的完成状态,并处理可能的异常。
    7. 捕获并处理各种异常,确保连接的稳定性和资源的正确释放。
    8. 在最终块中,取消所有相关任务并关闭连接。
  • 作用
    • 负责整个 WebSocket 会话的生命周期管理,包括消息接收、处理、任务调度和连接关闭。
    • 确保系统能够同时处理多个客户端的请求,并保持高效的并发性能。

3.15. _process_when_payload 方法

async def _process_when_payload(self, flow_id: str, chat_id: str, autogen_pool: ThreadPoolManager, **kwargs):
    user_id = kwargs.get('user_id')
    graph_data = kwargs.get('graph_data')
    payload = kwargs.get('payload')
    key = get_cache_key(flow_id, chat_id)
    context = kwargs.get('context_dict').get(key)

    status_ = context.get('status')

    if payload and status_ != 'init':
        logger.error('act=input_before_complete payload={} status={}', payload, status_)

    if not payload:
        payload = context.get('payload')
    context['payload'] = payload
    is_begin = bool(status_ == 'init' and 'action' not in payload)
    base_param = {'user_id': user_id, 'flow_id': flow_id, 'chat_id': chat_id}
    start_resp = ChatResponse(type='begin', category='system', **base_param)
    if is_begin:
        await self.send_json(flow_id, chat_id, start_resp)
        if chat_id:
            res = ChatMessageDao.get_messages_by_chat_id(chat_id=chat_id)
            if len(res) <= 1:  # 说明是新建会话
                websocket = self.active_connections[key]
                login_user = UserPayload(**{
                    'user_id': user_id,
                    'user_name': UserDao.get_user(user_id).user_name,
                })
                AuditLogService.create_chat_flow(login_user, get_request_ip(websocket), flow_id)
    start_resp.type = 'start'

    step_resp = ChatResponse(type='end', category='system', **base_param)
    langchain_obj_key = get_cache_key(flow_id, chat_id)
    if status_ == 'init':
        has_file, graph_data = await self.preper_payload(payload, graph_data, langchain_obj_key, flow_id, chat_id, start_resp, step_resp)
        status_ = 'init_object'
        context.update({'status': status_})
        context.update({'has_file': has_file})

    if not self.in_memory_cache.get(langchain_obj_key) and status_ == 'init_object':
        thread_pool.submit(
            key,
            self.init_langchain_object_task,
            flow_id,
            chat_id,
            user_id,
            graph_data,
            trace_id=chat_id
        )
        status_ = 'waiting_object'
        context.update({'status': status_})

    if payload and self.in_memory_cache.get(langchain_obj_key):
        action, over = await self.preper_action(flow_id, chat_id, langchain_obj_key, payload, start_resp, step_resp)
        logger.debug(f"processing_message message={payload.get('inputs')} action={action} over={over}")
        if not over:
            from bisheng_langchain.chains.autogen.auto_gen import AutoGenChain
            from bisheng.chat.handlers import Handler
            params = {
                'session': self,
                'client_id': flow_id,
                'chat_id': chat_id,
                'action': action,
                'payload': payload,
                'user_id': user_id,
                'trace_id': chat_id
            }
            if isinstance(self.in_memory_cache.get(langchain_obj_key), AutoGenChain):
                logger.info(f'autogen_submit {langchain_obj_key}')
                autogen_pool.submit(
                    key,
                    Handler(stream_queue=self.stream_queue[key]).dispatch_task,
                    **params
                )
            else:
                thread_pool.submit(
                    key,
                    Handler(stream_queue=self.stream_queue[key]).dispatch_task,
                    **params
                )
        status_ = 'init'
        context.update({'status': status_})
        context.update({'payload': {}})  # clean message
  • 用途
    • 处理接收到的消息负载,根据当前的会话状态和消息内容执行相应的操作。
  • 功能
    1. 提取并初始化相关参数,如 user_idgraph_datapayloadcontext
    2. 判断是否是会话的开始,并发送 start_resp 消息。
    3. 检查是否存在文件上传或变量输入,调用 preper_payload 方法进行预处理。
    4. 根据缓存中的 langchain_obj_key 判断是否需要初始化 LangChain 对象,提交相应的任务到线程池。
    5. 处理具体的操作动作,如 autogenstopclear_history 等,调用 preper_action 方法进行准备。
    6. 根据 langchain_obj_key 的类型,提交不同的任务处理器(如 AutoGenChain)到线程池。
    7. 更新会话状态和上下文信息,确保下次处理能够正确执行。
  • 作用
    • 负责解析和处理来自客户端的消息,根据会话状态和内容执行相应的业务逻辑,如生成报告、处理文件上传等。

3.16. preper_reuse_connection 方法

def preper_reuse_connection(self, flow_id: str, chat_id: str, websocket: WebSocket):
    message = ''
    with session_getter() as session:
        gragh_data = session.get(Flow, flow_id)
        if not gragh_data:
            message = '该技能已被删除'
        if gragh_data.status != 2:
            message = '当前技能未上线,无法直接对话'
    gragh_data = gragh_data.data
    self.reuse_connect(flow_id, chat_id, websocket)
    return gragh_data, message
  • 用途
    • 准备复用现有的 WebSocket 连接,检查技能状态并更新连接信息。
  • 功能
    1. 使用 session_getter 获取数据库会话,查询特定 flow_idFlow 对象。
    2. 根据 Flow 对象的存在与状态,设置相应的错误消息。
    3. 调用 reuse_connect 方法更新连接和消息队列。
    4. 返回 gragh_data 和可能的错误消息。
  • 作用
    • 确保在复用连接时,技能状态是有效的,避免不必要的错误或资源浪费。

3.17. preper_payload 方法

async def preper_payload(
    self,
    payload,
    graph_data,
    langchain_obj_key,
    client_id,
    chat_id,
    start_resp: ChatResponse,
    step_resp: ChatResponse
):
    has_file = False
    has_variable = False
    if 'inputs' in payload and ('data' in payload['inputs'] or 'file_path' in payload['inputs']):
        node_data = payload['inputs'].get('data', '') or [payload['inputs']]
        graph_data = self.refresh_graph_data(graph_data, node_data)
        node_loader = False

        for nod in node_data:
            if any('Loader' in x['id'] for x in find_next_node(graph_data, nod['id'])):
                node_loader = True
                break
        if node_loader:
            self.set_cache(langchain_obj_key, None)  # rebuild object
        has_file = any(['InputFile' in nd.get('id', '') for nd in node_data])
        has_variable = any(['VariableNode' in nd.get('id', '') for nd in node_data])
    if has_file:
        step_resp.intermediate_steps = '文件上传完成,开始解析'
        await self.send_json(client_id, chat_id, start_resp)
        await self.send_json(client_id, chat_id, step_resp, add=False)
        await self.send_json(client_id, chat_id, start_resp)
        logger.info('input_file start_log')
        await asyncio.sleep(-1)  # 快速的跳过
    elif has_variable:
        await self.send_json(client_id, chat_id, start_resp)
        logger.info('input_variable start_log')
        await asyncio.sleep(-1)  # 快速的跳过
    return has_file, graph_data
  • 用途
    • 预处理收到的消息负载,识别是否包含文件上传或变量输入,并相应地更新会话状态和缓存。
  • 功能
    1. 检查 payload 是否包含文件数据或变量数据。
    2. 根据 node_data 更新 graph_data
    3. 判断是否需要加载节点(node_loader),并根据需要重建 LangChain 对象。
    4. 设置 has_filehas_variable 标志。
    5. 如果包含文件,发送相应的响应消息,并跳过进一步处理。
    6. 如果包含变量,同样发送响应消息并跳过进一步处理。
    7. 返回 has_file 标志和更新后的 graph_data
  • 作用
    • 识别并处理特定类型的消息负载,确保系统能够正确响应文件上传或变量输入等特定操作。

3.18. preper_action 方法

async def preper_action(
    self,
    client_id,
    chat_id,
    langchain_obj_key,
    payload,
    start_resp: ChatResponse,
    step_resp: ChatResponse
):
    langchain_obj = self.in_memory_cache.get(langchain_obj_key)
    batch_question = []
    action = ''
    over = False
    if isinstance(langchain_obj, Report):
        action = 'report'
        step_resp.intermediate_steps = '文件解析完成,开始生成报告'
        await self.send_json(client_id, chat_id, step_resp)
    elif payload.get('action') == 'stop':
        action = 'stop'
    elif 'action' in payload:
        action = 'autogen'
    elif 'clear_history' in payload and payload['clear_history']:
        self.chat_history.empty_history(client_id, chat_id)
        action = 'clear_history'
        over = True
    elif 'data' in payload['inputs'] or 'file_path' in payload['inputs']:
        action = 'auto_file'
        batch_question = self.in_memory_cache.get(langchain_obj_key + '_question')
        payload['inputs']['questions'] = batch_question
        if not batch_question:
            file_msg = payload['inputs']
            file_msg.pop('id', '')
            file_msg.pop('data', '')
            file = ChatMessage(
                flow_id=client_id,
                chat_id=chat_id,
                is_bot=False,
                message=file_msg,
                type='end',
                user_id=step_resp.user_id
            )
            self.chat_history.add_message(client_id, chat_id, file)
            step_resp.message = ''
            step_resp.intermediate_steps = '文件解析完成'
            await self.send_json(client_id, chat_id, step_resp)
            start_resp.type = 'close'
            await self.send_json(client_id, chat_id, start_resp)
            over = True
        else:
            step_resp.intermediate_steps = '文件解析完成,开始执行'
            await self.send_json(client_id, chat_id, step_resp, add=False)
    await asyncio.sleep(-1)  # 快速的跳过
    return action, over
  • 用途
    • 准备和执行具体的操作动作,根据消息内容和会话状态决定下一步行动。
  • 功能
    1. 获取缓存中的 LangChain 对象。
    2. 根据 LangChain 对象的类型和 payload 中的内容,确定执行的动作类型(如生成报告、停止操作、自动生成等)。
    3. 根据动作类型,更新响应消息并发送给客户端。
    4. 对特定动作(如清除历史记录),执行相应的操作并设置标志。
  • 作用
    • 确保系统能够根据不同的请求和状态,执行适当的业务逻辑,如生成报告、处理文件上传等。

3.19. init_langchain_object_task 方法

async def init_langchain_object_task(
    self, flow_id, chat_id, user_id, graph_data
):
    key_node = get_cache_key(flow_id, chat_id)
    logger.info(f'init_langchain build_begin key={key_node}')
    with session_getter() as session:
        db_user = session.get(User, user_id)  # 用来支持节点判断用户权限
    artifacts = {}
    start_time = time.time()
    graph = await build_flow_no_yield(
        graph_data=graph_data,
        artifacts=artifacts,
        process_file=True,
        flow_id=UUID(flow_id).hex,
        chat_id=chat_id,
        user_name=db_user.user_name
    )
    await graph.abuild()
    logger.info(f'init_langchain build_end timecost={time.time() - start_time}')
    question = []
    for node in graph.vertices:
        if node.vertex_type in {'InputNode', 'AudioInputNode', 'FileInputNode'}:
            question_parse = await node.get_result()
            if isinstance(question_parse, list):
                question.extend(question_parse)
            else:
                question.append(question_parse)

    self.set_cache(key_node + '_question', question)
    input_nodes = graph.get_input_nodes()
    for node in input_nodes:
        # 只存储chain
        if node.base_type == 'inputOutput' and node.vertex_type != 'Report':
            continue
        self.set_cache(key_node, await node.get_result())
        self.set_cache(key_node + '_artifacts', artifacts)
    return flow_id, chat_id
  • 用途
    • 初始化 LangChain 对象,准备处理复杂的任务如生成报告、处理文件等。
  • 功能
    1. 根据 flow_idchat_id 生成唯一键。
    2. 获取用户信息,以支持权限判断。
    3. 使用 build_flow_no_yield 函数构建任务流程图,并进行必要的文件处理。
    4. 遍历流程图中的节点,收集输入问题并存储到缓存中。
    5. 将构建完成的对象和相关工件(artifacts)存储到缓存中。
  • 作用
    • 准备和初始化复杂的任务对象,确保后续的任务处理能够顺利进行。

3.20. refresh_graph_data 方法

def refresh_graph_data(self, graph_data: dict, node_data: List[dict]):
    tweak = process_node_data(node_data)
    """upload file to make flow work"""
    return process_tweaks(graph_data, tweaks=tweak)
  • 用途
    • 更新和调整流程图数据,根据上传的节点数据进行必要的处理。
  • 功能
    1. 处理传入的 node_data,生成调整参数(tweak)。
    2. 调用 process_tweaks 函数,根据调整参数更新 graph_data
    3. 返回更新后的 graph_data
  • 作用
    • 确保流程图数据能够根据最新的节点输入进行动态调整,支持系统的灵活性和扩展性。

4. 设计模式与架构

4.1. 观察者模式

ChatManager 使用观察者模式来响应 cache_manager 的更新。这种模式允许 ChatManagercache_manager 发生变化时,自动执行相应的逻辑(如更新聊天历史)。

  • 实现细节:
    • ChatManager 在初始化时调用 self.cache_manager.attach(self.update),将自身的 update 方法注册为 cache_manager 的观察者。
    • cache_manager 更新时,会调用 ChatManagerupdate 方法,执行相关操作。

4.2. 异步编程

ChatManager 广泛使用 asyncio 进行异步编程,以支持高并发的 WebSocket 连接和消息处理。

  • 实现细节:
    • 使用 async def 定义异步方法,如 connectsend_messagehandle_websocket 等。
    • 使用 asyncio.Task 来管理后台任务,确保系统能够同时处理多个任务而不阻塞主线程。

4.3. 线程池与任务管理

ChatManager 使用线程池(如 thread_poolautogen_pool)来处理需要长时间运行的任务,如生成报告或处理复杂的消息负载。

  • 实现细节:
    • 通过 thread_pool.submitautogen_pool.submit 提交任务,确保这些任务在后台线程中运行,不会阻塞主线程。
    • 使用 asyncio.Queue(如 stream_queue)来管理和协调流式输出结果。

4.4. 缓存管理

ChatManager 使用 InMemoryCachecache_manager 来管理缓存数据,提升系统性能和响应速度。

  • 实现细节:
    • InMemoryCache 用于存储客户端特定的临时数据,支持快速访问。
    • cache_manager 负责管理更广泛的缓存数据,并通过观察者模式通知 ChatManager 进行更新。

5. 实际应用场景

ChatManager 类适用于需要高并发、实时通信和复杂任务处理的聊天系统。以下是一些具体的应用场景:

5.1. 实时聊天应用

  • 功能:
    • 支持多个客户端同时连接并进行实时消息交换。
    • 维护每个聊天会话的历史记录,支持查询和回溯。
    • 处理文件上传和下载,支持文件解析和处理。

5.2. AI 助手或聊天机器人

  • 功能:
    • 集成 LangChain 等 AI 处理库,支持智能响应和任务执行。
    • 管理复杂的任务流程,如生成报告、处理自然语言输入等。
    • 通过缓存和历史记录优化响应速度和准确性。

5.3. 客户支持系统

  • 功能:
    • 支持客户与客服代表之间的实时通信。
    • 记录和分析聊天历史,提供数据支持和业务分析。
    • 管理后台任务,如自动回复、客户分类等。

6. 潜在优化与建议

虽然 ChatManager 类功能全面,但在实际应用中可能还存在一些优化空间:

6.1. 错误处理与异常管理

  • 当前情况
    • 部分方法(如 close_connection)已经实现了异常捕获和日志记录,但整体错误处理可能不够全面。
  • 建议
    • 在所有关键操作中添加更全面的异常处理,确保系统在出现意外情况时能够优雅地恢复或记录详细的错误信息。
    • 使用自定义异常类来处理特定类型的错误,提高代码的可读性和可维护性。

6.2. 性能优化

  • 当前情况
    • 使用多个线程池管理后台任务,但具体的任务调度和资源管理可能需要进一步优化。
  • 建议
    • 根据系统负载和性能需求,动态调整线程池的大小和任务调度策略。
    • 监控系统性能指标,识别和解决潜在的性能瓶颈。

6.3. 安全性增强

  • 当前情况
    • WebSocket 连接的安全性和认证机制可能需要加强,尤其是在处理敏感数据时。
  • 建议
    • 实现更严格的身份验证和授权机制,确保只有授权用户能够访问特定的聊天会话。
    • 加密 WebSocket 连接,保护数据传输的安全性。
    • 定期审查和更新安全策略,防止潜在的安全漏洞。

6.4. 可扩展性与模块化

  • 当前情况
    • ChatManager 类功能较多,可能会随着功能的增加而变得臃肿。
  • 建议
    • ChatManager 的功能拆分为多个模块或子类,如连接管理模块、消息处理模块、任务调度模块等。
    • 使用设计模式(如单一职责原则)确保每个模块专注于特定的功能,提升代码的可维护性和可扩展性。

6.5. 文档与注释

  • 当前情况
    • 部分方法缺乏详细的文档和注释,可能会影响团队协作和代码维护。
  • 建议
    • 为每个方法和关键代码块添加详细的文档和注释,解释其用途、参数、返回值和关键逻辑。
    • 使用类型注解和 docstrings 提升代码的可读性和可维护性。

7. 总结

ChatManager 类是一个功能强大且复杂的类,负责管理 WebSocket 连接、处理消息、维护聊天历史、管理缓存以及协调后台任务。通过使用观察者模式、异步编程和线程池管理,ChatManager 能够高效地处理多个客户端的实时通信需求,并支持复杂的业务逻辑。

核心优势

  • 高并发支持:通过异步编程和线程池,能够同时处理大量的 WebSocket 连接和消息。
  • 灵活的缓存管理:结合 cache_managerInMemoryCache,提升系统性能和响应速度。
  • 全面的聊天历史管理:确保每个聊天会话的历史记录被正确记录和存储,支持后续的查询和分析。
  • 观察者模式的应用:实现了松耦合的系统架构,能够在缓存变化时自动更新聊天历史。

潜在挑战

  • 复杂性管理:随着系统功能的增加,ChatManager 可能会变得更加复杂,需要良好的模块化和代码组织策略。
  • 性能与资源管理:需要持续监控系统性能,优化线程池和任务调度策略,以应对不断增长的负载。
  • 安全性保障:在处理敏感数据和实时通信时,必须确保系统的安全性和数据的保护。

通过持续的优化和维护,ChatManager 可以在实时聊天系统中发挥关键作用,提供高效、稳定和安全的通信服务。如果您有更多关于 ChatManager 类的具体问题或需要进一步的解释,请随时告诉我!

dispatch_client

方法概述

async def dispatch_client(
    self,
    request: Request,  # 原始请求体
    client_id: str,
    chat_id: str,
    login_user: UserPayload,
    work_type: WorkType,
    websocket: WebSocket,
    graph_data: dict = None
):
    client_key = uuid.uuid4().hex
    chat_client = ChatClient(request,
                             client_key,
                             client_id,
                             chat_id,
                             login_user.user_id,
                             login_user,
                             work_type,
                             websocket,
                             graph_data=graph_data)
    await self.accept_client(client_key, chat_client, websocket)
    logger.debug(
        f'act=accept_client client_key={client_key} client_id={client_id} chat_id={chat_id}')
    try:
        while True:
            try:
                json_payload_receive = await asyncio.wait_for(websocket.receive_json(),
                                                              timeout=2.0)
            except asyncio.TimeoutError:
                continue
            try:
                payload = json.loads(json_payload_receive) if json_payload_receive else {}
            except TypeError:
                payload = json_payload_receive
            # client内部处理自己的业务逻辑
            # TODO zgq:这里可以增加线程池防止阻塞
            await chat_client.handle_message(payload)
    except WebSocketDisconnect as e:
        logger.info('act=rcv_client_disconnect {}', str(e))
    except IgnoreException:
        # client 内部自己关闭了ws链接,并无异常的情况
        pass
    except Exception as e:
        # Handle any exceptions that might occur
        logger.exception(str(e))
        await self.close_client(client_key,
                                code=status.WS_1011_INTERNAL_ERROR,
                                reason='后端未知错误类型')
    finally:
        try:
            await self.close_client(client_key,
                                    code=status.WS_1000_NORMAL_CLOSURE,
                                    reason='Client disconnected')
        except Exception as e:
            logger.exception(e)
        self.clear_client(client_key)

方法的主要职责

  1. 管理单个客户端的 WebSocket 连接
    • 接受新的 WebSocket 连接。
    • 维护与客户端的连接状态。
    • 处理客户端发送的消息。
    • 处理连接断开和异常情况。
  2. ChatClient 的协作
    • 创建并管理 ChatClient 实例,负责具体的消息处理逻辑。
  3. 错误处理和资源清理
    • 处理不同类型的异常,确保在出现问题时能够优雅地关闭连接并清理资源。

详细解析

让我们逐行解析这个方法,理解每一部分的具体作用和背后的逻辑。

1. 方法签名和参数

async def dispatch_client(
    self,
    request: Request,  # 原始请求体
    client_id: str,
    chat_id: str,
    login_user: UserPayload,
    work_type: WorkType,
    websocket: WebSocket,
    graph_data: dict = None
):
  • self:指向 ChatManager 实例本身,允许访问和修改类的属性和方法。
  • request: Request:客户端发起的原始 HTTP 请求对象,包含请求的详细信息。
  • client_id: str:标识客户端的唯一 ID,通常用于区分不同的用户或会话。
  • chat_id: str:标识具体聊天会话的 ID,用于管理和区分不同的聊天。
  • login_user: UserPayload:登录用户的详细信息,包含用户的身份和权限等。
  • work_type: WorkType:指示聊天的工作类型,可能影响聊天的处理逻辑。
  • websocket: WebSocket:与客户端的 WebSocket 连接对象,用于实时通信。
  • graph_data: dict = None:可选参数,可能用于描述聊天的流程或图形数据结构。

2. 生成 client_key 并创建 ChatClient 实例

client_key = uuid.uuid4().hex
chat_client = ChatClient(
    request,
    client_key,
    client_id,
    chat_id,
    login_user.user_id,
    login_user,
    work_type,
    websocket,
    graph_data=graph_data
)
  • 生成唯一的 client_key
    • 使用 uuid.uuid4().hex 生成一个唯一的十六进制字符串,确保每个客户端连接都有一个唯一的标识符。
  • 创建 ChatClient 实例:
    • ChatClient 可能是一个封装了具体聊天逻辑的类,负责处理和响应来自客户端的消息。
    • 传递多个参数以初始化 ChatClient,包括请求信息、客户端 ID、聊天 ID、用户信息、工作类型、WebSocket 连接对象,以及可选的图形数据。

3. 接受客户端并注册 ChatClient

await self.accept_client(client_key, chat_client, websocket)
logger.debug(
    f'act=accept_client client_key={client_key} client_id={client_id} chat_id={chat_id}')
  • 调用 accept_client 方法:
    • 负责注册新的客户端连接,将 ChatClient 实例与 client_key 关联起来,并存储 WebSocket 连接对象。
  • 记录调试日志:
    • 使用 logger.debug 记录接受客户端的详细信息,便于调试和监控。

4. 进入消息接收和处理循环

try:
    while True:
        try:
            json_payload_receive = await asyncio.wait_for(websocket.receive_json(), timeout=2.0)
        except asyncio.TimeoutError:
            continue
        try:
            payload = json.loads(json_payload_receive) if json_payload_receive else {}
        except TypeError:
            payload = json_payload_receive
        # client内部处理自己的业务逻辑
        # TODO zgq:这里可以增加线程池防止阻塞
        await chat_client.handle_message(payload)
4.1. 无限循环 (while True)
  • 目的:
    • 持续监听和接收来自客户端的消息,直到连接断开或出现异常。
4.2. 接收 JSON 消息
json_payload_receive = await asyncio.wait_for(websocket.receive_json(), timeout=2.0)
  • websocket.receive_json()
    • 异步方法,等待接收客户端发送的 JSON 格式的消息。
  • asyncio.wait_for(..., timeout=2.0)
    • 设置一个超时时间为 2 秒,如果在此时间内没有接收到消息,则抛出 asyncio.TimeoutError
4.3. 处理超时
except asyncio.TimeoutError:
    continue
  • 目的:
    • 如果在 2 秒内没有接收到消息,则捕获 TimeoutError 并继续循环,等待下一次消息接收。
  • 作用:
    • 防止在长时间没有消息时,连接因超时而被错误地关闭。
4.4. 解析接收到的消息
try:
    payload = json.loads(json_payload_receive) if json_payload_receive else {}
except TypeError:
    payload = json_payload_receive
  • json.loads(json_payload_receive)
    • 尝试将接收到的 JSON 字符串解析为 Python 字典对象。
  • 错误处理:
    • 如果解析失败(如消息不是有效的 JSON 格式),捕获 TypeError 并将 payload 设置为原始接收到的内容。
4.5. 处理消息
await chat_client.handle_message(payload)
  • 调用 ChatClienthandle_message 方法:
    • 负责处理具体的业务逻辑,如解析命令、生成响应、与其他服务交互等。
  • 注意事项:
    • 阻塞风险:在评论中提到 # TODO zgq:这里可以增加线程池防止阻塞,表明 handle_message 可能包含耗时操作,可能导致事件循环阻塞。
    • 优化建议:
      • 如果 handle_message 包含阻塞操作,可以考虑使用线程池(如 concurrent.futures.ThreadPoolExecutor)或进程池来异步执行这些操作,确保不会阻塞主事件循环。

5. 异常处理

except WebSocketDisconnect as e:
    logger.info('act=rcv_client_disconnect {}', str(e))
except IgnoreException:
    # client 内部自己关闭了ws链接,并无异常的情况
    pass
except Exception as e:
    # Handle any exceptions that might occur
    logger.exception(str(e))
    await self.close_client(client_key,
                            code=status.WS_1011_INTERNAL_ERROR,
                            reason='后端未知错误类型')
5.1. 捕获 WebSocketDisconnect
except WebSocketDisconnect as e:
    logger.info('act=rcv_client_disconnect {}', str(e))
  • 目的:
    • 捕获客户端断开 WebSocket 连接的异常。
  • 操作:
    • 记录连接断开的信息,通常这是预期内的情况,不需要进一步处理。
5.2. 捕获 IgnoreException
except IgnoreException:
    # client 内部自己关闭了ws链接,并无异常的情况
    pass
  • 目的:
    • 处理 IgnoreException,这可能是自定义的异常,用于表示某些特定的无需进一步处理的情况。
  • 操作:
    • 通过 pass 语句忽略此异常,不进行任何操作。
5.3. 捕获所有其他异常
except Exception as e:
    # Handle any exceptions that might occur
    logger.exception(str(e))
    await self.close_client(client_key,
                            code=status.WS_1011_INTERNAL_ERROR,
                            reason='后端未知错误类型')
  • 目的:
    • 捕获除上述特定异常外的所有其他异常,确保系统的稳定性。
  • 操作:
    • 记录异常信息,便于调试和错误追踪。
    • 调用 close_client 方法,关闭 WebSocket 连接,传递适当的关闭码和原因。

6. 资源清理 (finally 块)

finally:
    try:
        await self.close_client(client_key,
                                code=status.WS_1000_NORMAL_CLOSURE,
                                reason='Client disconnected')
    except Exception as e:
        logger.exception(e)
    self.clear_client(client_key)
  • 目的:
    • 确保无论连接是如何终止的,都能够正确关闭 WebSocket 连接并清理相关资源。
  • 操作:
    1. 调用 close_client 方法:
      • 传递关闭码 status.WS_1000_NORMAL_CLOSURE 和原因 'Client disconnected',表示正常的连接关闭。
    2. 捕获并记录可能的异常:
      • 如果在关闭连接时出现异常,记录异常信息,防止程序崩溃。
    3. 调用 clear_client 方法:
      • active_clients 中移除客户端的记录,确保资源得到释放,防止内存泄漏。

方法流程图

为了更清晰地理解 dispatch_client 方法的流程,我们可以将其简化为以下步骤:

  1. 初始化
    • 生成 client_key
    • 创建 ChatClient 实例。
    • 接受并注册客户端连接。
  2. 消息处理循环
    • 持续监听客户端发送的消息。
    • 超时后继续等待。
    • 解析消息。
    • 交给 ChatClient 处理。
  3. 异常处理
    • 处理连接断开、忽略异常和其他未知异常。
  4. 资源清理
    • 关闭连接。
    • 清理客户端记录。

与其他组件的交互

1. ChatClient

  • 职责:
    • 封装具体的消息处理逻辑。
    • 负责解析和响应来自客户端的消息。
  • 交互:
    • ChatManager 在消息接收循环中调用 ChatClienthandle_message 方法,传递解析后的 payload

2. accept_client 方法

async def accept_client(self, client_key: str, chat_client: ChatClient, websocket: WebSocket):
    await websocket.accept()
    self.active_clients[client_key] = chat_client
  • 职责:
    • 接受新的 WebSocket 连接。
    • ChatClient 实例与 client_key 关联,并存储在 active_clients 字典中。
  • 作用:
    • 确保每个连接都有一个唯一的标识符,并且可以通过 client_key 快速访问对应的 ChatClient

3. close_client 方法

async def close_client(self, client_key: str, code: int, reason: str):
    if chat_client := self.active_clients.get(client_key):
        try:
            await chat_client.websocket.close(code=code, reason=reason)
            self.clear_client(client_key)
        except RuntimeError as exc:
            if isinstance(exc, concurrent.futures.CancelledError):
                continue
            logger.exception('feature_key={} {}', client_key, e)
            erro_resp = ChatResponse(**base_param)
            context = context_dict.get(future_key)
            if context.get('status') == 'init':
                erro_resp.intermediate_steps = f'LLM 技能执行错误. error={str(e)}'
            elif context.get('has_file'):
                erro_resp.intermediate_steps = f'文档解析失败,点击输入框上传按钮重新上传\n\n{str(e)}'
            else:
                erro_resp.intermediate_steps = f'Input data is parsed fail. error={str(e)}'
            context['status'] = 'init'
            await self.send_json(context.get('flow_id'), context.get('chat_id'), erro_resp)
            erro_resp.type = 'close'
            await self.send_json(context.get('flow_id'), context.get('chat_id'), erro_resp)
  • 职责
    • 关闭特定客户端的 WebSocket 连接。
    • 清理 active_clients 中的记录。
    • 处理关闭连接时可能出现的异常,发送错误响应给客户端。
  • 作用
    • 确保在需要关闭连接时,能够优雅地断开连接并通知客户端。

4. clear_client 方法

def clear_client(self, client_key: str):
    if client_key not in self.active_clients:
        logger.warning('close_client client_key={} not in active_clients', client_key)
        return
    logger.info('close_client client_key={}', client_key)
    self.active_clients.pop(client_key, None)
  • 职责:
    • active_clients 字典中移除指定的 client_key,清理客户端的记录。
  • 作用:
    • 确保在客户端断开连接后,相关的资源和记录被正确释放,防止内存泄漏。

异常处理策略

dispatch_client 方法中,异常处理是确保系统稳定性的重要部分。以下是对不同异常类型的处理策略:

1. WebSocketDisconnect

  • 情景:
    • 客户端主动断开 WebSocket 连接。
  • 处理:
    • 记录连接断开的信息,不需要进一步操作,因为这是一种预期内的行为。

2. IgnoreException

  • 情景:
    • 可能是自定义异常,用于表示无需进一步处理的特定情况。
  • 处理:
    • 通过 pass 忽略,不进行任何操作。

3. 其他异常 (Exception)

  • 情景:
    • 任何未预见的错误或异常情况,如网络错误、解析错误等。
  • 处理:
    • 记录详细的异常信息,便于调试和错误追踪。
    • 调用 close_client 方法,使用 WS_1011_INTERNAL_ERROR 关闭连接,通知客户端出现未知错误。

4. finally 块中的异常处理

  • 情景:
    • 在资源清理过程中可能会出现异常,如关闭连接失败。
  • 处理:
    • 捕获并记录异常,确保即使在清理过程中出现问题,也不会导致程序崩溃。
    • 调用 clear_client 方法,移除客户端记录,确保资源得到释放。

优化建议

1. 增加线程池或进程池防止阻塞

在消息处理过程中,存在潜在的阻塞风险,特别是当 ChatClient.handle_message 方法执行耗时操作时。为了防止阻塞主事件循环,可以考虑使用线程池或进程池来处理这些任务。

实现示例
from concurrent.futures import ThreadPoolExecutor

# 在 ChatManager 的 __init__ 方法中初始化线程池
def __init__(self):
    # ... 其他初始化代码
    self.executor = ThreadPoolExecutor(max_workers=10)

# 修改消息处理部分
try:
    while True:
        try:
            json_payload_receive = await asyncio.wait_for(websocket.receive_json(), timeout=2.0)
        except asyncio.TimeoutError:
            continue
        try:
            payload = json.loads(json_payload_receive) if json_payload_receive else {}
        except TypeError:
            payload = json_payload_receive
        # 使用线程池处理消息
        loop = asyncio.get_event_loop()
        loop.run_in_executor(self.executor, chat_client.handle_message, payload)
except WebSocketDisconnect as e:
    # ... 其他异常处理

2. 优化日志记录

  • 细化日志级别:
    • 使用不同的日志级别(如 infowarningerrordebug)来记录不同重要性的事件,便于日志分析和监控。
  • 结构化日志:
    • 使用结构化日志格式(如 JSON),便于日志的自动化分析和搜索。

3. 增强安全性

  • 身份验证和授权:
    • 确保只有经过身份验证的用户能够建立 WebSocket 连接,并根据用户的权限限制其访问范围。
  • 数据加密:
    • 使用加密协议(如 WSS)来保护 WebSocket 连接中的数据传输,防止数据被窃听或篡改。

4. 增强资源管理

  • 连接限制:
    • 实现连接数限制,防止过多的并发连接导致资源耗尽。
  • 心跳机制:
    • 实现心跳机制,定期发送心跳消息以检测和断开不活跃或僵尸连接。

5. 提高代码可读性和可维护性

  • 代码注释:
    • 为复杂的逻辑或关键步骤添加详细的注释,便于其他开发者理解代码。
  • 模块化:
    • dispatch_client 方法中的不同功能模块化,拆分成多个小方法,提高代码的可读性和可维护性。

总结

dispatch_client 方法是 ChatManager 类中的核心方法,负责管理单个客户端的 WebSocket 连接、接收和处理消息,以及处理连接断开和异常情况。以下是对该方法的关键点总结:

  1. 连接管理:
    • 生成唯一的 client_key,确保每个连接的唯一性。
    • 创建并注册 ChatClient 实例,封装具体的消息处理逻辑。
  2. 消息接收与处理:
    • 使用异步循环持续接收来自客户端的消息。
    • 设置超时机制,防止长时间无消息导致连接异常。
    • 解析接收到的消息,并交由 ChatClient 处理。
  3. 异常处理:
    • 处理客户端主动断开连接的情况。
    • 捕获并处理其他潜在的异常,确保系统稳定性。
  4. 资源清理:
    • 确保在连接断开或异常时,能够正确关闭连接并清理相关资源,防止内存泄漏或资源浪费。
  5. 优化与扩展:
    • 提出了一些优化建议,如使用线程池防止阻塞、增强日志记录、提高安全性、优化资源管理和提升代码可读性。

通过对 dispatch_client 方法的详细解析,您可以更好地理解其在聊天系统中的作用,并根据具体需求进行优化和扩展。如果您有更多关于这个方法或其他部分的疑问,欢迎继续提问!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值