概述
这段代码主要实现了一个基于WebSocket的聊天管理系统,使用FastAPI框架构建。主要功能包括管理聊天历史、处理客户端连接、消息传递、缓存管理以及与后端服务的交互。
主要组件
- 类
ChatHistory- 功能:管理每个客户端的聊天历史记录。
- 方法:
add_message:将消息添加到聊天历史中,并将其存储到数据库。empty_history:清空指定客户端的聊天历史。
- 类
ChatManager- 功能:管理WebSocket连接、处理消息传递、管理缓存和任务调度。
- 属性:
active_connections:存储当前活跃的WebSocket连接。chat_history:实例化的ChatHistory对象,用于管理聊天记录。cache_manager和in_memory_cache:用于缓存管理。task_manager:管理异步任务。active_clients:记录已连接的客户端信息。stream_queue:记录流式输出结果的队列。
- 主要方法:
connect和reuse_connect:处理新的WebSocket连接或复用现有连接。disconnect:断开WebSocket连接。send_message和send_json:向客户端发送消息或JSON数据。close_connection和close_client:关闭连接并进行清理。ping:发送心跳消息。set_cache:设置缓存。accept_client和clear_client:接受新客户端连接并清理客户端信息。dispatch_client:处理客户端的消息接收和业务逻辑分发。handle_websocket:处理WebSocket的整体连接生命周期,包括接收消息、处理负载和管理任务。_process_when_payload:处理接收到的消息负载,并根据业务逻辑发送响应。preper_reuse_connection、preper_payload和preper_action:辅助方法,用于准备复用连接、处理负载和准备动作。init_langchain_object_task:初始化LangChain对象的任务,涉及图数据的构建和缓存设置。refresh_graph_data:刷新图数据,处理节点数据的调整。
关键功能点
- WebSocket管理:通过
handle_websocket方法管理WebSocket的连接生命周期,包括接收和发送消息、处理断开连接等。 - 消息处理:接收到客户端消息后,利用异步任务和线程池处理业务逻辑,确保不会阻塞主事件循环。
- 缓存管理:使用
cache_manager和in_memory_cache进行数据缓存,加快数据访问速度,并减少数据库查询。 - 任务调度:利用
ThreadPoolManager和thread_pool管理并发任务,确保高效的任务执行和资源利用。 - 数据库交互:通过
session_getter和相关DAO类(如ChatMessageDao、UserDao)与数据库进行交互,管理用户信息和聊天记录。 - 日志记录:使用
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_id和chat_id组合而成的唯一标识符,用于区分不同的聊天会话。 - 值(
List[ChatMessage]):存储对应聊天会话的所有消息列表。
- 键(
-
defaultdict(list):- 使用
defaultdict可以自动为新键创建一个空列表,避免在添加消息时需要先检查键是否存在。 - 例如,当一个新的
client_id和chat_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:- 表示要添加到聊天历史中的消息对象。
方法逻辑详解
-
记录开始时间
t1 = time.time()- 用于测量消息添加到数据库所花费的时间,便于性能监控和日志记录。
-
动态导入
ChatMessage模型from bisheng.database.models.message import ChatMessage- 延迟导入(在方法内部导入)可以减少模块间的耦合,避免在类初始化时引发不必要的依赖加载。
-
设置消息的
flow_id和chat_idmessage.flow_id = client_id message.chat_id = chat_id- 将
client_id和chat_id赋值给消息对象,确保消息与正确的会话关联。
- 将
-
条件判断:决定是否将消息保存到数据库
if chat_id and (message.message or message.intermediate_steps or message.files) and message.type != 'stream':- 条件:
chat_id存在:确保消息属于某个有效的聊天会话。message.message或message.intermediate_steps或message.files存在:确保消息中有实际内容需要保存。message.type != 'stream':排除类型为 ‘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__)- 复制消息对象:
- 使用
copy()方法创建消息的副本,避免直接修改原始消息对象。
- 使用
- 序列化
message字段:- 如果
message.message是字典类型,则将其序列化为 JSON 字符串。 - 这样做可以确保在数据库中以可存储的格式保存复杂的数据结构。
- 如果
- 处理
files字段:- 将
message.files序列化为 JSON 字符串,如果存在文件信息。 - 从消息副本中移除
files字段,因为文件信息已经被单独处理。
- 将
- 创建数据库消息对象:
- 使用处理后的
msg数据创建一个新的ChatMessage实例(数据库模型)。 - 这将用于将消息存储到数据库中。
- 使用处理后的
- 复制消息对象:
-
日志记录
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.idsession_getter():- 这是一个上下文管理器,用于获取数据库会话(通常基于 SQLAlchemy 会话)。
- 确保数据库操作的原子性和事务管理。
seesion.add(db_message):- 将
db_message添加到当前会话中,准备插入数据库。
- 将
seesion.commit():- 提交事务,将所有挂起的更改(如新增消息)持久化到数据库。
seesion.refresh(db_message):- 刷新
db_message对象,以确保它包含最新的数据库状态(如自动生成的主键 ID)。
- 刷新
message.message_id = db_message.id:- 将数据库生成的
id赋值给原始消息对象,确保消息对象在内存中也包含数据库中的主键。
- 将数据库生成的
-
通知观察者
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:- 特定聊天会话的唯一标识符。
方法逻辑详解
-
获取唯一键
get_cache_key(client_id, chat_id)- 使用
get_cache_key函数将client_id和chat_id组合成一个唯一的字符串键。 - 这种方式确保不同的聊天会话在
history字典中有独立的条目。
- 使用
-
清空聊天历史
self.history[get_cache_key(client_id, chat_id)] = []- 将指定
client_id和chat_id的聊天历史列表重置为空列表,意味着清除了所有历史消息。
- 将指定
作用
- 用途:
- 当用户选择清除聊天历史记录时,调用此方法即可快速清空相关的消息记录。
- 影响:
- 不仅在内存中清除了聊天历史,还需要确保其他相关的数据存储(如数据库中的记录)也得到同步清理(这部分可能在其他地方处理)。
综合分析与系统中的作用
1. 聊天历史管理
- 存储机制:
ChatHistory类通过self.history字典管理不同客户端和聊天会话的消息记录。- 使用
defaultdict(list)确保每个新的聊天会话自动初始化一个空的消息列表,简化了消息添加的逻辑。
- 消息添加:
add_message方法不仅在内存中添加消息,还将消息持久化到数据库中,确保消息的持久性和可恢复性。- 对于需要持久化的消息类型(非
FileResponse),通过notify()方法通知系统中的其他组件(如前端)有新的消息可用。
- 消息清空:
empty_history方法允许系统在需要时(如用户请求清除历史记录)快速清空指定聊天会话的消息历史。
2. 观察者模式的应用
Subject类的角色:- 通过继承
Subject,ChatHistory成为了一个被观察者。它可以有多个观察者注册在其上,接收关于聊天历史变化的通知。
- 通过继承
- 通知机制:
- 当
add_message方法添加新消息时,调用self.notify()触发通知机制,使得所有观察者可以感知到聊天历史的变化,并作出相应的反应(如更新前端界面、日志记录等)。
- 当
3. 数据持久化与一致性
- 数据库交互:
- 通过
session_getter管理数据库会话,确保消息的持久化操作安全、原子且高效。 - 将消息对象转换为数据库模型
ChatMessage,并处理可能的复杂字段(如序列化的 JSON 数据),确保数据在数据库中的一致性和完整性。
- 通过
- 消息 ID 的同步:
- 在数据库保存消息后,将数据库生成的
id赋值给内存中的消息对象,保持内存数据和数据库数据的一致性。
- 在数据库保存消息后,将数据库生成的
4. 性能与效率
- 延迟导入:
- 在
add_message方法内部导入数据库模型,减少了模块加载时的依赖和开销,提高了系统启动效率。
- 在
- 条件判断优化:
- 通过条件判断仅对需要持久化的消息进行数据库操作,避免了不必要的资源消耗。
- 日志记录:
- 记录消息保存的时间和内容,有助于性能监控、调试和故障排查。
5. 灵活性与扩展性
- 支持多种消息类型:
- 通过检查消息类型(如排除
stream类型),ChatHistory可以灵活处理不同类型的消息,适应多样化的业务需求。
- 通过检查消息类型(如排除
- 观察者的扩展:
- 由于采用了观察者模式,未来可以方便地添加新的观察者(如新的日志记录器、实时监控系统等),无需修改
ChatHistory类的核心逻辑。
- 由于采用了观察者模式,未来可以方便地添加新的观察者(如新的日志记录器、实时监控系统等),无需修改
潜在改进与优化建议
虽然 ChatHistory 类功能完备,但在实际应用中可能还存在一些优化空间:
-
错误处理与异常管理:
- 当前的
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}') # 可能的处理措施 - 当前的
-
异步支持:
- 当前的
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 ... - 当前的
-
批量操作优化:
- 在高频消息场景下,频繁的数据库提交可能会成为瓶颈。
- 可以考虑批量保存消息,减少数据库事务的频率,提升整体性能。
-
消息过期与清理策略:
- 长时间运行的系统中,聊天历史可能会变得非常庞大,占用大量内存和数据库空间。
- 需要设计适当的消息过期与清理策略,如基于时间或数量的历史记录限制。
-
数据索引与查询优化:
- 确保数据库中的
ChatMessage表对flow_id和chat_id等常用查询字段建立索引,以加快查询速度。
- 确保数据库中的
-
安全性考虑:
- 确保消息内容在存储和传输过程中得到适当的加密和保护,防止敏感信息泄露。
总结
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_id和chat_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)
- 用途:
- 将
ChatManager的update方法注册为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更新时被调用。
- 作为观察者模式中的回调方法,当
- 功能:
- 检查当前的
client_id是否在活跃连接中。 - 获取
cache_manager的最新缓存对象。 - 创建一个
FileResponse对象,包含从缓存中获取的数据。 - 将
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 连接请求。
- 功能:
- 接受 WebSocket 连接。
- 将连接对象存储到
active_connections字典中,键为client_id和chat_id组合的唯一键。 - 为该会话创建一个新的消息队列,并存储到
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 连接,通常在某些特定情况下需要复用连接时调用。
- 功能:
- 更新
active_connections和stream_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 连接。
- 功能:
- 根据传入的
key(如果有)或通过client_id和chat_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)
- 用途:
- 向特定客户端发送文本消息。
- 功能:
- 根据
client_id和chat_id获取对应的 WebSocket 连接。 - 通过 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 格式的消息。
- 功能:
- 设置消息的
flow_id和chat_id属性。 - 根据唯一键获取对应的 WebSocket 连接。
- 如果
add参数为True,将消息添加到聊天历史中。 - 通过 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 连接,并进行必要的清理工作。
- 功能:
- 获取对应的 WebSocket 连接。
- 尝试关闭 WebSocket 连接,传递关闭码和原因。
- 调用
disconnect方法移除连接对象。 - 如果提供了
key_list,则遍历并移除这些额外的连接。 - 捕获并记录可能的运行时错误,避免程序崩溃。
- 作用:
- 确保在连接关闭时,资源得到正确释放,系统保持稳定。
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请求。
- 向客户端发送一个
- 功能:
- 创建一个
ChatMessage对象,内容为'pong'。 - 使用
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
- 用途:
- 设置特定客户端的缓存数据。
- 功能:
- 使用
in_memory_cache的set方法,将langchain_object存储在客户端的缓存中。 - 返回该
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
- 用途:
- 接受并注册一个新的客户端连接。
- 功能:
- 接受 WebSocket 连接。
- 将
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)
- 用途:
- 清除特定客户端的注册信息。
- 功能:
- 检查
client_key是否存在于active_clients字典中。 - 如果存在,移除该客户端的记录。
- 如果不存在,记录警告信息。
- 检查
- 作用:
- 确保在客户端断开连接或需要移除时,相关的客户端信息得到正确清理,防止内存泄漏或资源浪费。
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 连接,并处理可能出现的异常。
- 功能:
- 获取对应的
ChatClient对象。 - 尝试关闭其 WebSocket 连接,传递关闭码和原因。
- 调用
clear_client方法移除客户端记录。 - 捕获并处理可能的运行时错误,记录异常并发送错误响应给客户端。
- 获取对应的
- 作用:
- 确保在关闭客户端连接时,能够正确处理异常情况,保持系统的稳定性和一致性。
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)
- 用途:
- 分发和处理特定客户端的消息,管理整个消息接收和处理的生命周期。
- 功能:
- 生成一个唯一的
client_key。 - 创建一个新的
ChatClient对象,封装客户端的各种信息和状态。 - 调用
accept_client方法注册新的客户端。 - 进入一个无限循环,持续接收和处理来自客户端的消息。
- 处理不同类型的异常,如 WebSocket 断开、忽略异常、以及其他未知异常。
- 在最终块中,确保客户端连接得到正确关闭和清理。
- 生成一个唯一的
- 作用:
- 确保每个客户端的消息能够被持续接收和正确处理,同时在断开连接或出现异常时,能够优雅地关闭连接并清理资源。
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 连接,负责接收和处理来自客户端的消息。
- 功能:
- 通过
flow_id和chat_id生成唯一键,并建立连接。 - 初始化
context_dict,用于跟踪每个会话的状态。 - 进入消息接收循环,持续接收来自客户端的 JSON 消息。
- 根据消息内容决定是否复用连接或创建新的会话。
- 调用
_process_when_payload方法处理具体的消息负载。 - 监控后台任务的完成状态,并处理可能的异常。
- 捕获并处理各种异常,确保连接的稳定性和资源的正确释放。
- 在最终块中,取消所有相关任务并关闭连接。
- 通过
- 作用:
- 负责整个 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
- 用途:
- 处理接收到的消息负载,根据当前的会话状态和消息内容执行相应的操作。
- 功能:
- 提取并初始化相关参数,如
user_id、graph_data、payload和context。 - 判断是否是会话的开始,并发送
start_resp消息。 - 检查是否存在文件上传或变量输入,调用
preper_payload方法进行预处理。 - 根据缓存中的
langchain_obj_key判断是否需要初始化 LangChain 对象,提交相应的任务到线程池。 - 处理具体的操作动作,如
autogen、stop、clear_history等,调用preper_action方法进行准备。 - 根据
langchain_obj_key的类型,提交不同的任务处理器(如AutoGenChain)到线程池。 - 更新会话状态和上下文信息,确保下次处理能够正确执行。
- 提取并初始化相关参数,如
- 作用:
- 负责解析和处理来自客户端的消息,根据会话状态和内容执行相应的业务逻辑,如生成报告、处理文件上传等。
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 连接,检查技能状态并更新连接信息。
- 功能:
- 使用
session_getter获取数据库会话,查询特定flow_id的Flow对象。 - 根据
Flow对象的存在与状态,设置相应的错误消息。 - 调用
reuse_connect方法更新连接和消息队列。 - 返回
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
- 用途:
- 预处理收到的消息负载,识别是否包含文件上传或变量输入,并相应地更新会话状态和缓存。
- 功能:
- 检查
payload是否包含文件数据或变量数据。 - 根据
node_data更新graph_data。 - 判断是否需要加载节点(
node_loader),并根据需要重建 LangChain 对象。 - 设置
has_file和has_variable标志。 - 如果包含文件,发送相应的响应消息,并跳过进一步处理。
- 如果包含变量,同样发送响应消息并跳过进一步处理。
- 返回
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
- 用途:
- 准备和执行具体的操作动作,根据消息内容和会话状态决定下一步行动。
- 功能:
- 获取缓存中的 LangChain 对象。
- 根据 LangChain 对象的类型和
payload中的内容,确定执行的动作类型(如生成报告、停止操作、自动生成等)。 - 根据动作类型,更新响应消息并发送给客户端。
- 对特定动作(如清除历史记录),执行相应的操作并设置标志。
- 作用:
- 确保系统能够根据不同的请求和状态,执行适当的业务逻辑,如生成报告、处理文件上传等。
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 对象,准备处理复杂的任务如生成报告、处理文件等。
- 功能:
- 根据
flow_id和chat_id生成唯一键。 - 获取用户信息,以支持权限判断。
- 使用
build_flow_no_yield函数构建任务流程图,并进行必要的文件处理。 - 遍历流程图中的节点,收集输入问题并存储到缓存中。
- 将构建完成的对象和相关工件(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)
- 用途:
- 更新和调整流程图数据,根据上传的节点数据进行必要的处理。
- 功能:
- 处理传入的
node_data,生成调整参数(tweak)。 - 调用
process_tweaks函数,根据调整参数更新graph_data。 - 返回更新后的
graph_data。
- 处理传入的
- 作用:
- 确保流程图数据能够根据最新的节点输入进行动态调整,支持系统的灵活性和扩展性。
4. 设计模式与架构
4.1. 观察者模式
ChatManager 使用观察者模式来响应 cache_manager 的更新。这种模式允许 ChatManager 在 cache_manager 发生变化时,自动执行相应的逻辑(如更新聊天历史)。
- 实现细节:
ChatManager在初始化时调用self.cache_manager.attach(self.update),将自身的update方法注册为cache_manager的观察者。- 当
cache_manager更新时,会调用ChatManager的update方法,执行相关操作。
4.2. 异步编程
ChatManager 广泛使用 asyncio 进行异步编程,以支持高并发的 WebSocket 连接和消息处理。
- 实现细节:
- 使用
async def定义异步方法,如connect、send_message、handle_websocket等。 - 使用
asyncio.Task来管理后台任务,确保系统能够同时处理多个任务而不阻塞主线程。
- 使用
4.3. 线程池与任务管理
ChatManager 使用线程池(如 thread_pool 和 autogen_pool)来处理需要长时间运行的任务,如生成报告或处理复杂的消息负载。
- 实现细节:
- 通过
thread_pool.submit和autogen_pool.submit提交任务,确保这些任务在后台线程中运行,不会阻塞主线程。 - 使用
asyncio.Queue(如stream_queue)来管理和协调流式输出结果。
- 通过
4.4. 缓存管理
ChatManager 使用 InMemoryCache 和 cache_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_manager和InMemoryCache,提升系统性能和响应速度。 - 全面的聊天历史管理:确保每个聊天会话的历史记录被正确记录和存储,支持后续的查询和分析。
- 观察者模式的应用:实现了松耦合的系统架构,能够在缓存变化时自动更新聊天历史。
潜在挑战
- 复杂性管理:随着系统功能的增加,
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)
方法的主要职责
- 管理单个客户端的 WebSocket 连接:
- 接受新的 WebSocket 连接。
- 维护与客户端的连接状态。
- 处理客户端发送的消息。
- 处理连接断开和异常情况。
- 与
ChatClient的协作:- 创建并管理
ChatClient实例,负责具体的消息处理逻辑。
- 创建并管理
- 错误处理和资源清理:
- 处理不同类型的异常,确保在出现问题时能够优雅地关闭连接并清理资源。
详细解析
让我们逐行解析这个方法,理解每一部分的具体作用和背后的逻辑。
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。
- 设置一个超时时间为 2 秒,如果在此时间内没有接收到消息,则抛出
4.3. 处理超时
except asyncio.TimeoutError:
continue
- 目的:
- 如果在 2 秒内没有接收到消息,则捕获
TimeoutError并继续循环,等待下一次消息接收。
- 如果在 2 秒内没有接收到消息,则捕获
- 作用:
- 防止在长时间没有消息时,连接因超时而被错误地关闭。
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设置为原始接收到的内容。
- 如果解析失败(如消息不是有效的 JSON 格式),捕获
4.5. 处理消息
await chat_client.handle_message(payload)
- 调用
ChatClient的handle_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 连接并清理相关资源。
- 操作:
- 调用
close_client方法:- 传递关闭码
status.WS_1000_NORMAL_CLOSURE和原因'Client disconnected',表示正常的连接关闭。
- 传递关闭码
- 捕获并记录可能的异常:
- 如果在关闭连接时出现异常,记录异常信息,防止程序崩溃。
- 调用
clear_client方法:- 从
active_clients中移除客户端的记录,确保资源得到释放,防止内存泄漏。
- 从
- 调用
方法流程图
为了更清晰地理解 dispatch_client 方法的流程,我们可以将其简化为以下步骤:
- 初始化:
- 生成
client_key。 - 创建
ChatClient实例。 - 接受并注册客户端连接。
- 生成
- 消息处理循环:
- 持续监听客户端发送的消息。
- 超时后继续等待。
- 解析消息。
- 交给
ChatClient处理。
- 异常处理:
- 处理连接断开、忽略异常和其他未知异常。
- 资源清理:
- 关闭连接。
- 清理客户端记录。
与其他组件的交互
1. ChatClient 类
- 职责:
- 封装具体的消息处理逻辑。
- 负责解析和响应来自客户端的消息。
- 交互:
ChatManager在消息接收循环中调用ChatClient的handle_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. 优化日志记录
- 细化日志级别:
- 使用不同的日志级别(如
info、warning、error、debug)来记录不同重要性的事件,便于日志分析和监控。
- 使用不同的日志级别(如
- 结构化日志:
- 使用结构化日志格式(如 JSON),便于日志的自动化分析和搜索。
3. 增强安全性
- 身份验证和授权:
- 确保只有经过身份验证的用户能够建立 WebSocket 连接,并根据用户的权限限制其访问范围。
- 数据加密:
- 使用加密协议(如 WSS)来保护 WebSocket 连接中的数据传输,防止数据被窃听或篡改。
4. 增强资源管理
- 连接限制:
- 实现连接数限制,防止过多的并发连接导致资源耗尽。
- 心跳机制:
- 实现心跳机制,定期发送心跳消息以检测和断开不活跃或僵尸连接。
5. 提高代码可读性和可维护性
- 代码注释:
- 为复杂的逻辑或关键步骤添加详细的注释,便于其他开发者理解代码。
- 模块化:
- 将
dispatch_client方法中的不同功能模块化,拆分成多个小方法,提高代码的可读性和可维护性。
- 将
总结
dispatch_client 方法是 ChatManager 类中的核心方法,负责管理单个客户端的 WebSocket 连接、接收和处理消息,以及处理连接断开和异常情况。以下是对该方法的关键点总结:
- 连接管理:
- 生成唯一的
client_key,确保每个连接的唯一性。 - 创建并注册
ChatClient实例,封装具体的消息处理逻辑。
- 生成唯一的
- 消息接收与处理:
- 使用异步循环持续接收来自客户端的消息。
- 设置超时机制,防止长时间无消息导致连接异常。
- 解析接收到的消息,并交由
ChatClient处理。
- 异常处理:
- 处理客户端主动断开连接的情况。
- 捕获并处理其他潜在的异常,确保系统稳定性。
- 资源清理:
- 确保在连接断开或异常时,能够正确关闭连接并清理相关资源,防止内存泄漏或资源浪费。
- 优化与扩展:
- 提出了一些优化建议,如使用线程池防止阻塞、增强日志记录、提高安全性、优化资源管理和提升代码可读性。
通过对 dispatch_client 方法的详细解析,您可以更好地理解其在聊天系统中的作用,并根据具体需求进行优化和扩展。如果您有更多关于这个方法或其他部分的疑问,欢迎继续提问!
1579

被折叠的 条评论
为什么被折叠?



