From 0eac9135c07bc7897cddf70859c8018f4d21b01d Mon Sep 17 00:00:00 2001 From: fdc Date: Mon, 30 Jun 2025 17:58:18 +0800 Subject: [PATCH 001/107] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E6=B5=81?= =?UTF-8?q?=E5=BC=8F=E6=B6=88=E6=81=AF=E5=A4=84=E7=90=86=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fix.MD | 47 +++++++ pkg/core/entities.py | 2 +- pkg/pipeline/process/handlers/chat.py | 40 +++++- pkg/pipeline/respback/respback.py | 22 ++++ pkg/platform/adapter.py | 21 ++++ pkg/provider/entities.py | 83 +++++++++++++ pkg/provider/modelmgr/requester.py | 5 +- pkg/provider/modelmgr/requesters/chatcmpl.py | 90 ++++++++++++-- pkg/provider/runners/localagent.py | 123 +++++++++++++++---- 9 files changed, 387 insertions(+), 46 deletions(-) create mode 100644 fix.MD diff --git a/fix.MD b/fix.MD new file mode 100644 index 00000000..51927eb9 --- /dev/null +++ b/fix.MD @@ -0,0 +1,47 @@ +## 底层模型请求器 + +- pkg/provider/modelmgr/requesters/... + +给 invoke_llm 加个 stream: bool 参数,并允许 invoke_llm 返回两种参数:原来的 llm_entities.Message(非流式)和 返回 llm_entities.MessageChunk(流式,需要新增这个实体)的 AsyncGenerator + +## Runner + +- pkg/provider/runners/... + +每个runner的run方法也允许传入stream: bool。 + +现在的run方法本身就是生成器(AsyncGenerator),因为agent是有多回合的,会生成多条Message。但现在需要支持文本消息可以分段。 + +现在run方法应该返回 AsyncGenerator[ Union[ Message, AsyncGenerator[MessageChunk] ] ]。 + +对于 local agent 的实现上,调用模型invoke_llm时,传入stream,当发现模型返回的是Message时,即按照现在的写法操作Message;当返回的是 AsyncGenerator 时,需要 yield MessageChunk 给上层,同时需要注意判断工具调用。 + +## 流水线 + +- pkg/pipeline/process/handlers/chat.py + +之前这里就已经有一个生成器写法了,用于处理 AsyncGenerator[Message],但现在需要加上一个判断,如果yield出来的是 Message 则按照现在的处理;如果yield出来的是 AsyncGenerator,那么就需要再 async for 一层; + +因为流水线是基于责任链模式设计的,这里的生成结果只需要放入 Query 对象中,供下一层处理。 + +所以需要在 Query 对象中支持存入MessageChunk,现在只支持存 Message 到 resp_messages,这里得设计一下。 + +## 回复阶段 + +最终会在 pkg/pipeline/respback/respback.py 中检出 query 中的信息并发回,这里也要改成支持 MessagChunk 的。 + +这里应该判断适配器是否支持流式,若不支持,应该等待所有 MessageChunk 生成,拼接成 Message 再转换成 MessageChain 调用 send_message(); + +若支持,则uuid生成一个message id,使用该message id调用适配器的 reply_message_chunk 方法。 + +## 机器人适配器 + +因为机器人可能会由于用户配置项不同而表现为对流式的支持性不同,比如飞书默认不支持流式,需要用户额外配置卡片。 + +所以需要新增一个方法 `is_stream_output_supported() -> bool`,这个让每个适配器来判断并返回是否支持流式; + +在发送时,得加两个方法 `send_message_chunk(target_type: str, target_id: str, message_id: , message: MessageChain)` + +message_id 确定同一条消息,由调用方生成; + +`reply_message_chunk(message_source: MessageEvent, message: MessageChain)` \ No newline at end of file diff --git a/pkg/core/entities.py b/pkg/core/entities.py index 4caf18ed..4873d9ce 100644 --- a/pkg/core/entities.py +++ b/pkg/core/entities.py @@ -87,7 +87,7 @@ class Query(pydantic.BaseModel): """使用的函数,由前置处理器阶段设置""" resp_messages: ( - typing.Optional[list[llm_entities.Message]] | typing.Optional[list[platform_message.MessageChain]] + typing.Optional[list[llm_entities.Message]] | typing.Optional[list[platform_message.MessageChain]] | typing.Optional[list[llm_entities.MessageChunk]] ) = [] """由Process阶段生成的回复消息对象列表""" diff --git a/pkg/pipeline/process/handlers/chat.py b/pkg/pipeline/process/handlers/chat.py index 35fa1611..c90d283b 100644 --- a/pkg/pipeline/process/handlers/chat.py +++ b/pkg/pipeline/process/handlers/chat.py @@ -1,5 +1,6 @@ from __future__ import annotations +from itertools import accumulate import typing import traceback @@ -59,6 +60,8 @@ class ChatMessageHandler(handler.MessageHandler): text_length = 0 + is_stream = query.adapter.is_stream_output_supported() + try: for r in runner_module.preregistered_runners: if r.name == query.pipeline_config['ai']['runner']['runner']: @@ -66,18 +69,43 @@ class ChatMessageHandler(handler.MessageHandler): break else: raise ValueError(f'未找到请求运行器: {query.pipeline_config["ai"]["runner"]["runner"]}') + if is_stream: + accumulated_messages = [] + async for result in runner.run(query): + accumulated_messages.append(result) + query.resp_messages.append(result) - async for result in runner.run(query): - query.resp_messages.append(result) + self.ap.logger.info(f'对话({query.query_id})流式响应: {self.cut_str(result.readable_str())}') - self.ap.logger.info(f'对话({query.query_id})响应: {self.cut_str(result.readable_str())}') + if result.content is not None: + text_length += len(result.content) - if result.content is not None: - text_length += len(result.content) + # current_chain = platform_message.MessageChain([]) + # for msg in accumulated_messages: + # if msg.content is not None: + # current_chain.append(platform_message.Plain(msg.content)) + # query.resp_message_chain = [current_chain] + + + + + - yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) + yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) + else: + + async for result in runner.run(query): + query.resp_messages.append(result) + + self.ap.logger.info(f'对话({query.query_id})响应: {self.cut_str(result.readable_str())}') + + if result.content is not None: + text_length += len(result.content) + + yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) query.session.using_conversation.messages.append(query.user_message) + query.session.using_conversation.messages.extend(query.resp_messages) except Exception as e: self.ap.logger.error(f'对话({query.query_id})请求失败: {type(e).__name__} {str(e)}') diff --git a/pkg/pipeline/respback/respback.py b/pkg/pipeline/respback/respback.py index 39d3abb1..7654896b 100644 --- a/pkg/pipeline/respback/respback.py +++ b/pkg/pipeline/respback/respback.py @@ -36,6 +36,28 @@ class SendResponseBackStage(stage.PipelineStage): quote_origin = query.pipeline_config['output']['misc']['quote-origin'] + has_chunks = any(isinstance(msg, llm_entities.MessageChunk) for msg in query.resp_messages) + if has_chunks and hasattr(query.adapter,'reply_message_chunk'): + + async def message_generator(): + for msg in query.resp_messages: + if isinstance(msg, llm_entities.MessageChunk): + yield msg.content + else: + yield msg.content + await query.adapter.reply_message_chunk( + message_source=query.message_event, + message_id=query.message_event.message_id, + message_generator=message_generator(), + quote_origin=quote_origin, + ) + else: + await query.adapter.reply_message( + message_source=query.message_event, + message=query.resp_message_chain[-1], + quote_origin=quote_origin, + ) + await query.adapter.reply_message( message_source=query.message_event, message=query.resp_message_chain[-1], diff --git a/pkg/platform/adapter.py b/pkg/platform/adapter.py index f28ad3dc..c841ae98 100644 --- a/pkg/platform/adapter.py +++ b/pkg/platform/adapter.py @@ -49,11 +49,27 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): async def reply_message( self, message_source: platform_events.MessageEvent, + message_id: int, message: platform_message.MessageChain, quote_origin: bool = False, ): """回复消息 + Args: + message_source (platform.types.MessageEvent): 消息源事件 + message_id (int): 消息ID + message (platform.types.MessageChain): 消息链 + quote_origin (bool, optional): 是否引用原消息. Defaults to False. + """ + raise NotImplementedError + + async def reply_message_chunk( + self, + message_source: platform_events.MessageEvent, + message: platform_message.MessageChain, + quote_origin: bool = False, + ): + """回复消息(流式输出) Args: message_source (platform.types.MessageEvent): 消息源事件 message (platform.types.MessageChain): 消息链 @@ -94,6 +110,11 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): async def run_async(self): """异步运行""" raise NotImplementedError + + + async def is_stream_output_supported(self) -> bool: + """是否支持流式输出""" + return False async def kill(self) -> bool: """关闭适配器 diff --git a/pkg/provider/entities.py b/pkg/provider/entities.py index 94b812d9..a149fea3 100644 --- a/pkg/provider/entities.py +++ b/pkg/provider/entities.py @@ -125,6 +125,89 @@ class Message(pydantic.BaseModel): return platform_message.MessageChain(mc) +class MessageChunk(pydantic.BaseModel): + """消息""" + + role: str # user, system, assistant, tool, command, plugin + """消息的角色""" + + name: typing.Optional[str] = None + """名称,仅函数调用返回时设置""" + + all_content: typing.Optional[str] = None + """所有内容""" + + content: typing.Optional[list[ContentElement]] | typing.Optional[str] = None + """内容""" + + # tool_calls: typing.Optional[list[ToolCall]] = None + """工具调用""" + + tool_call_id: typing.Optional[str] = None + + tool_calls: typing.Optional[list[ToolCallChunk]] = None + + is_final: bool = False + + def readable_str(self) -> str: + if self.content is not None: + return str(self.role) + ': ' + str(self.get_content_platform_message_chain()) + elif self.tool_calls is not None: + return f'调用工具: {self.tool_calls[0].id}' + else: + return '未知消息' + + def get_content_platform_message_chain(self, prefix_text: str = '') -> platform_message.MessageChain | None: + """将内容转换为平台消息 MessageChain 对象 + + Args: + prefix_text (str): 首个文字组件的前缀文本 + """ + + if self.content is None: + return None + elif isinstance(self.content, str): + return platform_message.MessageChain([platform_message.Plain(prefix_text + self.content)]) + elif isinstance(self.content, list): + mc = [] + for ce in self.content: + if ce.type == 'text': + mc.append(platform_message.Plain(ce.text)) + elif ce.type == 'image_url': + if ce.image_url.url.startswith('http'): + mc.append(platform_message.Image(url=ce.image_url.url)) + else: # base64 + b64_str = ce.image_url.url + + if b64_str.startswith('data:'): + b64_str = b64_str.split(',')[1] + + mc.append(platform_message.Image(base64=b64_str)) + + # 找第一个文字组件 + if prefix_text: + for i, c in enumerate(mc): + if isinstance(c, platform_message.Plain): + mc[i] = platform_message.Plain(prefix_text + c.text) + break + else: + mc.insert(0, platform_message.Plain(prefix_text)) + + return platform_message.MessageChain(mc) + + +class ToolCallChunk(pydantic.BaseModel): + """工具调用""" + + id: str + """工具调用ID""" + + type: str + """工具调用类型""" + + function: FunctionCall + """函数调用""" + class Prompt(pydantic.BaseModel): """供AI使用的Prompt""" diff --git a/pkg/provider/modelmgr/requester.py b/pkg/provider/modelmgr/requester.py index 244f4c82..3e5e791f 100644 --- a/pkg/provider/modelmgr/requester.py +++ b/pkg/provider/modelmgr/requester.py @@ -60,8 +60,9 @@ class LLMAPIRequester(metaclass=abc.ABCMeta): model: RuntimeLLMModel, messages: typing.List[llm_entities.Message], funcs: typing.List[tools_entities.LLMFunction] = None, + stream: bool = False, extra_args: dict[str, typing.Any] = {}, - ) -> llm_entities.Message: + ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: """调用API Args: @@ -71,6 +72,6 @@ class LLMAPIRequester(metaclass=abc.ABCMeta): extra_args (dict[str, typing.Any], optional): 额外的参数. Defaults to {}. Returns: - llm_entities.Message: 返回消息对象 + llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: 返回消息对象 """ pass diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index 513086e5..22931611 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -57,13 +57,35 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): message = llm_entities.Message(**chatcmpl_message) return message + + async def _make_msg_chunk( + self, + chat_completion: chat_completion.ChatCompletion, + ) -> llm_entities.MessageChunk: + choice = chat_completion.choices[0] + delta = choice.delta.model_dump() + # 确保 role 字段存在且不为 None + if 'role' not in delta or delta['role'] is None: + delta['role'] = 'assistant' + + reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None + + # deepseek的reasoner模型 + if reasoning_content is not None: + delta['content'] = '\n' + reasoning_content + '\n\n' + delta['content'] + + message = llm_entities.MessageChunk(**delta) + + return message + async def _closure( self, query: core_entities.Query, req_messages: list[dict], use_model: requester.RuntimeLLMModel, use_funcs: list[tools_entities.LLMFunction] = None, + stream: bool = False, extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message: self.client.api_key = use_model.token_mgr.get_token() @@ -91,13 +113,42 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): args['messages'] = messages - # 发送请求 - resp = await self._req(args, extra_body=extra_args) + if stream: + current_content = '' + async for chunk in await self._req(args, extra_body=extra_args): - # 处理请求结果 - message = await self._make_msg(resp) + # 处理流式消息 + delta_message = await self._make_msg_chunk( + chat_completion=chunk, + ) + if delta_message.content: + current_content += delta_message.content + delta_message.all_content = current_content + + # 检查是否为最后一个块 + if chunk.choices[0].finish_reason is not None: + delta_message.is_final = True - return message + yield delta_message + return + + else: + + # 非流式请求 + resp = await self._req(args, extra_body=extra_args) + # 处理请求结果 + # 发送请求 + resp = await self._req(args, extra_body=extra_args) + + # 处理请求结果 + message = await self._make_msg(resp) + + return message + + + + + async def invoke_llm( self, @@ -105,8 +156,9 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): model: requester.RuntimeLLMModel, messages: typing.List[llm_entities.Message], funcs: typing.List[tools_entities.LLMFunction] = None, + stream: bool = False, extra_args: dict[str, typing.Any] = {}, - ) -> llm_entities.Message: + ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: req_messages = [] # req_messages 仅用于类内,外部同步由 query.messages 进行 for m in messages: msg_dict = m.dict(exclude_none=True) @@ -119,13 +171,25 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): req_messages.append(msg_dict) try: - return await self._closure( - query=query, - req_messages=req_messages, - use_model=model, - use_funcs=funcs, - extra_args=extra_args, - ) + if stream: + async for item in self._closure( + query=query, + req_messages=req_messages, + use_model=model, + use_funcs=funcs, + stream=stream, + extra_args=extra_args, + ): + yield item + return + else: + return await self._closure( + query=query, + req_messages=req_messages, + use_model=model, + use_funcs=funcs, + extra_args=extra_args, + ) except asyncio.TimeoutError: raise errors.RequesterError('请求超时') except openai.BadRequestError as e: diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index 7d5e04c5..02b2db16 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +from ssl import ALERT_DESCRIPTION_BAD_CERTIFICATE_HASH_VALUE import typing from .. import runner @@ -12,26 +13,68 @@ from .. import entities as llm_entities class LocalAgentRunner(runner.RequestRunner): """本地Agent请求运行器""" - async def run(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: + class ToolCallTracker: + """工具调用追踪器""" + def __init__(self): + self.active_calls: dict[str,dict] = {} + self.completed_calls: list[llm_entities.ToolCall] = [] + + async def run(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message | llm_entities.MessageChunk, None]: """运行请求""" pending_tool_calls = [] req_messages = query.prompt.messages.copy() + query.messages.copy() + [query.user_message] - # 首次请求 - msg = await query.use_llm_model.requester.invoke_llm( - query, - query.use_llm_model, - req_messages, - query.use_funcs, - extra_args=query.use_llm_model.model_entity.extra_args, - ) + is_stream = query.adapter.is_stream_output_supported() + # while True: + # pass + if not is_stream: + # 非流式输出,直接请求 + msg = await query.use_llm_model.requester.invoke_llm( + query, + query.use_llm_model, + req_messages, + query.use_funcs, + extra_args=query.use_llm_model.model_entity.extra_args, + ) + yield msg + final_msg = msg + else: + # 流式输出,需要处理工具调用 + tool_calls_map: dict[str, llm_entities.ToolCall] = {} + async for msg in await query.use_llm_model.requester.invoke_llm( + query, + query.use_llm_model, + req_messages, + query.use_funcs, + stream=is_stream, + extra_args=query.use_llm_model.model_entity.extra_args, + ): + yield msg + if msg.tool_calls: + for tool_call in msg.tool_calls: + if tool_call.id not in tool_calls_map: + tool_calls_map[tool_call.id] = llm_entities.ToolCall( + id=tool_call.id, + type=tool_call.type, + function=llm_entities.FunctionCall( + name=tool_call.function.name if tool_call.function else '', + arguments='' + ), + ) + if tool_call.function and tool_call.function.arguments: + # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 + tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + final_msg = llm_entities.Message( + role=msg.role, + content=msg.all_content, + tool_calls=list(tool_calls_map.values()), + ) - yield msg + + pending_tool_calls = final_msg.tool_calls - pending_tool_calls = msg.tool_calls - - req_messages.append(msg) + req_messages.append(final_msg) # 持续请求,只要还有待处理的工具调用就继续处理调用 while pending_tool_calls: @@ -60,17 +103,49 @@ class LocalAgentRunner(runner.RequestRunner): req_messages.append(err_msg) - # 处理完所有调用,再次请求 - msg = await query.use_llm_model.requester.invoke_llm( - query, - query.use_llm_model, - req_messages, - query.use_funcs, - extra_args=query.use_llm_model.model_entity.extra_args, - ) + if is_stream: + tool_calls_map = {} + async for msg in await query.use_llm_model.requester.invoke_llm( + query, + query.use_llm_model, + req_messages, + query.use_funcs, + stream=is_stream, + extra_args=query.use_llm_model.model_entity.extra_args, + ): + yield msg + if msg.tool_calls: + for tool_call in msg.tool_calls: + if tool_call.id not in tool_calls_map: + tool_calls_map[tool_call.id] = llm_entities.ToolCall( + id=tool_call.id, + type=tool_call.type, + function=llm_entities.FunctionCall( + name=tool_call.function.name if tool_call.function else '', + arguments='' + ), + ) + if tool_call.function and tool_call.function.arguments: + # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 + tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + final_msg = llm_entities.Message( + role=msg.role, + content=all_content, + tool_calls=list(tool_calls_map.values()), + ) + else: + # 处理完所有调用,再次请求 + msg = await query.use_llm_model.requester.invoke_llm( + query, + query.use_llm_model, + req_messages, + query.use_funcs, + extra_args=query.use_llm_model.model_entity.extra_args, + ) - yield msg + yield msg + final_msg = msg - pending_tool_calls = msg.tool_calls + pending_tool_calls = final_msg.tool_calls - req_messages.append(msg) + req_messages.append(final_msg) From 48c9d66ab8151f7bf5ccdab6b4a6981e1b7b6600 Mon Sep 17 00:00:00 2001 From: fdc Date: Tue, 1 Jul 2025 18:03:05 +0800 Subject: [PATCH 002/107] =?UTF-8?q?chat=E4=B8=AD=E7=9A=84=E6=B5=81?= =?UTF-8?q?=E5=BC=8F=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/pipeline/process/handlers/chat.py | 23 +++++++++++------------ pkg/pipeline/respback/respback.py | 23 +++++++++-------------- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/pkg/pipeline/process/handlers/chat.py b/pkg/pipeline/process/handlers/chat.py index c90d283b..9b3e0cd5 100644 --- a/pkg/pipeline/process/handlers/chat.py +++ b/pkg/pipeline/process/handlers/chat.py @@ -70,15 +70,15 @@ class ChatMessageHandler(handler.MessageHandler): else: raise ValueError(f'未找到请求运行器: {query.pipeline_config["ai"]["runner"]["runner"]}') if is_stream: - accumulated_messages = [] - async for result in runner.run(query): - accumulated_messages.append(result) - query.resp_messages.append(result) + async for results in runner.run(query): + async for result in results: - self.ap.logger.info(f'对话({query.query_id})流式响应: {self.cut_str(result.readable_str())}') + query.resp_messages.append(result) - if result.content is not None: - text_length += len(result.content) + self.ap.logger.info(f'对话({query.query_id})流式响应: {self.cut_str(result.readable_str())}') + + if result.content is not None: + text_length += len(result.content) # current_chain = platform_message.MessageChain([]) # for msg in accumulated_messages: @@ -86,12 +86,11 @@ class ChatMessageHandler(handler.MessageHandler): # current_chain.append(platform_message.Plain(msg.content)) # query.resp_message_chain = [current_chain] - - - - + yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) + # query.resp_messages.append(results) + # self.ap.logger.info(f'对话({query.query_id})响应') + # yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) - yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) else: async for result in runner.run(query): diff --git a/pkg/pipeline/respback/respback.py b/pkg/pipeline/respback/respback.py index 7654896b..4ac4e1e3 100644 --- a/pkg/pipeline/respback/respback.py +++ b/pkg/pipeline/respback/respback.py @@ -7,6 +7,8 @@ import asyncio from ...platform.types import events as platform_events from ...platform.types import message as platform_message +from ...provider import entities as llm_entities + from .. import stage, entities from ...core import entities as core_entities @@ -38,17 +40,10 @@ class SendResponseBackStage(stage.PipelineStage): has_chunks = any(isinstance(msg, llm_entities.MessageChunk) for msg in query.resp_messages) if has_chunks and hasattr(query.adapter,'reply_message_chunk'): - - async def message_generator(): - for msg in query.resp_messages: - if isinstance(msg, llm_entities.MessageChunk): - yield msg.content - else: - yield msg.content await query.adapter.reply_message_chunk( message_source=query.message_event, - message_id=query.message_event.message_id, - message_generator=message_generator(), + message_id=query.query_id, + message_generator=query.resp_message_chain[-1], quote_origin=quote_origin, ) else: @@ -58,10 +53,10 @@ class SendResponseBackStage(stage.PipelineStage): quote_origin=quote_origin, ) - await query.adapter.reply_message( - message_source=query.message_event, - message=query.resp_message_chain[-1], - quote_origin=quote_origin, - ) + # await query.adapter.reply_message( + # message_source=query.message_event, + # message=query.resp_message_chain[-1], + # quote_origin=quote_origin, + # ) return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) From 8670ae82a35e47e95d74b5a5d1410bd80ebd936b Mon Sep 17 00:00:00 2001 From: fdc Date: Wed, 2 Jul 2025 10:49:50 +0800 Subject: [PATCH 003/107] =?UTF-8?q?fix:=E4=BF=AE=E6=94=B9=E6=89=8B?= =?UTF-8?q?=E8=AF=AFmessage=5Fid=E5=86=99=E8=BF=9Breply=5Fmessage=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/platform/adapter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/platform/adapter.py b/pkg/platform/adapter.py index c841ae98..18403b75 100644 --- a/pkg/platform/adapter.py +++ b/pkg/platform/adapter.py @@ -49,7 +49,6 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): async def reply_message( self, message_source: platform_events.MessageEvent, - message_id: int, message: platform_message.MessageChain, quote_origin: bool = False, ): @@ -57,7 +56,6 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): Args: message_source (platform.types.MessageEvent): 消息源事件 - message_id (int): 消息ID message (platform.types.MessageChain): 消息链 quote_origin (bool, optional): 是否引用原消息. Defaults to False. """ @@ -66,12 +64,14 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): async def reply_message_chunk( self, message_source: platform_events.MessageEvent, + message_id: int, message: platform_message.MessageChain, quote_origin: bool = False, ): """回复消息(流式输出) Args: message_source (platform.types.MessageEvent): 消息源事件 + message_id (int): 消息ID message (platform.types.MessageChain): 消息链 quote_origin (bool, optional): 是否引用原消息. Defaults to False. """ From 3c6e858c358786e7df666a9572263aa44afbcc1a Mon Sep 17 00:00:00 2001 From: zejiewang <511217265@qq.com> Date: Sun, 18 May 2025 12:03:01 +0800 Subject: [PATCH 004/107] feat:support dify message streaming output (#1437) * fix:lark adapter listeners init problem * feat:support dify streaming mode * feat:remove some log * fix(bot form): field desc missing * fix: not compatible with chatflow --------- Co-authored-by: wangzejie Co-authored-by: Junyan Qin --- pkg/platform/botmgr.py | 4 +- pkg/platform/sources/lark.py | 108 ++++++++++++++++++++++++++++ pkg/platform/sources/lark.yaml | 17 +++++ pkg/provider/runners/difysvapi.py | 41 +++++++++-- templates/metadata/pipeline/ai.yaml | 15 ++++ 5 files changed, 180 insertions(+), 5 deletions(-) diff --git a/pkg/platform/botmgr.py b/pkg/platform/botmgr.py index 5855525f..1da5eec8 100644 --- a/pkg/platform/botmgr.py +++ b/pkg/platform/botmgr.py @@ -120,8 +120,10 @@ class RuntimeBot: if isinstance(e, asyncio.CancelledError): self.task_context.set_current_action('Exited.') return + + traceback_str = traceback.format_exc() self.task_context.set_current_action('Exited with error.') - await self.logger.error(f'平台适配器运行出错:\n{e}\n{traceback.format_exc()}') + await self.logger.error(f'平台适配器运行出错:\n{e}\n{traceback_str}') self.task_wrapper = self.ap.task_mgr.create_task( exception_wrapper(), diff --git a/pkg/platform/sources/lark.py b/pkg/platform/sources/lark.py index d1116362..49ff53be 100644 --- a/pkg/platform/sources/lark.py +++ b/pkg/platform/sources/lark.py @@ -9,6 +9,7 @@ import re import base64 import uuid import json +import time import datetime import hashlib from Crypto.Cipher import AES @@ -320,6 +321,10 @@ class LarkEventConverter(adapter.EventConverter): ) +CARD_ID_CACHE_SIZE = 500 +CARD_ID_CACHE_MAX_LIFETIME = 20 * 60 # 20分钟 + + class LarkAdapter(adapter.MessagePlatformAdapter): bot: lark_oapi.ws.Client api_client: lark_oapi.Client @@ -338,6 +343,8 @@ class LarkAdapter(adapter.MessagePlatformAdapter): config: dict quart_app: quart.Quart ap: app.Application + + message_id_to_card_id: typing.Dict[str, typing.Tuple[str, int]] def __init__(self, config: dict, ap: app.Application, logger: EventLogger): self.config = config @@ -345,6 +352,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): self.logger = logger self.quart_app = quart.Quart(__name__) self.listeners = {} + self.message_id_to_card_id = {} @self.quart_app.route('/lark/callback', methods=['POST']) async def lark_callback(): @@ -390,6 +398,19 @@ class LarkAdapter(adapter.MessagePlatformAdapter): return {'code': 500, 'message': 'error'} async def on_message(event: lark_oapi.im.v1.P2ImMessageReceiveV1): + if self.config['enable-card-reply'] and event.event.message.message_id not in self.message_id_to_card_id: + self.ap.logger.debug('卡片回复模式开启') + # 开启卡片回复模式. 这里可以实现飞书一发消息,马上创建卡片进行回复"思考中..." + reply_message_id = await self.create_message_card(event.event.message.message_id) + self.message_id_to_card_id[event.event.message.message_id] = (reply_message_id, time.time()) + + if len(self.message_id_to_card_id) > CARD_ID_CACHE_SIZE: + self.message_id_to_card_id = { + k: v + for k, v in self.message_id_to_card_id.items() + if v[1] > time.time() - CARD_ID_CACHE_MAX_LIFETIME + } + lb_event = await self.event_converter.target2yiri(event, self.api_client) await self.listeners[type(lb_event)](lb_event, self) @@ -409,11 +430,93 @@ class LarkAdapter(adapter.MessagePlatformAdapter): async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): pass + async def create_message_card(self, message_id: str) -> str: + """ + 创建卡片消息。 + 使用卡片消息是因为普通消息更新次数有限制,而大模型流式返回结果可能很多而超过限制,而飞书卡片没有这个限制 + """ + + # TODO 目前只支持卡片模板方式,且卡片变量一定是content,未来这块要做成可配置 + # 发消息马上就会回复显示初始化的content信息,即思考中 + content = { + 'type': 'template', + 'data': {'template_id': self.config['card_template_id'], 'template_variable': {'content': 'Thinking...'}}, + } + request: ReplyMessageRequest = ( + ReplyMessageRequest.builder() + .message_id(message_id) + .request_body( + ReplyMessageRequestBody.builder().content(json.dumps(content)).msg_type('interactive').build() + ) + .build() + ) + + # 发起请求 + response: ReplyMessageResponse = await self.api_client.im.v1.message.areply(request) + + # 处理失败返回 + if not response.success(): + raise Exception( + f'client.im.v1.message.reply failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}' + ) + return response.data.message_id + async def reply_message( self, message_source: platform_events.MessageEvent, message: platform_message.MessageChain, quote_origin: bool = False, + ): + if self.config['enable-card-reply']: + await self.reply_card_message(message_source, message, quote_origin) + else: + await self.reply_normal_message(message_source, message, quote_origin) + + async def reply_card_message( + self, + message_source: platform_events.MessageEvent, + message: platform_message.MessageChain, + quote_origin: bool = False, + ): + """ + 回复消息变成更新卡片消息 + """ + lark_message = await self.message_converter.yiri2target(message, self.api_client) + + text_message = '' + for ele in lark_message[0]: + if ele['tag'] == 'text': + text_message += ele['text'] + elif ele['tag'] == 'md': + text_message += ele['text'] + + content = { + 'type': 'template', + 'data': {'template_id': self.config['card_template_id'], 'template_variable': {'content': text_message}}, + } + + request: PatchMessageRequest = ( + PatchMessageRequest.builder() + .message_id(self.message_id_to_card_id[message_source.message_chain.message_id][0]) + .request_body(PatchMessageRequestBody.builder().content(json.dumps(content)).build()) + .build() + ) + + # 发起请求 + response: PatchMessageResponse = self.api_client.im.v1.message.patch(request) + + # 处理失败返回 + if not response.success(): + raise Exception( + f'client.im.v1.message.patch failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}' + ) + return + + async def reply_normal_message( + self, + message_source: platform_events.MessageEvent, + message: platform_message.MessageChain, + quote_origin: bool = False, ): # 不再需要了,因为message_id已经被包含到message_chain中 # lark_event = await self.event_converter.yiri2target(message_source) @@ -492,4 +595,9 @@ class LarkAdapter(adapter.MessagePlatformAdapter): ) async def kill(self) -> bool: + # 需要断开连接,不然旧的连接会继续运行,导致飞书消息来时会随机选择一个连接 + # 断开时lark.ws.Client的_receive_message_loop会打印error日志: receive message loop exit。然后进行重连, + # 所以要设置_auto_reconnect=False,让其不重连。 + self.bot._auto_reconnect = False + await self.bot._disconnect() return False diff --git a/pkg/platform/sources/lark.yaml b/pkg/platform/sources/lark.yaml index f51bab76..bafaba81 100644 --- a/pkg/platform/sources/lark.yaml +++ b/pkg/platform/sources/lark.yaml @@ -65,6 +65,23 @@ spec: type: string required: true default: "" + - name: enable-card-reply + label: + en_US: Enable Card Reply Mode + zh_Hans: 启用飞书卡片回复模式 + description: + en_US: If enabled, the bot will use the card of lark reply mode + zh_Hans: 如果启用,将使用飞书卡片方式来回复内容 + type: boolean + required: true + default: false + - name: card_template_id + label: + en_US: card template id + zh_Hans: 卡片模板ID + type: string + required: true + default: "填写你的卡片template_id" execution: python: path: ./lark.py diff --git a/pkg/provider/runners/difysvapi.py b/pkg/provider/runners/difysvapi.py index b2542491..98b50f86 100644 --- a/pkg/provider/runners/difysvapi.py +++ b/pkg/provider/runners/difysvapi.py @@ -108,7 +108,13 @@ class DifyServiceAPIRunner(runner.RequestRunner): mode = 'basic' # 标记是基础编排还是工作流编排 - basic_mode_pending_chunk = '' + stream_output_pending_chunk = '' + + batch_pending_max_size = self.pipeline_config['ai']['dify-service-api'].get( + 'output-batch-size', 0 + ) # 积累一定量的消息更新消息一次 + + batch_pending_index = 0 inputs = {} @@ -126,6 +132,13 @@ class DifyServiceAPIRunner(runner.RequestRunner): ): self.ap.logger.debug('dify-chat-chunk: ' + str(chunk)) + # 查询异常情况 + if chunk['event'] == 'error': + yield llm_entities.Message( + role='assistant', + content=f"查询异常: [{chunk['code']}]. {chunk['message']}.\n请重试,如果还报错,请用 **!reset** 命令重置对话再尝试。", + ) + if chunk['event'] == 'workflow_started': mode = 'workflow' @@ -136,15 +149,35 @@ class DifyServiceAPIRunner(runner.RequestRunner): role='assistant', content=self._try_convert_thinking(chunk['data']['outputs']['answer']), ) + elif chunk['event'] == 'message': + stream_output_pending_chunk += chunk['answer'] + if self.pipeline_config['ai']['dify-service-api'].get('enable-streaming', False): + # 消息数超过量就输出,从而达到streaming的效果 + batch_pending_index += 1 + if batch_pending_index >= batch_pending_max_size: + yield llm_entities.Message( + role='assistant', + content=self._try_convert_thinking(stream_output_pending_chunk), + ) + batch_pending_index = 0 elif mode == 'basic': if chunk['event'] == 'message': - basic_mode_pending_chunk += chunk['answer'] + stream_output_pending_chunk += chunk['answer'] + if self.pipeline_config['ai']['dify-service-api'].get('enable-streaming', False): + # 消息数超过量就输出,从而达到streaming的效果 + batch_pending_index += 1 + if batch_pending_index >= batch_pending_max_size: + yield llm_entities.Message( + role='assistant', + content=self._try_convert_thinking(stream_output_pending_chunk), + ) + batch_pending_index = 0 elif chunk['event'] == 'message_end': yield llm_entities.Message( role='assistant', - content=self._try_convert_thinking(basic_mode_pending_chunk), + content=self._try_convert_thinking(stream_output_pending_chunk), ) - basic_mode_pending_chunk = '' + stream_output_pending_chunk = '' if chunk is None: raise errors.DifyAPIError('Dify API 没有返回任何响应,请检查网络连接和API配置') diff --git a/templates/metadata/pipeline/ai.yaml b/templates/metadata/pipeline/ai.yaml index 90732dc8..fb2672d4 100644 --- a/templates/metadata/pipeline/ai.yaml +++ b/templates/metadata/pipeline/ai.yaml @@ -128,6 +128,21 @@ stages: label: en_US: Remove zh_Hans: 移除 + - name: enable-streaming + label: + en_US: enable streaming mode + zh_Hans: 开启流式输出 + type: boolean + required: true + default: false + - name: output-batch-size + label: + en_US: output batch size + zh_Hans: 输出批次大小(积累多少条消息后一起输出) + type: integer + required: true + default: 10 + - name: dashscope-app-api label: en_US: Aliyun Dashscope App API From 4005a8a3e225fc99b5cadaef5d906972d3d7eb7e Mon Sep 17 00:00:00 2001 From: fdc Date: Thu, 3 Jul 2025 22:58:17 +0800 Subject: [PATCH 005/107] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=E9=A3=9E?= =?UTF-8?q?=E4=B9=A6=E4=B8=AD=E7=9A=84=E6=B5=81=E5=BC=8F=E4=BD=86=E6=98=AF?= =?UTF-8?q?=E5=A5=BD=E5=83=8F=E8=BF=98=E6=9C=89=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/platform/sources/lark.py | 142 ++++++++++++++++++++++++----------- 1 file changed, 100 insertions(+), 42 deletions(-) diff --git a/pkg/platform/sources/lark.py b/pkg/platform/sources/lark.py index 49ff53be..9c9b5605 100644 --- a/pkg/platform/sources/lark.py +++ b/pkg/platform/sources/lark.py @@ -346,6 +346,8 @@ class LarkAdapter(adapter.MessagePlatformAdapter): message_id_to_card_id: typing.Dict[str, typing.Tuple[str, int]] + card_id_dict: dict[str, str] + def __init__(self, config: dict, ap: app.Application, logger: EventLogger): self.config = config self.ap = ap @@ -353,6 +355,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): self.quart_app = quart.Quart(__name__) self.listeners = {} self.message_id_to_card_id = {} + self.card_id_dict = {} @self.quart_app.route('/lark/callback', methods=['POST']) async def lark_callback(): @@ -397,11 +400,69 @@ class LarkAdapter(adapter.MessagePlatformAdapter): await self.logger.error(f"Error in lark callback: {traceback.format_exc()}") return {'code': 500, 'message': 'error'} + + def is_stream_output_supported() -> bool: + is_stream = False + if self.config.get("",None): + is_stream = True + + return is_stream + + async def create_card_id(): + try: + is_stream = is_stream_output_supported() + if is_stream: + self.ap.logger.debug('飞书支持stream输出,创建卡片......') + + card_id = '' + if self.card_id_dict: + card_id = [k for k,v in self.card_id_dict.items() if (v+datetime.timedelta(days=14))< datetime.datetime.now()][0] + + if self.card_id_dict is None or card_id == '': + # content = { + # "type": "card_json", + # "data": {"schema":"2.0","header":{"title":{"content":"bot","tag":"plain_text"}},"body":{"elements":[{"tag":"markdown","content":""}]}} + # } + card_data = {"schema":"2.0","header":{"title":{"content":"bot","tag":"plain_text"}}, + "body":{"elements":[{"tag":"markdown","content":""}]},"config": {"streaming_mode": True, + "streaming_config": {"print_strategy": "fast"}}} + + request: CreateCardRequest = ( + CreateCardRequest.builder() + .request_body( + CreateCardRequestBody.builder() + .type("card_json") + .data(json.dumps(card_data)) + .build() + ) + ) + # 发起请求 + response: CreateCardResponse = await self.api_client.im.v1.card.create(request) + + + # 处理失败返回 + if not response.success(): + raise Exception( + f"client.cardkit.v1.card.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}") + + self.ap.logger.debug(f'飞书卡片创建成功,卡片ID: {response.data.card_id}') + self.card_id_dict[response.data.card_id] = datetime.datetime.now() + + card_id = response.data.card_id + return card_id + + except Exception as e: + self.ap.logger.error(f'飞书卡片创建失败,错误信息: {e}') + + + + async def on_message(event: lark_oapi.im.v1.P2ImMessageReceiveV1): - if self.config['enable-card-reply'] and event.event.message.message_id not in self.message_id_to_card_id: + if is_stream_output_supported(): self.ap.logger.debug('卡片回复模式开启') # 开启卡片回复模式. 这里可以实现飞书一发消息,马上创建卡片进行回复"思考中..." - reply_message_id = await self.create_message_card(event.event.message.message_id) + card_id = await create_card_id() + reply_message_id = await self.create_message_card(card_id, event.event.message.message_id) self.message_id_to_card_id[event.event.message.message_id] = (reply_message_id, time.time()) if len(self.message_id_to_card_id) > CARD_ID_CACHE_SIZE: @@ -430,7 +491,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): pass - async def create_message_card(self, message_id: str) -> str: + async def create_message_card(self, card_id: str, message_id: str) -> str: """ 创建卡片消息。 使用卡片消息是因为普通消息更新次数有限制,而大模型流式返回结果可能很多而超过限制,而飞书卡片没有这个限制 @@ -440,7 +501,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): # 发消息马上就会回复显示初始化的content信息,即思考中 content = { 'type': 'template', - 'data': {'template_id': self.config['card_template_id'], 'template_variable': {'content': 'Thinking...'}}, + 'data': {'template_id': card_id, 'template_variable': {'content': 'Thinking...'}}, } request: ReplyMessageRequest = ( ReplyMessageRequest.builder() @@ -467,12 +528,40 @@ class LarkAdapter(adapter.MessagePlatformAdapter): message: platform_message.MessageChain, quote_origin: bool = False, ): - if self.config['enable-card-reply']: - await self.reply_card_message(message_source, message, quote_origin) - else: - await self.reply_normal_message(message_source, message, quote_origin) + # 不再需要了,因为message_id已经被包含到message_chain中 + # lark_event = await self.event_converter.yiri2target(message_source) + lark_message = await self.message_converter.yiri2target(message, self.api_client) - async def reply_card_message( + final_content = { + 'zh_Hans': { + 'title': '', + 'content': lark_message, + }, + } + + request: ReplyMessageRequest = ( + ReplyMessageRequest.builder() + .message_id(message_source.message_chain.message_id) + .request_body( + ReplyMessageRequestBody.builder() + .content(json.dumps(final_content)) + .msg_type('post') + .reply_in_thread(False) + .uuid(str(uuid.uuid4())) + .build() + ) + .build() + ) + + response: ReplyMessageResponse = await self.api_client.im.v1.message.areply(request) + + if not response.success(): + raise Exception( + f'client.im.v1.message.reply failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}' + ) + + + async def reply_message_chunk( self, message_source: platform_events.MessageEvent, message: platform_message.MessageChain, @@ -512,43 +601,12 @@ class LarkAdapter(adapter.MessagePlatformAdapter): ) return - async def reply_normal_message( - self, - message_source: platform_events.MessageEvent, - message: platform_message.MessageChain, - quote_origin: bool = False, - ): - # 不再需要了,因为message_id已经被包含到message_chain中 - # lark_event = await self.event_converter.yiri2target(message_source) - lark_message = await self.message_converter.yiri2target(message, self.api_client) - final_content = { - 'zh_Hans': { - 'title': '', - 'content': lark_message, - }, - } - request: ReplyMessageRequest = ( - ReplyMessageRequest.builder() - .message_id(message_source.message_chain.message_id) - .request_body( - ReplyMessageRequestBody.builder() - .content(json.dumps(final_content)) - .msg_type('post') - .reply_in_thread(False) - .uuid(str(uuid.uuid4())) - .build() - ) - .build() - ) - response: ReplyMessageResponse = await self.api_client.im.v1.message.areply(request) - if not response.success(): - raise Exception( - f'client.im.v1.message.reply failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}' - ) + + async def is_muted(self, group_id: int) -> bool: return False From 68cdd163d30f02e03fdade10feb6006bf7128d31 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Fri, 4 Jul 2025 03:26:44 +0800 Subject: [PATCH 006/107] =?UTF-8?q?=E6=B5=81=E5=BC=8F=E5=9F=BA=E6=9C=AC?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=E5=B7=B2=E9=80=9A=E8=BF=87=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E4=BA=86yield=E5=92=8Creturn=E7=9A=84=E5=86=B2=E7=AA=81?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/pipeline/process/handlers/chat.py | 50 ++++-- pkg/pipeline/respback/respback.py | 9 +- pkg/platform/adapter.py | 4 + pkg/platform/sources/lark.py | 135 ++++++++++----- pkg/provider/entities.py | 4 +- pkg/provider/modelmgr/requester.py | 27 ++- pkg/provider/modelmgr/requesters/chatcmpl.py | 167 +++++++++++++++---- pkg/provider/runners/localagent.py | 44 ++--- 8 files changed, 323 insertions(+), 117 deletions(-) diff --git a/pkg/pipeline/process/handlers/chat.py b/pkg/pipeline/process/handlers/chat.py index 9b3e0cd5..3a5925cc 100644 --- a/pkg/pipeline/process/handlers/chat.py +++ b/pkg/pipeline/process/handlers/chat.py @@ -59,8 +59,11 @@ class ChatMessageHandler(handler.MessageHandler): query.user_message.content = event_ctx.event.alter text_length = 0 - - is_stream = query.adapter.is_stream_output_supported() + try: + is_stream = query.adapter.is_stream + except AttributeError: + is_stream = False + print(is_stream) try: for r in runner_module.preregistered_runners: @@ -70,31 +73,44 @@ class ChatMessageHandler(handler.MessageHandler): else: raise ValueError(f'未找到请求运行器: {query.pipeline_config["ai"]["runner"]["runner"]}') if is_stream: - async for results in runner.run(query): - async for result in results: + # async for results in runner.run(query): + async for result in runner.run(query): + print(result) + query.resp_messages.append(result) + print(result) - query.resp_messages.append(result) + self.ap.logger.info(f'对话({query.query_id})响应: {self.cut_str(result.readable_str())}') - self.ap.logger.info(f'对话({query.query_id})流式响应: {self.cut_str(result.readable_str())}') + if result.content is not None: + text_length += len(result.content) - if result.content is not None: - text_length += len(result.content) - - # current_chain = platform_message.MessageChain([]) - # for msg in accumulated_messages: - # if msg.content is not None: - # current_chain.append(platform_message.Plain(msg.content)) - # query.resp_message_chain = [current_chain] - - yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) + yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) + # for result in results: + # + # query.resp_messages.append(result) + # print(result) + # + # self.ap.logger.info(f'对话({query.query_id})流式响应: {self.cut_str(result.content)}') + # + # if result.content is not None: + # text_length += len(result.content) + # + # # current_chain = platform_message.MessageChain([]) + # # for msg in accumulated_messages: + # # if msg.content is not None: + # # current_chain.append(platform_message.Plain(msg.content)) + # # query.resp_message_chain = [current_chain] + # + # yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) # query.resp_messages.append(results) # self.ap.logger.info(f'对话({query.query_id})响应') # yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) else: - + print("非流式") async for result in runner.run(query): query.resp_messages.append(result) + print(result) self.ap.logger.info(f'对话({query.query_id})响应: {self.cut_str(result.readable_str())}') diff --git a/pkg/pipeline/respback/respback.py b/pkg/pipeline/respback/respback.py index 4ac4e1e3..52714ce2 100644 --- a/pkg/pipeline/respback/respback.py +++ b/pkg/pipeline/respback/respback.py @@ -3,6 +3,7 @@ from __future__ import annotations import random import asyncio +from typing_inspection.typing_objects import is_final from ...platform.types import events as platform_events from ...platform.types import message as platform_message @@ -39,12 +40,16 @@ class SendResponseBackStage(stage.PipelineStage): quote_origin = query.pipeline_config['output']['misc']['quote-origin'] has_chunks = any(isinstance(msg, llm_entities.MessageChunk) for msg in query.resp_messages) + print(has_chunks) if has_chunks and hasattr(query.adapter,'reply_message_chunk'): + is_final = [msg.is_final for msg in query.resp_messages][0] + print(is_final) await query.adapter.reply_message_chunk( message_source=query.message_event, - message_id=query.query_id, - message_generator=query.resp_message_chain[-1], + message_id=query.message_event.message_chain.message_id, + message=query.resp_message_chain[-1], quote_origin=quote_origin, + is_final=is_final, ) else: await query.adapter.reply_message( diff --git a/pkg/platform/adapter.py b/pkg/platform/adapter.py index 18403b75..3951326c 100644 --- a/pkg/platform/adapter.py +++ b/pkg/platform/adapter.py @@ -25,6 +25,8 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): logger: EventLogger + is_stream: bool + def __init__(self, config: dict, ap: app.Application, logger: EventLogger): """初始化适配器 @@ -67,6 +69,7 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): message_id: int, message: platform_message.MessageChain, quote_origin: bool = False, + is_final: bool = False, ): """回复消息(流式输出) Args: @@ -114,6 +117,7 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): async def is_stream_output_supported(self) -> bool: """是否支持流式输出""" + self.is_stream = False return False async def kill(self) -> bool: diff --git a/pkg/platform/sources/lark.py b/pkg/platform/sources/lark.py index 9c9b5605..af57d66c 100644 --- a/pkg/platform/sources/lark.py +++ b/pkg/platform/sources/lark.py @@ -18,6 +18,7 @@ import aiohttp import lark_oapi.ws.exception import quart from lark_oapi.api.im.v1 import * +from lark_oapi.api.cardkit.v1 import * from .. import adapter from ...core import app @@ -348,6 +349,8 @@ class LarkAdapter(adapter.MessagePlatformAdapter): card_id_dict: dict[str, str] + seq: int + def __init__(self, config: dict, ap: app.Application, logger: EventLogger): self.config = config self.ap = ap @@ -356,6 +359,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): self.listeners = {} self.message_id_to_card_id = {} self.card_id_dict = {} + self.seq = 0 @self.quart_app.route('/lark/callback', methods=['POST']) async def lark_callback(): @@ -401,54 +405,79 @@ class LarkAdapter(adapter.MessagePlatformAdapter): return {'code': 500, 'message': 'error'} - def is_stream_output_supported() -> bool: + async def is_stream_output_supported() -> bool: is_stream = False - if self.config.get("",None): + if self.config.get("enable-card-reply",None): is_stream = True + self.is_stream = is_stream return is_stream - async def create_card_id(): + async def create_card_id(message_id): try: - is_stream = is_stream_output_supported() + is_stream = await is_stream_output_supported() if is_stream: self.ap.logger.debug('飞书支持stream输出,创建卡片......') - card_id = '' - if self.card_id_dict: - card_id = [k for k,v in self.card_id_dict.items() if (v+datetime.timedelta(days=14))< datetime.datetime.now()][0] + # card_id = '' + # # if self.card_id_dict: + # # card_id = [k for k,v in self.card_id_dict.items() if (v+datetime.timedelta(days=14))< datetime.datetime.now()][0] + # + # if self.card_id_dict is None: + # # content = { + # # "type": "card_json", + # # "data": {"schema":"2.0","header":{"title":{"content":"bot","tag":"plain_text"}},"body":{"elements":[{"tag":"markdown","content":""}]}} + # # } + # card_data = {"schema":"2.0","header":{"title":{"content":"bot","tag":"plain_text"}}, + # "body":{"elements":[{"tag":"markdown","content":""}]},"config": {"streaming_mode": True, + # "streaming_config": {"print_strategy": "fast"}}} + # + # request: CreateCardRequest = CreateCardRequest.builder() \ + # .request_body( + # CreateCardRequestBody.builder() + # .type("card_json") + # .data(json.dumps(card_data)) \ + # .build() + # ).build() + # + # # 发起请求 + # response: CreateCardResponse = self.api_client.cardkit.v1.card.create(request) + # + # + # # 处理失败返回 + # if not response.success(): + # raise Exception( + # f"client.cardkit.v1.card.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}") + # + # self.ap.logger.debug(f'飞书卡片创建成功,卡片ID: {response.data.card_id}') + # self.card_id_dict[response.data.card_id] = datetime.datetime.now() + # + # card_id = response.data.card_id + card_data = {"schema": "2.0", "header": {"title": {"content": "bot", "tag": "plain_text"}}, + "body": {"elements": [{"tag": "markdown", "content": "[思考中.....]","element_id":"markdown_1"}]}, + "config": {"streaming_mode": True, + "streaming_config": {"print_strategy": "fast"}}} - if self.card_id_dict is None or card_id == '': - # content = { - # "type": "card_json", - # "data": {"schema":"2.0","header":{"title":{"content":"bot","tag":"plain_text"}},"body":{"elements":[{"tag":"markdown","content":""}]}} - # } - card_data = {"schema":"2.0","header":{"title":{"content":"bot","tag":"plain_text"}}, - "body":{"elements":[{"tag":"markdown","content":""}]},"config": {"streaming_mode": True, - "streaming_config": {"print_strategy": "fast"}}} + request: CreateCardRequest = CreateCardRequest.builder() \ + .request_body( + CreateCardRequestBody.builder() + .type("card_json") + .data(json.dumps(card_data)) \ + .build() + ).build() - request: CreateCardRequest = ( - CreateCardRequest.builder() - .request_body( - CreateCardRequestBody.builder() - .type("card_json") - .data(json.dumps(card_data)) - .build() - ) - ) - # 发起请求 - response: CreateCardResponse = await self.api_client.im.v1.card.create(request) + # 发起请求 + response: CreateCardResponse = self.api_client.cardkit.v1.card.create(request) + # 处理失败返回 + if not response.success(): + raise Exception( + f"client.cardkit.v1.card.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}") - # 处理失败返回 - if not response.success(): - raise Exception( - f"client.cardkit.v1.card.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}") + self.ap.logger.debug(f'飞书卡片创建成功,卡片ID: {response.data.card_id}') + self.card_id_dict[message_id] = response.data.card_id - self.ap.logger.debug(f'飞书卡片创建成功,卡片ID: {response.data.card_id}') - self.card_id_dict[response.data.card_id] = datetime.datetime.now() - - card_id = response.data.card_id + card_id = response.data.card_id return card_id except Exception as e: @@ -458,10 +487,10 @@ class LarkAdapter(adapter.MessagePlatformAdapter): async def on_message(event: lark_oapi.im.v1.P2ImMessageReceiveV1): - if is_stream_output_supported(): + if await is_stream_output_supported(): self.ap.logger.debug('卡片回复模式开启') # 开启卡片回复模式. 这里可以实现飞书一发消息,马上创建卡片进行回复"思考中..." - card_id = await create_card_id() + card_id = await create_card_id(event.event.message.message_id) reply_message_id = await self.create_message_card(card_id, event.event.message.message_id) self.message_id_to_card_id[event.event.message.message_id] = (reply_message_id, time.time()) @@ -500,8 +529,8 @@ class LarkAdapter(adapter.MessagePlatformAdapter): # TODO 目前只支持卡片模板方式,且卡片变量一定是content,未来这块要做成可配置 # 发消息马上就会回复显示初始化的content信息,即思考中 content = { - 'type': 'template', - 'data': {'template_id': card_id, 'template_variable': {'content': 'Thinking...'}}, + 'type': 'card', + 'data': {'card_id': card_id, 'template_variable': {'content': 'Thinking...'}}, } request: ReplyMessageRequest = ( ReplyMessageRequest.builder() @@ -564,35 +593,49 @@ class LarkAdapter(adapter.MessagePlatformAdapter): async def reply_message_chunk( self, message_source: platform_events.MessageEvent, + message_id: str, message: platform_message.MessageChain, quote_origin: bool = False, + is_final: bool = False, ): """ 回复消息变成更新卡片消息 """ lark_message = await self.message_converter.yiri2target(message, self.api_client) + if not is_final: + self.seq += 1 + + + text_message = '' for ele in lark_message[0]: if ele['tag'] == 'text': text_message += ele['text'] elif ele['tag'] == 'md': text_message += ele['text'] + print(text_message) content = { - 'type': 'template', - 'data': {'template_id': self.config['card_template_id'], 'template_variable': {'content': text_message}}, + 'type': 'card_json', + 'data': {'card_id': self.card_id_dict[message_id], 'elements': {'content': text_message}}, } - request: PatchMessageRequest = ( - PatchMessageRequest.builder() - .message_id(self.message_id_to_card_id[message_source.message_chain.message_id][0]) - .request_body(PatchMessageRequestBody.builder().content(json.dumps(content)).build()) + request: ContentCardElementRequest = ContentCardElementRequest.builder() \ + .card_id(self.card_id_dict[message_id]) \ + .element_id("markdown_1") \ + .request_body(ContentCardElementRequestBody.builder() + # .uuid("a0d69e20-1dd1-458b-k525-dfeca4015204") + .content(text_message) + .sequence(self.seq) + .build()) \ .build() - ) + if is_final: + self.seq = 0 # 发起请求 - response: PatchMessageResponse = self.api_client.im.v1.message.patch(request) + response: ContentCardElementResponse = self.api_client.cardkit.v1.card_element.content(request) + # 处理失败返回 if not response.success(): diff --git a/pkg/provider/entities.py b/pkg/provider/entities.py index a149fea3..e8037e68 100644 --- a/pkg/provider/entities.py +++ b/pkg/provider/entities.py @@ -140,12 +140,12 @@ class MessageChunk(pydantic.BaseModel): content: typing.Optional[list[ContentElement]] | typing.Optional[str] = None """内容""" - # tool_calls: typing.Optional[list[ToolCall]] = None + tool_calls: typing.Optional[list[ToolCall]] = None """工具调用""" tool_call_id: typing.Optional[str] = None - tool_calls: typing.Optional[list[ToolCallChunk]] = None + # tool_calls: typing.Optional[list[ToolCallChunk]] = None is_final: bool = False diff --git a/pkg/provider/modelmgr/requester.py b/pkg/provider/modelmgr/requester.py index 3e5e791f..49a28f56 100644 --- a/pkg/provider/modelmgr/requester.py +++ b/pkg/provider/modelmgr/requester.py @@ -62,7 +62,7 @@ class LLMAPIRequester(metaclass=abc.ABCMeta): funcs: typing.List[tools_entities.LLMFunction] = None, stream: bool = False, extra_args: dict[str, typing.Any] = {}, - ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: + ) -> llm_entities.Message: """调用API Args: @@ -72,6 +72,29 @@ class LLMAPIRequester(metaclass=abc.ABCMeta): extra_args (dict[str, typing.Any], optional): 额外的参数. Defaults to {}. Returns: - llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: 返回消息对象 + llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk]: 返回消息对象 + """ + pass + + @abc.abstractmethod + async def invoke_llm_stream( + self, + query: core_entities.Query, + model: RuntimeLLMModel, + messages: typing.List[llm_entities.Message], + funcs: typing.List[tools_entities.LLMFunction] = None, + stream: bool = False, + extra_args: dict[str, typing.Any] = {}, + ) -> llm_entities.MessageChunk: + """调用API + + Args: + model (RuntimeLLMModel): 使用的模型信息 + messages (typing.List[llm_entities.Message]): 消息对象列表 + funcs (typing.List[tools_entities.LLMFunction], optional): 使用的工具函数列表. Defaults to None. + extra_args (dict[str, typing.Any], optional): 额外的参数. Defaults to {}. + + Returns: + llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk]: 返回消息对象 """ pass diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index 22931611..f06041fc 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -38,6 +38,15 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): ) -> chat_completion.ChatCompletion: return await self.client.chat.completions.create(**args, extra_body=extra_body) + async def _req_stream( + self, + args: dict, + extra_body: dict = {}, + ) -> chat_completion.ChatCompletion: + + async for chunk in await self.client.chat.completions.create(**args, extra_body=extra_body): + yield chunk + async def _make_msg( self, chat_completion: chat_completion.ChatCompletion, @@ -62,9 +71,19 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): self, chat_completion: chat_completion.ChatCompletion, ) -> llm_entities.MessageChunk: - choice = chat_completion.choices[0] - delta = choice.delta.model_dump() + + # 处理流式chunk和完整响应的差异 + # print(chat_completion.choices[0]) + if hasattr(chat_completion, 'choices'): + # 完整响应模式 + choice = chat_completion.choices[0] + delta = choice.delta.model_dump() if hasattr(choice, 'delta') else choice.message.model_dump() + else: + # 流式chunk模式 + delta = chat_completion.delta.model_dump() if hasattr(chat_completion, 'delta') else {} + # 确保 role 字段存在且不为 None + # print(delta) if 'role' not in delta or delta['role'] is None: delta['role'] = 'assistant' @@ -78,8 +97,8 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): message = llm_entities.MessageChunk(**delta) return message - - async def _closure( + + async def _closure_stream( self, query: core_entities.Query, req_messages: list[dict], @@ -87,7 +106,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): use_funcs: list[tools_entities.LLMFunction] = None, stream: bool = False, extra_args: dict[str, typing.Any] = {}, - ) -> llm_entities.Message: + ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: self.client.api_key = use_model.token_mgr.get_token() args = {} @@ -115,36 +134,76 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): if stream: current_content = '' - async for chunk in await self._req(args, extra_body=extra_args): + args["stream"] = True + async for chunk in self._req_stream(args, extra_body=extra_args): + # print(chunk) # 处理流式消息 - delta_message = await self._make_msg_chunk( - chat_completion=chunk, - ) + delta_message = await self._make_msg_chunk(chunk) if delta_message.content: current_content += delta_message.content + delta_message.content = current_content + print(current_content) delta_message.all_content = current_content - - # 检查是否为最后一个块 - if chunk.choices[0].finish_reason is not None: + + # # 检查是否为最后一个块 + # if chunk.finish_reason is not None: + # delta_message.is_final = True + # + # yield delta_message + # 检查结束标志 + chunk_choices = getattr(chunk, 'choices', None) + if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): delta_message.is_final = True - yield delta_message - return - - else: + yield delta_message + # return - # 非流式请求 - resp = await self._req(args, extra_body=extra_args) - # 处理请求结果 - # 发送请求 - resp = await self._req(args, extra_body=extra_args) + + async def _closure( + self, + query: core_entities.Query, + req_messages: list[dict], + use_model: requester.RuntimeLLMModel, + use_funcs: list[tools_entities.LLMFunction] = None, + stream: bool = False, + extra_args: dict[str, typing.Any] = {}, + ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: + self.client.api_key = use_model.token_mgr.get_token() - # 处理请求结果 - message = await self._make_msg(resp) + args = {} + args['model'] = use_model.model_entity.name - return message - + if use_funcs: + tools = await self.ap.tool_mgr.generate_tools_for_openai(use_funcs) + + if tools: + args['tools'] = tools + + # 设置此次请求中的messages + messages = req_messages.copy() + + # 检查vision + for msg in messages: + if 'content' in msg and isinstance(msg['content'], list): + for me in msg['content']: + if me['type'] == 'image_base64': + me['image_url'] = {'url': me['image_base64']} + me['type'] = 'image_url' + del me['image_base64'] + + args['messages'] = messages + + + + # 发送请求 + + resp = await self._req(args, extra_body=extra_args) + # 处理请求结果 + message = await self._make_msg(resp) + + + return message @@ -171,8 +230,9 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): req_messages.append(msg_dict) try: + if stream: - async for item in self._closure( + async for item in self._closure_stream( query=query, req_messages=req_messages, use_model=model, @@ -180,16 +240,17 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): stream=stream, extra_args=extra_args, ): - yield item - return + return item else: - return await self._closure( + print(req_messages) + msg = await self._closure( query=query, req_messages=req_messages, use_model=model, use_funcs=funcs, extra_args=extra_args, ) + return msg except asyncio.TimeoutError: raise errors.RequesterError('请求超时') except openai.BadRequestError as e: @@ -205,3 +266,51 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): raise errors.RequesterError(f'请求过于频繁或余额不足: {e.message}') except openai.APIError as e: raise errors.RequesterError(f'请求错误: {e.message}') + + async def invoke_llm_stream( + self, + query: core_entities.Query, + model: requester.RuntimeLLMModel, + messages: typing.List[llm_entities.Message], + funcs: typing.List[tools_entities.LLMFunction] = None, + stream: bool = False, + extra_args: dict[str, typing.Any] = {}, + ) -> llm_entities.MessageChunk: + req_messages = [] # req_messages 仅用于类内,外部同步由 query.messages 进行 + for m in messages: + msg_dict = m.dict(exclude_none=True) + content = msg_dict.get('content') + if isinstance(content, list): + # 检查 content 列表中是否每个部分都是文本 + if all(isinstance(part, dict) and part.get('type') == 'text' for part in content): + # 将所有文本部分合并为一个字符串 + msg_dict['content'] = '\n'.join(part['text'] for part in content) + req_messages.append(msg_dict) + + try: + if stream: + async for item in self._closure_stream( + query=query, + req_messages=req_messages, + use_model=model, + use_funcs=funcs, + stream=stream, + extra_args=extra_args, + ): + yield item + + except asyncio.TimeoutError: + raise errors.RequesterError('请求超时') + except openai.BadRequestError as e: + if 'context_length_exceeded' in e.message: + raise errors.RequesterError(f'上文过长,请重置会话: {e.message}') + else: + raise errors.RequesterError(f'请求参数错误: {e.message}') + except openai.AuthenticationError as e: + raise errors.RequesterError(f'无效的 api-key: {e.message}') + except openai.NotFoundError as e: + raise errors.RequesterError(f'请求路径错误: {e.message}') + except openai.RateLimitError as e: + raise errors.RequesterError(f'请求过于频繁或余额不足: {e.message}') + except openai.APIError as e: + raise errors.RequesterError(f'请求错误: {e.message}') \ No newline at end of file diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index 02b2db16..da97e334 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -24,25 +24,30 @@ class LocalAgentRunner(runner.RequestRunner): pending_tool_calls = [] req_messages = query.prompt.messages.copy() + query.messages.copy() + [query.user_message] - - is_stream = query.adapter.is_stream_output_supported() + try: + is_stream = query.adapter.is_stream + except AttributeError: + is_stream = False # while True: # pass if not is_stream: # 非流式输出,直接请求 + # print(123) msg = await query.use_llm_model.requester.invoke_llm( query, query.use_llm_model, req_messages, query.use_funcs, + is_stream, extra_args=query.use_llm_model.model_entity.extra_args, ) yield msg final_msg = msg + print(final_msg) else: # 流式输出,需要处理工具调用 tool_calls_map: dict[str, llm_entities.ToolCall] = {} - async for msg in await query.use_llm_model.requester.invoke_llm( + async for msg in query.use_llm_model.requester.invoke_llm_stream( query, query.use_llm_model, req_messages, @@ -51,20 +56,20 @@ class LocalAgentRunner(runner.RequestRunner): extra_args=query.use_llm_model.model_entity.extra_args, ): yield msg - if msg.tool_calls: - for tool_call in msg.tool_calls: - if tool_call.id not in tool_calls_map: - tool_calls_map[tool_call.id] = llm_entities.ToolCall( - id=tool_call.id, - type=tool_call.type, - function=llm_entities.FunctionCall( - name=tool_call.function.name if tool_call.function else '', - arguments='' - ), - ) - if tool_call.function and tool_call.function.arguments: - # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 - tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + # if msg.tool_calls: + # for tool_call in msg.tool_calls: + # if tool_call.id not in tool_calls_map: + # tool_calls_map[tool_call.id] = llm_entities.ToolCall( + # id=tool_call.id, + # type=tool_call.type, + # function=llm_entities.FunctionCall( + # name=tool_call.function.name if tool_call.function else '', + # arguments='' + # ), + # ) + # if tool_call.function and tool_call.function.arguments: + # # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 + # tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments final_msg = llm_entities.Message( role=msg.role, content=msg.all_content, @@ -105,7 +110,7 @@ class LocalAgentRunner(runner.RequestRunner): if is_stream: tool_calls_map = {} - async for msg in await query.use_llm_model.requester.invoke_llm( + async for msg in await query.use_llm_model.requester.invoke_llm_stream( query, query.use_llm_model, req_messages, @@ -130,10 +135,11 @@ class LocalAgentRunner(runner.RequestRunner): tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments final_msg = llm_entities.Message( role=msg.role, - content=all_content, + content=msg.all_content, tool_calls=list(tool_calls_map.values()), ) else: + print("非流式") # 处理完所有调用,再次请求 msg = await query.use_llm_model.requester.invoke_llm( query, From 301509b1dbb73243c393b394057254e19dc07b58 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Sat, 12 Jul 2025 18:09:24 +0800 Subject: [PATCH 007/107] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E4=BA=86?= =?UTF-8?q?=E5=9B=A0=E4=B8=BA=E8=BF=AD=E4=BB=A3=E6=95=B0=E6=8D=AE=E5=8F=AA?= =?UTF-8?q?=E6=8E=A8=E5=85=A5resq=5Fmessages=E5=92=8Cresq=5Fmessage=5Fchai?= =?UTF-8?q?n=E5=AF=BC=E8=87=B4=E7=BC=93=E5=AD=98=E5=88=B0=E5=86=85?= =?UTF-8?q?=E5=AD=98=E4=B8=AD=E7=9A=84=E6=95=B0=E6=8D=AE=E5=92=8C=E5=86=99?= =?UTF-8?q?=E5=85=A5log=E4=B8=AD=E7=9A=84=E6=95=B0=E6=8D=AE=E9=87=8F?= =?UTF-8?q?=E5=BA=9E=E5=A4=A7=EF=BC=8C=E4=BB=A5=E5=8F=8A=E5=B8=A6=E6=9C=89?= =?UTF-8?q?=E6=B7=B1=E5=BA=A6=E6=80=9D=E8=80=83=E6=A8=A1=E5=9E=8B=E7=9A=84?= =?UTF-8?q?think=E5=A2=9E=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/pipeline/process/handlers/chat.py | 6 +- pkg/provider/modelmgr/requester.py | 1 - pkg/provider/modelmgr/requesters/chatcmpl.py | 89 +++++++++----------- pkg/provider/runners/localagent.py | 1 - 4 files changed, 47 insertions(+), 50 deletions(-) diff --git a/pkg/pipeline/process/handlers/chat.py b/pkg/pipeline/process/handlers/chat.py index 3a5925cc..c7b6ab85 100644 --- a/pkg/pipeline/process/handlers/chat.py +++ b/pkg/pipeline/process/handlers/chat.py @@ -75,7 +75,11 @@ class ChatMessageHandler(handler.MessageHandler): if is_stream: # async for results in runner.run(query): async for result in runner.run(query): - print(result) + if query.resp_messages: + query.resp_messages.pop() + if query.resp_message_chain: + query.resp_message_chain.pop() + query.resp_messages.append(result) print(result) diff --git a/pkg/provider/modelmgr/requester.py b/pkg/provider/modelmgr/requester.py index 49a28f56..7830e522 100644 --- a/pkg/provider/modelmgr/requester.py +++ b/pkg/provider/modelmgr/requester.py @@ -60,7 +60,6 @@ class LLMAPIRequester(metaclass=abc.ABCMeta): model: RuntimeLLMModel, messages: typing.List[llm_entities.Message], funcs: typing.List[tools_entities.LLMFunction] = None, - stream: bool = False, extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message: """调用API diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index f06041fc..5c303089 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -17,12 +17,15 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): """OpenAI ChatCompletion API 请求器""" client: openai.AsyncClient + is_content:bool default_config: dict[str, typing.Any] = { 'base_url': 'https://api.openai.com/v1', 'timeout': 120, } + + async def initialize(self): self.client = openai.AsyncClient( api_key='', @@ -30,6 +33,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): timeout=self.requester_cfg['timeout'], http_client=httpx.AsyncClient(trust_env=True, timeout=self.requester_cfg['timeout']), ) + self.is_content = False async def _req( self, @@ -69,6 +73,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): async def _make_msg_chunk( self, + index:int, chat_completion: chat_completion.ChatCompletion, ) -> llm_entities.MessageChunk: @@ -83,7 +88,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): delta = chat_completion.delta.model_dump() if hasattr(chat_completion, 'delta') else {} # 确保 role 字段存在且不为 None - # print(delta) + # print(delta.values()) if 'role' not in delta or delta['role'] is None: delta['role'] = 'assistant' @@ -91,8 +96,17 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None # deepseek的reasoner模型 - if reasoning_content is not None: - delta['content'] = '\n' + reasoning_content + '\n\n' + delta['content'] + if reasoning_content is not None and index == 0: + delta['content'] += f'\n{reasoning_content}' + elif reasoning_content is None: + if self.is_content: + delta['content'] = delta['content'] + else: + delta['content'] = f'\n\n\n{delta["content"]}' + self.is_content = True + else: + delta['content'] += reasoning_content + message = llm_entities.MessageChunk(**delta) @@ -135,23 +149,17 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): if stream: current_content = '' args["stream"] = True + chunk_idx = 0 + self.is_content = False async for chunk in self._req_stream(args, extra_body=extra_args): - # print(chunk) - # 处理流式消息 - delta_message = await self._make_msg_chunk(chunk) + delta_message = await self._make_msg_chunk(chunk_idx,chunk) + # print(delta_message) if delta_message.content: current_content += delta_message.content delta_message.content = current_content - print(current_content) - delta_message.all_content = current_content - - # # 检查是否为最后一个块 - # if chunk.finish_reason is not None: - # delta_message.is_final = True - # - # yield delta_message - # 检查结束标志 + # delta_message.all_content = current_content + chunk_idx += 1 chunk_choices = getattr(chunk, 'choices', None) if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): delta_message.is_final = True @@ -215,9 +223,8 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): model: requester.RuntimeLLMModel, messages: typing.List[llm_entities.Message], funcs: typing.List[tools_entities.LLMFunction] = None, - stream: bool = False, extra_args: dict[str, typing.Any] = {}, - ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: + ) -> llm_entities.Message: req_messages = [] # req_messages 仅用于类内,外部同步由 query.messages 进行 for m in messages: msg_dict = m.dict(exclude_none=True) @@ -231,26 +238,14 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): try: - if stream: - async for item in self._closure_stream( - query=query, - req_messages=req_messages, - use_model=model, - use_funcs=funcs, - stream=stream, - extra_args=extra_args, - ): - return item - else: - print(req_messages) - msg = await self._closure( - query=query, - req_messages=req_messages, - use_model=model, - use_funcs=funcs, - extra_args=extra_args, - ) - return msg + msg = await self._closure( + query=query, + req_messages=req_messages, + use_model=model, + use_funcs=funcs, + extra_args=extra_args, + ) + return msg except asyncio.TimeoutError: raise errors.RequesterError('请求超时') except openai.BadRequestError as e: @@ -288,16 +283,16 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): req_messages.append(msg_dict) try: - if stream: - async for item in self._closure_stream( - query=query, - req_messages=req_messages, - use_model=model, - use_funcs=funcs, - stream=stream, - extra_args=extra_args, - ): - yield item + async for item in self._closure_stream( + query=query, + req_messages=req_messages, + use_model=model, + use_funcs=funcs, + stream=stream, + extra_args=extra_args, + ): + yield item + print(item) except asyncio.TimeoutError: raise errors.RequesterError('请求超时') diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index da97e334..06b5d772 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -38,7 +38,6 @@ class LocalAgentRunner(runner.RequestRunner): query.use_llm_model, req_messages, query.use_funcs, - is_stream, extra_args=query.use_llm_model.model_entity.extra_args, ) yield msg From 0be08d888225ff1e00f20168a5ecc23442633ab5 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Sun, 13 Jul 2025 22:41:39 +0800 Subject: [PATCH 008/107] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E4=BA=86?= =?UTF-8?q?=E5=9B=A0=E4=B8=BA=E8=BF=AD=E4=BB=A3=E6=95=B0=E6=8D=AE=E5=8F=AA?= =?UTF-8?q?=E6=8E=A8=E5=85=A5resq=5Fmessages=E5=92=8Cresq=5Fmessage=5Fchai?= =?UTF-8?q?n=E5=AF=BC=E8=87=B4=E7=BC=93=E5=AD=98=E5=88=B0=E5=86=85?= =?UTF-8?q?=E5=AD=98=E4=B8=AD=E7=9A=84=E6=95=B0=E6=8D=AE=E5=92=8C=E5=86=99?= =?UTF-8?q?=E5=85=A5log=E4=B8=AD=E7=9A=84=E6=95=B0=E6=8D=AE=E9=87=8F?= =?UTF-8?q?=E5=BA=9E=E5=A4=A7=EF=BC=8C=E4=BB=A5=E5=8F=8A=E6=9C=89=E6=80=9D?= =?UTF-8?q?=E8=80=83=E7=9A=84think=E5=A4=84=E7=90=86=20feat:=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E5=B8=A6=E6=9C=89=E6=B7=B1=E5=BA=A6=E6=80=9D=E8=80=83?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E7=9A=84think=E7=9A=84=E5=8E=BBthink?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=20feat:dify=E4=B8=AD=E8=81=8A=E5=A4=A9?= =?UTF-8?q?=E6=9C=BA=E5=99=A8=E4=BA=BA=EF=BC=8Cchatflow=E5=AF=B9=E6=B5=81?= =?UTF-8?q?=E5=BC=8F=E7=9A=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/platform/sources/lark.py | 8 +-- pkg/provider/modelmgr/requesters/chatcmpl.py | 66 +++++++++++++----- pkg/provider/runners/difysvapi.py | 73 ++++++++++++++------ templates/metadata/pipeline/trigger.yaml | 7 ++ 4 files changed, 113 insertions(+), 41 deletions(-) diff --git a/pkg/platform/sources/lark.py b/pkg/platform/sources/lark.py index af57d66c..fd710330 100644 --- a/pkg/platform/sources/lark.py +++ b/pkg/platform/sources/lark.py @@ -359,7 +359,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): self.listeners = {} self.message_id_to_card_id = {} self.card_id_dict = {} - self.seq = 0 + self.seq = 1 @self.quart_app.route('/lark/callback', methods=['POST']) async def lark_callback(): @@ -456,7 +456,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): card_data = {"schema": "2.0", "header": {"title": {"content": "bot", "tag": "plain_text"}}, "body": {"elements": [{"tag": "markdown", "content": "[思考中.....]","element_id":"markdown_1"}]}, "config": {"streaming_mode": True, - "streaming_config": {"print_strategy": "fast"}}} + "streaming_config": {"print_strategy": "delay"}}} # delay / fast request: CreateCardRequest = CreateCardRequest.builder() \ .request_body( @@ -620,7 +620,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): 'type': 'card_json', 'data': {'card_id': self.card_id_dict[message_id], 'elements': {'content': text_message}}, } - + print(self.seq) request: ContentCardElementRequest = ContentCardElementRequest.builder() \ .card_id(self.card_id_dict[message_id]) \ .element_id("markdown_1") \ @@ -632,7 +632,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): .build() if is_final: - self.seq = 0 + self.seq = 1 # 发起请求 response: ContentCardElementResponse = self.api_client.cardkit.v1.card_element.content(request) diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index 5c303089..40bdf4c7 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -8,7 +8,7 @@ import openai.types.chat.chat_completion as chat_completion import httpx from .. import errors, requester -from ....core import entities as core_entities +from ....core import entities as core_entities, app from ... import entities as llm_entities from ...tools import entities as tools_entities @@ -25,7 +25,6 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): } - async def initialize(self): self.client = openai.AsyncClient( api_key='', @@ -53,6 +52,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): async def _make_msg( self, + pipeline_config: dict[str, typing.Any], chat_completion: chat_completion.ChatCompletion, ) -> llm_entities.Message: chatcmpl_message = chat_completion.choices[0].message.model_dump() @@ -64,8 +64,12 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): reasoning_content = chatcmpl_message['reasoning_content'] if 'reasoning_content' in chatcmpl_message else None # deepseek的reasoner模型 - if reasoning_content is not None: - chatcmpl_message['content'] = '\n' + reasoning_content + '\n\n' + chatcmpl_message['content'] + print(pipeline_config['trigger'].get('misc', '').get('remove_think')) + if pipeline_config['trigger'].get('misc', '').get('remove_think'): + pass + else: + if reasoning_content is not None : + chatcmpl_message['content'] = '\n' + reasoning_content + '\n\n' + chatcmpl_message['content'] message = llm_entities.Message(**chatcmpl_message) @@ -73,7 +77,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): async def _make_msg_chunk( self, - index:int, + pipeline_config: dict[str, typing.Any], chat_completion: chat_completion.ChatCompletion, ) -> llm_entities.MessageChunk: @@ -96,16 +100,22 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None # deepseek的reasoner模型 - if reasoning_content is not None and index == 0: - delta['content'] += f'\n{reasoning_content}' - elif reasoning_content is None: - if self.is_content: - delta['content'] = delta['content'] + if pipeline_config['trigger'].get('misc', '').get('remove_think'): + if reasoning_content is not None : + pass else: - delta['content'] = f'\n\n\n{delta["content"]}' - self.is_content = True + delta['content'] = delta['content'] else: - delta['content'] += reasoning_content + if reasoning_content is not None: + delta['content'] += f'\n{reasoning_content}' + elif reasoning_content is None: + if self.is_content: + delta['content'] = delta['content'] + else: + delta['content'] = f'\n\n\n{delta["content"]}' + self.is_content = True + else: + delta['content'] += reasoning_content message = llm_entities.MessageChunk(**delta) @@ -151,20 +161,41 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): args["stream"] = True chunk_idx = 0 self.is_content = False + tool_calls_map: dict[str, llm_entities.ToolCall] = {} + pipeline_config = query.pipeline_config async for chunk in self._req_stream(args, extra_body=extra_args): # 处理流式消息 - delta_message = await self._make_msg_chunk(chunk_idx,chunk) - # print(delta_message) + delta_message = await self._make_msg_chunk(pipeline_config,chunk) if delta_message.content: current_content += delta_message.content delta_message.content = current_content + print(current_content) # delta_message.all_content = current_content + if delta_message.tool_calls: + for tool_call in delta_message.tool_calls: + if tool_call.id not in tool_calls_map: + tool_calls_map[tool_call.id] = llm_entities.ToolCall( + id=tool_call.id, + type=tool_call.type, + function=llm_entities.FunctionCall( + name=tool_call.function.name if tool_call.function else '', + arguments='' + ), + ) + if tool_call.function and tool_call.function.arguments: + # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 + tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + + chunk_idx += 1 chunk_choices = getattr(chunk, 'choices', None) if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): delta_message.is_final = True + delta_message.content = current_content - yield delta_message + if chunk_idx % 64 == 0 or delta_message.is_final: + + yield delta_message # return @@ -208,7 +239,8 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): resp = await self._req(args, extra_body=extra_args) # 处理请求结果 - message = await self._make_msg(resp) + pipeline_config = query.pipeline_config + message = await self._make_msg(pipeline_config,resp) return message diff --git a/pkg/provider/runners/difysvapi.py b/pkg/provider/runners/difysvapi.py index 98b50f86..1dfde547 100644 --- a/pkg/provider/runners/difysvapi.py +++ b/pkg/provider/runners/difysvapi.py @@ -95,6 +95,11 @@ class DifyServiceAPIRunner(runner.RequestRunner): cov_id = query.session.using_conversation.uuid or '' query.variables['conversation_id'] = cov_id + try: + is_stream = query.adapter.is_stream + except AttributeError: + is_stream = False + plain_text, image_ids = await self._preprocess_user_message(query) files = [ @@ -144,40 +149,54 @@ class DifyServiceAPIRunner(runner.RequestRunner): if mode == 'workflow': if chunk['event'] == 'node_finished': - if chunk['data']['node_type'] == 'answer': - yield llm_entities.Message( - role='assistant', - content=self._try_convert_thinking(chunk['data']['outputs']['answer']), - ) + if not is_stream: + + if chunk['data']['node_type'] == 'answer': + yield llm_entities.Message( + role='assistant', + content=self._try_convert_thinking(chunk['data']['outputs']['answer']), + ) + else: + if chunk['data']['node_type'] == 'answer': + yield llm_entities.MessageChunk( + role='assistant', + content=self._try_convert_thinking(chunk['data']['outputs']['answer']), + is_final=True, + ) elif chunk['event'] == 'message': stream_output_pending_chunk += chunk['answer'] - if self.pipeline_config['ai']['dify-service-api'].get('enable-streaming', False): + if is_stream: # 消息数超过量就输出,从而达到streaming的效果 batch_pending_index += 1 if batch_pending_index >= batch_pending_max_size: - yield llm_entities.Message( + yield llm_entities.MessageChunk( role='assistant', content=self._try_convert_thinking(stream_output_pending_chunk), ) batch_pending_index = 0 elif mode == 'basic': - if chunk['event'] == 'message': - stream_output_pending_chunk += chunk['answer'] - if self.pipeline_config['ai']['dify-service-api'].get('enable-streaming', False): - # 消息数超过量就输出,从而达到streaming的效果 - batch_pending_index += 1 - if batch_pending_index >= batch_pending_max_size: + if chunk['event'] == 'message' or chunk['event'] == 'message_end': + if chunk['event'] == 'message_end': + is_final = True + if is_stream and batch_pending_index % batch_pending_max_size == 0: + # 消息数超过量就输出,从而达到streaming的效果 + batch_pending_index += 1 + # if batch_pending_index >= batch_pending_max_size: + yield llm_entities.MessageChunk( + role='assistant', + content=self._try_convert_thinking(stream_output_pending_chunk), + is_final=is_final, + ) + # batch_pending_index = 0 + elif not is_stream: yield llm_entities.Message( role='assistant', content=self._try_convert_thinking(stream_output_pending_chunk), ) - batch_pending_index = 0 - elif chunk['event'] == 'message_end': - yield llm_entities.Message( - role='assistant', - content=self._try_convert_thinking(stream_output_pending_chunk), - ) - stream_output_pending_chunk = '' + stream_output_pending_chunk = '' + else: + stream_output_pending_chunk += chunk['answer'] + is_final = False if chunk is None: raise errors.DifyAPIError('Dify API 没有返回任何响应,请检查网络连接和API配置') @@ -191,6 +210,13 @@ class DifyServiceAPIRunner(runner.RequestRunner): cov_id = query.session.using_conversation.uuid or '' query.variables['conversation_id'] = cov_id + try: + is_stream = query.adapter.is_stream + except AttributeError: + is_stream = False + + batch_pending_index = 0 + plain_text, image_ids = await self._preprocess_user_message(query) files = [ @@ -285,6 +311,13 @@ class DifyServiceAPIRunner(runner.RequestRunner): query.variables['conversation_id'] = query.session.using_conversation.uuid + try: + is_stream = query.adapter.is_stream + except AttributeError: + is_stream = False + + batch_pending_index = 0 + plain_text, image_ids = await self._preprocess_user_message(query) files = [ diff --git a/templates/metadata/pipeline/trigger.yaml b/templates/metadata/pipeline/trigger.yaml index 949b2698..165e488e 100644 --- a/templates/metadata/pipeline/trigger.yaml +++ b/templates/metadata/pipeline/trigger.yaml @@ -132,3 +132,10 @@ stages: type: boolean required: true default: true + - name: remove_think + label: + en_US: remove think + zh_Hans: 删除深度思考消息 + type: boolean + required: true + default: true From 4e1d81c9f8ed6c758f51ddde11694d02fc34ef83 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Mon, 14 Jul 2025 00:40:02 +0800 Subject: [PATCH 009/107] feat:add dify _agent_chat_message streaming --- pkg/provider/runners/difysvapi.py | 77 ++++++++++++++++++++++--------- 1 file changed, 56 insertions(+), 21 deletions(-) diff --git a/pkg/provider/runners/difysvapi.py b/pkg/provider/runners/difysvapi.py index 1dfde547..566dc0f8 100644 --- a/pkg/provider/runners/difysvapi.py +++ b/pkg/provider/runners/difysvapi.py @@ -251,11 +251,26 @@ class DifyServiceAPIRunner(runner.RequestRunner): if chunk['event'] in ignored_events: continue + batch_pending_index += 1 + + if chunk['event'] == 'agent_message' or chunk['event'] == 'message_end': + if chunk['event'] == 'message_end': + print(chunk['event']) + # break + is_final = True + else: + is_final = False + pending_agent_message += chunk['answer'] + if is_stream: + if batch_pending_index % 64 == 0 or is_final: + yield llm_entities.MessageChunk( + role='assistant', + content=self._try_convert_thinking(pending_agent_message), + is_final=is_final, + ) - if chunk['event'] == 'agent_message': - pending_agent_message += chunk['answer'] else: - if pending_agent_message.strip() != '': + if pending_agent_message.strip() != '' and not is_stream: pending_agent_message = pending_agent_message.replace('Action:', '') yield llm_entities.Message( role='assistant', @@ -268,19 +283,34 @@ class DifyServiceAPIRunner(runner.RequestRunner): continue if chunk['tool']: - msg = llm_entities.Message( - role='assistant', - tool_calls=[ - llm_entities.ToolCall( - id=chunk['id'], - type='function', - function=llm_entities.FunctionCall( - name=chunk['tool'], - arguments=json.dumps({}), - ), - ) - ], - ) + if is_stream: + msg = llm_entities.MessageChunk( + role='assistant', + tool_calls=[ + llm_entities.ToolCall( + id=chunk['id'], + type='function', + function=llm_entities.FunctionCall( + name=chunk['tool'], + arguments=json.dumps({}), + ), + ) + ], + ) + else: + msg = llm_entities.Message( + role='assistant', + tool_calls=[ + llm_entities.ToolCall( + id=chunk['id'], + type='function', + function=llm_entities.FunctionCall( + name=chunk['tool'], + arguments=json.dumps({}), + ), + ) + ], + ) yield msg if chunk['event'] == 'message_file': if chunk['type'] == 'image' and chunk['belongs_to'] == 'assistant': @@ -290,11 +320,16 @@ class DifyServiceAPIRunner(runner.RequestRunner): base_url = base_url[:-3] image_url = base_url + chunk['url'] - - yield llm_entities.Message( - role='assistant', - content=[llm_entities.ContentElement.from_image_url(image_url)], - ) + if is_stream: + yield llm_entities.MessageChunk( + role='assistant', + content=[llm_entities.ContentElement.from_image_url(image_url)], + ) + else: + yield llm_entities.Message( + role='assistant', + content=[llm_entities.ContentElement.from_image_url(image_url)], + ) if chunk['event'] == 'error': raise errors.DifyAPIError('dify 服务错误: ' + chunk['message']) From 0e68a922bdc89df3d934fdc9f392396b4d89709a Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Mon, 14 Jul 2025 01:42:42 +0800 Subject: [PATCH 010/107] perf:del dify stream in ai.yaml config.and enbale stream in lark.yaml. fix:localagent remove_think bug --- pkg/platform/sources/lark.py | 8 ++++---- pkg/platform/sources/lark.yaml | 17 +++++------------ pkg/provider/modelmgr/requesters/chatcmpl.py | 8 +++----- pkg/provider/runners/difysvapi.py | 15 +++++++-------- templates/metadata/pipeline/ai.yaml | 15 +-------------- 5 files changed, 20 insertions(+), 43 deletions(-) diff --git a/pkg/platform/sources/lark.py b/pkg/platform/sources/lark.py index fd710330..71c8045c 100644 --- a/pkg/platform/sources/lark.py +++ b/pkg/platform/sources/lark.py @@ -407,7 +407,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): async def is_stream_output_supported() -> bool: is_stream = False - if self.config.get("enable-card-reply",None): + if self.config.get("enable-stream-reply",None): is_stream = True self.is_stream = is_stream @@ -603,8 +603,8 @@ class LarkAdapter(adapter.MessagePlatformAdapter): """ lark_message = await self.message_converter.yiri2target(message, self.api_client) - if not is_final: - self.seq += 1 + + self.seq += 1 @@ -620,7 +620,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): 'type': 'card_json', 'data': {'card_id': self.card_id_dict[message_id], 'elements': {'content': text_message}}, } - print(self.seq) + request: ContentCardElementRequest = ContentCardElementRequest.builder() \ .card_id(self.card_id_dict[message_id]) \ .element_id("markdown_1") \ diff --git a/pkg/platform/sources/lark.yaml b/pkg/platform/sources/lark.yaml index bafaba81..94414b2e 100644 --- a/pkg/platform/sources/lark.yaml +++ b/pkg/platform/sources/lark.yaml @@ -65,23 +65,16 @@ spec: type: string required: true default: "" - - name: enable-card-reply + - name: enable-stream-reply label: - en_US: Enable Card Reply Mode - zh_Hans: 启用飞书卡片回复模式 + en_US: Enable Stream Reply Mode + zh_Hans: 启用飞书流式回复模式 description: - en_US: If enabled, the bot will use the card of lark reply mode - zh_Hans: 如果启用,将使用飞书卡片方式来回复内容 + en_US: If enabled, the bot will use the stream of lark reply mode + zh_Hans: 如果启用,将使用飞书流式方式来回复内容 type: boolean required: true default: false - - name: card_template_id - label: - en_US: card template id - zh_Hans: 卡片模板ID - type: string - required: true - default: "填写你的卡片template_id" execution: python: path: ./lark.py diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index 40bdf4c7..c07065d6 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -64,7 +64,6 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): reasoning_content = chatcmpl_message['reasoning_content'] if 'reasoning_content' in chatcmpl_message else None # deepseek的reasoner模型 - print(pipeline_config['trigger'].get('misc', '').get('remove_think')) if pipeline_config['trigger'].get('misc', '').get('remove_think'): pass else: @@ -79,6 +78,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): self, pipeline_config: dict[str, typing.Any], chat_completion: chat_completion.ChatCompletion, + idx: int, ) -> llm_entities.MessageChunk: # 处理流式chunk和完整响应的差异 @@ -106,7 +106,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): else: delta['content'] = delta['content'] else: - if reasoning_content is not None: + if reasoning_content is not None and idx == 0: delta['content'] += f'\n{reasoning_content}' elif reasoning_content is None: if self.is_content: @@ -165,11 +165,10 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): pipeline_config = query.pipeline_config async for chunk in self._req_stream(args, extra_body=extra_args): # 处理流式消息 - delta_message = await self._make_msg_chunk(pipeline_config,chunk) + delta_message = await self._make_msg_chunk(pipeline_config,chunk,chunk_idx) if delta_message.content: current_content += delta_message.content delta_message.content = current_content - print(current_content) # delta_message.all_content = current_content if delta_message.tool_calls: for tool_call in delta_message.tool_calls: @@ -324,7 +323,6 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): extra_args=extra_args, ): yield item - print(item) except asyncio.TimeoutError: raise errors.RequesterError('请求超时') diff --git a/pkg/provider/runners/difysvapi.py b/pkg/provider/runners/difysvapi.py index 566dc0f8..24318716 100644 --- a/pkg/provider/runners/difysvapi.py +++ b/pkg/provider/runners/difysvapi.py @@ -115,9 +115,7 @@ class DifyServiceAPIRunner(runner.RequestRunner): stream_output_pending_chunk = '' - batch_pending_max_size = self.pipeline_config['ai']['dify-service-api'].get( - 'output-batch-size', 0 - ) # 积累一定量的消息更新消息一次 + batch_pending_max_size = 64 # 积累一定量的消息更新消息一次 batch_pending_index = 0 @@ -255,14 +253,13 @@ class DifyServiceAPIRunner(runner.RequestRunner): if chunk['event'] == 'agent_message' or chunk['event'] == 'message_end': if chunk['event'] == 'message_end': - print(chunk['event']) # break is_final = True else: is_final = False pending_agent_message += chunk['answer'] if is_stream: - if batch_pending_index % 64 == 0 or is_final: + if batch_pending_index % 32 == 0 or is_final: yield llm_entities.MessageChunk( role='assistant', content=self._try_convert_thinking(pending_agent_message), @@ -276,7 +273,7 @@ class DifyServiceAPIRunner(runner.RequestRunner): role='assistant', content=self._try_convert_thinking(pending_agent_message), ) - pending_agent_message = '' + if chunk['event'] == 'agent_thought': if chunk['tool'] != '' and chunk['observation'] != '': # 工具调用结果,跳过 @@ -312,7 +309,7 @@ class DifyServiceAPIRunner(runner.RequestRunner): ], ) yield msg - if chunk['event'] == 'message_file': + elif chunk['event'] == 'message_file': if chunk['type'] == 'image' and chunk['belongs_to'] == 'assistant': base_url = self.dify_client.base_url @@ -330,8 +327,10 @@ class DifyServiceAPIRunner(runner.RequestRunner): role='assistant', content=[llm_entities.ContentElement.from_image_url(image_url)], ) - if chunk['event'] == 'error': + elif chunk['event'] == 'error': raise errors.DifyAPIError('dify 服务错误: ' + chunk['message']) + else: + pending_agent_message = '' if chunk is None: raise errors.DifyAPIError('Dify API 没有返回任何响应,请检查网络连接和API配置') diff --git a/templates/metadata/pipeline/ai.yaml b/templates/metadata/pipeline/ai.yaml index fb2672d4..ca739ce1 100644 --- a/templates/metadata/pipeline/ai.yaml +++ b/templates/metadata/pipeline/ai.yaml @@ -128,20 +128,7 @@ stages: label: en_US: Remove zh_Hans: 移除 - - name: enable-streaming - label: - en_US: enable streaming mode - zh_Hans: 开启流式输出 - type: boolean - required: true - default: false - - name: output-batch-size - label: - en_US: output batch size - zh_Hans: 输出批次大小(积累多少条消息后一起输出) - type: integer - required: true - default: 10 + - name: dashscope-app-api label: From c74cf38e9fc6b9c6def763cc96c716c5d73e4349 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Mon, 14 Jul 2025 23:53:55 +0800 Subject: [PATCH 011/107] feat:add deepseek and modelscope llm stream,and giteeai think in content remove_think --- pkg/provider/modelmgr/requesters/chatcmpl.py | 11 +- .../modelmgr/requesters/deepseekchatcmpl.py | 6 +- .../modelmgr/requesters/giteeaichatcmpl.py | 167 +++++++++++++++++- .../modelmgr/requesters/modelscopechatcmpl.py | 48 +++++ 4 files changed, 226 insertions(+), 6 deletions(-) diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index c07065d6..f30bfd4e 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -52,10 +52,11 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): async def _make_msg( self, - pipeline_config: dict[str, typing.Any], chat_completion: chat_completion.ChatCompletion, + pipeline_config: dict[str, typing.Any] = {'trigger': {'misc': {'remove_think': False}}}, ) -> llm_entities.Message: chatcmpl_message = chat_completion.choices[0].message.model_dump() + # print(chatcmpl_message.keys(),chatcmpl_message.values()) # 确保 role 字段存在且不为 None if 'role' not in chatcmpl_message or chatcmpl_message['role'] is None: @@ -65,6 +66,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): # deepseek的reasoner模型 if pipeline_config['trigger'].get('misc', '').get('remove_think'): + pass else: if reasoning_content is not None : @@ -92,13 +94,16 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): delta = chat_completion.delta.model_dump() if hasattr(chat_completion, 'delta') else {} # 确保 role 字段存在且不为 None - # print(delta.values()) + # print(delta.keys(),delta.values()) if 'role' not in delta or delta['role'] is None: delta['role'] = 'assistant' reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None + delta['content'] = '' if delta['content'] is None else delta['content'] + # print(reasoning_content) + # deepseek的reasoner模型 if pipeline_config['trigger'].get('misc', '').get('remove_think'): if reasoning_content is not None : @@ -239,7 +244,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): resp = await self._req(args, extra_body=extra_args) # 处理请求结果 pipeline_config = query.pipeline_config - message = await self._make_msg(pipeline_config,resp) + message = await self._make_msg(resp,pipeline_config) return message diff --git a/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py b/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py index 6d664b01..f57f624f 100644 --- a/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py +++ b/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py @@ -49,10 +49,12 @@ class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions): # 发送请求 resp = await self._req(args, extra_body=extra_args) + # print(resp) + if resp is None: raise errors.RequesterError('接口返回为空,请确定模型提供商服务是否正常') - + pipeline_config = query.pipeline_config # 处理请求结果 - message = await self._make_msg(resp) + message = await self._make_msg(resp,pipeline_config) return message diff --git a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py index 3795ef99..ce1b075f 100644 --- a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py +++ b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py @@ -8,6 +8,9 @@ from .. import requester from ....core import entities as core_entities from ... import entities as llm_entities from ...tools import entities as tools_entities +import re +import openai.types.chat.chat_completion as chat_completion + class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): @@ -17,6 +20,7 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): 'base_url': 'https://ai.gitee.com/v1', 'timeout': 120, } + is_think:bool = False async def _closure( self, @@ -46,6 +50,167 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): resp = await self._req(args, extra_body=extra_args) - message = await self._make_msg(resp) + pipeline_config = query.pipeline_config + + message = await self._make_msg(resp,pipeline_config) return message + + + async def _make_msg( + self, + chat_completion: chat_completion.ChatCompletion, + pipeline_config: dict[str, typing.Any] = {'trigger': {'misc': {'remove_think': False}}}, + ) -> llm_entities.Message: + chatcmpl_message = chat_completion.choices[0].message.model_dump() + # print(chatcmpl_message.keys(), chatcmpl_message.values()) + + # 确保 role 字段存在且不为 None + if 'role' not in chatcmpl_message or chatcmpl_message['role'] is None: + chatcmpl_message['role'] = 'assistant' + + reasoning_content = chatcmpl_message['reasoning_content'] if 'reasoning_content' in chatcmpl_message else None + + # deepseek的reasoner模型 + if pipeline_config['trigger'].get('misc', '').get('remove_think'): + chatcmpl_message['content'] = re.sub(r'.*?', '', chatcmpl_message['content'], flags=re.DOTALL) + else: + if reasoning_content is not None: + chatcmpl_message['content'] = '\n' + reasoning_content + '\n\n' + chatcmpl_message['content'] + + message = llm_entities.Message(**chatcmpl_message) + + return message + + + async def _make_msg_chunk( + self, + pipeline_config: dict[str, typing.Any], + chat_completion: chat_completion.ChatCompletion, + idx: int, + ) -> llm_entities.MessageChunk: + + # 处理流式chunk和完整响应的差异 + # print(chat_completion.choices[0]) + if hasattr(chat_completion, 'choices'): + # 完整响应模式 + choice = chat_completion.choices[0] + delta = choice.delta.model_dump() if hasattr(choice, 'delta') else choice.message.model_dump() + else: + # 流式chunk模式 + delta = chat_completion.delta.model_dump() if hasattr(chat_completion, 'delta') else {} + + # 确保 role 字段存在且不为 None + if 'role' not in delta or delta['role'] is None: + delta['role'] = 'assistant' + + + reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None + + delta['content'] = '' if delta['content'] is None else delta['content'] + # print(reasoning_content) + + # deepseek的reasoner模型 + if pipeline_config['trigger'].get('misc', '').get('remove_think'): + if delta['content'] == '': + self.is_think = True + delta['content'] = '' + if delta['content'] == rf'': + self.is_think = False + delta['content'] = '' + if not self.is_think: + delta['content'] = delta['content'] + else: + delta['content'] = '' + else: + if reasoning_content is not None and idx == 0: + delta['content'] += f'\n{reasoning_content}' + elif reasoning_content is None: + if self.is_content: + delta['content'] = delta['content'] + else: + delta['content'] = f'\n\n\n{delta["content"]}' + self.is_content = True + else: + delta['content'] += reasoning_content + + + message = llm_entities.MessageChunk(**delta) + + return message + + async def _closure_stream( + self, + query: core_entities.Query, + req_messages: list[dict], + use_model: requester.RuntimeLLMModel, + use_funcs: list[tools_entities.LLMFunction] = None, + stream: bool = False, + extra_args: dict[str, typing.Any] = {}, + ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: + self.client.api_key = use_model.token_mgr.get_token() + + args = {} + args['model'] = use_model.model_entity.name + + if use_funcs: + tools = await self.ap.tool_mgr.generate_tools_for_openai(use_funcs) + + if tools: + args['tools'] = tools + + # 设置此次请求中的messages + messages = req_messages.copy() + + # 检查vision + for msg in messages: + if 'content' in msg and isinstance(msg['content'], list): + for me in msg['content']: + if me['type'] == 'image_base64': + me['image_url'] = {'url': me['image_base64']} + me['type'] = 'image_url' + del me['image_base64'] + + args['messages'] = messages + + if stream: + current_content = '' + args["stream"] = True + chunk_idx = 0 + self.is_content = False + tool_calls_map: dict[str, llm_entities.ToolCall] = {} + pipeline_config = query.pipeline_config + async for chunk in self._req_stream(args, extra_body=extra_args): + # 处理流式消息 + delta_message = await self._make_msg_chunk(pipeline_config,chunk,chunk_idx) + if delta_message.content: + current_content += delta_message.content + delta_message.content = current_content + # delta_message.all_content = current_content + if delta_message.tool_calls: + for tool_call in delta_message.tool_calls: + if tool_call.id not in tool_calls_map: + tool_calls_map[tool_call.id] = llm_entities.ToolCall( + id=tool_call.id, + type=tool_call.type, + function=llm_entities.FunctionCall( + name=tool_call.function.name if tool_call.function else '', + arguments='' + ), + ) + if tool_call.function and tool_call.function.arguments: + # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 + tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + + + chunk_idx += 1 + chunk_choices = getattr(chunk, 'choices', None) + if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): + delta_message.is_final = True + delta_message.content = current_content + + if chunk_idx % 64 == 0 or delta_message.is_final: + + yield delta_message + + diff --git a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py index b8868f4d..44022d01 100644 --- a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py +++ b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py @@ -202,3 +202,51 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester): raise errors.RequesterError(f'请求过于频繁或余额不足: {e.message}') except openai.APIError as e: raise errors.RequesterError(f'请求错误: {e.message}') + + + async def invoke_llm_stream( + self, + query: core_entities.Query, + model: requester.RuntimeLLMModel, + messages: typing.List[llm_entities.Message], + funcs: typing.List[tools_entities.LLMFunction] = None, + stream: bool = False, + extra_args: dict[str, typing.Any] = {}, + ) -> llm_entities.MessageChunk: + req_messages = [] # req_messages 仅用于类内,外部同步由 query.messages 进行 + for m in messages: + msg_dict = m.dict(exclude_none=True) + content = msg_dict.get('content') + if isinstance(content, list): + # 检查 content 列表中是否每个部分都是文本 + if all(isinstance(part, dict) and part.get('type') == 'text' for part in content): + # 将所有文本部分合并为一个字符串 + msg_dict['content'] = '\n'.join(part['text'] for part in content) + req_messages.append(msg_dict) + + try: + async for item in self._closure_stream( + query=query, + req_messages=req_messages, + use_model=model, + use_funcs=funcs, + stream=stream, + extra_args=extra_args, + ): + yield item + + except asyncio.TimeoutError: + raise errors.RequesterError('请求超时') + except openai.BadRequestError as e: + if 'context_length_exceeded' in e.message: + raise errors.RequesterError(f'上文过长,请重置会话: {e.message}') + else: + raise errors.RequesterError(f'请求参数错误: {e.message}') + except openai.AuthenticationError as e: + raise errors.RequesterError(f'无效的 api-key: {e.message}') + except openai.NotFoundError as e: + raise errors.RequesterError(f'请求路径错误: {e.message}') + except openai.RateLimitError as e: + raise errors.RequesterError(f'请求过于频繁或余额不足: {e.message}') + except openai.APIError as e: + raise errors.RequesterError(f'请求错误: {e.message}') \ No newline at end of file From d15df3338f1c376880e80ee2b4b802165c28e723 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Tue, 15 Jul 2025 00:50:42 +0800 Subject: [PATCH 012/107] feat:add ppio and openrouter llm stream,and ppio think in content remove_think. fix: giteeai stream no remove_think content add char"" --- .../modelmgr/requesters/giteeaichatcmpl.py | 10 +- .../modelmgr/requesters/modelscopechatcmpl.py | 137 ++++++++++++++++ .../modelmgr/requesters/ppiochatcmpl.py | 152 ++++++++++++++++++ 3 files changed, 290 insertions(+), 9 deletions(-) diff --git a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py index ce1b075f..2a618c9f 100644 --- a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py +++ b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py @@ -123,15 +123,7 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): else: delta['content'] = '' else: - if reasoning_content is not None and idx == 0: - delta['content'] += f'\n{reasoning_content}' - elif reasoning_content is None: - if self.is_content: - delta['content'] = delta['content'] - else: - delta['content'] = f'\n\n\n{delta["content"]}' - self.is_content = True - else: + if reasoning_content is not None: delta['content'] += reasoning_content diff --git a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py index 44022d01..1a303d22 100644 --- a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py +++ b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py @@ -164,6 +164,143 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester): return message + async def _req_stream( + self, + args: dict, + extra_body: dict = {}, + ) -> chat_completion.ChatCompletion: + + async for chunk in await self.client.chat.completions.create(**args, extra_body=extra_body): + yield chunk + + async def _make_msg_chunk( + self, + pipeline_config: dict[str, typing.Any], + chat_completion: chat_completion.ChatCompletion, + idx: int, + ) -> llm_entities.MessageChunk: + + # 处理流式chunk和完整响应的差异 + # print(chat_completion.choices[0]) + if hasattr(chat_completion, 'choices'): + # 完整响应模式 + choice = chat_completion.choices[0] + delta = choice.delta.model_dump() if hasattr(choice, 'delta') else choice.message.model_dump() + else: + # 流式chunk模式 + delta = chat_completion.delta.model_dump() if hasattr(chat_completion, 'delta') else {} + + # 确保 role 字段存在且不为 None + # print(delta.keys(),delta.values()) + if 'role' not in delta or delta['role'] is None: + delta['role'] = 'assistant' + + + reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None + + delta['content'] = '' if delta['content'] is None else delta['content'] + # print(reasoning_content) + + # deepseek的reasoner模型 + if pipeline_config['trigger'].get('misc', '').get('remove_think'): + if reasoning_content is not None : + pass + else: + delta['content'] = delta['content'] + else: + if reasoning_content is not None and idx == 0: + delta['content'] += f'\n{reasoning_content}' + elif reasoning_content is None: + if self.is_content: + delta['content'] = delta['content'] + else: + delta['content'] = f'\n\n\n{delta["content"]}' + self.is_content = True + else: + delta['content'] += reasoning_content + + + message = llm_entities.MessageChunk(**delta) + + return message + + async def _closure_stream( + self, + query: core_entities.Query, + req_messages: list[dict], + use_model: requester.RuntimeLLMModel, + use_funcs: list[tools_entities.LLMFunction] = None, + stream: bool = False, + extra_args: dict[str, typing.Any] = {}, + ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: + self.client.api_key = use_model.token_mgr.get_token() + + args = {} + args['model'] = use_model.model_entity.name + + if use_funcs: + tools = await self.ap.tool_mgr.generate_tools_for_openai(use_funcs) + + if tools: + args['tools'] = tools + + # 设置此次请求中的messages + messages = req_messages.copy() + + # 检查vision + for msg in messages: + if 'content' in msg and isinstance(msg['content'], list): + for me in msg['content']: + if me['type'] == 'image_base64': + me['image_url'] = {'url': me['image_base64']} + me['type'] = 'image_url' + del me['image_base64'] + + args['messages'] = messages + + if stream: + current_content = '' + args["stream"] = True + chunk_idx = 0 + self.is_content = False + tool_calls_map: dict[str, llm_entities.ToolCall] = {} + pipeline_config = query.pipeline_config + async for chunk in self._req_stream(args, extra_body=extra_args): + # 处理流式消息 + delta_message = await self._make_msg_chunk(pipeline_config,chunk,chunk_idx) + if delta_message.content: + current_content += delta_message.content + delta_message.content = current_content + # delta_message.all_content = current_content + if delta_message.tool_calls: + for tool_call in delta_message.tool_calls: + if tool_call.id not in tool_calls_map: + tool_calls_map[tool_call.id] = llm_entities.ToolCall( + id=tool_call.id, + type=tool_call.type, + function=llm_entities.FunctionCall( + name=tool_call.function.name if tool_call.function else '', + arguments='' + ), + ) + if tool_call.function and tool_call.function.arguments: + # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 + tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + + + chunk_idx += 1 + chunk_choices = getattr(chunk, 'choices', None) + if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): + delta_message.is_final = True + delta_message.content = current_content + + if chunk_idx % 64 == 0 or delta_message.is_final: + + yield delta_message + # return + + + async def invoke_llm( self, query: core_entities.Query, diff --git a/pkg/provider/modelmgr/requesters/ppiochatcmpl.py b/pkg/provider/modelmgr/requesters/ppiochatcmpl.py index 7e78ddb8..85b321a7 100644 --- a/pkg/provider/modelmgr/requesters/ppiochatcmpl.py +++ b/pkg/provider/modelmgr/requesters/ppiochatcmpl.py @@ -4,6 +4,12 @@ import openai import typing from . import chatcmpl +import openai.types.chat.chat_completion as chat_completion +from .. import errors, requester +from ....core import entities as core_entities, app +from ... import entities as llm_entities +from ...tools import entities as tools_entities +import re class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): @@ -15,3 +21,149 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): 'base_url': 'https://api.ppinfra.com/v3/openai', 'timeout': 120, } + + is_think: bool = False + + async def _make_msg( + self, + chat_completion: chat_completion.ChatCompletion, + pipeline_config: dict[str, typing.Any] = {'trigger': {'misc': {'remove_think': False}}}, + ) -> llm_entities.Message: + chatcmpl_message = chat_completion.choices[0].message.model_dump() + # print(chatcmpl_message.keys(), chatcmpl_message.values()) + + # 确保 role 字段存在且不为 None + if 'role' not in chatcmpl_message or chatcmpl_message['role'] is None: + chatcmpl_message['role'] = 'assistant' + + reasoning_content = chatcmpl_message['reasoning_content'] if 'reasoning_content' in chatcmpl_message else None + + # deepseek的reasoner模型 + if pipeline_config['trigger'].get('misc', '').get('remove_think'): + chatcmpl_message['content'] = re.sub(r'.*?', '', chatcmpl_message['content'], flags=re.DOTALL) + else: + if reasoning_content is not None: + chatcmpl_message['content'] = '\n' + reasoning_content + '\n\n' + chatcmpl_message['content'] + + message = llm_entities.Message(**chatcmpl_message) + + return message + + + async def _make_msg_chunk( + self, + pipeline_config: dict[str, typing.Any], + chat_completion: chat_completion.ChatCompletion, + idx: int, + ) -> llm_entities.MessageChunk: + # 处理流式chunk和完整响应的差异 + # print(chat_completion.choices[0]) + if hasattr(chat_completion, 'choices'): + # 完整响应模式 + choice = chat_completion.choices[0] + delta = choice.delta.model_dump() if hasattr(choice, 'delta') else choice.message.model_dump() + else: + # 流式chunk模式 + delta = chat_completion.delta.model_dump() if hasattr(chat_completion, 'delta') else {} + + # 确保 role 字段存在且不为 None + if 'role' not in delta or delta['role'] is None: + delta['role'] = 'assistant' + + reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None + + delta['content'] = '' if delta['content'] is None else delta['content'] + # print(reasoning_content) + + # deepseek的reasoner模型 + if pipeline_config['trigger'].get('misc', '').get('remove_think'): + if '' in delta['content']: + self.is_think = True + delta['content'] = '' + if rf'' in delta['content']: + self.is_think = False + delta['content'] = '' + if not self.is_think: + delta['content'] = delta['content'] + else: + delta['content'] = '' + else: + if reasoning_content is not None: + delta['content'] += reasoning_content + + message = llm_entities.MessageChunk(**delta) + + return message + + + async def _closure_stream( + self, + query: core_entities.Query, + req_messages: list[dict], + use_model: requester.RuntimeLLMModel, + use_funcs: list[tools_entities.LLMFunction] = None, + stream: bool = False, + extra_args: dict[str, typing.Any] = {}, + ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: + self.client.api_key = use_model.token_mgr.get_token() + + args = {} + args['model'] = use_model.model_entity.name + + if use_funcs: + tools = await self.ap.tool_mgr.generate_tools_for_openai(use_funcs) + + if tools: + args['tools'] = tools + + # 设置此次请求中的messages + messages = req_messages.copy() + + # 检查vision + for msg in messages: + if 'content' in msg and isinstance(msg['content'], list): + for me in msg['content']: + if me['type'] == 'image_base64': + me['image_url'] = {'url': me['image_base64']} + me['type'] = 'image_url' + del me['image_base64'] + + args['messages'] = messages + + if stream: + current_content = '' + args["stream"] = True + chunk_idx = 0 + self.is_content = False + tool_calls_map: dict[str, llm_entities.ToolCall] = {} + pipeline_config = query.pipeline_config + async for chunk in self._req_stream(args, extra_body=extra_args): + # 处理流式消息 + delta_message = await self._make_msg_chunk(pipeline_config, chunk, chunk_idx) + if delta_message.content: + current_content += delta_message.content + delta_message.content = current_content + # delta_message.all_content = current_content + if delta_message.tool_calls: + for tool_call in delta_message.tool_calls: + if tool_call.id not in tool_calls_map: + tool_calls_map[tool_call.id] = llm_entities.ToolCall( + id=tool_call.id, + type=tool_call.type, + function=llm_entities.FunctionCall( + name=tool_call.function.name if tool_call.function else '', + arguments='' + ), + ) + if tool_call.function and tool_call.function.arguments: + # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 + tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + + chunk_idx += 1 + chunk_choices = getattr(chunk, 'choices', None) + if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): + delta_message.is_final = True + delta_message.content = current_content + + if chunk_idx % 64 == 0 or delta_message.is_final: + yield delta_message From 11e52a3ade134d144fb24ced8069a6f0e57eef01 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Thu, 17 Jul 2025 14:29:30 +0800 Subject: [PATCH 013/107] feat:add telegram stream --- pkg/platform/sources/telegram.py | 74 ++++++++++++++++++++++++++++++ pkg/platform/sources/telegram.yaml | 10 ++++ 2 files changed, 84 insertions(+) diff --git a/pkg/platform/sources/telegram.py b/pkg/platform/sources/telegram.py index 266d994e..363ad702 100644 --- a/pkg/platform/sources/telegram.py +++ b/pkg/platform/sources/telegram.py @@ -1,5 +1,7 @@ from __future__ import annotations +import time + import telegram import telegram.ext from telegram import Update @@ -143,6 +145,8 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): config: dict ap: app.Application + msg_stream_id: dict + listeners: typing.Dict[ typing.Type[platform_events.Event], typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], @@ -152,6 +156,7 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): self.config = config self.ap = ap self.logger = logger + self.msg_stream_id = {} async def telegram_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): if update.message.from_user.is_bot: @@ -160,6 +165,7 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): try: lb_event = await self.event_converter.target2yiri(update, self.bot, self.bot_account_id) await self.listeners[type(lb_event)](lb_event, self) + await self.is_stream_output_supported() except Exception as e: await self.logger.error(f"Error in telegram callback: {traceback.format_exc()}") @@ -200,6 +206,74 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): await self.bot.send_message(**args) + + async def reply_message_chunk( + self, + message_source: platform_events.MessageEvent, + message_id: int, + message: platform_message.MessageChain, + quote_origin: bool = False, + is_final: bool = False, + ): + + assert isinstance(message_source.source_platform_object, Update) + components = await TelegramMessageConverter.yiri2target(message, self.bot) + args = {} + message_id = message_source.source_platform_object.message.id + if quote_origin: + args['reply_to_message_id'] = message_source.source_platform_object.message.id + + component = components[0] + if message_id not in self.msg_stream_id: + # time.sleep(0.6) + if component['type'] == 'text': + if self.config['markdown_card'] is True: + content = telegramify_markdown.markdownify( + content=component['text'], + ) + else: + content = component['text'] + args = { + 'chat_id': message_source.source_platform_object.effective_chat.id, + 'text': content, + } + if self.config['markdown_card'] is True: + args['parse_mode'] = 'MarkdownV2' + + + send_msg = await self.bot.send_message(**args) + send_msg_id = send_msg.message_id + self.msg_stream_id[message_id] = send_msg_id + else: + if component['type'] == 'text': + if self.config['markdown_card'] is True: + content = telegramify_markdown.markdownify( + content=component['text'], + ) + else: + content = component['text'] + args = { + 'message_id': self.msg_stream_id[message_id], + 'chat_id': message_source.source_platform_object.effective_chat.id, + 'text': content, + } + if self.config['markdown_card'] is True: + args['parse_mode'] = 'MarkdownV2' + + await self.bot.edit_message_text(**args) + if is_final: + self.msg_stream_id.pop(message_id) + + + async def is_stream_output_supported(self) -> bool: + is_stream = False + if self.config.get("enable-stream-reply", None): + is_stream = True + self.is_stream = is_stream + + return is_stream + + async def is_muted(self, group_id: int) -> bool: return False diff --git a/pkg/platform/sources/telegram.yaml b/pkg/platform/sources/telegram.yaml index 43b9284b..d29c359e 100644 --- a/pkg/platform/sources/telegram.yaml +++ b/pkg/platform/sources/telegram.yaml @@ -25,6 +25,16 @@ spec: type: boolean required: false default: true + - name: enable-stream-reply + label: + en_US: Enable Stream Reply Mode + zh_Hans: 启用电报流式回复模式 + description: + en_US: If enabled, the bot will use the stream of telegram reply mode + zh_Hans: 如果启用,将使用电报流式方式来回复内容 + type: boolean + required: true + default: false execution: python: path: ./telegram.py From adb0bf247387dae14e581567d1f0fd4a13e30200 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Sat, 19 Jul 2025 01:05:44 +0800 Subject: [PATCH 014/107] feat:add dingtalk stream --- pkg/platform/sources/dingtalk.py | 73 ++++++++++++++++++++++++++---- pkg/platform/sources/dingtalk.yaml | 17 +++++++ 2 files changed, 81 insertions(+), 9 deletions(-) diff --git a/pkg/platform/sources/dingtalk.py b/pkg/platform/sources/dingtalk.py index 3147c984..f297019d 100644 --- a/pkg/platform/sources/dingtalk.py +++ b/pkg/platform/sources/dingtalk.py @@ -99,11 +99,13 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): message_converter: DingTalkMessageConverter = DingTalkMessageConverter() event_converter: DingTalkEventConverter = DingTalkEventConverter() config: dict + card_instance_id_dict: dict def __init__(self, config: dict, ap: app.Application, logger: EventLogger): self.config = config self.ap = ap self.logger = logger + self.card_instance_id_dict = {} required_keys = [ 'client_id', 'client_secret', @@ -116,6 +118,15 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): self.bot_account_id = self.config['robot_name'] + self.bot = DingTalkClient( + client_id=config['client_id'], + client_secret=config['client_secret'], + robot_name=config['robot_name'], + robot_code=config['robot_code'], + markdown_card=config['markdown_card'], + logger=self.logger, + ) + async def reply_message( self, message_source: platform_events.MessageEvent, @@ -130,6 +141,34 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): content, at = await DingTalkMessageConverter.yiri2target(message) await self.bot.send_message(content, incoming_message, at) + async def reply_message_chunk( + self, + message_source: platform_events.MessageEvent, + message_id: int, + message: platform_message.MessageChain, + quote_origin: bool = False, + is_final: bool = False, + ): + event = await DingTalkEventConverter.yiri2target( + message_source, + ) + incoming_message = event.incoming_message + + msg_id = incoming_message.message_id + + content, at = await DingTalkMessageConverter.yiri2target(message) + # is_stream = self.config['enable-stream-reply'] + # print(content) + card_template_id = self.config['card_template_id'] + if msg_id not in self.card_instance_id_dict: + card_instance,card_instance_id = await self.bot.create_and_card(card_template_id,incoming_message,at) + self.card_instance_id_dict[msg_id] = (card_instance,card_instance_id) + else: + card_instance,card_instance_id = self.card_instance_id_dict[msg_id] + # print(card_instance_id) + await self.bot.send_card_message(card_instance,card_instance_id,content,is_final) + + async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): content = await DingTalkMessageConverter.yiri2target(message) if target_type == 'person': @@ -137,6 +176,21 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): if target_type == 'group': await self.bot.send_proactive_message_to_group(target_id, content) + async def is_stream_output_supported(self) -> bool: + is_stream = False + if self.config.get("enable-stream-reply", None): + is_stream = True + self.is_stream = is_stream + + return is_stream + + async def create_message_card(self,message_id: str, incoming_message): + card_template_id = self.config['card_template_id'] + + card_instance, card_instance_id = await self.bot.create_and_card(card_template_id, incoming_message) + self.card_instance_id_dict[message_id] = (card_instance, card_instance_id) + + def register_listener( self, event_type: typing.Type[platform_events.Event], @@ -144,6 +198,7 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): ): async def on_message(event: DingTalkEvent): try: + await self.is_stream_output_supported() return await callback( await self.event_converter.target2yiri(event, self.config['robot_name']), self, @@ -157,15 +212,15 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): self.bot.on_message('GroupMessage')(on_message) async def run_async(self): - config = self.config - self.bot = DingTalkClient( - client_id=config['client_id'], - client_secret=config['client_secret'], - robot_name=config['robot_name'], - robot_code=config['robot_code'], - markdown_card=config['markdown_card'], - logger=self.logger, - ) + # config = self.config + # self.bot = DingTalkClient( + # client_id=config['client_id'], + # client_secret=config['client_secret'], + # robot_name=config['robot_name'], + # robot_code=config['robot_code'], + # markdown_card=config['markdown_card'], + # logger=self.logger, + # ) await self.bot.start() async def kill(self) -> bool: diff --git a/pkg/platform/sources/dingtalk.yaml b/pkg/platform/sources/dingtalk.yaml index fac2d6ff..70855c2b 100644 --- a/pkg/platform/sources/dingtalk.yaml +++ b/pkg/platform/sources/dingtalk.yaml @@ -46,6 +46,23 @@ spec: type: boolean required: false default: true + - name: enable-stream-reply + label: + en_US: Enable Stream Reply Mode + zh_Hans: 启用钉钉卡片流式回复模式 + description: + en_US: If enabled, the bot will use the stream of lark reply mode + zh_Hans: 如果启用,将使用钉钉卡片流式方式来回复内容 + type: boolean + required: true + default: false + - name: card_template_id + label: + en_US: card template id + zh_Hans: 卡片模板ID + type: string + required: true + default: "填写你的卡片template_id" execution: python: path: ./dingtalk.py From f58c8497c3674f52a3daf2a7585943d716adccb0 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Sun, 20 Jul 2025 23:53:20 +0800 Subject: [PATCH 015/107] feat:add dingtalk stream fix:adapter is_stream_output_supported bug fix:stream message reply chunk in message_id --- libs/dingtalk_api/api.py | 43 ++++++++ pkg/pipeline/process/handlers/chat.py | 32 +++--- pkg/pipeline/respback/respback.py | 2 +- pkg/platform/adapter.py | 4 + pkg/platform/sources/dingtalk.py | 22 ++-- pkg/platform/sources/lark.py | 143 +++++++++----------------- pkg/provider/entities.py | 2 + pkg/provider/runners/difysvapi.py | 6 +- pkg/provider/runners/localagent.py | 4 +- 9 files changed, 133 insertions(+), 125 deletions(-) diff --git a/libs/dingtalk_api/api.py b/libs/dingtalk_api/api.py index d323df1e..d1c7065f 100644 --- a/libs/dingtalk_api/api.py +++ b/libs/dingtalk_api/api.py @@ -3,6 +3,7 @@ import json import time from typing import Callable import dingtalk_stream # type: ignore +from dingtalk_stream import AckMessage, ChatbotHandler, CallbackHandler, CallbackMessage, ChatbotMessage, AICardReplier from .EchoHandler import EchoTextHandler from .dingtalkevent import DingTalkEvent import httpx @@ -253,6 +254,48 @@ class DingTalkClient: await self.logger.error(f'failed to send proactive massage to group: {traceback.format_exc()}') raise Exception(f'failed to send proactive massage to group: {traceback.format_exc()}') + async def create_and_card(self, temp_card_id: str, incoming_message: dingtalk_stream.ChatbotMessage,quote_origin:bool=False): + content_key = "content" + card_data = {content_key: ""} + + card_instance = dingtalk_stream.AICardReplier( + self.client, incoming_message + ) + # print(card_instance) + # 先投放卡片: https://open.dingtalk.com/document/orgapp/create-and-deliver-cards + card_instance_id = await card_instance.async_create_and_deliver_card( + temp_card_id, card_data, + ) + return card_instance,card_instance_id + + async def send_card_message(self, + card_instance, + card_instance_id: str,content: str,is_final: bool): + content_key = "content" + try: + await card_instance.async_streaming( + card_instance_id, + content_key=content_key, + content_value=content, + append=False, + finished=is_final, + failed=False, + ) + except Exception as e: + self.logger.exception(e) + await card_instance.async_streaming( + card_instance_id, + content_key=content_key, + content_value="", + append=False, + finished=is_final, + failed=True, + ) + + + + + async def start(self): """启动 WebSocket 连接,监听消息""" await self.client.start() diff --git a/pkg/pipeline/process/handlers/chat.py b/pkg/pipeline/process/handlers/chat.py index c7b6ab85..682be62d 100644 --- a/pkg/pipeline/process/handlers/chat.py +++ b/pkg/pipeline/process/handlers/chat.py @@ -1,5 +1,6 @@ from __future__ import annotations +import uuid from itertools import accumulate import typing import traceback @@ -47,7 +48,6 @@ class ChatMessageHandler(handler.MessageHandler): if event_ctx.is_prevented_default(): if event_ctx.event.reply is not None: mc = platform_message.MessageChain(event_ctx.event.reply) - query.resp_messages.append(mc) yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) @@ -60,7 +60,7 @@ class ChatMessageHandler(handler.MessageHandler): text_length = 0 try: - is_stream = query.adapter.is_stream + is_stream = await query.adapter.is_stream_output_supported() except AttributeError: is_stream = False print(is_stream) @@ -73,22 +73,26 @@ class ChatMessageHandler(handler.MessageHandler): else: raise ValueError(f'未找到请求运行器: {query.pipeline_config["ai"]["runner"]["runner"]}') if is_stream: - # async for results in runner.run(query): - async for result in runner.run(query): - if query.resp_messages: - query.resp_messages.pop() - if query.resp_message_chain: - query.resp_message_chain.pop() + resp_message_id = uuid.uuid4() + if await query.adapter.create_message_card(resp_message_id,query.message_event.source_platform_object): + async for result in runner.run(query): + result.resp_message_id = resp_message_id + if query.resp_messages: + query.resp_messages.pop() + if query.resp_message_chain: + query.resp_message_chain.pop() - query.resp_messages.append(result) - print(result) - self.ap.logger.info(f'对话({query.query_id})响应: {self.cut_str(result.readable_str())}') + query.resp_messages.append(result) + self.ap.logger.info(f'对话({query.query_id})流式响应: {self.cut_str(result.readable_str())}') - if result.content is not None: - text_length += len(result.content) + if result.content is not None: + text_length += len(result.content) - yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) + yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) + else: + yield entities.StageProcessResult(result_type=entities.ResultType.INTERRUPT, + new_query=query) # for result in results: # # query.resp_messages.append(result) diff --git a/pkg/pipeline/respback/respback.py b/pkg/pipeline/respback/respback.py index 52714ce2..9a410b3f 100644 --- a/pkg/pipeline/respback/respback.py +++ b/pkg/pipeline/respback/respback.py @@ -46,7 +46,7 @@ class SendResponseBackStage(stage.PipelineStage): print(is_final) await query.adapter.reply_message_chunk( message_source=query.message_event, - message_id=query.message_event.message_chain.message_id, + message_id=query.resp_messages[-1].resp_message_id, message=query.resp_message_chain[-1], quote_origin=quote_origin, is_final=is_final, diff --git a/pkg/platform/adapter.py b/pkg/platform/adapter.py index 3951326c..d4b48ef6 100644 --- a/pkg/platform/adapter.py +++ b/pkg/platform/adapter.py @@ -80,6 +80,10 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): """ raise NotImplementedError + async def create_message_card(self,message_id,event): + '''创建卡片消息''' + return False + async def is_muted(self, group_id: int) -> bool: """获取账号是否在指定群被禁言""" raise NotImplementedError diff --git a/pkg/platform/sources/dingtalk.py b/pkg/platform/sources/dingtalk.py index f297019d..a3c91f41 100644 --- a/pkg/platform/sources/dingtalk.py +++ b/pkg/platform/sources/dingtalk.py @@ -154,19 +154,15 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): ) incoming_message = event.incoming_message - msg_id = incoming_message.message_id + # msg_id = incoming_message.message_id content, at = await DingTalkMessageConverter.yiri2target(message) - # is_stream = self.config['enable-stream-reply'] - # print(content) - card_template_id = self.config['card_template_id'] - if msg_id not in self.card_instance_id_dict: - card_instance,card_instance_id = await self.bot.create_and_card(card_template_id,incoming_message,at) - self.card_instance_id_dict[msg_id] = (card_instance,card_instance_id) - else: - card_instance,card_instance_id = self.card_instance_id_dict[msg_id] + + card_instance,card_instance_id = self.card_instance_id_dict[message_id] # print(card_instance_id) await self.bot.send_card_message(card_instance,card_instance_id,content,is_final) + if is_final: + self.card_instance_id_dict.pop(message_id) async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): @@ -180,15 +176,15 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): is_stream = False if self.config.get("enable-stream-reply", None): is_stream = True - self.is_stream = is_stream - return is_stream - async def create_message_card(self,message_id: str, incoming_message): + async def create_message_card(self,message_id,event): card_template_id = self.config['card_template_id'] - + incoming_message = event.incoming_message + # message_id = incoming_message.message_id card_instance, card_instance_id = await self.bot.create_and_card(card_template_id, incoming_message) self.card_instance_id_dict[message_id] = (card_instance, card_instance_id) + return True def register_listener( diff --git a/pkg/platform/sources/lark.py b/pkg/platform/sources/lark.py index 71c8045c..1e7f00b2 100644 --- a/pkg/platform/sources/lark.py +++ b/pkg/platform/sources/lark.py @@ -360,6 +360,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): self.message_id_to_card_id = {} self.card_id_dict = {} self.seq = 1 + self.card_id_time = {} @self.quart_app.route('/lark/callback', methods=['POST']) async def lark_callback(): @@ -405,101 +406,13 @@ class LarkAdapter(adapter.MessagePlatformAdapter): return {'code': 500, 'message': 'error'} - async def is_stream_output_supported() -> bool: - is_stream = False - if self.config.get("enable-stream-reply",None): - is_stream = True - self.is_stream = is_stream - return is_stream - - async def create_card_id(message_id): - try: - is_stream = await is_stream_output_supported() - if is_stream: - self.ap.logger.debug('飞书支持stream输出,创建卡片......') - # card_id = '' - # # if self.card_id_dict: - # # card_id = [k for k,v in self.card_id_dict.items() if (v+datetime.timedelta(days=14))< datetime.datetime.now()][0] - # - # if self.card_id_dict is None: - # # content = { - # # "type": "card_json", - # # "data": {"schema":"2.0","header":{"title":{"content":"bot","tag":"plain_text"}},"body":{"elements":[{"tag":"markdown","content":""}]}} - # # } - # card_data = {"schema":"2.0","header":{"title":{"content":"bot","tag":"plain_text"}}, - # "body":{"elements":[{"tag":"markdown","content":""}]},"config": {"streaming_mode": True, - # "streaming_config": {"print_strategy": "fast"}}} - # - # request: CreateCardRequest = CreateCardRequest.builder() \ - # .request_body( - # CreateCardRequestBody.builder() - # .type("card_json") - # .data(json.dumps(card_data)) \ - # .build() - # ).build() - # - # # 发起请求 - # response: CreateCardResponse = self.api_client.cardkit.v1.card.create(request) - # - # - # # 处理失败返回 - # if not response.success(): - # raise Exception( - # f"client.cardkit.v1.card.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}") - # - # self.ap.logger.debug(f'飞书卡片创建成功,卡片ID: {response.data.card_id}') - # self.card_id_dict[response.data.card_id] = datetime.datetime.now() - # - # card_id = response.data.card_id - card_data = {"schema": "2.0", "header": {"title": {"content": "bot", "tag": "plain_text"}}, - "body": {"elements": [{"tag": "markdown", "content": "[思考中.....]","element_id":"markdown_1"}]}, - "config": {"streaming_mode": True, - "streaming_config": {"print_strategy": "delay"}}} # delay / fast - request: CreateCardRequest = CreateCardRequest.builder() \ - .request_body( - CreateCardRequestBody.builder() - .type("card_json") - .data(json.dumps(card_data)) \ - .build() - ).build() - - # 发起请求 - response: CreateCardResponse = self.api_client.cardkit.v1.card.create(request) - - # 处理失败返回 - if not response.success(): - raise Exception( - f"client.cardkit.v1.card.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}") - - self.ap.logger.debug(f'飞书卡片创建成功,卡片ID: {response.data.card_id}') - self.card_id_dict[message_id] = response.data.card_id - - card_id = response.data.card_id - return card_id - - except Exception as e: - self.ap.logger.error(f'飞书卡片创建失败,错误信息: {e}') - async def on_message(event: lark_oapi.im.v1.P2ImMessageReceiveV1): - if await is_stream_output_supported(): - self.ap.logger.debug('卡片回复模式开启') - # 开启卡片回复模式. 这里可以实现飞书一发消息,马上创建卡片进行回复"思考中..." - card_id = await create_card_id(event.event.message.message_id) - reply_message_id = await self.create_message_card(card_id, event.event.message.message_id) - self.message_id_to_card_id[event.event.message.message_id] = (reply_message_id, time.time()) - - if len(self.message_id_to_card_id) > CARD_ID_CACHE_SIZE: - self.message_id_to_card_id = { - k: v - for k, v in self.message_id_to_card_id.items() - if v[1] > time.time() - CARD_ID_CACHE_MAX_LIFETIME - } lb_event = await self.event_converter.target2yiri(event, self.api_client) @@ -520,21 +433,64 @@ class LarkAdapter(adapter.MessagePlatformAdapter): async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): pass - async def create_message_card(self, card_id: str, message_id: str) -> str: + async def is_stream_output_supported(self) -> bool: + is_stream = False + if self.config.get("enable-stream-reply", None): + is_stream = True + return is_stream + + async def create_card_id(self,message_id): + try: + is_stream = await self.is_stream_output_supported() + if is_stream: + self.ap.logger.debug('飞书支持stream输出,创建卡片......') + + card_data = {"schema": "2.0", "header": {"title": {"content": "bot", "tag": "plain_text"}}, + "body": {"elements": [ + {"tag": "markdown", "content": "[思考中.....]", "element_id": "markdown_1"}]}, + "config": {"streaming_mode": True, + "streaming_config": {"print_strategy": "delay"}}} # delay / fast + + request: CreateCardRequest = CreateCardRequest.builder() \ + .request_body( + CreateCardRequestBody.builder() + .type("card_json") + .data(json.dumps(card_data)) \ + .build() + ).build() + + # 发起请求 + response: CreateCardResponse = self.api_client.cardkit.v1.card.create(request) + + # 处理失败返回 + if not response.success(): + raise Exception( + f"client.cardkit.v1.card.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}") + + self.ap.logger.debug(f'飞书卡片创建成功,卡片ID: {response.data.card_id}') + self.card_id_dict[message_id] = response.data.card_id + + card_id = response.data.card_id + return card_id + + except Exception as e: + self.ap.logger.error(f'飞书卡片创建失败,错误信息: {e}') + + async def create_message_card(self,message_id,event) -> str: """ 创建卡片消息。 使用卡片消息是因为普通消息更新次数有限制,而大模型流式返回结果可能很多而超过限制,而飞书卡片没有这个限制 """ + # message_id = event.message_chain.message_id - # TODO 目前只支持卡片模板方式,且卡片变量一定是content,未来这块要做成可配置 - # 发消息马上就会回复显示初始化的content信息,即思考中 + card_id = await self.create_card_id(message_id) content = { 'type': 'card', 'data': {'card_id': card_id, 'template_variable': {'content': 'Thinking...'}}, } request: ReplyMessageRequest = ( ReplyMessageRequest.builder() - .message_id(message_id) + .message_id(event.message_chain.message_id) .request_body( ReplyMessageRequestBody.builder().content(json.dumps(content)).msg_type('interactive').build() ) @@ -549,7 +505,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): raise Exception( f'client.im.v1.message.reply failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}' ) - return response.data.message_id + return True async def reply_message( self, @@ -633,6 +589,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): if is_final: self.seq = 1 + self.card_id_dict.pop(message_id) # 发起请求 response: ContentCardElementResponse = self.api_client.cardkit.v1.card_element.content(request) diff --git a/pkg/provider/entities.py b/pkg/provider/entities.py index e8037e68..df2b5487 100644 --- a/pkg/provider/entities.py +++ b/pkg/provider/entities.py @@ -127,6 +127,8 @@ class Message(pydantic.BaseModel): class MessageChunk(pydantic.BaseModel): """消息""" + resp_message_id: typing.Optional[str] = None + """消息id""" role: str # user, system, assistant, tool, command, plugin """消息的角色""" diff --git a/pkg/provider/runners/difysvapi.py b/pkg/provider/runners/difysvapi.py index 24318716..f0c36ca1 100644 --- a/pkg/provider/runners/difysvapi.py +++ b/pkg/provider/runners/difysvapi.py @@ -96,7 +96,7 @@ class DifyServiceAPIRunner(runner.RequestRunner): query.variables['conversation_id'] = cov_id try: - is_stream = query.adapter.is_stream + is_stream = await query.adapter.is_stream_output_supported() except AttributeError: is_stream = False @@ -209,7 +209,7 @@ class DifyServiceAPIRunner(runner.RequestRunner): query.variables['conversation_id'] = cov_id try: - is_stream = query.adapter.is_stream + is_stream = await query.adapter.is_stream_output_supported() except AttributeError: is_stream = False @@ -346,7 +346,7 @@ class DifyServiceAPIRunner(runner.RequestRunner): query.variables['conversation_id'] = query.session.using_conversation.uuid try: - is_stream = query.adapter.is_stream + is_stream = await query.adapter.is_stream_output_supported() except AttributeError: is_stream = False diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index 06b5d772..92522f27 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -25,7 +25,9 @@ class LocalAgentRunner(runner.RequestRunner): req_messages = query.prompt.messages.copy() + query.messages.copy() + [query.user_message] try: - is_stream = query.adapter.is_stream + # print(await query.adapter.is_stream_output_supported()) + is_stream = await query.adapter.is_stream_output_supported() + except AttributeError: is_stream = False # while True: From 63ec2a8c3490f46b31c639913ea6b2c5619c778f Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Mon, 21 Jul 2025 17:28:11 +0800 Subject: [PATCH 016/107] fix:lark message_id and dingtalk incoming_message --- pkg/pipeline/process/handlers/chat.py | 2 +- pkg/platform/sources/dingtalk.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/pipeline/process/handlers/chat.py b/pkg/pipeline/process/handlers/chat.py index 682be62d..c88d3ce1 100644 --- a/pkg/pipeline/process/handlers/chat.py +++ b/pkg/pipeline/process/handlers/chat.py @@ -74,7 +74,7 @@ class ChatMessageHandler(handler.MessageHandler): raise ValueError(f'未找到请求运行器: {query.pipeline_config["ai"]["runner"]["runner"]}') if is_stream: resp_message_id = uuid.uuid4() - if await query.adapter.create_message_card(resp_message_id,query.message_event.source_platform_object): + if await query.adapter.create_message_card(resp_message_id,query.message_event): async for result in runner.run(query): result.resp_message_id = resp_message_id if query.resp_messages: diff --git a/pkg/platform/sources/dingtalk.py b/pkg/platform/sources/dingtalk.py index a3c91f41..7de5975c 100644 --- a/pkg/platform/sources/dingtalk.py +++ b/pkg/platform/sources/dingtalk.py @@ -180,7 +180,7 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): async def create_message_card(self,message_id,event): card_template_id = self.config['card_template_id'] - incoming_message = event.incoming_message + incoming_message = event.source_platform_object.incoming_message # message_id = incoming_message.message_id card_instance, card_instance_id = await self.bot.create_and_card(card_template_id, incoming_message) self.card_instance_id_dict[message_id] = (card_instance, card_instance_id) From 8f8c8ff367499329c348b321a5cf4edc328017a9 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Mon, 21 Jul 2025 18:45:45 +0800 Subject: [PATCH 017/107] feat:add dashscopeapi stream fix:dify 64chunk yield --- pkg/provider/runners/dashscopeapi.py | 198 ++++++++++++++++++++------- pkg/provider/runners/difysvapi.py | 2 +- 2 files changed, 147 insertions(+), 53 deletions(-) diff --git a/pkg/provider/runners/dashscopeapi.py b/pkg/provider/runners/dashscopeapi.py index 02cb0b51..fe72b0a8 100644 --- a/pkg/provider/runners/dashscopeapi.py +++ b/pkg/provider/runners/dashscopeapi.py @@ -113,39 +113,84 @@ class DashScopeAPIRunner(runner.RequestRunner): # "session_file_ids": ["FILE_ID1"], # FILE_ID1 替换为实际的临时文件ID,逗号隔开多个 # } ) + idx_chunk = 0 + try: + # print(await query.adapter.is_stream_output_supported()) + is_stream = await query.adapter.is_stream_output_supported() - for chunk in response: - if chunk.get('status_code') != 200: - raise DashscopeAPIError( - f'Dashscope API 请求失败: status_code={chunk.get("status_code")} message={chunk.get("message")} request_id={chunk.get("request_id")} ' - ) - if not chunk: - continue + except AttributeError: + is_stream = False + if is_stream: + for chunk in response: + if chunk.get('status_code') != 200: + raise DashscopeAPIError( + f'Dashscope API 请求失败: status_code={chunk.get("status_code")} message={chunk.get("message")} request_id={chunk.get("request_id")} ' + ) + if not chunk: + continue + idx_chunk += 1 + # 获取流式传输的output + stream_output = chunk.get('output', {}) + if stream_output.get('text') is not None: + pending_content += stream_output.get('text') + # 是否是流式最后一个chunk + is_final = False if stream_output.get('finish_reason', False) == 'null' else True - # 获取流式传输的output - stream_output = chunk.get('output', {}) - if stream_output.get('text') is not None: - pending_content += stream_output.get('text') + # 获取模型传出的参考资料列表 + references_dict_list = stream_output.get('doc_references', []) - # 保存当前会话的session_id用于下次对话的语境 - query.session.using_conversation.uuid = stream_output.get('session_id') + # 从模型传出的参考资料信息中提取用于替换的字典 + if references_dict_list is not None: + for doc in references_dict_list: + if doc.get('index_id') is not None: + references_dict[doc.get('index_id')] = doc.get('doc_name') - # 获取模型传出的参考资料列表 - references_dict_list = stream_output.get('doc_references', []) + # 将参考资料替换到文本中 + pending_content = self._replace_references(pending_content, references_dict) - # 从模型传出的参考资料信息中提取用于替换的字典 - if references_dict_list is not None: - for doc in references_dict_list: - if doc.get('index_id') is not None: - references_dict[doc.get('index_id')] = doc.get('doc_name') + if idx_chunk % 64 == 0 or is_final: + yield llm_entities.MessageChunk( + role='assistant', + content=pending_content, + is_final=is_final, + ) + # 保存当前会话的session_id用于下次对话的语境 + query.session.using_conversation.uuid = stream_output.get('session_id') + else: + for chunk in response: + if chunk.get('status_code') != 200: + raise DashscopeAPIError( + f'Dashscope API 请求失败: status_code={chunk.get("status_code")} message={chunk.get("message")} request_id={chunk.get("request_id")} ' + ) + if not chunk: + continue + idx_chunk += 1 + # 获取流式传输的output + stream_output = chunk.get('output', {}) + if stream_output.get('text') is not None: + pending_content += stream_output.get('text') - # 将参考资料替换到文本中 - pending_content = self._replace_references(pending_content, references_dict) + # 保存当前会话的session_id用于下次对话的语境 + query.session.using_conversation.uuid = stream_output.get('session_id') - yield llm_entities.Message( - role='assistant', - content=pending_content, - ) + # 获取模型传出的参考资料列表 + references_dict_list = stream_output.get('doc_references', []) + + # 从模型传出的参考资料信息中提取用于替换的字典 + if references_dict_list is not None: + for doc in references_dict_list: + if doc.get('index_id') is not None: + references_dict[doc.get('index_id')] = doc.get('doc_name') + + # 将参考资料替换到文本中 + pending_content = self._replace_references(pending_content, references_dict) + + + + yield llm_entities.Message( + role='assistant', + content=pending_content, + ) async def _workflow_messages(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: """Dashscope 工作流对话请求""" @@ -177,38 +222,87 @@ class DashScopeAPIRunner(runner.RequestRunner): ) # 处理API返回的流式输出 - for chunk in response: - if chunk.get('status_code') != 200: - raise DashscopeAPIError( - f'Dashscope API 请求失败: status_code={chunk.get("status_code")} message={chunk.get("message")} request_id={chunk.get("request_id")} ' - ) - if not chunk: - continue + try: + # print(await query.adapter.is_stream_output_supported()) + is_stream = await query.adapter.is_stream_output_supported() - # 获取流式传输的output - stream_output = chunk.get('output', {}) - if stream_output.get('text') is not None: - pending_content += stream_output.get('text') + except AttributeError: + is_stream = False + idx_chunk = 0 + if is_stream: + for chunk in response: + if chunk.get('status_code') != 200: + raise DashscopeAPIError( + f'Dashscope API 请求失败: status_code={chunk.get("status_code")} message={chunk.get("message")} request_id={chunk.get("request_id")} ' + ) + if not chunk: + continue + idx_chunk += 1 + # 获取流式传输的output + stream_output = chunk.get('output', {}) + if stream_output.get('text') is not None: + pending_content += stream_output.get('text') - # 保存当前会话的session_id用于下次对话的语境 - query.session.using_conversation.uuid = stream_output.get('session_id') + is_final = False if stream_output.get('finish_reason', False) == 'null' else True - # 获取模型传出的参考资料列表 - references_dict_list = stream_output.get('doc_references', []) + # 获取模型传出的参考资料列表 + references_dict_list = stream_output.get('doc_references', []) - # 从模型传出的参考资料信息中提取用于替换的字典 - if references_dict_list is not None: - for doc in references_dict_list: - if doc.get('index_id') is not None: - references_dict[doc.get('index_id')] = doc.get('doc_name') + # 从模型传出的参考资料信息中提取用于替换的字典 + if references_dict_list is not None: + for doc in references_dict_list: + if doc.get('index_id') is not None: + references_dict[doc.get('index_id')] = doc.get('doc_name') - # 将参考资料替换到文本中 - pending_content = self._replace_references(pending_content, references_dict) + # 将参考资料替换到文本中 + pending_content = self._replace_references(pending_content, references_dict) + if is_final: + yield llm_entities.MessageChunk( + role='assistant', + content=pending_content, + is_final=is_final, - yield llm_entities.Message( - role='assistant', - content=pending_content, - ) + ) + + # 保存当前会话的session_id用于下次对话的语境 + query.session.using_conversation.uuid = stream_output.get('session_id') + + + else: + for chunk in response: + if chunk.get('status_code') != 200: + raise DashscopeAPIError( + f'Dashscope API 请求失败: status_code={chunk.get("status_code")} message={chunk.get("message")} request_id={chunk.get("request_id")} ' + ) + if not chunk: + continue + + # 获取流式传输的output + stream_output = chunk.get('output', {}) + if stream_output.get('text') is not None: + pending_content += stream_output.get('text') + + is_final = False if stream_output.get('finish_reason', False) == 'null' else True + + # 保存当前会话的session_id用于下次对话的语境 + query.session.using_conversation.uuid = stream_output.get('session_id') + + # 获取模型传出的参考资料列表 + references_dict_list = stream_output.get('doc_references', []) + + # 从模型传出的参考资料信息中提取用于替换的字典 + if references_dict_list is not None: + for doc in references_dict_list: + if doc.get('index_id') is not None: + references_dict[doc.get('index_id')] = doc.get('doc_name') + + # 将参考资料替换到文本中 + pending_content = self._replace_references(pending_content, references_dict) + + yield llm_entities.Message( + role='assistant', + content=pending_content, + ) async def run(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: """运行""" diff --git a/pkg/provider/runners/difysvapi.py b/pkg/provider/runners/difysvapi.py index f0c36ca1..7c7d81ad 100644 --- a/pkg/provider/runners/difysvapi.py +++ b/pkg/provider/runners/difysvapi.py @@ -259,7 +259,7 @@ class DifyServiceAPIRunner(runner.RequestRunner): is_final = False pending_agent_message += chunk['answer'] if is_stream: - if batch_pending_index % 32 == 0 or is_final: + if batch_pending_index % 64 == 0 or is_final: yield llm_entities.MessageChunk( role='assistant', content=self._try_convert_thinking(pending_agent_message), From 307f6acd8c1752a3cda2c480e6d505ac783915a0 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Tue, 29 Jul 2025 23:09:02 +0800 Subject: [PATCH 018/107] fix:del some print ,and amend respback on stream judge ,and del in dingtalk this is_stream_output_supported() use --- pkg/pipeline/process/handlers/chat.py | 35 ++------- pkg/pipeline/respback/respback.py | 12 +-- pkg/platform/adapter.py | 18 +++-- pkg/platform/sources/dingtalk.py | 23 ++---- pkg/platform/sources/lark.py | 77 ++++++++----------- pkg/platform/sources/telegram.py | 13 +--- pkg/provider/modelmgr/requesters/chatcmpl.py | 49 ++++-------- .../modelmgr/requesters/deepseekchatcmpl.py | 2 +- pkg/provider/runners/dashscopeapi.py | 4 - pkg/provider/runners/difysvapi.py | 2 - pkg/provider/runners/localagent.py | 54 ++++++------- 11 files changed, 101 insertions(+), 188 deletions(-) diff --git a/pkg/pipeline/process/handlers/chat.py b/pkg/pipeline/process/handlers/chat.py index c88d3ce1..4b190370 100644 --- a/pkg/pipeline/process/handlers/chat.py +++ b/pkg/pipeline/process/handlers/chat.py @@ -63,7 +63,6 @@ class ChatMessageHandler(handler.MessageHandler): is_stream = await query.adapter.is_stream_output_supported() except AttributeError: is_stream = False - print(is_stream) try: for r in runner_module.preregistered_runners: @@ -74,7 +73,7 @@ class ChatMessageHandler(handler.MessageHandler): raise ValueError(f'未找到请求运行器: {query.pipeline_config["ai"]["runner"]["runner"]}') if is_stream: resp_message_id = uuid.uuid4() - if await query.adapter.create_message_card(resp_message_id,query.message_event): + if await query.adapter.create_message_card(resp_message_id, query.message_event): async for result in runner.run(query): result.resp_message_id = resp_message_id if query.resp_messages: @@ -82,43 +81,21 @@ class ChatMessageHandler(handler.MessageHandler): if query.resp_message_chain: query.resp_message_chain.pop() - query.resp_messages.append(result) - self.ap.logger.info(f'对话({query.query_id})流式响应: {self.cut_str(result.readable_str())}') + self.ap.logger.info( + f'对话({query.query_id})流式响应: {self.cut_str(result.readable_str())}' + ) if result.content is not None: text_length += len(result.content) yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) else: - yield entities.StageProcessResult(result_type=entities.ResultType.INTERRUPT, - new_query=query) - # for result in results: - # - # query.resp_messages.append(result) - # print(result) - # - # self.ap.logger.info(f'对话({query.query_id})流式响应: {self.cut_str(result.content)}') - # - # if result.content is not None: - # text_length += len(result.content) - # - # # current_chain = platform_message.MessageChain([]) - # # for msg in accumulated_messages: - # # if msg.content is not None: - # # current_chain.append(platform_message.Plain(msg.content)) - # # query.resp_message_chain = [current_chain] - # - # yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) - # query.resp_messages.append(results) - # self.ap.logger.info(f'对话({query.query_id})响应') - # yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) + yield entities.StageProcessResult(result_type=entities.ResultType.INTERRUPT, new_query=query) else: - print("非流式") async for result in runner.run(query): query.resp_messages.append(result) - print(result) self.ap.logger.info(f'对话({query.query_id})响应: {self.cut_str(result.readable_str())}') @@ -128,7 +105,7 @@ class ChatMessageHandler(handler.MessageHandler): yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) query.session.using_conversation.messages.append(query.user_message) - + query.session.using_conversation.messages.extend(query.resp_messages) except Exception as e: self.ap.logger.error(f'对话({query.query_id})请求失败: {type(e).__name__} {str(e)}') diff --git a/pkg/pipeline/respback/respback.py b/pkg/pipeline/respback/respback.py index 9a410b3f..f4153218 100644 --- a/pkg/pipeline/respback/respback.py +++ b/pkg/pipeline/respback/respback.py @@ -39,11 +39,9 @@ class SendResponseBackStage(stage.PipelineStage): quote_origin = query.pipeline_config['output']['misc']['quote-origin'] - has_chunks = any(isinstance(msg, llm_entities.MessageChunk) for msg in query.resp_messages) - print(has_chunks) - if has_chunks and hasattr(query.adapter,'reply_message_chunk'): + # has_chunks = any(isinstance(msg, llm_entities.MessageChunk) for msg in query.resp_messages) + if await query.adapter.is_stream_output_supported(): is_final = [msg.is_final for msg in query.resp_messages][0] - print(is_final) await query.adapter.reply_message_chunk( message_source=query.message_event, message_id=query.resp_messages[-1].resp_message_id, @@ -58,10 +56,6 @@ class SendResponseBackStage(stage.PipelineStage): quote_origin=quote_origin, ) - # await query.adapter.reply_message( - # message_source=query.message_event, - # message=query.resp_message_chain[-1], - # quote_origin=quote_origin, - # ) + return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) diff --git a/pkg/platform/adapter.py b/pkg/platform/adapter.py index d4b48ef6..e4369efb 100644 --- a/pkg/platform/adapter.py +++ b/pkg/platform/adapter.py @@ -25,7 +25,6 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): logger: EventLogger - is_stream: bool def __init__(self, config: dict, ap: app.Application, logger: EventLogger): """初始化适配器 @@ -62,26 +61,31 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): quote_origin (bool, optional): 是否引用原消息. Defaults to False. """ raise NotImplementedError - + async def reply_message_chunk( self, - message_source: platform_events.MessageEvent, + message_source: platform_events.MessageEvent, message_id: int, message: platform_message.MessageChain, quote_origin: bool = False, is_final: bool = False, - ): + ): """回复消息(流式输出) Args: message_source (platform.types.MessageEvent): 消息源事件 message_id (int): 消息ID message (platform.types.MessageChain): 消息链 quote_origin (bool, optional): 是否引用原消息. Defaults to False. + is_final (bool, optional): 流式是否结束. Defaults to False. """ raise NotImplementedError - async def create_message_card(self,message_id,event): - '''创建卡片消息''' + async def create_message_card(self, message_id:typing.Type[str,int], event:platform_events.MessageEvent) -> bool: + """创建卡片消息 + Args: + message_id (str): 消息ID + event (platform_events.MessageEvent): 消息源事件 + """ return False async def is_muted(self, group_id: int) -> bool: @@ -117,11 +121,9 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): async def run_async(self): """异步运行""" raise NotImplementedError - async def is_stream_output_supported(self) -> bool: """是否支持流式输出""" - self.is_stream = False return False async def kill(self) -> bool: diff --git a/pkg/platform/sources/dingtalk.py b/pkg/platform/sources/dingtalk.py index 7de5975c..187bafb0 100644 --- a/pkg/platform/sources/dingtalk.py +++ b/pkg/platform/sources/dingtalk.py @@ -148,7 +148,7 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): message: platform_message.MessageChain, quote_origin: bool = False, is_final: bool = False, - ): + ): event = await DingTalkEventConverter.yiri2target( message_source, ) @@ -158,13 +158,12 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): content, at = await DingTalkMessageConverter.yiri2target(message) - card_instance,card_instance_id = self.card_instance_id_dict[message_id] + card_instance, card_instance_id = self.card_instance_id_dict[message_id] # print(card_instance_id) - await self.bot.send_card_message(card_instance,card_instance_id,content,is_final) + await self.bot.send_card_message(card_instance, card_instance_id, content, is_final) if is_final: self.card_instance_id_dict.pop(message_id) - async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): content = await DingTalkMessageConverter.yiri2target(message) if target_type == 'person': @@ -174,11 +173,11 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): async def is_stream_output_supported(self) -> bool: is_stream = False - if self.config.get("enable-stream-reply", None): + if self.config.get('enable-stream-reply', None): is_stream = True return is_stream - async def create_message_card(self,message_id,event): + async def create_message_card(self, message_id, event): card_template_id = self.config['card_template_id'] incoming_message = event.source_platform_object.incoming_message # message_id = incoming_message.message_id @@ -186,7 +185,6 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): self.card_instance_id_dict[message_id] = (card_instance, card_instance_id) return True - def register_listener( self, event_type: typing.Type[platform_events.Event], @@ -194,7 +192,6 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): ): async def on_message(event: DingTalkEvent): try: - await self.is_stream_output_supported() return await callback( await self.event_converter.target2yiri(event, self.config['robot_name']), self, @@ -208,15 +205,7 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): self.bot.on_message('GroupMessage')(on_message) async def run_async(self): - # config = self.config - # self.bot = DingTalkClient( - # client_id=config['client_id'], - # client_secret=config['client_secret'], - # robot_name=config['robot_name'], - # robot_code=config['robot_code'], - # markdown_card=config['markdown_card'], - # logger=self.logger, - # ) + await self.bot.start() async def kill(self) -> bool: diff --git a/pkg/platform/sources/lark.py b/pkg/platform/sources/lark.py index 1e7f00b2..dcafbf9f 100644 --- a/pkg/platform/sources/lark.py +++ b/pkg/platform/sources/lark.py @@ -344,7 +344,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): config: dict quart_app: quart.Quart ap: app.Application - + message_id_to_card_id: typing.Dict[str, typing.Tuple[str, int]] card_id_dict: dict[str, str] @@ -395,25 +395,17 @@ class LarkAdapter(adapter.MessagePlatformAdapter): try: event = await self.event_converter.target2yiri(p2v1, self.api_client) except Exception as e: - await self.logger.error(f"Error in lark callback: {traceback.format_exc()}") + await self.logger.error(f'Error in lark callback: {traceback.format_exc()}') if event.__class__ in self.listeners: await self.listeners[event.__class__](event, self) return {'code': 200, 'message': 'ok'} except Exception as e: - await self.logger.error(f"Error in lark callback: {traceback.format_exc()}") + await self.logger.error(f'Error in lark callback: {traceback.format_exc()}') return {'code': 500, 'message': 'error'} - - - - - - - async def on_message(event: lark_oapi.im.v1.P2ImMessageReceiveV1): - lb_event = await self.event_converter.target2yiri(event, self.api_client) await self.listeners[type(lb_event)](lb_event, self) @@ -435,29 +427,28 @@ class LarkAdapter(adapter.MessagePlatformAdapter): async def is_stream_output_supported(self) -> bool: is_stream = False - if self.config.get("enable-stream-reply", None): + if self.config.get('enable-stream-reply', None): is_stream = True return is_stream - async def create_card_id(self,message_id): + async def create_card_id(self, message_id): try: is_stream = await self.is_stream_output_supported() if is_stream: self.ap.logger.debug('飞书支持stream输出,创建卡片......') - card_data = {"schema": "2.0", "header": {"title": {"content": "bot", "tag": "plain_text"}}, - "body": {"elements": [ - {"tag": "markdown", "content": "[思考中.....]", "element_id": "markdown_1"}]}, - "config": {"streaming_mode": True, - "streaming_config": {"print_strategy": "delay"}}} # delay / fast + card_data = { + 'schema': '2.0', + 'header': {'title': {'content': 'bot', 'tag': 'plain_text'}}, + 'body': {'elements': [{'tag': 'markdown', 'content': '[思考中.....]', 'element_id': 'markdown_1'}]}, + 'config': {'streaming_mode': True, 'streaming_config': {'print_strategy': 'delay'}}, + } # delay / fast - request: CreateCardRequest = CreateCardRequest.builder() \ - .request_body( - CreateCardRequestBody.builder() - .type("card_json") - .data(json.dumps(card_data)) \ + request: CreateCardRequest = ( + CreateCardRequest.builder() + .request_body(CreateCardRequestBody.builder().type('card_json').data(json.dumps(card_data)).build()) .build() - ).build() + ) # 发起请求 response: CreateCardResponse = self.api_client.cardkit.v1.card.create(request) @@ -465,7 +456,8 @@ class LarkAdapter(adapter.MessagePlatformAdapter): # 处理失败返回 if not response.success(): raise Exception( - f"client.cardkit.v1.card.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}") + f'client.cardkit.v1.card.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}' + ) self.ap.logger.debug(f'飞书卡片创建成功,卡片ID: {response.data.card_id}') self.card_id_dict[message_id] = response.data.card_id @@ -476,7 +468,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): except Exception as e: self.ap.logger.error(f'飞书卡片创建失败,错误信息: {e}') - async def create_message_card(self,message_id,event) -> str: + async def create_message_card(self, message_id, event) -> str: """ 创建卡片消息。 使用卡片消息是因为普通消息更新次数有限制,而大模型流式返回结果可能很多而超过限制,而飞书卡片没有这个限制 @@ -545,7 +537,6 @@ class LarkAdapter(adapter.MessagePlatformAdapter): f'client.im.v1.message.reply failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}' ) - async def reply_message_chunk( self, message_source: platform_events.MessageEvent, @@ -559,33 +550,33 @@ class LarkAdapter(adapter.MessagePlatformAdapter): """ lark_message = await self.message_converter.yiri2target(message, self.api_client) - self.seq += 1 - - text_message = '' for ele in lark_message[0]: if ele['tag'] == 'text': text_message += ele['text'] elif ele['tag'] == 'md': text_message += ele['text'] - print(text_message) content = { 'type': 'card_json', 'data': {'card_id': self.card_id_dict[message_id], 'elements': {'content': text_message}}, } - request: ContentCardElementRequest = ContentCardElementRequest.builder() \ - .card_id(self.card_id_dict[message_id]) \ - .element_id("markdown_1") \ - .request_body(ContentCardElementRequestBody.builder() - # .uuid("a0d69e20-1dd1-458b-k525-dfeca4015204") - .content(text_message) - .sequence(self.seq) - .build()) \ + request: ContentCardElementRequest = ( + ContentCardElementRequest.builder() + .card_id(self.card_id_dict[message_id]) + .element_id('markdown_1') + .request_body( + ContentCardElementRequestBody.builder() + # .uuid("a0d69e20-1dd1-458b-k525-dfeca4015204") + .content(text_message) + .sequence(self.seq) + .build() + ) .build() + ) if is_final: self.seq = 1 @@ -593,7 +584,6 @@ class LarkAdapter(adapter.MessagePlatformAdapter): # 发起请求 response: ContentCardElementResponse = self.api_client.cardkit.v1.card_element.content(request) - # 处理失败返回 if not response.success(): raise Exception( @@ -601,13 +591,6 @@ class LarkAdapter(adapter.MessagePlatformAdapter): ) return - - - - - - - async def is_muted(self, group_id: int) -> bool: return False diff --git a/pkg/platform/sources/telegram.py b/pkg/platform/sources/telegram.py index 363ad702..e021c7b7 100644 --- a/pkg/platform/sources/telegram.py +++ b/pkg/platform/sources/telegram.py @@ -167,7 +167,7 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): await self.listeners[type(lb_event)](lb_event, self) await self.is_stream_output_supported() except Exception as e: - await self.logger.error(f"Error in telegram callback: {traceback.format_exc()}") + await self.logger.error(f'Error in telegram callback: {traceback.format_exc()}') self.application = ApplicationBuilder().token(self.config['token']).build() self.bot = self.application.bot @@ -206,7 +206,6 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): await self.bot.send_message(**args) - async def reply_message_chunk( self, message_source: platform_events.MessageEvent, @@ -214,8 +213,7 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): message: platform_message.MessageChain, quote_origin: bool = False, is_final: bool = False, - ): - + ): assert isinstance(message_source.source_platform_object, Update) components = await TelegramMessageConverter.yiri2target(message, self.bot) args = {} @@ -240,7 +238,6 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): if self.config['markdown_card'] is True: args['parse_mode'] = 'MarkdownV2' - send_msg = await self.bot.send_message(**args) send_msg_id = send_msg.message_id self.msg_stream_id[message_id] = send_msg_id @@ -264,16 +261,12 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): if is_final: self.msg_stream_id.pop(message_id) - async def is_stream_output_supported(self) -> bool: is_stream = False - if self.config.get("enable-stream-reply", None): + if self.config.get('enable-stream-reply', None): is_stream = True - self.is_stream = is_stream - return is_stream - async def is_muted(self, group_id: int) -> bool: return False diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index f30bfd4e..6e72d78e 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -17,14 +17,13 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): """OpenAI ChatCompletion API 请求器""" client: openai.AsyncClient - is_content:bool + is_content: bool default_config: dict[str, typing.Any] = { 'base_url': 'https://api.openai.com/v1', 'timeout': 120, } - async def initialize(self): self.client = openai.AsyncClient( api_key='', @@ -46,7 +45,6 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): args: dict, extra_body: dict = {}, ) -> chat_completion.ChatCompletion: - async for chunk in await self.client.chat.completions.create(**args, extra_body=extra_body): yield chunk @@ -66,23 +64,23 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): # deepseek的reasoner模型 if pipeline_config['trigger'].get('misc', '').get('remove_think'): - pass else: - if reasoning_content is not None : - chatcmpl_message['content'] = '\n' + reasoning_content + '\n\n' + chatcmpl_message['content'] + if reasoning_content is not None: + chatcmpl_message['content'] = ( + '\n' + reasoning_content + '\n\n' + chatcmpl_message['content'] + ) message = llm_entities.Message(**chatcmpl_message) return message - + async def _make_msg_chunk( self, pipeline_config: dict[str, typing.Any], chat_completion: chat_completion.ChatCompletion, idx: int, ) -> llm_entities.MessageChunk: - # 处理流式chunk和完整响应的差异 # print(chat_completion.choices[0]) if hasattr(chat_completion, 'choices'): @@ -98,7 +96,6 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): if 'role' not in delta or delta['role'] is None: delta['role'] = 'assistant' - reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None delta['content'] = '' if delta['content'] is None else delta['content'] @@ -106,13 +103,13 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): # deepseek的reasoner模型 if pipeline_config['trigger'].get('misc', '').get('remove_think'): - if reasoning_content is not None : + if reasoning_content is not None: pass else: delta['content'] = delta['content'] else: if reasoning_content is not None and idx == 0: - delta['content'] += f'\n{reasoning_content}' + delta['content'] += f'\n{reasoning_content}' elif reasoning_content is None: if self.is_content: delta['content'] = delta['content'] @@ -122,7 +119,6 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): else: delta['content'] += reasoning_content - message = llm_entities.MessageChunk(**delta) return message @@ -135,9 +131,10 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): use_funcs: list[tools_entities.LLMFunction] = None, stream: bool = False, extra_args: dict[str, typing.Any] = {}, - ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: + ) ->llm_entities.MessageChunk: self.client.api_key = use_model.token_mgr.get_token() + args = {} args['model'] = use_model.model_entity.name @@ -163,14 +160,14 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): if stream: current_content = '' - args["stream"] = True + args['stream'] = True chunk_idx = 0 self.is_content = False tool_calls_map: dict[str, llm_entities.ToolCall] = {} pipeline_config = query.pipeline_config async for chunk in self._req_stream(args, extra_body=extra_args): # 处理流式消息 - delta_message = await self._make_msg_chunk(pipeline_config,chunk,chunk_idx) + delta_message = await self._make_msg_chunk(pipeline_config, chunk, chunk_idx) if delta_message.content: current_content += delta_message.content delta_message.content = current_content @@ -182,15 +179,13 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): id=tool_call.id, type=tool_call.type, function=llm_entities.FunctionCall( - name=tool_call.function.name if tool_call.function else '', - arguments='' + name=tool_call.function.name if tool_call.function else '', arguments='' ), ) if tool_call.function and tool_call.function.arguments: # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments - chunk_idx += 1 chunk_choices = getattr(chunk, 'choices', None) if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): @@ -198,11 +193,9 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): delta_message.content = current_content if chunk_idx % 64 == 0 or delta_message.is_final: - yield delta_message # return - async def _closure( self, query: core_entities.Query, @@ -211,7 +204,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): use_funcs: list[tools_entities.LLMFunction] = None, stream: bool = False, extra_args: dict[str, typing.Any] = {}, - ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: + ) -> llm_entities.Message: self.client.api_key = use_model.token_mgr.get_token() args = {} @@ -237,22 +230,15 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): args['messages'] = messages - - # 发送请求 resp = await self._req(args, extra_body=extra_args) # 处理请求结果 pipeline_config = query.pipeline_config - message = await self._make_msg(resp,pipeline_config) - + message = await self._make_msg(resp, pipeline_config) return message - - - - async def invoke_llm( self, query: core_entities.Query, @@ -273,7 +259,6 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): req_messages.append(msg_dict) try: - msg = await self._closure( query=query, req_messages=req_messages, @@ -306,7 +291,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): funcs: typing.List[tools_entities.LLMFunction] = None, stream: bool = False, extra_args: dict[str, typing.Any] = {}, - ) -> llm_entities.MessageChunk: + ) -> llm_entities.MessageChunk: req_messages = [] # req_messages 仅用于类内,外部同步由 query.messages 进行 for m in messages: msg_dict = m.dict(exclude_none=True) @@ -343,4 +328,4 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): except openai.RateLimitError as e: raise errors.RequesterError(f'请求过于频繁或余额不足: {e.message}') except openai.APIError as e: - raise errors.RequesterError(f'请求错误: {e.message}') \ No newline at end of file + raise errors.RequesterError(f'请求错误: {e.message}') diff --git a/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py b/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py index f57f624f..d75d0fb6 100644 --- a/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py +++ b/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py @@ -55,6 +55,6 @@ class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions): raise errors.RequesterError('接口返回为空,请确定模型提供商服务是否正常') pipeline_config = query.pipeline_config # 处理请求结果 - message = await self._make_msg(resp,pipeline_config) + message = await self._make_msg(resp, pipeline_config) return message diff --git a/pkg/provider/runners/dashscopeapi.py b/pkg/provider/runners/dashscopeapi.py index fe72b0a8..9bb5824c 100644 --- a/pkg/provider/runners/dashscopeapi.py +++ b/pkg/provider/runners/dashscopeapi.py @@ -185,8 +185,6 @@ class DashScopeAPIRunner(runner.RequestRunner): # 将参考资料替换到文本中 pending_content = self._replace_references(pending_content, references_dict) - - yield llm_entities.Message( role='assistant', content=pending_content, @@ -261,13 +259,11 @@ class DashScopeAPIRunner(runner.RequestRunner): role='assistant', content=pending_content, is_final=is_final, - ) # 保存当前会话的session_id用于下次对话的语境 query.session.using_conversation.uuid = stream_output.get('session_id') - else: for chunk in response: if chunk.get('status_code') != 200: diff --git a/pkg/provider/runners/difysvapi.py b/pkg/provider/runners/difysvapi.py index 7c7d81ad..8182cc54 100644 --- a/pkg/provider/runners/difysvapi.py +++ b/pkg/provider/runners/difysvapi.py @@ -148,7 +148,6 @@ class DifyServiceAPIRunner(runner.RequestRunner): if mode == 'workflow': if chunk['event'] == 'node_finished': if not is_stream: - if chunk['data']['node_type'] == 'answer': yield llm_entities.Message( role='assistant', @@ -274,7 +273,6 @@ class DifyServiceAPIRunner(runner.RequestRunner): content=self._try_convert_thinking(pending_agent_message), ) - if chunk['event'] == 'agent_thought': if chunk['tool'] != '' and chunk['observation'] != '': # 工具调用结果,跳过 continue diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index 92522f27..b70d4157 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -1,7 +1,6 @@ from __future__ import annotations import json -from ssl import ALERT_DESCRIPTION_BAD_CERTIFICATE_HASH_VALUE import typing from .. import runner @@ -15,26 +14,27 @@ class LocalAgentRunner(runner.RequestRunner): class ToolCallTracker: """工具调用追踪器""" + def __init__(self): - self.active_calls: dict[str,dict] = {} + self.active_calls: dict[str, dict] = {} self.completed_calls: list[llm_entities.ToolCall] = [] - async def run(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message | llm_entities.MessageChunk, None]: + async def run( + self, query: core_entities.Query + ) -> typing.AsyncGenerator[llm_entities.Message | llm_entities.MessageChunk, None]: """运行请求""" pending_tool_calls = [] req_messages = query.prompt.messages.copy() + query.messages.copy() + [query.user_message] try: - # print(await query.adapter.is_stream_output_supported()) is_stream = await query.adapter.is_stream_output_supported() except AttributeError: is_stream = False - # while True: - # pass + if not is_stream: # 非流式输出,直接请求 - # print(123) + msg = await query.use_llm_model.requester.invoke_llm( query, query.use_llm_model, @@ -44,7 +44,6 @@ class LocalAgentRunner(runner.RequestRunner): ) yield msg final_msg = msg - print(final_msg) else: # 流式输出,需要处理工具调用 tool_calls_map: dict[str, llm_entities.ToolCall] = {} @@ -55,29 +54,28 @@ class LocalAgentRunner(runner.RequestRunner): query.use_funcs, stream=is_stream, extra_args=query.use_llm_model.model_entity.extra_args, - ): + ): yield msg - # if msg.tool_calls: - # for tool_call in msg.tool_calls: - # if tool_call.id not in tool_calls_map: - # tool_calls_map[tool_call.id] = llm_entities.ToolCall( - # id=tool_call.id, - # type=tool_call.type, - # function=llm_entities.FunctionCall( - # name=tool_call.function.name if tool_call.function else '', - # arguments='' - # ), - # ) - # if tool_call.function and tool_call.function.arguments: - # # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 - # tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + if msg.tool_calls: + for tool_call in msg.tool_calls: + if tool_call.id not in tool_calls_map: + tool_calls_map[tool_call.id] = llm_entities.ToolCall( + id=tool_call.id, + type=tool_call.type, + function=llm_entities.FunctionCall( + name=tool_call.function.name if tool_call.function else '', + arguments='' + ), + ) + if tool_call.function and tool_call.function.arguments: + # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 + tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments final_msg = llm_entities.Message( role=msg.role, content=msg.all_content, tool_calls=list(tool_calls_map.values()), ) - pending_tool_calls = final_msg.tool_calls req_messages.append(final_msg) @@ -117,8 +115,8 @@ class LocalAgentRunner(runner.RequestRunner): req_messages, query.use_funcs, stream=is_stream, - extra_args=query.use_llm_model.model_entity.extra_args, - ): + extra_args=query.use_llm_model.model_entity.extra_args, + ): yield msg if msg.tool_calls: for tool_call in msg.tool_calls: @@ -127,8 +125,7 @@ class LocalAgentRunner(runner.RequestRunner): id=tool_call.id, type=tool_call.type, function=llm_entities.FunctionCall( - name=tool_call.function.name if tool_call.function else '', - arguments='' + name=tool_call.function.name if tool_call.function else '', arguments='' ), ) if tool_call.function and tool_call.function.arguments: @@ -140,7 +137,6 @@ class LocalAgentRunner(runner.RequestRunner): tool_calls=list(tool_calls_map.values()), ) else: - print("非流式") # 处理完所有调用,再次请求 msg = await query.use_llm_model.requester.invoke_llm( query, From 3291266f5de9fdb30d4d13b17a748c233a98547c Mon Sep 17 00:00:00 2001 From: fdc <2213070223@qq.com> Date: Wed, 30 Jul 2025 15:21:59 +0800 Subject: [PATCH 019/107] fix:in chat judge create_message_card telegram reply_message_chunk no message --- pkg/pipeline/process/handlers/chat.py | 32 +++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/pkg/pipeline/process/handlers/chat.py b/pkg/pipeline/process/handlers/chat.py index 4b190370..483dd0b7 100644 --- a/pkg/pipeline/process/handlers/chat.py +++ b/pkg/pipeline/process/handlers/chat.py @@ -73,25 +73,25 @@ class ChatMessageHandler(handler.MessageHandler): raise ValueError(f'未找到请求运行器: {query.pipeline_config["ai"]["runner"]["runner"]}') if is_stream: resp_message_id = uuid.uuid4() - if await query.adapter.create_message_card(resp_message_id, query.message_event): - async for result in runner.run(query): - result.resp_message_id = resp_message_id - if query.resp_messages: - query.resp_messages.pop() - if query.resp_message_chain: - query.resp_message_chain.pop() + await query.adapter.create_message_card(resp_message_id, query.message_event) + async for result in runner.run(query): + result.resp_message_id = resp_message_id + if query.resp_messages: + query.resp_messages.pop() + if query.resp_message_chain: + query.resp_message_chain.pop() - query.resp_messages.append(result) - self.ap.logger.info( - f'对话({query.query_id})流式响应: {self.cut_str(result.readable_str())}' - ) + query.resp_messages.append(result) + self.ap.logger.info( + f'对话({query.query_id})流式响应: {self.cut_str(result.readable_str())}' + ) - if result.content is not None: - text_length += len(result.content) + if result.content is not None: + text_length += len(result.content) - yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) - else: - yield entities.StageProcessResult(result_type=entities.ResultType.INTERRUPT, new_query=query) + yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) + # else: + # yield entities.StageProcessResult(result_type=entities.ResultType.INTERRUPT, new_query=query) else: async for result in runner.run(query): From daaf4b54ef44be335cb02beecc800b68a2f27495 Mon Sep 17 00:00:00 2001 From: fdc <2213070223@qq.com> Date: Wed, 30 Jul 2025 17:06:14 +0800 Subject: [PATCH 020/107] feat: add webchat stream but only some --- .../controller/groups/pipelines/webchat.py | 36 +++++++++-- pkg/platform/sources/webchat.py | 61 ++++++++++++++++--- pkg/platform/sources/webchat.yaml | 13 +++- web/src/app/infra/http/HttpClient.ts | 3 + 4 files changed, 97 insertions(+), 16 deletions(-) diff --git a/pkg/api/http/controller/groups/pipelines/webchat.py b/pkg/api/http/controller/groups/pipelines/webchat.py index 005738db..a3bf8585 100644 --- a/pkg/api/http/controller/groups/pipelines/webchat.py +++ b/pkg/api/http/controller/groups/pipelines/webchat.py @@ -1,3 +1,5 @@ +import json + import quart from ... import group @@ -9,10 +11,16 @@ class WebChatDebugRouterGroup(group.RouterGroup): @self.route('/send', methods=['POST']) async def send_message(pipeline_uuid: str) -> str: """发送调试消息到流水线""" + + async def stream_generator(generator): + async for message in generator: + yield rf"data:{json.dumps({'message': message})}\n\n" + yield "data:{'type': 'end'}\n\n''" try: data = await quart.request.get_json() session_type = data.get('session_type', 'person') message_chain_obj = data.get('message', []) + is_stream = data.get('is_stream', False) if not message_chain_obj: return self.http_status(400, -1, 'message is required') @@ -25,13 +33,29 @@ class WebChatDebugRouterGroup(group.RouterGroup): if not webchat_adapter: return self.http_status(404, -1, 'WebChat adapter not found') - result = await webchat_adapter.send_webchat_message(pipeline_uuid, session_type, message_chain_obj) + if is_stream: + + generator = webchat_adapter.send_webchat_message(pipeline_uuid, session_type, message_chain_obj, is_stream) + + return quart.Response( + stream_generator(generator), + mimetype='text/event-stream' + ) + + else: + # result = await webchat_adapter.send_webchat_message(pipeline_uuid, session_type, message_chain_obj) + result = None + async for message in webchat_adapter.send_webchat_message(pipeline_uuid, session_type, message_chain_obj): + result = message + if result is not None: + return self.success( + data={ + 'message': result, + } + ) + else: + return self.http_status(400, -1, 'message is required') - return self.success( - data={ - 'message': result, - } - ) except Exception as e: return self.http_status(500, -1, f'Internal server error: {str(e)}') diff --git a/pkg/platform/sources/webchat.py b/pkg/platform/sources/webchat.py index 51b0479f..7fd7bb3b 100644 --- a/pkg/platform/sources/webchat.py +++ b/pkg/platform/sources/webchat.py @@ -25,11 +25,13 @@ class WebChatSession: id: str message_lists: dict[str, list[WebChatMessage]] = {} resp_waiters: dict[int, asyncio.Future[WebChatMessage]] + resp_queues = dict[int, asyncio.Queue[WebChatMessage]] def __init__(self, id: str): self.id = id self.message_lists = {} self.resp_waiters = {} + self.resp_queues = {} def get_message_list(self, pipeline_uuid: str) -> list[WebChatMessage]: if pipeline_uuid not in self.message_lists: @@ -108,6 +110,35 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): return message_data.model_dump() + async def reply_message_chunk( + self, + message_source: platform_events.MessageEvent, + message_id: str, + message: platform_message.MessageChain, + quote_origin: bool = False, + is_fianl: bool = False, + ) -> dict: + """回复消息""" + message_data = WebChatMessage( + id=-1, + role='assistant', + content=str(message), + message_chain=[component.__dict__ for component in message], + timestamp=datetime.now().isoformat(), + ) + + # notify waiter + if isinstance(message_source, platform_events.FriendMessage): + queue = self.webchat_person_session.resp_queues[message_source.message_chain.message_id] + elif isinstance(message_source, platform_events.GroupMessage): + queue = self.webchat_group_session.resp_queues[message_source.message_chain.message_id] + + queue.put(message_data) + if is_fianl: + queue.put(None) + + return message_data.model_dump() + def register_listener( self, event_type: typing.Type[platform_events.Event], @@ -140,7 +171,8 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): await self.logger.info('WebChat调试适配器正在停止') async def send_webchat_message( - self, pipeline_uuid: str, session_type: str, message_chain_obj: typing.List[dict] + self, pipeline_uuid: str, session_type: str, message_chain_obj: typing.List[dict], + is_stream: bool = False, ) -> dict: """发送调试消息到流水线""" if session_type == 'person': @@ -188,18 +220,29 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): if event.__class__ in self.listeners: await self.listeners[event.__class__](event, self) - # set waiter - waiter = asyncio.Future[WebChatMessage]() - use_session.resp_waiters[message_id] = waiter - waiter.add_done_callback(lambda future: use_session.resp_waiters.pop(message_id)) + if is_stream: + queue = use_session.resp_queues[message_id] + while True: + resp_message = await queue.get() + if resp_message is None: + resp_message.id = len(use_session.get_message_list(pipeline_uuid)) + 1 + use_session.get_message_list(pipeline_uuid).append(resp_message) + break + yield resp_message.model_dump() - resp_message = await waiter + else: + # set waiter + waiter = asyncio.Future[WebChatMessage]() + use_session.resp_waiters[message_id] = waiter + waiter.add_done_callback(lambda future: use_session.resp_waiters.pop(message_id)) - resp_message.id = len(use_session.get_message_list(pipeline_uuid)) + 1 + resp_message = await waiter - use_session.get_message_list(pipeline_uuid).append(resp_message) + resp_message.id = len(use_session.get_message_list(pipeline_uuid)) + 1 - return resp_message.model_dump() + use_session.get_message_list(pipeline_uuid).append(resp_message) + + yield resp_message.model_dump() def get_webchat_messages(self, pipeline_uuid: str, session_type: str) -> list[dict]: """获取调试消息历史""" diff --git a/pkg/platform/sources/webchat.yaml b/pkg/platform/sources/webchat.yaml index 4e8cc38e..0b1d4c29 100644 --- a/pkg/platform/sources/webchat.yaml +++ b/pkg/platform/sources/webchat.yaml @@ -9,7 +9,18 @@ metadata: en_US: "WebChat adapter for pipeline debugging" zh_Hans: "用于流水线调试的网页聊天适配器" icon: "" -spec: {} +spec: + config: + - name: enable-stream-reply + label: + en_US: Enable Stream Reply Mode + zh_Hans: 启用电报流式回复模式 + description: + en_US: If enabled, the bot will use the stream of telegram reply mode + zh_Hans: 如果启用,将使用电报流式方式来回复内容 + type: boolean + required: true + default: false execution: python: path: "webchat.py" diff --git a/web/src/app/infra/http/HttpClient.ts b/web/src/app/infra/http/HttpClient.ts index a86cdbe8..34c9f61f 100644 --- a/web/src/app/infra/http/HttpClient.ts +++ b/web/src/app/infra/http/HttpClient.ts @@ -34,6 +34,7 @@ import { } from '@/app/infra/entities/api'; import { GetBotLogsRequest } from '@/app/infra/http/requestParam/bots/GetBotLogsRequest'; import { GetBotLogsResponse } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse'; +import {boolean} from "zod"; type JSONValue = string | number | boolean | JSONObject | JSONArray | null; interface JSONObject { @@ -309,12 +310,14 @@ class HttpClient { messageChain: object[], pipelineId: string, timeout: number = 15000, + is_stream: boolean = false, ): Promise { return this.post( `/api/v1/pipelines/${pipelineId}/chat/send`, { session_type: sessionType, message: messageChain, + is_stream: is_stream, }, { timeout, From 6e08bf71c9acf6ca2e3f7dc9fe999114e2ca187d Mon Sep 17 00:00:00 2001 From: fdc <2213070223@qq.com> Date: Thu, 31 Jul 2025 09:51:25 +0800 Subject: [PATCH 021/107] feat:webchat frontend stream --- .../components/debug-dialog/DebugDialog.tsx | 112 +++++++++++++++--- web/src/app/infra/http/HttpClient.ts | 83 +++++++++++++ web/src/i18n/locales/en-US.ts | 1 + web/src/i18n/locales/ja-JP.ts | 1 + web/src/i18n/locales/zh-Hans.ts | 1 + 5 files changed, 181 insertions(+), 17 deletions(-) diff --git a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx index a84389e0..9fde4bc2 100644 --- a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx +++ b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx @@ -10,6 +10,7 @@ import { cn } from '@/lib/utils'; import { Message } from '@/app/infra/entities/message'; import { toast } from 'sonner'; import AtBadge from './AtBadge'; +import { Switch } from '@/components/ui/switch'; interface MessageComponent { type: 'At' | 'Plain'; @@ -36,6 +37,7 @@ export default function DebugDialog({ const [showAtPopover, setShowAtPopover] = useState(false); const [hasAt, setHasAt] = useState(false); const [isHovering, setIsHovering] = useState(false); + const [isStreaming, setIsStreaming] = useState(false); const messagesEndRef = useRef(null); const inputRef = useRef(null); const popoverRef = useRef(null); @@ -157,27 +159,96 @@ export default function DebugDialog({ // for showing text_content = '@webchatbot' + text_content; } - const userMessage: Message = { - id: -1, - role: 'user', - content: text_content, - timestamp: new Date().toISOString(), - message_chain: messageChain, - }; + id: -1, + role: 'user', + content: text_content, + timestamp: new Date().toISOString(), + message_chain: messageChain, + }; + // 根据isStreaming状态决定使用哪种传输方式 + if (isStreaming) { + // 创建初始bot消息 + const botMessage: Message = { + id: -1, + role: 'assistant', + content: '', + timestamp: new Date().toISOString(), + message_chain: [{ type: 'Plain', text: '' }], + }; - setMessages((prevMessages) => [...prevMessages, userMessage]); - setInputValue(''); - setHasAt(false); + // 添加用户消息和初始bot消息到状态 - const response = await httpClient.sendWebChatMessage( - sessionType, - messageChain, - selectedPipelineId, - 120000, - ); + setMessages((prevMessages) => [...prevMessages, userMessage, botMessage]); + setInputValue(''); + setHasAt(false); - setMessages((prevMessages) => [...prevMessages, response.message]); + try { + let botMessageId = botMessage.id; + let accumulatedContent = ''; + + await httpClient.sendStreamingWebChatMessage( + sessionType, + messageChain, + selectedPipelineId, + (data) => { + // 处理流式响应数据 + if (data.message) { + accumulatedContent += data.message; + + // 更新bot消息 + setMessages((prevMessages) => { + const updatedMessages = [...prevMessages]; + const botMessageIndex = updatedMessages.findIndex( + (msg) => msg.id === botMessageId && msg.role === 'assistant' + ); + + if (botMessageIndex !== -1) { + const updatedBotMessage = { + ...updatedMessages[botMessageIndex], + content: accumulatedContent, + message_chain: [{ type: 'Plain', text: accumulatedContent }], + }; + updatedMessages[botMessageIndex] = updatedBotMessage; + } + + return updatedMessages; + }); + } + }, + () => { + // 流传输完成 + console.log('Streaming completed'); + }, + (error) => { + // 处理错误 + console.error('Streaming error:', error); + if (sessionType === 'person') { + toast.error(t('pipelines.debugDialog.sendFailed')); + } + } + ); + } catch (error) { + console.error('Failed to send streaming message:', error); + if (sessionType === 'person') { + toast.error(t('pipelines.debugDialog.sendFailed')); + } + } + } else { + + setMessages((prevMessages) => [...prevMessages, userMessage]); + setInputValue(''); + setHasAt(false); + + const response = await httpClient.sendWebChatMessage( + sessionType, + messageChain, + selectedPipelineId, + 120000, + ); + + setMessages((prevMessages) => [...prevMessages, response.message]); + } } catch ( // eslint-disable-next-line @typescript-eslint/no-explicit-any error: any @@ -306,6 +377,13 @@ export default function DebugDialog({
+
+ {t('pipelines.debugDialog.streaming')} + +
{hasAt && ( diff --git a/web/src/app/infra/http/HttpClient.ts b/web/src/app/infra/http/HttpClient.ts index 34c9f61f..015df00c 100644 --- a/web/src/app/infra/http/HttpClient.ts +++ b/web/src/app/infra/http/HttpClient.ts @@ -325,6 +325,89 @@ class HttpClient { ); } + public async sendStreamingWebChatMessage( + sessionType: string, + messageChain: object[], + pipelineId: string, + onMessage: (data: any) => void, + onComplete: () => void, + onError: (error: any) => void, + ): Promise { + try { + const url = `${this.baseURL}/api/v1/pipelines/${pipelineId}/chat/send`; + + // 使用fetch发送流式请求,因为axios在浏览器环境中不直接支持流式响应 + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + session_type: sessionType, + message: messageChain, + is_stream: true, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + if (!response.body) { + throw new Error('ReadableStream not supported'); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + // 读取流式响应 + try { + while (true) { + const { done, value } = await reader.read(); + + if (done) { + onComplete(); + break; + } + + // 解码数据 + buffer += decoder.decode(value, { stream: true }); + + // 处理完整的JSON对象 + const lines = buffer.split('\n\n'); + buffer = lines.pop() || ''; + + for (const line of lines) { + if (line.startsWith('data: ')) { + try { + const data = JSON.parse(line.slice(6)); + + if (data.type === 'end') { + // 流传输结束 + reader.cancel(); + onComplete(); + return; + } + + if (data.message) { + // 处理消息数据 + onMessage(data); + } + } catch (error) { + console.error('Error parsing streaming data:', error); + } + } + } + } + } finally { + reader.releaseLock(); + } + } catch (error) { + onError(error); + } + } + public getWebChatHistoryMessages( pipelineId: string, sessionType: string, diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index 0e171e4b..dd276519 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -228,6 +228,7 @@ const enUS = { loadMessagesFailed: 'Failed to load messages', loadPipelinesFailed: 'Failed to load pipelines', atTips: 'Mention the bot', + streaming: 'Streaming', }, }, register: { diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index f1783a35..9a600d3a 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -229,6 +229,7 @@ const jaJP = { loadMessagesFailed: 'メッセージの読み込みに失敗しました', loadPipelinesFailed: 'パイプラインの読み込みに失敗しました', atTips: 'ボットをメンション', + streaming: 'ストリーミング', }, }, register: { diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index 2a960131..62eb6563 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -223,6 +223,7 @@ const zhHans = { loadMessagesFailed: '加载消息失败', loadPipelinesFailed: '加载流水线失败', atTips: '提及机器人', + streaming: '流式传输', }, }, register: { From e4d951b1747fc450b1e146417e68373abf8a8e1e Mon Sep 17 00:00:00 2001 From: fdc <2213070223@qq.com> Date: Thu, 31 Jul 2025 10:01:47 +0800 Subject: [PATCH 022/107] fix: is_stream_output_supperted in webchat return --- pkg/platform/sources/webchat.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/platform/sources/webchat.py b/pkg/platform/sources/webchat.py index 7fd7bb3b..2e8b7b99 100644 --- a/pkg/platform/sources/webchat.py +++ b/pkg/platform/sources/webchat.py @@ -51,6 +51,8 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): typing.Callable[[platform_events.Event, msadapter.MessagePlatformAdapter], None], ] = {} + is_stream: bool + def __init__(self, config: dict, ap: app.Application, logger: EventLogger): self.ap = ap self.logger = logger @@ -61,6 +63,8 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): self.bot_account_id = 'webchatbot' + self.is_stream = False + async def send_message( self, target_type: str, @@ -138,6 +142,9 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): queue.put(None) return message_data.model_dump() + + async def is_stream_output_supported(self) -> bool: + return self.is_stream def register_listener( self, @@ -172,8 +179,9 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): async def send_webchat_message( self, pipeline_uuid: str, session_type: str, message_chain_obj: typing.List[dict], - is_stream: bool = False, + is_stream: bool = False, ) -> dict: + self.is_stream = is_stream """发送调试消息到流水线""" if session_type == 'person': use_session = self.webchat_person_session From 5560a4f52dc0237ef7db6955cc0ddc0ecf779df7 Mon Sep 17 00:00:00 2001 From: fdc <2213070223@qq.com> Date: Thu, 31 Jul 2025 10:28:43 +0800 Subject: [PATCH 023/107] fix:lsome bug --- .../components/debug-dialog/DebugDialog.tsx | 47 +++++++++++-------- web/src/app/infra/http/HttpClient.ts | 20 ++++---- 2 files changed, 38 insertions(+), 29 deletions(-) diff --git a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx index 9fde4bc2..0a6330dc 100644 --- a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx +++ b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx @@ -46,6 +46,18 @@ export default function DebugDialog({ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; + const loadMessages = async (pipelineId: string) => { + try { + const response = await httpClient.getWebChatHistoryMessages( + pipelineId, + sessionType, + ); + setMessages(response.messages); + } catch (error) { + console.error('Failed to load messages:', error); + } + }; + useEffect(() => { scrollToBottom(); }, [messages]); @@ -61,7 +73,7 @@ export default function DebugDialog({ if (open) { loadMessages(selectedPipelineId); } - }, [sessionType, selectedPipelineId]); + }, [sessionType, selectedPipelineId, open, loadMessages]); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -86,18 +98,6 @@ export default function DebugDialog({ } }, [showAtPopover]); - const loadMessages = async (pipelineId: string) => { - try { - const response = await httpClient.getWebChatHistoryMessages( - pipelineId, - sessionType, - ); - setMessages(response.messages); - } catch (error) { - console.error('Failed to load messages:', error); - } - }; - const handleInputChange = (e: React.ChangeEvent) => { const value = e.target.value; if (sessionType === 'group') { @@ -179,12 +179,16 @@ export default function DebugDialog({ // 添加用户消息和初始bot消息到状态 - setMessages((prevMessages) => [...prevMessages, userMessage, botMessage]); + setMessages((prevMessages) => [ + ...prevMessages, + userMessage, + botMessage, + ]); setInputValue(''); setHasAt(false); try { - let botMessageId = botMessage.id; + const botMessageId = botMessage.id; let accumulatedContent = ''; await httpClient.sendStreamingWebChatMessage( @@ -200,14 +204,17 @@ export default function DebugDialog({ setMessages((prevMessages) => { const updatedMessages = [...prevMessages]; const botMessageIndex = updatedMessages.findIndex( - (msg) => msg.id === botMessageId && msg.role === 'assistant' + (msg) => + msg.id === botMessageId && msg.role === 'assistant', ); if (botMessageIndex !== -1) { const updatedBotMessage = { ...updatedMessages[botMessageIndex], content: accumulatedContent, - message_chain: [{ type: 'Plain', text: accumulatedContent }], + message_chain: [ + { type: 'Plain', text: accumulatedContent }, + ] }; updatedMessages[botMessageIndex] = updatedBotMessage; } @@ -226,7 +233,7 @@ export default function DebugDialog({ if (sessionType === 'person') { toast.error(t('pipelines.debugDialog.sendFailed')); } - } + }, ); } catch (error) { console.error('Failed to send streaming message:', error); @@ -378,7 +385,9 @@ export default function DebugDialog({
- {t('pipelines.debugDialog.streaming')} + + {t('pipelines.debugDialog.streaming')} + void, + onMessage: (data: ApiRespWebChatMessage) => void, onComplete: () => void, - onError: (error: any) => void, + onError: (error: Error) => void, ): Promise { try { const url = `${this.baseURL}/api/v1/pipelines/${pipelineId}/chat/send`; - + // 使用fetch发送流式请求,因为axios在浏览器环境中不直接支持流式响应 const response = await fetch(url, { method: 'POST', @@ -365,7 +365,7 @@ class HttpClient { try { while (true) { const { done, value } = await reader.read(); - + if (done) { onComplete(); break; @@ -373,23 +373,23 @@ class HttpClient { // 解码数据 buffer += decoder.decode(value, { stream: true }); - + // 处理完整的JSON对象 const lines = buffer.split('\n\n'); buffer = lines.pop() || ''; - + for (const line of lines) { if (line.startsWith('data: ')) { try { const data = JSON.parse(line.slice(6)); - + if (data.type === 'end') { // 流传输结束 reader.cancel(); onComplete(); return; } - + if (data.message) { // 处理消息数据 onMessage(data); @@ -404,7 +404,7 @@ class HttpClient { reader.releaseLock(); } } catch (error) { - onError(error); + onError(error as Error); } } From cb88da7f02e336fecc514cf898f2675e2f7b3809 Mon Sep 17 00:00:00 2001 From: fdc <2213070223@qq.com> Date: Thu, 31 Jul 2025 10:34:36 +0800 Subject: [PATCH 024/107] fix: frontend bug --- .../components/debug-dialog/DebugDialog.tsx | 38 +++++++++---------- web/src/app/infra/http/HttpClient.ts | 1 - 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx index 0a6330dc..45bf8b38 100644 --- a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx +++ b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { httpClient } from '@/app/infra/http/HttpClient'; import { DialogContent } from '@/components/ui/dialog'; @@ -46,7 +46,7 @@ export default function DebugDialog({ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; - const loadMessages = async (pipelineId: string) => { + const loadMessages = useCallback(async (pipelineId: string) => { try { const response = await httpClient.getWebChatHistoryMessages( pipelineId, @@ -56,7 +56,7 @@ export default function DebugDialog({ } catch (error) { console.error('Failed to load messages:', error); } - }; + }, [sessionType]); useEffect(() => { scrollToBottom(); @@ -160,12 +160,12 @@ export default function DebugDialog({ text_content = '@webchatbot' + text_content; } const userMessage: Message = { - id: -1, - role: 'user', - content: text_content, - timestamp: new Date().toISOString(), - message_chain: messageChain, - }; + id: -1, + role: 'user', + content: text_content, + timestamp: new Date().toISOString(), + message_chain: messageChain, + }; // 根据isStreaming状态决定使用哪种传输方式 if (isStreaming) { // 创建初始bot消息 @@ -199,7 +199,7 @@ export default function DebugDialog({ // 处理流式响应数据 if (data.message) { accumulatedContent += data.message; - + // 更新bot消息 setMessages((prevMessages) => { const updatedMessages = [...prevMessages]; @@ -207,18 +207,18 @@ export default function DebugDialog({ (msg) => msg.id === botMessageId && msg.role === 'assistant', ); - + if (botMessageIndex !== -1) { const updatedBotMessage = { ...updatedMessages[botMessageIndex], content: accumulatedContent, message_chain: [ { type: 'Plain', text: accumulatedContent }, - ] + ], }; updatedMessages[botMessageIndex] = updatedBotMessage; } - + return updatedMessages; }); } @@ -434,12 +434,12 @@ export default function DebugDialog({
+ onClick={sendMessage} + disabled={!inputValue.trim() && !hasAt} + className="rounded-md bg-[#2288ee] hover:bg-[#2288ee] w-20 text-white px-6 py-2 text-base font-medium transition-none flex items-center gap-2 shadow-none" + > + <>{t('pipelines.debugDialog.send')} +
diff --git a/web/src/app/infra/http/HttpClient.ts b/web/src/app/infra/http/HttpClient.ts index a6e0c172..c3d887b8 100644 --- a/web/src/app/infra/http/HttpClient.ts +++ b/web/src/app/infra/http/HttpClient.ts @@ -35,7 +35,6 @@ import { import { GetBotLogsRequest } from '@/app/infra/http/requestParam/bots/GetBotLogsRequest'; import { GetBotLogsResponse } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse'; - type JSONValue = string | number | boolean | JSONObject | JSONArray | null; interface JSONObject { [key: string]: JSONValue; From d9a2bb9a06bcf911dcc5ef830c046ead1a56593a Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Thu, 31 Jul 2025 14:49:12 +0800 Subject: [PATCH 025/107] fix:webchat stream judge bug and frontend bug --- .../controller/groups/pipelines/webchat.py | 2 +- pkg/platform/sources/webchat.py | 34 ++++++++++----- .../components/debug-dialog/DebugDialog.tsx | 43 +++++++++---------- web/src/app/infra/http/HttpClient.ts | 9 +++- 4 files changed, 54 insertions(+), 34 deletions(-) diff --git a/pkg/api/http/controller/groups/pipelines/webchat.py b/pkg/api/http/controller/groups/pipelines/webchat.py index a3bf8585..bebac818 100644 --- a/pkg/api/http/controller/groups/pipelines/webchat.py +++ b/pkg/api/http/controller/groups/pipelines/webchat.py @@ -15,7 +15,7 @@ class WebChatDebugRouterGroup(group.RouterGroup): async def stream_generator(generator): async for message in generator: yield rf"data:{json.dumps({'message': message})}\n\n" - yield "data:{'type': 'end'}\n\n''" + yield "data:{type: end}\n\n''" try: data = await quart.request.get_json() session_type = data.get('session_type', 'person') diff --git a/pkg/platform/sources/webchat.py b/pkg/platform/sources/webchat.py index 2e8b7b99..274c5657 100644 --- a/pkg/platform/sources/webchat.py +++ b/pkg/platform/sources/webchat.py @@ -19,6 +19,7 @@ class WebChatMessage(BaseModel): content: str message_chain: list[dict] timestamp: str + is_final: bool = False class WebChatSession: @@ -117,10 +118,10 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): async def reply_message_chunk( self, message_source: platform_events.MessageEvent, - message_id: str, + message_id: int, message: platform_message.MessageChain, quote_origin: bool = False, - is_fianl: bool = False, + is_final: bool = False, ) -> dict: """回复消息""" message_data = WebChatMessage( @@ -132,14 +133,21 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): ) # notify waiter - if isinstance(message_source, platform_events.FriendMessage): - queue = self.webchat_person_session.resp_queues[message_source.message_chain.message_id] - elif isinstance(message_source, platform_events.GroupMessage): - queue = self.webchat_group_session.resp_queues[message_source.message_chain.message_id] + session = (self.webchat_group_session if isinstance(message_source, platform_events.GroupMessage) else self.webchat_person_session) + if message_source.message_chain.message_id not in session.resp_waiters: + # session.resp_waiters[message_source.message_chain.message_id] = asyncio.Queue() + queue = session.resp_queues[message_source.message_chain.message_id] + + # if isinstance(message_source, platform_events.FriendMessage): + # queue = self.webchat_person_session.resp_queues[message_source.message_chain.message_id] + # elif isinstance(message_source, platform_events.GroupMessage): + # queue = self.webchat_group_session.resp_queues[message_source.message_chain.message_id] + if is_final: + message_data.is_final = True + # print(message_data) + await queue.put(message_data) + - queue.put(message_data) - if is_fianl: - queue.put(None) return message_data.model_dump() @@ -192,6 +200,10 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): message_id = len(use_session.get_message_list(pipeline_uuid)) + 1 + if is_stream: + use_session.resp_queues[message_id] = asyncio.Queue() + logger.debug(f"Initialized queue for message_id: {message_id}") + use_session.get_message_list(pipeline_uuid).append( WebChatMessage( id=message_id, @@ -232,9 +244,11 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): queue = use_session.resp_queues[message_id] while True: resp_message = await queue.get() - if resp_message is None: + print(resp_message) + if resp_message.is_final: resp_message.id = len(use_session.get_message_list(pipeline_uuid)) + 1 use_session.get_message_list(pipeline_uuid).append(resp_message) + yield resp_message.model_dump() break yield resp_message.model_dump() diff --git a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx index 45bf8b38..2c051b00 100644 --- a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx +++ b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx @@ -46,17 +46,20 @@ export default function DebugDialog({ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; - const loadMessages = useCallback(async (pipelineId: string) => { - try { - const response = await httpClient.getWebChatHistoryMessages( - pipelineId, - sessionType, - ); - setMessages(response.messages); - } catch (error) { - console.error('Failed to load messages:', error); - } - }, [sessionType]); + const loadMessages = useCallback( + async (pipelineId: string) => { + try { + const response = await httpClient.getWebChatHistoryMessages( + pipelineId, + sessionType, + ); + setMessages(response.messages); + } catch (error) { + console.error('Failed to load messages:', error); + } + }, + [sessionType], + ); useEffect(() => { scrollToBottom(); @@ -242,7 +245,6 @@ export default function DebugDialog({ } } } else { - setMessages((prevMessages) => [...prevMessages, userMessage]); setInputValue(''); setHasAt(false); @@ -388,10 +390,7 @@ export default function DebugDialog({ {t('pipelines.debugDialog.streaming')} - +
{hasAt && ( @@ -434,12 +433,12 @@ export default function DebugDialog({
+ onClick={sendMessage} + disabled={!inputValue.trim() && !hasAt} + className="rounded-md bg-[#2288ee] hover:bg-[#2288ee] w-20 text-white px-6 py-2 text-base font-medium transition-none flex items-center gap-2 shadow-none" + > + <>{t('pipelines.debugDialog.send')} + diff --git a/web/src/app/infra/http/HttpClient.ts b/web/src/app/infra/http/HttpClient.ts index c3d887b8..ba36cc56 100644 --- a/web/src/app/infra/http/HttpClient.ts +++ b/web/src/app/infra/http/HttpClient.ts @@ -333,13 +333,20 @@ class HttpClient { onError: (error: Error) => void, ): Promise { try { - const url = `${this.baseURL}/api/v1/pipelines/${pipelineId}/chat/send`; + // 构造完整的URL,处理相对路径的情况 + let url = `${this.baseURL}/api/v1/pipelines/${pipelineId}/chat/send`; + if (this.baseURL === '/') { + // 获取用户访问的完整URL + const baseURL = window.location.origin; + url = `${baseURL}/api/v1/pipelines/${pipelineId}/chat/send`; + } // 使用fetch发送流式请求,因为axios在浏览器环境中不直接支持流式响应 const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', + Authorization: `Bearer ${this.getSessionSync()}`, }, body: JSON.stringify({ session_type: sessionType, From 0ce81a2df201b144b8f7181550e53ade1d33ba39 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Fri, 1 Aug 2025 11:33:16 +0800 Subject: [PATCH 026/107] feat: webchat stream is ok --- pkg/api/http/controller/groups/pipelines/webchat.py | 4 ++-- pkg/platform/sources/webchat.py | 5 +++-- .../components/debug-dialog/DebugDialog.tsx | 12 +++++++----- web/src/app/infra/http/HttpClient.ts | 6 ++---- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/pkg/api/http/controller/groups/pipelines/webchat.py b/pkg/api/http/controller/groups/pipelines/webchat.py index bebac818..f8698b01 100644 --- a/pkg/api/http/controller/groups/pipelines/webchat.py +++ b/pkg/api/http/controller/groups/pipelines/webchat.py @@ -14,8 +14,8 @@ class WebChatDebugRouterGroup(group.RouterGroup): async def stream_generator(generator): async for message in generator: - yield rf"data:{json.dumps({'message': message})}\n\n" - yield "data:{type: end}\n\n''" + yield f"data: {json.dumps({'message': message})}\n\n" + yield "data: {\"type\": \"end\"}\n\n" try: data = await quart.request.get_json() session_type = data.get('session_type', 'person') diff --git a/pkg/platform/sources/webchat.py b/pkg/platform/sources/webchat.py index 274c5657..f7f3d964 100644 --- a/pkg/platform/sources/webchat.py +++ b/pkg/platform/sources/webchat.py @@ -242,11 +242,12 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): if is_stream: queue = use_session.resp_queues[message_id] + msg_id = len(use_session.get_message_list(pipeline_uuid)) + 1 while True: resp_message = await queue.get() - print(resp_message) + resp_message.id = msg_id if resp_message.is_final: - resp_message.id = len(use_session.get_message_list(pipeline_uuid)) + 1 + resp_message.id = msg_id use_session.get_message_list(pipeline_uuid).append(resp_message) yield resp_message.model_dump() break diff --git a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx index 2c051b00..7b0607e4 100644 --- a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx +++ b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx @@ -201,15 +201,17 @@ export default function DebugDialog({ (data) => { // 处理流式响应数据 if (data.message) { - accumulatedContent += data.message; + accumulatedContent += data.message.content; // 更新bot消息 setMessages((prevMessages) => { const updatedMessages = [...prevMessages]; - const botMessageIndex = updatedMessages.findIndex( - (msg) => - msg.id === botMessageId && msg.role === 'assistant', - ); + // const botMessageIndex = updatedMessages.findIndex( + // (msg) => + // msg.id === botMessageId && msg.role === 'assistant', + // ); + // 使用索引来更新消息,而不是id匹配 + const botMessageIndex = updatedMessages.length - 1; if (botMessageIndex !== -1) { const updatedBotMessage = { diff --git a/web/src/app/infra/http/HttpClient.ts b/web/src/app/infra/http/HttpClient.ts index ba36cc56..2fcda964 100644 --- a/web/src/app/infra/http/HttpClient.ts +++ b/web/src/app/infra/http/HttpClient.ts @@ -309,14 +309,12 @@ class HttpClient { messageChain: object[], pipelineId: string, timeout: number = 15000, - is_stream: boolean = false, ): Promise { return this.post( `/api/v1/pipelines/${pipelineId}/chat/send`, { session_type: sessionType, message: messageChain, - is_stream: is_stream, }, { timeout, @@ -382,10 +380,10 @@ class HttpClient { // 处理完整的JSON对象 const lines = buffer.split('\n\n'); - buffer = lines.pop() || ''; + buffer = ''; for (const line of lines) { - if (line.startsWith('data: ')) { + if (line.startsWith('data:')) { try { const data = JSON.parse(line.slice(6)); From 52280d7a055d951cc18dc033c873df7c1f6da8c8 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Sat, 2 Aug 2025 01:42:22 +0800 Subject: [PATCH 027/107] feat: add webchat Word-by-word output fix:webchat on message stream bug --- .../components/debug-dialog/DebugDialog.tsx | 112 +++++++++++++----- web/src/app/infra/http/HttpClient.ts | 4 +- 2 files changed, 84 insertions(+), 32 deletions(-) diff --git a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx index 7b0607e4..c45a7085 100644 --- a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx +++ b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx @@ -42,9 +42,24 @@ export default function DebugDialog({ const inputRef = useRef(null); const popoverRef = useRef(null); - const scrollToBottom = () => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }; + // const scrollToBottom = () => { + // messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + // }; + + const scrollToBottom = useCallback(() => { + // 使用setTimeout确保在DOM更新后执行滚动 + setTimeout(() => { + const scrollArea = document.querySelector('.scroll-area') as HTMLElement; + if (scrollArea) { + scrollArea.scrollTo({ + top: scrollArea.scrollHeight, + behavior: 'smooth', + }); + } + // 同时确保messagesEndRef也滚动到视图 + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, 0); + }, []); const loadMessages = useCallback( async (pipelineId: string) => { @@ -60,10 +75,10 @@ export default function DebugDialog({ }, [sessionType], ); - + // 在useEffect中监听messages变化时滚动 useEffect(() => { scrollToBottom(); - }, [messages]); + }, [messages, scrollToBottom]); useEffect(() => { if (open) { @@ -175,7 +190,7 @@ export default function DebugDialog({ const botMessage: Message = { id: -1, role: 'assistant', - content: '', + content: '生成中...', timestamp: new Date().toISOString(), message_chain: [{ type: 'Plain', text: '' }], }; @@ -191,8 +206,9 @@ export default function DebugDialog({ setHasAt(false); try { - const botMessageId = botMessage.id; - let accumulatedContent = ''; + let fullContent = ''; // 保存完整内容 + let displayContent = ''; // 当前显示内容 + let typingInterval: NodeJS.Timeout; await httpClient.sendStreamingWebChatMessage( sessionType, @@ -201,40 +217,76 @@ export default function DebugDialog({ (data) => { // 处理流式响应数据 if (data.message) { - accumulatedContent += data.message.content; + // 更新完整内容 + fullContent = data.message.content; - // 更新bot消息 - setMessages((prevMessages) => { - const updatedMessages = [...prevMessages]; - // const botMessageIndex = updatedMessages.findIndex( - // (msg) => - // msg.id === botMessageId && msg.role === 'assistant', - // ); - // 使用索引来更新消息,而不是id匹配 - const botMessageIndex = updatedMessages.length - 1; + // 清除之前的打字效果 + if (typingInterval) { + clearInterval(typingInterval); + } - if (botMessageIndex !== -1) { - const updatedBotMessage = { - ...updatedMessages[botMessageIndex], - content: accumulatedContent, - message_chain: [ - { type: 'Plain', text: accumulatedContent }, - ], - }; - updatedMessages[botMessageIndex] = updatedBotMessage; + // 开始新的打字效果 + let currentPos = displayContent.length; + const targetContent = fullContent; + + typingInterval = setInterval(() => { + if (currentPos < targetContent.length) { + displayContent = targetContent.substring(0, currentPos + 1); + currentPos++; + + // 更新bot消息 + setMessages((prevMessages) => { + const updatedMessages = [...prevMessages]; + const botMessageIndex = updatedMessages.length - 1; + + if (botMessageIndex !== -1) { + const updatedBotMessage = { + ...updatedMessages[botMessageIndex], + content: displayContent, + message_chain: [ + { type: 'Plain', text: displayContent }, + ], + }; + updatedMessages[botMessageIndex] = updatedBotMessage; + } + setTimeout(scrollToBottom, 0); // 确保在状态更新后滚动 + return updatedMessages; + }); + } else { + clearInterval(typingInterval); } - - return updatedMessages; - }); + }, 30); // 调整这个值可以改变打字速度 } }, () => { // 流传输完成 console.log('Streaming completed'); + if (typingInterval) { + clearInterval(typingInterval); + } + // 确保最终内容完全显示 + setMessages((prevMessages) => { + const updatedMessages = [...prevMessages]; + const botMessageIndex = updatedMessages.length - 1; + + if (botMessageIndex !== -1) { + const updatedBotMessage = { + ...updatedMessages[botMessageIndex], + content: fullContent, + message_chain: [{ type: 'Plain', text: fullContent }], + }; + updatedMessages[botMessageIndex] = updatedBotMessage; + } + setTimeout(scrollToBottom, 0); // 确保在状态更新后滚动 + return updatedMessages; + }); }, (error) => { // 处理错误 console.error('Streaming error:', error); + if (typingInterval) { + clearInterval(typingInterval); + } if (sessionType === 'person') { toast.error(t('pipelines.debugDialog.sendFailed')); } diff --git a/web/src/app/infra/http/HttpClient.ts b/web/src/app/infra/http/HttpClient.ts index 2fcda964..0a8aa36c 100644 --- a/web/src/app/infra/http/HttpClient.ts +++ b/web/src/app/infra/http/HttpClient.ts @@ -380,12 +380,12 @@ class HttpClient { // 处理完整的JSON对象 const lines = buffer.split('\n\n'); - buffer = ''; + buffer = lines.pop() || ''; for (const line of lines) { if (line.startsWith('data:')) { try { - const data = JSON.parse(line.slice(6)); + const data = JSON.parse(line.slice(5)); if (data.type === 'end') { // 流传输结束 From 377d455ec1ebe020348ee1174f431afbcb1cc549 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 3 Aug 2025 13:08:51 +0800 Subject: [PATCH 028/107] perf: ruff format & remove `stream` params in requester --- libs/dingtalk_api/api.py | 30 +- libs/wechatpad_api/__init__.py | 2 +- libs/wechatpad_api/api/chatroom.py | 6 +- libs/wechatpad_api/api/downloadpai.py | 25 +- libs/wechatpad_api/api/friend.py | 5 - libs/wechatpad_api/api/login.py | 60 +-- libs/wechatpad_api/api/message.py | 111 ++--- libs/wechatpad_api/util/http_util.py | 48 +- .../controller/groups/pipelines/webchat.py | 20 +- pkg/core/entities.py | 4 +- pkg/pipeline/cntfilter/cntfilter.py | 2 +- pkg/pipeline/process/handlers/chat.py | 5 +- pkg/pipeline/respback/respback.py | 4 - pkg/platform/adapter.py | 17 +- pkg/platform/sources/aiocqhttp.py | 230 ++++++---- pkg/platform/sources/dingtalk.py | 9 +- pkg/platform/sources/discord.py | 20 +- pkg/platform/sources/lark.py | 13 +- pkg/platform/sources/nakuru.py | 5 +- pkg/platform/sources/officialaccount.py | 4 +- pkg/platform/sources/qqbotpy.py | 2 +- pkg/platform/sources/qqofficial.py | 9 +- pkg/platform/sources/slack.py | 8 +- pkg/platform/sources/telegram.py | 3 +- pkg/platform/sources/webchat.py | 17 +- pkg/platform/sources/wechatpad.py | 431 +++++++----------- pkg/platform/sources/wecom.py | 6 +- pkg/platform/sources/wecomcs.py | 6 +- pkg/platform/types/message.py | 17 +- pkg/provider/entities.py | 4 +- pkg/provider/modelmgr/requester.py | 17 +- pkg/provider/modelmgr/requesters/chatcmpl.py | 78 ++-- .../modelmgr/requesters/giteeaichatcmpl.py | 99 ++-- .../modelmgr/requesters/modelscopechatcmpl.py | 96 ++-- .../modelmgr/requesters/ppiochatcmpl.py | 111 +++-- pkg/provider/runners/difysvapi.py | 4 +- pkg/provider/runners/localagent.py | 3 +- pkg/utils/image.py | 8 +- pkg/utils/importutil.py | 2 +- 39 files changed, 685 insertions(+), 856 deletions(-) diff --git a/libs/dingtalk_api/api.py b/libs/dingtalk_api/api.py index d1c7065f..3d483a3a 100644 --- a/libs/dingtalk_api/api.py +++ b/libs/dingtalk_api/api.py @@ -3,7 +3,6 @@ import json import time from typing import Callable import dingtalk_stream # type: ignore -from dingtalk_stream import AckMessage, ChatbotHandler, CallbackHandler, CallbackMessage, ChatbotMessage, AICardReplier from .EchoHandler import EchoTextHandler from .dingtalkevent import DingTalkEvent import httpx @@ -254,24 +253,23 @@ class DingTalkClient: await self.logger.error(f'failed to send proactive massage to group: {traceback.format_exc()}') raise Exception(f'failed to send proactive massage to group: {traceback.format_exc()}') - async def create_and_card(self, temp_card_id: str, incoming_message: dingtalk_stream.ChatbotMessage,quote_origin:bool=False): - content_key = "content" - card_data = {content_key: ""} + async def create_and_card( + self, temp_card_id: str, incoming_message: dingtalk_stream.ChatbotMessage, quote_origin: bool = False + ): + content_key = 'content' + card_data = {content_key: ''} - card_instance = dingtalk_stream.AICardReplier( - self.client, incoming_message - ) + card_instance = dingtalk_stream.AICardReplier(self.client, incoming_message) # print(card_instance) # 先投放卡片: https://open.dingtalk.com/document/orgapp/create-and-deliver-cards card_instance_id = await card_instance.async_create_and_deliver_card( - temp_card_id, card_data, + temp_card_id, + card_data, ) - return card_instance,card_instance_id + return card_instance, card_instance_id - async def send_card_message(self, - card_instance, - card_instance_id: str,content: str,is_final: bool): - content_key = "content" + async def send_card_message(self, card_instance, card_instance_id: str, content: str, is_final: bool): + content_key = 'content' try: await card_instance.async_streaming( card_instance_id, @@ -286,16 +284,12 @@ class DingTalkClient: await card_instance.async_streaming( card_instance_id, content_key=content_key, - content_value="", + content_value='', append=False, finished=is_final, failed=True, ) - - - - async def start(self): """启动 WebSocket 连接,监听消息""" await self.client.start() diff --git a/libs/wechatpad_api/__init__.py b/libs/wechatpad_api/__init__.py index 23c23fb2..9ac533f7 100644 --- a/libs/wechatpad_api/__init__.py +++ b/libs/wechatpad_api/__init__.py @@ -1 +1 @@ -from .client import WeChatPadClient \ No newline at end of file +from .client import WeChatPadClient as WeChatPadClient diff --git a/libs/wechatpad_api/api/chatroom.py b/libs/wechatpad_api/api/chatroom.py index a7af207c..2d9281a2 100644 --- a/libs/wechatpad_api/api/chatroom.py +++ b/libs/wechatpad_api/api/chatroom.py @@ -1,4 +1,4 @@ -from libs.wechatpad_api.util.http_util import async_request, post_json +from libs.wechatpad_api.util.http_util import post_json class ChatRoomApi: @@ -7,8 +7,6 @@ class ChatRoomApi: self.token = token def get_chatroom_member_detail(self, chatroom_name): - params = { - "ChatRoomName": chatroom_name - } + params = {'ChatRoomName': chatroom_name} url = self.base_url + '/group/GetChatroomMemberDetail' return post_json(url, token=self.token, data=params) diff --git a/libs/wechatpad_api/api/downloadpai.py b/libs/wechatpad_api/api/downloadpai.py index a82a5674..2d45fac6 100644 --- a/libs/wechatpad_api/api/downloadpai.py +++ b/libs/wechatpad_api/api/downloadpai.py @@ -1,32 +1,23 @@ -from libs.wechatpad_api.util.http_util import async_request, post_json +from libs.wechatpad_api.util.http_util import post_json import httpx import base64 + class DownloadApi: def __init__(self, base_url, token): self.base_url = base_url self.token = token def send_download(self, aeskey, file_type, file_url): - json_data = { - "AesKey": aeskey, - "FileType": file_type, - "FileURL": file_url - } - url = self.base_url + "/message/SendCdnDownload" + json_data = {'AesKey': aeskey, 'FileType': file_type, 'FileURL': file_url} + url = self.base_url + '/message/SendCdnDownload' return post_json(url, token=self.token, data=json_data) - def get_msg_voice(self,buf_id, length, new_msgid): - json_data = { - "Bufid": buf_id, - "Length": length, - "NewMsgId": new_msgid, - "ToUserName": "" - } - url = self.base_url + "/message/GetMsgVoice" + def get_msg_voice(self, buf_id, length, new_msgid): + json_data = {'Bufid': buf_id, 'Length': length, 'NewMsgId': new_msgid, 'ToUserName': ''} + url = self.base_url + '/message/GetMsgVoice' return post_json(url, token=self.token, data=json_data) - async def download_url_to_base64(self, download_url): async with httpx.AsyncClient() as client: response = await client.get(download_url) @@ -36,4 +27,4 @@ class DownloadApi: base64_str = base64.b64encode(file_bytes).decode('utf-8') # 返回字符串格式 return base64_str else: - raise Exception('获取文件失败') \ No newline at end of file + raise Exception('获取文件失败') diff --git a/libs/wechatpad_api/api/friend.py b/libs/wechatpad_api/api/friend.py index 00701a5d..a7a448aa 100644 --- a/libs/wechatpad_api/api/friend.py +++ b/libs/wechatpad_api/api/friend.py @@ -1,11 +1,6 @@ -from libs.wechatpad_api.util.http_util import post_json,async_request -from typing import List, Dict, Any, Optional - - class FriendApi: """联系人API类,处理所有与联系人相关的操作""" def __init__(self, base_url: str, token: str): self.base_url = base_url self.token = token - diff --git a/libs/wechatpad_api/api/login.py b/libs/wechatpad_api/api/login.py index 142a3c85..4aa4ae8d 100644 --- a/libs/wechatpad_api/api/login.py +++ b/libs/wechatpad_api/api/login.py @@ -1,37 +1,34 @@ -from libs.wechatpad_api.util.http_util import async_request,post_json,get_json +from libs.wechatpad_api.util.http_util import post_json, get_json class LoginApi: def __init__(self, base_url: str, token: str = None, admin_key: str = None): - ''' + """ Args: base_url: 原始路径 token: token admin_key: 管理员key - ''' + """ self.base_url = base_url self.token = token # self.admin_key = admin_key - def get_token(self, admin_key, day: int=365): + def get_token(self, admin_key, day: int = 365): # 获取普通token - url = f"{self.base_url}/admin/GenAuthKey1" - json_data = { - "Count": 1, - "Days": day - } + url = f'{self.base_url}/admin/GenAuthKey1' + json_data = {'Count': 1, 'Days': day} return post_json(base_url=url, token=admin_key, data=json_data) - def get_login_qr(self, Proxy: str = ""): - ''' + def get_login_qr(self, Proxy: str = ''): + """ Args: Proxy:异地使用时代理 Returns:json数据 - ''' + """ """ { @@ -49,54 +46,37 @@ class LoginApi: } """ - #获取登录二维码 - url = f"{self.base_url}/login/GetLoginQrCodeNew" + # 获取登录二维码 + url = f'{self.base_url}/login/GetLoginQrCodeNew' check = False - if Proxy != "": + if Proxy != '': check = True - json_data = { - "Check": check, - "Proxy": Proxy - } + json_data = {'Check': check, 'Proxy': Proxy} return post_json(base_url=url, token=self.token, data=json_data) - def get_login_status(self): # 获取登录状态 url = f'{self.base_url}/login/GetLoginStatus' return get_json(base_url=url, token=self.token) - - def logout(self): # 退出登录 url = f'{self.base_url}/login/LogOut' return post_json(base_url=url, token=self.token) - - - - def wake_up_login(self, Proxy: str = ""): + def wake_up_login(self, Proxy: str = ''): # 唤醒登录 url = f'{self.base_url}/login/WakeUpLogin' check = False - if Proxy != "": + if Proxy != '': check = True - json_data = { - "Check": check, - "Proxy": "" - } + json_data = {'Check': check, 'Proxy': ''} return post_json(base_url=url, token=self.token, data=json_data) - - - def login(self,admin_key): + def login(self, admin_key): login_status = self.get_login_status() - if login_status["Code"] == 300 and login_status["Text"] == "你已退出微信": - print("token已经失效,重新获取") + if login_status['Code'] == 300 and login_status['Text'] == '你已退出微信': + print('token已经失效,重新获取') token_data = self.get_token(admin_key) - self.token = token_data["Data"][0] - - - + self.token = token_data['Data'][0] diff --git a/libs/wechatpad_api/api/message.py b/libs/wechatpad_api/api/message.py index 2089ce96..cca76313 100644 --- a/libs/wechatpad_api/api/message.py +++ b/libs/wechatpad_api/api/message.py @@ -1,5 +1,4 @@ - -from libs.wechatpad_api.util.http_util import async_request, post_json +from libs.wechatpad_api.util.http_util import post_json class MessageApi: @@ -7,8 +6,8 @@ class MessageApi: self.base_url = base_url self.token = token - def post_text(self, to_wxid, content, ats: list= []): - ''' + def post_text(self, to_wxid, content, ats: list = []): + """ Args: app_id: 微信id @@ -18,106 +17,64 @@ class MessageApi: Returns: - ''' - url = self.base_url + "/message/SendTextMessage" + """ + url = self.base_url + '/message/SendTextMessage' """发送文字消息""" json_data = { - "MsgItem": [ - { - "AtWxIDList": ats, - "ImageContent": "", - "MsgType": 0, - "TextContent": content, - "ToUserName": to_wxid - } - ] - } - return post_json(base_url=url, token=self.token, data=json_data) + 'MsgItem': [ + {'AtWxIDList': ats, 'ImageContent': '', 'MsgType': 0, 'TextContent': content, 'ToUserName': to_wxid} + ] + } + return post_json(base_url=url, token=self.token, data=json_data) - - - - def post_image(self, to_wxid, img_url, ats: list= []): + def post_image(self, to_wxid, img_url, ats: list = []): """发送图片消息""" # 这里好像可以尝试发送多个暂时未测试 json_data = { - "MsgItem": [ - { - "AtWxIDList": ats, - "ImageContent": img_url, - "MsgType": 0, - "TextContent": '', - "ToUserName": to_wxid - } + 'MsgItem': [ + {'AtWxIDList': ats, 'ImageContent': img_url, 'MsgType': 0, 'TextContent': '', 'ToUserName': to_wxid} ] } - url = self.base_url + "/message/SendImageMessage" + url = self.base_url + '/message/SendImageMessage' return post_json(base_url=url, token=self.token, data=json_data) def post_voice(self, to_wxid, voice_data, voice_forma, voice_duration): """发送语音消息""" json_data = { - "ToUserName": to_wxid, - "VoiceData": voice_data, - "VoiceFormat": voice_forma, - "VoiceSecond": voice_duration + 'ToUserName': to_wxid, + 'VoiceData': voice_data, + 'VoiceFormat': voice_forma, + 'VoiceSecond': voice_duration, } - url = self.base_url + "/message/SendVoice" + url = self.base_url + '/message/SendVoice' return post_json(base_url=url, token=self.token, data=json_data) - - - - def post_name_card(self, alias, to_wxid, nick_name, name_card_wxid, flag): """发送名片消息""" param = { - "CardAlias": alias, - "CardFlag": flag, - "CardNickName": nick_name, - "CardWxId": name_card_wxid, - "ToUserName": to_wxid + 'CardAlias': alias, + 'CardFlag': flag, + 'CardNickName': nick_name, + 'CardWxId': name_card_wxid, + 'ToUserName': to_wxid, } - url = f"{self.base_url}/message/ShareCardMessage" + url = f'{self.base_url}/message/ShareCardMessage' return post_json(base_url=url, token=self.token, data=param) - def post_emoji(self, to_wxid, emoji_md5, emoji_size:int=0): + def post_emoji(self, to_wxid, emoji_md5, emoji_size: int = 0): """发送emoji消息""" - json_data = { - "EmojiList": [ - { - "EmojiMd5": emoji_md5, - "EmojiSize": emoji_size, - "ToUserName": to_wxid - } - ] - } - url = f"{self.base_url}/message/SendEmojiMessage" + json_data = {'EmojiList': [{'EmojiMd5': emoji_md5, 'EmojiSize': emoji_size, 'ToUserName': to_wxid}]} + url = f'{self.base_url}/message/SendEmojiMessage' return post_json(base_url=url, token=self.token, data=json_data) - def post_app_msg(self, to_wxid,xml_data, contenttype:int=0): + def post_app_msg(self, to_wxid, xml_data, contenttype: int = 0): """发送appmsg消息""" - json_data = { - "AppList": [ - { - "ContentType": contenttype, - "ContentXML": xml_data, - "ToUserName": to_wxid - } - ] - } - url = f"{self.base_url}/message/SendAppMessage" + json_data = {'AppList': [{'ContentType': contenttype, 'ContentXML': xml_data, 'ToUserName': to_wxid}]} + url = f'{self.base_url}/message/SendAppMessage' return post_json(base_url=url, token=self.token, data=json_data) - - def revoke_msg(self, to_wxid, msg_id, new_msg_id, create_time): """撤回消息""" - param = { - "ClientMsgId": msg_id, - "CreateTime": create_time, - "NewMsgId": new_msg_id, - "ToUserName": to_wxid - } - url = f"{self.base_url}/message/RevokeMsg" - return post_json(base_url=url, token=self.token, data=param) \ No newline at end of file + param = {'ClientMsgId': msg_id, 'CreateTime': create_time, 'NewMsgId': new_msg_id, 'ToUserName': to_wxid} + url = f'{self.base_url}/message/RevokeMsg' + return post_json(base_url=url, token=self.token, data=param) diff --git a/libs/wechatpad_api/util/http_util.py b/libs/wechatpad_api/util/http_util.py index 754003e9..447c29df 100644 --- a/libs/wechatpad_api/util/http_util.py +++ b/libs/wechatpad_api/util/http_util.py @@ -1,10 +1,9 @@ import requests +import aiohttp + def post_json(base_url, token, data=None): - headers = { - 'Content-Type': 'application/json' - } - + headers = {'Content-Type': 'application/json'} url = base_url + f'?key={token}' @@ -18,14 +17,12 @@ def post_json(base_url, token, data=None): else: raise RuntimeError(response.text) except Exception as e: - print(f"http请求失败, url={url}, exception={e}") + print(f'http请求失败, url={url}, exception={e}') raise RuntimeError(str(e)) -def get_json(base_url, token): - headers = { - 'Content-Type': 'application/json' - } +def get_json(base_url, token): + headers = {'Content-Type': 'application/json'} url = base_url + f'?key={token}' @@ -39,21 +36,18 @@ def get_json(base_url, token): else: raise RuntimeError(response.text) except Exception as e: - print(f"http请求失败, url={url}, exception={e}") + print(f'http请求失败, url={url}, exception={e}') raise RuntimeError(str(e)) -import aiohttp -import asyncio - async def async_request( - base_url: str, - token_key: str, - method: str = 'POST', - params: dict = None, - # headers: dict = None, - data: dict = None, - json: dict = None + base_url: str, + token_key: str, + method: str = 'POST', + params: dict = None, + # headers: dict = None, + data: dict = None, + json: dict = None, ): """ 通用异步请求函数 @@ -67,18 +61,11 @@ async def async_request( :param json: JSON数据 :return: 响应文本 """ - headers = { - 'Content-Type': 'application/json' - } - url = f"{base_url}?key={token_key}" + headers = {'Content-Type': 'application/json'} + url = f'{base_url}?key={token_key}' async with aiohttp.ClientSession() as session: async with session.request( - method=method, - url=url, - params=params, - headers=headers, - data=data, - json=json + method=method, url=url, params=params, headers=headers, data=data, json=json ) as response: response.raise_for_status() # 如果状态码不是200,抛出异常 result = await response.json() @@ -89,4 +76,3 @@ async def async_request( # return await result # else: # raise RuntimeError("请求失败",response.text) - diff --git a/pkg/api/http/controller/groups/pipelines/webchat.py b/pkg/api/http/controller/groups/pipelines/webchat.py index f8698b01..62e5da3f 100644 --- a/pkg/api/http/controller/groups/pipelines/webchat.py +++ b/pkg/api/http/controller/groups/pipelines/webchat.py @@ -14,8 +14,9 @@ class WebChatDebugRouterGroup(group.RouterGroup): async def stream_generator(generator): async for message in generator: - yield f"data: {json.dumps({'message': message})}\n\n" - yield "data: {\"type\": \"end\"}\n\n" + yield f'data: {json.dumps({"message": message})}\n\n' + yield 'data: {"type": "end"}\n\n' + try: data = await quart.request.get_json() session_type = data.get('session_type', 'person') @@ -34,18 +35,18 @@ class WebChatDebugRouterGroup(group.RouterGroup): return self.http_status(404, -1, 'WebChat adapter not found') if is_stream: - - generator = webchat_adapter.send_webchat_message(pipeline_uuid, session_type, message_chain_obj, is_stream) - - return quart.Response( - stream_generator(generator), - mimetype='text/event-stream' + generator = webchat_adapter.send_webchat_message( + pipeline_uuid, session_type, message_chain_obj, is_stream ) + return quart.Response(stream_generator(generator), mimetype='text/event-stream') + else: # result = await webchat_adapter.send_webchat_message(pipeline_uuid, session_type, message_chain_obj) result = None - async for message in webchat_adapter.send_webchat_message(pipeline_uuid, session_type, message_chain_obj): + async for message in webchat_adapter.send_webchat_message( + pipeline_uuid, session_type, message_chain_obj + ): result = message if result is not None: return self.success( @@ -56,7 +57,6 @@ class WebChatDebugRouterGroup(group.RouterGroup): else: return self.http_status(400, -1, 'message is required') - except Exception as e: return self.http_status(500, -1, f'Internal server error: {str(e)}') diff --git a/pkg/core/entities.py b/pkg/core/entities.py index 4873d9ce..1efee3fc 100644 --- a/pkg/core/entities.py +++ b/pkg/core/entities.py @@ -87,7 +87,9 @@ class Query(pydantic.BaseModel): """使用的函数,由前置处理器阶段设置""" resp_messages: ( - typing.Optional[list[llm_entities.Message]] | typing.Optional[list[platform_message.MessageChain]] | typing.Optional[list[llm_entities.MessageChunk]] + typing.Optional[list[llm_entities.Message]] + | typing.Optional[list[platform_message.MessageChain]] + | typing.Optional[list[llm_entities.MessageChunk]] ) = [] """由Process阶段生成的回复消息对象列表""" diff --git a/pkg/pipeline/cntfilter/cntfilter.py b/pkg/pipeline/cntfilter/cntfilter.py index 0bbc5103..e035c1d0 100644 --- a/pkg/pipeline/cntfilter/cntfilter.py +++ b/pkg/pipeline/cntfilter/cntfilter.py @@ -67,7 +67,7 @@ class ContentFilterStage(stage.PipelineStage): if query.pipeline_config['safety']['content-filter']['scope'] == 'output-msg': return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) if not message.strip(): - return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) + return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) else: for filter in self.filter_chain: if filter_entities.EnableStage.PRE in filter.enable_stages: diff --git a/pkg/pipeline/process/handlers/chat.py b/pkg/pipeline/process/handlers/chat.py index 483dd0b7..a81d8e3f 100644 --- a/pkg/pipeline/process/handlers/chat.py +++ b/pkg/pipeline/process/handlers/chat.py @@ -1,7 +1,6 @@ from __future__ import annotations import uuid -from itertools import accumulate import typing import traceback @@ -82,9 +81,7 @@ class ChatMessageHandler(handler.MessageHandler): query.resp_message_chain.pop() query.resp_messages.append(result) - self.ap.logger.info( - f'对话({query.query_id})流式响应: {self.cut_str(result.readable_str())}' - ) + self.ap.logger.info(f'对话({query.query_id})流式响应: {self.cut_str(result.readable_str())}') if result.content is not None: text_length += len(result.content) diff --git a/pkg/pipeline/respback/respback.py b/pkg/pipeline/respback/respback.py index f4153218..c7824856 100644 --- a/pkg/pipeline/respback/respback.py +++ b/pkg/pipeline/respback/respback.py @@ -3,12 +3,10 @@ from __future__ import annotations import random import asyncio -from typing_inspection.typing_objects import is_final from ...platform.types import events as platform_events from ...platform.types import message as platform_message -from ...provider import entities as llm_entities from .. import stage, entities from ...core import entities as core_entities @@ -56,6 +54,4 @@ class SendResponseBackStage(stage.PipelineStage): quote_origin=quote_origin, ) - - return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) diff --git a/pkg/platform/adapter.py b/pkg/platform/adapter.py index e4369efb..3412be3c 100644 --- a/pkg/platform/adapter.py +++ b/pkg/platform/adapter.py @@ -25,7 +25,6 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): logger: EventLogger - def __init__(self, config: dict, ap: app.Application, logger: EventLogger): """初始化适配器 @@ -80,12 +79,12 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): """ raise NotImplementedError - async def create_message_card(self, message_id:typing.Type[str,int], event:platform_events.MessageEvent) -> bool: + async def create_message_card(self, message_id: typing.Type[str, int], event: platform_events.MessageEvent) -> bool: """创建卡片消息 Args: message_id (str): 消息ID event (platform_events.MessageEvent): 消息源事件 - """ + """ return False async def is_muted(self, group_id: int) -> bool: @@ -94,8 +93,8 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): def register_listener( self, - event_type: typing.Type[platform_message.Event], - callback: typing.Callable[[platform_message.Event, MessagePlatformAdapter], None], + event_type: typing.Type[platform_events.Event], + callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], ): """注册事件监听器 @@ -107,8 +106,8 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): def unregister_listener( self, - event_type: typing.Type[platform_message.Event], - callback: typing.Callable[[platform_message.Event, MessagePlatformAdapter], None], + event_type: typing.Type[platform_events.Event], + callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], ): """注销事件监听器 @@ -167,7 +166,7 @@ class EventConverter: """事件转换器基类""" @staticmethod - def yiri2target(event: typing.Type[platform_message.Event]): + def yiri2target(event: typing.Type[platform_events.Event]): """将源平台事件转换为目标平台事件 Args: @@ -179,7 +178,7 @@ class EventConverter: raise NotImplementedError @staticmethod - def target2yiri(event: typing.Any) -> platform_message.Event: + def target2yiri(event: typing.Any) -> platform_events.Event: """将目标平台事件的调用参数转换为源平台的事件参数对象 Args: diff --git a/pkg/platform/sources/aiocqhttp.py b/pkg/platform/sources/aiocqhttp.py index 3f3ef512..c75d2c77 100644 --- a/pkg/platform/sources/aiocqhttp.py +++ b/pkg/platform/sources/aiocqhttp.py @@ -16,7 +16,6 @@ from ..logger import EventLogger class AiocqhttpMessageConverter(adapter.MessageConverter): - @staticmethod async def yiri2target( message_chain: platform_message.MessageChain, @@ -62,87 +61,170 @@ class AiocqhttpMessageConverter(adapter.MessageConverter): for node in msg.node_list: msg_list.extend((await AiocqhttpMessageConverter.yiri2target(node.message_chain))[0]) elif isinstance(msg, platform_message.File): - msg_list.append({"type":"file", "data":{'file': msg.url, "name": msg.name}}) + msg_list.append({'type': 'file', 'data': {'file': msg.url, 'name': msg.name}}) elif isinstance(msg, platform_message.Face): - if msg.face_type=='face': + if msg.face_type == 'face': msg_list.append(aiocqhttp.MessageSegment.face(msg.face_id)) - elif msg.face_type=='rps': + elif msg.face_type == 'rps': msg_list.append(aiocqhttp.MessageSegment.rps()) - elif msg.face_type=='dice': + elif msg.face_type == 'dice': msg_list.append(aiocqhttp.MessageSegment.dice()) - else: msg_list.append(aiocqhttp.MessageSegment.text(str(msg))) return msg_list, msg_id, msg_time @staticmethod - async def target2yiri(message: str, message_id: int = -1,bot=None): + async def target2yiri(message: str, message_id: int = -1, bot=None): print(message) message = aiocqhttp.Message(message) def get_face_name(face_id): face_code_dict = { - "2": '好色', - "4": "得意", "5": "流泪", "8": "睡", "9": "大哭", "10": "尴尬", "12": "调皮", "14": "微笑", "16": "酷", - "21": "可爱", - "23": "傲慢", "24": "饥饿", "25": "困", "26": "惊恐", "27": "流汗", "28": "憨笑", "29": "悠闲", - "30": "奋斗", - "32": "疑问", "33": "嘘", "34": "晕", "38": "敲打", "39": "再见", "41": "发抖", "42": "爱情", - "43": "跳跳", - "49": "拥抱", "53": "蛋糕", "60": "咖啡", "63": "玫瑰", "66": "爱心", "74": "太阳", "75": "月亮", - "76": "赞", - "78": "握手", "79": "胜利", "85": "飞吻", "89": "西瓜", "96": "冷汗", "97": "擦汗", "98": "抠鼻", - "99": "鼓掌", - "100": "糗大了", "101": "坏笑", "102": "左哼哼", "103": "右哼哼", "104": "哈欠", "106": "委屈", - "109": "左亲亲", - "111": "可怜", "116": "示爱", "118": "抱拳", "120": "拳头", "122": "爱你", "123": "NO", "124": "OK", - "125": "转圈", - "129": "挥手", "144": "喝彩", "147": "棒棒糖", "171": "茶", "173": "泪奔", "174": "无奈", "175": "卖萌", - "176": "小纠结", "179": "doge", "180": "惊喜", "181": "骚扰", "182": "笑哭", "183": "我最美", - "201": "点赞", - "203": "托脸", "212": "托腮", "214": "啵啵", "219": "蹭一蹭", "222": "抱抱", "227": "拍手", - "232": "佛系", - "240": "喷脸", "243": "甩头", "246": "加油抱抱", "262": "脑阔疼", "264": "捂脸", "265": "辣眼睛", - "266": "哦哟", - "267": "头秃", "268": "问号脸", "269": "暗中观察", "270": "emm", "271": "吃瓜", "272": "呵呵哒", - "273": "我酸了", - "277": "汪汪", "278": "汗", "281": "无眼笑", "282": "敬礼", "284": "面无表情", "285": "摸鱼", - "287": "哦", - "289": "睁眼", "290": "敲开心", "293": "摸锦鲤", "294": "期待", "297": "拜谢", "298": "元宝", - "299": "牛啊", - "305": "右亲亲", "306": "牛气冲天", "307": "喵喵", "314": "仔细分析", "315": "加油", "318": "崇拜", - "319": "比心", - "320": "庆祝", "322": "拒绝", "324": "吃糖", "326": "生气" + '2': '好色', + '4': '得意', + '5': '流泪', + '8': '睡', + '9': '大哭', + '10': '尴尬', + '12': '调皮', + '14': '微笑', + '16': '酷', + '21': '可爱', + '23': '傲慢', + '24': '饥饿', + '25': '困', + '26': '惊恐', + '27': '流汗', + '28': '憨笑', + '29': '悠闲', + '30': '奋斗', + '32': '疑问', + '33': '嘘', + '34': '晕', + '38': '敲打', + '39': '再见', + '41': '发抖', + '42': '爱情', + '43': '跳跳', + '49': '拥抱', + '53': '蛋糕', + '60': '咖啡', + '63': '玫瑰', + '66': '爱心', + '74': '太阳', + '75': '月亮', + '76': '赞', + '78': '握手', + '79': '胜利', + '85': '飞吻', + '89': '西瓜', + '96': '冷汗', + '97': '擦汗', + '98': '抠鼻', + '99': '鼓掌', + '100': '糗大了', + '101': '坏笑', + '102': '左哼哼', + '103': '右哼哼', + '104': '哈欠', + '106': '委屈', + '109': '左亲亲', + '111': '可怜', + '116': '示爱', + '118': '抱拳', + '120': '拳头', + '122': '爱你', + '123': 'NO', + '124': 'OK', + '125': '转圈', + '129': '挥手', + '144': '喝彩', + '147': '棒棒糖', + '171': '茶', + '173': '泪奔', + '174': '无奈', + '175': '卖萌', + '176': '小纠结', + '179': 'doge', + '180': '惊喜', + '181': '骚扰', + '182': '笑哭', + '183': '我最美', + '201': '点赞', + '203': '托脸', + '212': '托腮', + '214': '啵啵', + '219': '蹭一蹭', + '222': '抱抱', + '227': '拍手', + '232': '佛系', + '240': '喷脸', + '243': '甩头', + '246': '加油抱抱', + '262': '脑阔疼', + '264': '捂脸', + '265': '辣眼睛', + '266': '哦哟', + '267': '头秃', + '268': '问号脸', + '269': '暗中观察', + '270': 'emm', + '271': '吃瓜', + '272': '呵呵哒', + '273': '我酸了', + '277': '汪汪', + '278': '汗', + '281': '无眼笑', + '282': '敬礼', + '284': '面无表情', + '285': '摸鱼', + '287': '哦', + '289': '睁眼', + '290': '敲开心', + '293': '摸锦鲤', + '294': '期待', + '297': '拜谢', + '298': '元宝', + '299': '牛啊', + '305': '右亲亲', + '306': '牛气冲天', + '307': '喵喵', + '314': '仔细分析', + '315': '加油', + '318': '崇拜', + '319': '比心', + '320': '庆祝', + '322': '拒绝', + '324': '吃糖', + '326': '生气', } - return face_code_dict.get(face_id,'') + return face_code_dict.get(face_id, '') async def process_message_data(msg_data, reply_list): - if msg_data["type"] == "image": - image_base64, image_format = await image.qq_image_url_to_base64(msg_data["data"]['url']) - reply_list.append( - platform_message.Image(base64=f'data:image/{image_format};base64,{image_base64}')) + if msg_data['type'] == 'image': + image_base64, image_format = await image.qq_image_url_to_base64(msg_data['data']['url']) + reply_list.append(platform_message.Image(base64=f'data:image/{image_format};base64,{image_base64}')) - elif msg_data["type"] == "text": - reply_list.append(platform_message.Plain(text=msg_data["data"]["text"])) + elif msg_data['type'] == 'text': + reply_list.append(platform_message.Plain(text=msg_data['data']['text'])) - elif msg_data["type"] == "forward": # 这里来应该传入转发消息组,暂时传入qoute - for forward_msg_datas in msg_data["data"]["content"]: - for forward_msg_data in forward_msg_datas["message"]: + elif msg_data['type'] == 'forward': # 这里来应该传入转发消息组,暂时传入qoute + for forward_msg_datas in msg_data['data']['content']: + for forward_msg_data in forward_msg_datas['message']: await process_message_data(forward_msg_data, reply_list) - elif msg_data["type"] == "at": - if msg_data["data"]['qq'] == 'all': + elif msg_data['type'] == 'at': + if msg_data['data']['qq'] == 'all': reply_list.append(platform_message.AtAll()) else: reply_list.append( platform_message.At( - target=msg_data["data"]['qq'], + target=msg_data['data']['qq'], ) ) - yiri_msg_list = [] yiri_msg_list.append(platform_message.Source(id=message_id, time=datetime.datetime.now())) @@ -161,10 +243,10 @@ class AiocqhttpMessageConverter(adapter.MessageConverter): elif msg.type == 'text': yiri_msg_list.append(platform_message.Plain(text=msg.data['text'])) elif msg.type == 'image': - emoji_id = msg.data.get("emoji_package_id", None) + emoji_id = msg.data.get('emoji_package_id', None) if emoji_id: face_id = emoji_id - face_name = msg.data.get("summary", '') + face_name = msg.data.get('summary', '') image_msg = platform_message.Face(face_id=face_id, face_name=face_name) else: image_base64, image_format = await image.qq_image_url_to_base64(msg.data['url']) @@ -178,14 +260,15 @@ class AiocqhttpMessageConverter(adapter.MessageConverter): # await process_message_data(msg_data, yiri_msg_list) pass - elif msg.type == 'reply': # 此处处理引用消息传入Qoute - msg_datas = await bot.get_msg(message_id=msg.data["id"]) + msg_datas = await bot.get_msg(message_id=msg.data['id']) - for msg_data in msg_datas["message"]: + for msg_data in msg_datas['message']: await process_message_data(msg_data, reply_list) - reply_msg = platform_message.Quote(message_id=msg.data["id"],sender_id=msg_datas["user_id"],origin=reply_list) + reply_msg = platform_message.Quote( + message_id=msg.data['id'], sender_id=msg_datas['user_id'], origin=reply_list + ) yiri_msg_list.append(reply_msg) elif msg.type == 'file': @@ -193,50 +276,36 @@ class AiocqhttpMessageConverter(adapter.MessageConverter): file_id = msg.data['file_id'] file_data = await bot.get_file(file_id=file_id) file_name = file_data.get('file_name') - file_path = file_data.get('file') + # file_path = file_data.get('file') file_url = file_data.get('file_url') file_size = file_data.get('file_size') - yiri_msg_list.append(platform_message.File(id=file_id, name=file_name,url=file_url,size=file_size)) + yiri_msg_list.append(platform_message.File(id=file_id, name=file_name, url=file_url, size=file_size)) elif msg.type == 'face': face_id = msg.data['id'] face_name = msg.data['raw']['faceText'] if not face_name: face_name = get_face_name(face_id) - yiri_msg_list.append(platform_message.Face(face_id=int(face_id),face_name=face_name.replace('/',''))) + yiri_msg_list.append(platform_message.Face(face_id=int(face_id), face_name=face_name.replace('/', ''))) elif msg.type == 'rps': face_id = msg.data['result'] - yiri_msg_list.append(platform_message.Face(face_type="rps",face_id=int(face_id),face_name='猜拳')) + yiri_msg_list.append(platform_message.Face(face_type='rps', face_id=int(face_id), face_name='猜拳')) elif msg.type == 'dice': face_id = msg.data['result'] - yiri_msg_list.append(platform_message.Face(face_type='dice',face_id=int(face_id),face_name='骰子')) - - - - - - - - + yiri_msg_list.append(platform_message.Face(face_type='dice', face_id=int(face_id), face_name='骰子')) chain = platform_message.MessageChain(yiri_msg_list) return chain - - - - class AiocqhttpEventConverter(adapter.EventConverter): @staticmethod async def yiri2target(event: platform_events.MessageEvent, bot_account_id: int): return event.source_platform_object @staticmethod - async def target2yiri(event: aiocqhttp.Event,bot=None): - yiri_chain = await AiocqhttpMessageConverter.target2yiri(event.message, event.message_id,bot) - - + async def target2yiri(event: aiocqhttp.Event, bot=None): + yiri_chain = await AiocqhttpMessageConverter.target2yiri(event.message, event.message_id, bot) if event.message_type == 'group': permission = 'MEMBER' @@ -316,7 +385,6 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter): aiocq_msg = (await AiocqhttpMessageConverter.yiri2target(message))[0] if target_type == 'group': - await self.bot.send_group_msg(group_id=int(target_id), message=aiocq_msg) elif target_type == 'person': await self.bot.send_private_msg(user_id=int(target_id), message=aiocq_msg) @@ -345,7 +413,7 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter): async def on_message(event: aiocqhttp.Event): self.bot_account_id = event.self_id try: - return await callback(await self.event_converter.target2yiri(event,self.bot), self) + return await callback(await self.event_converter.target2yiri(event, self.bot), self) except Exception: await self.logger.error(f'Error in on_message: {traceback.format_exc()}') traceback.print_exc() diff --git a/pkg/platform/sources/dingtalk.py b/pkg/platform/sources/dingtalk.py index 187bafb0..8bd6e187 100644 --- a/pkg/platform/sources/dingtalk.py +++ b/pkg/platform/sources/dingtalk.py @@ -149,10 +149,10 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): quote_origin: bool = False, is_final: bool = False, ): - event = await DingTalkEventConverter.yiri2target( - message_source, - ) - incoming_message = event.incoming_message + # event = await DingTalkEventConverter.yiri2target( + # message_source, + # ) + # incoming_message = event.incoming_message # msg_id = incoming_message.message_id @@ -205,7 +205,6 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): self.bot.on_message('GroupMessage')(on_message) async def run_async(self): - await self.bot.start() async def kill(self) -> bool: diff --git a/pkg/platform/sources/discord.py b/pkg/platform/sources/discord.py index 4f5cac28..6cc09a72 100644 --- a/pkg/platform/sources/discord.py +++ b/pkg/platform/sources/discord.py @@ -8,7 +8,6 @@ import base64 import uuid import os import datetime -import io import aiohttp @@ -78,10 +77,10 @@ class DiscordMessageConverter(adapter.MessageConverter): # 确保路径没有空字节 clean_path = ele.path.replace('\x00', '') clean_path = os.path.abspath(clean_path) - + if not os.path.exists(clean_path): continue # 跳过不存在的文件 - + try: with open(clean_path, 'rb') as f: image_bytes = f.read() @@ -101,12 +100,13 @@ class DiscordMessageConverter(adapter.MessageConverter): filename = f'{uuid.uuid4()}.webp' # 默认保持PNG except Exception as e: - print(f"Error reading image file {clean_path}: {e}") + print(f'Error reading image file {clean_path}: {e}') continue # 跳过读取失败的文件 if image_bytes: # 使用BytesIO创建文件对象,避免路径问题 import io + image_files.append(discord.File(fp=io.BytesIO(image_bytes), filename=filename)) elif isinstance(ele, platform_message.Plain): text_string += ele.text @@ -261,25 +261,25 @@ class DiscordAdapter(adapter.MessagePlatformAdapter): async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): msg_to_send, image_files = await self.message_converter.yiri2target(message) - + try: # 获取频道对象 channel = self.bot.get_channel(int(target_id)) if channel is None: # 如果本地缓存中没有,尝试从API获取 channel = await self.bot.fetch_channel(int(target_id)) - + args = { 'content': msg_to_send, } - + if len(image_files) > 0: args['files'] = image_files - + await channel.send(**args) - + except Exception as e: - await self.logger.error(f"Discord send_message failed: {e}") + await self.logger.error(f'Discord send_message failed: {e}') raise e async def reply_message( diff --git a/pkg/platform/sources/lark.py b/pkg/platform/sources/lark.py index dcafbf9f..5369be00 100644 --- a/pkg/platform/sources/lark.py +++ b/pkg/platform/sources/lark.py @@ -9,7 +9,6 @@ import re import base64 import uuid import json -import time import datetime import hashlib from Crypto.Cipher import AES @@ -394,14 +393,14 @@ class LarkAdapter(adapter.MessagePlatformAdapter): if 'im.message.receive_v1' == type: try: event = await self.event_converter.target2yiri(p2v1, self.api_client) - except Exception as e: + except Exception: await self.logger.error(f'Error in lark callback: {traceback.format_exc()}') if event.__class__ in self.listeners: await self.listeners[event.__class__](event, self) return {'code': 200, 'message': 'ok'} - except Exception as e: + except Exception: await self.logger.error(f'Error in lark callback: {traceback.format_exc()}') return {'code': 500, 'message': 'error'} @@ -559,10 +558,10 @@ class LarkAdapter(adapter.MessagePlatformAdapter): elif ele['tag'] == 'md': text_message += ele['text'] - content = { - 'type': 'card_json', - 'data': {'card_id': self.card_id_dict[message_id], 'elements': {'content': text_message}}, - } + # content = { + # 'type': 'card_json', + # 'data': {'card_id': self.card_id_dict[message_id], 'elements': {'content': text_message}}, + # } request: ContentCardElementRequest = ( ContentCardElementRequest.builder() diff --git a/pkg/platform/sources/nakuru.py b/pkg/platform/sources/nakuru.py index 389a2db1..16ad54db 100644 --- a/pkg/platform/sources/nakuru.py +++ b/pkg/platform/sources/nakuru.py @@ -72,8 +72,9 @@ class NakuruProjectMessageConverter(adapter_model.MessageConverter): content=content_list, ) nakuru_forward_node_list.append(nakuru_forward_node) - except Exception as e: + except Exception: import traceback + traceback.print_exc() nakuru_msg_list.append(nakuru_forward_node_list) @@ -276,7 +277,7 @@ class NakuruAdapter(adapter_model.MessagePlatformAdapter): # 注册监听器 self.bot.receiver(source_cls.__name__)(listener_wrapper) except Exception as e: - self.logger.error(f"Error in nakuru register_listener: {traceback.format_exc()}") + self.logger.error(f'Error in nakuru register_listener: {traceback.format_exc()}') raise e def unregister_listener( diff --git a/pkg/platform/sources/officialaccount.py b/pkg/platform/sources/officialaccount.py index 030db56d..3fc1e393 100644 --- a/pkg/platform/sources/officialaccount.py +++ b/pkg/platform/sources/officialaccount.py @@ -125,8 +125,8 @@ class OfficialAccountAdapter(adapter.MessagePlatformAdapter): self.bot_account_id = event.receiver_id try: return await callback(await self.event_converter.target2yiri(event), self) - except Exception as e: - await self.logger.error(f"Error in officialaccount callback: {traceback.format_exc()}") + except Exception: + await self.logger.error(f'Error in officialaccount callback: {traceback.format_exc()}') if event_type == platform_events.FriendMessage: self.bot.on_message('text')(on_message) diff --git a/pkg/platform/sources/qqbotpy.py b/pkg/platform/sources/qqbotpy.py index 39c8dc8a..d4a4d526 100644 --- a/pkg/platform/sources/qqbotpy.py +++ b/pkg/platform/sources/qqbotpy.py @@ -501,7 +501,7 @@ class OfficialAdapter(adapter_model.MessagePlatformAdapter): for event_handler in event_handler_mapping[event_type]: setattr(self.bot, event_handler, wrapper) except Exception as e: - self.logger.error(f"Error in qqbotpy callback: {traceback.format_exc()}") + self.logger.error(f'Error in qqbotpy callback: {traceback.format_exc()}') raise e def unregister_listener( diff --git a/pkg/platform/sources/qqofficial.py b/pkg/platform/sources/qqofficial.py index c61afea4..63ab531f 100644 --- a/pkg/platform/sources/qqofficial.py +++ b/pkg/platform/sources/qqofficial.py @@ -154,10 +154,7 @@ class QQOfficialAdapter(adapter.MessagePlatformAdapter): raise ParamNotEnoughError('QQ官方机器人缺少相关配置项,请查看文档或联系管理员') self.bot = QQOfficialClient( - app_id=config['appid'], - secret=config['secret'], - token=config['token'], - logger=self.logger + app_id=config['appid'], secret=config['secret'], token=config['token'], logger=self.logger ) async def reply_message( @@ -224,8 +221,8 @@ class QQOfficialAdapter(adapter.MessagePlatformAdapter): self.bot_account_id = 'justbot' try: return await callback(await self.event_converter.target2yiri(event), self) - except Exception as e: - await self.logger.error(f"Error in qqofficial callback: {traceback.format_exc()}") + except Exception: + await self.logger.error(f'Error in qqofficial callback: {traceback.format_exc()}') if event_type == platform_events.FriendMessage: self.bot.on_message('DIRECT_MESSAGE_CREATE')(on_message) diff --git a/pkg/platform/sources/slack.py b/pkg/platform/sources/slack.py index 6dfcff59..1bd5aa2d 100644 --- a/pkg/platform/sources/slack.py +++ b/pkg/platform/sources/slack.py @@ -104,7 +104,9 @@ class SlackAdapter(adapter.MessagePlatformAdapter): if missing_keys: raise ParamNotEnoughError('Slack机器人缺少相关配置项,请查看文档或联系管理员') - self.bot = SlackClient(bot_token=self.config['bot_token'], signing_secret=self.config['signing_secret'], logger=self.logger) + self.bot = SlackClient( + bot_token=self.config['bot_token'], signing_secret=self.config['signing_secret'], logger=self.logger + ) async def reply_message( self, @@ -139,8 +141,8 @@ class SlackAdapter(adapter.MessagePlatformAdapter): self.bot_account_id = 'SlackBot' try: return await callback(await self.event_converter.target2yiri(event, self.bot), self) - except Exception as e: - await self.logger.error(f"Error in slack callback: {traceback.format_exc()}") + except Exception: + await self.logger.error(f'Error in slack callback: {traceback.format_exc()}') if event_type == platform_events.FriendMessage: self.bot.on_message('im')(on_message) diff --git a/pkg/platform/sources/telegram.py b/pkg/platform/sources/telegram.py index e021c7b7..d39bf23d 100644 --- a/pkg/platform/sources/telegram.py +++ b/pkg/platform/sources/telegram.py @@ -1,6 +1,5 @@ from __future__ import annotations -import time import telegram import telegram.ext @@ -166,7 +165,7 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): lb_event = await self.event_converter.target2yiri(update, self.bot, self.bot_account_id) await self.listeners[type(lb_event)](lb_event, self) await self.is_stream_output_supported() - except Exception as e: + except Exception: await self.logger.error(f'Error in telegram callback: {traceback.format_exc()}') self.application = ApplicationBuilder().token(self.config['token']).build() diff --git a/pkg/platform/sources/webchat.py b/pkg/platform/sources/webchat.py index f7f3d964..fce28bc2 100644 --- a/pkg/platform/sources/webchat.py +++ b/pkg/platform/sources/webchat.py @@ -133,7 +133,11 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): ) # notify waiter - session = (self.webchat_group_session if isinstance(message_source, platform_events.GroupMessage) else self.webchat_person_session) + session = ( + self.webchat_group_session + if isinstance(message_source, platform_events.GroupMessage) + else self.webchat_person_session + ) if message_source.message_chain.message_id not in session.resp_waiters: # session.resp_waiters[message_source.message_chain.message_id] = asyncio.Queue() queue = session.resp_queues[message_source.message_chain.message_id] @@ -147,10 +151,8 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): # print(message_data) await queue.put(message_data) - - return message_data.model_dump() - + async def is_stream_output_supported(self) -> bool: return self.is_stream @@ -186,7 +188,10 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): await self.logger.info('WebChat调试适配器正在停止') async def send_webchat_message( - self, pipeline_uuid: str, session_type: str, message_chain_obj: typing.List[dict], + self, + pipeline_uuid: str, + session_type: str, + message_chain_obj: typing.List[dict], is_stream: bool = False, ) -> dict: self.is_stream = is_stream @@ -202,7 +207,7 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): if is_stream: use_session.resp_queues[message_id] = asyncio.Queue() - logger.debug(f"Initialized queue for message_id: {message_id}") + logger.debug(f'Initialized queue for message_id: {message_id}') use_session.get_message_list(pipeline_uuid).append( WebChatMessage( diff --git a/pkg/platform/sources/wechatpad.py b/pkg/platform/sources/wechatpad.py index fdd4a69b..5d8ec75d 100644 --- a/pkg/platform/sources/wechatpad.py +++ b/pkg/platform/sources/wechatpad.py @@ -1,5 +1,4 @@ import requests -import websockets import websocket import json import time @@ -10,53 +9,41 @@ from libs.wechatpad_api.client import WeChatPadClient import typing import asyncio import traceback -import time import re import base64 -import uuid -import json -import os import copy -import datetime import threading import quart -import aiohttp from .. import adapter -from ...pipeline.longtext.strategies import forward from ...core import app from ..types import message as platform_message from ..types import events as platform_events from ..types import entities as platform_entities -from ...utils import image from ..logger import EventLogger import xml.etree.ElementTree as ET -from typing import Optional, List, Tuple +from typing import Optional, Tuple from functools import partial import logging -class WeChatPadMessageConverter(adapter.MessageConverter): +class WeChatPadMessageConverter(adapter.MessageConverter): def __init__(self, config: dict): self.config = config - self.bot = WeChatPadClient(self.config["wechatpad_url"],self.config["token"]) - self.logger = logging.getLogger("WeChatPadMessageConverter") + self.bot = WeChatPadClient(self.config['wechatpad_url'], self.config['token']) + self.logger = logging.getLogger('WeChatPadMessageConverter') @staticmethod - async def yiri2target( - message_chain: platform_message.MessageChain - ) -> list[dict]: + async def yiri2target(message_chain: platform_message.MessageChain) -> list[dict]: content_list = [] - current_file_path = os.path.abspath(__file__) - - + # current_file_path = os.path.abspath(__file__) for component in message_chain: if isinstance(component, platform_message.At): - content_list.append({"type": "at", "target": component.target}) + content_list.append({'type': 'at', 'target': component.target}) elif isinstance(component, platform_message.Plain): - content_list.append({"type": "text", "content": component.text}) + content_list.append({'type': 'text', 'content': component.text}) elif isinstance(component, platform_message.Image): if component.url: async with httpx.AsyncClient() as client: @@ -68,15 +55,16 @@ class WeChatPadMessageConverter(adapter.MessageConverter): else: raise Exception('获取文件失败') # pass - content_list.append({"type": "image", "image": base64_str}) + content_list.append({'type': 'image', 'image': base64_str}) elif component.base64: - content_list.append({"type": "image", "image": component.base64}) + content_list.append({'type': 'image', 'image': component.base64}) elif isinstance(component, platform_message.WeChatEmoji): content_list.append( - {'type': 'WeChatEmoji', 'emoji_md5': component.emoji_md5, 'emoji_size': component.emoji_size}) + {'type': 'WeChatEmoji', 'emoji_md5': component.emoji_md5, 'emoji_size': component.emoji_size} + ) elif isinstance(component, platform_message.Voice): - content_list.append({"type": "voice", "data": component.url, "duration": component.length, "forma": 0}) + content_list.append({'type': 'voice', 'data': component.url, 'duration': component.length, 'forma': 0}) elif isinstance(component, platform_message.WeChatAppMsg): content_list.append({'type': 'WeChatAppMsg', 'app_msg': component.app_msg}) elif isinstance(component, platform_message.Forward): @@ -86,28 +74,23 @@ class WeChatPadMessageConverter(adapter.MessageConverter): return content_list - - async def target2yiri( - self, - message: dict, - bot_account_id: str - ) -> platform_message.MessageChain: + async def target2yiri(self, message: dict, bot_account_id: str) -> platform_message.MessageChain: """外部消息转平台消息""" # 数据预处理 message_list = [] ats_bot = False # 是否被@ - content = message["content"]["str"] + content = message['content']['str'] content_no_preifx = content # 群消息则去掉前缀 is_group_message = self._is_group_message(message) if is_group_message: ats_bot = self._ats_bot(message, bot_account_id) - if "@所有人" in content: + if '@所有人' in content: message_list.append(platform_message.AtAll()) elif ats_bot: message_list.append(platform_message.At(target=bot_account_id)) content_no_preifx, _ = self._extract_content_and_sender(content) - msg_type = message["msg_type"] + msg_type = message['msg_type'] # 映射消息类型到处理器方法 handler_map = { @@ -129,11 +112,7 @@ class WeChatPadMessageConverter(adapter.MessageConverter): return platform_message.MessageChain(message_list) - async def _handler_text( - self, - message: Optional[dict], - content_no_preifx: str - ) -> platform_message.MessageChain: + async def _handler_text(self, message: Optional[dict], content_no_preifx: str) -> platform_message.MessageChain: """处理文本消息 (msg_type=1)""" if message and self._is_group_message(message): pattern = r'@\S{1,20}' @@ -141,16 +120,12 @@ class WeChatPadMessageConverter(adapter.MessageConverter): return platform_message.MessageChain([platform_message.Plain(content_no_preifx)]) - async def _handler_image( - self, - message: Optional[dict], - content_no_preifx: str - ) -> platform_message.MessageChain: + async def _handler_image(self, message: Optional[dict], content_no_preifx: str) -> platform_message.MessageChain: """处理图像消息 (msg_type=3)""" try: image_xml = content_no_preifx if not image_xml: - return platform_message.MessageChain([platform_message.Unknown("[图片内容为空]")]) + return platform_message.MessageChain([platform_message.Unknown('[图片内容为空]')]) root = ET.fromstring(image_xml) # 提取img标签的属性 @@ -160,28 +135,22 @@ class WeChatPadMessageConverter(adapter.MessageConverter): cdnthumburl = img_tag.get('cdnthumburl') # cdnmidimgurl = img_tag.get('cdnmidimgurl') - image_data = self.bot.cdn_download(aeskey=aeskey, file_type=1, file_url=cdnthumburl) - if image_data["Data"]['FileData'] == '': + if image_data['Data']['FileData'] == '': image_data = self.bot.cdn_download(aeskey=aeskey, file_type=2, file_url=cdnthumburl) - base64_str = image_data["Data"]['FileData'] + base64_str = image_data['Data']['FileData'] # self.logger.info(f"data:image/png;base64,{base64_str}") - elements = [ - platform_message.Image(base64=f"data:image/png;base64,{base64_str}"), + platform_message.Image(base64=f'data:image/png;base64,{base64_str}'), # platform_message.WeChatForwardImage(xml_data=image_xml) # 微信消息转发 ] return platform_message.MessageChain(elements) except Exception as e: - self.logger.error(f"处理图片失败: {str(e)}") - return platform_message.MessageChain([platform_message.Unknown("[图片处理失败]")]) + self.logger.error(f'处理图片失败: {str(e)}') + return platform_message.MessageChain([platform_message.Unknown('[图片处理失败]')]) - async def _handler_voice( - self, - message: Optional[dict], - content_no_preifx: str - ) -> platform_message.MessageChain: + async def _handler_voice(self, message: Optional[dict], content_no_preifx: str) -> platform_message.MessageChain: """处理语音消息 (msg_type=34)""" message_List = [] try: @@ -197,39 +166,33 @@ class WeChatPadMessageConverter(adapter.MessageConverter): bufid = voicemsg.get('bufid') length = voicemsg.get('voicelength') voice_data = self.bot.get_msg_voice(buf_id=str(bufid), length=int(length), msgid=str(new_msg_id)) - audio_base64 = voice_data["Data"]['Base64'] + audio_base64 = voice_data['Data']['Base64'] # 验证语音数据有效性 if not audio_base64: - message_List.append(platform_message.Unknown(text="[语音内容为空]")) + message_List.append(platform_message.Unknown(text='[语音内容为空]')) return platform_message.MessageChain(message_List) # 转换为平台支持的语音格式(如 Silk 格式) - voice_element = platform_message.Voice( - base64=f"data:audio/silk;base64,{audio_base64}" - ) + voice_element = platform_message.Voice(base64=f'data:audio/silk;base64,{audio_base64}') message_List.append(voice_element) except KeyError as e: - self.logger.error(f"语音数据字段缺失: {str(e)}") - message_List.append(platform_message.Unknown(text="[语音数据解析失败]")) + self.logger.error(f'语音数据字段缺失: {str(e)}') + message_List.append(platform_message.Unknown(text='[语音数据解析失败]')) except Exception as e: - self.logger.error(f"处理语音消息异常: {str(e)}") - message_List.append(platform_message.Unknown(text="[语音处理失败]")) + self.logger.error(f'处理语音消息异常: {str(e)}') + message_List.append(platform_message.Unknown(text='[语音处理失败]')) return platform_message.MessageChain(message_List) - async def _handler_compound( - self, - message: Optional[dict], - content_no_preifx: str - ) -> platform_message.MessageChain: + async def _handler_compound(self, message: Optional[dict], content_no_preifx: str) -> platform_message.MessageChain: """处理复合消息 (msg_type=49),根据子类型分派""" try: xml_data = ET.fromstring(content_no_preifx) appmsg_data = xml_data.find('.//appmsg') if appmsg_data: - data_type = appmsg_data.findtext('.//type', "") + data_type = appmsg_data.findtext('.//type', '') # 二次分派处理器 sub_handler_map = { '57': self._handler_compound_quote, @@ -238,9 +201,9 @@ class WeChatPadMessageConverter(adapter.MessageConverter): '74': self._handler_compound_file, '33': self._handler_compound_mini_program, '36': self._handler_compound_mini_program, - '2000': partial(self._handler_compound_unsupported, text="[转账消息]"), - '2001': partial(self._handler_compound_unsupported, text="[红包消息]"), - '51': partial(self._handler_compound_unsupported, text="[视频号消息]"), + '2000': partial(self._handler_compound_unsupported, text='[转账消息]'), + '2001': partial(self._handler_compound_unsupported, text='[红包消息]'), + '51': partial(self._handler_compound_unsupported, text='[视频号消息]'), } handler = sub_handler_map.get(data_type, self._handler_compound_unsupported) @@ -251,56 +214,51 @@ class WeChatPadMessageConverter(adapter.MessageConverter): else: return platform_message.MessageChain([platform_message.Unknown(text=content_no_preifx)]) except Exception as e: - self.logger.error(f"解析复合消息失败: {str(e)}") + self.logger.error(f'解析复合消息失败: {str(e)}') return platform_message.MessageChain([platform_message.Unknown(text=content_no_preifx)]) async def _handler_compound_quote( - self, - message: Optional[dict], - xml_data: ET.Element + self, message: Optional[dict], xml_data: ET.Element ) -> platform_message.MessageChain: """处理引用消息 (data_type=57)""" message_list = [] -# self.logger.info("_handler_compound_quote", ET.tostring(xml_data, encoding='unicode')) + # self.logger.info("_handler_compound_quote", ET.tostring(xml_data, encoding='unicode')) appmsg_data = xml_data.find('.//appmsg') - quote_data = "" # 引用原文 - quote_id = None # 引用消息的原发送者 - tousername = None # 接收方: 所属微信的wxid - user_data = "" # 用户消息 + quote_data = '' # 引用原文 + # quote_id = None # 引用消息的原发送者 + # tousername = None # 接收方: 所属微信的wxid + user_data = '' # 用户消息 sender_id = xml_data.findtext('.//fromusername') # 发送方:单聊用户/群member # 引用消息转发 if appmsg_data: - user_data = appmsg_data.findtext('.//title') or "" + user_data = appmsg_data.findtext('.//title') or '' quote_data = appmsg_data.find('.//refermsg').findtext('.//content') - quote_id = appmsg_data.find('.//refermsg').findtext('.//chatusr') - message_list.append( - platform_message.WeChatAppMsg( - app_msg=ET.tostring(appmsg_data, encoding='unicode')) - ) - if message: - tousername = message['to_user_name']["str"] - + # quote_id = appmsg_data.find('.//refermsg').findtext('.//chatusr') + message_list.append(platform_message.WeChatAppMsg(app_msg=ET.tostring(appmsg_data, encoding='unicode'))) + # if message: + # tousername = message['to_user_name']['str'] + if quote_data: quote_data_message_list = platform_message.MessageChain() # 文本消息 try: - if "" not in quote_data: + if '' not in quote_data: quote_data_message_list.append(platform_message.Plain(quote_data)) else: # 引用消息展开 quote_data_xml = ET.fromstring(quote_data) - if quote_data_xml.find("img"): + if quote_data_xml.find('img'): quote_data_message_list.extend(await self._handler_image(None, quote_data)) - elif quote_data_xml.find("voicemsg"): + elif quote_data_xml.find('voicemsg'): quote_data_message_list.extend(await self._handler_voice(None, quote_data)) - elif quote_data_xml.find("videomsg"): + elif quote_data_xml.find('videomsg'): quote_data_message_list.extend(await self._handler_default(None, quote_data)) # 先不处理 else: # appmsg quote_data_message_list.extend(await self._handler_compound(None, quote_data)) except Exception as e: - self.logger.error(f"处理引用消息异常 expcetion:{e}") + self.logger.error(f'处理引用消息异常 expcetion:{e}') quote_data_message_list.append(platform_message.Plain(quote_data)) message_list.append( platform_message.Quote( @@ -315,15 +273,11 @@ class WeChatPadMessageConverter(adapter.MessageConverter): return platform_message.MessageChain(message_list) - async def _handler_compound_file( - self, - message: dict, - xml_data: ET.Element - ) -> platform_message.MessageChain: + async def _handler_compound_file(self, message: dict, xml_data: ET.Element) -> platform_message.MessageChain: """处理文件消息 (data_type=6)""" file_data = xml_data.find('.//appmsg') - if file_data.findtext('.//type', "") == "74": + if file_data.findtext('.//type', '') == '74': return None else: @@ -346,22 +300,21 @@ class WeChatPadMessageConverter(adapter.MessageConverter): file_data = self.bot.cdn_download(aeskey=aeskey, file_type=5, file_url=cdnthumburl) - file_base64 = file_data["Data"]['FileData'] + file_base64 = file_data['Data']['FileData'] # print(file_data) - file_size = file_data["Data"]['TotalSize'] + file_size = file_data['Data']['TotalSize'] # print(file_base64) - return platform_message.MessageChain([ - platform_message.WeChatFile(file_id=file_id, file_name=file_name, file_size=file_size, - file_base64=file_base64), - platform_message.WeChatForwardFile(xml_data=xml_data_str) - ]) + return platform_message.MessageChain( + [ + platform_message.WeChatFile( + file_id=file_id, file_name=file_name, file_size=file_size, file_base64=file_base64 + ), + platform_message.WeChatForwardFile(xml_data=xml_data_str), + ] + ) - async def _handler_compound_link( - self, - message: dict, - xml_data: ET.Element - ) -> platform_message.MessageChain: + async def _handler_compound_link(self, message: dict, xml_data: ET.Element) -> platform_message.MessageChain: """处理链接消息(如公众号文章、外部网页)""" message_list = [] try: @@ -374,56 +327,38 @@ class WeChatPadMessageConverter(adapter.MessageConverter): link_title=appmsg.findtext('title', ''), link_desc=appmsg.findtext('des', ''), link_url=appmsg.findtext('url', ''), - link_thumb_url=appmsg.findtext("thumburl", '') # 这个字段拿不到 + link_thumb_url=appmsg.findtext('thumburl', ''), # 这个字段拿不到 ) ) # 还没有发链接的接口, 暂时还需要自己构造appmsg, 先用WeChatAppMsg。 - message_list.append( - platform_message.WeChatAppMsg( - app_msg=ET.tostring(appmsg, encoding='unicode') - ) - ) + message_list.append(platform_message.WeChatAppMsg(app_msg=ET.tostring(appmsg, encoding='unicode'))) except Exception as e: - self.logger.error(f"解析链接消息失败: {str(e)}") + self.logger.error(f'解析链接消息失败: {str(e)}') return platform_message.MessageChain(message_list) async def _handler_compound_mini_program( - self, - message: dict, - xml_data: ET.Element + self, message: dict, xml_data: ET.Element ) -> platform_message.MessageChain: """处理小程序消息(如小程序卡片、服务通知)""" xml_data_str = ET.tostring(xml_data, encoding='unicode') - return platform_message.MessageChain([ - platform_message.WeChatForwardMiniPrograms(xml_data=xml_data_str) - ]) + return platform_message.MessageChain([platform_message.WeChatForwardMiniPrograms(xml_data=xml_data_str)]) - async def _handler_default( - self, - message: Optional[dict], - content_no_preifx: str - ) -> platform_message.MessageChain: + async def _handler_default(self, message: Optional[dict], content_no_preifx: str) -> platform_message.MessageChain: """处理未知消息类型""" if message: - msg_type = message["msg_type"] + msg_type = message['msg_type'] else: - msg_type = "" - return platform_message.MessageChain([ - platform_message.Unknown(text=f"[未知消息类型 msg_type:{msg_type}]") - ]) + msg_type = '' + return platform_message.MessageChain([platform_message.Unknown(text=f'[未知消息类型 msg_type:{msg_type}]')]) def _handler_compound_unsupported( - self, - message: dict, - xml_data: str, - text: Optional[str] = None + self, message: dict, xml_data: str, text: Optional[str] = None ) -> platform_message.MessageChain: """处理未支持复合消息类型(msg_type=49)子类型""" if not text: - text = f"[xml_data={xml_data}]" + text = f'[xml_data={xml_data}]' content_list = [] - content_list.append( - platform_message.Unknown(text=f"[处理未支持复合消息类型[msg_type=49]|{text}")) + content_list.append(platform_message.Unknown(text=f'[处理未支持复合消息类型[msg_type=49]|{text}')) return platform_message.MessageChain(content_list) @@ -432,7 +367,7 @@ class WeChatPadMessageConverter(adapter.MessageConverter): ats_bot = False try: to_user_name = message['to_user_name']['str'] # 接收方: 所属微信的wxid - raw_content = message["content"]["str"] # 原始消息内容 + raw_content = message['content']['str'] # 原始消息内容 content_no_prefix, _ = self._extract_content_and_sender(raw_content) # 直接艾特机器人(这个有bug,当被引用的消息里面有@bot,会套娃 # ats_bot = ats_bot or (f"@{bot_account_id}" in content_no_prefix) @@ -443,7 +378,7 @@ class WeChatPadMessageConverter(adapter.MessageConverter): msg_source = message.get('msg_source', '') or '' if len(msg_source) > 0: msg_source_data = ET.fromstring(msg_source) - at_user_list = msg_source_data.findtext("atuserlist") or "" + at_user_list = msg_source_data.findtext('atuserlist') or '' ats_bot = ats_bot or (to_user_name in at_user_list) # 引用bot if message.get('msg_type', 0) == 49: @@ -454,7 +389,7 @@ class WeChatPadMessageConverter(adapter.MessageConverter): quote_id = appmsg_data.find('.//refermsg').findtext('.//chatusr') # 引用消息的原发送者 ats_bot = ats_bot or (quote_id == tousername) except Exception as e: - self.logger.error(f"_ats_bot got except: {e}") + self.logger.error(f'_ats_bot got except: {e}') finally: return ats_bot @@ -463,47 +398,41 @@ class WeChatPadMessageConverter(adapter.MessageConverter): try: # 检查消息开头,如果有 wxid_sbitaz0mt65n22:\n 则删掉 # add: 有些用户的wxid不是上述格式。换成user_name: - regex = re.compile(r"^[a-zA-Z0-9_\-]{5,20}:") - line_split = raw_content.split("\n") + regex = re.compile(r'^[a-zA-Z0-9_\-]{5,20}:') + line_split = raw_content.split('\n') if len(line_split) > 0 and regex.match(line_split[0]): - raw_content = "\n".join(line_split[1:]) - sender_id = line_split[0].strip(":") + raw_content = '\n'.join(line_split[1:]) + sender_id = line_split[0].strip(':') return raw_content, sender_id except Exception as e: - self.logger.error(f"_extract_content_and_sender got except: {e}") + self.logger.error(f'_extract_content_and_sender got except: {e}') finally: return raw_content, None # 是否是群消息 def _is_group_message(self, message: dict) -> bool: from_user_name = message['from_user_name']['str'] - return from_user_name.endswith("@chatroom") + return from_user_name.endswith('@chatroom') class WeChatPadEventConverter(adapter.EventConverter): - def __init__(self, config: dict): self.config = config self.message_converter = WeChatPadMessageConverter(config) - self.logger = logging.getLogger("WeChatPadEventConverter") - + self.logger = logging.getLogger('WeChatPadEventConverter') + @staticmethod - async def yiri2target( - event: platform_events.MessageEvent - ) -> dict: + async def yiri2target(event: platform_events.MessageEvent) -> dict: pass - async def target2yiri( - self, - event: dict, - bot_account_id: str - ) -> platform_events.MessageEvent: - + async def target2yiri(self, event: dict, bot_account_id: str) -> platform_events.MessageEvent: # 排除公众号以及微信团队消息 - if event['from_user_name']['str'].startswith('gh_') \ - or event['from_user_name']['str']=='weixin'\ - or event['from_user_name']['str'] == "newsapp"\ - or event['from_user_name']['str'] == self.config["wxid"]: + if ( + event['from_user_name']['str'].startswith('gh_') + or event['from_user_name']['str'] == 'weixin' + or event['from_user_name']['str'] == 'newsapp' + or event['from_user_name']['str'] == self.config['wxid'] + ): return None message_chain = await self.message_converter.target2yiri(copy.deepcopy(event), bot_account_id) @@ -512,7 +441,7 @@ class WeChatPadEventConverter(adapter.EventConverter): if '@chatroom' in event['from_user_name']['str']: # 找出开头的 wxid_ 字符串,以:结尾 - sender_wxid = event['content']['str'].split(":")[0] + sender_wxid = event['content']['str'].split(':')[0] return platform_events.GroupMessage( sender=platform_entities.GroupMember( @@ -524,13 +453,13 @@ class WeChatPadEventConverter(adapter.EventConverter): name=event['from_user_name']['str'], permission=platform_entities.Permission.Member, ), - special_title="", + special_title='', join_timestamp=0, last_speak_timestamp=0, mute_time_remaining=0, ), message_chain=message_chain, - time=event["create_time"], + time=event['create_time'], source_platform_object=event, ) else: @@ -541,13 +470,13 @@ class WeChatPadEventConverter(adapter.EventConverter): remark='', ), message_chain=message_chain, - time=event["create_time"], + time=event['create_time'], source_platform_object=event, ) class WeChatPadAdapter(adapter.MessagePlatformAdapter): - name: str = "WeChatPad" # 定义适配器名称 + name: str = 'WeChatPad' # 定义适配器名称 bot: WeChatPadClient quart_app: quart.Quart @@ -580,27 +509,21 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter): # self.ap.logger.debug(f"Gewechat callback event: {data}") # print(data) - try: event = await self.event_converter.target2yiri(data.copy(), self.bot_account_id) - except Exception as e: - await self.logger.error(f"Error in wechatpad callback: {traceback.format_exc()}") + except Exception: + await self.logger.error(f'Error in wechatpad callback: {traceback.format_exc()}') if event.__class__ in self.listeners: await self.listeners[event.__class__](event, self) return 'ok' - - async def _handle_message( - self, - message: platform_message.MessageChain, - target_id: str - ): + async def _handle_message(self, message: platform_message.MessageChain, target_id: str): """统一消息处理核心逻辑""" content_list = await self.message_converter.yiri2target(message) # print(content_list) - at_targets = [item["target"] for item in content_list if item["type"] == "at"] + at_targets = [item['target'] for item in content_list if item['type'] == 'at'] # print(at_targets) # 处理@逻辑 at_targets = at_targets or [] @@ -608,7 +531,7 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter): if at_targets: member_info = self.bot.get_chatroom_member_detail( target_id, - )["Data"]["member_data"]["chatroom_member_list"] + )['Data']['member_data']['chatroom_member_list'] # 处理消息组件 for msg in content_list: @@ -616,63 +539,51 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter): if msg['type'] == 'text' and at_targets: at_nick_name_list = [] for member in member_info: - if member["user_name"] in at_targets: + if member['user_name'] in at_targets: at_nick_name_list.append(f'@{member["nick_name"]}') msg['content'] = f'{" ".join(at_nick_name_list)} {msg["content"]}' # 统一消息派发 handler_map = { 'text': lambda msg: self.bot.send_text_message( - to_wxid=target_id, - message=msg['content'], - ats=at_targets + to_wxid=target_id, message=msg['content'], ats=at_targets ), 'image': lambda msg: self.bot.send_image_message( - to_wxid=target_id, - img_url=msg["image"], - ats = at_targets + to_wxid=target_id, img_url=msg['image'], ats=at_targets ), 'WeChatEmoji': lambda msg: self.bot.send_emoji_message( - to_wxid=target_id, - emoji_md5=msg['emoji_md5'], - emoji_size=msg['emoji_size'] + to_wxid=target_id, emoji_md5=msg['emoji_md5'], emoji_size=msg['emoji_size'] ), - 'voice': lambda msg: self.bot.send_voice_message( to_wxid=target_id, voice_data=msg['data'], - voice_duration=msg["duration"], - voice_forma=msg["forma"], + voice_duration=msg['duration'], + voice_forma=msg['forma'], ), 'WeChatAppMsg': lambda msg: self.bot.send_app_message( to_wxid=target_id, app_message=msg['app_msg'], type=0, ), - 'at': lambda msg: None + 'at': lambda msg: None, } if handler := handler_map.get(msg['type']): handler(msg) # self.ap.logger.warning(f"未处理的消息类型: {ret}") else: - self.ap.logger.warning(f"未处理的消息类型: {msg['type']}") + self.ap.logger.warning(f'未处理的消息类型: {msg["type"]}') continue - async def send_message( - self, - target_type: str, - target_id: str, - message: platform_message.MessageChain - ): + async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): """主动发送消息""" return await self._handle_message(message, target_id) async def reply_message( - self, - message_source: platform_events.MessageEvent, - message: platform_message.MessageChain, - quote_origin: bool = False + self, + message_source: platform_events.MessageEvent, + message: platform_message.MessageChain, + quote_origin: bool = False, ): """回复消息""" if message_source.source_platform_object: @@ -683,58 +594,49 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter): pass def register_listener( - self, - event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None] + self, + event_type: typing.Type[platform_events.Event], + callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], ): self.listeners[event_type] = callback def unregister_listener( - self, - event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None] + self, + event_type: typing.Type[platform_events.Event], + callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], ): pass async def run_async(self): - - if not self.config["admin_key"] and not self.config["token"]: - raise RuntimeError("无wechatpad管理密匙,请填入配置文件后重启") + if not self.config['admin_key'] and not self.config['token']: + raise RuntimeError('无wechatpad管理密匙,请填入配置文件后重启') else: - if self.config["token"]: - self.bot = WeChatPadClient( - self.config['wechatpad_url'], - self.config["token"] - ) + if self.config['token']: + self.bot = WeChatPadClient(self.config['wechatpad_url'], self.config['token']) data = self.bot.get_login_status() self.ap.logger.info(data) - if data["Code"] == 300 and data["Text"] == "你已退出微信": + if data['Code'] == 300 and data['Text'] == '你已退出微信': response = requests.post( - f"{self.config['wechatpad_url']}/admin/GenAuthKey1?key={self.config['admin_key']}", - json={"Count": 1, "Days": 365} + f'{self.config["wechatpad_url"]}/admin/GenAuthKey1?key={self.config["admin_key"]}', + json={'Count': 1, 'Days': 365}, ) if response.status_code != 200: - raise Exception(f"获取token失败: {response.text}") - self.config["token"] = response.json()["Data"][0] + raise Exception(f'获取token失败: {response.text}') + self.config['token'] = response.json()['Data'][0] - elif not self.config["token"]: + elif not self.config['token']: response = requests.post( - f"{self.config['wechatpad_url']}/admin/GenAuthKey1?key={self.config['admin_key']}", - json={"Count": 1, "Days": 365} + f'{self.config["wechatpad_url"]}/admin/GenAuthKey1?key={self.config["admin_key"]}', + json={'Count': 1, 'Days': 365}, ) if response.status_code != 200: - raise Exception(f"获取token失败: {response.text}") - self.config["token"] = response.json()["Data"][0] + raise Exception(f'获取token失败: {response.text}') + self.config['token'] = response.json()['Data'][0] - self.bot = WeChatPadClient( - self.config['wechatpad_url'], - self.config["token"], - logger=self.logger - ) - self.ap.logger.info(self.config["token"]) + self.bot = WeChatPadClient(self.config['wechatpad_url'], self.config['token'], logger=self.logger) + self.ap.logger.info(self.config['token']) thread_1 = threading.Event() - def wechat_login_process(): # 不登录,这些先注释掉,避免登陆态尝试拉qrcode。 # login_data =self.bot.get_login_qr() @@ -742,67 +644,54 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter): # url = login_data['Data']["QrCodeUrl"] # self.ap.logger.info(login_data) - - profile =self.bot.get_profile() + profile = self.bot.get_profile() self.ap.logger.info(profile) - self.bot_account_id = profile["Data"]["userInfo"]["nickName"]["str"] - self.config["wxid"] = profile["Data"]["userInfo"]["userName"]["str"] + self.bot_account_id = profile['Data']['userInfo']['nickName']['str'] + self.config['wxid'] = profile['Data']['userInfo']['userName']['str'] thread_1.set() - # asyncio.create_task(wechat_login_process) threading.Thread(target=wechat_login_process).start() def connect_websocket_sync() -> None: - thread_1.wait() - uri = f"{self.config['wechatpad_ws']}/GetSyncMsg?key={self.config['token']}" - self.ap.logger.info(f"Connecting to WebSocket: {uri}") + uri = f'{self.config["wechatpad_ws"]}/GetSyncMsg?key={self.config["token"]}' + self.ap.logger.info(f'Connecting to WebSocket: {uri}') + def on_message(ws, message): try: data = json.loads(message) - self.ap.logger.debug(f"Received message: {data}") + self.ap.logger.debug(f'Received message: {data}') # 这里需要确保ws_message是同步的,或者使用asyncio.run调用异步方法 asyncio.run(self.ws_message(data)) except json.JSONDecodeError: - self.ap.logger.error(f"Non-JSON message: {message[:100]}...") + self.ap.logger.error(f'Non-JSON message: {message[:100]}...') def on_error(ws, error): - self.ap.logger.error(f"WebSocket error: {str(error)[:200]}") + self.ap.logger.error(f'WebSocket error: {str(error)[:200]}') def on_close(ws, close_status_code, close_msg): - self.ap.logger.info("WebSocket closed, reconnecting...") + self.ap.logger.info('WebSocket closed, reconnecting...') time.sleep(5) connect_websocket_sync() # 自动重连 def on_open(ws): - self.ap.logger.info("WebSocket connected successfully!") + self.ap.logger.info('WebSocket connected successfully!') ws = websocket.WebSocketApp( - uri, - on_message=on_message, - on_error=on_error, - on_close=on_close, - on_open=on_open - ) - ws.run_forever( - ping_interval=60, - ping_timeout=20 + uri, on_message=on_message, on_error=on_error, on_close=on_close, on_open=on_open ) + ws.run_forever(ping_interval=60, ping_timeout=20) # 直接调用同步版本(会阻塞) # connect_websocket_sync() # 这行代码会在WebSocket连接断开后才会执行 # self.ap.logger.info("WebSocket client thread started") - thread = threading.Thread( - target=connect_websocket_sync, - name="WebSocketClientThread", - daemon=True - ) + thread = threading.Thread(target=connect_websocket_sync, name='WebSocketClientThread', daemon=True) thread.start() - self.ap.logger.info("WebSocket client thread started") + self.ap.logger.info('WebSocket client thread started') async def kill(self) -> bool: pass diff --git a/pkg/platform/sources/wecom.py b/pkg/platform/sources/wecom.py index f1cc677e..7be05a85 100644 --- a/pkg/platform/sources/wecom.py +++ b/pkg/platform/sources/wecom.py @@ -157,7 +157,7 @@ class WecomAdapter(adapter.MessagePlatformAdapter): token=config['token'], EncodingAESKey=config['EncodingAESKey'], contacts_secret=config['contacts_secret'], - logger=self.logger + logger=self.logger, ) async def reply_message( @@ -201,8 +201,8 @@ class WecomAdapter(adapter.MessagePlatformAdapter): self.bot_account_id = event.receiver_id try: return await callback(await self.event_converter.target2yiri(event), self) - except Exception as e: - await self.logger.error(f"Error in wecom callback: {traceback.format_exc()}") + except Exception: + await self.logger.error(f'Error in wecom callback: {traceback.format_exc()}') if event_type == platform_events.FriendMessage: self.bot.on_message('text')(on_message) diff --git a/pkg/platform/sources/wecomcs.py b/pkg/platform/sources/wecomcs.py index aab8d394..da84ac6d 100644 --- a/pkg/platform/sources/wecomcs.py +++ b/pkg/platform/sources/wecomcs.py @@ -145,7 +145,7 @@ class WecomCSAdapter(adapter.MessagePlatformAdapter): secret=config['secret'], token=config['token'], EncodingAESKey=config['EncodingAESKey'], - logger=self.logger + logger=self.logger, ) async def reply_message( @@ -178,8 +178,8 @@ class WecomCSAdapter(adapter.MessagePlatformAdapter): self.bot_account_id = event.receiver_id try: return await callback(await self.event_converter.target2yiri(event), self) - except Exception as e: - await self.logger.error(f"Error in wecomcs callback: {traceback.format_exc()}") + except Exception: + await self.logger.error(f'Error in wecomcs callback: {traceback.format_exc()}') if event_type == platform_events.FriendMessage: self.bot.on_message('text')(on_message) diff --git a/pkg/platform/types/message.py b/pkg/platform/types/message.py index 7dad4145..ecd7cc96 100644 --- a/pkg/platform/types/message.py +++ b/pkg/platform/types/message.py @@ -812,12 +812,14 @@ class File(MessageComponent): def __str__(self): return f'[文件]{self.name}' + class Face(MessageComponent): """系统表情 此处将超级表情骰子/划拳,一同归类于face 当face_type为rps(划拳)时 face_id 对应的是手势 当face_type为dice(骰子)时 face_id 对应的是点数 """ + type: str = 'Face' """表情类型""" face_type: str = 'face' @@ -834,15 +836,15 @@ class Face(MessageComponent): elif self.face_type == 'rps': return f'[表情]{self.face_name}({self.rps_data(self.face_id)})' - - def rps_data(self,face_id): - rps_dict ={ - 1 : "布", - 2 : "剪刀", - 3 : "石头", + def rps_data(self, face_id): + rps_dict = { + 1: '布', + 2: '剪刀', + 3: '石头', } return rps_dict[face_id] + # ================ 个人微信专用组件 ================ @@ -971,5 +973,6 @@ class WeChatFile(MessageComponent): """文件地址""" file_base64: str = '' """base64""" + def __str__(self): - return f'[文件]{self.file_name}' \ No newline at end of file + return f'[文件]{self.file_name}' diff --git a/pkg/provider/entities.py b/pkg/provider/entities.py index df2b5487..ff1e4526 100644 --- a/pkg/provider/entities.py +++ b/pkg/provider/entities.py @@ -127,6 +127,7 @@ class Message(pydantic.BaseModel): class MessageChunk(pydantic.BaseModel): """消息""" + resp_message_id: typing.Optional[str] = None """消息id""" @@ -148,7 +149,7 @@ class MessageChunk(pydantic.BaseModel): tool_call_id: typing.Optional[str] = None # tool_calls: typing.Optional[list[ToolCallChunk]] = None - + is_final: bool = False def readable_str(self) -> str: @@ -210,6 +211,7 @@ class ToolCallChunk(pydantic.BaseModel): function: FunctionCall """函数调用""" + class Prompt(pydantic.BaseModel): """供AI使用的Prompt""" diff --git a/pkg/provider/modelmgr/requester.py b/pkg/provider/modelmgr/requester.py index 7830e522..1545a2e4 100644 --- a/pkg/provider/modelmgr/requester.py +++ b/pkg/provider/modelmgr/requester.py @@ -71,19 +71,18 @@ class LLMAPIRequester(metaclass=abc.ABCMeta): extra_args (dict[str, typing.Any], optional): 额外的参数. Defaults to {}. Returns: - llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk]: 返回消息对象 + llm_entities.Message: 返回消息对象 """ pass @abc.abstractmethod async def invoke_llm_stream( - self, - query: core_entities.Query, - model: RuntimeLLMModel, - messages: typing.List[llm_entities.Message], - funcs: typing.List[tools_entities.LLMFunction] = None, - stream: bool = False, - extra_args: dict[str, typing.Any] = {}, + self, + query: core_entities.Query, + model: RuntimeLLMModel, + messages: typing.List[llm_entities.Message], + funcs: typing.List[tools_entities.LLMFunction] = None, + extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.MessageChunk: """调用API @@ -94,6 +93,6 @@ class LLMAPIRequester(metaclass=abc.ABCMeta): extra_args (dict[str, typing.Any], optional): 额外的参数. Defaults to {}. Returns: - llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk]: 返回消息对象 + typing.AsyncGenerator[llm_entities.MessageChunk]: 返回消息对象 """ pass diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index 6e72d78e..f05af8c3 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -8,7 +8,7 @@ import openai.types.chat.chat_completion as chat_completion import httpx from .. import errors, requester -from ....core import entities as core_entities, app +from ....core import entities as core_entities from ... import entities as llm_entities from ...tools import entities as tools_entities @@ -129,12 +129,10 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): req_messages: list[dict], use_model: requester.RuntimeLLMModel, use_funcs: list[tools_entities.LLMFunction] = None, - stream: bool = False, extra_args: dict[str, typing.Any] = {}, - ) ->llm_entities.MessageChunk: + ) -> llm_entities.MessageChunk: self.client.api_key = use_model.token_mgr.get_token() - args = {} args['model'] = use_model.model_entity.name @@ -158,43 +156,42 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): args['messages'] = messages - if stream: - current_content = '' - args['stream'] = True - chunk_idx = 0 - self.is_content = False - tool_calls_map: dict[str, llm_entities.ToolCall] = {} - pipeline_config = query.pipeline_config - async for chunk in self._req_stream(args, extra_body=extra_args): - # 处理流式消息 - delta_message = await self._make_msg_chunk(pipeline_config, chunk, chunk_idx) - if delta_message.content: - current_content += delta_message.content - delta_message.content = current_content - # delta_message.all_content = current_content - if delta_message.tool_calls: - for tool_call in delta_message.tool_calls: - if tool_call.id not in tool_calls_map: - tool_calls_map[tool_call.id] = llm_entities.ToolCall( - id=tool_call.id, - type=tool_call.type, - function=llm_entities.FunctionCall( - name=tool_call.function.name if tool_call.function else '', arguments='' - ), - ) - if tool_call.function and tool_call.function.arguments: - # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 - tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + current_content = '' + args['stream'] = True + chunk_idx = 0 + self.is_content = False + tool_calls_map: dict[str, llm_entities.ToolCall] = {} + pipeline_config = query.pipeline_config + async for chunk in self._req_stream(args, extra_body=extra_args): + # 处理流式消息 + delta_message = await self._make_msg_chunk(pipeline_config, chunk, chunk_idx) + if delta_message.content: + current_content += delta_message.content + delta_message.content = current_content + # delta_message.all_content = current_content + if delta_message.tool_calls: + for tool_call in delta_message.tool_calls: + if tool_call.id not in tool_calls_map: + tool_calls_map[tool_call.id] = llm_entities.ToolCall( + id=tool_call.id, + type=tool_call.type, + function=llm_entities.FunctionCall( + name=tool_call.function.name if tool_call.function else '', arguments='' + ), + ) + if tool_call.function and tool_call.function.arguments: + # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 + tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments - chunk_idx += 1 - chunk_choices = getattr(chunk, 'choices', None) - if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): - delta_message.is_final = True - delta_message.content = current_content + chunk_idx += 1 + chunk_choices = getattr(chunk, 'choices', None) + if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): + delta_message.is_final = True + delta_message.content = current_content - if chunk_idx % 64 == 0 or delta_message.is_final: - yield delta_message - # return + if chunk_idx % 64 == 0 or delta_message.is_final: + yield delta_message + # return async def _closure( self, @@ -202,7 +199,6 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): req_messages: list[dict], use_model: requester.RuntimeLLMModel, use_funcs: list[tools_entities.LLMFunction] = None, - stream: bool = False, extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message: self.client.api_key = use_model.token_mgr.get_token() @@ -289,7 +285,6 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): model: requester.RuntimeLLMModel, messages: typing.List[llm_entities.Message], funcs: typing.List[tools_entities.LLMFunction] = None, - stream: bool = False, extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.MessageChunk: req_messages = [] # req_messages 仅用于类内,外部同步由 query.messages 进行 @@ -309,7 +304,6 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): req_messages=req_messages, use_model=model, use_funcs=funcs, - stream=stream, extra_args=extra_args, ): yield item diff --git a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py index 2a618c9f..1c19a534 100644 --- a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py +++ b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py @@ -12,7 +12,6 @@ import re import openai.types.chat.chat_completion as chat_completion - class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): """Gitee AI ChatCompletions API 请求器""" @@ -20,7 +19,7 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): 'base_url': 'https://ai.gitee.com/v1', 'timeout': 120, } - is_think:bool = False + is_think: bool = False async def _closure( self, @@ -52,15 +51,14 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): pipeline_config = query.pipeline_config - message = await self._make_msg(resp,pipeline_config) + message = await self._make_msg(resp, pipeline_config) return message - async def _make_msg( - self, - chat_completion: chat_completion.ChatCompletion, - pipeline_config: dict[str, typing.Any] = {'trigger': {'misc': {'remove_think': False}}}, + self, + chat_completion: chat_completion.ChatCompletion, + pipeline_config: dict[str, typing.Any] = {'trigger': {'misc': {'remove_think': False}}}, ) -> llm_entities.Message: chatcmpl_message = chat_completion.choices[0].message.model_dump() # print(chatcmpl_message.keys(), chatcmpl_message.values()) @@ -73,23 +71,25 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): # deepseek的reasoner模型 if pipeline_config['trigger'].get('misc', '').get('remove_think'): - chatcmpl_message['content'] = re.sub(r'.*?', '', chatcmpl_message['content'], flags=re.DOTALL) + chatcmpl_message['content'] = re.sub( + r'.*?', '', chatcmpl_message['content'], flags=re.DOTALL + ) else: if reasoning_content is not None: - chatcmpl_message['content'] = '\n' + reasoning_content + '\n\n' + chatcmpl_message['content'] + chatcmpl_message['content'] = ( + '\n' + reasoning_content + '\n\n' + chatcmpl_message['content'] + ) message = llm_entities.Message(**chatcmpl_message) return message - async def _make_msg_chunk( self, pipeline_config: dict[str, typing.Any], chat_completion: chat_completion.ChatCompletion, idx: int, ) -> llm_entities.MessageChunk: - # 处理流式chunk和完整响应的差异 # print(chat_completion.choices[0]) if hasattr(chat_completion, 'choices'): @@ -104,7 +104,6 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): if 'role' not in delta or delta['role'] is None: delta['role'] = 'assistant' - reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None delta['content'] = '' if delta['content'] is None else delta['content'] @@ -115,7 +114,7 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): if delta['content'] == '': self.is_think = True delta['content'] = '' - if delta['content'] == rf'': + if delta['content'] == r'
': self.is_think = False delta['content'] = '' if not self.is_think: @@ -126,7 +125,6 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): if reasoning_content is not None: delta['content'] += reasoning_content - message = llm_entities.MessageChunk(**delta) return message @@ -137,7 +135,6 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): req_messages: list[dict], use_model: requester.RuntimeLLMModel, use_funcs: list[tools_entities.LLMFunction] = None, - stream: bool = False, extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: self.client.api_key = use_model.token_mgr.get_token() @@ -165,44 +162,38 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): args['messages'] = messages - if stream: - current_content = '' - args["stream"] = True - chunk_idx = 0 - self.is_content = False - tool_calls_map: dict[str, llm_entities.ToolCall] = {} - pipeline_config = query.pipeline_config - async for chunk in self._req_stream(args, extra_body=extra_args): - # 处理流式消息 - delta_message = await self._make_msg_chunk(pipeline_config,chunk,chunk_idx) - if delta_message.content: - current_content += delta_message.content - delta_message.content = current_content - # delta_message.all_content = current_content - if delta_message.tool_calls: - for tool_call in delta_message.tool_calls: - if tool_call.id not in tool_calls_map: - tool_calls_map[tool_call.id] = llm_entities.ToolCall( - id=tool_call.id, - type=tool_call.type, - function=llm_entities.FunctionCall( - name=tool_call.function.name if tool_call.function else '', - arguments='' - ), - ) - if tool_call.function and tool_call.function.arguments: - # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 - tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments - - - chunk_idx += 1 - chunk_choices = getattr(chunk, 'choices', None) - if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): - delta_message.is_final = True - delta_message.content = current_content - - if chunk_idx % 64 == 0 or delta_message.is_final: - - yield delta_message + current_content = '' + args['stream'] = True + chunk_idx = 0 + self.is_content = False + tool_calls_map: dict[str, llm_entities.ToolCall] = {} + pipeline_config = query.pipeline_config + async for chunk in self._req_stream(args, extra_body=extra_args): + # 处理流式消息 + delta_message = await self._make_msg_chunk(pipeline_config, chunk, chunk_idx) + if delta_message.content: + current_content += delta_message.content + delta_message.content = current_content + # delta_message.all_content = current_content + if delta_message.tool_calls: + for tool_call in delta_message.tool_calls: + if tool_call.id not in tool_calls_map: + tool_calls_map[tool_call.id] = llm_entities.ToolCall( + id=tool_call.id, + type=tool_call.type, + function=llm_entities.FunctionCall( + name=tool_call.function.name if tool_call.function else '', arguments='' + ), + ) + if tool_call.function and tool_call.function.arguments: + # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 + tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + chunk_idx += 1 + chunk_choices = getattr(chunk, 'choices', None) + if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): + delta_message.is_final = True + delta_message.content = current_content + if chunk_idx % 64 == 0 or delta_message.is_final: + yield delta_message diff --git a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py index 1a303d22..b98ae7ff 100644 --- a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py +++ b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py @@ -165,11 +165,10 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester): return message async def _req_stream( - self, - args: dict, - extra_body: dict = {}, + self, + args: dict, + extra_body: dict = {}, ) -> chat_completion.ChatCompletion: - async for chunk in await self.client.chat.completions.create(**args, extra_body=extra_body): yield chunk @@ -179,7 +178,6 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester): chat_completion: chat_completion.ChatCompletion, idx: int, ) -> llm_entities.MessageChunk: - # 处理流式chunk和完整响应的差异 # print(chat_completion.choices[0]) if hasattr(chat_completion, 'choices'): @@ -195,7 +193,6 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester): if 'role' not in delta or delta['role'] is None: delta['role'] = 'assistant' - reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None delta['content'] = '' if delta['content'] is None else delta['content'] @@ -203,13 +200,13 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester): # deepseek的reasoner模型 if pipeline_config['trigger'].get('misc', '').get('remove_think'): - if reasoning_content is not None : + if reasoning_content is not None: pass else: delta['content'] = delta['content'] else: if reasoning_content is not None and idx == 0: - delta['content'] += f'\n{reasoning_content}' + delta['content'] += f'\n{reasoning_content}' elif reasoning_content is None: if self.is_content: delta['content'] = delta['content'] @@ -219,7 +216,6 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester): else: delta['content'] += reasoning_content - message = llm_entities.MessageChunk(**delta) return message @@ -230,7 +226,6 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester): req_messages: list[dict], use_model: requester.RuntimeLLMModel, use_funcs: list[tools_entities.LLMFunction] = None, - stream: bool = False, extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: self.client.api_key = use_model.token_mgr.get_token() @@ -258,48 +253,42 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester): args['messages'] = messages - if stream: - current_content = '' - args["stream"] = True - chunk_idx = 0 - self.is_content = False - tool_calls_map: dict[str, llm_entities.ToolCall] = {} - pipeline_config = query.pipeline_config - async for chunk in self._req_stream(args, extra_body=extra_args): - # 处理流式消息 - delta_message = await self._make_msg_chunk(pipeline_config,chunk,chunk_idx) - if delta_message.content: - current_content += delta_message.content - delta_message.content = current_content - # delta_message.all_content = current_content - if delta_message.tool_calls: - for tool_call in delta_message.tool_calls: - if tool_call.id not in tool_calls_map: - tool_calls_map[tool_call.id] = llm_entities.ToolCall( - id=tool_call.id, - type=tool_call.type, - function=llm_entities.FunctionCall( - name=tool_call.function.name if tool_call.function else '', - arguments='' - ), - ) - if tool_call.function and tool_call.function.arguments: - # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 - tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments - - - chunk_idx += 1 - chunk_choices = getattr(chunk, 'choices', None) - if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): - delta_message.is_final = True - delta_message.content = current_content - - if chunk_idx % 64 == 0 or delta_message.is_final: - - yield delta_message - # return + current_content = '' + args['stream'] = True + chunk_idx = 0 + self.is_content = False + tool_calls_map: dict[str, llm_entities.ToolCall] = {} + pipeline_config = query.pipeline_config + async for chunk in self._req_stream(args, extra_body=extra_args): + # 处理流式消息 + delta_message = await self._make_msg_chunk(pipeline_config, chunk, chunk_idx) + if delta_message.content: + current_content += delta_message.content + delta_message.content = current_content + # delta_message.all_content = current_content + if delta_message.tool_calls: + for tool_call in delta_message.tool_calls: + if tool_call.id not in tool_calls_map: + tool_calls_map[tool_call.id] = llm_entities.ToolCall( + id=tool_call.id, + type=tool_call.type, + function=llm_entities.FunctionCall( + name=tool_call.function.name if tool_call.function else '', arguments='' + ), + ) + if tool_call.function and tool_call.function.arguments: + # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 + tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + chunk_idx += 1 + chunk_choices = getattr(chunk, 'choices', None) + if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): + delta_message.is_final = True + delta_message.content = current_content + if chunk_idx % 64 == 0 or delta_message.is_final: + yield delta_message + # return async def invoke_llm( self, @@ -340,16 +329,14 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester): except openai.APIError as e: raise errors.RequesterError(f'请求错误: {e.message}') - async def invoke_llm_stream( self, query: core_entities.Query, model: requester.RuntimeLLMModel, messages: typing.List[llm_entities.Message], funcs: typing.List[tools_entities.LLMFunction] = None, - stream: bool = False, extra_args: dict[str, typing.Any] = {}, - ) -> llm_entities.MessageChunk: + ) -> llm_entities.MessageChunk: req_messages = [] # req_messages 仅用于类内,外部同步由 query.messages 进行 for m in messages: msg_dict = m.dict(exclude_none=True) @@ -367,7 +354,6 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester): req_messages=req_messages, use_model=model, use_funcs=funcs, - stream=stream, extra_args=extra_args, ): yield item @@ -386,4 +372,4 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester): except openai.RateLimitError as e: raise errors.RequesterError(f'请求过于频繁或余额不足: {e.message}') except openai.APIError as e: - raise errors.RequesterError(f'请求错误: {e.message}') \ No newline at end of file + raise errors.RequesterError(f'请求错误: {e.message}') diff --git a/pkg/provider/modelmgr/requesters/ppiochatcmpl.py b/pkg/provider/modelmgr/requesters/ppiochatcmpl.py index 85b321a7..46da6e01 100644 --- a/pkg/provider/modelmgr/requesters/ppiochatcmpl.py +++ b/pkg/provider/modelmgr/requesters/ppiochatcmpl.py @@ -5,8 +5,8 @@ import typing from . import chatcmpl import openai.types.chat.chat_completion as chat_completion -from .. import errors, requester -from ....core import entities as core_entities, app +from .. import requester +from ....core import entities as core_entities from ... import entities as llm_entities from ...tools import entities as tools_entities import re @@ -25,9 +25,9 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): is_think: bool = False async def _make_msg( - self, - chat_completion: chat_completion.ChatCompletion, - pipeline_config: dict[str, typing.Any] = {'trigger': {'misc': {'remove_think': False}}}, + self, + chat_completion: chat_completion.ChatCompletion, + pipeline_config: dict[str, typing.Any] = {'trigger': {'misc': {'remove_think': False}}}, ) -> llm_entities.Message: chatcmpl_message = chat_completion.choices[0].message.model_dump() # print(chatcmpl_message.keys(), chatcmpl_message.values()) @@ -40,21 +40,24 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): # deepseek的reasoner模型 if pipeline_config['trigger'].get('misc', '').get('remove_think'): - chatcmpl_message['content'] = re.sub(r'.*?', '', chatcmpl_message['content'], flags=re.DOTALL) + chatcmpl_message['content'] = re.sub( + r'.*?', '', chatcmpl_message['content'], flags=re.DOTALL + ) else: if reasoning_content is not None: - chatcmpl_message['content'] = '\n' + reasoning_content + '\n\n' + chatcmpl_message['content'] + chatcmpl_message['content'] = ( + '\n' + reasoning_content + '\n\n' + chatcmpl_message['content'] + ) message = llm_entities.Message(**chatcmpl_message) return message - async def _make_msg_chunk( - self, - pipeline_config: dict[str, typing.Any], - chat_completion: chat_completion.ChatCompletion, - idx: int, + self, + pipeline_config: dict[str, typing.Any], + chat_completion: chat_completion.ChatCompletion, + idx: int, ) -> llm_entities.MessageChunk: # 处理流式chunk和完整响应的差异 # print(chat_completion.choices[0]) @@ -80,7 +83,7 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): if '' in delta['content']: self.is_think = True delta['content'] = '' - if rf'' in delta['content']: + if r'' in delta['content']: self.is_think = False delta['content'] = '' if not self.is_think: @@ -95,15 +98,13 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): return message - async def _closure_stream( - self, - query: core_entities.Query, - req_messages: list[dict], - use_model: requester.RuntimeLLMModel, - use_funcs: list[tools_entities.LLMFunction] = None, - stream: bool = False, - extra_args: dict[str, typing.Any] = {}, + self, + query: core_entities.Query, + req_messages: list[dict], + use_model: requester.RuntimeLLMModel, + use_funcs: list[tools_entities.LLMFunction] = None, + extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: self.client.api_key = use_model.token_mgr.get_token() @@ -130,40 +131,38 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): args['messages'] = messages - if stream: - current_content = '' - args["stream"] = True - chunk_idx = 0 - self.is_content = False - tool_calls_map: dict[str, llm_entities.ToolCall] = {} - pipeline_config = query.pipeline_config - async for chunk in self._req_stream(args, extra_body=extra_args): - # 处理流式消息 - delta_message = await self._make_msg_chunk(pipeline_config, chunk, chunk_idx) - if delta_message.content: - current_content += delta_message.content - delta_message.content = current_content - # delta_message.all_content = current_content - if delta_message.tool_calls: - for tool_call in delta_message.tool_calls: - if tool_call.id not in tool_calls_map: - tool_calls_map[tool_call.id] = llm_entities.ToolCall( - id=tool_call.id, - type=tool_call.type, - function=llm_entities.FunctionCall( - name=tool_call.function.name if tool_call.function else '', - arguments='' - ), - ) - if tool_call.function and tool_call.function.arguments: - # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 - tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + current_content = '' + args['stream'] = True + chunk_idx = 0 + self.is_content = False + tool_calls_map: dict[str, llm_entities.ToolCall] = {} + pipeline_config = query.pipeline_config + async for chunk in self._req_stream(args, extra_body=extra_args): + # 处理流式消息 + delta_message = await self._make_msg_chunk(pipeline_config, chunk, chunk_idx) + if delta_message.content: + current_content += delta_message.content + delta_message.content = current_content + # delta_message.all_content = current_content + if delta_message.tool_calls: + for tool_call in delta_message.tool_calls: + if tool_call.id not in tool_calls_map: + tool_calls_map[tool_call.id] = llm_entities.ToolCall( + id=tool_call.id, + type=tool_call.type, + function=llm_entities.FunctionCall( + name=tool_call.function.name if tool_call.function else '', arguments='' + ), + ) + if tool_call.function and tool_call.function.arguments: + # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 + tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments - chunk_idx += 1 - chunk_choices = getattr(chunk, 'choices', None) - if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): - delta_message.is_final = True - delta_message.content = current_content + chunk_idx += 1 + chunk_choices = getattr(chunk, 'choices', None) + if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): + delta_message.is_final = True + delta_message.content = current_content - if chunk_idx % 64 == 0 or delta_message.is_final: - yield delta_message + if chunk_idx % 64 == 0 or delta_message.is_final: + yield delta_message diff --git a/pkg/provider/runners/difysvapi.py b/pkg/provider/runners/difysvapi.py index 8182cc54..40a3140c 100644 --- a/pkg/provider/runners/difysvapi.py +++ b/pkg/provider/runners/difysvapi.py @@ -348,7 +348,9 @@ class DifyServiceAPIRunner(runner.RequestRunner): except AttributeError: is_stream = False - batch_pending_index = 0 + _ = is_stream + + # batch_pending_index = 0 plain_text, image_ids = await self._preprocess_user_message(query) diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index b70d4157..30c48cf6 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -63,8 +63,7 @@ class LocalAgentRunner(runner.RequestRunner): id=tool_call.id, type=tool_call.type, function=llm_entities.FunctionCall( - name=tool_call.function.name if tool_call.function else '', - arguments='' + name=tool_call.function.name if tool_call.function else '', arguments='' ), ) if tool_call.function and tool_call.function.arguments: diff --git a/pkg/utils/image.py b/pkg/utils/image.py index f69d29d2..d9518e12 100644 --- a/pkg/utils/image.py +++ b/pkg/utils/image.py @@ -204,9 +204,9 @@ async def get_slack_image_to_base64(pic_url: str, bot_token: str): try: async with aiohttp.ClientSession() as session: async with session.get(pic_url, headers=headers) as resp: - mime_type = resp.headers.get("Content-Type", "application/octet-stream") + mime_type = resp.headers.get('Content-Type', 'application/octet-stream') file_bytes = await resp.read() - base64_str = base64.b64encode(file_bytes).decode("utf-8") - return f"data:{mime_type};base64,{base64_str}" + base64_str = base64.b64encode(file_bytes).decode('utf-8') + return f'data:{mime_type};base64,{base64_str}' except Exception as e: - raise (e) \ No newline at end of file + raise (e) diff --git a/pkg/utils/importutil.py b/pkg/utils/importutil.py index 8acc5c45..1933d611 100644 --- a/pkg/utils/importutil.py +++ b/pkg/utils/importutil.py @@ -32,7 +32,7 @@ def import_dir(path: str): rel_path = full_path.replace(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '') rel_path = rel_path[1:] rel_path = rel_path.replace('/', '.')[:-3] - rel_path = rel_path.replace("\\",".") + rel_path = rel_path.replace('\\', '.') importlib.import_module(rel_path) From 84129e33391b32eb7abd4b4eef69d8bc8a4be4f9 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 3 Aug 2025 15:30:11 +0800 Subject: [PATCH 029/107] perf: minor fixes --- pkg/pipeline/process/handlers/chat.py | 1 + pkg/platform/sources/webchat.yaml | 12 +----------- pkg/provider/entities.py | 2 -- pkg/provider/modelmgr/requester.py | 1 - pkg/provider/runners/localagent.py | 2 -- templates/metadata/pipeline/trigger.yaml | 7 +++++-- .../components/debug-dialog/DebugDialog.tsx | 12 ++++++++---- 7 files changed, 15 insertions(+), 22 deletions(-) diff --git a/pkg/pipeline/process/handlers/chat.py b/pkg/pipeline/process/handlers/chat.py index a81d8e3f..a1928703 100644 --- a/pkg/pipeline/process/handlers/chat.py +++ b/pkg/pipeline/process/handlers/chat.py @@ -106,6 +106,7 @@ class ChatMessageHandler(handler.MessageHandler): query.session.using_conversation.messages.extend(query.resp_messages) except Exception as e: self.ap.logger.error(f'对话({query.query_id})请求失败: {type(e).__name__} {str(e)}') + traceback.print_exc() hide_exception_info = query.pipeline_config['output']['misc']['hide-exception'] diff --git a/pkg/platform/sources/webchat.yaml b/pkg/platform/sources/webchat.yaml index 0b1d4c29..748dfc8c 100644 --- a/pkg/platform/sources/webchat.yaml +++ b/pkg/platform/sources/webchat.yaml @@ -10,17 +10,7 @@ metadata: zh_Hans: "用于流水线调试的网页聊天适配器" icon: "" spec: - config: - - name: enable-stream-reply - label: - en_US: Enable Stream Reply Mode - zh_Hans: 启用电报流式回复模式 - description: - en_US: If enabled, the bot will use the stream of telegram reply mode - zh_Hans: 如果启用,将使用电报流式方式来回复内容 - type: boolean - required: true - default: false + config: [] execution: python: path: "webchat.py" diff --git a/pkg/provider/entities.py b/pkg/provider/entities.py index ff1e4526..9dcaffcd 100644 --- a/pkg/provider/entities.py +++ b/pkg/provider/entities.py @@ -148,8 +148,6 @@ class MessageChunk(pydantic.BaseModel): tool_call_id: typing.Optional[str] = None - # tool_calls: typing.Optional[list[ToolCallChunk]] = None - is_final: bool = False def readable_str(self) -> str: diff --git a/pkg/provider/modelmgr/requester.py b/pkg/provider/modelmgr/requester.py index 1545a2e4..5701d846 100644 --- a/pkg/provider/modelmgr/requester.py +++ b/pkg/provider/modelmgr/requester.py @@ -75,7 +75,6 @@ class LLMAPIRequester(metaclass=abc.ABCMeta): """ pass - @abc.abstractmethod async def invoke_llm_stream( self, query: core_entities.Query, diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index 30c48cf6..15b118c8 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -52,7 +52,6 @@ class LocalAgentRunner(runner.RequestRunner): query.use_llm_model, req_messages, query.use_funcs, - stream=is_stream, extra_args=query.use_llm_model.model_entity.extra_args, ): yield msg @@ -113,7 +112,6 @@ class LocalAgentRunner(runner.RequestRunner): query.use_llm_model, req_messages, query.use_funcs, - stream=is_stream, extra_args=query.use_llm_model.model_entity.extra_args, ): yield msg diff --git a/templates/metadata/pipeline/trigger.yaml b/templates/metadata/pipeline/trigger.yaml index 165e488e..08a2714b 100644 --- a/templates/metadata/pipeline/trigger.yaml +++ b/templates/metadata/pipeline/trigger.yaml @@ -134,8 +134,11 @@ stages: default: true - name: remove_think label: - en_US: remove think - zh_Hans: 删除深度思考消息 + en_US: Remove CoT + zh_Hans: 删除思维链 + description: + en_US: If enabled, LangBot will remove the LLM thought content in response + zh_Hans: 如果启用,将自动删除大模型回复中的模型思考内容 type: boolean required: true default: true diff --git a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx index c45a7085..833c98d8 100644 --- a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx +++ b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx @@ -190,7 +190,7 @@ export default function DebugDialog({ const botMessage: Message = { id: -1, role: 'assistant', - content: '生成中...', + content: 'Generating...', timestamp: new Date().toISOString(), message_chain: [{ type: 'Plain', text: '' }], }; @@ -216,6 +216,7 @@ export default function DebugDialog({ selectedPipelineId, (data) => { // 处理流式响应数据 + console.log('data', data); if (data.message) { // 更新完整内容 fullContent = data.message.content; @@ -231,8 +232,11 @@ export default function DebugDialog({ typingInterval = setInterval(() => { if (currentPos < targetContent.length) { - displayContent = targetContent.substring(0, currentPos + 1); - currentPos++; + displayContent = targetContent.substring( + 0, + currentPos + 10, + ); + currentPos += 10; // 更新bot消息 setMessages((prevMessages) => { @@ -255,7 +259,7 @@ export default function DebugDialog({ } else { clearInterval(typingInterval); } - }, 30); // 调整这个值可以改变打字速度 + }, 1); // 调整这个值可以改变打字速度 } }, () => { From 48d11540aee4a2db4287375c759ea8545e765242 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 3 Aug 2025 17:18:44 +0800 Subject: [PATCH 030/107] feat: no longer use typewriter in debug dialog --- .../components/debug-dialog/DebugDialog.tsx | 104 +++++------------- 1 file changed, 25 insertions(+), 79 deletions(-) diff --git a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx index 833c98d8..8505f4f9 100644 --- a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx +++ b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx @@ -37,15 +37,11 @@ export default function DebugDialog({ const [showAtPopover, setShowAtPopover] = useState(false); const [hasAt, setHasAt] = useState(false); const [isHovering, setIsHovering] = useState(false); - const [isStreaming, setIsStreaming] = useState(false); + const [isStreaming, setIsStreaming] = useState(true); const messagesEndRef = useRef(null); const inputRef = useRef(null); const popoverRef = useRef(null); - // const scrollToBottom = () => { - // messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - // }; - const scrollToBottom = useCallback(() => { // 使用setTimeout确保在DOM更新后执行滚动 setTimeout(() => { @@ -177,6 +173,7 @@ export default function DebugDialog({ // for showing text_content = '@webchatbot' + text_content; } + const userMessage: Message = { id: -1, role: 'user', @@ -186,13 +183,15 @@ export default function DebugDialog({ }; // 根据isStreaming状态决定使用哪种传输方式 if (isStreaming) { + // streaming // 创建初始bot消息 - const botMessage: Message = { - id: -1, + const placeholderRandomId = Math.floor(Math.random() * 1000000); + const botMessagePlaceholder: Message = { + id: placeholderRandomId, role: 'assistant', content: 'Generating...', timestamp: new Date().toISOString(), - message_chain: [{ type: 'Plain', text: '' }], + message_chain: [{ type: 'Plain', text: 'Generating...' }], }; // 添加用户消息和初始bot消息到状态 @@ -200,16 +199,11 @@ export default function DebugDialog({ setMessages((prevMessages) => [ ...prevMessages, userMessage, - botMessage, + botMessagePlaceholder, ]); setInputValue(''); setHasAt(false); - try { - let fullContent = ''; // 保存完整内容 - let displayContent = ''; // 当前显示内容 - let typingInterval: NodeJS.Timeout; - await httpClient.sendStreamingWebChatMessage( sessionType, messageChain, @@ -219,78 +213,29 @@ export default function DebugDialog({ console.log('data', data); if (data.message) { // 更新完整内容 - fullContent = data.message.content; - // 清除之前的打字效果 - if (typingInterval) { - clearInterval(typingInterval); - } - - // 开始新的打字效果 - let currentPos = displayContent.length; - const targetContent = fullContent; - - typingInterval = setInterval(() => { - if (currentPos < targetContent.length) { - displayContent = targetContent.substring( - 0, - currentPos + 10, - ); - currentPos += 10; - - // 更新bot消息 - setMessages((prevMessages) => { - const updatedMessages = [...prevMessages]; - const botMessageIndex = updatedMessages.length - 1; - - if (botMessageIndex !== -1) { - const updatedBotMessage = { - ...updatedMessages[botMessageIndex], - content: displayContent, - message_chain: [ - { type: 'Plain', text: displayContent }, - ], - }; - updatedMessages[botMessageIndex] = updatedBotMessage; - } - setTimeout(scrollToBottom, 0); // 确保在状态更新后滚动 - return updatedMessages; - }); - } else { - clearInterval(typingInterval); + setMessages((prevMessages) => { + const updatedMessages = [...prevMessages]; + const botMessageIndex = updatedMessages.findIndex( + (message) => message.id === placeholderRandomId, + ); + if (botMessageIndex !== -1) { + updatedMessages[botMessageIndex] = { + ...updatedMessages[botMessageIndex], + content: data.message.content, + message_chain: [ + { type: 'Plain', text: data.message.content }, + ], + }; } - }, 1); // 调整这个值可以改变打字速度 + return updatedMessages; + }); } }, - () => { - // 流传输完成 - console.log('Streaming completed'); - if (typingInterval) { - clearInterval(typingInterval); - } - // 确保最终内容完全显示 - setMessages((prevMessages) => { - const updatedMessages = [...prevMessages]; - const botMessageIndex = updatedMessages.length - 1; - - if (botMessageIndex !== -1) { - const updatedBotMessage = { - ...updatedMessages[botMessageIndex], - content: fullContent, - message_chain: [{ type: 'Plain', text: fullContent }], - }; - updatedMessages[botMessageIndex] = updatedBotMessage; - } - setTimeout(scrollToBottom, 0); // 确保在状态更新后滚动 - return updatedMessages; - }); - }, + () => {}, (error) => { // 处理错误 console.error('Streaming error:', error); - if (typingInterval) { - clearInterval(typingInterval); - } if (sessionType === 'person') { toast.error(t('pipelines.debugDialog.sendFailed')); } @@ -303,6 +248,7 @@ export default function DebugDialog({ } } } else { + // non-streaming setMessages((prevMessages) => [...prevMessages, userMessage]); setInputValue(''); setHasAt(false); From 44ac8b2b637790049e6b8c8d8e9ad710f36cebc6 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Sun, 3 Aug 2025 23:23:51 +0800 Subject: [PATCH 031/107] fix: In the runner, every 8 tokens yield --- pkg/provider/modelmgr/requesters/chatcmpl.py | 3 +- .../modelmgr/requesters/giteeaichatcmpl.py | 3 +- .../modelmgr/requesters/modelscopechatcmpl.py | 3 +- pkg/provider/runners/localagent.py | 31 ++++++++++--------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index f05af8c3..aa783bee 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -189,8 +189,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): delta_message.is_final = True delta_message.content = current_content - if chunk_idx % 64 == 0 or delta_message.is_final: - yield delta_message + yield delta_message # return async def _closure( diff --git a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py index 1c19a534..7ac9fa1a 100644 --- a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py +++ b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py @@ -195,5 +195,4 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): delta_message.is_final = True delta_message.content = current_content - if chunk_idx % 64 == 0 or delta_message.is_final: - yield delta_message + yield delta_message diff --git a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py index b98ae7ff..9a4e723e 100644 --- a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py +++ b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py @@ -286,8 +286,7 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester): delta_message.is_final = True delta_message.content = current_content - if chunk_idx % 64 == 0 or delta_message.is_final: - yield delta_message + yield delta_message # return async def invoke_llm( diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index 15b118c8..35a4ca17 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -47,6 +47,7 @@ class LocalAgentRunner(runner.RequestRunner): else: # 流式输出,需要处理工具调用 tool_calls_map: dict[str, llm_entities.ToolCall] = {} + msg_idx = 0 async for msg in query.use_llm_model.requester.invoke_llm_stream( query, query.use_llm_model, @@ -54,7 +55,9 @@ class LocalAgentRunner(runner.RequestRunner): query.use_funcs, extra_args=query.use_llm_model.model_entity.extra_args, ): - yield msg + msg_idx = msg_idx + 1 + if msg_idx % 8 == 0 or msg.is_final: + yield msg if msg.tool_calls: for tool_call in msg.tool_calls: if tool_call.id not in tool_calls_map: @@ -115,19 +118,19 @@ class LocalAgentRunner(runner.RequestRunner): extra_args=query.use_llm_model.model_entity.extra_args, ): yield msg - if msg.tool_calls: - for tool_call in msg.tool_calls: - if tool_call.id not in tool_calls_map: - tool_calls_map[tool_call.id] = llm_entities.ToolCall( - id=tool_call.id, - type=tool_call.type, - function=llm_entities.FunctionCall( - name=tool_call.function.name if tool_call.function else '', arguments='' - ), - ) - if tool_call.function and tool_call.function.arguments: - # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 - tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + if msg.tool_calls: + for tool_call in msg.tool_calls: + if tool_call.id not in tool_calls_map: + tool_calls_map[tool_call.id] = llm_entities.ToolCall( + id=tool_call.id, + type=tool_call.type, + function=llm_entities.FunctionCall( + name=tool_call.function.name if tool_call.function else '', arguments='' + ), + ) + if tool_call.function and tool_call.function.arguments: + # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 + tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments final_msg = llm_entities.Message( role=msg.role, content=msg.all_content, From ba4b5255a2e0641f8a5090a657de8149fd449012 Mon Sep 17 00:00:00 2001 From: zejiewang <511217265@qq.com> Date: Sun, 18 May 2025 12:03:01 +0800 Subject: [PATCH 032/107] feat:support dify message streaming output (#1437) * fix:lark adapter listeners init problem * feat:support dify streaming mode * feat:remove some log * fix(bot form): field desc missing * fix: not compatible with chatflow --------- Co-authored-by: wangzejie Co-authored-by: Junyan Qin --- pkg/platform/botmgr.py | 4 +- pkg/platform/sources/lark.py | 108 ++++++++++++++++++++++++++++ pkg/platform/sources/lark.yaml | 17 +++++ pkg/provider/runners/difysvapi.py | 41 +++++++++-- templates/metadata/pipeline/ai.yaml | 15 ++++ 5 files changed, 180 insertions(+), 5 deletions(-) diff --git a/pkg/platform/botmgr.py b/pkg/platform/botmgr.py index 5855525f..1da5eec8 100644 --- a/pkg/platform/botmgr.py +++ b/pkg/platform/botmgr.py @@ -120,8 +120,10 @@ class RuntimeBot: if isinstance(e, asyncio.CancelledError): self.task_context.set_current_action('Exited.') return + + traceback_str = traceback.format_exc() self.task_context.set_current_action('Exited with error.') - await self.logger.error(f'平台适配器运行出错:\n{e}\n{traceback.format_exc()}') + await self.logger.error(f'平台适配器运行出错:\n{e}\n{traceback_str}') self.task_wrapper = self.ap.task_mgr.create_task( exception_wrapper(), diff --git a/pkg/platform/sources/lark.py b/pkg/platform/sources/lark.py index f8faf522..67b4f101 100644 --- a/pkg/platform/sources/lark.py +++ b/pkg/platform/sources/lark.py @@ -9,6 +9,7 @@ import re import base64 import uuid import json +import time import datetime import hashlib from Crypto.Cipher import AES @@ -320,6 +321,10 @@ class LarkEventConverter(adapter.EventConverter): ) +CARD_ID_CACHE_SIZE = 500 +CARD_ID_CACHE_MAX_LIFETIME = 20 * 60 # 20分钟 + + class LarkAdapter(adapter.MessagePlatformAdapter): bot: lark_oapi.ws.Client api_client: lark_oapi.Client @@ -338,6 +343,8 @@ class LarkAdapter(adapter.MessagePlatformAdapter): config: dict quart_app: quart.Quart ap: app.Application + + message_id_to_card_id: typing.Dict[str, typing.Tuple[str, int]] def __init__(self, config: dict, ap: app.Application, logger: EventLogger): self.config = config @@ -345,6 +352,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): self.logger = logger self.quart_app = quart.Quart(__name__) self.listeners = {} + self.message_id_to_card_id = {} @self.quart_app.route('/lark/callback', methods=['POST']) async def lark_callback(): @@ -390,6 +398,19 @@ class LarkAdapter(adapter.MessagePlatformAdapter): return {'code': 500, 'message': 'error'} async def on_message(event: lark_oapi.im.v1.P2ImMessageReceiveV1): + if self.config['enable-card-reply'] and event.event.message.message_id not in self.message_id_to_card_id: + self.ap.logger.debug('卡片回复模式开启') + # 开启卡片回复模式. 这里可以实现飞书一发消息,马上创建卡片进行回复"思考中..." + reply_message_id = await self.create_message_card(event.event.message.message_id) + self.message_id_to_card_id[event.event.message.message_id] = (reply_message_id, time.time()) + + if len(self.message_id_to_card_id) > CARD_ID_CACHE_SIZE: + self.message_id_to_card_id = { + k: v + for k, v in self.message_id_to_card_id.items() + if v[1] > time.time() - CARD_ID_CACHE_MAX_LIFETIME + } + lb_event = await self.event_converter.target2yiri(event, self.api_client) await self.listeners[type(lb_event)](lb_event, self) @@ -409,11 +430,93 @@ class LarkAdapter(adapter.MessagePlatformAdapter): async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): pass + async def create_message_card(self, message_id: str) -> str: + """ + 创建卡片消息。 + 使用卡片消息是因为普通消息更新次数有限制,而大模型流式返回结果可能很多而超过限制,而飞书卡片没有这个限制 + """ + + # TODO 目前只支持卡片模板方式,且卡片变量一定是content,未来这块要做成可配置 + # 发消息马上就会回复显示初始化的content信息,即思考中 + content = { + 'type': 'template', + 'data': {'template_id': self.config['card_template_id'], 'template_variable': {'content': 'Thinking...'}}, + } + request: ReplyMessageRequest = ( + ReplyMessageRequest.builder() + .message_id(message_id) + .request_body( + ReplyMessageRequestBody.builder().content(json.dumps(content)).msg_type('interactive').build() + ) + .build() + ) + + # 发起请求 + response: ReplyMessageResponse = await self.api_client.im.v1.message.areply(request) + + # 处理失败返回 + if not response.success(): + raise Exception( + f'client.im.v1.message.reply failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}' + ) + return response.data.message_id + async def reply_message( self, message_source: platform_events.MessageEvent, message: platform_message.MessageChain, quote_origin: bool = False, + ): + if self.config['enable-card-reply']: + await self.reply_card_message(message_source, message, quote_origin) + else: + await self.reply_normal_message(message_source, message, quote_origin) + + async def reply_card_message( + self, + message_source: platform_events.MessageEvent, + message: platform_message.MessageChain, + quote_origin: bool = False, + ): + """ + 回复消息变成更新卡片消息 + """ + lark_message = await self.message_converter.yiri2target(message, self.api_client) + + text_message = '' + for ele in lark_message[0]: + if ele['tag'] == 'text': + text_message += ele['text'] + elif ele['tag'] == 'md': + text_message += ele['text'] + + content = { + 'type': 'template', + 'data': {'template_id': self.config['card_template_id'], 'template_variable': {'content': text_message}}, + } + + request: PatchMessageRequest = ( + PatchMessageRequest.builder() + .message_id(self.message_id_to_card_id[message_source.message_chain.message_id][0]) + .request_body(PatchMessageRequestBody.builder().content(json.dumps(content)).build()) + .build() + ) + + # 发起请求 + response: PatchMessageResponse = self.api_client.im.v1.message.patch(request) + + # 处理失败返回 + if not response.success(): + raise Exception( + f'client.im.v1.message.patch failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}' + ) + return + + async def reply_normal_message( + self, + message_source: platform_events.MessageEvent, + message: platform_message.MessageChain, + quote_origin: bool = False, ): # 不再需要了,因为message_id已经被包含到message_chain中 # lark_event = await self.event_converter.yiri2target(message_source) @@ -492,4 +595,9 @@ class LarkAdapter(adapter.MessagePlatformAdapter): ) async def kill(self) -> bool: + # 需要断开连接,不然旧的连接会继续运行,导致飞书消息来时会随机选择一个连接 + # 断开时lark.ws.Client的_receive_message_loop会打印error日志: receive message loop exit。然后进行重连, + # 所以要设置_auto_reconnect=False,让其不重连。 + self.bot._auto_reconnect = False + await self.bot._disconnect() return False diff --git a/pkg/platform/sources/lark.yaml b/pkg/platform/sources/lark.yaml index f51bab76..bafaba81 100644 --- a/pkg/platform/sources/lark.yaml +++ b/pkg/platform/sources/lark.yaml @@ -65,6 +65,23 @@ spec: type: string required: true default: "" + - name: enable-card-reply + label: + en_US: Enable Card Reply Mode + zh_Hans: 启用飞书卡片回复模式 + description: + en_US: If enabled, the bot will use the card of lark reply mode + zh_Hans: 如果启用,将使用飞书卡片方式来回复内容 + type: boolean + required: true + default: false + - name: card_template_id + label: + en_US: card template id + zh_Hans: 卡片模板ID + type: string + required: true + default: "填写你的卡片template_id" execution: python: path: ./lark.py diff --git a/pkg/provider/runners/difysvapi.py b/pkg/provider/runners/difysvapi.py index b2542491..98b50f86 100644 --- a/pkg/provider/runners/difysvapi.py +++ b/pkg/provider/runners/difysvapi.py @@ -108,7 +108,13 @@ class DifyServiceAPIRunner(runner.RequestRunner): mode = 'basic' # 标记是基础编排还是工作流编排 - basic_mode_pending_chunk = '' + stream_output_pending_chunk = '' + + batch_pending_max_size = self.pipeline_config['ai']['dify-service-api'].get( + 'output-batch-size', 0 + ) # 积累一定量的消息更新消息一次 + + batch_pending_index = 0 inputs = {} @@ -126,6 +132,13 @@ class DifyServiceAPIRunner(runner.RequestRunner): ): self.ap.logger.debug('dify-chat-chunk: ' + str(chunk)) + # 查询异常情况 + if chunk['event'] == 'error': + yield llm_entities.Message( + role='assistant', + content=f"查询异常: [{chunk['code']}]. {chunk['message']}.\n请重试,如果还报错,请用 **!reset** 命令重置对话再尝试。", + ) + if chunk['event'] == 'workflow_started': mode = 'workflow' @@ -136,15 +149,35 @@ class DifyServiceAPIRunner(runner.RequestRunner): role='assistant', content=self._try_convert_thinking(chunk['data']['outputs']['answer']), ) + elif chunk['event'] == 'message': + stream_output_pending_chunk += chunk['answer'] + if self.pipeline_config['ai']['dify-service-api'].get('enable-streaming', False): + # 消息数超过量就输出,从而达到streaming的效果 + batch_pending_index += 1 + if batch_pending_index >= batch_pending_max_size: + yield llm_entities.Message( + role='assistant', + content=self._try_convert_thinking(stream_output_pending_chunk), + ) + batch_pending_index = 0 elif mode == 'basic': if chunk['event'] == 'message': - basic_mode_pending_chunk += chunk['answer'] + stream_output_pending_chunk += chunk['answer'] + if self.pipeline_config['ai']['dify-service-api'].get('enable-streaming', False): + # 消息数超过量就输出,从而达到streaming的效果 + batch_pending_index += 1 + if batch_pending_index >= batch_pending_max_size: + yield llm_entities.Message( + role='assistant', + content=self._try_convert_thinking(stream_output_pending_chunk), + ) + batch_pending_index = 0 elif chunk['event'] == 'message_end': yield llm_entities.Message( role='assistant', - content=self._try_convert_thinking(basic_mode_pending_chunk), + content=self._try_convert_thinking(stream_output_pending_chunk), ) - basic_mode_pending_chunk = '' + stream_output_pending_chunk = '' if chunk is None: raise errors.DifyAPIError('Dify API 没有返回任何响应,请检查网络连接和API配置') diff --git a/templates/metadata/pipeline/ai.yaml b/templates/metadata/pipeline/ai.yaml index ffbefe63..4d0cb6c3 100644 --- a/templates/metadata/pipeline/ai.yaml +++ b/templates/metadata/pipeline/ai.yaml @@ -138,6 +138,21 @@ stages: label: en_US: Remove zh_Hans: 移除 + - name: enable-streaming + label: + en_US: enable streaming mode + zh_Hans: 开启流式输出 + type: boolean + required: true + default: false + - name: output-batch-size + label: + en_US: output batch size + zh_Hans: 输出批次大小(积累多少条消息后一起输出) + type: integer + required: true + default: 10 + - name: dashscope-app-api label: en_US: Aliyun Dashscope App API From b65670cd1acda03c845b9e0368cb8df0452467aa Mon Sep 17 00:00:00 2001 From: fdc Date: Mon, 30 Jun 2025 17:58:18 +0800 Subject: [PATCH 033/107] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E6=B5=81?= =?UTF-8?q?=E5=BC=8F=E6=B6=88=E6=81=AF=E5=A4=84=E7=90=86=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fix.MD | 47 +++++++ pkg/core/entities.py | 2 +- pkg/pipeline/process/handlers/chat.py | 36 +++++- pkg/pipeline/respback/respback.py | 22 ++++ pkg/platform/adapter.py | 21 ++++ pkg/provider/entities.py | 83 ++++++++++++ pkg/provider/modelmgr/requester.py | 5 +- pkg/provider/modelmgr/requesters/chatcmpl.py | 90 +++++++++++-- pkg/provider/runners/localagent.py | 125 +++++++++++++++---- 9 files changed, 385 insertions(+), 46 deletions(-) create mode 100644 fix.MD diff --git a/fix.MD b/fix.MD new file mode 100644 index 00000000..51927eb9 --- /dev/null +++ b/fix.MD @@ -0,0 +1,47 @@ +## 底层模型请求器 + +- pkg/provider/modelmgr/requesters/... + +给 invoke_llm 加个 stream: bool 参数,并允许 invoke_llm 返回两种参数:原来的 llm_entities.Message(非流式)和 返回 llm_entities.MessageChunk(流式,需要新增这个实体)的 AsyncGenerator + +## Runner + +- pkg/provider/runners/... + +每个runner的run方法也允许传入stream: bool。 + +现在的run方法本身就是生成器(AsyncGenerator),因为agent是有多回合的,会生成多条Message。但现在需要支持文本消息可以分段。 + +现在run方法应该返回 AsyncGenerator[ Union[ Message, AsyncGenerator[MessageChunk] ] ]。 + +对于 local agent 的实现上,调用模型invoke_llm时,传入stream,当发现模型返回的是Message时,即按照现在的写法操作Message;当返回的是 AsyncGenerator 时,需要 yield MessageChunk 给上层,同时需要注意判断工具调用。 + +## 流水线 + +- pkg/pipeline/process/handlers/chat.py + +之前这里就已经有一个生成器写法了,用于处理 AsyncGenerator[Message],但现在需要加上一个判断,如果yield出来的是 Message 则按照现在的处理;如果yield出来的是 AsyncGenerator,那么就需要再 async for 一层; + +因为流水线是基于责任链模式设计的,这里的生成结果只需要放入 Query 对象中,供下一层处理。 + +所以需要在 Query 对象中支持存入MessageChunk,现在只支持存 Message 到 resp_messages,这里得设计一下。 + +## 回复阶段 + +最终会在 pkg/pipeline/respback/respback.py 中检出 query 中的信息并发回,这里也要改成支持 MessagChunk 的。 + +这里应该判断适配器是否支持流式,若不支持,应该等待所有 MessageChunk 生成,拼接成 Message 再转换成 MessageChain 调用 send_message(); + +若支持,则uuid生成一个message id,使用该message id调用适配器的 reply_message_chunk 方法。 + +## 机器人适配器 + +因为机器人可能会由于用户配置项不同而表现为对流式的支持性不同,比如飞书默认不支持流式,需要用户额外配置卡片。 + +所以需要新增一个方法 `is_stream_output_supported() -> bool`,这个让每个适配器来判断并返回是否支持流式; + +在发送时,得加两个方法 `send_message_chunk(target_type: str, target_id: str, message_id: , message: MessageChain)` + +message_id 确定同一条消息,由调用方生成; + +`reply_message_chunk(message_source: MessageEvent, message: MessageChain)` \ No newline at end of file diff --git a/pkg/core/entities.py b/pkg/core/entities.py index 8dc51e5b..31514fa8 100644 --- a/pkg/core/entities.py +++ b/pkg/core/entities.py @@ -87,7 +87,7 @@ class Query(pydantic.BaseModel): """使用的函数,由前置处理器阶段设置""" resp_messages: ( - typing.Optional[list[llm_entities.Message]] | typing.Optional[list[platform_message.MessageChain]] + typing.Optional[list[llm_entities.Message]] | typing.Optional[list[platform_message.MessageChain]] | typing.Optional[list[llm_entities.MessageChunk]] ) = [] """由Process阶段生成的回复消息对象列表""" diff --git a/pkg/pipeline/process/handlers/chat.py b/pkg/pipeline/process/handlers/chat.py index 2aa08e17..5518f89d 100644 --- a/pkg/pipeline/process/handlers/chat.py +++ b/pkg/pipeline/process/handlers/chat.py @@ -1,5 +1,6 @@ from __future__ import annotations +from itertools import accumulate import typing import traceback @@ -59,6 +60,8 @@ class ChatMessageHandler(handler.MessageHandler): text_length = 0 + is_stream = query.adapter.is_stream_output_supported() + try: for r in runner_module.preregistered_runners: if r.name == query.pipeline_config['ai']['runner']['runner']: @@ -67,17 +70,38 @@ class ChatMessageHandler(handler.MessageHandler): else: raise ValueError(f'Request runner not found: {query.pipeline_config["ai"]["runner"]["runner"]}') - async for result in runner.run(query): - query.resp_messages.append(result) + if is_stream: + accumulated_messages = [] + async for result in runner.run(query): + accumulated_messages.append(result) + query.resp_messages.append(result) - self.ap.logger.info(f'Response({query.query_id}): {self.cut_str(result.readable_str())}') + self.ap.logger.info(f'对话({query.query_id})流式响应: {self.cut_str(result.readable_str())}') - if result.content is not None: - text_length += len(result.content) + if result.content is not None: + text_length += len(result.content) - yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) + # current_chain = platform_message.MessageChain([]) + # for msg in accumulated_messages: + # if msg.content is not None: + # current_chain.append(platform_message.Plain(msg.content)) + # query.resp_message_chain = [current_chain] + + yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) + else: + + async for result in runner.run(query): + query.resp_messages.append(result) + + self.ap.logger.info(f'对话({query.query_id})响应: {self.cut_str(result.readable_str())}') + + if result.content is not None: + text_length += len(result.content) + + yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) query.session.using_conversation.messages.append(query.user_message) + query.session.using_conversation.messages.extend(query.resp_messages) except Exception as e: self.ap.logger.error(f'Request failed({query.query_id}): {type(e).__name__} {str(e)}') diff --git a/pkg/pipeline/respback/respback.py b/pkg/pipeline/respback/respback.py index 39d3abb1..7654896b 100644 --- a/pkg/pipeline/respback/respback.py +++ b/pkg/pipeline/respback/respback.py @@ -36,6 +36,28 @@ class SendResponseBackStage(stage.PipelineStage): quote_origin = query.pipeline_config['output']['misc']['quote-origin'] + has_chunks = any(isinstance(msg, llm_entities.MessageChunk) for msg in query.resp_messages) + if has_chunks and hasattr(query.adapter,'reply_message_chunk'): + + async def message_generator(): + for msg in query.resp_messages: + if isinstance(msg, llm_entities.MessageChunk): + yield msg.content + else: + yield msg.content + await query.adapter.reply_message_chunk( + message_source=query.message_event, + message_id=query.message_event.message_id, + message_generator=message_generator(), + quote_origin=quote_origin, + ) + else: + await query.adapter.reply_message( + message_source=query.message_event, + message=query.resp_message_chain[-1], + quote_origin=quote_origin, + ) + await query.adapter.reply_message( message_source=query.message_event, message=query.resp_message_chain[-1], diff --git a/pkg/platform/adapter.py b/pkg/platform/adapter.py index f28ad3dc..c841ae98 100644 --- a/pkg/platform/adapter.py +++ b/pkg/platform/adapter.py @@ -49,11 +49,27 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): async def reply_message( self, message_source: platform_events.MessageEvent, + message_id: int, message: platform_message.MessageChain, quote_origin: bool = False, ): """回复消息 + Args: + message_source (platform.types.MessageEvent): 消息源事件 + message_id (int): 消息ID + message (platform.types.MessageChain): 消息链 + quote_origin (bool, optional): 是否引用原消息. Defaults to False. + """ + raise NotImplementedError + + async def reply_message_chunk( + self, + message_source: platform_events.MessageEvent, + message: platform_message.MessageChain, + quote_origin: bool = False, + ): + """回复消息(流式输出) Args: message_source (platform.types.MessageEvent): 消息源事件 message (platform.types.MessageChain): 消息链 @@ -94,6 +110,11 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): async def run_async(self): """异步运行""" raise NotImplementedError + + + async def is_stream_output_supported(self) -> bool: + """是否支持流式输出""" + return False async def kill(self) -> bool: """关闭适配器 diff --git a/pkg/provider/entities.py b/pkg/provider/entities.py index 94b812d9..a149fea3 100644 --- a/pkg/provider/entities.py +++ b/pkg/provider/entities.py @@ -125,6 +125,89 @@ class Message(pydantic.BaseModel): return platform_message.MessageChain(mc) +class MessageChunk(pydantic.BaseModel): + """消息""" + + role: str # user, system, assistant, tool, command, plugin + """消息的角色""" + + name: typing.Optional[str] = None + """名称,仅函数调用返回时设置""" + + all_content: typing.Optional[str] = None + """所有内容""" + + content: typing.Optional[list[ContentElement]] | typing.Optional[str] = None + """内容""" + + # tool_calls: typing.Optional[list[ToolCall]] = None + """工具调用""" + + tool_call_id: typing.Optional[str] = None + + tool_calls: typing.Optional[list[ToolCallChunk]] = None + + is_final: bool = False + + def readable_str(self) -> str: + if self.content is not None: + return str(self.role) + ': ' + str(self.get_content_platform_message_chain()) + elif self.tool_calls is not None: + return f'调用工具: {self.tool_calls[0].id}' + else: + return '未知消息' + + def get_content_platform_message_chain(self, prefix_text: str = '') -> platform_message.MessageChain | None: + """将内容转换为平台消息 MessageChain 对象 + + Args: + prefix_text (str): 首个文字组件的前缀文本 + """ + + if self.content is None: + return None + elif isinstance(self.content, str): + return platform_message.MessageChain([platform_message.Plain(prefix_text + self.content)]) + elif isinstance(self.content, list): + mc = [] + for ce in self.content: + if ce.type == 'text': + mc.append(platform_message.Plain(ce.text)) + elif ce.type == 'image_url': + if ce.image_url.url.startswith('http'): + mc.append(platform_message.Image(url=ce.image_url.url)) + else: # base64 + b64_str = ce.image_url.url + + if b64_str.startswith('data:'): + b64_str = b64_str.split(',')[1] + + mc.append(platform_message.Image(base64=b64_str)) + + # 找第一个文字组件 + if prefix_text: + for i, c in enumerate(mc): + if isinstance(c, platform_message.Plain): + mc[i] = platform_message.Plain(prefix_text + c.text) + break + else: + mc.insert(0, platform_message.Plain(prefix_text)) + + return platform_message.MessageChain(mc) + + +class ToolCallChunk(pydantic.BaseModel): + """工具调用""" + + id: str + """工具调用ID""" + + type: str + """工具调用类型""" + + function: FunctionCall + """函数调用""" + class Prompt(pydantic.BaseModel): """供AI使用的Prompt""" diff --git a/pkg/provider/modelmgr/requester.py b/pkg/provider/modelmgr/requester.py index 17697cdb..337154be 100644 --- a/pkg/provider/modelmgr/requester.py +++ b/pkg/provider/modelmgr/requester.py @@ -83,8 +83,9 @@ class ProviderAPIRequester(metaclass=abc.ABCMeta): model: RuntimeLLMModel, messages: typing.List[llm_entities.Message], funcs: typing.List[tools_entities.LLMFunction] = None, + stream: bool = False, extra_args: dict[str, typing.Any] = {}, - ) -> llm_entities.Message: + ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: """调用API Args: @@ -94,7 +95,7 @@ class ProviderAPIRequester(metaclass=abc.ABCMeta): extra_args (dict[str, typing.Any], optional): 额外的参数. Defaults to {}. Returns: - llm_entities.Message: 返回消息对象 + llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: 返回消息对象 """ pass diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index aaaf3751..ef6ecd71 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -57,13 +57,35 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): message = llm_entities.Message(**chatcmpl_message) return message + + async def _make_msg_chunk( + self, + chat_completion: chat_completion.ChatCompletion, + ) -> llm_entities.MessageChunk: + choice = chat_completion.choices[0] + delta = choice.delta.model_dump() + # 确保 role 字段存在且不为 None + if 'role' not in delta or delta['role'] is None: + delta['role'] = 'assistant' + + reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None + + # deepseek的reasoner模型 + if reasoning_content is not None: + delta['content'] = '\n' + reasoning_content + '\n\n' + delta['content'] + + message = llm_entities.MessageChunk(**delta) + + return message + async def _closure( self, query: core_entities.Query, req_messages: list[dict], use_model: requester.RuntimeLLMModel, use_funcs: list[tools_entities.LLMFunction] = None, + stream: bool = False, extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message: self.client.api_key = use_model.token_mgr.get_token() @@ -91,13 +113,42 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): args['messages'] = messages - # 发送请求 - resp = await self._req(args, extra_body=extra_args) + if stream: + current_content = '' + async for chunk in await self._req(args, extra_body=extra_args): - # 处理请求结果 - message = await self._make_msg(resp) + # 处理流式消息 + delta_message = await self._make_msg_chunk( + chat_completion=chunk, + ) + if delta_message.content: + current_content += delta_message.content + delta_message.all_content = current_content + + # 检查是否为最后一个块 + if chunk.choices[0].finish_reason is not None: + delta_message.is_final = True - return message + yield delta_message + return + + else: + + # 非流式请求 + resp = await self._req(args, extra_body=extra_args) + # 处理请求结果 + # 发送请求 + resp = await self._req(args, extra_body=extra_args) + + # 处理请求结果 + message = await self._make_msg(resp) + + return message + + + + + async def invoke_llm( self, @@ -105,8 +156,9 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): model: requester.RuntimeLLMModel, messages: typing.List[llm_entities.Message], funcs: typing.List[tools_entities.LLMFunction] = None, + stream: bool = False, extra_args: dict[str, typing.Any] = {}, - ) -> llm_entities.Message: + ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: req_messages = [] # req_messages 仅用于类内,外部同步由 query.messages 进行 for m in messages: msg_dict = m.dict(exclude_none=True) @@ -119,13 +171,25 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): req_messages.append(msg_dict) try: - return await self._closure( - query=query, - req_messages=req_messages, - use_model=model, - use_funcs=funcs, - extra_args=extra_args, - ) + if stream: + async for item in self._closure( + query=query, + req_messages=req_messages, + use_model=model, + use_funcs=funcs, + stream=stream, + extra_args=extra_args, + ): + yield item + return + else: + return await self._closure( + query=query, + req_messages=req_messages, + use_model=model, + use_funcs=funcs, + extra_args=extra_args, + ) except asyncio.TimeoutError: raise errors.RequesterError('请求超时') except openai.BadRequestError as e: diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index 1d3e88ac..78aaf2bb 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -2,6 +2,7 @@ from __future__ import annotations import json import copy +from ssl import ALERT_DESCRIPTION_BAD_CERTIFICATE_HASH_VALUE import typing from .. import runner from ...core import entities as core_entities @@ -27,7 +28,13 @@ Respond in the same language as the user's input. class LocalAgentRunner(runner.RequestRunner): """本地Agent请求运行器""" - async def run(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: + class ToolCallTracker: + """工具调用追踪器""" + def __init__(self): + self.active_calls: dict[str,dict] = {} + self.completed_calls: list[llm_entities.ToolCall] = [] + + async def run(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message | llm_entities.MessageChunk, None]: """运行请求""" pending_tool_calls = [] @@ -80,20 +87,57 @@ class LocalAgentRunner(runner.RequestRunner): req_messages = query.prompt.messages.copy() + query.messages.copy() + [user_message] - # 首次请求 - msg = await query.use_llm_model.requester.invoke_llm( - query, - query.use_llm_model, - req_messages, - query.use_funcs, - extra_args=query.use_llm_model.model_entity.extra_args, - ) + is_stream = query.adapter.is_stream_output_supported() + # while True: + # pass + if not is_stream: + # 非流式输出,直接请求 + msg = await query.use_llm_model.requester.invoke_llm( + query, + query.use_llm_model, + req_messages, + query.use_funcs, + extra_args=query.use_llm_model.model_entity.extra_args, + ) + yield msg + final_msg = msg + else: + # 流式输出,需要处理工具调用 + tool_calls_map: dict[str, llm_entities.ToolCall] = {} + async for msg in await query.use_llm_model.requester.invoke_llm( + query, + query.use_llm_model, + req_messages, + query.use_funcs, + stream=is_stream, + extra_args=query.use_llm_model.model_entity.extra_args, + ): + assert isinstance(msg, llm_entities.MessageChunk) + yield msg + if msg.tool_calls: + for tool_call in msg.tool_calls: + if tool_call.id not in tool_calls_map: + tool_calls_map[tool_call.id] = llm_entities.ToolCall( + id=tool_call.id, + type=tool_call.type, + function=llm_entities.FunctionCall( + name=tool_call.function.name if tool_call.function else '', + arguments='' + ), + ) + if tool_call.function and tool_call.function.arguments: + # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 + tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + final_msg = llm_entities.Message( + role=msg.role, + content=msg.all_content, + tool_calls=list(tool_calls_map.values()), + ) - yield msg + + pending_tool_calls = final_msg.tool_calls - pending_tool_calls = msg.tool_calls - - req_messages.append(msg) + req_messages.append(final_msg) # 持续请求,只要还有待处理的工具调用就继续处理调用 while pending_tool_calls: @@ -122,17 +166,50 @@ class LocalAgentRunner(runner.RequestRunner): req_messages.append(err_msg) - # 处理完所有调用,再次请求 - msg = await query.use_llm_model.requester.invoke_llm( - query, - query.use_llm_model, - req_messages, - query.use_funcs, - extra_args=query.use_llm_model.model_entity.extra_args, - ) + if is_stream: + tool_calls_map = {} + async for msg in await query.use_llm_model.requester.invoke_llm( + query, + query.use_llm_model, + req_messages, + query.use_funcs, + stream=is_stream, + extra_args=query.use_llm_model.model_entity.extra_args, + ): + assert isinstance(msg, llm_entities.MessageChunk) + yield msg + if msg.tool_calls: + for tool_call in msg.tool_calls: + if tool_call.id not in tool_calls_map: + tool_calls_map[tool_call.id] = llm_entities.ToolCall( + id=tool_call.id, + type=tool_call.type, + function=llm_entities.FunctionCall( + name=tool_call.function.name if tool_call.function else '', + arguments='' + ), + ) + if tool_call.function and tool_call.function.arguments: + # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 + tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + final_msg = llm_entities.Message( + role=msg.role, + content=msg.all_content, + tool_calls=list(tool_calls_map.values()), + ) + else: + # 处理完所有调用,再次请求 + msg = await query.use_llm_model.requester.invoke_llm( + query, + query.use_llm_model, + req_messages, + query.use_funcs, + extra_args=query.use_llm_model.model_entity.extra_args, + ) - yield msg + yield msg + final_msg = msg - pending_tool_calls = msg.tool_calls + pending_tool_calls = final_msg.tool_calls - req_messages.append(msg) + req_messages.append(final_msg) From 0d53843230115ed722271a2d47d7600e1d3547f5 Mon Sep 17 00:00:00 2001 From: fdc Date: Tue, 1 Jul 2025 18:03:05 +0800 Subject: [PATCH 034/107] =?UTF-8?q?chat=E4=B8=AD=E7=9A=84=E6=B5=81?= =?UTF-8?q?=E5=BC=8F=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/pipeline/process/handlers/chat.py | 21 ++++++++------------- pkg/pipeline/respback/respback.py | 23 +++++++++-------------- 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/pkg/pipeline/process/handlers/chat.py b/pkg/pipeline/process/handlers/chat.py index 5518f89d..084fff6f 100644 --- a/pkg/pipeline/process/handlers/chat.py +++ b/pkg/pipeline/process/handlers/chat.py @@ -71,23 +71,18 @@ class ChatMessageHandler(handler.MessageHandler): raise ValueError(f'Request runner not found: {query.pipeline_config["ai"]["runner"]["runner"]}') if is_stream: - accumulated_messages = [] - async for result in runner.run(query): - accumulated_messages.append(result) - query.resp_messages.append(result) + async for results in runner.run(query): + async for result in results: - self.ap.logger.info(f'对话({query.query_id})流式响应: {self.cut_str(result.readable_str())}') + query.resp_messages.append(result) - if result.content is not None: - text_length += len(result.content) + self.ap.logger.info(f'对话({query.query_id})流式响应: {self.cut_str(result.readable_str())}') - # current_chain = platform_message.MessageChain([]) - # for msg in accumulated_messages: - # if msg.content is not None: - # current_chain.append(platform_message.Plain(msg.content)) - # query.resp_message_chain = [current_chain] + if result.content is not None: + text_length += len(result.content) + + yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) - yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) else: async for result in runner.run(query): diff --git a/pkg/pipeline/respback/respback.py b/pkg/pipeline/respback/respback.py index 7654896b..4ac4e1e3 100644 --- a/pkg/pipeline/respback/respback.py +++ b/pkg/pipeline/respback/respback.py @@ -7,6 +7,8 @@ import asyncio from ...platform.types import events as platform_events from ...platform.types import message as platform_message +from ...provider import entities as llm_entities + from .. import stage, entities from ...core import entities as core_entities @@ -38,17 +40,10 @@ class SendResponseBackStage(stage.PipelineStage): has_chunks = any(isinstance(msg, llm_entities.MessageChunk) for msg in query.resp_messages) if has_chunks and hasattr(query.adapter,'reply_message_chunk'): - - async def message_generator(): - for msg in query.resp_messages: - if isinstance(msg, llm_entities.MessageChunk): - yield msg.content - else: - yield msg.content await query.adapter.reply_message_chunk( message_source=query.message_event, - message_id=query.message_event.message_id, - message_generator=message_generator(), + message_id=query.query_id, + message_generator=query.resp_message_chain[-1], quote_origin=quote_origin, ) else: @@ -58,10 +53,10 @@ class SendResponseBackStage(stage.PipelineStage): quote_origin=quote_origin, ) - await query.adapter.reply_message( - message_source=query.message_event, - message=query.resp_message_chain[-1], - quote_origin=quote_origin, - ) + # await query.adapter.reply_message( + # message_source=query.message_event, + # message=query.resp_message_chain[-1], + # quote_origin=quote_origin, + # ) return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) From 6e0e5802cc01ecc085f3a68414d9553b2a466bfb Mon Sep 17 00:00:00 2001 From: fdc Date: Wed, 2 Jul 2025 10:49:50 +0800 Subject: [PATCH 035/107] =?UTF-8?q?fix:=E4=BF=AE=E6=94=B9=E6=89=8B?= =?UTF-8?q?=E8=AF=AFmessage=5Fid=E5=86=99=E8=BF=9Breply=5Fmessage=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/platform/adapter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/platform/adapter.py b/pkg/platform/adapter.py index c841ae98..18403b75 100644 --- a/pkg/platform/adapter.py +++ b/pkg/platform/adapter.py @@ -49,7 +49,6 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): async def reply_message( self, message_source: platform_events.MessageEvent, - message_id: int, message: platform_message.MessageChain, quote_origin: bool = False, ): @@ -57,7 +56,6 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): Args: message_source (platform.types.MessageEvent): 消息源事件 - message_id (int): 消息ID message (platform.types.MessageChain): 消息链 quote_origin (bool, optional): 是否引用原消息. Defaults to False. """ @@ -66,12 +64,14 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): async def reply_message_chunk( self, message_source: platform_events.MessageEvent, + message_id: int, message: platform_message.MessageChain, quote_origin: bool = False, ): """回复消息(流式输出) Args: message_source (platform.types.MessageEvent): 消息源事件 + message_id (int): 消息ID message (platform.types.MessageChain): 消息链 quote_origin (bool, optional): 是否引用原消息. Defaults to False. """ From ee545a163f55e996593ea481e8093f8d1b9a9e31 Mon Sep 17 00:00:00 2001 From: fdc Date: Thu, 3 Jul 2025 22:58:17 +0800 Subject: [PATCH 036/107] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=E9=A3=9E?= =?UTF-8?q?=E4=B9=A6=E4=B8=AD=E7=9A=84=E6=B5=81=E5=BC=8F=E4=BD=86=E6=98=AF?= =?UTF-8?q?=E5=A5=BD=E5=83=8F=E8=BF=98=E6=9C=89=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/platform/sources/lark.py | 142 ++++++++++++++++++++++++----------- 1 file changed, 100 insertions(+), 42 deletions(-) diff --git a/pkg/platform/sources/lark.py b/pkg/platform/sources/lark.py index 67b4f101..696076a4 100644 --- a/pkg/platform/sources/lark.py +++ b/pkg/platform/sources/lark.py @@ -346,6 +346,8 @@ class LarkAdapter(adapter.MessagePlatformAdapter): message_id_to_card_id: typing.Dict[str, typing.Tuple[str, int]] + card_id_dict: dict[str, str] + def __init__(self, config: dict, ap: app.Application, logger: EventLogger): self.config = config self.ap = ap @@ -353,6 +355,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): self.quart_app = quart.Quart(__name__) self.listeners = {} self.message_id_to_card_id = {} + self.card_id_dict = {} @self.quart_app.route('/lark/callback', methods=['POST']) async def lark_callback(): @@ -397,11 +400,69 @@ class LarkAdapter(adapter.MessagePlatformAdapter): await self.logger.error(f'Error in lark callback: {traceback.format_exc()}') return {'code': 500, 'message': 'error'} + + def is_stream_output_supported() -> bool: + is_stream = False + if self.config.get("",None): + is_stream = True + + return is_stream + + async def create_card_id(): + try: + is_stream = is_stream_output_supported() + if is_stream: + self.ap.logger.debug('飞书支持stream输出,创建卡片......') + + card_id = '' + if self.card_id_dict: + card_id = [k for k,v in self.card_id_dict.items() if (v+datetime.timedelta(days=14))< datetime.datetime.now()][0] + + if self.card_id_dict is None or card_id == '': + # content = { + # "type": "card_json", + # "data": {"schema":"2.0","header":{"title":{"content":"bot","tag":"plain_text"}},"body":{"elements":[{"tag":"markdown","content":""}]}} + # } + card_data = {"schema":"2.0","header":{"title":{"content":"bot","tag":"plain_text"}}, + "body":{"elements":[{"tag":"markdown","content":""}]},"config": {"streaming_mode": True, + "streaming_config": {"print_strategy": "fast"}}} + + request: CreateCardRequest = ( + CreateCardRequest.builder() + .request_body( + CreateCardRequestBody.builder() + .type("card_json") + .data(json.dumps(card_data)) + .build() + ) + ) + # 发起请求 + response: CreateCardResponse = await self.api_client.im.v1.card.create(request) + + + # 处理失败返回 + if not response.success(): + raise Exception( + f"client.cardkit.v1.card.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}") + + self.ap.logger.debug(f'飞书卡片创建成功,卡片ID: {response.data.card_id}') + self.card_id_dict[response.data.card_id] = datetime.datetime.now() + + card_id = response.data.card_id + return card_id + + except Exception as e: + self.ap.logger.error(f'飞书卡片创建失败,错误信息: {e}') + + + + async def on_message(event: lark_oapi.im.v1.P2ImMessageReceiveV1): - if self.config['enable-card-reply'] and event.event.message.message_id not in self.message_id_to_card_id: + if is_stream_output_supported(): self.ap.logger.debug('卡片回复模式开启') # 开启卡片回复模式. 这里可以实现飞书一发消息,马上创建卡片进行回复"思考中..." - reply_message_id = await self.create_message_card(event.event.message.message_id) + card_id = await create_card_id() + reply_message_id = await self.create_message_card(card_id, event.event.message.message_id) self.message_id_to_card_id[event.event.message.message_id] = (reply_message_id, time.time()) if len(self.message_id_to_card_id) > CARD_ID_CACHE_SIZE: @@ -430,7 +491,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): pass - async def create_message_card(self, message_id: str) -> str: + async def create_message_card(self, card_id: str, message_id: str) -> str: """ 创建卡片消息。 使用卡片消息是因为普通消息更新次数有限制,而大模型流式返回结果可能很多而超过限制,而飞书卡片没有这个限制 @@ -440,7 +501,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): # 发消息马上就会回复显示初始化的content信息,即思考中 content = { 'type': 'template', - 'data': {'template_id': self.config['card_template_id'], 'template_variable': {'content': 'Thinking...'}}, + 'data': {'template_id': card_id, 'template_variable': {'content': 'Thinking...'}}, } request: ReplyMessageRequest = ( ReplyMessageRequest.builder() @@ -467,12 +528,40 @@ class LarkAdapter(adapter.MessagePlatformAdapter): message: platform_message.MessageChain, quote_origin: bool = False, ): - if self.config['enable-card-reply']: - await self.reply_card_message(message_source, message, quote_origin) - else: - await self.reply_normal_message(message_source, message, quote_origin) + # 不再需要了,因为message_id已经被包含到message_chain中 + # lark_event = await self.event_converter.yiri2target(message_source) + lark_message = await self.message_converter.yiri2target(message, self.api_client) - async def reply_card_message( + final_content = { + 'zh_Hans': { + 'title': '', + 'content': lark_message, + }, + } + + request: ReplyMessageRequest = ( + ReplyMessageRequest.builder() + .message_id(message_source.message_chain.message_id) + .request_body( + ReplyMessageRequestBody.builder() + .content(json.dumps(final_content)) + .msg_type('post') + .reply_in_thread(False) + .uuid(str(uuid.uuid4())) + .build() + ) + .build() + ) + + response: ReplyMessageResponse = await self.api_client.im.v1.message.areply(request) + + if not response.success(): + raise Exception( + f'client.im.v1.message.reply failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}' + ) + + + async def reply_message_chunk( self, message_source: platform_events.MessageEvent, message: platform_message.MessageChain, @@ -512,43 +601,12 @@ class LarkAdapter(adapter.MessagePlatformAdapter): ) return - async def reply_normal_message( - self, - message_source: platform_events.MessageEvent, - message: platform_message.MessageChain, - quote_origin: bool = False, - ): - # 不再需要了,因为message_id已经被包含到message_chain中 - # lark_event = await self.event_converter.yiri2target(message_source) - lark_message = await self.message_converter.yiri2target(message, self.api_client) - final_content = { - 'zh_Hans': { - 'title': '', - 'content': lark_message, - }, - } - request: ReplyMessageRequest = ( - ReplyMessageRequest.builder() - .message_id(message_source.message_chain.message_id) - .request_body( - ReplyMessageRequestBody.builder() - .content(json.dumps(final_content)) - .msg_type('post') - .reply_in_thread(False) - .uuid(str(uuid.uuid4())) - .build() - ) - .build() - ) - response: ReplyMessageResponse = await self.api_client.im.v1.message.areply(request) - if not response.success(): - raise Exception( - f'client.im.v1.message.reply failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}' - ) + + async def is_muted(self, group_id: int) -> bool: return False From 4908996cacc345f3a9c75e5c5da5b47c98651b26 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Fri, 4 Jul 2025 03:26:44 +0800 Subject: [PATCH 037/107] =?UTF-8?q?=E6=B5=81=E5=BC=8F=E5=9F=BA=E6=9C=AC?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=E5=B7=B2=E9=80=9A=E8=BF=87=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E4=BA=86yield=E5=92=8Creturn=E7=9A=84=E5=86=B2=E7=AA=81?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/pipeline/process/handlers/chat.py | 38 +++-- pkg/pipeline/respback/respback.py | 9 +- pkg/platform/adapter.py | 4 + pkg/platform/sources/lark.py | 135 ++++++++++------ pkg/provider/entities.py | 4 +- pkg/provider/modelmgr/requester.py | 27 +++- pkg/provider/modelmgr/requesters/chatcmpl.py | 159 +++++++++++++++---- pkg/provider/runners/localagent.py | 40 +++-- 8 files changed, 304 insertions(+), 112 deletions(-) diff --git a/pkg/pipeline/process/handlers/chat.py b/pkg/pipeline/process/handlers/chat.py index 084fff6f..1ccf9bb9 100644 --- a/pkg/pipeline/process/handlers/chat.py +++ b/pkg/pipeline/process/handlers/chat.py @@ -1,6 +1,6 @@ from __future__ import annotations -from itertools import accumulate +import uuid import typing import traceback @@ -59,8 +59,11 @@ class ChatMessageHandler(handler.MessageHandler): query.user_message.content = event_ctx.event.alter text_length = 0 - - is_stream = query.adapter.is_stream_output_supported() + try: + is_stream = query.adapter.is_stream + except AttributeError: + is_stream = False + print(is_stream) try: for r in runner_module.preregistered_runners: @@ -68,23 +71,28 @@ class ChatMessageHandler(handler.MessageHandler): runner = r(self.ap, query.pipeline_config) break else: - raise ValueError(f'Request runner not found: {query.pipeline_config["ai"]["runner"]["runner"]}') - + raise ValueError(f'未找到请求运行器: {query.pipeline_config["ai"]["runner"]["runner"]}') if is_stream: - async for results in runner.run(query): - async for result in results: + resp_message_id = uuid.uuid4() + await query.adapter.create_message_card(resp_message_id, query.message_event) + async for result in runner.run(query): + result.resp_message_id = resp_message_id + if query.resp_messages: + query.resp_messages.pop() + if query.resp_message_chain: + query.resp_message_chain.pop() - query.resp_messages.append(result) + query.resp_messages.append(result) + self.ap.logger.info(f'对话({query.query_id})流式响应: {self.cut_str(result.readable_str())}') - self.ap.logger.info(f'对话({query.query_id})流式响应: {self.cut_str(result.readable_str())}') + if result.content is not None: + text_length += len(result.content) - if result.content is not None: - text_length += len(result.content) - - yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) + yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) + # else: + # yield entities.StageProcessResult(result_type=entities.ResultType.INTERRUPT, new_query=query) else: - async for result in runner.run(query): query.resp_messages.append(result) @@ -96,7 +104,7 @@ class ChatMessageHandler(handler.MessageHandler): yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) query.session.using_conversation.messages.append(query.user_message) - + query.session.using_conversation.messages.extend(query.resp_messages) except Exception as e: self.ap.logger.error(f'Request failed({query.query_id}): {type(e).__name__} {str(e)}') diff --git a/pkg/pipeline/respback/respback.py b/pkg/pipeline/respback/respback.py index 4ac4e1e3..52714ce2 100644 --- a/pkg/pipeline/respback/respback.py +++ b/pkg/pipeline/respback/respback.py @@ -3,6 +3,7 @@ from __future__ import annotations import random import asyncio +from typing_inspection.typing_objects import is_final from ...platform.types import events as platform_events from ...platform.types import message as platform_message @@ -39,12 +40,16 @@ class SendResponseBackStage(stage.PipelineStage): quote_origin = query.pipeline_config['output']['misc']['quote-origin'] has_chunks = any(isinstance(msg, llm_entities.MessageChunk) for msg in query.resp_messages) + print(has_chunks) if has_chunks and hasattr(query.adapter,'reply_message_chunk'): + is_final = [msg.is_final for msg in query.resp_messages][0] + print(is_final) await query.adapter.reply_message_chunk( message_source=query.message_event, - message_id=query.query_id, - message_generator=query.resp_message_chain[-1], + message_id=query.message_event.message_chain.message_id, + message=query.resp_message_chain[-1], quote_origin=quote_origin, + is_final=is_final, ) else: await query.adapter.reply_message( diff --git a/pkg/platform/adapter.py b/pkg/platform/adapter.py index 18403b75..3951326c 100644 --- a/pkg/platform/adapter.py +++ b/pkg/platform/adapter.py @@ -25,6 +25,8 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): logger: EventLogger + is_stream: bool + def __init__(self, config: dict, ap: app.Application, logger: EventLogger): """初始化适配器 @@ -67,6 +69,7 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): message_id: int, message: platform_message.MessageChain, quote_origin: bool = False, + is_final: bool = False, ): """回复消息(流式输出) Args: @@ -114,6 +117,7 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): async def is_stream_output_supported(self) -> bool: """是否支持流式输出""" + self.is_stream = False return False async def kill(self) -> bool: diff --git a/pkg/platform/sources/lark.py b/pkg/platform/sources/lark.py index 696076a4..503ef225 100644 --- a/pkg/platform/sources/lark.py +++ b/pkg/platform/sources/lark.py @@ -18,6 +18,7 @@ import aiohttp import lark_oapi.ws.exception import quart from lark_oapi.api.im.v1 import * +from lark_oapi.api.cardkit.v1 import * from .. import adapter from ...core import app @@ -348,6 +349,8 @@ class LarkAdapter(adapter.MessagePlatformAdapter): card_id_dict: dict[str, str] + seq: int + def __init__(self, config: dict, ap: app.Application, logger: EventLogger): self.config = config self.ap = ap @@ -356,6 +359,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): self.listeners = {} self.message_id_to_card_id = {} self.card_id_dict = {} + self.seq = 0 @self.quart_app.route('/lark/callback', methods=['POST']) async def lark_callback(): @@ -401,54 +405,79 @@ class LarkAdapter(adapter.MessagePlatformAdapter): return {'code': 500, 'message': 'error'} - def is_stream_output_supported() -> bool: + async def is_stream_output_supported() -> bool: is_stream = False - if self.config.get("",None): + if self.config.get("enable-card-reply",None): is_stream = True + self.is_stream = is_stream return is_stream - async def create_card_id(): + async def create_card_id(message_id): try: - is_stream = is_stream_output_supported() + is_stream = await is_stream_output_supported() if is_stream: self.ap.logger.debug('飞书支持stream输出,创建卡片......') - card_id = '' - if self.card_id_dict: - card_id = [k for k,v in self.card_id_dict.items() if (v+datetime.timedelta(days=14))< datetime.datetime.now()][0] + # card_id = '' + # # if self.card_id_dict: + # # card_id = [k for k,v in self.card_id_dict.items() if (v+datetime.timedelta(days=14))< datetime.datetime.now()][0] + # + # if self.card_id_dict is None: + # # content = { + # # "type": "card_json", + # # "data": {"schema":"2.0","header":{"title":{"content":"bot","tag":"plain_text"}},"body":{"elements":[{"tag":"markdown","content":""}]}} + # # } + # card_data = {"schema":"2.0","header":{"title":{"content":"bot","tag":"plain_text"}}, + # "body":{"elements":[{"tag":"markdown","content":""}]},"config": {"streaming_mode": True, + # "streaming_config": {"print_strategy": "fast"}}} + # + # request: CreateCardRequest = CreateCardRequest.builder() \ + # .request_body( + # CreateCardRequestBody.builder() + # .type("card_json") + # .data(json.dumps(card_data)) \ + # .build() + # ).build() + # + # # 发起请求 + # response: CreateCardResponse = self.api_client.cardkit.v1.card.create(request) + # + # + # # 处理失败返回 + # if not response.success(): + # raise Exception( + # f"client.cardkit.v1.card.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}") + # + # self.ap.logger.debug(f'飞书卡片创建成功,卡片ID: {response.data.card_id}') + # self.card_id_dict[response.data.card_id] = datetime.datetime.now() + # + # card_id = response.data.card_id + card_data = {"schema": "2.0", "header": {"title": {"content": "bot", "tag": "plain_text"}}, + "body": {"elements": [{"tag": "markdown", "content": "[思考中.....]","element_id":"markdown_1"}]}, + "config": {"streaming_mode": True, + "streaming_config": {"print_strategy": "fast"}}} - if self.card_id_dict is None or card_id == '': - # content = { - # "type": "card_json", - # "data": {"schema":"2.0","header":{"title":{"content":"bot","tag":"plain_text"}},"body":{"elements":[{"tag":"markdown","content":""}]}} - # } - card_data = {"schema":"2.0","header":{"title":{"content":"bot","tag":"plain_text"}}, - "body":{"elements":[{"tag":"markdown","content":""}]},"config": {"streaming_mode": True, - "streaming_config": {"print_strategy": "fast"}}} + request: CreateCardRequest = CreateCardRequest.builder() \ + .request_body( + CreateCardRequestBody.builder() + .type("card_json") + .data(json.dumps(card_data)) \ + .build() + ).build() - request: CreateCardRequest = ( - CreateCardRequest.builder() - .request_body( - CreateCardRequestBody.builder() - .type("card_json") - .data(json.dumps(card_data)) - .build() - ) - ) - # 发起请求 - response: CreateCardResponse = await self.api_client.im.v1.card.create(request) + # 发起请求 + response: CreateCardResponse = self.api_client.cardkit.v1.card.create(request) + # 处理失败返回 + if not response.success(): + raise Exception( + f"client.cardkit.v1.card.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}") - # 处理失败返回 - if not response.success(): - raise Exception( - f"client.cardkit.v1.card.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}") + self.ap.logger.debug(f'飞书卡片创建成功,卡片ID: {response.data.card_id}') + self.card_id_dict[message_id] = response.data.card_id - self.ap.logger.debug(f'飞书卡片创建成功,卡片ID: {response.data.card_id}') - self.card_id_dict[response.data.card_id] = datetime.datetime.now() - - card_id = response.data.card_id + card_id = response.data.card_id return card_id except Exception as e: @@ -458,10 +487,10 @@ class LarkAdapter(adapter.MessagePlatformAdapter): async def on_message(event: lark_oapi.im.v1.P2ImMessageReceiveV1): - if is_stream_output_supported(): + if await is_stream_output_supported(): self.ap.logger.debug('卡片回复模式开启') # 开启卡片回复模式. 这里可以实现飞书一发消息,马上创建卡片进行回复"思考中..." - card_id = await create_card_id() + card_id = await create_card_id(event.event.message.message_id) reply_message_id = await self.create_message_card(card_id, event.event.message.message_id) self.message_id_to_card_id[event.event.message.message_id] = (reply_message_id, time.time()) @@ -500,8 +529,8 @@ class LarkAdapter(adapter.MessagePlatformAdapter): # TODO 目前只支持卡片模板方式,且卡片变量一定是content,未来这块要做成可配置 # 发消息马上就会回复显示初始化的content信息,即思考中 content = { - 'type': 'template', - 'data': {'template_id': card_id, 'template_variable': {'content': 'Thinking...'}}, + 'type': 'card', + 'data': {'card_id': card_id, 'template_variable': {'content': 'Thinking...'}}, } request: ReplyMessageRequest = ( ReplyMessageRequest.builder() @@ -564,35 +593,49 @@ class LarkAdapter(adapter.MessagePlatformAdapter): async def reply_message_chunk( self, message_source: platform_events.MessageEvent, + message_id: str, message: platform_message.MessageChain, quote_origin: bool = False, + is_final: bool = False, ): """ 回复消息变成更新卡片消息 """ lark_message = await self.message_converter.yiri2target(message, self.api_client) + if not is_final: + self.seq += 1 + + + text_message = '' for ele in lark_message[0]: if ele['tag'] == 'text': text_message += ele['text'] elif ele['tag'] == 'md': text_message += ele['text'] + print(text_message) content = { - 'type': 'template', - 'data': {'template_id': self.config['card_template_id'], 'template_variable': {'content': text_message}}, + 'type': 'card_json', + 'data': {'card_id': self.card_id_dict[message_id], 'elements': {'content': text_message}}, } - request: PatchMessageRequest = ( - PatchMessageRequest.builder() - .message_id(self.message_id_to_card_id[message_source.message_chain.message_id][0]) - .request_body(PatchMessageRequestBody.builder().content(json.dumps(content)).build()) + request: ContentCardElementRequest = ContentCardElementRequest.builder() \ + .card_id(self.card_id_dict[message_id]) \ + .element_id("markdown_1") \ + .request_body(ContentCardElementRequestBody.builder() + # .uuid("a0d69e20-1dd1-458b-k525-dfeca4015204") + .content(text_message) + .sequence(self.seq) + .build()) \ .build() - ) + if is_final: + self.seq = 0 # 发起请求 - response: PatchMessageResponse = self.api_client.im.v1.message.patch(request) + response: ContentCardElementResponse = self.api_client.cardkit.v1.card_element.content(request) + # 处理失败返回 if not response.success(): diff --git a/pkg/provider/entities.py b/pkg/provider/entities.py index a149fea3..e8037e68 100644 --- a/pkg/provider/entities.py +++ b/pkg/provider/entities.py @@ -140,12 +140,12 @@ class MessageChunk(pydantic.BaseModel): content: typing.Optional[list[ContentElement]] | typing.Optional[str] = None """内容""" - # tool_calls: typing.Optional[list[ToolCall]] = None + tool_calls: typing.Optional[list[ToolCall]] = None """工具调用""" tool_call_id: typing.Optional[str] = None - tool_calls: typing.Optional[list[ToolCallChunk]] = None + # tool_calls: typing.Optional[list[ToolCallChunk]] = None is_final: bool = False diff --git a/pkg/provider/modelmgr/requester.py b/pkg/provider/modelmgr/requester.py index 337154be..6b760616 100644 --- a/pkg/provider/modelmgr/requester.py +++ b/pkg/provider/modelmgr/requester.py @@ -85,7 +85,7 @@ class ProviderAPIRequester(metaclass=abc.ABCMeta): funcs: typing.List[tools_entities.LLMFunction] = None, stream: bool = False, extra_args: dict[str, typing.Any] = {}, - ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: + ) -> llm_entities.Message: """调用API Args: @@ -95,7 +95,30 @@ class ProviderAPIRequester(metaclass=abc.ABCMeta): extra_args (dict[str, typing.Any], optional): 额外的参数. Defaults to {}. Returns: - llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: 返回消息对象 + llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk]: 返回消息对象 + """ + pass + + @abc.abstractmethod + async def invoke_llm_stream( + self, + query: core_entities.Query, + model: RuntimeLLMModel, + messages: typing.List[llm_entities.Message], + funcs: typing.List[tools_entities.LLMFunction] = None, + stream: bool = False, + extra_args: dict[str, typing.Any] = {}, + ) -> llm_entities.MessageChunk: + """调用API + + Args: + model (RuntimeLLMModel): 使用的模型信息 + messages (typing.List[llm_entities.Message]): 消息对象列表 + funcs (typing.List[tools_entities.LLMFunction], optional): 使用的工具函数列表. Defaults to None. + extra_args (dict[str, typing.Any], optional): 额外的参数. Defaults to {}. + + Returns: + llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk]: 返回消息对象 """ pass diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index ef6ecd71..b3ddea53 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -38,6 +38,15 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): ) -> chat_completion.ChatCompletion: return await self.client.chat.completions.create(**args, extra_body=extra_body) + async def _req_stream( + self, + args: dict, + extra_body: dict = {}, + ) -> chat_completion.ChatCompletion: + + async for chunk in await self.client.chat.completions.create(**args, extra_body=extra_body): + yield chunk + async def _make_msg( self, chat_completion: chat_completion.ChatCompletion, @@ -62,9 +71,19 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): self, chat_completion: chat_completion.ChatCompletion, ) -> llm_entities.MessageChunk: - choice = chat_completion.choices[0] - delta = choice.delta.model_dump() + + # 处理流式chunk和完整响应的差异 + # print(chat_completion.choices[0]) + if hasattr(chat_completion, 'choices'): + # 完整响应模式 + choice = chat_completion.choices[0] + delta = choice.delta.model_dump() if hasattr(choice, 'delta') else choice.message.model_dump() + else: + # 流式chunk模式 + delta = chat_completion.delta.model_dump() if hasattr(chat_completion, 'delta') else {} + # 确保 role 字段存在且不为 None + # print(delta) if 'role' not in delta or delta['role'] is None: delta['role'] = 'assistant' @@ -78,8 +97,8 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): message = llm_entities.MessageChunk(**delta) return message - - async def _closure( + + async def _closure_stream( self, query: core_entities.Query, req_messages: list[dict], @@ -87,7 +106,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): use_funcs: list[tools_entities.LLMFunction] = None, stream: bool = False, extra_args: dict[str, typing.Any] = {}, - ) -> llm_entities.Message: + ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: self.client.api_key = use_model.token_mgr.get_token() args = {} @@ -115,36 +134,76 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): if stream: current_content = '' - async for chunk in await self._req(args, extra_body=extra_args): + args["stream"] = True + async for chunk in self._req_stream(args, extra_body=extra_args): + # print(chunk) # 处理流式消息 - delta_message = await self._make_msg_chunk( - chat_completion=chunk, - ) + delta_message = await self._make_msg_chunk(chunk) if delta_message.content: current_content += delta_message.content + delta_message.content = current_content + print(current_content) delta_message.all_content = current_content - - # 检查是否为最后一个块 - if chunk.choices[0].finish_reason is not None: + + # # 检查是否为最后一个块 + # if chunk.finish_reason is not None: + # delta_message.is_final = True + # + # yield delta_message + # 检查结束标志 + chunk_choices = getattr(chunk, 'choices', None) + if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): delta_message.is_final = True - yield delta_message - return - - else: + yield delta_message + # return - # 非流式请求 - resp = await self._req(args, extra_body=extra_args) - # 处理请求结果 - # 发送请求 - resp = await self._req(args, extra_body=extra_args) + + async def _closure( + self, + query: core_entities.Query, + req_messages: list[dict], + use_model: requester.RuntimeLLMModel, + use_funcs: list[tools_entities.LLMFunction] = None, + stream: bool = False, + extra_args: dict[str, typing.Any] = {}, + ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: + self.client.api_key = use_model.token_mgr.get_token() - # 处理请求结果 - message = await self._make_msg(resp) + args = {} + args['model'] = use_model.model_entity.name - return message - + if use_funcs: + tools = await self.ap.tool_mgr.generate_tools_for_openai(use_funcs) + + if tools: + args['tools'] = tools + + # 设置此次请求中的messages + messages = req_messages.copy() + + # 检查vision + for msg in messages: + if 'content' in msg and isinstance(msg['content'], list): + for me in msg['content']: + if me['type'] == 'image_base64': + me['image_url'] = {'url': me['image_base64']} + me['type'] = 'image_url' + del me['image_base64'] + + args['messages'] = messages + + + + # 发送请求 + + resp = await self._req(args, extra_body=extra_args) + # 处理请求结果 + message = await self._make_msg(resp) + + + return message @@ -171,8 +230,9 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): req_messages.append(msg_dict) try: + if stream: - async for item in self._closure( + async for item in self._closure_stream( query=query, req_messages=req_messages, use_model=model, @@ -180,16 +240,17 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): stream=stream, extra_args=extra_args, ): - yield item - return + return item else: - return await self._closure( + print(req_messages) + msg = await self._closure( query=query, req_messages=req_messages, use_model=model, use_funcs=funcs, extra_args=extra_args, ) + return msg except asyncio.TimeoutError: raise errors.RequesterError('请求超时') except openai.BadRequestError as e: @@ -233,6 +294,46 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): raise errors.RequesterError('请求超时') except openai.BadRequestError as e: raise errors.RequesterError(f'请求参数错误: {e.message}') + + async def invoke_llm_stream( + self, + query: core_entities.Query, + model: requester.RuntimeLLMModel, + messages: typing.List[llm_entities.Message], + funcs: typing.List[tools_entities.LLMFunction] = None, + stream: bool = False, + extra_args: dict[str, typing.Any] = {}, + ) -> llm_entities.MessageChunk: + req_messages = [] # req_messages 仅用于类内,外部同步由 query.messages 进行 + for m in messages: + msg_dict = m.dict(exclude_none=True) + content = msg_dict.get('content') + if isinstance(content, list): + # 检查 content 列表中是否每个部分都是文本 + if all(isinstance(part, dict) and part.get('type') == 'text' for part in content): + # 将所有文本部分合并为一个字符串 + msg_dict['content'] = '\n'.join(part['text'] for part in content) + req_messages.append(msg_dict) + + try: + if stream: + async for item in self._closure_stream( + query=query, + req_messages=req_messages, + use_model=model, + use_funcs=funcs, + stream=stream, + extra_args=extra_args, + ): + yield item + + except asyncio.TimeoutError: + raise errors.RequesterError('请求超时') + except openai.BadRequestError as e: + if 'context_length_exceeded' in e.message: + raise errors.RequesterError(f'上文过长,请重置会话: {e.message}') + else: + raise errors.RequesterError(f'请求参数错误: {e.message}') except openai.AuthenticationError as e: raise errors.RequesterError(f'无效的 api-key: {e.message}') except openai.NotFoundError as e: diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index 78aaf2bb..31c7e119 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -88,23 +88,30 @@ class LocalAgentRunner(runner.RequestRunner): req_messages = query.prompt.messages.copy() + query.messages.copy() + [user_message] is_stream = query.adapter.is_stream_output_supported() + try: + is_stream = query.adapter.is_stream + except AttributeError: + is_stream = False # while True: # pass if not is_stream: # 非流式输出,直接请求 + # print(123) msg = await query.use_llm_model.requester.invoke_llm( query, query.use_llm_model, req_messages, query.use_funcs, + is_stream, extra_args=query.use_llm_model.model_entity.extra_args, ) yield msg final_msg = msg + print(final_msg) else: # 流式输出,需要处理工具调用 tool_calls_map: dict[str, llm_entities.ToolCall] = {} - async for msg in await query.use_llm_model.requester.invoke_llm( + async for msg in query.use_llm_model.requester.invoke_llm_stream( query, query.use_llm_model, req_messages, @@ -114,20 +121,20 @@ class LocalAgentRunner(runner.RequestRunner): ): assert isinstance(msg, llm_entities.MessageChunk) yield msg - if msg.tool_calls: - for tool_call in msg.tool_calls: - if tool_call.id not in tool_calls_map: - tool_calls_map[tool_call.id] = llm_entities.ToolCall( - id=tool_call.id, - type=tool_call.type, - function=llm_entities.FunctionCall( - name=tool_call.function.name if tool_call.function else '', - arguments='' - ), - ) - if tool_call.function and tool_call.function.arguments: - # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 - tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + # if msg.tool_calls: + # for tool_call in msg.tool_calls: + # if tool_call.id not in tool_calls_map: + # tool_calls_map[tool_call.id] = llm_entities.ToolCall( + # id=tool_call.id, + # type=tool_call.type, + # function=llm_entities.FunctionCall( + # name=tool_call.function.name if tool_call.function else '', + # arguments='' + # ), + # ) + # if tool_call.function and tool_call.function.arguments: + # # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 + # tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments final_msg = llm_entities.Message( role=msg.role, content=msg.all_content, @@ -168,7 +175,7 @@ class LocalAgentRunner(runner.RequestRunner): if is_stream: tool_calls_map = {} - async for msg in await query.use_llm_model.requester.invoke_llm( + async for msg in await query.use_llm_model.requester.invoke_llm_stream( query, query.use_llm_model, req_messages, @@ -198,6 +205,7 @@ class LocalAgentRunner(runner.RequestRunner): tool_calls=list(tool_calls_map.values()), ) else: + print("非流式") # 处理完所有调用,再次请求 msg = await query.use_llm_model.requester.invoke_llm( query, From 5ce32d2f040231bdea67408981424abd8ea8288f Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Sat, 12 Jul 2025 18:09:24 +0800 Subject: [PATCH 038/107] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E4=BA=86?= =?UTF-8?q?=E5=9B=A0=E4=B8=BA=E8=BF=AD=E4=BB=A3=E6=95=B0=E6=8D=AE=E5=8F=AA?= =?UTF-8?q?=E6=8E=A8=E5=85=A5resq=5Fmessages=E5=92=8Cresq=5Fmessage=5Fchai?= =?UTF-8?q?n=E5=AF=BC=E8=87=B4=E7=BC=93=E5=AD=98=E5=88=B0=E5=86=85?= =?UTF-8?q?=E5=AD=98=E4=B8=AD=E7=9A=84=E6=95=B0=E6=8D=AE=E5=92=8C=E5=86=99?= =?UTF-8?q?=E5=85=A5log=E4=B8=AD=E7=9A=84=E6=95=B0=E6=8D=AE=E9=87=8F?= =?UTF-8?q?=E5=BA=9E=E5=A4=A7=EF=BC=8C=E4=BB=A5=E5=8F=8A=E5=B8=A6=E6=9C=89?= =?UTF-8?q?=E6=B7=B1=E5=BA=A6=E6=80=9D=E8=80=83=E6=A8=A1=E5=9E=8B=E7=9A=84?= =?UTF-8?q?think=E5=A2=9E=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/provider/modelmgr/requester.py | 1 - pkg/provider/modelmgr/requesters/chatcmpl.py | 89 +++++++++----------- pkg/provider/runners/localagent.py | 1 - 3 files changed, 42 insertions(+), 49 deletions(-) diff --git a/pkg/provider/modelmgr/requester.py b/pkg/provider/modelmgr/requester.py index 6b760616..fa4a9ff8 100644 --- a/pkg/provider/modelmgr/requester.py +++ b/pkg/provider/modelmgr/requester.py @@ -83,7 +83,6 @@ class ProviderAPIRequester(metaclass=abc.ABCMeta): model: RuntimeLLMModel, messages: typing.List[llm_entities.Message], funcs: typing.List[tools_entities.LLMFunction] = None, - stream: bool = False, extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message: """调用API diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index b3ddea53..844aa83f 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -17,12 +17,15 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): """OpenAI ChatCompletion API 请求器""" client: openai.AsyncClient + is_content:bool default_config: dict[str, typing.Any] = { 'base_url': 'https://api.openai.com/v1', 'timeout': 120, } + + async def initialize(self): self.client = openai.AsyncClient( api_key='', @@ -30,6 +33,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): timeout=self.requester_cfg['timeout'], http_client=httpx.AsyncClient(trust_env=True, timeout=self.requester_cfg['timeout']), ) + self.is_content = False async def _req( self, @@ -69,6 +73,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): async def _make_msg_chunk( self, + index:int, chat_completion: chat_completion.ChatCompletion, ) -> llm_entities.MessageChunk: @@ -83,7 +88,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): delta = chat_completion.delta.model_dump() if hasattr(chat_completion, 'delta') else {} # 确保 role 字段存在且不为 None - # print(delta) + # print(delta.values()) if 'role' not in delta or delta['role'] is None: delta['role'] = 'assistant' @@ -91,8 +96,17 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None # deepseek的reasoner模型 - if reasoning_content is not None: - delta['content'] = '\n' + reasoning_content + '\n\n' + delta['content'] + if reasoning_content is not None and index == 0: + delta['content'] += f'\n{reasoning_content}' + elif reasoning_content is None: + if self.is_content: + delta['content'] = delta['content'] + else: + delta['content'] = f'\n\n\n{delta["content"]}' + self.is_content = True + else: + delta['content'] += reasoning_content + message = llm_entities.MessageChunk(**delta) @@ -135,23 +149,17 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): if stream: current_content = '' args["stream"] = True + chunk_idx = 0 + self.is_content = False async for chunk in self._req_stream(args, extra_body=extra_args): - # print(chunk) - # 处理流式消息 - delta_message = await self._make_msg_chunk(chunk) + delta_message = await self._make_msg_chunk(chunk_idx,chunk) + # print(delta_message) if delta_message.content: current_content += delta_message.content delta_message.content = current_content - print(current_content) - delta_message.all_content = current_content - - # # 检查是否为最后一个块 - # if chunk.finish_reason is not None: - # delta_message.is_final = True - # - # yield delta_message - # 检查结束标志 + # delta_message.all_content = current_content + chunk_idx += 1 chunk_choices = getattr(chunk, 'choices', None) if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): delta_message.is_final = True @@ -215,9 +223,8 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): model: requester.RuntimeLLMModel, messages: typing.List[llm_entities.Message], funcs: typing.List[tools_entities.LLMFunction] = None, - stream: bool = False, extra_args: dict[str, typing.Any] = {}, - ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: + ) -> llm_entities.Message: req_messages = [] # req_messages 仅用于类内,外部同步由 query.messages 进行 for m in messages: msg_dict = m.dict(exclude_none=True) @@ -231,26 +238,14 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): try: - if stream: - async for item in self._closure_stream( - query=query, - req_messages=req_messages, - use_model=model, - use_funcs=funcs, - stream=stream, - extra_args=extra_args, - ): - return item - else: - print(req_messages) - msg = await self._closure( - query=query, - req_messages=req_messages, - use_model=model, - use_funcs=funcs, - extra_args=extra_args, - ) - return msg + msg = await self._closure( + query=query, + req_messages=req_messages, + use_model=model, + use_funcs=funcs, + extra_args=extra_args, + ) + return msg except asyncio.TimeoutError: raise errors.RequesterError('请求超时') except openai.BadRequestError as e: @@ -316,16 +311,16 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): req_messages.append(msg_dict) try: - if stream: - async for item in self._closure_stream( - query=query, - req_messages=req_messages, - use_model=model, - use_funcs=funcs, - stream=stream, - extra_args=extra_args, - ): - yield item + async for item in self._closure_stream( + query=query, + req_messages=req_messages, + use_model=model, + use_funcs=funcs, + stream=stream, + extra_args=extra_args, + ): + yield item + print(item) except asyncio.TimeoutError: raise errors.RequesterError('请求超时') diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index 31c7e119..79de89a4 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -102,7 +102,6 @@ class LocalAgentRunner(runner.RequestRunner): query.use_llm_model, req_messages, query.use_funcs, - is_stream, extra_args=query.use_llm_model.model_entity.extra_args, ) yield msg From f9a5507029095651bc9c396810475ec0bd53d107 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Sun, 13 Jul 2025 22:41:39 +0800 Subject: [PATCH 039/107] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E4=BA=86?= =?UTF-8?q?=E5=9B=A0=E4=B8=BA=E8=BF=AD=E4=BB=A3=E6=95=B0=E6=8D=AE=E5=8F=AA?= =?UTF-8?q?=E6=8E=A8=E5=85=A5resq=5Fmessages=E5=92=8Cresq=5Fmessage=5Fchai?= =?UTF-8?q?n=E5=AF=BC=E8=87=B4=E7=BC=93=E5=AD=98=E5=88=B0=E5=86=85?= =?UTF-8?q?=E5=AD=98=E4=B8=AD=E7=9A=84=E6=95=B0=E6=8D=AE=E5=92=8C=E5=86=99?= =?UTF-8?q?=E5=85=A5log=E4=B8=AD=E7=9A=84=E6=95=B0=E6=8D=AE=E9=87=8F?= =?UTF-8?q?=E5=BA=9E=E5=A4=A7=EF=BC=8C=E4=BB=A5=E5=8F=8A=E6=9C=89=E6=80=9D?= =?UTF-8?q?=E8=80=83=E7=9A=84think=E5=A4=84=E7=90=86=20feat:=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E5=B8=A6=E6=9C=89=E6=B7=B1=E5=BA=A6=E6=80=9D=E8=80=83?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E7=9A=84think=E7=9A=84=E5=8E=BBthink?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=20feat:dify=E4=B8=AD=E8=81=8A=E5=A4=A9?= =?UTF-8?q?=E6=9C=BA=E5=99=A8=E4=BA=BA=EF=BC=8Cchatflow=E5=AF=B9=E6=B5=81?= =?UTF-8?q?=E5=BC=8F=E7=9A=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/platform/sources/lark.py | 8 +-- pkg/provider/modelmgr/requesters/chatcmpl.py | 66 +++++++++++++----- pkg/provider/runners/difysvapi.py | 73 ++++++++++++++------ templates/metadata/pipeline/trigger.yaml | 7 ++ 4 files changed, 113 insertions(+), 41 deletions(-) diff --git a/pkg/platform/sources/lark.py b/pkg/platform/sources/lark.py index 503ef225..1816db8f 100644 --- a/pkg/platform/sources/lark.py +++ b/pkg/platform/sources/lark.py @@ -359,7 +359,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): self.listeners = {} self.message_id_to_card_id = {} self.card_id_dict = {} - self.seq = 0 + self.seq = 1 @self.quart_app.route('/lark/callback', methods=['POST']) async def lark_callback(): @@ -456,7 +456,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): card_data = {"schema": "2.0", "header": {"title": {"content": "bot", "tag": "plain_text"}}, "body": {"elements": [{"tag": "markdown", "content": "[思考中.....]","element_id":"markdown_1"}]}, "config": {"streaming_mode": True, - "streaming_config": {"print_strategy": "fast"}}} + "streaming_config": {"print_strategy": "delay"}}} # delay / fast request: CreateCardRequest = CreateCardRequest.builder() \ .request_body( @@ -620,7 +620,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): 'type': 'card_json', 'data': {'card_id': self.card_id_dict[message_id], 'elements': {'content': text_message}}, } - + print(self.seq) request: ContentCardElementRequest = ContentCardElementRequest.builder() \ .card_id(self.card_id_dict[message_id]) \ .element_id("markdown_1") \ @@ -632,7 +632,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): .build() if is_final: - self.seq = 0 + self.seq = 1 # 发起请求 response: ContentCardElementResponse = self.api_client.cardkit.v1.card_element.content(request) diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index 844aa83f..69c09b8e 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -8,7 +8,7 @@ import openai.types.chat.chat_completion as chat_completion import httpx from .. import errors, requester -from ....core import entities as core_entities +from ....core import entities as core_entities, app from ... import entities as llm_entities from ...tools import entities as tools_entities @@ -25,7 +25,6 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): } - async def initialize(self): self.client = openai.AsyncClient( api_key='', @@ -53,6 +52,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): async def _make_msg( self, + pipeline_config: dict[str, typing.Any], chat_completion: chat_completion.ChatCompletion, ) -> llm_entities.Message: chatcmpl_message = chat_completion.choices[0].message.model_dump() @@ -64,8 +64,12 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): reasoning_content = chatcmpl_message['reasoning_content'] if 'reasoning_content' in chatcmpl_message else None # deepseek的reasoner模型 - if reasoning_content is not None: - chatcmpl_message['content'] = '\n' + reasoning_content + '\n\n' + chatcmpl_message['content'] + print(pipeline_config['trigger'].get('misc', '').get('remove_think')) + if pipeline_config['trigger'].get('misc', '').get('remove_think'): + pass + else: + if reasoning_content is not None : + chatcmpl_message['content'] = '\n' + reasoning_content + '\n\n' + chatcmpl_message['content'] message = llm_entities.Message(**chatcmpl_message) @@ -73,7 +77,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): async def _make_msg_chunk( self, - index:int, + pipeline_config: dict[str, typing.Any], chat_completion: chat_completion.ChatCompletion, ) -> llm_entities.MessageChunk: @@ -96,16 +100,22 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None # deepseek的reasoner模型 - if reasoning_content is not None and index == 0: - delta['content'] += f'\n{reasoning_content}' - elif reasoning_content is None: - if self.is_content: - delta['content'] = delta['content'] + if pipeline_config['trigger'].get('misc', '').get('remove_think'): + if reasoning_content is not None : + pass else: - delta['content'] = f'\n\n\n{delta["content"]}' - self.is_content = True + delta['content'] = delta['content'] else: - delta['content'] += reasoning_content + if reasoning_content is not None: + delta['content'] += f'\n{reasoning_content}' + elif reasoning_content is None: + if self.is_content: + delta['content'] = delta['content'] + else: + delta['content'] = f'\n\n\n{delta["content"]}' + self.is_content = True + else: + delta['content'] += reasoning_content message = llm_entities.MessageChunk(**delta) @@ -151,20 +161,41 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): args["stream"] = True chunk_idx = 0 self.is_content = False + tool_calls_map: dict[str, llm_entities.ToolCall] = {} + pipeline_config = query.pipeline_config async for chunk in self._req_stream(args, extra_body=extra_args): # 处理流式消息 - delta_message = await self._make_msg_chunk(chunk_idx,chunk) - # print(delta_message) + delta_message = await self._make_msg_chunk(pipeline_config,chunk) if delta_message.content: current_content += delta_message.content delta_message.content = current_content + print(current_content) # delta_message.all_content = current_content + if delta_message.tool_calls: + for tool_call in delta_message.tool_calls: + if tool_call.id not in tool_calls_map: + tool_calls_map[tool_call.id] = llm_entities.ToolCall( + id=tool_call.id, + type=tool_call.type, + function=llm_entities.FunctionCall( + name=tool_call.function.name if tool_call.function else '', + arguments='' + ), + ) + if tool_call.function and tool_call.function.arguments: + # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 + tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + + chunk_idx += 1 chunk_choices = getattr(chunk, 'choices', None) if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): delta_message.is_final = True + delta_message.content = current_content - yield delta_message + if chunk_idx % 64 == 0 or delta_message.is_final: + + yield delta_message # return @@ -208,7 +239,8 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): resp = await self._req(args, extra_body=extra_args) # 处理请求结果 - message = await self._make_msg(resp) + pipeline_config = query.pipeline_config + message = await self._make_msg(pipeline_config,resp) return message diff --git a/pkg/provider/runners/difysvapi.py b/pkg/provider/runners/difysvapi.py index 98b50f86..1dfde547 100644 --- a/pkg/provider/runners/difysvapi.py +++ b/pkg/provider/runners/difysvapi.py @@ -95,6 +95,11 @@ class DifyServiceAPIRunner(runner.RequestRunner): cov_id = query.session.using_conversation.uuid or '' query.variables['conversation_id'] = cov_id + try: + is_stream = query.adapter.is_stream + except AttributeError: + is_stream = False + plain_text, image_ids = await self._preprocess_user_message(query) files = [ @@ -144,40 +149,54 @@ class DifyServiceAPIRunner(runner.RequestRunner): if mode == 'workflow': if chunk['event'] == 'node_finished': - if chunk['data']['node_type'] == 'answer': - yield llm_entities.Message( - role='assistant', - content=self._try_convert_thinking(chunk['data']['outputs']['answer']), - ) + if not is_stream: + + if chunk['data']['node_type'] == 'answer': + yield llm_entities.Message( + role='assistant', + content=self._try_convert_thinking(chunk['data']['outputs']['answer']), + ) + else: + if chunk['data']['node_type'] == 'answer': + yield llm_entities.MessageChunk( + role='assistant', + content=self._try_convert_thinking(chunk['data']['outputs']['answer']), + is_final=True, + ) elif chunk['event'] == 'message': stream_output_pending_chunk += chunk['answer'] - if self.pipeline_config['ai']['dify-service-api'].get('enable-streaming', False): + if is_stream: # 消息数超过量就输出,从而达到streaming的效果 batch_pending_index += 1 if batch_pending_index >= batch_pending_max_size: - yield llm_entities.Message( + yield llm_entities.MessageChunk( role='assistant', content=self._try_convert_thinking(stream_output_pending_chunk), ) batch_pending_index = 0 elif mode == 'basic': - if chunk['event'] == 'message': - stream_output_pending_chunk += chunk['answer'] - if self.pipeline_config['ai']['dify-service-api'].get('enable-streaming', False): - # 消息数超过量就输出,从而达到streaming的效果 - batch_pending_index += 1 - if batch_pending_index >= batch_pending_max_size: + if chunk['event'] == 'message' or chunk['event'] == 'message_end': + if chunk['event'] == 'message_end': + is_final = True + if is_stream and batch_pending_index % batch_pending_max_size == 0: + # 消息数超过量就输出,从而达到streaming的效果 + batch_pending_index += 1 + # if batch_pending_index >= batch_pending_max_size: + yield llm_entities.MessageChunk( + role='assistant', + content=self._try_convert_thinking(stream_output_pending_chunk), + is_final=is_final, + ) + # batch_pending_index = 0 + elif not is_stream: yield llm_entities.Message( role='assistant', content=self._try_convert_thinking(stream_output_pending_chunk), ) - batch_pending_index = 0 - elif chunk['event'] == 'message_end': - yield llm_entities.Message( - role='assistant', - content=self._try_convert_thinking(stream_output_pending_chunk), - ) - stream_output_pending_chunk = '' + stream_output_pending_chunk = '' + else: + stream_output_pending_chunk += chunk['answer'] + is_final = False if chunk is None: raise errors.DifyAPIError('Dify API 没有返回任何响应,请检查网络连接和API配置') @@ -191,6 +210,13 @@ class DifyServiceAPIRunner(runner.RequestRunner): cov_id = query.session.using_conversation.uuid or '' query.variables['conversation_id'] = cov_id + try: + is_stream = query.adapter.is_stream + except AttributeError: + is_stream = False + + batch_pending_index = 0 + plain_text, image_ids = await self._preprocess_user_message(query) files = [ @@ -285,6 +311,13 @@ class DifyServiceAPIRunner(runner.RequestRunner): query.variables['conversation_id'] = query.session.using_conversation.uuid + try: + is_stream = query.adapter.is_stream + except AttributeError: + is_stream = False + + batch_pending_index = 0 + plain_text, image_ids = await self._preprocess_user_message(query) files = [ diff --git a/templates/metadata/pipeline/trigger.yaml b/templates/metadata/pipeline/trigger.yaml index 949b2698..165e488e 100644 --- a/templates/metadata/pipeline/trigger.yaml +++ b/templates/metadata/pipeline/trigger.yaml @@ -132,3 +132,10 @@ stages: type: boolean required: true default: true + - name: remove_think + label: + en_US: remove think + zh_Hans: 删除深度思考消息 + type: boolean + required: true + default: true From f5a0cb91758c0b58cb5daa7891dccdd5ad31dd74 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Mon, 14 Jul 2025 00:40:02 +0800 Subject: [PATCH 040/107] feat:add dify _agent_chat_message streaming --- pkg/provider/runners/difysvapi.py | 77 ++++++++++++++++++++++--------- 1 file changed, 56 insertions(+), 21 deletions(-) diff --git a/pkg/provider/runners/difysvapi.py b/pkg/provider/runners/difysvapi.py index 1dfde547..566dc0f8 100644 --- a/pkg/provider/runners/difysvapi.py +++ b/pkg/provider/runners/difysvapi.py @@ -251,11 +251,26 @@ class DifyServiceAPIRunner(runner.RequestRunner): if chunk['event'] in ignored_events: continue + batch_pending_index += 1 + + if chunk['event'] == 'agent_message' or chunk['event'] == 'message_end': + if chunk['event'] == 'message_end': + print(chunk['event']) + # break + is_final = True + else: + is_final = False + pending_agent_message += chunk['answer'] + if is_stream: + if batch_pending_index % 64 == 0 or is_final: + yield llm_entities.MessageChunk( + role='assistant', + content=self._try_convert_thinking(pending_agent_message), + is_final=is_final, + ) - if chunk['event'] == 'agent_message': - pending_agent_message += chunk['answer'] else: - if pending_agent_message.strip() != '': + if pending_agent_message.strip() != '' and not is_stream: pending_agent_message = pending_agent_message.replace('Action:', '') yield llm_entities.Message( role='assistant', @@ -268,19 +283,34 @@ class DifyServiceAPIRunner(runner.RequestRunner): continue if chunk['tool']: - msg = llm_entities.Message( - role='assistant', - tool_calls=[ - llm_entities.ToolCall( - id=chunk['id'], - type='function', - function=llm_entities.FunctionCall( - name=chunk['tool'], - arguments=json.dumps({}), - ), - ) - ], - ) + if is_stream: + msg = llm_entities.MessageChunk( + role='assistant', + tool_calls=[ + llm_entities.ToolCall( + id=chunk['id'], + type='function', + function=llm_entities.FunctionCall( + name=chunk['tool'], + arguments=json.dumps({}), + ), + ) + ], + ) + else: + msg = llm_entities.Message( + role='assistant', + tool_calls=[ + llm_entities.ToolCall( + id=chunk['id'], + type='function', + function=llm_entities.FunctionCall( + name=chunk['tool'], + arguments=json.dumps({}), + ), + ) + ], + ) yield msg if chunk['event'] == 'message_file': if chunk['type'] == 'image' and chunk['belongs_to'] == 'assistant': @@ -290,11 +320,16 @@ class DifyServiceAPIRunner(runner.RequestRunner): base_url = base_url[:-3] image_url = base_url + chunk['url'] - - yield llm_entities.Message( - role='assistant', - content=[llm_entities.ContentElement.from_image_url(image_url)], - ) + if is_stream: + yield llm_entities.MessageChunk( + role='assistant', + content=[llm_entities.ContentElement.from_image_url(image_url)], + ) + else: + yield llm_entities.Message( + role='assistant', + content=[llm_entities.ContentElement.from_image_url(image_url)], + ) if chunk['event'] == 'error': raise errors.DifyAPIError('dify 服务错误: ' + chunk['message']) From f84a79bf7442470fba6dde0d2c55118a81925878 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Mon, 14 Jul 2025 01:42:42 +0800 Subject: [PATCH 041/107] perf:del dify stream in ai.yaml config.and enbale stream in lark.yaml. fix:localagent remove_think bug --- pkg/platform/sources/lark.py | 8 ++++---- pkg/platform/sources/lark.yaml | 17 +++++------------ pkg/provider/modelmgr/requesters/chatcmpl.py | 8 +++----- pkg/provider/runners/difysvapi.py | 15 +++++++-------- templates/metadata/pipeline/ai.yaml | 15 +-------------- 5 files changed, 20 insertions(+), 43 deletions(-) diff --git a/pkg/platform/sources/lark.py b/pkg/platform/sources/lark.py index 1816db8f..2fd0a081 100644 --- a/pkg/platform/sources/lark.py +++ b/pkg/platform/sources/lark.py @@ -407,7 +407,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): async def is_stream_output_supported() -> bool: is_stream = False - if self.config.get("enable-card-reply",None): + if self.config.get("enable-stream-reply",None): is_stream = True self.is_stream = is_stream @@ -603,8 +603,8 @@ class LarkAdapter(adapter.MessagePlatformAdapter): """ lark_message = await self.message_converter.yiri2target(message, self.api_client) - if not is_final: - self.seq += 1 + + self.seq += 1 @@ -620,7 +620,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): 'type': 'card_json', 'data': {'card_id': self.card_id_dict[message_id], 'elements': {'content': text_message}}, } - print(self.seq) + request: ContentCardElementRequest = ContentCardElementRequest.builder() \ .card_id(self.card_id_dict[message_id]) \ .element_id("markdown_1") \ diff --git a/pkg/platform/sources/lark.yaml b/pkg/platform/sources/lark.yaml index bafaba81..94414b2e 100644 --- a/pkg/platform/sources/lark.yaml +++ b/pkg/platform/sources/lark.yaml @@ -65,23 +65,16 @@ spec: type: string required: true default: "" - - name: enable-card-reply + - name: enable-stream-reply label: - en_US: Enable Card Reply Mode - zh_Hans: 启用飞书卡片回复模式 + en_US: Enable Stream Reply Mode + zh_Hans: 启用飞书流式回复模式 description: - en_US: If enabled, the bot will use the card of lark reply mode - zh_Hans: 如果启用,将使用飞书卡片方式来回复内容 + en_US: If enabled, the bot will use the stream of lark reply mode + zh_Hans: 如果启用,将使用飞书流式方式来回复内容 type: boolean required: true default: false - - name: card_template_id - label: - en_US: card template id - zh_Hans: 卡片模板ID - type: string - required: true - default: "填写你的卡片template_id" execution: python: path: ./lark.py diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index 69c09b8e..fbaf96fd 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -64,7 +64,6 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): reasoning_content = chatcmpl_message['reasoning_content'] if 'reasoning_content' in chatcmpl_message else None # deepseek的reasoner模型 - print(pipeline_config['trigger'].get('misc', '').get('remove_think')) if pipeline_config['trigger'].get('misc', '').get('remove_think'): pass else: @@ -79,6 +78,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): self, pipeline_config: dict[str, typing.Any], chat_completion: chat_completion.ChatCompletion, + idx: int, ) -> llm_entities.MessageChunk: # 处理流式chunk和完整响应的差异 @@ -106,7 +106,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): else: delta['content'] = delta['content'] else: - if reasoning_content is not None: + if reasoning_content is not None and idx == 0: delta['content'] += f'\n{reasoning_content}' elif reasoning_content is None: if self.is_content: @@ -165,11 +165,10 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): pipeline_config = query.pipeline_config async for chunk in self._req_stream(args, extra_body=extra_args): # 处理流式消息 - delta_message = await self._make_msg_chunk(pipeline_config,chunk) + delta_message = await self._make_msg_chunk(pipeline_config,chunk,chunk_idx) if delta_message.content: current_content += delta_message.content delta_message.content = current_content - print(current_content) # delta_message.all_content = current_content if delta_message.tool_calls: for tool_call in delta_message.tool_calls: @@ -352,7 +351,6 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): extra_args=extra_args, ): yield item - print(item) except asyncio.TimeoutError: raise errors.RequesterError('请求超时') diff --git a/pkg/provider/runners/difysvapi.py b/pkg/provider/runners/difysvapi.py index 566dc0f8..24318716 100644 --- a/pkg/provider/runners/difysvapi.py +++ b/pkg/provider/runners/difysvapi.py @@ -115,9 +115,7 @@ class DifyServiceAPIRunner(runner.RequestRunner): stream_output_pending_chunk = '' - batch_pending_max_size = self.pipeline_config['ai']['dify-service-api'].get( - 'output-batch-size', 0 - ) # 积累一定量的消息更新消息一次 + batch_pending_max_size = 64 # 积累一定量的消息更新消息一次 batch_pending_index = 0 @@ -255,14 +253,13 @@ class DifyServiceAPIRunner(runner.RequestRunner): if chunk['event'] == 'agent_message' or chunk['event'] == 'message_end': if chunk['event'] == 'message_end': - print(chunk['event']) # break is_final = True else: is_final = False pending_agent_message += chunk['answer'] if is_stream: - if batch_pending_index % 64 == 0 or is_final: + if batch_pending_index % 32 == 0 or is_final: yield llm_entities.MessageChunk( role='assistant', content=self._try_convert_thinking(pending_agent_message), @@ -276,7 +273,7 @@ class DifyServiceAPIRunner(runner.RequestRunner): role='assistant', content=self._try_convert_thinking(pending_agent_message), ) - pending_agent_message = '' + if chunk['event'] == 'agent_thought': if chunk['tool'] != '' and chunk['observation'] != '': # 工具调用结果,跳过 @@ -312,7 +309,7 @@ class DifyServiceAPIRunner(runner.RequestRunner): ], ) yield msg - if chunk['event'] == 'message_file': + elif chunk['event'] == 'message_file': if chunk['type'] == 'image' and chunk['belongs_to'] == 'assistant': base_url = self.dify_client.base_url @@ -330,8 +327,10 @@ class DifyServiceAPIRunner(runner.RequestRunner): role='assistant', content=[llm_entities.ContentElement.from_image_url(image_url)], ) - if chunk['event'] == 'error': + elif chunk['event'] == 'error': raise errors.DifyAPIError('dify 服务错误: ' + chunk['message']) + else: + pending_agent_message = '' if chunk is None: raise errors.DifyAPIError('Dify API 没有返回任何响应,请检查网络连接和API配置') diff --git a/templates/metadata/pipeline/ai.yaml b/templates/metadata/pipeline/ai.yaml index 4d0cb6c3..63c56a8a 100644 --- a/templates/metadata/pipeline/ai.yaml +++ b/templates/metadata/pipeline/ai.yaml @@ -138,20 +138,7 @@ stages: label: en_US: Remove zh_Hans: 移除 - - name: enable-streaming - label: - en_US: enable streaming mode - zh_Hans: 开启流式输出 - type: boolean - required: true - default: false - - name: output-batch-size - label: - en_US: output batch size - zh_Hans: 输出批次大小(积累多少条消息后一起输出) - type: integer - required: true - default: 10 + - name: dashscope-app-api label: From a7d638cc9a2e6e4b05e9e96bb5cb7c7b31f5bcfd Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Mon, 14 Jul 2025 23:53:55 +0800 Subject: [PATCH 042/107] feat:add deepseek and modelscope llm stream,and giteeai think in content remove_think --- pkg/provider/modelmgr/requesters/chatcmpl.py | 11 +- .../modelmgr/requesters/deepseekchatcmpl.py | 6 +- .../modelmgr/requesters/giteeaichatcmpl.py | 167 +++++++++++++++++- .../modelmgr/requesters/modelscopechatcmpl.py | 48 +++++ 4 files changed, 226 insertions(+), 6 deletions(-) diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index fbaf96fd..8e350bf6 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -52,10 +52,11 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): async def _make_msg( self, - pipeline_config: dict[str, typing.Any], chat_completion: chat_completion.ChatCompletion, + pipeline_config: dict[str, typing.Any] = {'trigger': {'misc': {'remove_think': False}}}, ) -> llm_entities.Message: chatcmpl_message = chat_completion.choices[0].message.model_dump() + # print(chatcmpl_message.keys(),chatcmpl_message.values()) # 确保 role 字段存在且不为 None if 'role' not in chatcmpl_message or chatcmpl_message['role'] is None: @@ -65,6 +66,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): # deepseek的reasoner模型 if pipeline_config['trigger'].get('misc', '').get('remove_think'): + pass else: if reasoning_content is not None : @@ -92,13 +94,16 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): delta = chat_completion.delta.model_dump() if hasattr(chat_completion, 'delta') else {} # 确保 role 字段存在且不为 None - # print(delta.values()) + # print(delta.keys(),delta.values()) if 'role' not in delta or delta['role'] is None: delta['role'] = 'assistant' reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None + delta['content'] = '' if delta['content'] is None else delta['content'] + # print(reasoning_content) + # deepseek的reasoner模型 if pipeline_config['trigger'].get('misc', '').get('remove_think'): if reasoning_content is not None : @@ -239,7 +244,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): resp = await self._req(args, extra_body=extra_args) # 处理请求结果 pipeline_config = query.pipeline_config - message = await self._make_msg(pipeline_config,resp) + message = await self._make_msg(resp,pipeline_config) return message diff --git a/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py b/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py index 6d664b01..f57f624f 100644 --- a/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py +++ b/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py @@ -49,10 +49,12 @@ class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions): # 发送请求 resp = await self._req(args, extra_body=extra_args) + # print(resp) + if resp is None: raise errors.RequesterError('接口返回为空,请确定模型提供商服务是否正常') - + pipeline_config = query.pipeline_config # 处理请求结果 - message = await self._make_msg(resp) + message = await self._make_msg(resp,pipeline_config) return message diff --git a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py index 3795ef99..ce1b075f 100644 --- a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py +++ b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py @@ -8,6 +8,9 @@ from .. import requester from ....core import entities as core_entities from ... import entities as llm_entities from ...tools import entities as tools_entities +import re +import openai.types.chat.chat_completion as chat_completion + class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): @@ -17,6 +20,7 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): 'base_url': 'https://ai.gitee.com/v1', 'timeout': 120, } + is_think:bool = False async def _closure( self, @@ -46,6 +50,167 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): resp = await self._req(args, extra_body=extra_args) - message = await self._make_msg(resp) + pipeline_config = query.pipeline_config + + message = await self._make_msg(resp,pipeline_config) return message + + + async def _make_msg( + self, + chat_completion: chat_completion.ChatCompletion, + pipeline_config: dict[str, typing.Any] = {'trigger': {'misc': {'remove_think': False}}}, + ) -> llm_entities.Message: + chatcmpl_message = chat_completion.choices[0].message.model_dump() + # print(chatcmpl_message.keys(), chatcmpl_message.values()) + + # 确保 role 字段存在且不为 None + if 'role' not in chatcmpl_message or chatcmpl_message['role'] is None: + chatcmpl_message['role'] = 'assistant' + + reasoning_content = chatcmpl_message['reasoning_content'] if 'reasoning_content' in chatcmpl_message else None + + # deepseek的reasoner模型 + if pipeline_config['trigger'].get('misc', '').get('remove_think'): + chatcmpl_message['content'] = re.sub(r'.*?', '', chatcmpl_message['content'], flags=re.DOTALL) + else: + if reasoning_content is not None: + chatcmpl_message['content'] = '\n' + reasoning_content + '\n\n' + chatcmpl_message['content'] + + message = llm_entities.Message(**chatcmpl_message) + + return message + + + async def _make_msg_chunk( + self, + pipeline_config: dict[str, typing.Any], + chat_completion: chat_completion.ChatCompletion, + idx: int, + ) -> llm_entities.MessageChunk: + + # 处理流式chunk和完整响应的差异 + # print(chat_completion.choices[0]) + if hasattr(chat_completion, 'choices'): + # 完整响应模式 + choice = chat_completion.choices[0] + delta = choice.delta.model_dump() if hasattr(choice, 'delta') else choice.message.model_dump() + else: + # 流式chunk模式 + delta = chat_completion.delta.model_dump() if hasattr(chat_completion, 'delta') else {} + + # 确保 role 字段存在且不为 None + if 'role' not in delta or delta['role'] is None: + delta['role'] = 'assistant' + + + reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None + + delta['content'] = '' if delta['content'] is None else delta['content'] + # print(reasoning_content) + + # deepseek的reasoner模型 + if pipeline_config['trigger'].get('misc', '').get('remove_think'): + if delta['content'] == '': + self.is_think = True + delta['content'] = '' + if delta['content'] == rf'': + self.is_think = False + delta['content'] = '' + if not self.is_think: + delta['content'] = delta['content'] + else: + delta['content'] = '' + else: + if reasoning_content is not None and idx == 0: + delta['content'] += f'\n{reasoning_content}' + elif reasoning_content is None: + if self.is_content: + delta['content'] = delta['content'] + else: + delta['content'] = f'\n\n\n{delta["content"]}' + self.is_content = True + else: + delta['content'] += reasoning_content + + + message = llm_entities.MessageChunk(**delta) + + return message + + async def _closure_stream( + self, + query: core_entities.Query, + req_messages: list[dict], + use_model: requester.RuntimeLLMModel, + use_funcs: list[tools_entities.LLMFunction] = None, + stream: bool = False, + extra_args: dict[str, typing.Any] = {}, + ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: + self.client.api_key = use_model.token_mgr.get_token() + + args = {} + args['model'] = use_model.model_entity.name + + if use_funcs: + tools = await self.ap.tool_mgr.generate_tools_for_openai(use_funcs) + + if tools: + args['tools'] = tools + + # 设置此次请求中的messages + messages = req_messages.copy() + + # 检查vision + for msg in messages: + if 'content' in msg and isinstance(msg['content'], list): + for me in msg['content']: + if me['type'] == 'image_base64': + me['image_url'] = {'url': me['image_base64']} + me['type'] = 'image_url' + del me['image_base64'] + + args['messages'] = messages + + if stream: + current_content = '' + args["stream"] = True + chunk_idx = 0 + self.is_content = False + tool_calls_map: dict[str, llm_entities.ToolCall] = {} + pipeline_config = query.pipeline_config + async for chunk in self._req_stream(args, extra_body=extra_args): + # 处理流式消息 + delta_message = await self._make_msg_chunk(pipeline_config,chunk,chunk_idx) + if delta_message.content: + current_content += delta_message.content + delta_message.content = current_content + # delta_message.all_content = current_content + if delta_message.tool_calls: + for tool_call in delta_message.tool_calls: + if tool_call.id not in tool_calls_map: + tool_calls_map[tool_call.id] = llm_entities.ToolCall( + id=tool_call.id, + type=tool_call.type, + function=llm_entities.FunctionCall( + name=tool_call.function.name if tool_call.function else '', + arguments='' + ), + ) + if tool_call.function and tool_call.function.arguments: + # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 + tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + + + chunk_idx += 1 + chunk_choices = getattr(chunk, 'choices', None) + if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): + delta_message.is_final = True + delta_message.content = current_content + + if chunk_idx % 64 == 0 or delta_message.is_final: + + yield delta_message + + diff --git a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py index 4708f671..20a315a8 100644 --- a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py +++ b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py @@ -202,3 +202,51 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): raise errors.RequesterError(f'请求过于频繁或余额不足: {e.message}') except openai.APIError as e: raise errors.RequesterError(f'请求错误: {e.message}') + + + async def invoke_llm_stream( + self, + query: core_entities.Query, + model: requester.RuntimeLLMModel, + messages: typing.List[llm_entities.Message], + funcs: typing.List[tools_entities.LLMFunction] = None, + stream: bool = False, + extra_args: dict[str, typing.Any] = {}, + ) -> llm_entities.MessageChunk: + req_messages = [] # req_messages 仅用于类内,外部同步由 query.messages 进行 + for m in messages: + msg_dict = m.dict(exclude_none=True) + content = msg_dict.get('content') + if isinstance(content, list): + # 检查 content 列表中是否每个部分都是文本 + if all(isinstance(part, dict) and part.get('type') == 'text' for part in content): + # 将所有文本部分合并为一个字符串 + msg_dict['content'] = '\n'.join(part['text'] for part in content) + req_messages.append(msg_dict) + + try: + async for item in self._closure_stream( + query=query, + req_messages=req_messages, + use_model=model, + use_funcs=funcs, + stream=stream, + extra_args=extra_args, + ): + yield item + + except asyncio.TimeoutError: + raise errors.RequesterError('请求超时') + except openai.BadRequestError as e: + if 'context_length_exceeded' in e.message: + raise errors.RequesterError(f'上文过长,请重置会话: {e.message}') + else: + raise errors.RequesterError(f'请求参数错误: {e.message}') + except openai.AuthenticationError as e: + raise errors.RequesterError(f'无效的 api-key: {e.message}') + except openai.NotFoundError as e: + raise errors.RequesterError(f'请求路径错误: {e.message}') + except openai.RateLimitError as e: + raise errors.RequesterError(f'请求过于频繁或余额不足: {e.message}') + except openai.APIError as e: + raise errors.RequesterError(f'请求错误: {e.message}') \ No newline at end of file From 0042629bf0d0de9f4901195ce61ca0da2dad54f4 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Tue, 15 Jul 2025 00:50:42 +0800 Subject: [PATCH 043/107] feat:add ppio and openrouter llm stream,and ppio think in content remove_think. fix: giteeai stream no remove_think content add char"" --- .../modelmgr/requesters/giteeaichatcmpl.py | 10 +- .../modelmgr/requesters/modelscopechatcmpl.py | 137 ++++++++++++++++ .../modelmgr/requesters/ppiochatcmpl.py | 152 ++++++++++++++++++ 3 files changed, 290 insertions(+), 9 deletions(-) diff --git a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py index ce1b075f..2a618c9f 100644 --- a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py +++ b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py @@ -123,15 +123,7 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): else: delta['content'] = '' else: - if reasoning_content is not None and idx == 0: - delta['content'] += f'\n{reasoning_content}' - elif reasoning_content is None: - if self.is_content: - delta['content'] = delta['content'] - else: - delta['content'] = f'\n\n\n{delta["content"]}' - self.is_content = True - else: + if reasoning_content is not None: delta['content'] += reasoning_content diff --git a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py index 20a315a8..c1888a5e 100644 --- a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py +++ b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py @@ -164,6 +164,143 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): return message + async def _req_stream( + self, + args: dict, + extra_body: dict = {}, + ) -> chat_completion.ChatCompletion: + + async for chunk in await self.client.chat.completions.create(**args, extra_body=extra_body): + yield chunk + + async def _make_msg_chunk( + self, + pipeline_config: dict[str, typing.Any], + chat_completion: chat_completion.ChatCompletion, + idx: int, + ) -> llm_entities.MessageChunk: + + # 处理流式chunk和完整响应的差异 + # print(chat_completion.choices[0]) + if hasattr(chat_completion, 'choices'): + # 完整响应模式 + choice = chat_completion.choices[0] + delta = choice.delta.model_dump() if hasattr(choice, 'delta') else choice.message.model_dump() + else: + # 流式chunk模式 + delta = chat_completion.delta.model_dump() if hasattr(chat_completion, 'delta') else {} + + # 确保 role 字段存在且不为 None + # print(delta.keys(),delta.values()) + if 'role' not in delta or delta['role'] is None: + delta['role'] = 'assistant' + + + reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None + + delta['content'] = '' if delta['content'] is None else delta['content'] + # print(reasoning_content) + + # deepseek的reasoner模型 + if pipeline_config['trigger'].get('misc', '').get('remove_think'): + if reasoning_content is not None : + pass + else: + delta['content'] = delta['content'] + else: + if reasoning_content is not None and idx == 0: + delta['content'] += f'\n{reasoning_content}' + elif reasoning_content is None: + if self.is_content: + delta['content'] = delta['content'] + else: + delta['content'] = f'\n\n\n{delta["content"]}' + self.is_content = True + else: + delta['content'] += reasoning_content + + + message = llm_entities.MessageChunk(**delta) + + return message + + async def _closure_stream( + self, + query: core_entities.Query, + req_messages: list[dict], + use_model: requester.RuntimeLLMModel, + use_funcs: list[tools_entities.LLMFunction] = None, + stream: bool = False, + extra_args: dict[str, typing.Any] = {}, + ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: + self.client.api_key = use_model.token_mgr.get_token() + + args = {} + args['model'] = use_model.model_entity.name + + if use_funcs: + tools = await self.ap.tool_mgr.generate_tools_for_openai(use_funcs) + + if tools: + args['tools'] = tools + + # 设置此次请求中的messages + messages = req_messages.copy() + + # 检查vision + for msg in messages: + if 'content' in msg and isinstance(msg['content'], list): + for me in msg['content']: + if me['type'] == 'image_base64': + me['image_url'] = {'url': me['image_base64']} + me['type'] = 'image_url' + del me['image_base64'] + + args['messages'] = messages + + if stream: + current_content = '' + args["stream"] = True + chunk_idx = 0 + self.is_content = False + tool_calls_map: dict[str, llm_entities.ToolCall] = {} + pipeline_config = query.pipeline_config + async for chunk in self._req_stream(args, extra_body=extra_args): + # 处理流式消息 + delta_message = await self._make_msg_chunk(pipeline_config,chunk,chunk_idx) + if delta_message.content: + current_content += delta_message.content + delta_message.content = current_content + # delta_message.all_content = current_content + if delta_message.tool_calls: + for tool_call in delta_message.tool_calls: + if tool_call.id not in tool_calls_map: + tool_calls_map[tool_call.id] = llm_entities.ToolCall( + id=tool_call.id, + type=tool_call.type, + function=llm_entities.FunctionCall( + name=tool_call.function.name if tool_call.function else '', + arguments='' + ), + ) + if tool_call.function and tool_call.function.arguments: + # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 + tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + + + chunk_idx += 1 + chunk_choices = getattr(chunk, 'choices', None) + if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): + delta_message.is_final = True + delta_message.content = current_content + + if chunk_idx % 64 == 0 or delta_message.is_final: + + yield delta_message + # return + + + async def invoke_llm( self, query: core_entities.Query, diff --git a/pkg/provider/modelmgr/requesters/ppiochatcmpl.py b/pkg/provider/modelmgr/requesters/ppiochatcmpl.py index 7e78ddb8..85b321a7 100644 --- a/pkg/provider/modelmgr/requesters/ppiochatcmpl.py +++ b/pkg/provider/modelmgr/requesters/ppiochatcmpl.py @@ -4,6 +4,12 @@ import openai import typing from . import chatcmpl +import openai.types.chat.chat_completion as chat_completion +from .. import errors, requester +from ....core import entities as core_entities, app +from ... import entities as llm_entities +from ...tools import entities as tools_entities +import re class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): @@ -15,3 +21,149 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): 'base_url': 'https://api.ppinfra.com/v3/openai', 'timeout': 120, } + + is_think: bool = False + + async def _make_msg( + self, + chat_completion: chat_completion.ChatCompletion, + pipeline_config: dict[str, typing.Any] = {'trigger': {'misc': {'remove_think': False}}}, + ) -> llm_entities.Message: + chatcmpl_message = chat_completion.choices[0].message.model_dump() + # print(chatcmpl_message.keys(), chatcmpl_message.values()) + + # 确保 role 字段存在且不为 None + if 'role' not in chatcmpl_message or chatcmpl_message['role'] is None: + chatcmpl_message['role'] = 'assistant' + + reasoning_content = chatcmpl_message['reasoning_content'] if 'reasoning_content' in chatcmpl_message else None + + # deepseek的reasoner模型 + if pipeline_config['trigger'].get('misc', '').get('remove_think'): + chatcmpl_message['content'] = re.sub(r'.*?', '', chatcmpl_message['content'], flags=re.DOTALL) + else: + if reasoning_content is not None: + chatcmpl_message['content'] = '\n' + reasoning_content + '\n\n' + chatcmpl_message['content'] + + message = llm_entities.Message(**chatcmpl_message) + + return message + + + async def _make_msg_chunk( + self, + pipeline_config: dict[str, typing.Any], + chat_completion: chat_completion.ChatCompletion, + idx: int, + ) -> llm_entities.MessageChunk: + # 处理流式chunk和完整响应的差异 + # print(chat_completion.choices[0]) + if hasattr(chat_completion, 'choices'): + # 完整响应模式 + choice = chat_completion.choices[0] + delta = choice.delta.model_dump() if hasattr(choice, 'delta') else choice.message.model_dump() + else: + # 流式chunk模式 + delta = chat_completion.delta.model_dump() if hasattr(chat_completion, 'delta') else {} + + # 确保 role 字段存在且不为 None + if 'role' not in delta or delta['role'] is None: + delta['role'] = 'assistant' + + reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None + + delta['content'] = '' if delta['content'] is None else delta['content'] + # print(reasoning_content) + + # deepseek的reasoner模型 + if pipeline_config['trigger'].get('misc', '').get('remove_think'): + if '' in delta['content']: + self.is_think = True + delta['content'] = '' + if rf'' in delta['content']: + self.is_think = False + delta['content'] = '' + if not self.is_think: + delta['content'] = delta['content'] + else: + delta['content'] = '' + else: + if reasoning_content is not None: + delta['content'] += reasoning_content + + message = llm_entities.MessageChunk(**delta) + + return message + + + async def _closure_stream( + self, + query: core_entities.Query, + req_messages: list[dict], + use_model: requester.RuntimeLLMModel, + use_funcs: list[tools_entities.LLMFunction] = None, + stream: bool = False, + extra_args: dict[str, typing.Any] = {}, + ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: + self.client.api_key = use_model.token_mgr.get_token() + + args = {} + args['model'] = use_model.model_entity.name + + if use_funcs: + tools = await self.ap.tool_mgr.generate_tools_for_openai(use_funcs) + + if tools: + args['tools'] = tools + + # 设置此次请求中的messages + messages = req_messages.copy() + + # 检查vision + for msg in messages: + if 'content' in msg and isinstance(msg['content'], list): + for me in msg['content']: + if me['type'] == 'image_base64': + me['image_url'] = {'url': me['image_base64']} + me['type'] = 'image_url' + del me['image_base64'] + + args['messages'] = messages + + if stream: + current_content = '' + args["stream"] = True + chunk_idx = 0 + self.is_content = False + tool_calls_map: dict[str, llm_entities.ToolCall] = {} + pipeline_config = query.pipeline_config + async for chunk in self._req_stream(args, extra_body=extra_args): + # 处理流式消息 + delta_message = await self._make_msg_chunk(pipeline_config, chunk, chunk_idx) + if delta_message.content: + current_content += delta_message.content + delta_message.content = current_content + # delta_message.all_content = current_content + if delta_message.tool_calls: + for tool_call in delta_message.tool_calls: + if tool_call.id not in tool_calls_map: + tool_calls_map[tool_call.id] = llm_entities.ToolCall( + id=tool_call.id, + type=tool_call.type, + function=llm_entities.FunctionCall( + name=tool_call.function.name if tool_call.function else '', + arguments='' + ), + ) + if tool_call.function and tool_call.function.arguments: + # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 + tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + + chunk_idx += 1 + chunk_choices = getattr(chunk, 'choices', None) + if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): + delta_message.is_final = True + delta_message.content = current_content + + if chunk_idx % 64 == 0 or delta_message.is_final: + yield delta_message From cffe493db00ca1ee055129ca3a5953059376805b Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Thu, 17 Jul 2025 14:29:30 +0800 Subject: [PATCH 044/107] feat:add telegram stream --- pkg/platform/sources/telegram.py | 76 +++++++++++++++++++++++++++++- pkg/platform/sources/telegram.yaml | 10 ++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/pkg/platform/sources/telegram.py b/pkg/platform/sources/telegram.py index c2fcc22e..efc7890f 100644 --- a/pkg/platform/sources/telegram.py +++ b/pkg/platform/sources/telegram.py @@ -1,5 +1,7 @@ from __future__ import annotations +import time + import telegram import telegram.ext from telegram import Update @@ -143,6 +145,8 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): config: dict ap: app.Application + msg_stream_id: dict + listeners: typing.Dict[ typing.Type[platform_events.Event], typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], @@ -152,6 +156,7 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): self.config = config self.ap = ap self.logger = logger + self.msg_stream_id = {} async def telegram_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): if update.message.from_user.is_bot: @@ -160,8 +165,9 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): try: lb_event = await self.event_converter.target2yiri(update, self.bot, self.bot_account_id) await self.listeners[type(lb_event)](lb_event, self) + await self.is_stream_output_supported() except Exception: - await self.logger.error(f'Error in telegram callback: {traceback.format_exc()}') + await self.logger.error(f"Error in telegram callback: {traceback.format_exc()}") self.application = ApplicationBuilder().token(self.config['token']).build() self.bot = self.application.bot @@ -200,6 +206,74 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): await self.bot.send_message(**args) + + async def reply_message_chunk( + self, + message_source: platform_events.MessageEvent, + message_id: int, + message: platform_message.MessageChain, + quote_origin: bool = False, + is_final: bool = False, + ): + + assert isinstance(message_source.source_platform_object, Update) + components = await TelegramMessageConverter.yiri2target(message, self.bot) + args = {} + message_id = message_source.source_platform_object.message.id + if quote_origin: + args['reply_to_message_id'] = message_source.source_platform_object.message.id + + component = components[0] + if message_id not in self.msg_stream_id: + # time.sleep(0.6) + if component['type'] == 'text': + if self.config['markdown_card'] is True: + content = telegramify_markdown.markdownify( + content=component['text'], + ) + else: + content = component['text'] + args = { + 'chat_id': message_source.source_platform_object.effective_chat.id, + 'text': content, + } + if self.config['markdown_card'] is True: + args['parse_mode'] = 'MarkdownV2' + + + send_msg = await self.bot.send_message(**args) + send_msg_id = send_msg.message_id + self.msg_stream_id[message_id] = send_msg_id + else: + if component['type'] == 'text': + if self.config['markdown_card'] is True: + content = telegramify_markdown.markdownify( + content=component['text'], + ) + else: + content = component['text'] + args = { + 'message_id': self.msg_stream_id[message_id], + 'chat_id': message_source.source_platform_object.effective_chat.id, + 'text': content, + } + if self.config['markdown_card'] is True: + args['parse_mode'] = 'MarkdownV2' + + await self.bot.edit_message_text(**args) + if is_final: + self.msg_stream_id.pop(message_id) + + + async def is_stream_output_supported(self) -> bool: + is_stream = False + if self.config.get("enable-stream-reply", None): + is_stream = True + self.is_stream = is_stream + + return is_stream + + async def is_muted(self, group_id: int) -> bool: return False diff --git a/pkg/platform/sources/telegram.yaml b/pkg/platform/sources/telegram.yaml index 43b9284b..d29c359e 100644 --- a/pkg/platform/sources/telegram.yaml +++ b/pkg/platform/sources/telegram.yaml @@ -25,6 +25,16 @@ spec: type: boolean required: false default: true + - name: enable-stream-reply + label: + en_US: Enable Stream Reply Mode + zh_Hans: 启用电报流式回复模式 + description: + en_US: If enabled, the bot will use the stream of telegram reply mode + zh_Hans: 如果启用,将使用电报流式方式来回复内容 + type: boolean + required: true + default: false execution: python: path: ./telegram.py From 43a259a1ae65f9eae3d5ea9e42ad254ab0a7f995 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Sat, 19 Jul 2025 01:05:44 +0800 Subject: [PATCH 045/107] feat:add dingtalk stream --- pkg/platform/sources/dingtalk.py | 46 ++++++++++++++++++++++++++++++ pkg/platform/sources/dingtalk.yaml | 17 +++++++++++ 2 files changed, 63 insertions(+) diff --git a/pkg/platform/sources/dingtalk.py b/pkg/platform/sources/dingtalk.py index a40b0f9b..a669a599 100644 --- a/pkg/platform/sources/dingtalk.py +++ b/pkg/platform/sources/dingtalk.py @@ -99,11 +99,13 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): message_converter: DingTalkMessageConverter = DingTalkMessageConverter() event_converter: DingTalkEventConverter = DingTalkEventConverter() config: dict + card_instance_id_dict: dict def __init__(self, config: dict, ap: app.Application, logger: EventLogger): self.config = config self.ap = ap self.logger = logger + self.card_instance_id_dict = {} required_keys = [ 'client_id', 'client_secret', @@ -139,6 +141,34 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): content, at = await DingTalkMessageConverter.yiri2target(message) await self.bot.send_message(content, incoming_message, at) + async def reply_message_chunk( + self, + message_source: platform_events.MessageEvent, + message_id: int, + message: platform_message.MessageChain, + quote_origin: bool = False, + is_final: bool = False, + ): + event = await DingTalkEventConverter.yiri2target( + message_source, + ) + incoming_message = event.incoming_message + + msg_id = incoming_message.message_id + + content, at = await DingTalkMessageConverter.yiri2target(message) + # is_stream = self.config['enable-stream-reply'] + # print(content) + card_template_id = self.config['card_template_id'] + if msg_id not in self.card_instance_id_dict: + card_instance,card_instance_id = await self.bot.create_and_card(card_template_id,incoming_message,at) + self.card_instance_id_dict[msg_id] = (card_instance,card_instance_id) + else: + card_instance,card_instance_id = self.card_instance_id_dict[msg_id] + # print(card_instance_id) + await self.bot.send_card_message(card_instance,card_instance_id,content,is_final) + + async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): content = await DingTalkMessageConverter.yiri2target(message) if target_type == 'person': @@ -146,6 +176,21 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): if target_type == 'group': await self.bot.send_proactive_message_to_group(target_id, content) + async def is_stream_output_supported(self) -> bool: + is_stream = False + if self.config.get("enable-stream-reply", None): + is_stream = True + self.is_stream = is_stream + + return is_stream + + async def create_message_card(self,message_id: str, incoming_message): + card_template_id = self.config['card_template_id'] + + card_instance, card_instance_id = await self.bot.create_and_card(card_template_id, incoming_message) + self.card_instance_id_dict[message_id] = (card_instance, card_instance_id) + + def register_listener( self, event_type: typing.Type[platform_events.Event], @@ -153,6 +198,7 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): ): async def on_message(event: DingTalkEvent): try: + await self.is_stream_output_supported() return await callback( await self.event_converter.target2yiri(event, self.config['robot_name']), self, diff --git a/pkg/platform/sources/dingtalk.yaml b/pkg/platform/sources/dingtalk.yaml index fac2d6ff..70855c2b 100644 --- a/pkg/platform/sources/dingtalk.yaml +++ b/pkg/platform/sources/dingtalk.yaml @@ -46,6 +46,23 @@ spec: type: boolean required: false default: true + - name: enable-stream-reply + label: + en_US: Enable Stream Reply Mode + zh_Hans: 启用钉钉卡片流式回复模式 + description: + en_US: If enabled, the bot will use the stream of lark reply mode + zh_Hans: 如果启用,将使用钉钉卡片流式方式来回复内容 + type: boolean + required: true + default: false + - name: card_template_id + label: + en_US: card template id + zh_Hans: 卡片模板ID + type: string + required: true + default: "填写你的卡片template_id" execution: python: path: ./dingtalk.py From 4905b5a7386bb6eb4f929308d32f1519e3b0ea99 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Sun, 20 Jul 2025 23:53:20 +0800 Subject: [PATCH 046/107] feat:add dingtalk stream fix:adapter is_stream_output_supported bug fix:stream message reply chunk in message_id --- libs/dingtalk_api/api.py | 43 ++++++++ pkg/pipeline/process/handlers/chat.py | 19 ++-- pkg/pipeline/respback/respback.py | 2 +- pkg/platform/adapter.py | 4 + pkg/platform/sources/dingtalk.py | 22 ++-- pkg/platform/sources/lark.py | 143 +++++++++----------------- pkg/provider/entities.py | 2 + pkg/provider/runners/difysvapi.py | 6 +- pkg/provider/runners/localagent.py | 4 +- 9 files changed, 124 insertions(+), 121 deletions(-) diff --git a/libs/dingtalk_api/api.py b/libs/dingtalk_api/api.py index d323df1e..d1c7065f 100644 --- a/libs/dingtalk_api/api.py +++ b/libs/dingtalk_api/api.py @@ -3,6 +3,7 @@ import json import time from typing import Callable import dingtalk_stream # type: ignore +from dingtalk_stream import AckMessage, ChatbotHandler, CallbackHandler, CallbackMessage, ChatbotMessage, AICardReplier from .EchoHandler import EchoTextHandler from .dingtalkevent import DingTalkEvent import httpx @@ -253,6 +254,48 @@ class DingTalkClient: await self.logger.error(f'failed to send proactive massage to group: {traceback.format_exc()}') raise Exception(f'failed to send proactive massage to group: {traceback.format_exc()}') + async def create_and_card(self, temp_card_id: str, incoming_message: dingtalk_stream.ChatbotMessage,quote_origin:bool=False): + content_key = "content" + card_data = {content_key: ""} + + card_instance = dingtalk_stream.AICardReplier( + self.client, incoming_message + ) + # print(card_instance) + # 先投放卡片: https://open.dingtalk.com/document/orgapp/create-and-deliver-cards + card_instance_id = await card_instance.async_create_and_deliver_card( + temp_card_id, card_data, + ) + return card_instance,card_instance_id + + async def send_card_message(self, + card_instance, + card_instance_id: str,content: str,is_final: bool): + content_key = "content" + try: + await card_instance.async_streaming( + card_instance_id, + content_key=content_key, + content_value=content, + append=False, + finished=is_final, + failed=False, + ) + except Exception as e: + self.logger.exception(e) + await card_instance.async_streaming( + card_instance_id, + content_key=content_key, + content_value="", + append=False, + finished=is_final, + failed=True, + ) + + + + + async def start(self): """启动 WebSocket 连接,监听消息""" await self.client.start() diff --git a/pkg/pipeline/process/handlers/chat.py b/pkg/pipeline/process/handlers/chat.py index 1ccf9bb9..0fe7f868 100644 --- a/pkg/pipeline/process/handlers/chat.py +++ b/pkg/pipeline/process/handlers/chat.py @@ -23,11 +23,11 @@ class ChatMessageHandler(handler.MessageHandler): self, query: core_entities.Query, ) -> typing.AsyncGenerator[entities.StageProcessResult, None]: - """Process""" - # Call API - # generator + """处理""" + # 调API + # 生成器 - # Trigger plugin event + # 触发插件事件 event_class = ( events.PersonNormalMessageReceived if query.launcher_type == core_entities.LauncherTypes.PERSON @@ -47,7 +47,6 @@ class ChatMessageHandler(handler.MessageHandler): if event_ctx.is_prevented_default(): if event_ctx.event.reply is not None: mc = platform_message.MessageChain(event_ctx.event.reply) - query.resp_messages.append(mc) yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) @@ -55,15 +54,14 @@ class ChatMessageHandler(handler.MessageHandler): yield entities.StageProcessResult(result_type=entities.ResultType.INTERRUPT, new_query=query) else: if event_ctx.event.alter is not None: - # if isinstance(event_ctx.event, str): # Currently not considering multi-modal alter + # if isinstance(event_ctx.event, str): # 现在暂时不考虑多模态alter query.user_message.content = event_ctx.event.alter text_length = 0 try: - is_stream = query.adapter.is_stream + is_stream = await query.adapter.is_stream_output_supported() except AttributeError: is_stream = False - print(is_stream) try: for r in runner_module.preregistered_runners: @@ -107,7 +105,8 @@ class ChatMessageHandler(handler.MessageHandler): query.session.using_conversation.messages.extend(query.resp_messages) except Exception as e: - self.ap.logger.error(f'Request failed({query.query_id}): {type(e).__name__} {str(e)}') + self.ap.logger.error(f'对话({query.query_id})请求失败: {type(e).__name__} {str(e)}') + traceback.print_exc() hide_exception_info = query.pipeline_config['output']['misc']['hide-exception'] @@ -120,4 +119,4 @@ class ChatMessageHandler(handler.MessageHandler): ) finally: # TODO statistics - pass + pass \ No newline at end of file diff --git a/pkg/pipeline/respback/respback.py b/pkg/pipeline/respback/respback.py index 52714ce2..9a410b3f 100644 --- a/pkg/pipeline/respback/respback.py +++ b/pkg/pipeline/respback/respback.py @@ -46,7 +46,7 @@ class SendResponseBackStage(stage.PipelineStage): print(is_final) await query.adapter.reply_message_chunk( message_source=query.message_event, - message_id=query.message_event.message_chain.message_id, + message_id=query.resp_messages[-1].resp_message_id, message=query.resp_message_chain[-1], quote_origin=quote_origin, is_final=is_final, diff --git a/pkg/platform/adapter.py b/pkg/platform/adapter.py index 3951326c..d4b48ef6 100644 --- a/pkg/platform/adapter.py +++ b/pkg/platform/adapter.py @@ -80,6 +80,10 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): """ raise NotImplementedError + async def create_message_card(self,message_id,event): + '''创建卡片消息''' + return False + async def is_muted(self, group_id: int) -> bool: """获取账号是否在指定群被禁言""" raise NotImplementedError diff --git a/pkg/platform/sources/dingtalk.py b/pkg/platform/sources/dingtalk.py index a669a599..4a312063 100644 --- a/pkg/platform/sources/dingtalk.py +++ b/pkg/platform/sources/dingtalk.py @@ -154,19 +154,15 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): ) incoming_message = event.incoming_message - msg_id = incoming_message.message_id + # msg_id = incoming_message.message_id content, at = await DingTalkMessageConverter.yiri2target(message) - # is_stream = self.config['enable-stream-reply'] - # print(content) - card_template_id = self.config['card_template_id'] - if msg_id not in self.card_instance_id_dict: - card_instance,card_instance_id = await self.bot.create_and_card(card_template_id,incoming_message,at) - self.card_instance_id_dict[msg_id] = (card_instance,card_instance_id) - else: - card_instance,card_instance_id = self.card_instance_id_dict[msg_id] + + card_instance,card_instance_id = self.card_instance_id_dict[message_id] # print(card_instance_id) await self.bot.send_card_message(card_instance,card_instance_id,content,is_final) + if is_final: + self.card_instance_id_dict.pop(message_id) async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): @@ -180,15 +176,15 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): is_stream = False if self.config.get("enable-stream-reply", None): is_stream = True - self.is_stream = is_stream - return is_stream - async def create_message_card(self,message_id: str, incoming_message): + async def create_message_card(self,message_id,event): card_template_id = self.config['card_template_id'] - + incoming_message = event.incoming_message + # message_id = incoming_message.message_id card_instance, card_instance_id = await self.bot.create_and_card(card_template_id, incoming_message) self.card_instance_id_dict[message_id] = (card_instance, card_instance_id) + return True def register_listener( diff --git a/pkg/platform/sources/lark.py b/pkg/platform/sources/lark.py index 2fd0a081..fb3d0c48 100644 --- a/pkg/platform/sources/lark.py +++ b/pkg/platform/sources/lark.py @@ -360,6 +360,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): self.message_id_to_card_id = {} self.card_id_dict = {} self.seq = 1 + self.card_id_time = {} @self.quart_app.route('/lark/callback', methods=['POST']) async def lark_callback(): @@ -405,101 +406,13 @@ class LarkAdapter(adapter.MessagePlatformAdapter): return {'code': 500, 'message': 'error'} - async def is_stream_output_supported() -> bool: - is_stream = False - if self.config.get("enable-stream-reply",None): - is_stream = True - self.is_stream = is_stream - return is_stream - - async def create_card_id(message_id): - try: - is_stream = await is_stream_output_supported() - if is_stream: - self.ap.logger.debug('飞书支持stream输出,创建卡片......') - # card_id = '' - # # if self.card_id_dict: - # # card_id = [k for k,v in self.card_id_dict.items() if (v+datetime.timedelta(days=14))< datetime.datetime.now()][0] - # - # if self.card_id_dict is None: - # # content = { - # # "type": "card_json", - # # "data": {"schema":"2.0","header":{"title":{"content":"bot","tag":"plain_text"}},"body":{"elements":[{"tag":"markdown","content":""}]}} - # # } - # card_data = {"schema":"2.0","header":{"title":{"content":"bot","tag":"plain_text"}}, - # "body":{"elements":[{"tag":"markdown","content":""}]},"config": {"streaming_mode": True, - # "streaming_config": {"print_strategy": "fast"}}} - # - # request: CreateCardRequest = CreateCardRequest.builder() \ - # .request_body( - # CreateCardRequestBody.builder() - # .type("card_json") - # .data(json.dumps(card_data)) \ - # .build() - # ).build() - # - # # 发起请求 - # response: CreateCardResponse = self.api_client.cardkit.v1.card.create(request) - # - # - # # 处理失败返回 - # if not response.success(): - # raise Exception( - # f"client.cardkit.v1.card.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}") - # - # self.ap.logger.debug(f'飞书卡片创建成功,卡片ID: {response.data.card_id}') - # self.card_id_dict[response.data.card_id] = datetime.datetime.now() - # - # card_id = response.data.card_id - card_data = {"schema": "2.0", "header": {"title": {"content": "bot", "tag": "plain_text"}}, - "body": {"elements": [{"tag": "markdown", "content": "[思考中.....]","element_id":"markdown_1"}]}, - "config": {"streaming_mode": True, - "streaming_config": {"print_strategy": "delay"}}} # delay / fast - request: CreateCardRequest = CreateCardRequest.builder() \ - .request_body( - CreateCardRequestBody.builder() - .type("card_json") - .data(json.dumps(card_data)) \ - .build() - ).build() - - # 发起请求 - response: CreateCardResponse = self.api_client.cardkit.v1.card.create(request) - - # 处理失败返回 - if not response.success(): - raise Exception( - f"client.cardkit.v1.card.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}") - - self.ap.logger.debug(f'飞书卡片创建成功,卡片ID: {response.data.card_id}') - self.card_id_dict[message_id] = response.data.card_id - - card_id = response.data.card_id - return card_id - - except Exception as e: - self.ap.logger.error(f'飞书卡片创建失败,错误信息: {e}') - async def on_message(event: lark_oapi.im.v1.P2ImMessageReceiveV1): - if await is_stream_output_supported(): - self.ap.logger.debug('卡片回复模式开启') - # 开启卡片回复模式. 这里可以实现飞书一发消息,马上创建卡片进行回复"思考中..." - card_id = await create_card_id(event.event.message.message_id) - reply_message_id = await self.create_message_card(card_id, event.event.message.message_id) - self.message_id_to_card_id[event.event.message.message_id] = (reply_message_id, time.time()) - - if len(self.message_id_to_card_id) > CARD_ID_CACHE_SIZE: - self.message_id_to_card_id = { - k: v - for k, v in self.message_id_to_card_id.items() - if v[1] > time.time() - CARD_ID_CACHE_MAX_LIFETIME - } lb_event = await self.event_converter.target2yiri(event, self.api_client) @@ -520,21 +433,64 @@ class LarkAdapter(adapter.MessagePlatformAdapter): async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): pass - async def create_message_card(self, card_id: str, message_id: str) -> str: + async def is_stream_output_supported(self) -> bool: + is_stream = False + if self.config.get("enable-stream-reply", None): + is_stream = True + return is_stream + + async def create_card_id(self,message_id): + try: + is_stream = await self.is_stream_output_supported() + if is_stream: + self.ap.logger.debug('飞书支持stream输出,创建卡片......') + + card_data = {"schema": "2.0", "header": {"title": {"content": "bot", "tag": "plain_text"}}, + "body": {"elements": [ + {"tag": "markdown", "content": "[思考中.....]", "element_id": "markdown_1"}]}, + "config": {"streaming_mode": True, + "streaming_config": {"print_strategy": "delay"}}} # delay / fast + + request: CreateCardRequest = CreateCardRequest.builder() \ + .request_body( + CreateCardRequestBody.builder() + .type("card_json") + .data(json.dumps(card_data)) \ + .build() + ).build() + + # 发起请求 + response: CreateCardResponse = self.api_client.cardkit.v1.card.create(request) + + # 处理失败返回 + if not response.success(): + raise Exception( + f"client.cardkit.v1.card.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}") + + self.ap.logger.debug(f'飞书卡片创建成功,卡片ID: {response.data.card_id}') + self.card_id_dict[message_id] = response.data.card_id + + card_id = response.data.card_id + return card_id + + except Exception as e: + self.ap.logger.error(f'飞书卡片创建失败,错误信息: {e}') + + async def create_message_card(self,message_id,event) -> str: """ 创建卡片消息。 使用卡片消息是因为普通消息更新次数有限制,而大模型流式返回结果可能很多而超过限制,而飞书卡片没有这个限制 """ + # message_id = event.message_chain.message_id - # TODO 目前只支持卡片模板方式,且卡片变量一定是content,未来这块要做成可配置 - # 发消息马上就会回复显示初始化的content信息,即思考中 + card_id = await self.create_card_id(message_id) content = { 'type': 'card', 'data': {'card_id': card_id, 'template_variable': {'content': 'Thinking...'}}, } request: ReplyMessageRequest = ( ReplyMessageRequest.builder() - .message_id(message_id) + .message_id(event.message_chain.message_id) .request_body( ReplyMessageRequestBody.builder().content(json.dumps(content)).msg_type('interactive').build() ) @@ -549,7 +505,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): raise Exception( f'client.im.v1.message.reply failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}' ) - return response.data.message_id + return True async def reply_message( self, @@ -633,6 +589,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): if is_final: self.seq = 1 + self.card_id_dict.pop(message_id) # 发起请求 response: ContentCardElementResponse = self.api_client.cardkit.v1.card_element.content(request) diff --git a/pkg/provider/entities.py b/pkg/provider/entities.py index e8037e68..df2b5487 100644 --- a/pkg/provider/entities.py +++ b/pkg/provider/entities.py @@ -127,6 +127,8 @@ class Message(pydantic.BaseModel): class MessageChunk(pydantic.BaseModel): """消息""" + resp_message_id: typing.Optional[str] = None + """消息id""" role: str # user, system, assistant, tool, command, plugin """消息的角色""" diff --git a/pkg/provider/runners/difysvapi.py b/pkg/provider/runners/difysvapi.py index 24318716..f0c36ca1 100644 --- a/pkg/provider/runners/difysvapi.py +++ b/pkg/provider/runners/difysvapi.py @@ -96,7 +96,7 @@ class DifyServiceAPIRunner(runner.RequestRunner): query.variables['conversation_id'] = cov_id try: - is_stream = query.adapter.is_stream + is_stream = await query.adapter.is_stream_output_supported() except AttributeError: is_stream = False @@ -209,7 +209,7 @@ class DifyServiceAPIRunner(runner.RequestRunner): query.variables['conversation_id'] = cov_id try: - is_stream = query.adapter.is_stream + is_stream = await query.adapter.is_stream_output_supported() except AttributeError: is_stream = False @@ -346,7 +346,7 @@ class DifyServiceAPIRunner(runner.RequestRunner): query.variables['conversation_id'] = query.session.using_conversation.uuid try: - is_stream = query.adapter.is_stream + is_stream = await query.adapter.is_stream_output_supported() except AttributeError: is_stream = False diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index 79de89a4..6b4da90b 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -89,7 +89,9 @@ class LocalAgentRunner(runner.RequestRunner): is_stream = query.adapter.is_stream_output_supported() try: - is_stream = query.adapter.is_stream + # print(await query.adapter.is_stream_output_supported()) + is_stream = await query.adapter.is_stream_output_supported() + except AttributeError: is_stream = False # while True: From 7728b4262bd195006f43f87b2c1aba53bb46b18c Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Mon, 21 Jul 2025 17:28:11 +0800 Subject: [PATCH 047/107] fix:lark message_id and dingtalk incoming_message --- pkg/platform/sources/dingtalk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/platform/sources/dingtalk.py b/pkg/platform/sources/dingtalk.py index 4a312063..d1859ab5 100644 --- a/pkg/platform/sources/dingtalk.py +++ b/pkg/platform/sources/dingtalk.py @@ -180,7 +180,7 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): async def create_message_card(self,message_id,event): card_template_id = self.config['card_template_id'] - incoming_message = event.incoming_message + incoming_message = event.source_platform_object.incoming_message # message_id = incoming_message.message_id card_instance, card_instance_id = await self.bot.create_and_card(card_template_id, incoming_message) self.card_instance_id_dict[message_id] = (card_instance, card_instance_id) From 074d359c8ef355478dae1eddda80570735102abc Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Mon, 21 Jul 2025 18:45:45 +0800 Subject: [PATCH 048/107] feat:add dashscopeapi stream fix:dify 64chunk yield --- pkg/provider/runners/dashscopeapi.py | 198 ++++++++++++++++++++------- pkg/provider/runners/difysvapi.py | 2 +- 2 files changed, 147 insertions(+), 53 deletions(-) diff --git a/pkg/provider/runners/dashscopeapi.py b/pkg/provider/runners/dashscopeapi.py index 02cb0b51..fe72b0a8 100644 --- a/pkg/provider/runners/dashscopeapi.py +++ b/pkg/provider/runners/dashscopeapi.py @@ -113,39 +113,84 @@ class DashScopeAPIRunner(runner.RequestRunner): # "session_file_ids": ["FILE_ID1"], # FILE_ID1 替换为实际的临时文件ID,逗号隔开多个 # } ) + idx_chunk = 0 + try: + # print(await query.adapter.is_stream_output_supported()) + is_stream = await query.adapter.is_stream_output_supported() - for chunk in response: - if chunk.get('status_code') != 200: - raise DashscopeAPIError( - f'Dashscope API 请求失败: status_code={chunk.get("status_code")} message={chunk.get("message")} request_id={chunk.get("request_id")} ' - ) - if not chunk: - continue + except AttributeError: + is_stream = False + if is_stream: + for chunk in response: + if chunk.get('status_code') != 200: + raise DashscopeAPIError( + f'Dashscope API 请求失败: status_code={chunk.get("status_code")} message={chunk.get("message")} request_id={chunk.get("request_id")} ' + ) + if not chunk: + continue + idx_chunk += 1 + # 获取流式传输的output + stream_output = chunk.get('output', {}) + if stream_output.get('text') is not None: + pending_content += stream_output.get('text') + # 是否是流式最后一个chunk + is_final = False if stream_output.get('finish_reason', False) == 'null' else True - # 获取流式传输的output - stream_output = chunk.get('output', {}) - if stream_output.get('text') is not None: - pending_content += stream_output.get('text') + # 获取模型传出的参考资料列表 + references_dict_list = stream_output.get('doc_references', []) - # 保存当前会话的session_id用于下次对话的语境 - query.session.using_conversation.uuid = stream_output.get('session_id') + # 从模型传出的参考资料信息中提取用于替换的字典 + if references_dict_list is not None: + for doc in references_dict_list: + if doc.get('index_id') is not None: + references_dict[doc.get('index_id')] = doc.get('doc_name') - # 获取模型传出的参考资料列表 - references_dict_list = stream_output.get('doc_references', []) + # 将参考资料替换到文本中 + pending_content = self._replace_references(pending_content, references_dict) - # 从模型传出的参考资料信息中提取用于替换的字典 - if references_dict_list is not None: - for doc in references_dict_list: - if doc.get('index_id') is not None: - references_dict[doc.get('index_id')] = doc.get('doc_name') + if idx_chunk % 64 == 0 or is_final: + yield llm_entities.MessageChunk( + role='assistant', + content=pending_content, + is_final=is_final, + ) + # 保存当前会话的session_id用于下次对话的语境 + query.session.using_conversation.uuid = stream_output.get('session_id') + else: + for chunk in response: + if chunk.get('status_code') != 200: + raise DashscopeAPIError( + f'Dashscope API 请求失败: status_code={chunk.get("status_code")} message={chunk.get("message")} request_id={chunk.get("request_id")} ' + ) + if not chunk: + continue + idx_chunk += 1 + # 获取流式传输的output + stream_output = chunk.get('output', {}) + if stream_output.get('text') is not None: + pending_content += stream_output.get('text') - # 将参考资料替换到文本中 - pending_content = self._replace_references(pending_content, references_dict) + # 保存当前会话的session_id用于下次对话的语境 + query.session.using_conversation.uuid = stream_output.get('session_id') - yield llm_entities.Message( - role='assistant', - content=pending_content, - ) + # 获取模型传出的参考资料列表 + references_dict_list = stream_output.get('doc_references', []) + + # 从模型传出的参考资料信息中提取用于替换的字典 + if references_dict_list is not None: + for doc in references_dict_list: + if doc.get('index_id') is not None: + references_dict[doc.get('index_id')] = doc.get('doc_name') + + # 将参考资料替换到文本中 + pending_content = self._replace_references(pending_content, references_dict) + + + + yield llm_entities.Message( + role='assistant', + content=pending_content, + ) async def _workflow_messages(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: """Dashscope 工作流对话请求""" @@ -177,38 +222,87 @@ class DashScopeAPIRunner(runner.RequestRunner): ) # 处理API返回的流式输出 - for chunk in response: - if chunk.get('status_code') != 200: - raise DashscopeAPIError( - f'Dashscope API 请求失败: status_code={chunk.get("status_code")} message={chunk.get("message")} request_id={chunk.get("request_id")} ' - ) - if not chunk: - continue + try: + # print(await query.adapter.is_stream_output_supported()) + is_stream = await query.adapter.is_stream_output_supported() - # 获取流式传输的output - stream_output = chunk.get('output', {}) - if stream_output.get('text') is not None: - pending_content += stream_output.get('text') + except AttributeError: + is_stream = False + idx_chunk = 0 + if is_stream: + for chunk in response: + if chunk.get('status_code') != 200: + raise DashscopeAPIError( + f'Dashscope API 请求失败: status_code={chunk.get("status_code")} message={chunk.get("message")} request_id={chunk.get("request_id")} ' + ) + if not chunk: + continue + idx_chunk += 1 + # 获取流式传输的output + stream_output = chunk.get('output', {}) + if stream_output.get('text') is not None: + pending_content += stream_output.get('text') - # 保存当前会话的session_id用于下次对话的语境 - query.session.using_conversation.uuid = stream_output.get('session_id') + is_final = False if stream_output.get('finish_reason', False) == 'null' else True - # 获取模型传出的参考资料列表 - references_dict_list = stream_output.get('doc_references', []) + # 获取模型传出的参考资料列表 + references_dict_list = stream_output.get('doc_references', []) - # 从模型传出的参考资料信息中提取用于替换的字典 - if references_dict_list is not None: - for doc in references_dict_list: - if doc.get('index_id') is not None: - references_dict[doc.get('index_id')] = doc.get('doc_name') + # 从模型传出的参考资料信息中提取用于替换的字典 + if references_dict_list is not None: + for doc in references_dict_list: + if doc.get('index_id') is not None: + references_dict[doc.get('index_id')] = doc.get('doc_name') - # 将参考资料替换到文本中 - pending_content = self._replace_references(pending_content, references_dict) + # 将参考资料替换到文本中 + pending_content = self._replace_references(pending_content, references_dict) + if is_final: + yield llm_entities.MessageChunk( + role='assistant', + content=pending_content, + is_final=is_final, - yield llm_entities.Message( - role='assistant', - content=pending_content, - ) + ) + + # 保存当前会话的session_id用于下次对话的语境 + query.session.using_conversation.uuid = stream_output.get('session_id') + + + else: + for chunk in response: + if chunk.get('status_code') != 200: + raise DashscopeAPIError( + f'Dashscope API 请求失败: status_code={chunk.get("status_code")} message={chunk.get("message")} request_id={chunk.get("request_id")} ' + ) + if not chunk: + continue + + # 获取流式传输的output + stream_output = chunk.get('output', {}) + if stream_output.get('text') is not None: + pending_content += stream_output.get('text') + + is_final = False if stream_output.get('finish_reason', False) == 'null' else True + + # 保存当前会话的session_id用于下次对话的语境 + query.session.using_conversation.uuid = stream_output.get('session_id') + + # 获取模型传出的参考资料列表 + references_dict_list = stream_output.get('doc_references', []) + + # 从模型传出的参考资料信息中提取用于替换的字典 + if references_dict_list is not None: + for doc in references_dict_list: + if doc.get('index_id') is not None: + references_dict[doc.get('index_id')] = doc.get('doc_name') + + # 将参考资料替换到文本中 + pending_content = self._replace_references(pending_content, references_dict) + + yield llm_entities.Message( + role='assistant', + content=pending_content, + ) async def run(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: """运行""" diff --git a/pkg/provider/runners/difysvapi.py b/pkg/provider/runners/difysvapi.py index f0c36ca1..7c7d81ad 100644 --- a/pkg/provider/runners/difysvapi.py +++ b/pkg/provider/runners/difysvapi.py @@ -259,7 +259,7 @@ class DifyServiceAPIRunner(runner.RequestRunner): is_final = False pending_agent_message += chunk['answer'] if is_stream: - if batch_pending_index % 32 == 0 or is_final: + if batch_pending_index % 64 == 0 or is_final: yield llm_entities.MessageChunk( role='assistant', content=self._try_convert_thinking(pending_agent_message), From a9776b7b53c45b3b8ebb52e0638c631e79642dda Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Tue, 29 Jul 2025 23:09:02 +0800 Subject: [PATCH 049/107] fix:del some print ,and amend respback on stream judge ,and del in dingtalk this is_stream_output_supported() use --- pkg/pipeline/respback/respback.py | 12 +- pkg/platform/adapter.py | 18 +- pkg/platform/sources/dingtalk.py | 13 +- pkg/platform/sources/lark.py | 154 ++++++++---------- pkg/platform/sources/telegram.py | 13 +- pkg/provider/modelmgr/requesters/chatcmpl.py | 47 ++---- .../modelmgr/requesters/deepseekchatcmpl.py | 2 +- pkg/provider/runners/dashscopeapi.py | 4 - pkg/provider/runners/difysvapi.py | 2 - pkg/provider/runners/localagent.py | 48 +++--- 10 files changed, 127 insertions(+), 186 deletions(-) diff --git a/pkg/pipeline/respback/respback.py b/pkg/pipeline/respback/respback.py index 9a410b3f..f4153218 100644 --- a/pkg/pipeline/respback/respback.py +++ b/pkg/pipeline/respback/respback.py @@ -39,11 +39,9 @@ class SendResponseBackStage(stage.PipelineStage): quote_origin = query.pipeline_config['output']['misc']['quote-origin'] - has_chunks = any(isinstance(msg, llm_entities.MessageChunk) for msg in query.resp_messages) - print(has_chunks) - if has_chunks and hasattr(query.adapter,'reply_message_chunk'): + # has_chunks = any(isinstance(msg, llm_entities.MessageChunk) for msg in query.resp_messages) + if await query.adapter.is_stream_output_supported(): is_final = [msg.is_final for msg in query.resp_messages][0] - print(is_final) await query.adapter.reply_message_chunk( message_source=query.message_event, message_id=query.resp_messages[-1].resp_message_id, @@ -58,10 +56,6 @@ class SendResponseBackStage(stage.PipelineStage): quote_origin=quote_origin, ) - # await query.adapter.reply_message( - # message_source=query.message_event, - # message=query.resp_message_chain[-1], - # quote_origin=quote_origin, - # ) + return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) diff --git a/pkg/platform/adapter.py b/pkg/platform/adapter.py index d4b48ef6..e4369efb 100644 --- a/pkg/platform/adapter.py +++ b/pkg/platform/adapter.py @@ -25,7 +25,6 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): logger: EventLogger - is_stream: bool def __init__(self, config: dict, ap: app.Application, logger: EventLogger): """初始化适配器 @@ -62,26 +61,31 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): quote_origin (bool, optional): 是否引用原消息. Defaults to False. """ raise NotImplementedError - + async def reply_message_chunk( self, - message_source: platform_events.MessageEvent, + message_source: platform_events.MessageEvent, message_id: int, message: platform_message.MessageChain, quote_origin: bool = False, is_final: bool = False, - ): + ): """回复消息(流式输出) Args: message_source (platform.types.MessageEvent): 消息源事件 message_id (int): 消息ID message (platform.types.MessageChain): 消息链 quote_origin (bool, optional): 是否引用原消息. Defaults to False. + is_final (bool, optional): 流式是否结束. Defaults to False. """ raise NotImplementedError - async def create_message_card(self,message_id,event): - '''创建卡片消息''' + async def create_message_card(self, message_id:typing.Type[str,int], event:platform_events.MessageEvent) -> bool: + """创建卡片消息 + Args: + message_id (str): 消息ID + event (platform_events.MessageEvent): 消息源事件 + """ return False async def is_muted(self, group_id: int) -> bool: @@ -117,11 +121,9 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): async def run_async(self): """异步运行""" raise NotImplementedError - async def is_stream_output_supported(self) -> bool: """是否支持流式输出""" - self.is_stream = False return False async def kill(self) -> bool: diff --git a/pkg/platform/sources/dingtalk.py b/pkg/platform/sources/dingtalk.py index d1859ab5..9f834f2a 100644 --- a/pkg/platform/sources/dingtalk.py +++ b/pkg/platform/sources/dingtalk.py @@ -148,7 +148,7 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): message: platform_message.MessageChain, quote_origin: bool = False, is_final: bool = False, - ): + ): event = await DingTalkEventConverter.yiri2target( message_source, ) @@ -158,13 +158,12 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): content, at = await DingTalkMessageConverter.yiri2target(message) - card_instance,card_instance_id = self.card_instance_id_dict[message_id] + card_instance, card_instance_id = self.card_instance_id_dict[message_id] # print(card_instance_id) - await self.bot.send_card_message(card_instance,card_instance_id,content,is_final) + await self.bot.send_card_message(card_instance, card_instance_id, content, is_final) if is_final: self.card_instance_id_dict.pop(message_id) - async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): content = await DingTalkMessageConverter.yiri2target(message) if target_type == 'person': @@ -174,11 +173,11 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): async def is_stream_output_supported(self) -> bool: is_stream = False - if self.config.get("enable-stream-reply", None): + if self.config.get('enable-stream-reply', None): is_stream = True return is_stream - async def create_message_card(self,message_id,event): + async def create_message_card(self, message_id, event): card_template_id = self.config['card_template_id'] incoming_message = event.source_platform_object.incoming_message # message_id = incoming_message.message_id @@ -186,7 +185,6 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): self.card_instance_id_dict[message_id] = (card_instance, card_instance_id) return True - def register_listener( self, event_type: typing.Type[platform_events.Event], @@ -194,7 +192,6 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): ): async def on_message(event: DingTalkEvent): try: - await self.is_stream_output_supported() return await callback( await self.event_converter.target2yiri(event, self.config['robot_name']), self, diff --git a/pkg/platform/sources/lark.py b/pkg/platform/sources/lark.py index fb3d0c48..0d7fc0fb 100644 --- a/pkg/platform/sources/lark.py +++ b/pkg/platform/sources/lark.py @@ -9,7 +9,6 @@ import re import base64 import uuid import json -import time import datetime import hashlib from Crypto.Cipher import AES @@ -344,12 +343,11 @@ class LarkAdapter(adapter.MessagePlatformAdapter): config: dict quart_app: quart.Quart ap: app.Application - - message_id_to_card_id: typing.Dict[str, typing.Tuple[str, int]] - card_id_dict: dict[str, str] - seq: int + card_id_dict: dict[str, str] # 消息id到卡片id的映射,便于创建卡片后的发送消息到指定卡片 + + seq: int # 用于在发送卡片消息中识别消息顺序,直接以seq作为标识 def __init__(self, config: dict, ap: app.Application, logger: EventLogger): self.config = config @@ -357,10 +355,9 @@ class LarkAdapter(adapter.MessagePlatformAdapter): self.logger = logger self.quart_app = quart.Quart(__name__) self.listeners = {} - self.message_id_to_card_id = {} self.card_id_dict = {} self.seq = 1 - self.card_id_time = {} + @self.quart_app.route('/lark/callback', methods=['POST']) async def lark_callback(): @@ -405,15 +402,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): await self.logger.error(f'Error in lark callback: {traceback.format_exc()}') return {'code': 500, 'message': 'error'} - - - - - - - async def on_message(event: lark_oapi.im.v1.P2ImMessageReceiveV1): - lb_event = await self.event_converter.target2yiri(event, self.api_client) await self.listeners[type(lb_event)](lb_event, self) @@ -435,51 +424,49 @@ class LarkAdapter(adapter.MessagePlatformAdapter): async def is_stream_output_supported(self) -> bool: is_stream = False - if self.config.get("enable-stream-reply", None): + if self.config.get('enable-stream-reply', None): is_stream = True return is_stream - async def create_card_id(self,message_id): + async def create_card_id(self, message_id): try: - is_stream = await self.is_stream_output_supported() - if is_stream: - self.ap.logger.debug('飞书支持stream输出,创建卡片......') + self.ap.logger.debug('飞书支持stream输出,创建卡片......') - card_data = {"schema": "2.0", "header": {"title": {"content": "bot", "tag": "plain_text"}}, - "body": {"elements": [ - {"tag": "markdown", "content": "[思考中.....]", "element_id": "markdown_1"}]}, - "config": {"streaming_mode": True, - "streaming_config": {"print_strategy": "delay"}}} # delay / fast + card_data = { + 'schema': '2.0', + 'header': {'title': {'content': 'bot', 'tag': 'plain_text'}}, + 'body': {'elements': [{'tag': 'markdown', 'content': '[思考中.....]', 'element_id': 'markdown_1'}]}, + 'config': {'streaming_mode': True, 'streaming_config': {'print_strategy': 'delay'}}, + } # delay / fast 创建卡片模板,delay 延迟打印,fast 实时打印,可以自定义更好看的消息模板 - request: CreateCardRequest = CreateCardRequest.builder() \ - .request_body( - CreateCardRequestBody.builder() - .type("card_json") - .data(json.dumps(card_data)) \ - .build() - ).build() + request: CreateCardRequest = ( + CreateCardRequest.builder() + .request_body(CreateCardRequestBody.builder().type('card_json').data(json.dumps(card_data)).build()) + .build() + ) - # 发起请求 - response: CreateCardResponse = self.api_client.cardkit.v1.card.create(request) + # 发起请求 + response: CreateCardResponse = self.api_client.cardkit.v1.card.create(request) - # 处理失败返回 - if not response.success(): - raise Exception( - f"client.cardkit.v1.card.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}") + # 处理失败返回 + if not response.success(): + raise Exception( + f'client.cardkit.v1.card.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}' + ) - self.ap.logger.debug(f'飞书卡片创建成功,卡片ID: {response.data.card_id}') - self.card_id_dict[message_id] = response.data.card_id + self.ap.logger.debug(f'飞书卡片创建成功,卡片ID: {response.data.card_id}') + self.card_id_dict[message_id] = response.data.card_id - card_id = response.data.card_id + card_id = response.data.card_id return card_id except Exception as e: self.ap.logger.error(f'飞书卡片创建失败,错误信息: {e}') - async def create_message_card(self,message_id,event) -> str: + async def create_message_card(self, message_id, event) -> str: """ 创建卡片消息。 - 使用卡片消息是因为普通消息更新次数有限制,而大模型流式返回结果可能很多而超过限制,而飞书卡片没有这个限制 + 使用卡片消息是因为普通消息更新次数有限制,而大模型流式返回结果可能很多而超过限制,而飞书卡片没有这个限制(api免费次数有限) """ # message_id = event.message_chain.message_id @@ -487,7 +474,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): content = { 'type': 'card', 'data': {'card_id': card_id, 'template_variable': {'content': 'Thinking...'}}, - } + } # 当收到消息时发送消息模板,可添加模板变量,详情查看飞书中接口文档 request: ReplyMessageRequest = ( ReplyMessageRequest.builder() .message_id(event.message_chain.message_id) @@ -545,7 +532,6 @@ class LarkAdapter(adapter.MessagePlatformAdapter): f'client.im.v1.message.reply failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}' ) - async def reply_message_chunk( self, message_source: platform_events.MessageEvent, @@ -557,56 +543,50 @@ class LarkAdapter(adapter.MessagePlatformAdapter): """ 回复消息变成更新卡片消息 """ - lark_message = await self.message_converter.yiri2target(message, self.api_client) - - self.seq += 1 + if (self.seq - 1) % 8 == 0 or is_final: + lark_message = await self.message_converter.yiri2target(message, self.api_client) - text_message = '' - for ele in lark_message[0]: - if ele['tag'] == 'text': - text_message += ele['text'] - elif ele['tag'] == 'md': - text_message += ele['text'] - print(text_message) + text_message = '' + for ele in lark_message[0]: + if ele['tag'] == 'text': + text_message += ele['text'] + elif ele['tag'] == 'md': + text_message += ele['text'] - content = { - 'type': 'card_json', - 'data': {'card_id': self.card_id_dict[message_id], 'elements': {'content': text_message}}, - } + # content = { + # 'type': 'card_json', + # 'data': {'card_id': self.card_id_dict[message_id], 'elements': {'content': text_message}}, + # } - request: ContentCardElementRequest = ContentCardElementRequest.builder() \ - .card_id(self.card_id_dict[message_id]) \ - .element_id("markdown_1") \ - .request_body(ContentCardElementRequestBody.builder() - # .uuid("a0d69e20-1dd1-458b-k525-dfeca4015204") - .content(text_message) - .sequence(self.seq) - .build()) \ - .build() - - if is_final: - self.seq = 1 - self.card_id_dict.pop(message_id) - # 发起请求 - response: ContentCardElementResponse = self.api_client.cardkit.v1.card_element.content(request) - - - # 处理失败返回 - if not response.success(): - raise Exception( - f'client.im.v1.message.patch failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}' + request: ContentCardElementRequest = ( + ContentCardElementRequest.builder() + .card_id(self.card_id_dict[message_id]) + .element_id('markdown_1') + .request_body( + ContentCardElementRequestBody.builder() + # .uuid("a0d69e20-1dd1-458b-k525-dfeca4015204") + .content(text_message) + .sequence(self.seq) + .build() + ) + .build() ) - return - - - - - + if is_final: + self.seq = 1 # 消息回复结束之后重置seq + self.card_id_dict.pop(message_id) # 清理已经使用过的卡片 + # 发起请求 + response: ContentCardElementResponse = self.api_client.cardkit.v1.card_element.content(request) + # 处理失败返回 + if not response.success(): + raise Exception( + f'client.im.v1.message.patch failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}' + ) + return async def is_muted(self, group_id: int) -> bool: return False @@ -658,4 +638,4 @@ class LarkAdapter(adapter.MessagePlatformAdapter): # 所以要设置_auto_reconnect=False,让其不重连。 self.bot._auto_reconnect = False await self.bot._disconnect() - return False + return False \ No newline at end of file diff --git a/pkg/platform/sources/telegram.py b/pkg/platform/sources/telegram.py index efc7890f..22ef63e8 100644 --- a/pkg/platform/sources/telegram.py +++ b/pkg/platform/sources/telegram.py @@ -167,7 +167,7 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): await self.listeners[type(lb_event)](lb_event, self) await self.is_stream_output_supported() except Exception: - await self.logger.error(f"Error in telegram callback: {traceback.format_exc()}") + await self.logger.error(f'Error in telegram callback: {traceback.format_exc()}') self.application = ApplicationBuilder().token(self.config['token']).build() self.bot = self.application.bot @@ -206,7 +206,6 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): await self.bot.send_message(**args) - async def reply_message_chunk( self, message_source: platform_events.MessageEvent, @@ -214,8 +213,7 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): message: platform_message.MessageChain, quote_origin: bool = False, is_final: bool = False, - ): - + ): assert isinstance(message_source.source_platform_object, Update) components = await TelegramMessageConverter.yiri2target(message, self.bot) args = {} @@ -240,7 +238,6 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): if self.config['markdown_card'] is True: args['parse_mode'] = 'MarkdownV2' - send_msg = await self.bot.send_message(**args) send_msg_id = send_msg.message_id self.msg_stream_id[message_id] = send_msg_id @@ -264,16 +261,12 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): if is_final: self.msg_stream_id.pop(message_id) - async def is_stream_output_supported(self) -> bool: is_stream = False - if self.config.get("enable-stream-reply", None): + if self.config.get('enable-stream-reply', None): is_stream = True - self.is_stream = is_stream - return is_stream - async def is_muted(self, group_id: int) -> bool: return False diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index 8e350bf6..d5c3b90a 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -17,14 +17,13 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): """OpenAI ChatCompletion API 请求器""" client: openai.AsyncClient - is_content:bool + is_content: bool default_config: dict[str, typing.Any] = { 'base_url': 'https://api.openai.com/v1', 'timeout': 120, } - async def initialize(self): self.client = openai.AsyncClient( api_key='', @@ -46,7 +45,6 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): args: dict, extra_body: dict = {}, ) -> chat_completion.ChatCompletion: - async for chunk in await self.client.chat.completions.create(**args, extra_body=extra_body): yield chunk @@ -66,23 +64,23 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): # deepseek的reasoner模型 if pipeline_config['trigger'].get('misc', '').get('remove_think'): - pass else: - if reasoning_content is not None : - chatcmpl_message['content'] = '\n' + reasoning_content + '\n\n' + chatcmpl_message['content'] + if reasoning_content is not None: + chatcmpl_message['content'] = ( + '\n' + reasoning_content + '\n\n' + chatcmpl_message['content'] + ) message = llm_entities.Message(**chatcmpl_message) return message - + async def _make_msg_chunk( self, pipeline_config: dict[str, typing.Any], chat_completion: chat_completion.ChatCompletion, idx: int, ) -> llm_entities.MessageChunk: - # 处理流式chunk和完整响应的差异 # print(chat_completion.choices[0]) if hasattr(chat_completion, 'choices'): @@ -98,7 +96,6 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): if 'role' not in delta or delta['role'] is None: delta['role'] = 'assistant' - reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None delta['content'] = '' if delta['content'] is None else delta['content'] @@ -106,13 +103,13 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): # deepseek的reasoner模型 if pipeline_config['trigger'].get('misc', '').get('remove_think'): - if reasoning_content is not None : + if reasoning_content is not None: pass else: delta['content'] = delta['content'] else: if reasoning_content is not None and idx == 0: - delta['content'] += f'\n{reasoning_content}' + delta['content'] += f'\n{reasoning_content}' elif reasoning_content is None: if self.is_content: delta['content'] = delta['content'] @@ -122,7 +119,6 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): else: delta['content'] += reasoning_content - message = llm_entities.MessageChunk(**delta) return message @@ -135,9 +131,10 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): use_funcs: list[tools_entities.LLMFunction] = None, stream: bool = False, extra_args: dict[str, typing.Any] = {}, - ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: + ) ->llm_entities.MessageChunk: self.client.api_key = use_model.token_mgr.get_token() + args = {} args['model'] = use_model.model_entity.name @@ -163,14 +160,14 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): if stream: current_content = '' - args["stream"] = True + args['stream'] = True chunk_idx = 0 self.is_content = False tool_calls_map: dict[str, llm_entities.ToolCall] = {} pipeline_config = query.pipeline_config async for chunk in self._req_stream(args, extra_body=extra_args): # 处理流式消息 - delta_message = await self._make_msg_chunk(pipeline_config,chunk,chunk_idx) + delta_message = await self._make_msg_chunk(pipeline_config, chunk, chunk_idx) if delta_message.content: current_content += delta_message.content delta_message.content = current_content @@ -182,15 +179,13 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): id=tool_call.id, type=tool_call.type, function=llm_entities.FunctionCall( - name=tool_call.function.name if tool_call.function else '', - arguments='' + name=tool_call.function.name if tool_call.function else '', arguments='' ), ) if tool_call.function and tool_call.function.arguments: # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments - chunk_idx += 1 chunk_choices = getattr(chunk, 'choices', None) if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): @@ -198,11 +193,9 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): delta_message.content = current_content if chunk_idx % 64 == 0 or delta_message.is_final: - yield delta_message # return - async def _closure( self, query: core_entities.Query, @@ -211,7 +204,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): use_funcs: list[tools_entities.LLMFunction] = None, stream: bool = False, extra_args: dict[str, typing.Any] = {}, - ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: + ) -> llm_entities.Message: self.client.api_key = use_model.token_mgr.get_token() args = {} @@ -237,22 +230,15 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): args['messages'] = messages - - # 发送请求 resp = await self._req(args, extra_body=extra_args) # 处理请求结果 pipeline_config = query.pipeline_config - message = await self._make_msg(resp,pipeline_config) - + message = await self._make_msg(resp, pipeline_config) return message - - - - async def invoke_llm( self, query: core_entities.Query, @@ -273,7 +259,6 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): req_messages.append(msg_dict) try: - msg = await self._closure( query=query, req_messages=req_messages, @@ -334,7 +319,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): funcs: typing.List[tools_entities.LLMFunction] = None, stream: bool = False, extra_args: dict[str, typing.Any] = {}, - ) -> llm_entities.MessageChunk: + ) -> llm_entities.MessageChunk: req_messages = [] # req_messages 仅用于类内,外部同步由 query.messages 进行 for m in messages: msg_dict = m.dict(exclude_none=True) diff --git a/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py b/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py index f57f624f..d75d0fb6 100644 --- a/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py +++ b/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py @@ -55,6 +55,6 @@ class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions): raise errors.RequesterError('接口返回为空,请确定模型提供商服务是否正常') pipeline_config = query.pipeline_config # 处理请求结果 - message = await self._make_msg(resp,pipeline_config) + message = await self._make_msg(resp, pipeline_config) return message diff --git a/pkg/provider/runners/dashscopeapi.py b/pkg/provider/runners/dashscopeapi.py index fe72b0a8..9bb5824c 100644 --- a/pkg/provider/runners/dashscopeapi.py +++ b/pkg/provider/runners/dashscopeapi.py @@ -185,8 +185,6 @@ class DashScopeAPIRunner(runner.RequestRunner): # 将参考资料替换到文本中 pending_content = self._replace_references(pending_content, references_dict) - - yield llm_entities.Message( role='assistant', content=pending_content, @@ -261,13 +259,11 @@ class DashScopeAPIRunner(runner.RequestRunner): role='assistant', content=pending_content, is_final=is_final, - ) # 保存当前会话的session_id用于下次对话的语境 query.session.using_conversation.uuid = stream_output.get('session_id') - else: for chunk in response: if chunk.get('status_code') != 200: diff --git a/pkg/provider/runners/difysvapi.py b/pkg/provider/runners/difysvapi.py index 7c7d81ad..8182cc54 100644 --- a/pkg/provider/runners/difysvapi.py +++ b/pkg/provider/runners/difysvapi.py @@ -148,7 +148,6 @@ class DifyServiceAPIRunner(runner.RequestRunner): if mode == 'workflow': if chunk['event'] == 'node_finished': if not is_stream: - if chunk['data']['node_type'] == 'answer': yield llm_entities.Message( role='assistant', @@ -274,7 +273,6 @@ class DifyServiceAPIRunner(runner.RequestRunner): content=self._try_convert_thinking(pending_agent_message), ) - if chunk['event'] == 'agent_thought': if chunk['tool'] != '' and chunk['observation'] != '': # 工具调用结果,跳过 continue diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index 6b4da90b..599b0b08 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -2,7 +2,6 @@ from __future__ import annotations import json import copy -from ssl import ALERT_DESCRIPTION_BAD_CERTIFICATE_HASH_VALUE import typing from .. import runner from ...core import entities as core_entities @@ -30,11 +29,14 @@ class LocalAgentRunner(runner.RequestRunner): class ToolCallTracker: """工具调用追踪器""" + def __init__(self): - self.active_calls: dict[str,dict] = {} + self.active_calls: dict[str, dict] = {} self.completed_calls: list[llm_entities.ToolCall] = [] - async def run(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message | llm_entities.MessageChunk, None]: + async def run( + self, query: core_entities.Query + ) -> typing.AsyncGenerator[llm_entities.Message | llm_entities.MessageChunk, None]: """运行请求""" pending_tool_calls = [] @@ -89,16 +91,14 @@ class LocalAgentRunner(runner.RequestRunner): is_stream = query.adapter.is_stream_output_supported() try: - # print(await query.adapter.is_stream_output_supported()) is_stream = await query.adapter.is_stream_output_supported() except AttributeError: is_stream = False - # while True: - # pass + if not is_stream: # 非流式输出,直接请求 - # print(123) + msg = await query.use_llm_model.requester.invoke_llm( query, query.use_llm_model, @@ -108,7 +108,6 @@ class LocalAgentRunner(runner.RequestRunner): ) yield msg final_msg = msg - print(final_msg) else: # 流式输出,需要处理工具调用 tool_calls_map: dict[str, llm_entities.ToolCall] = {} @@ -122,27 +121,26 @@ class LocalAgentRunner(runner.RequestRunner): ): assert isinstance(msg, llm_entities.MessageChunk) yield msg - # if msg.tool_calls: - # for tool_call in msg.tool_calls: - # if tool_call.id not in tool_calls_map: - # tool_calls_map[tool_call.id] = llm_entities.ToolCall( - # id=tool_call.id, - # type=tool_call.type, - # function=llm_entities.FunctionCall( - # name=tool_call.function.name if tool_call.function else '', - # arguments='' - # ), - # ) - # if tool_call.function and tool_call.function.arguments: - # # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 - # tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + if msg.tool_calls: + for tool_call in msg.tool_calls: + if tool_call.id not in tool_calls_map: + tool_calls_map[tool_call.id] = llm_entities.ToolCall( + id=tool_call.id, + type=tool_call.type, + function=llm_entities.FunctionCall( + name=tool_call.function.name if tool_call.function else '', + arguments='' + ), + ) + if tool_call.function and tool_call.function.arguments: + # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 + tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments final_msg = llm_entities.Message( role=msg.role, content=msg.all_content, tool_calls=list(tool_calls_map.values()), ) - pending_tool_calls = final_msg.tool_calls req_messages.append(final_msg) @@ -193,8 +191,7 @@ class LocalAgentRunner(runner.RequestRunner): id=tool_call.id, type=tool_call.type, function=llm_entities.FunctionCall( - name=tool_call.function.name if tool_call.function else '', - arguments='' + name=tool_call.function.name if tool_call.function else '', arguments='' ), ) if tool_call.function and tool_call.function.arguments: @@ -206,7 +203,6 @@ class LocalAgentRunner(runner.RequestRunner): tool_calls=list(tool_calls_map.values()), ) else: - print("非流式") # 处理完所有调用,再次请求 msg = await query.use_llm_model.requester.invoke_llm( query, From 8fe0992c15eba2585a58669a1e8a5a628c115bf0 Mon Sep 17 00:00:00 2001 From: fdc <2213070223@qq.com> Date: Wed, 30 Jul 2025 15:21:59 +0800 Subject: [PATCH 050/107] fix:in chat judge create_message_card telegram reply_message_chunk no message --- pkg/pipeline/process/handlers/chat.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/pipeline/process/handlers/chat.py b/pkg/pipeline/process/handlers/chat.py index 0fe7f868..0f802658 100644 --- a/pkg/pipeline/process/handlers/chat.py +++ b/pkg/pipeline/process/handlers/chat.py @@ -81,14 +81,14 @@ class ChatMessageHandler(handler.MessageHandler): query.resp_message_chain.pop() query.resp_messages.append(result) - self.ap.logger.info(f'对话({query.query_id})流式响应: {self.cut_str(result.readable_str())}') + self.ap.logger.info( + f'对话({query.query_id})流式响应: {self.cut_str(result.readable_str())}' + ) if result.content is not None: text_length += len(result.content) yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) - # else: - # yield entities.StageProcessResult(result_type=entities.ResultType.INTERRUPT, new_query=query) else: async for result in runner.run(query): From 2a17e89a999363a0be0e1f4e3814e3841ae5fe82 Mon Sep 17 00:00:00 2001 From: fdc <2213070223@qq.com> Date: Wed, 30 Jul 2025 17:06:14 +0800 Subject: [PATCH 051/107] feat: add webchat stream but only some --- .../controller/groups/pipelines/webchat.py | 36 +++++++++-- pkg/platform/sources/webchat.py | 61 ++++++++++++++++--- pkg/platform/sources/webchat.yaml | 13 +++- web/src/app/infra/http/HttpClient.ts | 3 + 4 files changed, 97 insertions(+), 16 deletions(-) diff --git a/pkg/api/http/controller/groups/pipelines/webchat.py b/pkg/api/http/controller/groups/pipelines/webchat.py index c8c8db54..4baaa1e5 100644 --- a/pkg/api/http/controller/groups/pipelines/webchat.py +++ b/pkg/api/http/controller/groups/pipelines/webchat.py @@ -1,3 +1,5 @@ +import json + import quart from ... import group @@ -9,10 +11,16 @@ class WebChatDebugRouterGroup(group.RouterGroup): @self.route('/send', methods=['POST']) async def send_message(pipeline_uuid: str) -> str: """Send a message to the pipeline for debugging""" + + async def stream_generator(generator): + async for message in generator: + yield rf"data:{json.dumps({'message': message})}\n\n" + yield "data:{'type': 'end'}\n\n''" try: data = await quart.request.get_json() session_type = data.get('session_type', 'person') message_chain_obj = data.get('message', []) + is_stream = data.get('is_stream', False) if not message_chain_obj: return self.http_status(400, -1, 'message is required') @@ -25,13 +33,29 @@ class WebChatDebugRouterGroup(group.RouterGroup): if not webchat_adapter: return self.http_status(404, -1, 'WebChat adapter not found') - result = await webchat_adapter.send_webchat_message(pipeline_uuid, session_type, message_chain_obj) + if is_stream: + + generator = webchat_adapter.send_webchat_message(pipeline_uuid, session_type, message_chain_obj, is_stream) + + return quart.Response( + stream_generator(generator), + mimetype='text/event-stream' + ) + + else: + # result = await webchat_adapter.send_webchat_message(pipeline_uuid, session_type, message_chain_obj) + result = None + async for message in webchat_adapter.send_webchat_message(pipeline_uuid, session_type, message_chain_obj): + result = message + if result is not None: + return self.success( + data={ + 'message': result, + } + ) + else: + return self.http_status(400, -1, 'message is required') - return self.success( - data={ - 'message': result, - } - ) except Exception as e: return self.http_status(500, -1, f'Internal server error: {str(e)}') diff --git a/pkg/platform/sources/webchat.py b/pkg/platform/sources/webchat.py index 51b0479f..7fd7bb3b 100644 --- a/pkg/platform/sources/webchat.py +++ b/pkg/platform/sources/webchat.py @@ -25,11 +25,13 @@ class WebChatSession: id: str message_lists: dict[str, list[WebChatMessage]] = {} resp_waiters: dict[int, asyncio.Future[WebChatMessage]] + resp_queues = dict[int, asyncio.Queue[WebChatMessage]] def __init__(self, id: str): self.id = id self.message_lists = {} self.resp_waiters = {} + self.resp_queues = {} def get_message_list(self, pipeline_uuid: str) -> list[WebChatMessage]: if pipeline_uuid not in self.message_lists: @@ -108,6 +110,35 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): return message_data.model_dump() + async def reply_message_chunk( + self, + message_source: platform_events.MessageEvent, + message_id: str, + message: platform_message.MessageChain, + quote_origin: bool = False, + is_fianl: bool = False, + ) -> dict: + """回复消息""" + message_data = WebChatMessage( + id=-1, + role='assistant', + content=str(message), + message_chain=[component.__dict__ for component in message], + timestamp=datetime.now().isoformat(), + ) + + # notify waiter + if isinstance(message_source, platform_events.FriendMessage): + queue = self.webchat_person_session.resp_queues[message_source.message_chain.message_id] + elif isinstance(message_source, platform_events.GroupMessage): + queue = self.webchat_group_session.resp_queues[message_source.message_chain.message_id] + + queue.put(message_data) + if is_fianl: + queue.put(None) + + return message_data.model_dump() + def register_listener( self, event_type: typing.Type[platform_events.Event], @@ -140,7 +171,8 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): await self.logger.info('WebChat调试适配器正在停止') async def send_webchat_message( - self, pipeline_uuid: str, session_type: str, message_chain_obj: typing.List[dict] + self, pipeline_uuid: str, session_type: str, message_chain_obj: typing.List[dict], + is_stream: bool = False, ) -> dict: """发送调试消息到流水线""" if session_type == 'person': @@ -188,18 +220,29 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): if event.__class__ in self.listeners: await self.listeners[event.__class__](event, self) - # set waiter - waiter = asyncio.Future[WebChatMessage]() - use_session.resp_waiters[message_id] = waiter - waiter.add_done_callback(lambda future: use_session.resp_waiters.pop(message_id)) + if is_stream: + queue = use_session.resp_queues[message_id] + while True: + resp_message = await queue.get() + if resp_message is None: + resp_message.id = len(use_session.get_message_list(pipeline_uuid)) + 1 + use_session.get_message_list(pipeline_uuid).append(resp_message) + break + yield resp_message.model_dump() - resp_message = await waiter + else: + # set waiter + waiter = asyncio.Future[WebChatMessage]() + use_session.resp_waiters[message_id] = waiter + waiter.add_done_callback(lambda future: use_session.resp_waiters.pop(message_id)) - resp_message.id = len(use_session.get_message_list(pipeline_uuid)) + 1 + resp_message = await waiter - use_session.get_message_list(pipeline_uuid).append(resp_message) + resp_message.id = len(use_session.get_message_list(pipeline_uuid)) + 1 - return resp_message.model_dump() + use_session.get_message_list(pipeline_uuid).append(resp_message) + + yield resp_message.model_dump() def get_webchat_messages(self, pipeline_uuid: str, session_type: str) -> list[dict]: """获取调试消息历史""" diff --git a/pkg/platform/sources/webchat.yaml b/pkg/platform/sources/webchat.yaml index 4e8cc38e..0b1d4c29 100644 --- a/pkg/platform/sources/webchat.yaml +++ b/pkg/platform/sources/webchat.yaml @@ -9,7 +9,18 @@ metadata: en_US: "WebChat adapter for pipeline debugging" zh_Hans: "用于流水线调试的网页聊天适配器" icon: "" -spec: {} +spec: + config: + - name: enable-stream-reply + label: + en_US: Enable Stream Reply Mode + zh_Hans: 启用电报流式回复模式 + description: + en_US: If enabled, the bot will use the stream of telegram reply mode + zh_Hans: 如果启用,将使用电报流式方式来回复内容 + type: boolean + required: true + default: false execution: python: path: "webchat.py" diff --git a/web/src/app/infra/http/HttpClient.ts b/web/src/app/infra/http/HttpClient.ts index 9a49c1e3..7c05bf09 100644 --- a/web/src/app/infra/http/HttpClient.ts +++ b/web/src/app/infra/http/HttpClient.ts @@ -42,6 +42,7 @@ import { } from '@/app/infra/entities/api'; import { GetBotLogsRequest } from '@/app/infra/http/requestParam/bots/GetBotLogsRequest'; import { GetBotLogsResponse } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse'; +import {boolean} from "zod"; type JSONValue = string | number | boolean | JSONObject | JSONArray | null; interface JSONObject { @@ -359,12 +360,14 @@ class HttpClient { messageChain: object[], pipelineId: string, timeout: number = 15000, + is_stream: boolean = false, ): Promise { return this.post( `/api/v1/pipelines/${pipelineId}/chat/send`, { session_type: sessionType, message: messageChain, + is_stream: is_stream, }, { timeout, From 00a8410c94348baef77190156d8be5b7af9721fb Mon Sep 17 00:00:00 2001 From: fdc <2213070223@qq.com> Date: Thu, 31 Jul 2025 09:51:25 +0800 Subject: [PATCH 052/107] feat:webchat frontend stream --- .../components/debug-dialog/DebugDialog.tsx | 112 +++++++++++++++--- web/src/app/infra/http/HttpClient.ts | 83 +++++++++++++ web/src/i18n/locales/en-US.ts | 1 + web/src/i18n/locales/ja-JP.ts | 1 + web/src/i18n/locales/zh-Hans.ts | 1 + 5 files changed, 181 insertions(+), 17 deletions(-) diff --git a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx index a84389e0..9fde4bc2 100644 --- a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx +++ b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx @@ -10,6 +10,7 @@ import { cn } from '@/lib/utils'; import { Message } from '@/app/infra/entities/message'; import { toast } from 'sonner'; import AtBadge from './AtBadge'; +import { Switch } from '@/components/ui/switch'; interface MessageComponent { type: 'At' | 'Plain'; @@ -36,6 +37,7 @@ export default function DebugDialog({ const [showAtPopover, setShowAtPopover] = useState(false); const [hasAt, setHasAt] = useState(false); const [isHovering, setIsHovering] = useState(false); + const [isStreaming, setIsStreaming] = useState(false); const messagesEndRef = useRef(null); const inputRef = useRef(null); const popoverRef = useRef(null); @@ -157,27 +159,96 @@ export default function DebugDialog({ // for showing text_content = '@webchatbot' + text_content; } - const userMessage: Message = { - id: -1, - role: 'user', - content: text_content, - timestamp: new Date().toISOString(), - message_chain: messageChain, - }; + id: -1, + role: 'user', + content: text_content, + timestamp: new Date().toISOString(), + message_chain: messageChain, + }; + // 根据isStreaming状态决定使用哪种传输方式 + if (isStreaming) { + // 创建初始bot消息 + const botMessage: Message = { + id: -1, + role: 'assistant', + content: '', + timestamp: new Date().toISOString(), + message_chain: [{ type: 'Plain', text: '' }], + }; - setMessages((prevMessages) => [...prevMessages, userMessage]); - setInputValue(''); - setHasAt(false); + // 添加用户消息和初始bot消息到状态 - const response = await httpClient.sendWebChatMessage( - sessionType, - messageChain, - selectedPipelineId, - 120000, - ); + setMessages((prevMessages) => [...prevMessages, userMessage, botMessage]); + setInputValue(''); + setHasAt(false); - setMessages((prevMessages) => [...prevMessages, response.message]); + try { + let botMessageId = botMessage.id; + let accumulatedContent = ''; + + await httpClient.sendStreamingWebChatMessage( + sessionType, + messageChain, + selectedPipelineId, + (data) => { + // 处理流式响应数据 + if (data.message) { + accumulatedContent += data.message; + + // 更新bot消息 + setMessages((prevMessages) => { + const updatedMessages = [...prevMessages]; + const botMessageIndex = updatedMessages.findIndex( + (msg) => msg.id === botMessageId && msg.role === 'assistant' + ); + + if (botMessageIndex !== -1) { + const updatedBotMessage = { + ...updatedMessages[botMessageIndex], + content: accumulatedContent, + message_chain: [{ type: 'Plain', text: accumulatedContent }], + }; + updatedMessages[botMessageIndex] = updatedBotMessage; + } + + return updatedMessages; + }); + } + }, + () => { + // 流传输完成 + console.log('Streaming completed'); + }, + (error) => { + // 处理错误 + console.error('Streaming error:', error); + if (sessionType === 'person') { + toast.error(t('pipelines.debugDialog.sendFailed')); + } + } + ); + } catch (error) { + console.error('Failed to send streaming message:', error); + if (sessionType === 'person') { + toast.error(t('pipelines.debugDialog.sendFailed')); + } + } + } else { + + setMessages((prevMessages) => [...prevMessages, userMessage]); + setInputValue(''); + setHasAt(false); + + const response = await httpClient.sendWebChatMessage( + sessionType, + messageChain, + selectedPipelineId, + 120000, + ); + + setMessages((prevMessages) => [...prevMessages, response.message]); + } } catch ( // eslint-disable-next-line @typescript-eslint/no-explicit-any error: any @@ -306,6 +377,13 @@ export default function DebugDialog({
+
+ {t('pipelines.debugDialog.streaming')} + +
{hasAt && ( diff --git a/web/src/app/infra/http/HttpClient.ts b/web/src/app/infra/http/HttpClient.ts index 7c05bf09..3c04dc01 100644 --- a/web/src/app/infra/http/HttpClient.ts +++ b/web/src/app/infra/http/HttpClient.ts @@ -375,6 +375,89 @@ class HttpClient { ); } + public async sendStreamingWebChatMessage( + sessionType: string, + messageChain: object[], + pipelineId: string, + onMessage: (data: any) => void, + onComplete: () => void, + onError: (error: any) => void, + ): Promise { + try { + const url = `${this.baseURL}/api/v1/pipelines/${pipelineId}/chat/send`; + + // 使用fetch发送流式请求,因为axios在浏览器环境中不直接支持流式响应 + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + session_type: sessionType, + message: messageChain, + is_stream: true, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + if (!response.body) { + throw new Error('ReadableStream not supported'); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + // 读取流式响应 + try { + while (true) { + const { done, value } = await reader.read(); + + if (done) { + onComplete(); + break; + } + + // 解码数据 + buffer += decoder.decode(value, { stream: true }); + + // 处理完整的JSON对象 + const lines = buffer.split('\n\n'); + buffer = lines.pop() || ''; + + for (const line of lines) { + if (line.startsWith('data: ')) { + try { + const data = JSON.parse(line.slice(6)); + + if (data.type === 'end') { + // 流传输结束 + reader.cancel(); + onComplete(); + return; + } + + if (data.message) { + // 处理消息数据 + onMessage(data); + } + } catch (error) { + console.error('Error parsing streaming data:', error); + } + } + } + } + } finally { + reader.releaseLock(); + } + } catch (error) { + onError(error); + } + } + public getWebChatHistoryMessages( pipelineId: string, sessionType: string, diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index 7c306d51..2da16025 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -233,6 +233,7 @@ const enUS = { loadMessagesFailed: 'Failed to load messages', loadPipelinesFailed: 'Failed to load pipelines', atTips: 'Mention the bot', + streaming: 'Streaming', }, }, knowledge: { diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index bdd6374d..03ec8398 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -235,6 +235,7 @@ const jaJP = { loadMessagesFailed: 'メッセージの読み込みに失敗しました', loadPipelinesFailed: 'パイプラインの読み込みに失敗しました', atTips: 'ボットをメンション', + streaming: 'ストリーミング', }, }, knowledge: { diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index 5209c5e2..cb156e46 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -228,6 +228,7 @@ const zhHans = { loadMessagesFailed: '加载消息失败', loadPipelinesFailed: '加载流水线失败', atTips: '提及机器人', + streaming: '流式传输', }, }, knowledge: { From 70f23f24b08a2f964d5ff2b29d383d0c3cabdd18 Mon Sep 17 00:00:00 2001 From: fdc <2213070223@qq.com> Date: Thu, 31 Jul 2025 10:01:47 +0800 Subject: [PATCH 053/107] fix: is_stream_output_supperted in webchat return --- pkg/platform/sources/webchat.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/platform/sources/webchat.py b/pkg/platform/sources/webchat.py index 7fd7bb3b..2e8b7b99 100644 --- a/pkg/platform/sources/webchat.py +++ b/pkg/platform/sources/webchat.py @@ -51,6 +51,8 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): typing.Callable[[platform_events.Event, msadapter.MessagePlatformAdapter], None], ] = {} + is_stream: bool + def __init__(self, config: dict, ap: app.Application, logger: EventLogger): self.ap = ap self.logger = logger @@ -61,6 +63,8 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): self.bot_account_id = 'webchatbot' + self.is_stream = False + async def send_message( self, target_type: str, @@ -138,6 +142,9 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): queue.put(None) return message_data.model_dump() + + async def is_stream_output_supported(self) -> bool: + return self.is_stream def register_listener( self, @@ -172,8 +179,9 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): async def send_webchat_message( self, pipeline_uuid: str, session_type: str, message_chain_obj: typing.List[dict], - is_stream: bool = False, + is_stream: bool = False, ) -> dict: + self.is_stream = is_stream """发送调试消息到流水线""" if session_type == 'person': use_session = self.webchat_person_session From d3ab16761dad30f5d84f825be8d4808c2e318d93 Mon Sep 17 00:00:00 2001 From: fdc <2213070223@qq.com> Date: Thu, 31 Jul 2025 10:28:43 +0800 Subject: [PATCH 054/107] fix:lsome bug --- .../components/debug-dialog/DebugDialog.tsx | 47 +++++++++++-------- web/src/app/infra/http/HttpClient.ts | 20 ++++---- 2 files changed, 38 insertions(+), 29 deletions(-) diff --git a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx index 9fde4bc2..0a6330dc 100644 --- a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx +++ b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx @@ -46,6 +46,18 @@ export default function DebugDialog({ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; + const loadMessages = async (pipelineId: string) => { + try { + const response = await httpClient.getWebChatHistoryMessages( + pipelineId, + sessionType, + ); + setMessages(response.messages); + } catch (error) { + console.error('Failed to load messages:', error); + } + }; + useEffect(() => { scrollToBottom(); }, [messages]); @@ -61,7 +73,7 @@ export default function DebugDialog({ if (open) { loadMessages(selectedPipelineId); } - }, [sessionType, selectedPipelineId]); + }, [sessionType, selectedPipelineId, open, loadMessages]); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -86,18 +98,6 @@ export default function DebugDialog({ } }, [showAtPopover]); - const loadMessages = async (pipelineId: string) => { - try { - const response = await httpClient.getWebChatHistoryMessages( - pipelineId, - sessionType, - ); - setMessages(response.messages); - } catch (error) { - console.error('Failed to load messages:', error); - } - }; - const handleInputChange = (e: React.ChangeEvent) => { const value = e.target.value; if (sessionType === 'group') { @@ -179,12 +179,16 @@ export default function DebugDialog({ // 添加用户消息和初始bot消息到状态 - setMessages((prevMessages) => [...prevMessages, userMessage, botMessage]); + setMessages((prevMessages) => [ + ...prevMessages, + userMessage, + botMessage, + ]); setInputValue(''); setHasAt(false); try { - let botMessageId = botMessage.id; + const botMessageId = botMessage.id; let accumulatedContent = ''; await httpClient.sendStreamingWebChatMessage( @@ -200,14 +204,17 @@ export default function DebugDialog({ setMessages((prevMessages) => { const updatedMessages = [...prevMessages]; const botMessageIndex = updatedMessages.findIndex( - (msg) => msg.id === botMessageId && msg.role === 'assistant' + (msg) => + msg.id === botMessageId && msg.role === 'assistant', ); if (botMessageIndex !== -1) { const updatedBotMessage = { ...updatedMessages[botMessageIndex], content: accumulatedContent, - message_chain: [{ type: 'Plain', text: accumulatedContent }], + message_chain: [ + { type: 'Plain', text: accumulatedContent }, + ] }; updatedMessages[botMessageIndex] = updatedBotMessage; } @@ -226,7 +233,7 @@ export default function DebugDialog({ if (sessionType === 'person') { toast.error(t('pipelines.debugDialog.sendFailed')); } - } + }, ); } catch (error) { console.error('Failed to send streaming message:', error); @@ -378,7 +385,9 @@ export default function DebugDialog({
- {t('pipelines.debugDialog.streaming')} + + {t('pipelines.debugDialog.streaming')} + void, + onMessage: (data: ApiRespWebChatMessage) => void, onComplete: () => void, - onError: (error: any) => void, + onError: (error: Error) => void, ): Promise { try { const url = `${this.baseURL}/api/v1/pipelines/${pipelineId}/chat/send`; - + // 使用fetch发送流式请求,因为axios在浏览器环境中不直接支持流式响应 const response = await fetch(url, { method: 'POST', @@ -415,7 +415,7 @@ class HttpClient { try { while (true) { const { done, value } = await reader.read(); - + if (done) { onComplete(); break; @@ -423,23 +423,23 @@ class HttpClient { // 解码数据 buffer += decoder.decode(value, { stream: true }); - + // 处理完整的JSON对象 const lines = buffer.split('\n\n'); buffer = lines.pop() || ''; - + for (const line of lines) { if (line.startsWith('data: ')) { try { const data = JSON.parse(line.slice(6)); - + if (data.type === 'end') { // 流传输结束 reader.cancel(); onComplete(); return; } - + if (data.message) { // 处理消息数据 onMessage(data); @@ -454,7 +454,7 @@ class HttpClient { reader.releaseLock(); } } catch (error) { - onError(error); + onError(error as Error); } } From c33a96823bf4061e305bb2f440aab542008a0a07 Mon Sep 17 00:00:00 2001 From: fdc <2213070223@qq.com> Date: Thu, 31 Jul 2025 10:34:36 +0800 Subject: [PATCH 055/107] fix: frontend bug --- .../components/debug-dialog/DebugDialog.tsx | 38 +++++++++---------- web/src/app/infra/http/HttpClient.ts | 1 - 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx index 0a6330dc..45bf8b38 100644 --- a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx +++ b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { httpClient } from '@/app/infra/http/HttpClient'; import { DialogContent } from '@/components/ui/dialog'; @@ -46,7 +46,7 @@ export default function DebugDialog({ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; - const loadMessages = async (pipelineId: string) => { + const loadMessages = useCallback(async (pipelineId: string) => { try { const response = await httpClient.getWebChatHistoryMessages( pipelineId, @@ -56,7 +56,7 @@ export default function DebugDialog({ } catch (error) { console.error('Failed to load messages:', error); } - }; + }, [sessionType]); useEffect(() => { scrollToBottom(); @@ -160,12 +160,12 @@ export default function DebugDialog({ text_content = '@webchatbot' + text_content; } const userMessage: Message = { - id: -1, - role: 'user', - content: text_content, - timestamp: new Date().toISOString(), - message_chain: messageChain, - }; + id: -1, + role: 'user', + content: text_content, + timestamp: new Date().toISOString(), + message_chain: messageChain, + }; // 根据isStreaming状态决定使用哪种传输方式 if (isStreaming) { // 创建初始bot消息 @@ -199,7 +199,7 @@ export default function DebugDialog({ // 处理流式响应数据 if (data.message) { accumulatedContent += data.message; - + // 更新bot消息 setMessages((prevMessages) => { const updatedMessages = [...prevMessages]; @@ -207,18 +207,18 @@ export default function DebugDialog({ (msg) => msg.id === botMessageId && msg.role === 'assistant', ); - + if (botMessageIndex !== -1) { const updatedBotMessage = { ...updatedMessages[botMessageIndex], content: accumulatedContent, message_chain: [ { type: 'Plain', text: accumulatedContent }, - ] + ], }; updatedMessages[botMessageIndex] = updatedBotMessage; } - + return updatedMessages; }); } @@ -434,12 +434,12 @@ export default function DebugDialog({
+ onClick={sendMessage} + disabled={!inputValue.trim() && !hasAt} + className="rounded-md bg-[#2288ee] hover:bg-[#2288ee] w-20 text-white px-6 py-2 text-base font-medium transition-none flex items-center gap-2 shadow-none" + > + <>{t('pipelines.debugDialog.send')} +
diff --git a/web/src/app/infra/http/HttpClient.ts b/web/src/app/infra/http/HttpClient.ts index f2506d0d..23721537 100644 --- a/web/src/app/infra/http/HttpClient.ts +++ b/web/src/app/infra/http/HttpClient.ts @@ -43,7 +43,6 @@ import { import { GetBotLogsRequest } from '@/app/infra/http/requestParam/bots/GetBotLogsRequest'; import { GetBotLogsResponse } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse'; - type JSONValue = string | number | boolean | JSONObject | JSONArray | null; interface JSONObject { [key: string]: JSONValue; From b45cc59322930e4f3022d6da46c12d63828a9828 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Thu, 31 Jul 2025 14:49:12 +0800 Subject: [PATCH 056/107] fix:webchat stream judge bug and frontend bug --- .../controller/groups/pipelines/webchat.py | 2 +- pkg/platform/sources/webchat.py | 34 ++++++++++----- .../components/debug-dialog/DebugDialog.tsx | 43 +++++++++---------- web/src/app/infra/http/HttpClient.ts | 9 +++- 4 files changed, 54 insertions(+), 34 deletions(-) diff --git a/pkg/api/http/controller/groups/pipelines/webchat.py b/pkg/api/http/controller/groups/pipelines/webchat.py index 4baaa1e5..9982b233 100644 --- a/pkg/api/http/controller/groups/pipelines/webchat.py +++ b/pkg/api/http/controller/groups/pipelines/webchat.py @@ -15,7 +15,7 @@ class WebChatDebugRouterGroup(group.RouterGroup): async def stream_generator(generator): async for message in generator: yield rf"data:{json.dumps({'message': message})}\n\n" - yield "data:{'type': 'end'}\n\n''" + yield "data:{type: end}\n\n''" try: data = await quart.request.get_json() session_type = data.get('session_type', 'person') diff --git a/pkg/platform/sources/webchat.py b/pkg/platform/sources/webchat.py index 2e8b7b99..274c5657 100644 --- a/pkg/platform/sources/webchat.py +++ b/pkg/platform/sources/webchat.py @@ -19,6 +19,7 @@ class WebChatMessage(BaseModel): content: str message_chain: list[dict] timestamp: str + is_final: bool = False class WebChatSession: @@ -117,10 +118,10 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): async def reply_message_chunk( self, message_source: platform_events.MessageEvent, - message_id: str, + message_id: int, message: platform_message.MessageChain, quote_origin: bool = False, - is_fianl: bool = False, + is_final: bool = False, ) -> dict: """回复消息""" message_data = WebChatMessage( @@ -132,14 +133,21 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): ) # notify waiter - if isinstance(message_source, platform_events.FriendMessage): - queue = self.webchat_person_session.resp_queues[message_source.message_chain.message_id] - elif isinstance(message_source, platform_events.GroupMessage): - queue = self.webchat_group_session.resp_queues[message_source.message_chain.message_id] + session = (self.webchat_group_session if isinstance(message_source, platform_events.GroupMessage) else self.webchat_person_session) + if message_source.message_chain.message_id not in session.resp_waiters: + # session.resp_waiters[message_source.message_chain.message_id] = asyncio.Queue() + queue = session.resp_queues[message_source.message_chain.message_id] + + # if isinstance(message_source, platform_events.FriendMessage): + # queue = self.webchat_person_session.resp_queues[message_source.message_chain.message_id] + # elif isinstance(message_source, platform_events.GroupMessage): + # queue = self.webchat_group_session.resp_queues[message_source.message_chain.message_id] + if is_final: + message_data.is_final = True + # print(message_data) + await queue.put(message_data) + - queue.put(message_data) - if is_fianl: - queue.put(None) return message_data.model_dump() @@ -192,6 +200,10 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): message_id = len(use_session.get_message_list(pipeline_uuid)) + 1 + if is_stream: + use_session.resp_queues[message_id] = asyncio.Queue() + logger.debug(f"Initialized queue for message_id: {message_id}") + use_session.get_message_list(pipeline_uuid).append( WebChatMessage( id=message_id, @@ -232,9 +244,11 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): queue = use_session.resp_queues[message_id] while True: resp_message = await queue.get() - if resp_message is None: + print(resp_message) + if resp_message.is_final: resp_message.id = len(use_session.get_message_list(pipeline_uuid)) + 1 use_session.get_message_list(pipeline_uuid).append(resp_message) + yield resp_message.model_dump() break yield resp_message.model_dump() diff --git a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx index 45bf8b38..2c051b00 100644 --- a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx +++ b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx @@ -46,17 +46,20 @@ export default function DebugDialog({ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; - const loadMessages = useCallback(async (pipelineId: string) => { - try { - const response = await httpClient.getWebChatHistoryMessages( - pipelineId, - sessionType, - ); - setMessages(response.messages); - } catch (error) { - console.error('Failed to load messages:', error); - } - }, [sessionType]); + const loadMessages = useCallback( + async (pipelineId: string) => { + try { + const response = await httpClient.getWebChatHistoryMessages( + pipelineId, + sessionType, + ); + setMessages(response.messages); + } catch (error) { + console.error('Failed to load messages:', error); + } + }, + [sessionType], + ); useEffect(() => { scrollToBottom(); @@ -242,7 +245,6 @@ export default function DebugDialog({ } } } else { - setMessages((prevMessages) => [...prevMessages, userMessage]); setInputValue(''); setHasAt(false); @@ -388,10 +390,7 @@ export default function DebugDialog({ {t('pipelines.debugDialog.streaming')} - +
{hasAt && ( @@ -434,12 +433,12 @@ export default function DebugDialog({
+ onClick={sendMessage} + disabled={!inputValue.trim() && !hasAt} + className="rounded-md bg-[#2288ee] hover:bg-[#2288ee] w-20 text-white px-6 py-2 text-base font-medium transition-none flex items-center gap-2 shadow-none" + > + <>{t('pipelines.debugDialog.send')} + diff --git a/web/src/app/infra/http/HttpClient.ts b/web/src/app/infra/http/HttpClient.ts index 23721537..0cd55930 100644 --- a/web/src/app/infra/http/HttpClient.ts +++ b/web/src/app/infra/http/HttpClient.ts @@ -383,13 +383,20 @@ class HttpClient { onError: (error: Error) => void, ): Promise { try { - const url = `${this.baseURL}/api/v1/pipelines/${pipelineId}/chat/send`; + // 构造完整的URL,处理相对路径的情况 + let url = `${this.baseURL}/api/v1/pipelines/${pipelineId}/chat/send`; + if (this.baseURL === '/') { + // 获取用户访问的完整URL + const baseURL = window.location.origin; + url = `${baseURL}/api/v1/pipelines/${pipelineId}/chat/send`; + } // 使用fetch发送流式请求,因为axios在浏览器环境中不直接支持流式响应 const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', + Authorization: `Bearer ${this.getSessionSync()}`, }, body: JSON.stringify({ session_type: sessionType, From c6deed4e6e79c7827e805345b01747cdf57ab846 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Fri, 1 Aug 2025 11:33:16 +0800 Subject: [PATCH 057/107] feat: webchat stream is ok --- pkg/api/http/controller/groups/pipelines/webchat.py | 4 ++-- pkg/platform/sources/webchat.py | 5 +++-- .../components/debug-dialog/DebugDialog.tsx | 12 +++++++----- web/src/app/infra/http/HttpClient.ts | 6 ++---- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/pkg/api/http/controller/groups/pipelines/webchat.py b/pkg/api/http/controller/groups/pipelines/webchat.py index 9982b233..6dc7f85a 100644 --- a/pkg/api/http/controller/groups/pipelines/webchat.py +++ b/pkg/api/http/controller/groups/pipelines/webchat.py @@ -14,8 +14,8 @@ class WebChatDebugRouterGroup(group.RouterGroup): async def stream_generator(generator): async for message in generator: - yield rf"data:{json.dumps({'message': message})}\n\n" - yield "data:{type: end}\n\n''" + yield f"data: {json.dumps({'message': message})}\n\n" + yield "data: {\"type\": \"end\"}\n\n" try: data = await quart.request.get_json() session_type = data.get('session_type', 'person') diff --git a/pkg/platform/sources/webchat.py b/pkg/platform/sources/webchat.py index 274c5657..f7f3d964 100644 --- a/pkg/platform/sources/webchat.py +++ b/pkg/platform/sources/webchat.py @@ -242,11 +242,12 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): if is_stream: queue = use_session.resp_queues[message_id] + msg_id = len(use_session.get_message_list(pipeline_uuid)) + 1 while True: resp_message = await queue.get() - print(resp_message) + resp_message.id = msg_id if resp_message.is_final: - resp_message.id = len(use_session.get_message_list(pipeline_uuid)) + 1 + resp_message.id = msg_id use_session.get_message_list(pipeline_uuid).append(resp_message) yield resp_message.model_dump() break diff --git a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx index 2c051b00..7b0607e4 100644 --- a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx +++ b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx @@ -201,15 +201,17 @@ export default function DebugDialog({ (data) => { // 处理流式响应数据 if (data.message) { - accumulatedContent += data.message; + accumulatedContent += data.message.content; // 更新bot消息 setMessages((prevMessages) => { const updatedMessages = [...prevMessages]; - const botMessageIndex = updatedMessages.findIndex( - (msg) => - msg.id === botMessageId && msg.role === 'assistant', - ); + // const botMessageIndex = updatedMessages.findIndex( + // (msg) => + // msg.id === botMessageId && msg.role === 'assistant', + // ); + // 使用索引来更新消息,而不是id匹配 + const botMessageIndex = updatedMessages.length - 1; if (botMessageIndex !== -1) { const updatedBotMessage = { diff --git a/web/src/app/infra/http/HttpClient.ts b/web/src/app/infra/http/HttpClient.ts index 0cd55930..aeabc063 100644 --- a/web/src/app/infra/http/HttpClient.ts +++ b/web/src/app/infra/http/HttpClient.ts @@ -359,14 +359,12 @@ class HttpClient { messageChain: object[], pipelineId: string, timeout: number = 15000, - is_stream: boolean = false, ): Promise { return this.post( `/api/v1/pipelines/${pipelineId}/chat/send`, { session_type: sessionType, message: messageChain, - is_stream: is_stream, }, { timeout, @@ -432,10 +430,10 @@ class HttpClient { // 处理完整的JSON对象 const lines = buffer.split('\n\n'); - buffer = lines.pop() || ''; + buffer = ''; for (const line of lines) { - if (line.startsWith('data: ')) { + if (line.startsWith('data:')) { try { const data = JSON.parse(line.slice(6)); From 68906c43ffb4f61e42df7f3cca72a05b1ecf5239 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Sat, 2 Aug 2025 01:42:22 +0800 Subject: [PATCH 058/107] feat: add webchat Word-by-word output fix:webchat on message stream bug --- .../components/debug-dialog/DebugDialog.tsx | 112 +++++++++++++----- web/src/app/infra/http/HttpClient.ts | 4 +- 2 files changed, 84 insertions(+), 32 deletions(-) diff --git a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx index 7b0607e4..c45a7085 100644 --- a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx +++ b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx @@ -42,9 +42,24 @@ export default function DebugDialog({ const inputRef = useRef(null); const popoverRef = useRef(null); - const scrollToBottom = () => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }; + // const scrollToBottom = () => { + // messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + // }; + + const scrollToBottom = useCallback(() => { + // 使用setTimeout确保在DOM更新后执行滚动 + setTimeout(() => { + const scrollArea = document.querySelector('.scroll-area') as HTMLElement; + if (scrollArea) { + scrollArea.scrollTo({ + top: scrollArea.scrollHeight, + behavior: 'smooth', + }); + } + // 同时确保messagesEndRef也滚动到视图 + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, 0); + }, []); const loadMessages = useCallback( async (pipelineId: string) => { @@ -60,10 +75,10 @@ export default function DebugDialog({ }, [sessionType], ); - + // 在useEffect中监听messages变化时滚动 useEffect(() => { scrollToBottom(); - }, [messages]); + }, [messages, scrollToBottom]); useEffect(() => { if (open) { @@ -175,7 +190,7 @@ export default function DebugDialog({ const botMessage: Message = { id: -1, role: 'assistant', - content: '', + content: '生成中...', timestamp: new Date().toISOString(), message_chain: [{ type: 'Plain', text: '' }], }; @@ -191,8 +206,9 @@ export default function DebugDialog({ setHasAt(false); try { - const botMessageId = botMessage.id; - let accumulatedContent = ''; + let fullContent = ''; // 保存完整内容 + let displayContent = ''; // 当前显示内容 + let typingInterval: NodeJS.Timeout; await httpClient.sendStreamingWebChatMessage( sessionType, @@ -201,40 +217,76 @@ export default function DebugDialog({ (data) => { // 处理流式响应数据 if (data.message) { - accumulatedContent += data.message.content; + // 更新完整内容 + fullContent = data.message.content; - // 更新bot消息 - setMessages((prevMessages) => { - const updatedMessages = [...prevMessages]; - // const botMessageIndex = updatedMessages.findIndex( - // (msg) => - // msg.id === botMessageId && msg.role === 'assistant', - // ); - // 使用索引来更新消息,而不是id匹配 - const botMessageIndex = updatedMessages.length - 1; + // 清除之前的打字效果 + if (typingInterval) { + clearInterval(typingInterval); + } - if (botMessageIndex !== -1) { - const updatedBotMessage = { - ...updatedMessages[botMessageIndex], - content: accumulatedContent, - message_chain: [ - { type: 'Plain', text: accumulatedContent }, - ], - }; - updatedMessages[botMessageIndex] = updatedBotMessage; + // 开始新的打字效果 + let currentPos = displayContent.length; + const targetContent = fullContent; + + typingInterval = setInterval(() => { + if (currentPos < targetContent.length) { + displayContent = targetContent.substring(0, currentPos + 1); + currentPos++; + + // 更新bot消息 + setMessages((prevMessages) => { + const updatedMessages = [...prevMessages]; + const botMessageIndex = updatedMessages.length - 1; + + if (botMessageIndex !== -1) { + const updatedBotMessage = { + ...updatedMessages[botMessageIndex], + content: displayContent, + message_chain: [ + { type: 'Plain', text: displayContent }, + ], + }; + updatedMessages[botMessageIndex] = updatedBotMessage; + } + setTimeout(scrollToBottom, 0); // 确保在状态更新后滚动 + return updatedMessages; + }); + } else { + clearInterval(typingInterval); } - - return updatedMessages; - }); + }, 30); // 调整这个值可以改变打字速度 } }, () => { // 流传输完成 console.log('Streaming completed'); + if (typingInterval) { + clearInterval(typingInterval); + } + // 确保最终内容完全显示 + setMessages((prevMessages) => { + const updatedMessages = [...prevMessages]; + const botMessageIndex = updatedMessages.length - 1; + + if (botMessageIndex !== -1) { + const updatedBotMessage = { + ...updatedMessages[botMessageIndex], + content: fullContent, + message_chain: [{ type: 'Plain', text: fullContent }], + }; + updatedMessages[botMessageIndex] = updatedBotMessage; + } + setTimeout(scrollToBottom, 0); // 确保在状态更新后滚动 + return updatedMessages; + }); }, (error) => { // 处理错误 console.error('Streaming error:', error); + if (typingInterval) { + clearInterval(typingInterval); + } if (sessionType === 'person') { toast.error(t('pipelines.debugDialog.sendFailed')); } diff --git a/web/src/app/infra/http/HttpClient.ts b/web/src/app/infra/http/HttpClient.ts index aeabc063..f6ff6a50 100644 --- a/web/src/app/infra/http/HttpClient.ts +++ b/web/src/app/infra/http/HttpClient.ts @@ -430,12 +430,12 @@ class HttpClient { // 处理完整的JSON对象 const lines = buffer.split('\n\n'); - buffer = ''; + buffer = lines.pop() || ''; for (const line of lines) { if (line.startsWith('data:')) { try { - const data = JSON.parse(line.slice(6)); + const data = JSON.parse(line.slice(5)); if (data.type === 'end') { // 流传输结束 From 47ff883fc73c652b5604048729e83ae0a8ce3b59 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 3 Aug 2025 13:08:51 +0800 Subject: [PATCH 059/107] perf: ruff format & remove `stream` params in requester --- libs/dingtalk_api/api.py | 30 ++--- .../controller/groups/pipelines/webchat.py | 20 ++-- pkg/core/entities.py | 4 +- pkg/pipeline/cntfilter/cntfilter.py | 2 +- pkg/pipeline/process/handlers/chat.py | 4 +- pkg/pipeline/respback/respback.py | 4 - pkg/platform/adapter.py | 17 ++- pkg/platform/sources/dingtalk.py | 8 +- pkg/platform/sources/discord.py | 1 - pkg/platform/sources/qqbotpy.py | 2 +- pkg/platform/sources/telegram.py | 1 - pkg/platform/sources/webchat.py | 17 ++- pkg/platform/sources/wechatpad.py | 13 +- pkg/platform/types/message.py | 17 +-- pkg/provider/entities.py | 4 +- pkg/provider/modelmgr/requester.py | 17 ++- pkg/provider/modelmgr/requesters/chatcmpl.py | 78 ++++++------ .../modelmgr/requesters/giteeaichatcmpl.py | 99 +++++++--------- .../modelmgr/requesters/modelscopechatcmpl.py | 96 +++++++-------- .../modelmgr/requesters/ppiochatcmpl.py | 111 +++++++++--------- pkg/provider/runners/difysvapi.py | 4 +- pkg/provider/runners/localagent.py | 3 +- pkg/utils/image.py | 8 +- pkg/utils/importutil.py | 2 +- 24 files changed, 263 insertions(+), 299 deletions(-) diff --git a/libs/dingtalk_api/api.py b/libs/dingtalk_api/api.py index d1c7065f..3d483a3a 100644 --- a/libs/dingtalk_api/api.py +++ b/libs/dingtalk_api/api.py @@ -3,7 +3,6 @@ import json import time from typing import Callable import dingtalk_stream # type: ignore -from dingtalk_stream import AckMessage, ChatbotHandler, CallbackHandler, CallbackMessage, ChatbotMessage, AICardReplier from .EchoHandler import EchoTextHandler from .dingtalkevent import DingTalkEvent import httpx @@ -254,24 +253,23 @@ class DingTalkClient: await self.logger.error(f'failed to send proactive massage to group: {traceback.format_exc()}') raise Exception(f'failed to send proactive massage to group: {traceback.format_exc()}') - async def create_and_card(self, temp_card_id: str, incoming_message: dingtalk_stream.ChatbotMessage,quote_origin:bool=False): - content_key = "content" - card_data = {content_key: ""} + async def create_and_card( + self, temp_card_id: str, incoming_message: dingtalk_stream.ChatbotMessage, quote_origin: bool = False + ): + content_key = 'content' + card_data = {content_key: ''} - card_instance = dingtalk_stream.AICardReplier( - self.client, incoming_message - ) + card_instance = dingtalk_stream.AICardReplier(self.client, incoming_message) # print(card_instance) # 先投放卡片: https://open.dingtalk.com/document/orgapp/create-and-deliver-cards card_instance_id = await card_instance.async_create_and_deliver_card( - temp_card_id, card_data, + temp_card_id, + card_data, ) - return card_instance,card_instance_id + return card_instance, card_instance_id - async def send_card_message(self, - card_instance, - card_instance_id: str,content: str,is_final: bool): - content_key = "content" + async def send_card_message(self, card_instance, card_instance_id: str, content: str, is_final: bool): + content_key = 'content' try: await card_instance.async_streaming( card_instance_id, @@ -286,16 +284,12 @@ class DingTalkClient: await card_instance.async_streaming( card_instance_id, content_key=content_key, - content_value="", + content_value='', append=False, finished=is_final, failed=True, ) - - - - async def start(self): """启动 WebSocket 连接,监听消息""" await self.client.start() diff --git a/pkg/api/http/controller/groups/pipelines/webchat.py b/pkg/api/http/controller/groups/pipelines/webchat.py index 6dc7f85a..c094731b 100644 --- a/pkg/api/http/controller/groups/pipelines/webchat.py +++ b/pkg/api/http/controller/groups/pipelines/webchat.py @@ -14,8 +14,9 @@ class WebChatDebugRouterGroup(group.RouterGroup): async def stream_generator(generator): async for message in generator: - yield f"data: {json.dumps({'message': message})}\n\n" - yield "data: {\"type\": \"end\"}\n\n" + yield f'data: {json.dumps({"message": message})}\n\n' + yield 'data: {"type": "end"}\n\n' + try: data = await quart.request.get_json() session_type = data.get('session_type', 'person') @@ -34,18 +35,18 @@ class WebChatDebugRouterGroup(group.RouterGroup): return self.http_status(404, -1, 'WebChat adapter not found') if is_stream: - - generator = webchat_adapter.send_webchat_message(pipeline_uuid, session_type, message_chain_obj, is_stream) - - return quart.Response( - stream_generator(generator), - mimetype='text/event-stream' + generator = webchat_adapter.send_webchat_message( + pipeline_uuid, session_type, message_chain_obj, is_stream ) + return quart.Response(stream_generator(generator), mimetype='text/event-stream') + else: # result = await webchat_adapter.send_webchat_message(pipeline_uuid, session_type, message_chain_obj) result = None - async for message in webchat_adapter.send_webchat_message(pipeline_uuid, session_type, message_chain_obj): + async for message in webchat_adapter.send_webchat_message( + pipeline_uuid, session_type, message_chain_obj + ): result = message if result is not None: return self.success( @@ -56,7 +57,6 @@ class WebChatDebugRouterGroup(group.RouterGroup): else: return self.http_status(400, -1, 'message is required') - except Exception as e: return self.http_status(500, -1, f'Internal server error: {str(e)}') diff --git a/pkg/core/entities.py b/pkg/core/entities.py index 31514fa8..5f357d78 100644 --- a/pkg/core/entities.py +++ b/pkg/core/entities.py @@ -87,7 +87,9 @@ class Query(pydantic.BaseModel): """使用的函数,由前置处理器阶段设置""" resp_messages: ( - typing.Optional[list[llm_entities.Message]] | typing.Optional[list[platform_message.MessageChain]] | typing.Optional[list[llm_entities.MessageChunk]] + typing.Optional[list[llm_entities.Message]] + | typing.Optional[list[platform_message.MessageChain]] + | typing.Optional[list[llm_entities.MessageChunk]] ) = [] """由Process阶段生成的回复消息对象列表""" diff --git a/pkg/pipeline/cntfilter/cntfilter.py b/pkg/pipeline/cntfilter/cntfilter.py index 0bbc5103..e035c1d0 100644 --- a/pkg/pipeline/cntfilter/cntfilter.py +++ b/pkg/pipeline/cntfilter/cntfilter.py @@ -67,7 +67,7 @@ class ContentFilterStage(stage.PipelineStage): if query.pipeline_config['safety']['content-filter']['scope'] == 'output-msg': return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) if not message.strip(): - return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) + return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) else: for filter in self.filter_chain: if filter_entities.EnableStage.PRE in filter.enable_stages: diff --git a/pkg/pipeline/process/handlers/chat.py b/pkg/pipeline/process/handlers/chat.py index 0f802658..6c428473 100644 --- a/pkg/pipeline/process/handlers/chat.py +++ b/pkg/pipeline/process/handlers/chat.py @@ -81,9 +81,7 @@ class ChatMessageHandler(handler.MessageHandler): query.resp_message_chain.pop() query.resp_messages.append(result) - self.ap.logger.info( - f'对话({query.query_id})流式响应: {self.cut_str(result.readable_str())}' - ) + self.ap.logger.info(f'对话({query.query_id})流式响应: {self.cut_str(result.readable_str())}') if result.content is not None: text_length += len(result.content) diff --git a/pkg/pipeline/respback/respback.py b/pkg/pipeline/respback/respback.py index f4153218..c7824856 100644 --- a/pkg/pipeline/respback/respback.py +++ b/pkg/pipeline/respback/respback.py @@ -3,12 +3,10 @@ from __future__ import annotations import random import asyncio -from typing_inspection.typing_objects import is_final from ...platform.types import events as platform_events from ...platform.types import message as platform_message -from ...provider import entities as llm_entities from .. import stage, entities from ...core import entities as core_entities @@ -56,6 +54,4 @@ class SendResponseBackStage(stage.PipelineStage): quote_origin=quote_origin, ) - - return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) diff --git a/pkg/platform/adapter.py b/pkg/platform/adapter.py index e4369efb..3412be3c 100644 --- a/pkg/platform/adapter.py +++ b/pkg/platform/adapter.py @@ -25,7 +25,6 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): logger: EventLogger - def __init__(self, config: dict, ap: app.Application, logger: EventLogger): """初始化适配器 @@ -80,12 +79,12 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): """ raise NotImplementedError - async def create_message_card(self, message_id:typing.Type[str,int], event:platform_events.MessageEvent) -> bool: + async def create_message_card(self, message_id: typing.Type[str, int], event: platform_events.MessageEvent) -> bool: """创建卡片消息 Args: message_id (str): 消息ID event (platform_events.MessageEvent): 消息源事件 - """ + """ return False async def is_muted(self, group_id: int) -> bool: @@ -94,8 +93,8 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): def register_listener( self, - event_type: typing.Type[platform_message.Event], - callback: typing.Callable[[platform_message.Event, MessagePlatformAdapter], None], + event_type: typing.Type[platform_events.Event], + callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], ): """注册事件监听器 @@ -107,8 +106,8 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): def unregister_listener( self, - event_type: typing.Type[platform_message.Event], - callback: typing.Callable[[platform_message.Event, MessagePlatformAdapter], None], + event_type: typing.Type[platform_events.Event], + callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], ): """注销事件监听器 @@ -167,7 +166,7 @@ class EventConverter: """事件转换器基类""" @staticmethod - def yiri2target(event: typing.Type[platform_message.Event]): + def yiri2target(event: typing.Type[platform_events.Event]): """将源平台事件转换为目标平台事件 Args: @@ -179,7 +178,7 @@ class EventConverter: raise NotImplementedError @staticmethod - def target2yiri(event: typing.Any) -> platform_message.Event: + def target2yiri(event: typing.Any) -> platform_events.Event: """将目标平台事件的调用参数转换为源平台的事件参数对象 Args: diff --git a/pkg/platform/sources/dingtalk.py b/pkg/platform/sources/dingtalk.py index 9f834f2a..8bd6e187 100644 --- a/pkg/platform/sources/dingtalk.py +++ b/pkg/platform/sources/dingtalk.py @@ -149,10 +149,10 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): quote_origin: bool = False, is_final: bool = False, ): - event = await DingTalkEventConverter.yiri2target( - message_source, - ) - incoming_message = event.incoming_message + # event = await DingTalkEventConverter.yiri2target( + # message_source, + # ) + # incoming_message = event.incoming_message # msg_id = incoming_message.message_id diff --git a/pkg/platform/sources/discord.py b/pkg/platform/sources/discord.py index c279e714..da32c7ac 100644 --- a/pkg/platform/sources/discord.py +++ b/pkg/platform/sources/discord.py @@ -8,7 +8,6 @@ import base64 import uuid import os import datetime -import io import asyncio from enum import Enum diff --git a/pkg/platform/sources/qqbotpy.py b/pkg/platform/sources/qqbotpy.py index 39c8dc8a..d4a4d526 100644 --- a/pkg/platform/sources/qqbotpy.py +++ b/pkg/platform/sources/qqbotpy.py @@ -501,7 +501,7 @@ class OfficialAdapter(adapter_model.MessagePlatformAdapter): for event_handler in event_handler_mapping[event_type]: setattr(self.bot, event_handler, wrapper) except Exception as e: - self.logger.error(f"Error in qqbotpy callback: {traceback.format_exc()}") + self.logger.error(f'Error in qqbotpy callback: {traceback.format_exc()}') raise e def unregister_listener( diff --git a/pkg/platform/sources/telegram.py b/pkg/platform/sources/telegram.py index 22ef63e8..d39bf23d 100644 --- a/pkg/platform/sources/telegram.py +++ b/pkg/platform/sources/telegram.py @@ -1,6 +1,5 @@ from __future__ import annotations -import time import telegram import telegram.ext diff --git a/pkg/platform/sources/webchat.py b/pkg/platform/sources/webchat.py index f7f3d964..fce28bc2 100644 --- a/pkg/platform/sources/webchat.py +++ b/pkg/platform/sources/webchat.py @@ -133,7 +133,11 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): ) # notify waiter - session = (self.webchat_group_session if isinstance(message_source, platform_events.GroupMessage) else self.webchat_person_session) + session = ( + self.webchat_group_session + if isinstance(message_source, platform_events.GroupMessage) + else self.webchat_person_session + ) if message_source.message_chain.message_id not in session.resp_waiters: # session.resp_waiters[message_source.message_chain.message_id] = asyncio.Queue() queue = session.resp_queues[message_source.message_chain.message_id] @@ -147,10 +151,8 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): # print(message_data) await queue.put(message_data) - - return message_data.model_dump() - + async def is_stream_output_supported(self) -> bool: return self.is_stream @@ -186,7 +188,10 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): await self.logger.info('WebChat调试适配器正在停止') async def send_webchat_message( - self, pipeline_uuid: str, session_type: str, message_chain_obj: typing.List[dict], + self, + pipeline_uuid: str, + session_type: str, + message_chain_obj: typing.List[dict], is_stream: bool = False, ) -> dict: self.is_stream = is_stream @@ -202,7 +207,7 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): if is_stream: use_session.resp_queues[message_id] = asyncio.Queue() - logger.debug(f"Initialized queue for message_id: {message_id}") + logger.debug(f'Initialized queue for message_id: {message_id}') use_session.get_message_list(pipeline_uuid).append( WebChatMessage( diff --git a/pkg/platform/sources/wechatpad.py b/pkg/platform/sources/wechatpad.py index 9bbb471d..895e77fb 100644 --- a/pkg/platform/sources/wechatpad.py +++ b/pkg/platform/sources/wechatpad.py @@ -241,8 +241,8 @@ class WeChatPadMessageConverter(adapter.MessageConverter): # self.logger.info("_handler_compound_quote", ET.tostring(xml_data, encoding='unicode')) appmsg_data = xml_data.find('.//appmsg') quote_data = '' # 引用原文 - quote_id = None # 引用消息的原发送者 - tousername = None # 接收方: 所属微信的wxid + # quote_id = None # 引用消息的原发送者 + # tousername = None # 接收方: 所属微信的wxid user_data = '' # 用户消息 sender_id = xml_data.findtext('.//fromusername') # 发送方:单聊用户/群member @@ -250,13 +250,10 @@ class WeChatPadMessageConverter(adapter.MessageConverter): if appmsg_data: user_data = appmsg_data.findtext('.//title') or '' quote_data = appmsg_data.find('.//refermsg').findtext('.//content') - quote_id = appmsg_data.find('.//refermsg').findtext('.//chatusr') + # quote_id = appmsg_data.find('.//refermsg').findtext('.//chatusr') message_list.append(platform_message.WeChatAppMsg(app_msg=ET.tostring(appmsg_data, encoding='unicode'))) - if message: - tousername = message['to_user_name']['str'] - - _ = quote_id - _ = tousername + # if message: + # tousername = message['to_user_name']['str'] if quote_data: quote_data_message_list = platform_message.MessageChain() diff --git a/pkg/platform/types/message.py b/pkg/platform/types/message.py index 7dad4145..ecd7cc96 100644 --- a/pkg/platform/types/message.py +++ b/pkg/platform/types/message.py @@ -812,12 +812,14 @@ class File(MessageComponent): def __str__(self): return f'[文件]{self.name}' + class Face(MessageComponent): """系统表情 此处将超级表情骰子/划拳,一同归类于face 当face_type为rps(划拳)时 face_id 对应的是手势 当face_type为dice(骰子)时 face_id 对应的是点数 """ + type: str = 'Face' """表情类型""" face_type: str = 'face' @@ -834,15 +836,15 @@ class Face(MessageComponent): elif self.face_type == 'rps': return f'[表情]{self.face_name}({self.rps_data(self.face_id)})' - - def rps_data(self,face_id): - rps_dict ={ - 1 : "布", - 2 : "剪刀", - 3 : "石头", + def rps_data(self, face_id): + rps_dict = { + 1: '布', + 2: '剪刀', + 3: '石头', } return rps_dict[face_id] + # ================ 个人微信专用组件 ================ @@ -971,5 +973,6 @@ class WeChatFile(MessageComponent): """文件地址""" file_base64: str = '' """base64""" + def __str__(self): - return f'[文件]{self.file_name}' \ No newline at end of file + return f'[文件]{self.file_name}' diff --git a/pkg/provider/entities.py b/pkg/provider/entities.py index df2b5487..ff1e4526 100644 --- a/pkg/provider/entities.py +++ b/pkg/provider/entities.py @@ -127,6 +127,7 @@ class Message(pydantic.BaseModel): class MessageChunk(pydantic.BaseModel): """消息""" + resp_message_id: typing.Optional[str] = None """消息id""" @@ -148,7 +149,7 @@ class MessageChunk(pydantic.BaseModel): tool_call_id: typing.Optional[str] = None # tool_calls: typing.Optional[list[ToolCallChunk]] = None - + is_final: bool = False def readable_str(self) -> str: @@ -210,6 +211,7 @@ class ToolCallChunk(pydantic.BaseModel): function: FunctionCall """函数调用""" + class Prompt(pydantic.BaseModel): """供AI使用的Prompt""" diff --git a/pkg/provider/modelmgr/requester.py b/pkg/provider/modelmgr/requester.py index fa4a9ff8..d28783b9 100644 --- a/pkg/provider/modelmgr/requester.py +++ b/pkg/provider/modelmgr/requester.py @@ -94,19 +94,18 @@ class ProviderAPIRequester(metaclass=abc.ABCMeta): extra_args (dict[str, typing.Any], optional): 额外的参数. Defaults to {}. Returns: - llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk]: 返回消息对象 + llm_entities.Message: 返回消息对象 """ pass @abc.abstractmethod async def invoke_llm_stream( - self, - query: core_entities.Query, - model: RuntimeLLMModel, - messages: typing.List[llm_entities.Message], - funcs: typing.List[tools_entities.LLMFunction] = None, - stream: bool = False, - extra_args: dict[str, typing.Any] = {}, + self, + query: core_entities.Query, + model: RuntimeLLMModel, + messages: typing.List[llm_entities.Message], + funcs: typing.List[tools_entities.LLMFunction] = None, + extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.MessageChunk: """调用API @@ -117,7 +116,7 @@ class ProviderAPIRequester(metaclass=abc.ABCMeta): extra_args (dict[str, typing.Any], optional): 额外的参数. Defaults to {}. Returns: - llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk]: 返回消息对象 + typing.AsyncGenerator[llm_entities.MessageChunk]: 返回消息对象 """ pass diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index d5c3b90a..4fcce481 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -8,7 +8,7 @@ import openai.types.chat.chat_completion as chat_completion import httpx from .. import errors, requester -from ....core import entities as core_entities, app +from ....core import entities as core_entities from ... import entities as llm_entities from ...tools import entities as tools_entities @@ -129,12 +129,10 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): req_messages: list[dict], use_model: requester.RuntimeLLMModel, use_funcs: list[tools_entities.LLMFunction] = None, - stream: bool = False, extra_args: dict[str, typing.Any] = {}, - ) ->llm_entities.MessageChunk: + ) -> llm_entities.MessageChunk: self.client.api_key = use_model.token_mgr.get_token() - args = {} args['model'] = use_model.model_entity.name @@ -158,43 +156,42 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): args['messages'] = messages - if stream: - current_content = '' - args['stream'] = True - chunk_idx = 0 - self.is_content = False - tool_calls_map: dict[str, llm_entities.ToolCall] = {} - pipeline_config = query.pipeline_config - async for chunk in self._req_stream(args, extra_body=extra_args): - # 处理流式消息 - delta_message = await self._make_msg_chunk(pipeline_config, chunk, chunk_idx) - if delta_message.content: - current_content += delta_message.content - delta_message.content = current_content - # delta_message.all_content = current_content - if delta_message.tool_calls: - for tool_call in delta_message.tool_calls: - if tool_call.id not in tool_calls_map: - tool_calls_map[tool_call.id] = llm_entities.ToolCall( - id=tool_call.id, - type=tool_call.type, - function=llm_entities.FunctionCall( - name=tool_call.function.name if tool_call.function else '', arguments='' - ), - ) - if tool_call.function and tool_call.function.arguments: - # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 - tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + current_content = '' + args['stream'] = True + chunk_idx = 0 + self.is_content = False + tool_calls_map: dict[str, llm_entities.ToolCall] = {} + pipeline_config = query.pipeline_config + async for chunk in self._req_stream(args, extra_body=extra_args): + # 处理流式消息 + delta_message = await self._make_msg_chunk(pipeline_config, chunk, chunk_idx) + if delta_message.content: + current_content += delta_message.content + delta_message.content = current_content + # delta_message.all_content = current_content + if delta_message.tool_calls: + for tool_call in delta_message.tool_calls: + if tool_call.id not in tool_calls_map: + tool_calls_map[tool_call.id] = llm_entities.ToolCall( + id=tool_call.id, + type=tool_call.type, + function=llm_entities.FunctionCall( + name=tool_call.function.name if tool_call.function else '', arguments='' + ), + ) + if tool_call.function and tool_call.function.arguments: + # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 + tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments - chunk_idx += 1 - chunk_choices = getattr(chunk, 'choices', None) - if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): - delta_message.is_final = True - delta_message.content = current_content + chunk_idx += 1 + chunk_choices = getattr(chunk, 'choices', None) + if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): + delta_message.is_final = True + delta_message.content = current_content - if chunk_idx % 64 == 0 or delta_message.is_final: - yield delta_message - # return + if chunk_idx % 64 == 0 or delta_message.is_final: + yield delta_message + # return async def _closure( self, @@ -202,7 +199,6 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): req_messages: list[dict], use_model: requester.RuntimeLLMModel, use_funcs: list[tools_entities.LLMFunction] = None, - stream: bool = False, extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message: self.client.api_key = use_model.token_mgr.get_token() @@ -317,7 +313,6 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): model: requester.RuntimeLLMModel, messages: typing.List[llm_entities.Message], funcs: typing.List[tools_entities.LLMFunction] = None, - stream: bool = False, extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.MessageChunk: req_messages = [] # req_messages 仅用于类内,外部同步由 query.messages 进行 @@ -337,7 +332,6 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): req_messages=req_messages, use_model=model, use_funcs=funcs, - stream=stream, extra_args=extra_args, ): yield item diff --git a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py index 2a618c9f..1c19a534 100644 --- a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py +++ b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py @@ -12,7 +12,6 @@ import re import openai.types.chat.chat_completion as chat_completion - class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): """Gitee AI ChatCompletions API 请求器""" @@ -20,7 +19,7 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): 'base_url': 'https://ai.gitee.com/v1', 'timeout': 120, } - is_think:bool = False + is_think: bool = False async def _closure( self, @@ -52,15 +51,14 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): pipeline_config = query.pipeline_config - message = await self._make_msg(resp,pipeline_config) + message = await self._make_msg(resp, pipeline_config) return message - async def _make_msg( - self, - chat_completion: chat_completion.ChatCompletion, - pipeline_config: dict[str, typing.Any] = {'trigger': {'misc': {'remove_think': False}}}, + self, + chat_completion: chat_completion.ChatCompletion, + pipeline_config: dict[str, typing.Any] = {'trigger': {'misc': {'remove_think': False}}}, ) -> llm_entities.Message: chatcmpl_message = chat_completion.choices[0].message.model_dump() # print(chatcmpl_message.keys(), chatcmpl_message.values()) @@ -73,23 +71,25 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): # deepseek的reasoner模型 if pipeline_config['trigger'].get('misc', '').get('remove_think'): - chatcmpl_message['content'] = re.sub(r'.*?', '', chatcmpl_message['content'], flags=re.DOTALL) + chatcmpl_message['content'] = re.sub( + r'.*?', '', chatcmpl_message['content'], flags=re.DOTALL + ) else: if reasoning_content is not None: - chatcmpl_message['content'] = '\n' + reasoning_content + '\n\n' + chatcmpl_message['content'] + chatcmpl_message['content'] = ( + '\n' + reasoning_content + '\n\n' + chatcmpl_message['content'] + ) message = llm_entities.Message(**chatcmpl_message) return message - async def _make_msg_chunk( self, pipeline_config: dict[str, typing.Any], chat_completion: chat_completion.ChatCompletion, idx: int, ) -> llm_entities.MessageChunk: - # 处理流式chunk和完整响应的差异 # print(chat_completion.choices[0]) if hasattr(chat_completion, 'choices'): @@ -104,7 +104,6 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): if 'role' not in delta or delta['role'] is None: delta['role'] = 'assistant' - reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None delta['content'] = '' if delta['content'] is None else delta['content'] @@ -115,7 +114,7 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): if delta['content'] == '': self.is_think = True delta['content'] = '' - if delta['content'] == rf'': + if delta['content'] == r'
': self.is_think = False delta['content'] = '' if not self.is_think: @@ -126,7 +125,6 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): if reasoning_content is not None: delta['content'] += reasoning_content - message = llm_entities.MessageChunk(**delta) return message @@ -137,7 +135,6 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): req_messages: list[dict], use_model: requester.RuntimeLLMModel, use_funcs: list[tools_entities.LLMFunction] = None, - stream: bool = False, extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: self.client.api_key = use_model.token_mgr.get_token() @@ -165,44 +162,38 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): args['messages'] = messages - if stream: - current_content = '' - args["stream"] = True - chunk_idx = 0 - self.is_content = False - tool_calls_map: dict[str, llm_entities.ToolCall] = {} - pipeline_config = query.pipeline_config - async for chunk in self._req_stream(args, extra_body=extra_args): - # 处理流式消息 - delta_message = await self._make_msg_chunk(pipeline_config,chunk,chunk_idx) - if delta_message.content: - current_content += delta_message.content - delta_message.content = current_content - # delta_message.all_content = current_content - if delta_message.tool_calls: - for tool_call in delta_message.tool_calls: - if tool_call.id not in tool_calls_map: - tool_calls_map[tool_call.id] = llm_entities.ToolCall( - id=tool_call.id, - type=tool_call.type, - function=llm_entities.FunctionCall( - name=tool_call.function.name if tool_call.function else '', - arguments='' - ), - ) - if tool_call.function and tool_call.function.arguments: - # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 - tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments - - - chunk_idx += 1 - chunk_choices = getattr(chunk, 'choices', None) - if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): - delta_message.is_final = True - delta_message.content = current_content - - if chunk_idx % 64 == 0 or delta_message.is_final: - - yield delta_message + current_content = '' + args['stream'] = True + chunk_idx = 0 + self.is_content = False + tool_calls_map: dict[str, llm_entities.ToolCall] = {} + pipeline_config = query.pipeline_config + async for chunk in self._req_stream(args, extra_body=extra_args): + # 处理流式消息 + delta_message = await self._make_msg_chunk(pipeline_config, chunk, chunk_idx) + if delta_message.content: + current_content += delta_message.content + delta_message.content = current_content + # delta_message.all_content = current_content + if delta_message.tool_calls: + for tool_call in delta_message.tool_calls: + if tool_call.id not in tool_calls_map: + tool_calls_map[tool_call.id] = llm_entities.ToolCall( + id=tool_call.id, + type=tool_call.type, + function=llm_entities.FunctionCall( + name=tool_call.function.name if tool_call.function else '', arguments='' + ), + ) + if tool_call.function and tool_call.function.arguments: + # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 + tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + chunk_idx += 1 + chunk_choices = getattr(chunk, 'choices', None) + if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): + delta_message.is_final = True + delta_message.content = current_content + if chunk_idx % 64 == 0 or delta_message.is_final: + yield delta_message diff --git a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py index c1888a5e..97201e47 100644 --- a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py +++ b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py @@ -165,11 +165,10 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): return message async def _req_stream( - self, - args: dict, - extra_body: dict = {}, + self, + args: dict, + extra_body: dict = {}, ) -> chat_completion.ChatCompletion: - async for chunk in await self.client.chat.completions.create(**args, extra_body=extra_body): yield chunk @@ -179,7 +178,6 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): chat_completion: chat_completion.ChatCompletion, idx: int, ) -> llm_entities.MessageChunk: - # 处理流式chunk和完整响应的差异 # print(chat_completion.choices[0]) if hasattr(chat_completion, 'choices'): @@ -195,7 +193,6 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): if 'role' not in delta or delta['role'] is None: delta['role'] = 'assistant' - reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None delta['content'] = '' if delta['content'] is None else delta['content'] @@ -203,13 +200,13 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): # deepseek的reasoner模型 if pipeline_config['trigger'].get('misc', '').get('remove_think'): - if reasoning_content is not None : + if reasoning_content is not None: pass else: delta['content'] = delta['content'] else: if reasoning_content is not None and idx == 0: - delta['content'] += f'\n{reasoning_content}' + delta['content'] += f'\n{reasoning_content}' elif reasoning_content is None: if self.is_content: delta['content'] = delta['content'] @@ -219,7 +216,6 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): else: delta['content'] += reasoning_content - message = llm_entities.MessageChunk(**delta) return message @@ -230,7 +226,6 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): req_messages: list[dict], use_model: requester.RuntimeLLMModel, use_funcs: list[tools_entities.LLMFunction] = None, - stream: bool = False, extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: self.client.api_key = use_model.token_mgr.get_token() @@ -258,48 +253,42 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): args['messages'] = messages - if stream: - current_content = '' - args["stream"] = True - chunk_idx = 0 - self.is_content = False - tool_calls_map: dict[str, llm_entities.ToolCall] = {} - pipeline_config = query.pipeline_config - async for chunk in self._req_stream(args, extra_body=extra_args): - # 处理流式消息 - delta_message = await self._make_msg_chunk(pipeline_config,chunk,chunk_idx) - if delta_message.content: - current_content += delta_message.content - delta_message.content = current_content - # delta_message.all_content = current_content - if delta_message.tool_calls: - for tool_call in delta_message.tool_calls: - if tool_call.id not in tool_calls_map: - tool_calls_map[tool_call.id] = llm_entities.ToolCall( - id=tool_call.id, - type=tool_call.type, - function=llm_entities.FunctionCall( - name=tool_call.function.name if tool_call.function else '', - arguments='' - ), - ) - if tool_call.function and tool_call.function.arguments: - # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 - tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments - - - chunk_idx += 1 - chunk_choices = getattr(chunk, 'choices', None) - if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): - delta_message.is_final = True - delta_message.content = current_content - - if chunk_idx % 64 == 0 or delta_message.is_final: - - yield delta_message - # return + current_content = '' + args['stream'] = True + chunk_idx = 0 + self.is_content = False + tool_calls_map: dict[str, llm_entities.ToolCall] = {} + pipeline_config = query.pipeline_config + async for chunk in self._req_stream(args, extra_body=extra_args): + # 处理流式消息 + delta_message = await self._make_msg_chunk(pipeline_config, chunk, chunk_idx) + if delta_message.content: + current_content += delta_message.content + delta_message.content = current_content + # delta_message.all_content = current_content + if delta_message.tool_calls: + for tool_call in delta_message.tool_calls: + if tool_call.id not in tool_calls_map: + tool_calls_map[tool_call.id] = llm_entities.ToolCall( + id=tool_call.id, + type=tool_call.type, + function=llm_entities.FunctionCall( + name=tool_call.function.name if tool_call.function else '', arguments='' + ), + ) + if tool_call.function and tool_call.function.arguments: + # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 + tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + chunk_idx += 1 + chunk_choices = getattr(chunk, 'choices', None) + if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): + delta_message.is_final = True + delta_message.content = current_content + if chunk_idx % 64 == 0 or delta_message.is_final: + yield delta_message + # return async def invoke_llm( self, @@ -340,16 +329,14 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): except openai.APIError as e: raise errors.RequesterError(f'请求错误: {e.message}') - async def invoke_llm_stream( self, query: core_entities.Query, model: requester.RuntimeLLMModel, messages: typing.List[llm_entities.Message], funcs: typing.List[tools_entities.LLMFunction] = None, - stream: bool = False, extra_args: dict[str, typing.Any] = {}, - ) -> llm_entities.MessageChunk: + ) -> llm_entities.MessageChunk: req_messages = [] # req_messages 仅用于类内,外部同步由 query.messages 进行 for m in messages: msg_dict = m.dict(exclude_none=True) @@ -367,7 +354,6 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): req_messages=req_messages, use_model=model, use_funcs=funcs, - stream=stream, extra_args=extra_args, ): yield item @@ -386,4 +372,4 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): except openai.RateLimitError as e: raise errors.RequesterError(f'请求过于频繁或余额不足: {e.message}') except openai.APIError as e: - raise errors.RequesterError(f'请求错误: {e.message}') \ No newline at end of file + raise errors.RequesterError(f'请求错误: {e.message}') diff --git a/pkg/provider/modelmgr/requesters/ppiochatcmpl.py b/pkg/provider/modelmgr/requesters/ppiochatcmpl.py index 85b321a7..46da6e01 100644 --- a/pkg/provider/modelmgr/requesters/ppiochatcmpl.py +++ b/pkg/provider/modelmgr/requesters/ppiochatcmpl.py @@ -5,8 +5,8 @@ import typing from . import chatcmpl import openai.types.chat.chat_completion as chat_completion -from .. import errors, requester -from ....core import entities as core_entities, app +from .. import requester +from ....core import entities as core_entities from ... import entities as llm_entities from ...tools import entities as tools_entities import re @@ -25,9 +25,9 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): is_think: bool = False async def _make_msg( - self, - chat_completion: chat_completion.ChatCompletion, - pipeline_config: dict[str, typing.Any] = {'trigger': {'misc': {'remove_think': False}}}, + self, + chat_completion: chat_completion.ChatCompletion, + pipeline_config: dict[str, typing.Any] = {'trigger': {'misc': {'remove_think': False}}}, ) -> llm_entities.Message: chatcmpl_message = chat_completion.choices[0].message.model_dump() # print(chatcmpl_message.keys(), chatcmpl_message.values()) @@ -40,21 +40,24 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): # deepseek的reasoner模型 if pipeline_config['trigger'].get('misc', '').get('remove_think'): - chatcmpl_message['content'] = re.sub(r'.*?', '', chatcmpl_message['content'], flags=re.DOTALL) + chatcmpl_message['content'] = re.sub( + r'.*?', '', chatcmpl_message['content'], flags=re.DOTALL + ) else: if reasoning_content is not None: - chatcmpl_message['content'] = '\n' + reasoning_content + '\n\n' + chatcmpl_message['content'] + chatcmpl_message['content'] = ( + '\n' + reasoning_content + '\n\n' + chatcmpl_message['content'] + ) message = llm_entities.Message(**chatcmpl_message) return message - async def _make_msg_chunk( - self, - pipeline_config: dict[str, typing.Any], - chat_completion: chat_completion.ChatCompletion, - idx: int, + self, + pipeline_config: dict[str, typing.Any], + chat_completion: chat_completion.ChatCompletion, + idx: int, ) -> llm_entities.MessageChunk: # 处理流式chunk和完整响应的差异 # print(chat_completion.choices[0]) @@ -80,7 +83,7 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): if '' in delta['content']: self.is_think = True delta['content'] = '' - if rf'' in delta['content']: + if r'' in delta['content']: self.is_think = False delta['content'] = '' if not self.is_think: @@ -95,15 +98,13 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): return message - async def _closure_stream( - self, - query: core_entities.Query, - req_messages: list[dict], - use_model: requester.RuntimeLLMModel, - use_funcs: list[tools_entities.LLMFunction] = None, - stream: bool = False, - extra_args: dict[str, typing.Any] = {}, + self, + query: core_entities.Query, + req_messages: list[dict], + use_model: requester.RuntimeLLMModel, + use_funcs: list[tools_entities.LLMFunction] = None, + extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: self.client.api_key = use_model.token_mgr.get_token() @@ -130,40 +131,38 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): args['messages'] = messages - if stream: - current_content = '' - args["stream"] = True - chunk_idx = 0 - self.is_content = False - tool_calls_map: dict[str, llm_entities.ToolCall] = {} - pipeline_config = query.pipeline_config - async for chunk in self._req_stream(args, extra_body=extra_args): - # 处理流式消息 - delta_message = await self._make_msg_chunk(pipeline_config, chunk, chunk_idx) - if delta_message.content: - current_content += delta_message.content - delta_message.content = current_content - # delta_message.all_content = current_content - if delta_message.tool_calls: - for tool_call in delta_message.tool_calls: - if tool_call.id not in tool_calls_map: - tool_calls_map[tool_call.id] = llm_entities.ToolCall( - id=tool_call.id, - type=tool_call.type, - function=llm_entities.FunctionCall( - name=tool_call.function.name if tool_call.function else '', - arguments='' - ), - ) - if tool_call.function and tool_call.function.arguments: - # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 - tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + current_content = '' + args['stream'] = True + chunk_idx = 0 + self.is_content = False + tool_calls_map: dict[str, llm_entities.ToolCall] = {} + pipeline_config = query.pipeline_config + async for chunk in self._req_stream(args, extra_body=extra_args): + # 处理流式消息 + delta_message = await self._make_msg_chunk(pipeline_config, chunk, chunk_idx) + if delta_message.content: + current_content += delta_message.content + delta_message.content = current_content + # delta_message.all_content = current_content + if delta_message.tool_calls: + for tool_call in delta_message.tool_calls: + if tool_call.id not in tool_calls_map: + tool_calls_map[tool_call.id] = llm_entities.ToolCall( + id=tool_call.id, + type=tool_call.type, + function=llm_entities.FunctionCall( + name=tool_call.function.name if tool_call.function else '', arguments='' + ), + ) + if tool_call.function and tool_call.function.arguments: + # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 + tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments - chunk_idx += 1 - chunk_choices = getattr(chunk, 'choices', None) - if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): - delta_message.is_final = True - delta_message.content = current_content + chunk_idx += 1 + chunk_choices = getattr(chunk, 'choices', None) + if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): + delta_message.is_final = True + delta_message.content = current_content - if chunk_idx % 64 == 0 or delta_message.is_final: - yield delta_message + if chunk_idx % 64 == 0 or delta_message.is_final: + yield delta_message diff --git a/pkg/provider/runners/difysvapi.py b/pkg/provider/runners/difysvapi.py index 8182cc54..40a3140c 100644 --- a/pkg/provider/runners/difysvapi.py +++ b/pkg/provider/runners/difysvapi.py @@ -348,7 +348,9 @@ class DifyServiceAPIRunner(runner.RequestRunner): except AttributeError: is_stream = False - batch_pending_index = 0 + _ = is_stream + + # batch_pending_index = 0 plain_text, image_ids = await self._preprocess_user_message(query) diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index 599b0b08..3ff0ce9d 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -128,8 +128,7 @@ class LocalAgentRunner(runner.RequestRunner): id=tool_call.id, type=tool_call.type, function=llm_entities.FunctionCall( - name=tool_call.function.name if tool_call.function else '', - arguments='' + name=tool_call.function.name if tool_call.function else '', arguments='' ), ) if tool_call.function and tool_call.function.arguments: diff --git a/pkg/utils/image.py b/pkg/utils/image.py index f69d29d2..d9518e12 100644 --- a/pkg/utils/image.py +++ b/pkg/utils/image.py @@ -204,9 +204,9 @@ async def get_slack_image_to_base64(pic_url: str, bot_token: str): try: async with aiohttp.ClientSession() as session: async with session.get(pic_url, headers=headers) as resp: - mime_type = resp.headers.get("Content-Type", "application/octet-stream") + mime_type = resp.headers.get('Content-Type', 'application/octet-stream') file_bytes = await resp.read() - base64_str = base64.b64encode(file_bytes).decode("utf-8") - return f"data:{mime_type};base64,{base64_str}" + base64_str = base64.b64encode(file_bytes).decode('utf-8') + return f'data:{mime_type};base64,{base64_str}' except Exception as e: - raise (e) \ No newline at end of file + raise (e) diff --git a/pkg/utils/importutil.py b/pkg/utils/importutil.py index 8acc5c45..1933d611 100644 --- a/pkg/utils/importutil.py +++ b/pkg/utils/importutil.py @@ -32,7 +32,7 @@ def import_dir(path: str): rel_path = full_path.replace(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '') rel_path = rel_path[1:] rel_path = rel_path.replace('/', '.')[:-3] - rel_path = rel_path.replace("\\",".") + rel_path = rel_path.replace('\\', '.') importlib.import_module(rel_path) From b9f768af25f895bf99bfe71320e2a2798839d00a Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 3 Aug 2025 15:30:11 +0800 Subject: [PATCH 060/107] perf: minor fixes --- pkg/platform/sources/webchat.yaml | 12 +----------- pkg/provider/entities.py | 2 -- pkg/provider/modelmgr/requester.py | 1 - pkg/provider/runners/localagent.py | 3 --- templates/metadata/pipeline/trigger.yaml | 7 +++++-- .../components/debug-dialog/DebugDialog.tsx | 12 ++++++++---- 6 files changed, 14 insertions(+), 23 deletions(-) diff --git a/pkg/platform/sources/webchat.yaml b/pkg/platform/sources/webchat.yaml index 0b1d4c29..748dfc8c 100644 --- a/pkg/platform/sources/webchat.yaml +++ b/pkg/platform/sources/webchat.yaml @@ -10,17 +10,7 @@ metadata: zh_Hans: "用于流水线调试的网页聊天适配器" icon: "" spec: - config: - - name: enable-stream-reply - label: - en_US: Enable Stream Reply Mode - zh_Hans: 启用电报流式回复模式 - description: - en_US: If enabled, the bot will use the stream of telegram reply mode - zh_Hans: 如果启用,将使用电报流式方式来回复内容 - type: boolean - required: true - default: false + config: [] execution: python: path: "webchat.py" diff --git a/pkg/provider/entities.py b/pkg/provider/entities.py index ff1e4526..9dcaffcd 100644 --- a/pkg/provider/entities.py +++ b/pkg/provider/entities.py @@ -148,8 +148,6 @@ class MessageChunk(pydantic.BaseModel): tool_call_id: typing.Optional[str] = None - # tool_calls: typing.Optional[list[ToolCallChunk]] = None - is_final: bool = False def readable_str(self) -> str: diff --git a/pkg/provider/modelmgr/requester.py b/pkg/provider/modelmgr/requester.py index d28783b9..6352b6c5 100644 --- a/pkg/provider/modelmgr/requester.py +++ b/pkg/provider/modelmgr/requester.py @@ -98,7 +98,6 @@ class ProviderAPIRequester(metaclass=abc.ABCMeta): """ pass - @abc.abstractmethod async def invoke_llm_stream( self, query: core_entities.Query, diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index 3ff0ce9d..dc8be15f 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -116,7 +116,6 @@ class LocalAgentRunner(runner.RequestRunner): query.use_llm_model, req_messages, query.use_funcs, - stream=is_stream, extra_args=query.use_llm_model.model_entity.extra_args, ): assert isinstance(msg, llm_entities.MessageChunk) @@ -178,10 +177,8 @@ class LocalAgentRunner(runner.RequestRunner): query.use_llm_model, req_messages, query.use_funcs, - stream=is_stream, extra_args=query.use_llm_model.model_entity.extra_args, ): - assert isinstance(msg, llm_entities.MessageChunk) yield msg if msg.tool_calls: for tool_call in msg.tool_calls: diff --git a/templates/metadata/pipeline/trigger.yaml b/templates/metadata/pipeline/trigger.yaml index 165e488e..08a2714b 100644 --- a/templates/metadata/pipeline/trigger.yaml +++ b/templates/metadata/pipeline/trigger.yaml @@ -134,8 +134,11 @@ stages: default: true - name: remove_think label: - en_US: remove think - zh_Hans: 删除深度思考消息 + en_US: Remove CoT + zh_Hans: 删除思维链 + description: + en_US: If enabled, LangBot will remove the LLM thought content in response + zh_Hans: 如果启用,将自动删除大模型回复中的模型思考内容 type: boolean required: true default: true diff --git a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx index c45a7085..833c98d8 100644 --- a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx +++ b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx @@ -190,7 +190,7 @@ export default function DebugDialog({ const botMessage: Message = { id: -1, role: 'assistant', - content: '生成中...', + content: 'Generating...', timestamp: new Date().toISOString(), message_chain: [{ type: 'Plain', text: '' }], }; @@ -216,6 +216,7 @@ export default function DebugDialog({ selectedPipelineId, (data) => { // 处理流式响应数据 + console.log('data', data); if (data.message) { // 更新完整内容 fullContent = data.message.content; @@ -231,8 +232,11 @@ export default function DebugDialog({ typingInterval = setInterval(() => { if (currentPos < targetContent.length) { - displayContent = targetContent.substring(0, currentPos + 1); - currentPos++; + displayContent = targetContent.substring( + 0, + currentPos + 10, + ); + currentPos += 10; // 更新bot消息 setMessages((prevMessages) => { @@ -255,7 +259,7 @@ export default function DebugDialog({ } else { clearInterval(typingInterval); } - }, 30); // 调整这个值可以改变打字速度 + }, 1); // 调整这个值可以改变打字速度 } }, () => { From c3ed4ef6a17f3d36b5ac55eae536cd54631c3d65 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 3 Aug 2025 17:18:44 +0800 Subject: [PATCH 061/107] feat: no longer use typewriter in debug dialog --- .../components/debug-dialog/DebugDialog.tsx | 104 +++++------------- 1 file changed, 25 insertions(+), 79 deletions(-) diff --git a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx index 833c98d8..8505f4f9 100644 --- a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx +++ b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx @@ -37,15 +37,11 @@ export default function DebugDialog({ const [showAtPopover, setShowAtPopover] = useState(false); const [hasAt, setHasAt] = useState(false); const [isHovering, setIsHovering] = useState(false); - const [isStreaming, setIsStreaming] = useState(false); + const [isStreaming, setIsStreaming] = useState(true); const messagesEndRef = useRef(null); const inputRef = useRef(null); const popoverRef = useRef(null); - // const scrollToBottom = () => { - // messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - // }; - const scrollToBottom = useCallback(() => { // 使用setTimeout确保在DOM更新后执行滚动 setTimeout(() => { @@ -177,6 +173,7 @@ export default function DebugDialog({ // for showing text_content = '@webchatbot' + text_content; } + const userMessage: Message = { id: -1, role: 'user', @@ -186,13 +183,15 @@ export default function DebugDialog({ }; // 根据isStreaming状态决定使用哪种传输方式 if (isStreaming) { + // streaming // 创建初始bot消息 - const botMessage: Message = { - id: -1, + const placeholderRandomId = Math.floor(Math.random() * 1000000); + const botMessagePlaceholder: Message = { + id: placeholderRandomId, role: 'assistant', content: 'Generating...', timestamp: new Date().toISOString(), - message_chain: [{ type: 'Plain', text: '' }], + message_chain: [{ type: 'Plain', text: 'Generating...' }], }; // 添加用户消息和初始bot消息到状态 @@ -200,16 +199,11 @@ export default function DebugDialog({ setMessages((prevMessages) => [ ...prevMessages, userMessage, - botMessage, + botMessagePlaceholder, ]); setInputValue(''); setHasAt(false); - try { - let fullContent = ''; // 保存完整内容 - let displayContent = ''; // 当前显示内容 - let typingInterval: NodeJS.Timeout; - await httpClient.sendStreamingWebChatMessage( sessionType, messageChain, @@ -219,78 +213,29 @@ export default function DebugDialog({ console.log('data', data); if (data.message) { // 更新完整内容 - fullContent = data.message.content; - // 清除之前的打字效果 - if (typingInterval) { - clearInterval(typingInterval); - } - - // 开始新的打字效果 - let currentPos = displayContent.length; - const targetContent = fullContent; - - typingInterval = setInterval(() => { - if (currentPos < targetContent.length) { - displayContent = targetContent.substring( - 0, - currentPos + 10, - ); - currentPos += 10; - - // 更新bot消息 - setMessages((prevMessages) => { - const updatedMessages = [...prevMessages]; - const botMessageIndex = updatedMessages.length - 1; - - if (botMessageIndex !== -1) { - const updatedBotMessage = { - ...updatedMessages[botMessageIndex], - content: displayContent, - message_chain: [ - { type: 'Plain', text: displayContent }, - ], - }; - updatedMessages[botMessageIndex] = updatedBotMessage; - } - setTimeout(scrollToBottom, 0); // 确保在状态更新后滚动 - return updatedMessages; - }); - } else { - clearInterval(typingInterval); + setMessages((prevMessages) => { + const updatedMessages = [...prevMessages]; + const botMessageIndex = updatedMessages.findIndex( + (message) => message.id === placeholderRandomId, + ); + if (botMessageIndex !== -1) { + updatedMessages[botMessageIndex] = { + ...updatedMessages[botMessageIndex], + content: data.message.content, + message_chain: [ + { type: 'Plain', text: data.message.content }, + ], + }; } - }, 1); // 调整这个值可以改变打字速度 + return updatedMessages; + }); } }, - () => { - // 流传输完成 - console.log('Streaming completed'); - if (typingInterval) { - clearInterval(typingInterval); - } - // 确保最终内容完全显示 - setMessages((prevMessages) => { - const updatedMessages = [...prevMessages]; - const botMessageIndex = updatedMessages.length - 1; - - if (botMessageIndex !== -1) { - const updatedBotMessage = { - ...updatedMessages[botMessageIndex], - content: fullContent, - message_chain: [{ type: 'Plain', text: fullContent }], - }; - updatedMessages[botMessageIndex] = updatedBotMessage; - } - setTimeout(scrollToBottom, 0); // 确保在状态更新后滚动 - return updatedMessages; - }); - }, + () => {}, (error) => { // 处理错误 console.error('Streaming error:', error); - if (typingInterval) { - clearInterval(typingInterval); - } if (sessionType === 'person') { toast.error(t('pipelines.debugDialog.sendFailed')); } @@ -303,6 +248,7 @@ export default function DebugDialog({ } } } else { + // non-streaming setMessages((prevMessages) => [...prevMessages, userMessage]); setInputValue(''); setHasAt(false); From fcef7841802955f2c90a99c68944d4670b55b975 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Sun, 3 Aug 2025 23:23:51 +0800 Subject: [PATCH 062/107] fix: In the runner, every 8 tokens yield --- pkg/provider/modelmgr/requesters/chatcmpl.py | 3 +- .../modelmgr/requesters/giteeaichatcmpl.py | 3 +- .../modelmgr/requesters/modelscopechatcmpl.py | 3 +- pkg/provider/runners/localagent.py | 34 ++++++++++--------- 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index 4fcce481..51ea864b 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -189,8 +189,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): delta_message.is_final = True delta_message.content = current_content - if chunk_idx % 64 == 0 or delta_message.is_final: - yield delta_message + yield delta_message # return async def _closure( diff --git a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py index 1c19a534..7ac9fa1a 100644 --- a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py +++ b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py @@ -195,5 +195,4 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): delta_message.is_final = True delta_message.content = current_content - if chunk_idx % 64 == 0 or delta_message.is_final: - yield delta_message + yield delta_message diff --git a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py index 97201e47..04987c19 100644 --- a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py +++ b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py @@ -286,8 +286,7 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): delta_message.is_final = True delta_message.content = current_content - if chunk_idx % 64 == 0 or delta_message.is_final: - yield delta_message + yield delta_message # return async def invoke_llm( diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index dc8be15f..0d7bdd0a 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -111,15 +111,17 @@ class LocalAgentRunner(runner.RequestRunner): else: # 流式输出,需要处理工具调用 tool_calls_map: dict[str, llm_entities.ToolCall] = {} + msg_idx = 0 async for msg in query.use_llm_model.requester.invoke_llm_stream( query, query.use_llm_model, req_messages, query.use_funcs, extra_args=query.use_llm_model.model_entity.extra_args, - ): - assert isinstance(msg, llm_entities.MessageChunk) - yield msg + ): + msg_idx = msg_idx + 1 + if msg_idx % 8 == 0 or msg.is_final: + yield msg if msg.tool_calls: for tool_call in msg.tool_calls: if tool_call.id not in tool_calls_map: @@ -180,19 +182,19 @@ class LocalAgentRunner(runner.RequestRunner): extra_args=query.use_llm_model.model_entity.extra_args, ): yield msg - if msg.tool_calls: - for tool_call in msg.tool_calls: - if tool_call.id not in tool_calls_map: - tool_calls_map[tool_call.id] = llm_entities.ToolCall( - id=tool_call.id, - type=tool_call.type, - function=llm_entities.FunctionCall( - name=tool_call.function.name if tool_call.function else '', arguments='' - ), - ) - if tool_call.function and tool_call.function.arguments: - # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 - tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + if msg.tool_calls: + for tool_call in msg.tool_calls: + if tool_call.id not in tool_calls_map: + tool_calls_map[tool_call.id] = llm_entities.ToolCall( + id=tool_call.id, + type=tool_call.type, + function=llm_entities.FunctionCall( + name=tool_call.function.name if tool_call.function else '', arguments='' + ), + ) + if tool_call.function and tool_call.function.arguments: + # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 + tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments final_msg = llm_entities.Message( role=msg.role, content=msg.all_content, From a62b38eda7741152ac9d742070c645579be2a502 Mon Sep 17 00:00:00 2001 From: fdc <2213070223@qq.com> Date: Mon, 4 Aug 2025 16:33:13 +0800 Subject: [PATCH 063/107] fix: In the reply_message_chunk of the adapter, the message is only streamed into the card or edited at the end of the 8th chunk return or streaming --- pkg/platform/sources/dingtalk.py | 21 ++++--- pkg/platform/sources/telegram.py | 95 +++++++++++++++++--------------- 2 files changed, 64 insertions(+), 52 deletions(-) diff --git a/pkg/platform/sources/dingtalk.py b/pkg/platform/sources/dingtalk.py index 8bd6e187..eacc2a23 100644 --- a/pkg/platform/sources/dingtalk.py +++ b/pkg/platform/sources/dingtalk.py @@ -1,3 +1,4 @@ +from re import S import traceback import typing from libs.dingtalk_api.dingtalkevent import DingTalkEvent @@ -99,13 +100,15 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): message_converter: DingTalkMessageConverter = DingTalkMessageConverter() event_converter: DingTalkEventConverter = DingTalkEventConverter() config: dict - card_instance_id_dict: dict + card_instance_id_dict: dict # 回复卡片消息字典,key为消息id,value为回复卡片实例id,用于在流式消息时判断是否发送到指定卡片 + seq: int # 消息顺序,直接以seq作为标识 def __init__(self, config: dict, ap: app.Application, logger: EventLogger): self.config = config self.ap = ap self.logger = logger self.card_instance_id_dict = {} + self.seq = 1 required_keys = [ 'client_id', 'client_secret', @@ -155,14 +158,16 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): # incoming_message = event.incoming_message # msg_id = incoming_message.message_id + self.seq += 1 + if (self.seq - 1) % 8 == 0 or is_final: + content, at = await DingTalkMessageConverter.yiri2target(message) - content, at = await DingTalkMessageConverter.yiri2target(message) - - card_instance, card_instance_id = self.card_instance_id_dict[message_id] - # print(card_instance_id) - await self.bot.send_card_message(card_instance, card_instance_id, content, is_final) - if is_final: - self.card_instance_id_dict.pop(message_id) + card_instance, card_instance_id = self.card_instance_id_dict[message_id] + # print(card_instance_id) + await self.bot.send_card_message(card_instance, card_instance_id, content, is_final) + if is_final: + self.seq = 1 # 消息回复结束之后重置seq + self.card_instance_id_dict.pop(message_id) # 消息回复结束之后删除卡片实例id async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): content = await DingTalkMessageConverter.yiri2target(message) diff --git a/pkg/platform/sources/telegram.py b/pkg/platform/sources/telegram.py index d39bf23d..3c81fd6b 100644 --- a/pkg/platform/sources/telegram.py +++ b/pkg/platform/sources/telegram.py @@ -144,7 +144,9 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): config: dict ap: app.Application - msg_stream_id: dict + msg_stream_id: dict # 流式消息id字典,key为流式消息id,value为首次消息源id,用于在流式消息时判断编辑那条消息 + + seq: int # 消息中识别消息顺序,直接以seq作为标识 listeners: typing.Dict[ typing.Type[platform_events.Event], @@ -156,6 +158,7 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): self.ap = ap self.logger = logger self.msg_stream_id = {} + self.seq = 1 async def telegram_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): if update.message.from_user.is_bot: @@ -213,52 +216,56 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): quote_origin: bool = False, is_final: bool = False, ): - assert isinstance(message_source.source_platform_object, Update) - components = await TelegramMessageConverter.yiri2target(message, self.bot) - args = {} - message_id = message_source.source_platform_object.message.id - if quote_origin: - args['reply_to_message_id'] = message_source.source_platform_object.message.id + self.seq += 1 + if (self.seq - 1) % 8 == 0 or is_final: - component = components[0] - if message_id not in self.msg_stream_id: - # time.sleep(0.6) - if component['type'] == 'text': - if self.config['markdown_card'] is True: - content = telegramify_markdown.markdownify( - content=component['text'], - ) - else: - content = component['text'] - args = { - 'chat_id': message_source.source_platform_object.effective_chat.id, - 'text': content, - } - if self.config['markdown_card'] is True: - args['parse_mode'] = 'MarkdownV2' + assert isinstance(message_source.source_platform_object, Update) + components = await TelegramMessageConverter.yiri2target(message, self.bot) + args = {} + message_id = message_source.source_platform_object.message.id + if quote_origin: + args['reply_to_message_id'] = message_source.source_platform_object.message.id - send_msg = await self.bot.send_message(**args) - send_msg_id = send_msg.message_id - self.msg_stream_id[message_id] = send_msg_id - else: - if component['type'] == 'text': - if self.config['markdown_card'] is True: - content = telegramify_markdown.markdownify( - content=component['text'], - ) - else: - content = component['text'] - args = { - 'message_id': self.msg_stream_id[message_id], - 'chat_id': message_source.source_platform_object.effective_chat.id, - 'text': content, - } - if self.config['markdown_card'] is True: - args['parse_mode'] = 'MarkdownV2' + component = components[0] + if message_id not in self.msg_stream_id: # 当消息回复第一次时,发送新消息 + # time.sleep(0.6) + if component['type'] == 'text': + if self.config['markdown_card'] is True: + content = telegramify_markdown.markdownify( + content=component['text'], + ) + else: + content = component['text'] + args = { + 'chat_id': message_source.source_platform_object.effective_chat.id, + 'text': content, + } + if self.config['markdown_card'] is True: + args['parse_mode'] = 'MarkdownV2' - await self.bot.edit_message_text(**args) - if is_final: - self.msg_stream_id.pop(message_id) + send_msg = await self.bot.send_message(**args) + send_msg_id = send_msg.message_id + self.msg_stream_id[message_id] = send_msg_id + else: # 存在消息的时候直接编辑消息1 + if component['type'] == 'text': + if self.config['markdown_card'] is True: + content = telegramify_markdown.markdownify( + content=component['text'], + ) + else: + content = component['text'] + args = { + 'message_id': self.msg_stream_id[message_id], + 'chat_id': message_source.source_platform_object.effective_chat.id, + 'text': content, + } + if self.config['markdown_card'] is True: + args['parse_mode'] = 'MarkdownV2' + + await self.bot.edit_message_text(**args) + if is_final: + self.seq = 1 # 消息回复结束之后重置seq + self.msg_stream_id.pop(message_id) # 消息回复结束之后删除流式消息id async def is_stream_output_supported(self) -> bool: is_stream = False From 8adc88a8c0ab426f69a539bf45df6132a7664510 Mon Sep 17 00:00:00 2001 From: fdc <2213070223@qq.com> Date: Mon, 4 Aug 2025 16:36:02 +0800 Subject: [PATCH 064/107] fix:Modify the remove_think that directly retrieves the configuration file from the requester, retrieves it from the runner, and passes it to the required function --- pkg/provider/modelmgr/requester.py | 4 ++++ .../modelmgr/requesters/anthropicmsgs.py | 3 ++- pkg/provider/modelmgr/requesters/chatcmpl.py | 16 ++++++++-------- .../modelmgr/requesters/deepseekchatcmpl.py | 4 ++-- .../modelmgr/requesters/giteeaichatcmpl.py | 16 ++++++++-------- .../modelmgr/requesters/modelscopechatcmpl.py | 11 +++++++---- .../modelmgr/requesters/moonshotchatcmpl.py | 3 ++- pkg/provider/modelmgr/requesters/ollamachat.py | 2 ++ pkg/provider/modelmgr/requesters/ppiochatcmpl.py | 12 ++++++------ pkg/provider/runners/localagent.py | 6 +++++- 10 files changed, 46 insertions(+), 31 deletions(-) diff --git a/pkg/provider/modelmgr/requester.py b/pkg/provider/modelmgr/requester.py index 6352b6c5..6af8ba70 100644 --- a/pkg/provider/modelmgr/requester.py +++ b/pkg/provider/modelmgr/requester.py @@ -84,6 +84,7 @@ class ProviderAPIRequester(metaclass=abc.ABCMeta): messages: typing.List[llm_entities.Message], funcs: typing.List[tools_entities.LLMFunction] = None, extra_args: dict[str, typing.Any] = {}, + remove_think: bool = False, ) -> llm_entities.Message: """调用API @@ -92,6 +93,7 @@ class ProviderAPIRequester(metaclass=abc.ABCMeta): messages (typing.List[llm_entities.Message]): 消息对象列表 funcs (typing.List[tools_entities.LLMFunction], optional): 使用的工具函数列表. Defaults to None. extra_args (dict[str, typing.Any], optional): 额外的参数. Defaults to {}. + remove_think (bool, optional): 是否移思考中的消息. Defaults to False. Returns: llm_entities.Message: 返回消息对象 @@ -105,6 +107,7 @@ class ProviderAPIRequester(metaclass=abc.ABCMeta): messages: typing.List[llm_entities.Message], funcs: typing.List[tools_entities.LLMFunction] = None, extra_args: dict[str, typing.Any] = {}, + remove_think: bool = False, ) -> llm_entities.MessageChunk: """调用API @@ -113,6 +116,7 @@ class ProviderAPIRequester(metaclass=abc.ABCMeta): messages (typing.List[llm_entities.Message]): 消息对象列表 funcs (typing.List[tools_entities.LLMFunction], optional): 使用的工具函数列表. Defaults to None. extra_args (dict[str, typing.Any], optional): 额外的参数. Defaults to {}. + remove_think (bool, optional): 是否移除思考中的消息. Defaults to False. Returns: typing.AsyncGenerator[llm_entities.MessageChunk]: 返回消息对象 diff --git a/pkg/provider/modelmgr/requesters/anthropicmsgs.py b/pkg/provider/modelmgr/requesters/anthropicmsgs.py index b195ae51..75f2bf7e 100644 --- a/pkg/provider/modelmgr/requesters/anthropicmsgs.py +++ b/pkg/provider/modelmgr/requesters/anthropicmsgs.py @@ -53,6 +53,7 @@ class AnthropicMessages(requester.ProviderAPIRequester): messages: typing.List[llm_entities.Message], funcs: typing.List[tools_entities.LLMFunction] = None, extra_args: dict[str, typing.Any] = {}, + remove_think: bool = False, ) -> llm_entities.Message: self.client.api_key = model.token_mgr.get_token() @@ -151,7 +152,7 @@ class AnthropicMessages(requester.ProviderAPIRequester): assert type(resp) is anthropic.types.message.Message for block in resp.content: - if block.type == 'thinking': + if not remove_think and block.type == 'thinking': args['content'] = '' + block.thinking + '\n' + args['content'] elif block.type == 'text': args['content'] += block.text diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index 51ea864b..04e7da20 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -51,7 +51,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): async def _make_msg( self, chat_completion: chat_completion.ChatCompletion, - pipeline_config: dict[str, typing.Any] = {'trigger': {'misc': {'remove_think': False}}}, + remove_think: bool = False, ) -> llm_entities.Message: chatcmpl_message = chat_completion.choices[0].message.model_dump() # print(chatcmpl_message.keys(),chatcmpl_message.values()) @@ -63,7 +63,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): reasoning_content = chatcmpl_message['reasoning_content'] if 'reasoning_content' in chatcmpl_message else None # deepseek的reasoner模型 - if pipeline_config['trigger'].get('misc', '').get('remove_think'): + if remove_think: pass else: if reasoning_content is not None: @@ -77,7 +77,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): async def _make_msg_chunk( self, - pipeline_config: dict[str, typing.Any], + remove_think: bool, chat_completion: chat_completion.ChatCompletion, idx: int, ) -> llm_entities.MessageChunk: @@ -102,7 +102,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): # print(reasoning_content) # deepseek的reasoner模型 - if pipeline_config['trigger'].get('misc', '').get('remove_think'): + if remove_think: if reasoning_content is not None: pass else: @@ -130,6 +130,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): use_model: requester.RuntimeLLMModel, use_funcs: list[tools_entities.LLMFunction] = None, extra_args: dict[str, typing.Any] = {}, + remove_think: bool = False, ) -> llm_entities.MessageChunk: self.client.api_key = use_model.token_mgr.get_token() @@ -161,10 +162,9 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): chunk_idx = 0 self.is_content = False tool_calls_map: dict[str, llm_entities.ToolCall] = {} - pipeline_config = query.pipeline_config async for chunk in self._req_stream(args, extra_body=extra_args): # 处理流式消息 - delta_message = await self._make_msg_chunk(pipeline_config, chunk, chunk_idx) + delta_message = await self._make_msg_chunk(remove_think, chunk, chunk_idx) if delta_message.content: current_content += delta_message.content delta_message.content = current_content @@ -199,6 +199,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): use_model: requester.RuntimeLLMModel, use_funcs: list[tools_entities.LLMFunction] = None, extra_args: dict[str, typing.Any] = {}, + remove_think: bool = False, ) -> llm_entities.Message: self.client.api_key = use_model.token_mgr.get_token() @@ -229,8 +230,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): resp = await self._req(args, extra_body=extra_args) # 处理请求结果 - pipeline_config = query.pipeline_config - message = await self._make_msg(resp, pipeline_config) + message = await self._make_msg(resp, remove_think) return message diff --git a/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py b/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py index d75d0fb6..4866caf4 100644 --- a/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py +++ b/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py @@ -24,6 +24,7 @@ class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions): use_model: requester.RuntimeLLMModel, use_funcs: list[tools_entities.LLMFunction] = None, extra_args: dict[str, typing.Any] = {}, + remove_think: bool = False, ) -> llm_entities.Message: self.client.api_key = use_model.token_mgr.get_token() @@ -53,8 +54,7 @@ class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions): if resp is None: raise errors.RequesterError('接口返回为空,请确定模型提供商服务是否正常') - pipeline_config = query.pipeline_config # 处理请求结果 - message = await self._make_msg(resp, pipeline_config) + message = await self._make_msg(resp, remove_think) return message diff --git a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py index 7ac9fa1a..a8d6eb16 100644 --- a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py +++ b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py @@ -28,6 +28,7 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): use_model: requester.RuntimeLLMModel, use_funcs: list[tools_entities.LLMFunction] = None, extra_args: dict[str, typing.Any] = {}, + remove_think: bool = False, ) -> llm_entities.Message: self.client.api_key = use_model.token_mgr.get_token() @@ -49,16 +50,15 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): resp = await self._req(args, extra_body=extra_args) - pipeline_config = query.pipeline_config - message = await self._make_msg(resp, pipeline_config) + message = await self._make_msg(resp, remove_think) return message async def _make_msg( self, chat_completion: chat_completion.ChatCompletion, - pipeline_config: dict[str, typing.Any] = {'trigger': {'misc': {'remove_think': False}}}, + remove_think: bool, ) -> llm_entities.Message: chatcmpl_message = chat_completion.choices[0].message.model_dump() # print(chatcmpl_message.keys(), chatcmpl_message.values()) @@ -70,7 +70,7 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): reasoning_content = chatcmpl_message['reasoning_content'] if 'reasoning_content' in chatcmpl_message else None # deepseek的reasoner模型 - if pipeline_config['trigger'].get('misc', '').get('remove_think'): + if remove_think: chatcmpl_message['content'] = re.sub( r'.*?', '', chatcmpl_message['content'], flags=re.DOTALL ) @@ -86,7 +86,7 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): async def _make_msg_chunk( self, - pipeline_config: dict[str, typing.Any], + remove_think: bool, chat_completion: chat_completion.ChatCompletion, idx: int, ) -> llm_entities.MessageChunk: @@ -110,7 +110,7 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): # print(reasoning_content) # deepseek的reasoner模型 - if pipeline_config['trigger'].get('misc', '').get('remove_think'): + if remove_think: if delta['content'] == '': self.is_think = True delta['content'] = '' @@ -136,6 +136,7 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): use_model: requester.RuntimeLLMModel, use_funcs: list[tools_entities.LLMFunction] = None, extra_args: dict[str, typing.Any] = {}, + remove_think: bool = False, ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: self.client.api_key = use_model.token_mgr.get_token() @@ -167,10 +168,9 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): chunk_idx = 0 self.is_content = False tool_calls_map: dict[str, llm_entities.ToolCall] = {} - pipeline_config = query.pipeline_config async for chunk in self._req_stream(args, extra_body=extra_args): # 处理流式消息 - delta_message = await self._make_msg_chunk(pipeline_config, chunk, chunk_idx) + delta_message = await self._make_msg_chunk(remove_think, chunk, chunk_idx) if delta_message.content: current_content += delta_message.content delta_message.content = current_content diff --git a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py index 04987c19..7895a87e 100644 --- a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py +++ b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py @@ -174,7 +174,7 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): async def _make_msg_chunk( self, - pipeline_config: dict[str, typing.Any], + remove_think: bool, chat_completion: chat_completion.ChatCompletion, idx: int, ) -> llm_entities.MessageChunk: @@ -199,7 +199,7 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): # print(reasoning_content) # deepseek的reasoner模型 - if pipeline_config['trigger'].get('misc', '').get('remove_think'): + if remove_think: if reasoning_content is not None: pass else: @@ -227,6 +227,7 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): use_model: requester.RuntimeLLMModel, use_funcs: list[tools_entities.LLMFunction] = None, extra_args: dict[str, typing.Any] = {}, + remove_think: bool = False, ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: self.client.api_key = use_model.token_mgr.get_token() @@ -258,10 +259,9 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): chunk_idx = 0 self.is_content = False tool_calls_map: dict[str, llm_entities.ToolCall] = {} - pipeline_config = query.pipeline_config async for chunk in self._req_stream(args, extra_body=extra_args): # 处理流式消息 - delta_message = await self._make_msg_chunk(pipeline_config, chunk, chunk_idx) + delta_message = await self._make_msg_chunk(remove_think, chunk, chunk_idx) if delta_message.content: current_content += delta_message.content delta_message.content = current_content @@ -296,6 +296,7 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): messages: typing.List[llm_entities.Message], funcs: typing.List[tools_entities.LLMFunction] = None, extra_args: dict[str, typing.Any] = {}, + remove_think: bool = False, ) -> llm_entities.Message: req_messages = [] # req_messages 仅用于类内,外部同步由 query.messages 进行 for m in messages: @@ -335,6 +336,7 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): messages: typing.List[llm_entities.Message], funcs: typing.List[tools_entities.LLMFunction] = None, extra_args: dict[str, typing.Any] = {}, + remove_think: bool = False, ) -> llm_entities.MessageChunk: req_messages = [] # req_messages 仅用于类内,外部同步由 query.messages 进行 for m in messages: @@ -354,6 +356,7 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): use_model=model, use_funcs=funcs, extra_args=extra_args, + remove_think=remove_think, ): yield item diff --git a/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py b/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py index f3621a09..b8c0e950 100644 --- a/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py +++ b/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py @@ -25,6 +25,7 @@ class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions): use_model: requester.RuntimeLLMModel, use_funcs: list[tools_entities.LLMFunction] = None, extra_args: dict[str, typing.Any] = {}, + remove_think: bool = False, ) -> llm_entities.Message: self.client.api_key = use_model.token_mgr.get_token() @@ -54,6 +55,6 @@ class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions): resp = await self._req(args, extra_body=extra_args) # 处理请求结果 - message = await self._make_msg(resp) + message = await self._make_msg(resp,remove_think) return message diff --git a/pkg/provider/modelmgr/requesters/ollamachat.py b/pkg/provider/modelmgr/requesters/ollamachat.py index 9e6f5a77..0a8943c0 100644 --- a/pkg/provider/modelmgr/requesters/ollamachat.py +++ b/pkg/provider/modelmgr/requesters/ollamachat.py @@ -110,6 +110,7 @@ class OllamaChatCompletions(requester.ProviderAPIRequester): messages: typing.List[llm_entities.Message], funcs: typing.List[tools_entities.LLMFunction] = None, extra_args: dict[str, typing.Any] = {}, + remove_think: bool = False, ) -> llm_entities.Message: req_messages: list = [] for m in messages: @@ -126,6 +127,7 @@ class OllamaChatCompletions(requester.ProviderAPIRequester): use_model=model, use_funcs=funcs, extra_args=extra_args, + remove_think=remove_think, ) except asyncio.TimeoutError: raise errors.RequesterError('请求超时') diff --git a/pkg/provider/modelmgr/requesters/ppiochatcmpl.py b/pkg/provider/modelmgr/requesters/ppiochatcmpl.py index 46da6e01..ca49df10 100644 --- a/pkg/provider/modelmgr/requesters/ppiochatcmpl.py +++ b/pkg/provider/modelmgr/requesters/ppiochatcmpl.py @@ -27,7 +27,7 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): async def _make_msg( self, chat_completion: chat_completion.ChatCompletion, - pipeline_config: dict[str, typing.Any] = {'trigger': {'misc': {'remove_think': False}}}, + remove_think: bool, ) -> llm_entities.Message: chatcmpl_message = chat_completion.choices[0].message.model_dump() # print(chatcmpl_message.keys(), chatcmpl_message.values()) @@ -39,7 +39,7 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): reasoning_content = chatcmpl_message['reasoning_content'] if 'reasoning_content' in chatcmpl_message else None # deepseek的reasoner模型 - if pipeline_config['trigger'].get('misc', '').get('remove_think'): + if remove_think: chatcmpl_message['content'] = re.sub( r'.*?', '', chatcmpl_message['content'], flags=re.DOTALL ) @@ -55,7 +55,7 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): async def _make_msg_chunk( self, - pipeline_config: dict[str, typing.Any], + remove_think: bool, chat_completion: chat_completion.ChatCompletion, idx: int, ) -> llm_entities.MessageChunk: @@ -79,7 +79,7 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): # print(reasoning_content) # deepseek的reasoner模型 - if pipeline_config['trigger'].get('misc', '').get('remove_think'): + if remove_think: if '' in delta['content']: self.is_think = True delta['content'] = '' @@ -105,6 +105,7 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): use_model: requester.RuntimeLLMModel, use_funcs: list[tools_entities.LLMFunction] = None, extra_args: dict[str, typing.Any] = {}, + remove_think: bool = False, ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: self.client.api_key = use_model.token_mgr.get_token() @@ -136,10 +137,9 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): chunk_idx = 0 self.is_content = False tool_calls_map: dict[str, llm_entities.ToolCall] = {} - pipeline_config = query.pipeline_config async for chunk in self._req_stream(args, extra_body=extra_args): # 处理流式消息 - delta_message = await self._make_msg_chunk(pipeline_config, chunk, chunk_idx) + delta_message = await self._make_msg_chunk(remove_think, chunk, chunk_idx) if delta_message.content: current_content += delta_message.content delta_message.content = current_content diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index 0d7bdd0a..03a9b43b 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -92,9 +92,11 @@ class LocalAgentRunner(runner.RequestRunner): is_stream = query.adapter.is_stream_output_supported() try: is_stream = await query.adapter.is_stream_output_supported() - except AttributeError: is_stream = False + + remove_think = self.pipeline_config['trigger'].get('misc', '').get('remove_think') + if not is_stream: # 非流式输出,直接请求 @@ -105,6 +107,7 @@ class LocalAgentRunner(runner.RequestRunner): req_messages, query.use_funcs, extra_args=query.use_llm_model.model_entity.extra_args, + remove_think=remove_think, ) yield msg final_msg = msg @@ -118,6 +121,7 @@ class LocalAgentRunner(runner.RequestRunner): req_messages, query.use_funcs, extra_args=query.use_llm_model.model_entity.extra_args, + remove_think=remove_think, ): msg_idx = msg_idx + 1 if msg_idx % 8 == 0 or msg.is_final: From 4a1d033ee9a78eff5b09aff4961d08487f660da4 Mon Sep 17 00:00:00 2001 From: fdc <2213070223@qq.com> Date: Mon, 4 Aug 2025 16:39:38 +0800 Subject: [PATCH 065/107] fix: Reduce chunk returns in dify and Hundred Refining Runner to every 8 chunks --- pkg/provider/modelmgr/requesters/moonshotchatcmpl.py | 2 +- pkg/provider/runners/dashscopeapi.py | 2 +- pkg/provider/runners/difysvapi.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py b/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py index b8c0e950..494b2b0f 100644 --- a/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py +++ b/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py @@ -55,6 +55,6 @@ class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions): resp = await self._req(args, extra_body=extra_args) # 处理请求结果 - message = await self._make_msg(resp,remove_think) + message = await self._make_msg(resp, remove_think) return message diff --git a/pkg/provider/runners/dashscopeapi.py b/pkg/provider/runners/dashscopeapi.py index 9bb5824c..7f66e6f0 100644 --- a/pkg/provider/runners/dashscopeapi.py +++ b/pkg/provider/runners/dashscopeapi.py @@ -148,7 +148,7 @@ class DashScopeAPIRunner(runner.RequestRunner): # 将参考资料替换到文本中 pending_content = self._replace_references(pending_content, references_dict) - if idx_chunk % 64 == 0 or is_final: + if idx_chunk % 8 == 0 or is_final: yield llm_entities.MessageChunk( role='assistant', content=pending_content, diff --git a/pkg/provider/runners/difysvapi.py b/pkg/provider/runners/difysvapi.py index 40a3140c..8c1307a5 100644 --- a/pkg/provider/runners/difysvapi.py +++ b/pkg/provider/runners/difysvapi.py @@ -115,7 +115,7 @@ class DifyServiceAPIRunner(runner.RequestRunner): stream_output_pending_chunk = '' - batch_pending_max_size = 64 # 积累一定量的消息更新消息一次 + batch_pending_max_size = 8 # 积累一定量的消息更新消息一次 batch_pending_index = 0 From 15e524c6e604f6f3354c611bb40065c391eafc31 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Mon, 4 Aug 2025 18:17:12 +0800 Subject: [PATCH 066/107] perf: move `remove-think` to output tab --- pkg/provider/modelmgr/requesters/chatcmpl.py | 2 ++ pkg/provider/runners/localagent.py | 9 +++++---- templates/default-pipeline-config.json | 3 ++- templates/metadata/pipeline/output.yaml | 10 ++++++++++ templates/metadata/pipeline/trigger.yaml | 10 ---------- 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index 04e7da20..fdef1459 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -313,6 +313,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): messages: typing.List[llm_entities.Message], funcs: typing.List[tools_entities.LLMFunction] = None, extra_args: dict[str, typing.Any] = {}, + remove_think: bool = False, ) -> llm_entities.MessageChunk: req_messages = [] # req_messages 仅用于类内,外部同步由 query.messages 进行 for m in messages: @@ -332,6 +333,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): use_model=model, use_funcs=funcs, extra_args=extra_args, + remove_think=remove_think, ): yield item diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index 03a9b43b..3da25679 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -94,9 +94,8 @@ class LocalAgentRunner(runner.RequestRunner): is_stream = await query.adapter.is_stream_output_supported() except AttributeError: is_stream = False - - remove_think = self.pipeline_config['trigger'].get('misc', '').get('remove_think') + remove_think = self.pipeline_config['output'].get('misc', '').get('remove-think') if not is_stream: # 非流式输出,直接请求 @@ -183,8 +182,9 @@ class LocalAgentRunner(runner.RequestRunner): query.use_llm_model, req_messages, query.use_funcs, - extra_args=query.use_llm_model.model_entity.extra_args, - ): + extra_args=query.use_llm_model.model_entity.extra_args, + remove_think=remove_think, + ): yield msg if msg.tool_calls: for tool_call in msg.tool_calls: @@ -212,6 +212,7 @@ class LocalAgentRunner(runner.RequestRunner): req_messages, query.use_funcs, extra_args=query.use_llm_model.model_entity.extra_args, + remove_think=remove_think, ) yield msg diff --git a/templates/default-pipeline-config.json b/templates/default-pipeline-config.json index d06e4661..855e2ac6 100644 --- a/templates/default-pipeline-config.json +++ b/templates/default-pipeline-config.json @@ -87,7 +87,8 @@ "hide-exception": true, "at-sender": true, "quote-origin": true, - "track-function-calls": false + "track-function-calls": false, + "remove-think": true } } } \ No newline at end of file diff --git a/templates/metadata/pipeline/output.yaml b/templates/metadata/pipeline/output.yaml index 9fe0cd25..66bb312c 100644 --- a/templates/metadata/pipeline/output.yaml +++ b/templates/metadata/pipeline/output.yaml @@ -105,3 +105,13 @@ stages: type: boolean required: true default: false + - name: remove-think + label: + en_US: Remove CoT + zh_Hans: 删除思维链 + description: + en_US: If enabled, LangBot will remove the LLM thought content in response + zh_Hans: 如果启用,将自动删除大模型回复中的模型思考内容 + type: boolean + required: true + default: true diff --git a/templates/metadata/pipeline/trigger.yaml b/templates/metadata/pipeline/trigger.yaml index 08a2714b..949b2698 100644 --- a/templates/metadata/pipeline/trigger.yaml +++ b/templates/metadata/pipeline/trigger.yaml @@ -132,13 +132,3 @@ stages: type: boolean required: true default: true - - name: remove_think - label: - en_US: Remove CoT - zh_Hans: 删除思维链 - description: - en_US: If enabled, LangBot will remove the LLM thought content in response - zh_Hans: 如果启用,将自动删除大模型回复中的模型思考内容 - type: boolean - required: true - default: true From 7f25d61531e6d00289da4c8f39c828c926b93a13 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Mon, 4 Aug 2025 23:00:54 +0800 Subject: [PATCH 067/107] fix: minor fix --- pkg/provider/modelmgr/requesters/chatcmpl.py | 3 ++- pkg/provider/runners/localagent.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index fdef1459..cf557755 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -55,7 +55,6 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): ) -> llm_entities.Message: chatcmpl_message = chat_completion.choices[0].message.model_dump() # print(chatcmpl_message.keys(),chatcmpl_message.values()) - # 确保 role 字段存在且不为 None if 'role' not in chatcmpl_message or chatcmpl_message['role'] is None: chatcmpl_message['role'] = 'assistant' @@ -241,6 +240,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): messages: typing.List[llm_entities.Message], funcs: typing.List[tools_entities.LLMFunction] = None, extra_args: dict[str, typing.Any] = {}, + remove_think: bool = False, ) -> llm_entities.Message: req_messages = [] # req_messages 仅用于类内,外部同步由 query.messages 进行 for m in messages: @@ -260,6 +260,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): use_model=model, use_funcs=funcs, extra_args=extra_args, + remove_think=remove_think, ) return msg except asyncio.TimeoutError: diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index 3da25679..918ec42c 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -89,7 +89,6 @@ class LocalAgentRunner(runner.RequestRunner): req_messages = query.prompt.messages.copy() + query.messages.copy() + [user_message] - is_stream = query.adapter.is_stream_output_supported() try: is_stream = await query.adapter.is_stream_output_supported() except AttributeError: From e88302f1b4008f844e1f110ebad633a74f9fecec Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Tue, 5 Aug 2025 04:24:03 +0800 Subject: [PATCH 068/107] fix:The handling logic of remove_think in the connector and Temporarily blocked the processing of streaming tool calls in the runner. --- pkg/provider/modelmgr/requesters/chatcmpl.py | 64 ++++++++--------- .../modelmgr/requesters/giteeaichatcmpl.py | 54 ++++++++------- .../modelmgr/requesters/modelscopechatcmpl.py | 69 ++++++++++--------- .../modelmgr/requesters/ppiochatcmpl.py | 60 ++++++++-------- pkg/provider/runners/localagent.py | 27 +++++--- 5 files changed, 145 insertions(+), 129 deletions(-) diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index cf557755..35d1ff92 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -17,7 +17,6 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): """OpenAI ChatCompletion API 请求器""" client: openai.AsyncClient - is_content: bool default_config: dict[str, typing.Any] = { 'base_url': 'https://api.openai.com/v1', @@ -31,7 +30,6 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): timeout=self.requester_cfg['timeout'], http_client=httpx.AsyncClient(trust_env=True, timeout=self.requester_cfg['timeout']), ) - self.is_content = False async def _req( self, @@ -76,22 +74,14 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): async def _make_msg_chunk( self, - remove_think: bool, - chat_completion: chat_completion.ChatCompletion, + delta: dict[str, typing.Any], idx: int, + is_content: bool, + is_think: bool, ) -> llm_entities.MessageChunk: # 处理流式chunk和完整响应的差异 # print(chat_completion.choices[0]) - if hasattr(chat_completion, 'choices'): - # 完整响应模式 - choice = chat_completion.choices[0] - delta = choice.delta.model_dump() if hasattr(choice, 'delta') else choice.message.model_dump() - else: - # 流式chunk模式 - delta = chat_completion.delta.model_dump() if hasattr(chat_completion, 'delta') else {} - # 确保 role 字段存在且不为 None - # print(delta.keys(),delta.values()) if 'role' not in delta or delta['role'] is None: delta['role'] = 'assistant' @@ -101,26 +91,23 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): # print(reasoning_content) # deepseek的reasoner模型 - if remove_think: - if reasoning_content is not None: - pass - else: - delta['content'] = delta['content'] - else: - if reasoning_content is not None and idx == 0: + if reasoning_content is not None and idx == 0: + if reasoning_content != '': delta['content'] += f'\n{reasoning_content}' - elif reasoning_content is None: - if self.is_content: - delta['content'] = delta['content'] - else: - delta['content'] = f'\n\n\n{delta["content"]}' - self.is_content = True - else: - delta['content'] += reasoning_content + is_think = True + elif reasoning_content is None and idx != 0: + if is_content: + delta['content'] = delta['content'] + elif is_think: + delta['content'] = f'\n\n\n{delta["content"]}' + is_content = True + is_think = False + else: + delta['content'] = reasoning_content message = llm_entities.MessageChunk(**delta) - return message + return message,is_content, is_think async def _closure_stream( self, @@ -159,11 +146,26 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): current_content = '' args['stream'] = True chunk_idx = 0 - self.is_content = False + is_content = False + is_think = False tool_calls_map: dict[str, llm_entities.ToolCall] = {} async for chunk in self._req_stream(args, extra_body=extra_args): + if hasattr(chunk, 'choices'): + # 完整响应模式 + choice = chunk.choices[0] + delta = choice.delta.model_dump() if hasattr(choice, 'delta') else choice.message.model_dump() + else: + # 流式chunk模式 + delta = chunk.delta.model_dump() if hasattr(chunk, 'delta') else {} + if remove_think: + reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None + if reasoning_content is not None: + continue # 处理流式消息 - delta_message = await self._make_msg_chunk(remove_think, chunk, chunk_idx) + delta_message,is_content,is_think = await self._make_msg_chunk(delta, + chunk_idx, + is_content, + is_think) if delta_message.content: current_content += delta_message.content delta_message.content = current_content diff --git a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py index a8d6eb16..0ff49798 100644 --- a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py +++ b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py @@ -19,7 +19,6 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): 'base_url': 'https://ai.gitee.com/v1', 'timeout': 120, } - is_think: bool = False async def _closure( self, @@ -86,19 +85,12 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): async def _make_msg_chunk( self, - remove_think: bool, - chat_completion: chat_completion.ChatCompletion, + delta: dict[str, typing.Any], idx: int, ) -> llm_entities.MessageChunk: # 处理流式chunk和完整响应的差异 # print(chat_completion.choices[0]) - if hasattr(chat_completion, 'choices'): - # 完整响应模式 - choice = chat_completion.choices[0] - delta = choice.delta.model_dump() if hasattr(choice, 'delta') else choice.message.model_dump() - else: - # 流式chunk模式 - delta = chat_completion.delta.model_dump() if hasattr(chat_completion, 'delta') else {} + # 确保 role 字段存在且不为 None if 'role' not in delta or delta['role'] is None: @@ -110,20 +102,9 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): # print(reasoning_content) # deepseek的reasoner模型 - if remove_think: - if delta['content'] == '': - self.is_think = True - delta['content'] = '' - if delta['content'] == r'': - self.is_think = False - delta['content'] = '' - if not self.is_think: - delta['content'] = delta['content'] - else: - delta['content'] = '' - else: - if reasoning_content is not None: - delta['content'] += reasoning_content + + if reasoning_content is not None: + delta['content'] += reasoning_content message = llm_entities.MessageChunk(**delta) @@ -166,11 +147,32 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): current_content = '' args['stream'] = True chunk_idx = 0 - self.is_content = False + is_think = False tool_calls_map: dict[str, llm_entities.ToolCall] = {} async for chunk in self._req_stream(args, extra_body=extra_args): # 处理流式消息 - delta_message = await self._make_msg_chunk(remove_think, chunk, chunk_idx) + if hasattr(chunk, 'choices'): + # 完整响应模式 + if chunk.choices: + choice = chunk.choices[0] + delta = choice.delta.model_dump() if hasattr(choice, 'delta') else choice.message.model_dump() + else: + continue + else: + # 流式chunk模式 + delta = chunk.delta.model_dump() if hasattr(chunk, 'delta') else {} + if remove_think: + print(delta) + if delta['content'] == '': + is_think = True + continue + elif delta['content'] == r'': + is_think = False + continue + elif is_think or delta['content'] == '\n\n': + continue + + delta_message = await self._make_msg_chunk(delta, chunk_idx) if delta_message.content: current_content += delta_message.content delta_message.content = current_content diff --git a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py index 7895a87e..f5db54a1 100644 --- a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py +++ b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py @@ -172,24 +172,15 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): async for chunk in await self.client.chat.completions.create(**args, extra_body=extra_body): yield chunk - async def _make_msg_chunk( - self, - remove_think: bool, - chat_completion: chat_completion.ChatCompletion, - idx: int, + async def _make_msg_chunk(self, + delta: dict[str, typing.Any], + idx: int, + is_content: bool, + is_think: bool, ) -> llm_entities.MessageChunk: # 处理流式chunk和完整响应的差异 # print(chat_completion.choices[0]) - if hasattr(chat_completion, 'choices'): - # 完整响应模式 - choice = chat_completion.choices[0] - delta = choice.delta.model_dump() if hasattr(choice, 'delta') else choice.message.model_dump() - else: - # 流式chunk模式 - delta = chat_completion.delta.model_dump() if hasattr(chat_completion, 'delta') else {} - # 确保 role 字段存在且不为 None - # print(delta.keys(),delta.values()) if 'role' not in delta or delta['role'] is None: delta['role'] = 'assistant' @@ -199,26 +190,23 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): # print(reasoning_content) # deepseek的reasoner模型 - if remove_think: - if reasoning_content is not None: - pass - else: - delta['content'] = delta['content'] - else: - if reasoning_content is not None and idx == 0: + if reasoning_content is not None and idx == 0: + if reasoning_content != '': delta['content'] += f'\n{reasoning_content}' - elif reasoning_content is None: - if self.is_content: - delta['content'] = delta['content'] - else: - delta['content'] = f'\n\n\n{delta["content"]}' - self.is_content = True - else: - delta['content'] += reasoning_content + is_think = True + elif reasoning_content == '' and idx != 0: + if is_content: + delta['content'] = delta['content'] + elif is_think: + delta['content'] = f'\n\n\n{delta["content"]}' + is_content = True + is_think = False + else: + delta['content'] = reasoning_content message = llm_entities.MessageChunk(**delta) - return message + return message, is_content, is_think async def _closure_stream( self, @@ -257,11 +245,28 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): current_content = '' args['stream'] = True chunk_idx = 0 - self.is_content = False + is_content = False + is_think = False tool_calls_map: dict[str, llm_entities.ToolCall] = {} async for chunk in self._req_stream(args, extra_body=extra_args): + if hasattr(chunk, 'choices'): + # 完整响应模式 + choice = chunk.choices[0] + delta = choice.delta.model_dump() if hasattr(choice, 'delta') else choice.message.model_dump() + else: + # 流式chunk模式 + delta = chunk.delta.model_dump() if hasattr(chunk, 'delta') else {} + print(delta) + if remove_think: + reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None + if reasoning_content != '': + continue + # 处理流式消息 + delta_message, is_content, is_think = await self._make_msg_chunk(delta, + chunk_idx, + is_content, + is_think) # 处理流式消息 - delta_message = await self._make_msg_chunk(remove_think, chunk, chunk_idx) if delta_message.content: current_content += delta_message.content delta_message.content = current_content diff --git a/pkg/provider/modelmgr/requesters/ppiochatcmpl.py b/pkg/provider/modelmgr/requesters/ppiochatcmpl.py index ca49df10..68acae81 100644 --- a/pkg/provider/modelmgr/requesters/ppiochatcmpl.py +++ b/pkg/provider/modelmgr/requesters/ppiochatcmpl.py @@ -54,20 +54,12 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): return message async def _make_msg_chunk( - self, - remove_think: bool, - chat_completion: chat_completion.ChatCompletion, - idx: int, + self, + delta: dict[str, typing.Any], + idx: int, ) -> llm_entities.MessageChunk: # 处理流式chunk和完整响应的差异 # print(chat_completion.choices[0]) - if hasattr(chat_completion, 'choices'): - # 完整响应模式 - choice = chat_completion.choices[0] - delta = choice.delta.model_dump() if hasattr(choice, 'delta') else choice.message.model_dump() - else: - # 流式chunk模式 - delta = chat_completion.delta.model_dump() if hasattr(chat_completion, 'delta') else {} # 确保 role 字段存在且不为 None if 'role' not in delta or delta['role'] is None: @@ -79,20 +71,9 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): # print(reasoning_content) # deepseek的reasoner模型 - if remove_think: - if '' in delta['content']: - self.is_think = True - delta['content'] = '' - if r'' in delta['content']: - self.is_think = False - delta['content'] = '' - if not self.is_think: - delta['content'] = delta['content'] - else: - delta['content'] = '' - else: - if reasoning_content is not None: - delta['content'] += reasoning_content + + if reasoning_content is not None: + delta['content'] += reasoning_content message = llm_entities.MessageChunk(**delta) @@ -135,11 +116,33 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): current_content = '' args['stream'] = True chunk_idx = 0 - self.is_content = False + is_think = False tool_calls_map: dict[str, llm_entities.ToolCall] = {} async for chunk in self._req_stream(args, extra_body=extra_args): # 处理流式消息 - delta_message = await self._make_msg_chunk(remove_think, chunk, chunk_idx) + if hasattr(chunk, 'choices'): + # 完整响应模式 + if chunk.choices: + choice = chunk.choices[0] + delta = choice.delta.model_dump() if hasattr(choice, 'delta') else choice.message.model_dump() + else: + continue + else: + # 流式chunk模式 + delta = chunk.delta.model_dump() if hasattr(chunk, 'delta') else {} + if remove_think: + if delta['content'] is not None: + if '' in delta['content']: + is_think = True + continue + elif delta['content'] == r'': + is_think = False + continue + elif is_think or delta['content'] == '\n\n': + continue + + delta_message = await self._make_msg_chunk(delta, chunk_idx) + # 处理流式消息 if delta_message.content: current_content += delta_message.content delta_message.content = current_content @@ -164,5 +167,4 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): delta_message.is_final = True delta_message.content = current_content - if chunk_idx % 64 == 0 or delta_message.is_final: - yield delta_message + yield delta_message diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index 918ec42c..1f17fafd 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -122,10 +122,11 @@ class LocalAgentRunner(runner.RequestRunner): remove_think=remove_think, ): msg_idx = msg_idx + 1 + tool_msg = msg if msg_idx % 8 == 0 or msg.is_final: yield msg - if msg.tool_calls: - for tool_call in msg.tool_calls: + if tool_msg.tool_calls: + for tool_call in tool_msg.tool_calls: if tool_call.id not in tool_calls_map: tool_calls_map[tool_call.id] = llm_entities.ToolCall( id=tool_call.id, @@ -137,9 +138,9 @@ class LocalAgentRunner(runner.RequestRunner): if tool_call.function and tool_call.function.arguments: # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments - final_msg = llm_entities.Message( - role=msg.role, - content=msg.all_content, + final_msg = llm_entities.MessageChunk( + role="tool", + content='', tool_calls=list(tool_calls_map.values()), ) @@ -176,6 +177,7 @@ class LocalAgentRunner(runner.RequestRunner): if is_stream: tool_calls_map = {} + msg_idx = 0 async for msg in await query.use_llm_model.requester.invoke_llm_stream( query, query.use_llm_model, @@ -184,9 +186,12 @@ class LocalAgentRunner(runner.RequestRunner): extra_args=query.use_llm_model.model_entity.extra_args, remove_think=remove_think, ): - yield msg - if msg.tool_calls: - for tool_call in msg.tool_calls: + msg_idx += 1 + tool_msg = msg + if msg_idx % 8 == 0 or msg.is_final: + yield msg + if tool_msg.tool_calls: + for tool_call in tool_msg.tool_calls: if tool_call.id not in tool_calls_map: tool_calls_map[tool_call.id] = llm_entities.ToolCall( id=tool_call.id, @@ -198,9 +203,9 @@ class LocalAgentRunner(runner.RequestRunner): if tool_call.function and tool_call.function.arguments: # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments - final_msg = llm_entities.Message( - role=msg.role, - content=msg.all_content, + final_msg = llm_entities.MessageChunk( + role="tool", + content='', tool_calls=list(tool_calls_map.values()), ) else: From c33c9eaab0f4d54397cb851d3044d33a14c21008 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Wed, 6 Aug 2025 15:45:35 +0800 Subject: [PATCH 069/107] chore: remove `remove_think` param in trigger.yaml --- templates/metadata/pipeline/trigger.yaml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/templates/metadata/pipeline/trigger.yaml b/templates/metadata/pipeline/trigger.yaml index 08a2714b..949b2698 100644 --- a/templates/metadata/pipeline/trigger.yaml +++ b/templates/metadata/pipeline/trigger.yaml @@ -132,13 +132,3 @@ stages: type: boolean required: true default: true - - name: remove_think - label: - en_US: Remove CoT - zh_Hans: 删除思维链 - description: - en_US: If enabled, LangBot will remove the LLM thought content in response - zh_Hans: 如果启用,将自动删除大模型回复中的模型思考内容 - type: boolean - required: true - default: true From 3a82ae8da5ec0a3eab055e08a7039a2ba341ce74 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Wed, 6 Aug 2025 23:00:57 +0800 Subject: [PATCH 070/107] fix: the bug in the "remove_think" function. --- pkg/provider/modelmgr/requesters/chatcmpl.py | 20 +++++++++------- .../modelmgr/requesters/modelscopechatcmpl.py | 24 +++++++++++-------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index 35d1ff92..dfe2ed71 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -85,16 +85,14 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): if 'role' not in delta or delta['role'] is None: delta['role'] = 'assistant' - reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None + reasoning_content = delta['reasoning_content'] delta['content'] = '' if delta['content'] is None else delta['content'] - # print(reasoning_content) # deepseek的reasoner模型 if reasoning_content is not None and idx == 0: - if reasoning_content != '': - delta['content'] += f'\n{reasoning_content}' - is_think = True + delta['content'] += f'\n{reasoning_content}' + is_think = True elif reasoning_content is None and idx != 0: if is_content: delta['content'] = delta['content'] @@ -102,7 +100,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): delta['content'] = f'\n\n\n{delta["content"]}' is_content = True is_think = False - else: + elif reasoning_content is not None and reasoning_content != '': delta['content'] = reasoning_content message = llm_entities.MessageChunk(**delta) @@ -157,10 +155,16 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): else: # 流式chunk模式 delta = chunk.delta.model_dump() if hasattr(chunk, 'delta') else {} + print(delta) + reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None + delta['reasoning_content'] = reasoning_content if remove_think: - reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None - if reasoning_content is not None: + if delta['reasoning_content'] is not None: continue + if ((delta['content'] == '' or delta.get('content',None) is None) and + (delta.get('reasoning_content',None) is None or delta['reasoning_content'] == '') and + chunk_idx == 0): # 此处将第一条空消息排除,大部分模型第一条消息携带的是role,但是在role直接处理为ass + continue # 处理流式消息 delta_message,is_content,is_think = await self._make_msg_chunk(delta, chunk_idx, diff --git a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py index f5db54a1..e02b0d07 100644 --- a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py +++ b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py @@ -184,24 +184,24 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): if 'role' not in delta or delta['role'] is None: delta['role'] = 'assistant' - reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None + reasoning_content = delta['reasoning_content'] delta['content'] = '' if delta['content'] is None else delta['content'] # print(reasoning_content) # deepseek的reasoner模型 + if reasoning_content is not None and idx == 0: - if reasoning_content != '': - delta['content'] += f'\n{reasoning_content}' - is_think = True - elif reasoning_content == '' and idx != 0: + delta['content'] += f'\n{reasoning_content}' + is_think = True + elif reasoning_content is None and idx != 0: if is_content: delta['content'] = delta['content'] elif is_think: delta['content'] = f'\n\n\n{delta["content"]}' is_content = True is_think = False - else: + elif reasoning_content is not None: delta['content'] = reasoning_content message = llm_entities.MessageChunk(**delta) @@ -256,12 +256,16 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): else: # 流式chunk模式 delta = chunk.delta.model_dump() if hasattr(chunk, 'delta') else {} - print(delta) + reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None + delta['reasoning_content'] = None if reasoning_content == '' else reasoning_content # 直接不管有没有思考消息,构造一个,方便去除思考判断 if remove_think: - reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None - if reasoning_content != '': + if delta['reasoning_content'] is not None: continue - # 处理流式消息 + if ((delta['content'] == '' or delta.get('content', None) is None) and + (delta.get('reasoning_content', None) is None or delta['reasoning_content'] == '') and + chunk_idx == 0): # 此处将第一条空消息排除,大部分模型第一条消息携带的是role,但是在role直接处理为ass + continue + # 处理流式消息 delta_message, is_content, is_think = await self._make_msg_chunk(delta, chunk_idx, is_content, From 02dbe80d2fd596e7374b0079ec7871cb1d009531 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Thu, 7 Aug 2025 10:01:04 +0800 Subject: [PATCH 071/107] perf: model testing --- pkg/api/http/service/model.py | 2 +- pkg/provider/modelmgr/requesters/anthropicmsgs.py | 4 ++-- .../modelmgr/requesters/anthropicmsgs.yaml | 2 +- .../component/embedding-form/EmbeddingForm.tsx | 13 +++++++++++++ .../app/home/models/component/llm-form/LLMForm.tsx | 14 +++++++++++++- .../components/debug-dialog/DebugDialog.tsx | 2 +- 6 files changed, 31 insertions(+), 6 deletions(-) diff --git a/pkg/api/http/service/model.py b/pkg/api/http/service/model.py index d8457da3..d3f3d5d8 100644 --- a/pkg/api/http/service/model.py +++ b/pkg/api/http/service/model.py @@ -101,7 +101,7 @@ class LLMModelsService: model=runtime_llm_model, messages=[llm_entities.Message(role='user', content='Hello, world!')], funcs=[], - extra_args={}, + extra_args=model_data.get('extra_args', {}), ) diff --git a/pkg/provider/modelmgr/requesters/anthropicmsgs.py b/pkg/provider/modelmgr/requesters/anthropicmsgs.py index 75f2bf7e..e0850c03 100644 --- a/pkg/provider/modelmgr/requesters/anthropicmsgs.py +++ b/pkg/provider/modelmgr/requesters/anthropicmsgs.py @@ -21,7 +21,7 @@ class AnthropicMessages(requester.ProviderAPIRequester): client: anthropic.AsyncAnthropic default_config: dict[str, typing.Any] = { - 'base_url': 'https://api.anthropic.com/v1', + 'base_url': 'https://api.anthropic.com', 'timeout': 120, } @@ -44,6 +44,7 @@ class AnthropicMessages(requester.ProviderAPIRequester): self.client = anthropic.AsyncAnthropic( api_key='', http_client=httpx_client, + base_url=self.requester_cfg['base_url'], ) async def invoke_llm( @@ -141,7 +142,6 @@ class AnthropicMessages(requester.ProviderAPIRequester): args['tools'] = tools try: - # print(json.dumps(args, indent=4, ensure_ascii=False)) resp = await self.client.messages.create(**args) args = { diff --git a/pkg/provider/modelmgr/requesters/anthropicmsgs.yaml b/pkg/provider/modelmgr/requesters/anthropicmsgs.yaml index 7dbcf3ed..e3f745fb 100644 --- a/pkg/provider/modelmgr/requesters/anthropicmsgs.yaml +++ b/pkg/provider/modelmgr/requesters/anthropicmsgs.yaml @@ -14,7 +14,7 @@ spec: zh_Hans: 基础 URL type: string required: true - default: "https://api.anthropic.com/v1" + default: "https://api.anthropic.com" - name: timeout label: en_US: Timeout diff --git a/web/src/app/home/models/component/embedding-form/EmbeddingForm.tsx b/web/src/app/home/models/component/embedding-form/EmbeddingForm.tsx index d50885ae..18be1662 100644 --- a/web/src/app/home/models/component/embedding-form/EmbeddingForm.tsx +++ b/web/src/app/home/models/component/embedding-form/EmbeddingForm.tsx @@ -298,6 +298,18 @@ export default function EmbeddingForm({ function testEmbeddingModelInForm() { setModelTesting(true); + const extraArgsObj: Record = {}; + form + .getValues('extra_args') + ?.forEach((arg: { key: string; type: string; value: string }) => { + if (arg.type === 'number') { + extraArgsObj[arg.key] = Number(arg.value); + } else if (arg.type === 'boolean') { + extraArgsObj[arg.key] = arg.value === 'true'; + } else { + extraArgsObj[arg.key] = arg.value; + } + }); httpClient .testEmbeddingModel('_', { uuid: '', @@ -309,6 +321,7 @@ export default function EmbeddingForm({ timeout: 120, }, api_keys: [form.getValues('api_key')], + extra_args: extraArgsObj, }) .then((res) => { console.log(res); diff --git a/web/src/app/home/models/component/llm-form/LLMForm.tsx b/web/src/app/home/models/component/llm-form/LLMForm.tsx index 73cc32fe..f7c329e1 100644 --- a/web/src/app/home/models/component/llm-form/LLMForm.tsx +++ b/web/src/app/home/models/component/llm-form/LLMForm.tsx @@ -312,6 +312,18 @@ export default function LLMForm({ function testLLMModelInForm() { setModelTesting(true); + const extraArgsObj: Record = {}; + form + .getValues('extra_args') + ?.forEach((arg: { key: string; type: string; value: string }) => { + if (arg.type === 'number') { + extraArgsObj[arg.key] = Number(arg.value); + } else if (arg.type === 'boolean') { + extraArgsObj[arg.key] = arg.value === 'true'; + } else { + extraArgsObj[arg.key] = arg.value; + } + }); httpClient .testLLMModel('_', { uuid: '', @@ -324,7 +336,7 @@ export default function LLMForm({ }, api_keys: [form.getValues('api_key')], abilities: form.getValues('abilities'), - extra_args: form.getValues('extra_args'), + extra_args: extraArgsObj, }) .then((res) => { console.log(res); diff --git a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx index 8505f4f9..e29af1c3 100644 --- a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx +++ b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx @@ -257,7 +257,7 @@ export default function DebugDialog({ sessionType, messageChain, selectedPipelineId, - 120000, + 180000, ); setMessages((prevMessages) => [...prevMessages, response.message]); From 9736d0708a5d0bc53ff94fdf02ad2bfc44e9740c Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Thu, 7 Aug 2025 10:15:09 +0800 Subject: [PATCH 072/107] fix: missing deps --- web/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/web/package.json b/web/package.json index 255ae452..cd869438 100644 --- a/web/package.json +++ b/web/package.json @@ -21,6 +21,7 @@ "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@hookform/resolvers": "^5.0.1", "@radix-ui/react-checkbox": "^1.3.1", "@radix-ui/react-dialog": "^1.1.14", From 261f50b8eca0a80c121217e316ae555244cc3833 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Thu, 7 Aug 2025 15:47:57 +0800 Subject: [PATCH 073/107] feat: refactor with cursor max mode claude 4.1 opus --- pkg/pipeline/respback/respback.py | 1 + pkg/provider/modelmgr/requesters/chatcmpl.py | 229 ++++++++++++------- pkg/provider/runners/localagent.py | 75 ++++-- 3 files changed, 201 insertions(+), 104 deletions(-) diff --git a/pkg/pipeline/respback/respback.py b/pkg/pipeline/respback/respback.py index c7824856..bc91dffe 100644 --- a/pkg/pipeline/respback/respback.py +++ b/pkg/pipeline/respback/respback.py @@ -38,6 +38,7 @@ class SendResponseBackStage(stage.PipelineStage): quote_origin = query.pipeline_config['output']['misc']['quote-origin'] # has_chunks = any(isinstance(msg, llm_entities.MessageChunk) for msg in query.resp_messages) + # TODO 命令与流式的兼容性问题 if await query.adapter.is_stream_output_supported(): is_final = [msg.is_final for msg in query.resp_messages][0] await query.adapter.reply_message_chunk( diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index dfe2ed71..f8ea8593 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -42,7 +42,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): self, args: dict, extra_body: dict = {}, - ) -> chat_completion.ChatCompletion: + ): async for chunk in await self.client.chat.completions.create(**args, extra_body=extra_body): yield chunk @@ -52,60 +52,73 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): remove_think: bool = False, ) -> llm_entities.Message: chatcmpl_message = chat_completion.choices[0].message.model_dump() - # print(chatcmpl_message.keys(),chatcmpl_message.values()) + # 确保 role 字段存在且不为 None if 'role' not in chatcmpl_message or chatcmpl_message['role'] is None: chatcmpl_message['role'] = 'assistant' - reasoning_content = chatcmpl_message['reasoning_content'] if 'reasoning_content' in chatcmpl_message else None + # 处理思维链 + content = chatcmpl_message.get('content', '') + reasoning_content = chatcmpl_message.get('reasoning_content', None) - # deepseek的reasoner模型 - if remove_think: - pass - else: - if reasoning_content is not None: - chatcmpl_message['content'] = ( - '\n' + reasoning_content + '\n\n' + chatcmpl_message['content'] - ) + processed_content, _ = await self._process_thinking_content( + content=content, reasoning_content=reasoning_content, remove_think=remove_think + ) + + chatcmpl_message['content'] = processed_content + + # 移除 reasoning_content 字段,避免传递给 Message + if 'reasoning_content' in chatcmpl_message: + del chatcmpl_message['reasoning_content'] message = llm_entities.Message(**chatcmpl_message) - return message - async def _make_msg_chunk( + async def _process_thinking_content( self, - delta: dict[str, typing.Any], - idx: int, - is_content: bool, - is_think: bool, - ) -> llm_entities.MessageChunk: - # 处理流式chunk和完整响应的差异 - # print(chat_completion.choices[0]) + content: str, + reasoning_content: str = None, + remove_think: bool = False, + ) -> tuple[str, str]: + """处理思维链内容 - if 'role' not in delta or delta['role'] is None: - delta['role'] = 'assistant' + Args: + content: 原始内容 + reasoning_content: reasoning_content 字段内容 + remove_think: 是否移除思维链 - reasoning_content = delta['reasoning_content'] + Returns: + (处理后的内容, 提取的思维链内容) + """ + thinking_content = '' - delta['content'] = '' if delta['content'] is None else delta['content'] + # 1. 从 reasoning_content 提取思维链 + if reasoning_content: + thinking_content = reasoning_content - # deepseek的reasoner模型 - if reasoning_content is not None and idx == 0: - delta['content'] += f'\n{reasoning_content}' - is_think = True - elif reasoning_content is None and idx != 0: - if is_content: - delta['content'] = delta['content'] - elif is_think: - delta['content'] = f'\n\n\n{delta["content"]}' - is_content = True - is_think = False - elif reasoning_content is not None and reasoning_content != '': - delta['content'] = reasoning_content + # 2. 从 content 中提取 标签内容 + if content and '' in content and '' in content: + import re - message = llm_entities.MessageChunk(**delta) + think_pattern = r'(.*?)' + think_matches = re.findall(think_pattern, content, re.DOTALL) + if think_matches: + # 如果已有 reasoning_content,则追加 + if thinking_content: + thinking_content += '\n' + '\n'.join(think_matches) + else: + thinking_content = '\n'.join(think_matches) + # 移除 content 中的 标签 + content = re.sub(think_pattern, '', content, flags=re.DOTALL).strip() - return message,is_content, is_think + # 3. 根据 remove_think 参数决定是否保留思维链 + if remove_think: + return content, '' + else: + # 如果有思维链内容,将其以 格式添加到 content 开头 + if thinking_content: + content = f'\n{thinking_content}\n\n{content}'.strip() + return content, thinking_content async def _closure_stream( self, @@ -123,7 +136,6 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): if use_funcs: tools = await self.ap.tool_mgr.generate_tools_for_openai(use_funcs) - if tools: args['tools'] = tools @@ -140,62 +152,105 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): del me['image_base64'] args['messages'] = messages - - current_content = '' args['stream'] = True - chunk_idx = 0 - is_content = False - is_think = False + + # 流式处理状态 tool_calls_map: dict[str, llm_entities.ToolCall] = {} + chunk_idx = 0 + thinking_started = False + thinking_ended = False + role = 'assistant' # 默认角色 + accumulated_reasoning = '' # 仅用于判断何时结束思维链 + async for chunk in self._req_stream(args, extra_body=extra_args): - if hasattr(chunk, 'choices'): - # 完整响应模式 + # 解析 chunk 数据 + if hasattr(chunk, 'choices') and chunk.choices: choice = chunk.choices[0] - delta = choice.delta.model_dump() if hasattr(choice, 'delta') else choice.message.model_dump() + delta = choice.delta.model_dump() if hasattr(choice, 'delta') else {} + finish_reason = getattr(choice, 'finish_reason', None) else: - # 流式chunk模式 - delta = chunk.delta.model_dump() if hasattr(chunk, 'delta') else {} - print(delta) - reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None - delta['reasoning_content'] = reasoning_content - if remove_think: - if delta['reasoning_content'] is not None: + delta = {} + finish_reason = None + + # 从第一个 chunk 获取 role,后续使用这个 role + if 'role' in delta and delta['role']: + role = delta['role'] + + # 获取增量内容 + delta_content = delta.get('content', '') + reasoning_content = delta.get('reasoning_content', '') + + # 处理 reasoning_content + if reasoning_content: + accumulated_reasoning += reasoning_content + # 如果设置了 remove_think,跳过 reasoning_content + if remove_think: + chunk_idx += 1 continue - if ((delta['content'] == '' or delta.get('content',None) is None) and - (delta.get('reasoning_content',None) is None or delta['reasoning_content'] == '') and - chunk_idx == 0): # 此处将第一条空消息排除,大部分模型第一条消息携带的是role,但是在role直接处理为ass + + # 第一次出现 reasoning_content,添加 开始标签 + if not thinking_started: + thinking_started = True + delta_content = '\n' + reasoning_content + else: + # 继续输出 reasoning_content + delta_content = reasoning_content + elif thinking_started and not thinking_ended and delta_content: + # reasoning_content 结束,normal content 开始,添加 结束标签 + thinking_ended = True + delta_content = '\n\n' + delta_content + + # 处理 content 中已有的 标签(如果需要移除) + if delta_content and remove_think and '' in delta_content: + import re + + # 移除 标签及其内容 + delta_content = re.sub(r'.*?', '', delta_content, flags=re.DOTALL) + + # 处理工具调用增量 + delta_tool_calls = None + if delta.get('tool_calls'): + delta_tool_calls = [] + for tool_call in delta['tool_calls']: + tc_id = tool_call.get('id') + if tc_id: + if tc_id not in tool_calls_map: + # 新的工具调用 + tool_calls_map[tc_id] = llm_entities.ToolCall( + id=tc_id, + type=tool_call.get('type', 'function'), + function=llm_entities.FunctionCall( + name=tool_call.get('function', {}).get('name', ''), + arguments=tool_call.get('function', {}).get('arguments', ''), + ), + ) + delta_tool_calls.append(tool_calls_map[tc_id]) + else: + # 追加函数参数 + func_args = tool_call.get('function', {}).get('arguments', '') + if func_args: + tool_calls_map[tc_id].function.arguments += func_args + # 返回更新后的完整工具调用 + delta_tool_calls.append(tool_calls_map[tc_id]) + + # 跳过空的第一个 chunk(只有 role 没有内容) + if chunk_idx == 0 and not delta_content and not reasoning_content and not delta.get('tool_calls'): + chunk_idx += 1 continue - # 处理流式消息 - delta_message,is_content,is_think = await self._make_msg_chunk(delta, - chunk_idx, - is_content, - is_think) - if delta_message.content: - current_content += delta_message.content - delta_message.content = current_content - # delta_message.all_content = current_content - if delta_message.tool_calls: - for tool_call in delta_message.tool_calls: - if tool_call.id not in tool_calls_map: - tool_calls_map[tool_call.id] = llm_entities.ToolCall( - id=tool_call.id, - type=tool_call.type, - function=llm_entities.FunctionCall( - name=tool_call.function.name if tool_call.function else '', arguments='' - ), - ) - if tool_call.function and tool_call.function.arguments: - # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 - tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + # 构建 MessageChunk - 只包含增量内容 + chunk_data = { + 'role': role, + 'content': delta_content if delta_content else None, + 'tool_calls': delta_tool_calls if delta_tool_calls else None, + 'is_final': bool(finish_reason), + } + + # 移除 None 值 + chunk_data = {k: v for k, v in chunk_data.items() if v is not None} + + yield llm_entities.MessageChunk(**chunk_data) chunk_idx += 1 - chunk_choices = getattr(chunk, 'choices', None) - if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): - delta_message.is_final = True - delta_message.content = current_content - - yield delta_message - # return async def _closure( self, diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index 1f17fafd..754082ea 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -113,6 +113,9 @@ class LocalAgentRunner(runner.RequestRunner): # 流式输出,需要处理工具调用 tool_calls_map: dict[str, llm_entities.ToolCall] = {} msg_idx = 0 + accumulated_content = '' # 从开始累积的所有内容 + last_role = 'assistant' + async for msg in query.use_llm_model.requester.invoke_llm_stream( query, query.use_llm_model, @@ -122,11 +125,18 @@ class LocalAgentRunner(runner.RequestRunner): remove_think=remove_think, ): msg_idx = msg_idx + 1 - tool_msg = msg - if msg_idx % 8 == 0 or msg.is_final: - yield msg - if tool_msg.tool_calls: - for tool_call in tool_msg.tool_calls: + + # 记录角色 + if msg.role: + last_role = msg.role + + # 累积内容 + if msg.content: + accumulated_content += msg.content + + # 处理工具调用 + if msg.tool_calls: + for tool_call in msg.tool_calls: if tool_call.id not in tool_calls_map: tool_calls_map[tool_call.id] = llm_entities.ToolCall( id=tool_call.id, @@ -138,10 +148,21 @@ class LocalAgentRunner(runner.RequestRunner): if tool_call.function and tool_call.function.arguments: # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + + # 每8个chunk或最后一个chunk时,输出所有累积的内容 + if msg_idx % 8 == 0 or msg.is_final: + yield llm_entities.MessageChunk( + role=last_role, + content=accumulated_content, # 输出所有累积内容 + tool_calls=list(tool_calls_map.values()) if (tool_calls_map and msg.is_final) else None, + is_final=msg.is_final, + ) + + # 创建最终消息用于后续处理 final_msg = llm_entities.MessageChunk( - role="tool", - content='', - tool_calls=list(tool_calls_map.values()), + role=last_role, + content=accumulated_content, + tool_calls=list(tool_calls_map.values()) if tool_calls_map else None, ) pending_tool_calls = final_msg.tool_calls @@ -178,7 +199,10 @@ class LocalAgentRunner(runner.RequestRunner): if is_stream: tool_calls_map = {} msg_idx = 0 - async for msg in await query.use_llm_model.requester.invoke_llm_stream( + accumulated_content = '' # 从开始累积的所有内容 + last_role = 'assistant' + + async for msg in query.use_llm_model.requester.invoke_llm_stream( query, query.use_llm_model, req_messages, @@ -187,11 +211,18 @@ class LocalAgentRunner(runner.RequestRunner): remove_think=remove_think, ): msg_idx += 1 - tool_msg = msg - if msg_idx % 8 == 0 or msg.is_final: - yield msg - if tool_msg.tool_calls: - for tool_call in tool_msg.tool_calls: + + # 记录角色 + if msg.role: + last_role = msg.role + + # 累积内容 + if msg.content: + accumulated_content += msg.content + + # 处理工具调用 + if msg.tool_calls: + for tool_call in msg.tool_calls: if tool_call.id not in tool_calls_map: tool_calls_map[tool_call.id] = llm_entities.ToolCall( id=tool_call.id, @@ -203,10 +234,20 @@ class LocalAgentRunner(runner.RequestRunner): if tool_call.function and tool_call.function.arguments: # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + + # 每8个chunk或最后一个chunk时,输出所有累积的内容 + if msg_idx % 8 == 0 or msg.is_final: + yield llm_entities.MessageChunk( + role=last_role, + content=accumulated_content, # 输出所有累积内容 + tool_calls=list(tool_calls_map.values()) if (tool_calls_map and msg.is_final) else None, + is_final=msg.is_final, + ) + final_msg = llm_entities.MessageChunk( - role="tool", - content='', - tool_calls=list(tool_calls_map.values()), + role=last_role, + content=accumulated_content, + tool_calls=list(tool_calls_map.values()) if tool_calls_map else None, ) else: # 处理完所有调用,再次请求 From eede354d3b8859944eb1e6b20bccce9355adbcc3 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Sat, 9 Aug 2025 02:46:13 +0800 Subject: [PATCH 074/107] fix:chatcmpl.py del content ,in the ppiochatcmpl.py and modelsopechatcmpl.py fun _closure_stream stream logic --- pkg/provider/modelmgr/requesters/chatcmpl.py | 10 +- .../modelmgr/requesters/modelscopechatcmpl.py | 137 ++++++++++++------ .../modelmgr/requesters/ppiochatcmpl.py | 102 ++++++++----- 3 files changed, 159 insertions(+), 90 deletions(-) diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index f8ea8593..adeaa251 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -201,11 +201,11 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): delta_content = '\n\n' + delta_content # 处理 content 中已有的 标签(如果需要移除) - if delta_content and remove_think and '' in delta_content: - import re - - # 移除 标签及其内容 - delta_content = re.sub(r'.*?', '', delta_content, flags=re.DOTALL) + # if delta_content and remove_think and '' in delta_content: + # import re + # + # # 移除 标签及其内容 + # delta_content = re.sub(r'.*?', '', delta_content, flags=re.DOTALL) # 处理工具调用增量 delta_tool_calls = None diff --git a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py index e02b0d07..0007623e 100644 --- a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py +++ b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py @@ -241,61 +241,106 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): del me['image_base64'] args['messages'] = messages - - current_content = '' args['stream'] = True - chunk_idx = 0 - is_content = False - is_think = False + + + # 流式处理状态 tool_calls_map: dict[str, llm_entities.ToolCall] = {} + chunk_idx = 0 + thinking_started = False + thinking_ended = False + role = 'assistant' # 默认角色 + accumulated_reasoning = '' # 仅用于判断何时结束思维链 + async for chunk in self._req_stream(args, extra_body=extra_args): - if hasattr(chunk, 'choices'): - # 完整响应模式 + # 解析 chunk 数据 + if hasattr(chunk, 'choices') and chunk.choices: choice = chunk.choices[0] - delta = choice.delta.model_dump() if hasattr(choice, 'delta') else choice.message.model_dump() + delta = choice.delta.model_dump() if hasattr(choice, 'delta') else {} + finish_reason = getattr(choice, 'finish_reason', None) else: - # 流式chunk模式 - delta = chunk.delta.model_dump() if hasattr(chunk, 'delta') else {} - reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None - delta['reasoning_content'] = None if reasoning_content == '' else reasoning_content # 直接不管有没有思考消息,构造一个,方便去除思考判断 - if remove_think: - if delta['reasoning_content'] is not None: + delta = {} + finish_reason = None + + # 从第一个 chunk 获取 role,后续使用这个 role + if 'role' in delta and delta['role']: + role = delta['role'] + + # 获取增量内容 + delta_content = delta.get('content', '') + reasoning_content = delta.get('reasoning_content', '') + + # 处理 reasoning_content + if reasoning_content: + accumulated_reasoning += reasoning_content + # 如果设置了 remove_think,跳过 reasoning_content + if remove_think: + chunk_idx += 1 continue - if ((delta['content'] == '' or delta.get('content', None) is None) and - (delta.get('reasoning_content', None) is None or delta['reasoning_content'] == '') and - chunk_idx == 0): # 此处将第一条空消息排除,大部分模型第一条消息携带的是role,但是在role直接处理为ass + + # 第一次出现 reasoning_content,添加 开始标签 + if not thinking_started: + thinking_started = True + delta_content = '\n' + reasoning_content + else: + # 继续输出 reasoning_content + delta_content = reasoning_content + elif thinking_started and not thinking_ended and delta_content: + # reasoning_content 结束,normal content 开始,添加 结束标签 + thinking_ended = True + delta_content = '\n\n' + delta_content + + # 处理 content 中已有的 标签(如果需要移除) + # if delta_content and remove_think and '' in delta_content: + # import re + # + # # 移除 标签及其内容 + # delta_content = re.sub(r'.*?', '', delta_content, flags=re.DOTALL) + + # 处理工具调用增量 + delta_tool_calls = None + if delta.get('tool_calls'): + delta_tool_calls = [] + for tool_call in delta['tool_calls']: + tc_id = tool_call.get('id') + if tc_id: + if tc_id not in tool_calls_map: + # 新的工具调用 + tool_calls_map[tc_id] = llm_entities.ToolCall( + id=tc_id, + type=tool_call.get('type', 'function'), + function=llm_entities.FunctionCall( + name=tool_call.get('function', {}).get('name', ''), + arguments=tool_call.get('function', {}).get('arguments', ''), + ), + ) + delta_tool_calls.append(tool_calls_map[tc_id]) + else: + # 追加函数参数 + func_args = tool_call.get('function', {}).get('arguments', '') + if func_args: + tool_calls_map[tc_id].function.arguments += func_args + # 返回更新后的完整工具调用 + delta_tool_calls.append(tool_calls_map[tc_id]) + + # 跳过空的第一个 chunk(只有 role 没有内容) + if chunk_idx == 0 and not delta_content and not reasoning_content and not delta.get('tool_calls'): + chunk_idx += 1 continue - # 处理流式消息 - delta_message, is_content, is_think = await self._make_msg_chunk(delta, - chunk_idx, - is_content, - is_think) - # 处理流式消息 - if delta_message.content: - current_content += delta_message.content - delta_message.content = current_content - # delta_message.all_content = current_content - if delta_message.tool_calls: - for tool_call in delta_message.tool_calls: - if tool_call.id not in tool_calls_map: - tool_calls_map[tool_call.id] = llm_entities.ToolCall( - id=tool_call.id, - type=tool_call.type, - function=llm_entities.FunctionCall( - name=tool_call.function.name if tool_call.function else '', arguments='' - ), - ) - if tool_call.function and tool_call.function.arguments: - # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 - tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + # 构建 MessageChunk - 只包含增量内容 + chunk_data = { + 'role': role, + 'content': delta_content if delta_content else None, + 'tool_calls': delta_tool_calls if delta_tool_calls else None, + 'is_final': bool(finish_reason), + } + + # 移除 None 值 + chunk_data = {k: v for k, v in chunk_data.items() if v is not None} + + yield llm_entities.MessageChunk(**chunk_data) chunk_idx += 1 - chunk_choices = getattr(chunk, 'choices', None) - if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): - delta_message.is_final = True - delta_message.content = current_content - - yield delta_message # return async def invoke_llm( diff --git a/pkg/provider/modelmgr/requesters/ppiochatcmpl.py b/pkg/provider/modelmgr/requesters/ppiochatcmpl.py index 68acae81..49f03143 100644 --- a/pkg/provider/modelmgr/requesters/ppiochatcmpl.py +++ b/pkg/provider/modelmgr/requesters/ppiochatcmpl.py @@ -112,24 +112,32 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): del me['image_base64'] args['messages'] = messages - - current_content = '' args['stream'] = True - chunk_idx = 0 - is_think = False + tool_calls_map: dict[str, llm_entities.ToolCall] = {} + chunk_idx = 0 + thinking_started = False + thinking_ended = False + role = 'assistant' # 默认角色 + accumulated_reasoning = '' # 仅用于判断何时结束思维链 async for chunk in self._req_stream(args, extra_body=extra_args): - # 处理流式消息 - if hasattr(chunk, 'choices'): - # 完整响应模式 - if chunk.choices: - choice = chunk.choices[0] - delta = choice.delta.model_dump() if hasattr(choice, 'delta') else choice.message.model_dump() - else: - continue + # 解析 chunk 数据 + if hasattr(chunk, 'choices') and chunk.choices: + choice = chunk.choices[0] + delta = choice.delta.model_dump() if hasattr(choice, 'delta') else {} + finish_reason = getattr(choice, 'finish_reason', None) else: - # 流式chunk模式 - delta = chunk.delta.model_dump() if hasattr(chunk, 'delta') else {} + delta = {} + finish_reason = None + + # 从第一个 chunk 获取 role,后续使用这个 role + if 'role' in delta and delta['role']: + role = delta['role'] + + # 获取增量内容 + delta_content = delta.get('content', '') + # reasoning_content = delta.get('reasoning_content', '') + if remove_think: if delta['content'] is not None: if '' in delta['content']: @@ -141,30 +149,46 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): elif is_think or delta['content'] == '\n\n': continue - delta_message = await self._make_msg_chunk(delta, chunk_idx) - # 处理流式消息 - if delta_message.content: - current_content += delta_message.content - delta_message.content = current_content - # delta_message.all_content = current_content - if delta_message.tool_calls: - for tool_call in delta_message.tool_calls: - if tool_call.id not in tool_calls_map: - tool_calls_map[tool_call.id] = llm_entities.ToolCall( - id=tool_call.id, - type=tool_call.type, - function=llm_entities.FunctionCall( - name=tool_call.function.name if tool_call.function else '', arguments='' - ), - ) - if tool_call.function and tool_call.function.arguments: - # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 - tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments + delta_tool_calls = None + if delta.get('tool_calls'): + delta_tool_calls = [] + for tool_call in delta['tool_calls']: + tc_id = tool_call.get('id') + if tc_id: + if tc_id not in tool_calls_map: + # 新的工具调用 + tool_calls_map[tc_id] = llm_entities.ToolCall( + id=tc_id, + type=tool_call.get('type', 'function'), + function=llm_entities.FunctionCall( + name=tool_call.get('function', {}).get('name', ''), + arguments=tool_call.get('function', {}).get('arguments', ''), + ), + ) + delta_tool_calls.append(tool_calls_map[tc_id]) + else: + # 追加函数参数 + func_args = tool_call.get('function', {}).get('arguments', '') + if func_args: + tool_calls_map[tc_id].function.arguments += func_args + # 返回更新后的完整工具调用 + delta_tool_calls.append(tool_calls_map[tc_id]) + # 跳过空的第一个 chunk(只有 role 没有内容) + if chunk_idx == 0 and not delta_content and not delta.get('tool_calls'): + chunk_idx += 1 + continue + + # 构建 MessageChunk - 只包含增量内容 + chunk_data = { + 'role': role, + 'content': delta_content if delta_content else None, + 'tool_calls': delta_tool_calls if delta_tool_calls else None, + 'is_final': bool(finish_reason), + } + + # 移除 None 值 + chunk_data = {k: v for k, v in chunk_data.items() if v is not None} + + yield llm_entities.MessageChunk(**chunk_data) chunk_idx += 1 - chunk_choices = getattr(chunk, 'choices', None) - if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): - delta_message.is_final = True - delta_message.content = current_content - - yield delta_message From 7c59bc1ce504bc3cd2efd262a6e5a5ef6aa49aa8 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Sun, 10 Aug 2025 00:09:19 +0800 Subject: [PATCH 075/107] feat:add anthropic stream ouput --- .../modelmgr/requesters/anthropicmsgs.py | 184 +++++++++++++++++- 1 file changed, 182 insertions(+), 2 deletions(-) diff --git a/pkg/provider/modelmgr/requesters/anthropicmsgs.py b/pkg/provider/modelmgr/requesters/anthropicmsgs.py index e0850c03..0c73068c 100644 --- a/pkg/provider/modelmgr/requesters/anthropicmsgs.py +++ b/pkg/provider/modelmgr/requesters/anthropicmsgs.py @@ -135,6 +135,14 @@ class AnthropicMessages(requester.ProviderAPIRequester): args['messages'] = req_messages + if args["thinking"]: + args['thinking'] = { + "type": "enabled", + "budget_tokens": 10000 + } + else: + args.pop('thinking') + if funcs: tools = await self.ap.tool_mgr.generate_tools_for_anthropic(funcs) @@ -148,12 +156,12 @@ class AnthropicMessages(requester.ProviderAPIRequester): 'content': '', 'role': resp.role, } - + print(type(resp)) assert type(resp) is anthropic.types.message.Message for block in resp.content: if not remove_think and block.type == 'thinking': - args['content'] = '' + block.thinking + '\n' + args['content'] + args['content'] = '\n' + block.thinking + '\n\n' + args['content'] elif block.type == 'text': args['content'] += block.text elif block.type == 'tool_use': @@ -177,3 +185,175 @@ class AnthropicMessages(requester.ProviderAPIRequester): raise errors.RequesterError(f'模型无效: {e.message}') else: raise errors.RequesterError(f'请求地址无效: {e.message}') + + + async def invoke_llm_stream( + self, + query: core_entities.Query, + model: requester.RuntimeLLMModel, + messages: typing.List[llm_entities.Message], + funcs: typing.List[tools_entities.LLMFunction] = None, + extra_args: dict[str, typing.Any] = {}, + remove_think: bool = False, + ) -> llm_entities.Message: + self.client.api_key = model.token_mgr.get_token() + + args = extra_args.copy() + args['model'] = model.model_entity.name + args['stream'] = True + + # 处理消息 + + # system + system_role_message = None + + for i, m in enumerate(messages): + if m.role == 'system': + system_role_message = m + + break + + if system_role_message: + messages.pop(i) + + if isinstance(system_role_message, llm_entities.Message) and isinstance(system_role_message.content, str): + args['system'] = system_role_message.content + + req_messages = [] + + for m in messages: + if m.role == 'tool': + tool_call_id = m.tool_call_id + + req_messages.append( + { + 'role': 'user', + 'content': [ + { + 'type': 'tool_result', + 'tool_use_id': tool_call_id, + 'content': m.content, + } + ], + } + ) + + continue + + msg_dict = m.dict(exclude_none=True) + + if isinstance(m.content, str) and m.content.strip() != '': + msg_dict['content'] = [{'type': 'text', 'text': m.content}] + elif isinstance(m.content, list): + for i, ce in enumerate(m.content): + if ce.type == 'image_base64': + image_b64, image_format = await image.extract_b64_and_format(ce.image_base64) + + alter_image_ele = { + 'type': 'image', + 'source': { + 'type': 'base64', + 'media_type': f'image/{image_format}', + 'data': image_b64, + }, + } + msg_dict['content'][i] = alter_image_ele + + if m.tool_calls: + for tool_call in m.tool_calls: + msg_dict['content'].append( + { + 'type': 'tool_use', + 'id': tool_call.id, + 'name': tool_call.function.name, + 'input': json.loads(tool_call.function.arguments), + } + ) + + del msg_dict['tool_calls'] + + req_messages.append(msg_dict) + if args["thinking"]: + args['thinking'] = { + "type": "enabled", + "budget_tokens": 10000 + } + else: + args.pop('thinking') + + args['messages'] = req_messages + + if funcs: + tools = await self.ap.tool_mgr.generate_tools_for_anthropic(funcs) + + if tools: + args['tools'] = tools + + try: + role = 'assistant' # 默认角色 + # chunk_idx = 0 + think_started = False + think_ended = False + finish_reason = False + content = '' + async for chunk in await self.client.messages.create(**args): + # print(chunk) + if isinstance(chunk, anthropic.types.raw_content_block_start_event.RawContentBlockStartEvent): # 记录开始 + if chunk.content_block.type == 'thinking' and not remove_think: + think_started = True + continue + elif chunk.content_block.type == 'text' and not remove_think: + think_ended = True + continue + elif isinstance(chunk, anthropic.types.raw_content_block_delta_event.RawContentBlockDeltaEvent): + if chunk.delta.type == "thinking_delta": + if think_started: + think_started = False + content = '\n' + chunk.delta.thinking + elif remove_think: + continue + else: + content = chunk.delta.thinking + elif chunk.delta.type == "text_delta": + if think_ended: + think_ended = False + content = '\n\n' + chunk.delta.text + else: + content = chunk.delta.text + elif isinstance(chunk, anthropic.types.raw_content_block_stop_event.RawContentBlockStopEvent): + continue # 记录raw_content_block结束的 + + elif isinstance(chunk, anthropic.types.raw_message_delta_event.RawMessageDeltaEvent): + if chunk.delta.stop_reason == "end_turn": + finish_reason = True + elif isinstance(chunk, anthropic.types.raw_message_stop_event.RawMessageStopEvent): + continue # 这个好像是完全结束 + else: + print(chunk) + continue + + + args = { + 'content': content, + 'role': role, + "is_final": finish_reason + } + # if chunk_idx == 0: + # chunk_idx += 1 + # continue + + # assert type(chunk) is anthropic.types.message.Chunk + + + yield llm_entities.MessageChunk(**args) + + # return llm_entities.Message(**args) + except anthropic.AuthenticationError as e: + raise errors.RequesterError(f'api-key 无效: {e.message}') + except anthropic.BadRequestError as e: + raise errors.RequesterError(str(e.message)) + except anthropic.NotFoundError as e: + if 'model: ' in str(e): + raise errors.RequesterError(f'模型无效: {e.message}') + else: + raise errors.RequesterError(f'请求地址无效: {e.message}') From 8c5cb718121a7c8107489702500f2fb1c6721c64 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Sun, 10 Aug 2025 00:16:13 +0800 Subject: [PATCH 076/107] fix:del the chatcmpl.py useless logic,and in the modelscopechatcmpl.py Non-streaming add and del logic,and fix the ppiochatcmpl.py stream logic and the giteeaichatcmpl.py inherit ppiochatcmpl.py --- pkg/provider/modelmgr/requesters/chatcmpl.py | 5 +- .../modelmgr/requesters/giteeaichatcmpl.py | 182 +----------------- .../modelmgr/requesters/modelscopechatcmpl.py | 69 +++---- .../modelmgr/requesters/ppiochatcmpl.py | 58 ++++-- 4 files changed, 78 insertions(+), 236 deletions(-) diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index adeaa251..2d2a0b7e 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -160,7 +160,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): thinking_started = False thinking_ended = False role = 'assistant' # 默认角色 - accumulated_reasoning = '' # 仅用于判断何时结束思维链 + # accumulated_reasoning = '' # 仅用于判断何时结束思维链 async for chunk in self._req_stream(args, extra_body=extra_args): # 解析 chunk 数据 @@ -182,7 +182,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): # 处理 reasoning_content if reasoning_content: - accumulated_reasoning += reasoning_content + # accumulated_reasoning += reasoning_content # 如果设置了 remove_think,跳过 reasoning_content if remove_think: chunk_idx += 1 @@ -289,6 +289,7 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): # 发送请求 resp = await self._req(args, extra_body=extra_args) + print(resp) # 处理请求结果 message = await self._make_msg(resp, remove_think) diff --git a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py index 0ff49798..f8cf15ca 100644 --- a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py +++ b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py @@ -3,7 +3,7 @@ from __future__ import annotations import typing -from . import chatcmpl +from . import ppiochatcmpl from .. import requester from ....core import entities as core_entities from ... import entities as llm_entities @@ -12,7 +12,7 @@ import re import openai.types.chat.chat_completion as chat_completion -class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): +class GiteeAIChatCompletions(ppiochatcmpl.PPIOChatCompletions): """Gitee AI ChatCompletions API 请求器""" default_config: dict[str, typing.Any] = { @@ -20,181 +20,3 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): 'timeout': 120, } - async def _closure( - self, - query: core_entities.Query, - req_messages: list[dict], - use_model: requester.RuntimeLLMModel, - use_funcs: list[tools_entities.LLMFunction] = None, - extra_args: dict[str, typing.Any] = {}, - remove_think: bool = False, - ) -> llm_entities.Message: - self.client.api_key = use_model.token_mgr.get_token() - - args = {} - args['model'] = use_model.model_entity.name - - if use_funcs: - tools = await self.ap.tool_mgr.generate_tools_for_openai(use_funcs) - - if tools: - args['tools'] = tools - - # gitee 不支持多模态,把content都转换成纯文字 - for m in req_messages: - if 'content' in m and isinstance(m['content'], list): - m['content'] = ' '.join([c['text'] for c in m['content']]) - - args['messages'] = req_messages - - resp = await self._req(args, extra_body=extra_args) - - - message = await self._make_msg(resp, remove_think) - - return message - - async def _make_msg( - self, - chat_completion: chat_completion.ChatCompletion, - remove_think: bool, - ) -> llm_entities.Message: - chatcmpl_message = chat_completion.choices[0].message.model_dump() - # print(chatcmpl_message.keys(), chatcmpl_message.values()) - - # 确保 role 字段存在且不为 None - if 'role' not in chatcmpl_message or chatcmpl_message['role'] is None: - chatcmpl_message['role'] = 'assistant' - - reasoning_content = chatcmpl_message['reasoning_content'] if 'reasoning_content' in chatcmpl_message else None - - # deepseek的reasoner模型 - if remove_think: - chatcmpl_message['content'] = re.sub( - r'.*?', '', chatcmpl_message['content'], flags=re.DOTALL - ) - else: - if reasoning_content is not None: - chatcmpl_message['content'] = ( - '\n' + reasoning_content + '\n\n' + chatcmpl_message['content'] - ) - - message = llm_entities.Message(**chatcmpl_message) - - return message - - async def _make_msg_chunk( - self, - delta: dict[str, typing.Any], - idx: int, - ) -> llm_entities.MessageChunk: - # 处理流式chunk和完整响应的差异 - # print(chat_completion.choices[0]) - - - # 确保 role 字段存在且不为 None - if 'role' not in delta or delta['role'] is None: - delta['role'] = 'assistant' - - reasoning_content = delta['reasoning_content'] if 'reasoning_content' in delta else None - - delta['content'] = '' if delta['content'] is None else delta['content'] - # print(reasoning_content) - - # deepseek的reasoner模型 - - if reasoning_content is not None: - delta['content'] += reasoning_content - - message = llm_entities.MessageChunk(**delta) - - return message - - async def _closure_stream( - self, - query: core_entities.Query, - req_messages: list[dict], - use_model: requester.RuntimeLLMModel, - use_funcs: list[tools_entities.LLMFunction] = None, - extra_args: dict[str, typing.Any] = {}, - remove_think: bool = False, - ) -> llm_entities.Message | typing.AsyncGenerator[llm_entities.MessageChunk, None]: - self.client.api_key = use_model.token_mgr.get_token() - - args = {} - args['model'] = use_model.model_entity.name - - if use_funcs: - tools = await self.ap.tool_mgr.generate_tools_for_openai(use_funcs) - - if tools: - args['tools'] = tools - - # 设置此次请求中的messages - messages = req_messages.copy() - - # 检查vision - for msg in messages: - if 'content' in msg and isinstance(msg['content'], list): - for me in msg['content']: - if me['type'] == 'image_base64': - me['image_url'] = {'url': me['image_base64']} - me['type'] = 'image_url' - del me['image_base64'] - - args['messages'] = messages - - current_content = '' - args['stream'] = True - chunk_idx = 0 - is_think = False - tool_calls_map: dict[str, llm_entities.ToolCall] = {} - async for chunk in self._req_stream(args, extra_body=extra_args): - # 处理流式消息 - if hasattr(chunk, 'choices'): - # 完整响应模式 - if chunk.choices: - choice = chunk.choices[0] - delta = choice.delta.model_dump() if hasattr(choice, 'delta') else choice.message.model_dump() - else: - continue - else: - # 流式chunk模式 - delta = chunk.delta.model_dump() if hasattr(chunk, 'delta') else {} - if remove_think: - print(delta) - if delta['content'] == '': - is_think = True - continue - elif delta['content'] == r'': - is_think = False - continue - elif is_think or delta['content'] == '\n\n': - continue - - delta_message = await self._make_msg_chunk(delta, chunk_idx) - if delta_message.content: - current_content += delta_message.content - delta_message.content = current_content - # delta_message.all_content = current_content - if delta_message.tool_calls: - for tool_call in delta_message.tool_calls: - if tool_call.id not in tool_calls_map: - tool_calls_map[tool_call.id] = llm_entities.ToolCall( - id=tool_call.id, - type=tool_call.type, - function=llm_entities.FunctionCall( - name=tool_call.function.name if tool_call.function else '', arguments='' - ), - ) - if tool_call.function and tool_call.function.arguments: - # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 - tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments - - chunk_idx += 1 - chunk_choices = getattr(chunk, 'choices', None) - if chunk_choices and getattr(chunk_choices[0], 'finish_reason', None): - delta_message.is_final = True - delta_message.content = current_content - - yield delta_message diff --git a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py index 0007623e..c526313a 100644 --- a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py +++ b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py @@ -36,6 +36,7 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): self, args: dict, extra_body: dict = {}, + remove_think:bool = False, ) -> chat_completion.ChatCompletion: args['stream'] = True @@ -47,11 +48,35 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): resp_gen: openai.AsyncStream = await self.client.chat.completions.create(**args, extra_body=extra_body) + chunk_idx = 0 + thinking_started = False + thinking_ended = False async for chunk in resp_gen: # print(chunk) if not chunk or not chunk.id or not chunk.choices or not chunk.choices[0] or not chunk.choices[0].delta: continue + reasoning_content = chunk.choices[0].delta.reasoning_content + # 处理 reasoning_content + if reasoning_content: + # accumulated_reasoning += reasoning_content + # 如果设置了 remove_think,跳过 reasoning_content + if remove_think: + chunk_idx += 1 + continue + + # 第一次出现 reasoning_content,添加 开始标签 + if not thinking_started: + thinking_started = True + pending_content += '\n' + reasoning_content + else: + # 继续输出 reasoning_content + pending_content += reasoning_content + elif thinking_started and not thinking_ended and chunk.choices[0].delta.content: + # reasoning_content 结束,normal content 开始,添加 结束标签 + thinking_ended = True + pending_content += '\n\n' + chunk.choices[0].delta.content + if chunk.choices[0].delta.content is not None: pending_content += chunk.choices[0].delta.content @@ -130,6 +155,7 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): use_model: requester.RuntimeLLMModel, use_funcs: list[tools_entities.LLMFunction] = None, extra_args: dict[str, typing.Any] = {}, + remove_think:bool = False, ) -> llm_entities.Message: self.client.api_key = use_model.token_mgr.get_token() @@ -157,7 +183,7 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): args['messages'] = messages # 发送请求 - resp = await self._req(args, extra_body=extra_args) + resp = await self._req(args, extra_body=extra_args, remove_think=remove_think) # 处理请求结果 message = await self._make_msg(resp) @@ -172,41 +198,6 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): async for chunk in await self.client.chat.completions.create(**args, extra_body=extra_body): yield chunk - async def _make_msg_chunk(self, - delta: dict[str, typing.Any], - idx: int, - is_content: bool, - is_think: bool, - ) -> llm_entities.MessageChunk: - # 处理流式chunk和完整响应的差异 - # print(chat_completion.choices[0]) - - if 'role' not in delta or delta['role'] is None: - delta['role'] = 'assistant' - - reasoning_content = delta['reasoning_content'] - - delta['content'] = '' if delta['content'] is None else delta['content'] - # print(reasoning_content) - - # deepseek的reasoner模型 - - if reasoning_content is not None and idx == 0: - delta['content'] += f'\n{reasoning_content}' - is_think = True - elif reasoning_content is None and idx != 0: - if is_content: - delta['content'] = delta['content'] - elif is_think: - delta['content'] = f'\n\n\n{delta["content"]}' - is_content = True - is_think = False - elif reasoning_content is not None: - delta['content'] = reasoning_content - - message = llm_entities.MessageChunk(**delta) - - return message, is_content, is_think async def _closure_stream( self, @@ -250,7 +241,7 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): thinking_started = False thinking_ended = False role = 'assistant' # 默认角色 - accumulated_reasoning = '' # 仅用于判断何时结束思维链 + # accumulated_reasoning = '' # 仅用于判断何时结束思维链 async for chunk in self._req_stream(args, extra_body=extra_args): # 解析 chunk 数据 @@ -272,7 +263,7 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): # 处理 reasoning_content if reasoning_content: - accumulated_reasoning += reasoning_content + # accumulated_reasoning += reasoning_content # 如果设置了 remove_think,跳过 reasoning_content if remove_think: chunk_idx += 1 @@ -365,7 +356,7 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): try: return await self._closure( - query=query, req_messages=req_messages, use_model=model, use_funcs=funcs, extra_args=extra_args + query=query, req_messages=req_messages, use_model=model, use_funcs=funcs, extra_args=extra_args, remove_think=remove_think ) except asyncio.TimeoutError: raise errors.RequesterError('请求超时') diff --git a/pkg/provider/modelmgr/requesters/ppiochatcmpl.py b/pkg/provider/modelmgr/requesters/ppiochatcmpl.py index 49f03143..967bb676 100644 --- a/pkg/provider/modelmgr/requesters/ppiochatcmpl.py +++ b/pkg/provider/modelmgr/requesters/ppiochatcmpl.py @@ -39,20 +39,45 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): reasoning_content = chatcmpl_message['reasoning_content'] if 'reasoning_content' in chatcmpl_message else None # deepseek的reasoner模型 - if remove_think: - chatcmpl_message['content'] = re.sub( - r'.*?', '', chatcmpl_message['content'], flags=re.DOTALL - ) - else: - if reasoning_content is not None: - chatcmpl_message['content'] = ( - '\n' + reasoning_content + '\n\n' + chatcmpl_message['content'] - ) + chatcmpl_message["content"] = await self._process_thinking_content( + chatcmpl_message['content'],reasoning_content,remove_think) + + # 移除 reasoning_content 字段,避免传递给 Message + if 'reasoning_content' in chatcmpl_message: + del chatcmpl_message['reasoning_content'] + message = llm_entities.Message(**chatcmpl_message) return message + async def _process_thinking_content( + self, + content: str, + reasoning_content: str = None, + remove_think: bool = False, + ) -> tuple[str, str]: + """处理思维链内容 + + Args: + content: 原始内容 + reasoning_content: reasoning_content 字段内容 + remove_think: 是否移除思维链 + + Returns: + 处理后的内容 + """ + if remove_think: + content = re.sub( + r'.*?', '', content, flags=re.DOTALL + ) + else: + if reasoning_content is not None: + content = ( + '\n' + reasoning_content + '\n\n' + content + ) + return content + async def _make_msg_chunk( self, delta: dict[str, typing.Any], @@ -119,7 +144,6 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): thinking_started = False thinking_ended = False role = 'assistant' # 默认角色 - accumulated_reasoning = '' # 仅用于判断何时结束思维链 async for chunk in self._req_stream(args, extra_body=extra_args): # 解析 chunk 数据 if hasattr(chunk, 'choices') and chunk.choices: @@ -140,14 +164,18 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): if remove_think: if delta['content'] is not None: - if '' in delta['content']: - is_think = True + if '' in delta['content'] and not thinking_started and not thinking_ended: + thinking_started = True continue - elif delta['content'] == r'': - is_think = False + elif delta['content'] == r'' and not thinking_ended: + thinking_ended = True continue - elif is_think or delta['content'] == '\n\n': + elif thinking_ended and delta['content'] == '\n\n' and thinking_started: + thinking_started = False continue + elif thinking_started and not thinking_ended: + continue + delta_tool_calls = None if delta.get('tool_calls'): From 03b11481ed03d890446e5d811ddcca446c403724 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Sun, 10 Aug 2025 00:28:55 +0800 Subject: [PATCH 077/107] fix:fix remove_think logic, and end fix --- pkg/provider/modelmgr/requesters/anthropicmsgs.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/provider/modelmgr/requesters/anthropicmsgs.py b/pkg/provider/modelmgr/requesters/anthropicmsgs.py index 0c73068c..f89fb136 100644 --- a/pkg/provider/modelmgr/requesters/anthropicmsgs.py +++ b/pkg/provider/modelmgr/requesters/anthropicmsgs.py @@ -299,12 +299,12 @@ class AnthropicMessages(requester.ProviderAPIRequester): async for chunk in await self.client.messages.create(**args): # print(chunk) if isinstance(chunk, anthropic.types.raw_content_block_start_event.RawContentBlockStartEvent): # 记录开始 - if chunk.content_block.type == 'thinking' and not remove_think: - think_started = True - continue - elif chunk.content_block.type == 'text' and not remove_think: - think_ended = True - continue + if not remove_think: + if chunk.content_block.type == 'thinking': + think_started = True + elif chunk.content_block.type == 'text': + think_ended = True + continue elif isinstance(chunk, anthropic.types.raw_content_block_delta_event.RawContentBlockDeltaEvent): if chunk.delta.type == "thinking_delta": if think_started: @@ -317,7 +317,7 @@ class AnthropicMessages(requester.ProviderAPIRequester): elif chunk.delta.type == "text_delta": if think_ended: think_ended = False - content = '\n\n' + chunk.delta.text + content = '\n\n' + chunk.delta.text else: content = chunk.delta.text elif isinstance(chunk, anthropic.types.raw_content_block_stop_event.RawContentBlockStopEvent): From 46452de7b5d8990f2668f3753ded8763fb479f43 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Sun, 10 Aug 2025 23:14:57 +0800 Subject: [PATCH 078/107] fix:The handling of the streaming tool calls has been fixed, but there are still bugs in the model's reply messages with thoughtfulness. --- .../controller/groups/pipelines/webchat.py | 11 ++++- pkg/pipeline/pipelinemgr.py | 21 +++++++--- pkg/pipeline/process/handlers/chat.py | 4 +- pkg/pipeline/respback/respback.py | 1 + .../modelmgr/requesters/anthropicmsgs.py | 37 ++++++++++++---- pkg/provider/modelmgr/requesters/chatcmpl.py | 42 +++++++------------ .../modelmgr/requesters/modelscopechatcmpl.py | 32 ++++---------- .../modelmgr/requesters/ppiochatcmpl.py | 37 +++++++--------- pkg/provider/runners/localagent.py | 22 ++++++---- web/src/app/infra/http/HttpClient.ts | 3 ++ 10 files changed, 112 insertions(+), 98 deletions(-) diff --git a/pkg/api/http/controller/groups/pipelines/webchat.py b/pkg/api/http/controller/groups/pipelines/webchat.py index c094731b..ae201934 100644 --- a/pkg/api/http/controller/groups/pipelines/webchat.py +++ b/pkg/api/http/controller/groups/pipelines/webchat.py @@ -13,6 +13,7 @@ class WebChatDebugRouterGroup(group.RouterGroup): """Send a message to the pipeline for debugging""" async def stream_generator(generator): + yield 'data: {"type": "start"}\n\n' async for message in generator: yield f'data: {json.dumps({"message": message})}\n\n' yield 'data: {"type": "end"}\n\n' @@ -38,8 +39,14 @@ class WebChatDebugRouterGroup(group.RouterGroup): generator = webchat_adapter.send_webchat_message( pipeline_uuid, session_type, message_chain_obj, is_stream ) - - return quart.Response(stream_generator(generator), mimetype='text/event-stream') + # 设置正确的响应头 + headers = { + 'Content-Type': 'text/event-stream', + 'Transfer-Encoding': 'chunked', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive' + } + return quart.Response(stream_generator(generator), mimetype='text/event-stream',headers=headers) else: # result = await webchat_adapter.send_webchat_message(pipeline_uuid, session_type, message_chain_obj) diff --git a/pkg/pipeline/pipelinemgr.py b/pkg/pipeline/pipelinemgr.py index 77df09dc..79bd4ec6 100644 --- a/pkg/pipeline/pipelinemgr.py +++ b/pkg/pipeline/pipelinemgr.py @@ -93,12 +93,21 @@ class RuntimePipeline: query.message_event, platform_events.GroupMessage ): result.user_notice.insert(0, platform_message.At(query.message_event.sender.id)) - - await query.adapter.reply_message( - message_source=query.message_event, - message=result.user_notice, - quote_origin=query.pipeline_config['output']['misc']['quote-origin'], - ) + if await query.adapter.is_stream_output_supported(): + print(query.resp_messages[-1]) + await query.adapter.reply_message_chunk( + message_source=query.message_event, + message_id=str(query.resp_messages[-1].resp_message_id), + message=result.user_notice, + quote_origin=query.pipeline_config['output']['misc']['quote-origin'], + is_final=[msg.is_final for msg in query.resp_messages][0] + ) + else: + await query.adapter.reply_message( + message_source=query.message_event, + message=result.user_notice, + quote_origin=query.pipeline_config['output']['misc']['quote-origin'], + ) if result.debug_notice: self.ap.logger.debug(result.debug_notice) if result.console_notice: diff --git a/pkg/pipeline/process/handlers/chat.py b/pkg/pipeline/process/handlers/chat.py index 6c428473..e913bbc2 100644 --- a/pkg/pipeline/process/handlers/chat.py +++ b/pkg/pipeline/process/handlers/chat.py @@ -72,9 +72,9 @@ class ChatMessageHandler(handler.MessageHandler): raise ValueError(f'未找到请求运行器: {query.pipeline_config["ai"]["runner"]["runner"]}') if is_stream: resp_message_id = uuid.uuid4() - await query.adapter.create_message_card(resp_message_id, query.message_event) + await query.adapter.create_message_card(str(resp_message_id), query.message_event) async for result in runner.run(query): - result.resp_message_id = resp_message_id + result.resp_message_id = str(resp_message_id) if query.resp_messages: query.resp_messages.pop() if query.resp_message_chain: diff --git a/pkg/pipeline/respback/respback.py b/pkg/pipeline/respback/respback.py index bc91dffe..5683ce92 100644 --- a/pkg/pipeline/respback/respback.py +++ b/pkg/pipeline/respback/respback.py @@ -41,6 +41,7 @@ class SendResponseBackStage(stage.PipelineStage): # TODO 命令与流式的兼容性问题 if await query.adapter.is_stream_output_supported(): is_final = [msg.is_final for msg in query.resp_messages][0] + print(query.resp_messages[-1]) await query.adapter.reply_message_chunk( message_source=query.message_event, message_id=query.resp_messages[-1].resp_message_id, diff --git a/pkg/provider/modelmgr/requesters/anthropicmsgs.py b/pkg/provider/modelmgr/requesters/anthropicmsgs.py index f89fb136..a337af4f 100644 --- a/pkg/provider/modelmgr/requesters/anthropicmsgs.py +++ b/pkg/provider/modelmgr/requesters/anthropicmsgs.py @@ -273,13 +273,11 @@ class AnthropicMessages(requester.ProviderAPIRequester): del msg_dict['tool_calls'] req_messages.append(msg_dict) - if args["thinking"]: + if args.get("thinking", False): args['thinking'] = { "type": "enabled", "budget_tokens": 10000 } - else: - args.pop('thinking') args['messages'] = req_messages @@ -296,15 +294,32 @@ class AnthropicMessages(requester.ProviderAPIRequester): think_ended = False finish_reason = False content = '' + tool_name = '' + tool_id = '' + tool_calls = [] async for chunk in await self.client.messages.create(**args): - # print(chunk) + print(chunk) + tool_call = {"id":None, 'function': {"name": None, "arguments": None},'type':'function'} if isinstance(chunk, anthropic.types.raw_content_block_start_event.RawContentBlockStartEvent): # 记录开始 + if chunk.content_block.type == 'tool_use': + + if chunk.content_block.name is not None: + tool_name = chunk.content_block.name + if chunk.content_block.id is not None: + tool_id = chunk.content_block.id + + + tool_call['function']['name'] = tool_name + tool_call['function']['arguments'] = '' + tool_call['id'] = tool_id + + print(chunk.content_block) if not remove_think: - if chunk.content_block.type == 'thinking': + if chunk.content_block.type == 'thinking' and not remove_think: think_started = True - elif chunk.content_block.type == 'text': + elif chunk.content_block.type == 'text' and chunk.index != 0 and not remove_think: think_ended = True - continue + continue elif isinstance(chunk, anthropic.types.raw_content_block_delta_event.RawContentBlockDeltaEvent): if chunk.delta.type == "thinking_delta": if think_started: @@ -320,6 +335,10 @@ class AnthropicMessages(requester.ProviderAPIRequester): content = '\n\n' + chunk.delta.text else: content = chunk.delta.text + elif chunk.delta.type == "input_json_delta": + tool_call['function']["arguments"] = chunk.delta.partial_json + tool_call['function']['name'] = tool_name + tool_call['id'] = tool_id elif isinstance(chunk, anthropic.types.raw_content_block_stop_event.RawContentBlockStopEvent): continue # 记录raw_content_block结束的 @@ -333,10 +352,12 @@ class AnthropicMessages(requester.ProviderAPIRequester): continue + args = { 'content': content, 'role': role, - "is_final": finish_reason + "is_final": finish_reason, + 'tool_calls': None if tool_call['id'] is None else [tool_call], } # if chunk_idx == 0: # chunk_idx += 1 diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index 2d2a0b7e..7afda84f 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -160,18 +160,21 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): thinking_started = False thinking_ended = False role = 'assistant' # 默认角色 + tool_id = "" + tool_name = '' # accumulated_reasoning = '' # 仅用于判断何时结束思维链 async for chunk in self._req_stream(args, extra_body=extra_args): # 解析 chunk 数据 + if hasattr(chunk, 'choices') and chunk.choices: choice = chunk.choices[0] delta = choice.delta.model_dump() if hasattr(choice, 'delta') else {} + finish_reason = getattr(choice, 'finish_reason', None) else: delta = {} finish_reason = None - # 从第一个 chunk 获取 role,后续使用这个 role if 'role' in delta and delta['role']: role = delta['role'] @@ -208,41 +211,29 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): # delta_content = re.sub(r'.*?', '', delta_content, flags=re.DOTALL) # 处理工具调用增量 - delta_tool_calls = None + # delta_tool_calls = None if delta.get('tool_calls'): - delta_tool_calls = [] for tool_call in delta['tool_calls']: - tc_id = tool_call.get('id') - if tc_id: - if tc_id not in tool_calls_map: - # 新的工具调用 - tool_calls_map[tc_id] = llm_entities.ToolCall( - id=tc_id, - type=tool_call.get('type', 'function'), - function=llm_entities.FunctionCall( - name=tool_call.get('function', {}).get('name', ''), - arguments=tool_call.get('function', {}).get('arguments', ''), - ), - ) - delta_tool_calls.append(tool_calls_map[tc_id]) - else: - # 追加函数参数 - func_args = tool_call.get('function', {}).get('arguments', '') - if func_args: - tool_calls_map[tc_id].function.arguments += func_args - # 返回更新后的完整工具调用 - delta_tool_calls.append(tool_calls_map[tc_id]) + if tool_call['id'] and tool_call['function']['name']: + tool_id = tool_call['id'] + tool_name = tool_call['function']['name'] + else: + tool_call['id'] = tool_id + tool_call['function']['name'] = tool_name + if tool_call['type'] is None: + tool_call['type'] = 'function' + + # 跳过空的第一个 chunk(只有 role 没有内容) if chunk_idx == 0 and not delta_content and not reasoning_content and not delta.get('tool_calls'): chunk_idx += 1 continue - # 构建 MessageChunk - 只包含增量内容 chunk_data = { 'role': role, 'content': delta_content if delta_content else None, - 'tool_calls': delta_tool_calls if delta_tool_calls else None, + 'tool_calls': delta.get('tool_calls'), 'is_final': bool(finish_reason), } @@ -289,7 +280,6 @@ class OpenAIChatCompletions(requester.ProviderAPIRequester): # 发送请求 resp = await self._req(args, extra_body=extra_args) - print(resp) # 处理请求结果 message = await self._make_msg(resp, remove_think) diff --git a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py index c526313a..9d8861da 100644 --- a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py +++ b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py @@ -289,30 +289,16 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): # delta_content = re.sub(r'.*?', '', delta_content, flags=re.DOTALL) # 处理工具调用增量 - delta_tool_calls = None if delta.get('tool_calls'): - delta_tool_calls = [] for tool_call in delta['tool_calls']: - tc_id = tool_call.get('id') - if tc_id: - if tc_id not in tool_calls_map: - # 新的工具调用 - tool_calls_map[tc_id] = llm_entities.ToolCall( - id=tc_id, - type=tool_call.get('type', 'function'), - function=llm_entities.FunctionCall( - name=tool_call.get('function', {}).get('name', ''), - arguments=tool_call.get('function', {}).get('arguments', ''), - ), - ) - delta_tool_calls.append(tool_calls_map[tc_id]) - else: - # 追加函数参数 - func_args = tool_call.get('function', {}).get('arguments', '') - if func_args: - tool_calls_map[tc_id].function.arguments += func_args - # 返回更新后的完整工具调用 - delta_tool_calls.append(tool_calls_map[tc_id]) + if tool_call['id'] and tool_call['function']['name']: + tool_id = tool_call['id'] + tool_name = tool_call['function']['name'] + else: + tool_call['id'] = tool_id + tool_call['function']['name'] = tool_name + if tool_call['type'] is None: + tool_call['type'] = 'function' # 跳过空的第一个 chunk(只有 role 没有内容) if chunk_idx == 0 and not delta_content and not reasoning_content and not delta.get('tool_calls'): @@ -323,7 +309,7 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): chunk_data = { 'role': role, 'content': delta_content if delta_content else None, - 'tool_calls': delta_tool_calls if delta_tool_calls else None, + 'tool_calls': delta.get('tool_calls'), 'is_final': bool(finish_reason), } diff --git a/pkg/provider/modelmgr/requesters/ppiochatcmpl.py b/pkg/provider/modelmgr/requesters/ppiochatcmpl.py index 967bb676..4af1cde0 100644 --- a/pkg/provider/modelmgr/requesters/ppiochatcmpl.py +++ b/pkg/provider/modelmgr/requesters/ppiochatcmpl.py @@ -179,39 +179,30 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions): delta_tool_calls = None if delta.get('tool_calls'): - delta_tool_calls = [] for tool_call in delta['tool_calls']: - tc_id = tool_call.get('id') - if tc_id: - if tc_id not in tool_calls_map: - # 新的工具调用 - tool_calls_map[tc_id] = llm_entities.ToolCall( - id=tc_id, - type=tool_call.get('type', 'function'), - function=llm_entities.FunctionCall( - name=tool_call.get('function', {}).get('name', ''), - arguments=tool_call.get('function', {}).get('arguments', ''), - ), - ) - delta_tool_calls.append(tool_calls_map[tc_id]) - else: - # 追加函数参数 - func_args = tool_call.get('function', {}).get('arguments', '') - if func_args: - tool_calls_map[tc_id].function.arguments += func_args - # 返回更新后的完整工具调用 - delta_tool_calls.append(tool_calls_map[tc_id]) + if tool_call['id'] and tool_call['function']['name']: + tool_id = tool_call['id'] + tool_name = tool_call['function']['name'] + + if tool_call['id'] is None: + tool_call['id'] = tool_id + if tool_call['function']['name'] is None: + tool_call['function']['name'] = tool_name + if tool_call['function']['arguments'] is None: + tool_call['function']['arguments'] = '' + if tool_call['type'] is None: + tool_call['type'] = 'function' # 跳过空的第一个 chunk(只有 role 没有内容) if chunk_idx == 0 and not delta_content and not delta.get('tool_calls'): chunk_idx += 1 continue - # 构建 MessageChunk - 只包含增量内容 + # 构建 MessageChunk - 只包含增量内容 chunk_data = { 'role': role, 'content': delta_content if delta_content else None, - 'tool_calls': delta_tool_calls if delta_tool_calls else None, + 'tool_calls': delta.get('tool_calls'), 'is_final': bool(finish_reason), } diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index 754082ea..73d873ec 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -148,13 +148,13 @@ class LocalAgentRunner(runner.RequestRunner): if tool_call.function and tool_call.function.arguments: # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments - + print(list(tool_calls_map.values()) if (tool_calls_map and msg.is_final) else None) + # continue # 每8个chunk或最后一个chunk时,输出所有累积的内容 if msg_idx % 8 == 0 or msg.is_final: yield llm_entities.MessageChunk( role=last_role, content=accumulated_content, # 输出所有累积内容 - tool_calls=list(tool_calls_map.values()) if (tool_calls_map and msg.is_final) else None, is_final=msg.is_final, ) @@ -178,12 +178,18 @@ class LocalAgentRunner(runner.RequestRunner): parameters = json.loads(func.arguments) func_ret = await self.ap.tool_mgr.execute_func_call(query, func.name, parameters) - - msg = llm_entities.Message( - role='tool', - content=json.dumps(func_ret, ensure_ascii=False), - tool_call_id=tool_call.id, - ) + if is_stream: + msg = llm_entities.MessageChunk( + role='tool', + content=json.dumps(func_ret, ensure_ascii=False), + tool_call_id=tool_call.id, + ) + else: + msg = llm_entities.Message( + role='tool', + content=json.dumps(func_ret, ensure_ascii=False), + tool_call_id=tool_call.id, + ) yield msg diff --git a/web/src/app/infra/http/HttpClient.ts b/web/src/app/infra/http/HttpClient.ts index f6ff6a50..35c18680 100644 --- a/web/src/app/infra/http/HttpClient.ts +++ b/web/src/app/infra/http/HttpClient.ts @@ -443,6 +443,9 @@ class HttpClient { onComplete(); return; } + if (data.type === 'start') { + console,log(data.type) + } if (data.message) { // 处理消息数据 From a381069bcceefba965e5452d9729f8ba157b8c02 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Mon, 11 Aug 2025 23:05:47 +0800 Subject: [PATCH 079/107] fix:fix tool_result argument bug --- .../modelmgr/requesters/anthropicmsgs.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/pkg/provider/modelmgr/requesters/anthropicmsgs.py b/pkg/provider/modelmgr/requesters/anthropicmsgs.py index a337af4f..6651f186 100644 --- a/pkg/provider/modelmgr/requesters/anthropicmsgs.py +++ b/pkg/provider/modelmgr/requesters/anthropicmsgs.py @@ -91,7 +91,9 @@ class AnthropicMessages(requester.ProviderAPIRequester): { 'type': 'tool_result', 'tool_use_id': tool_call_id, - 'content': m.content, + 'is_error':False, + 'content': [{"type": "text", + "text": m.content}], } ], } @@ -135,13 +137,11 @@ class AnthropicMessages(requester.ProviderAPIRequester): args['messages'] = req_messages - if args["thinking"]: + if 'thinking' in args: args['thinking'] = { "type": "enabled", "budget_tokens": 10000 } - else: - args.pop('thinking') if funcs: tools = await self.ap.tool_mgr.generate_tools_for_anthropic(funcs) @@ -156,7 +156,6 @@ class AnthropicMessages(requester.ProviderAPIRequester): 'content': '', 'role': resp.role, } - print(type(resp)) assert type(resp) is anthropic.types.message.Message for block in resp.content: @@ -232,7 +231,9 @@ class AnthropicMessages(requester.ProviderAPIRequester): { 'type': 'tool_result', 'tool_use_id': tool_call_id, - 'content': m.content, + 'is_error':False, # 暂时直接写false + 'content': [{"type": "text", + "text": m.content}], # 这里要是list包裹,应该是多个返回的情况?type类型好像也可以填其他的,暂时只写text } ], } @@ -273,7 +274,7 @@ class AnthropicMessages(requester.ProviderAPIRequester): del msg_dict['tool_calls'] req_messages.append(msg_dict) - if args.get("thinking", False): + if 'thinking' in args: args['thinking'] = { "type": "enabled", "budget_tokens": 10000 @@ -298,7 +299,6 @@ class AnthropicMessages(requester.ProviderAPIRequester): tool_id = '' tool_calls = [] async for chunk in await self.client.messages.create(**args): - print(chunk) tool_call = {"id":None, 'function': {"name": None, "arguments": None},'type':'function'} if isinstance(chunk, anthropic.types.raw_content_block_start_event.RawContentBlockStartEvent): # 记录开始 if chunk.content_block.type == 'tool_use': @@ -313,7 +313,6 @@ class AnthropicMessages(requester.ProviderAPIRequester): tool_call['function']['arguments'] = '' tool_call['id'] = tool_id - print(chunk.content_block) if not remove_think: if chunk.content_block.type == 'thinking' and not remove_think: think_started = True From 73014762283b6f784aa110af27500dc6e1c7d9db Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Mon, 11 Aug 2025 23:36:01 +0800 Subject: [PATCH 080/107] fix:Because the message_id was popped out, it caused the issue where the tool couldn't find the message_id after being invoked. --- pkg/platform/sources/dingtalk.py | 5 +++-- pkg/platform/sources/lark.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pkg/platform/sources/dingtalk.py b/pkg/platform/sources/dingtalk.py index eacc2a23..cfd9a0ca 100644 --- a/pkg/platform/sources/dingtalk.py +++ b/pkg/platform/sources/dingtalk.py @@ -158,8 +158,9 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): # incoming_message = event.incoming_message # msg_id = incoming_message.message_id - self.seq += 1 if (self.seq - 1) % 8 == 0 or is_final: + self.seq += 1 + content, at = await DingTalkMessageConverter.yiri2target(message) card_instance, card_instance_id = self.card_instance_id_dict[message_id] @@ -167,7 +168,7 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): await self.bot.send_card_message(card_instance, card_instance_id, content, is_final) if is_final: self.seq = 1 # 消息回复结束之后重置seq - self.card_instance_id_dict.pop(message_id) # 消息回复结束之后删除卡片实例id + # self.card_instance_id_dict.pop(message_id) # 消息回复结束之后删除卡片实例id async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): content = await DingTalkMessageConverter.yiri2target(message) diff --git a/pkg/platform/sources/lark.py b/pkg/platform/sources/lark.py index 0d7fc0fb..d170d388 100644 --- a/pkg/platform/sources/lark.py +++ b/pkg/platform/sources/lark.py @@ -543,9 +543,10 @@ class LarkAdapter(adapter.MessagePlatformAdapter): """ 回复消息变成更新卡片消息 """ - self.seq += 1 + if (self.seq - 1) % 8 == 0 or is_final: + self.seq += 1 lark_message = await self.message_converter.yiri2target(message, self.api_client) @@ -577,7 +578,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): if is_final: self.seq = 1 # 消息回复结束之后重置seq - self.card_id_dict.pop(message_id) # 清理已经使用过的卡片 + # self.card_id_dict.pop(message_id) # 清理已经使用过的卡片 # 发起请求 response: ContentCardElementResponse = self.api_client.cardkit.v1.card_element.content(request) From ed57d2fafa6086f25d494b903db8536b4ead1bbf Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Mon, 11 Aug 2025 23:49:19 +0800 Subject: [PATCH 081/107] del localagent.py print --- pkg/provider/runners/localagent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index 73d873ec..d8732eac 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -148,7 +148,7 @@ class LocalAgentRunner(runner.RequestRunner): if tool_call.function and tool_call.function.arguments: # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments - print(list(tool_calls_map.values()) if (tool_calls_map and msg.is_final) else None) + # print(list(tool_calls_map.values()) if (tool_calls_map and msg.is_final) else None) # continue # 每8个chunk或最后一个chunk时,输出所有累积的内容 if msg_idx % 8 == 0 or msg.is_final: From 0607a0fa5cee3b20b980f4de6b393bf033b1aea6 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Tue, 12 Aug 2025 00:04:21 +0800 Subject: [PATCH 082/107] fix: in the modelscopechatcmpl.py stream tool_calls arguments bug, --- .../modelmgr/requesters/modelscopechatcmpl.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py index 9d8861da..d45c5d9e 100644 --- a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py +++ b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py @@ -290,15 +290,19 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): # 处理工具调用增量 if delta.get('tool_calls'): + print(delta.get('tool_calls')) for tool_call in delta['tool_calls']: - if tool_call['id'] and tool_call['function']['name']: + if tool_call['id'] != '': tool_id = tool_call['id'] + if tool_call['function']['name'] is not None: tool_name = tool_call['function']['name'] - else: - tool_call['id'] = tool_id - tool_call['function']['name'] = tool_name + if tool_call['type'] is None: tool_call['type'] = 'function' + tool_call['id'] = tool_id + tool_call['function']['name'] = tool_name + tool_call['function']['arguments'] = "" if tool_call['function']['arguments'] is None else tool_call['function']['arguments'] + # 跳过空的第一个 chunk(只有 role 没有内容) if chunk_idx == 0 and not delta_content and not reasoning_content and not delta.get('tool_calls'): From 6d35fc408c31f9e1dce02759284a6a53a015955b Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Tue, 12 Aug 2025 11:15:17 +0800 Subject: [PATCH 083/107] fix: some time in the anthropicmsgs.py mesg_dcit["content"] is str can not append --- pkg/provider/modelmgr/requesters/anthropicmsgs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/provider/modelmgr/requesters/anthropicmsgs.py b/pkg/provider/modelmgr/requesters/anthropicmsgs.py index 6651f186..e9adf390 100644 --- a/pkg/provider/modelmgr/requesters/anthropicmsgs.py +++ b/pkg/provider/modelmgr/requesters/anthropicmsgs.py @@ -259,7 +259,8 @@ class AnthropicMessages(requester.ProviderAPIRequester): }, } msg_dict['content'][i] = alter_image_ele - + if isinstance(msg_dict['content'], str) and msg_dict['content'] == '': + msg_dict['content'] = [] # 这里不知道为什么会莫名有个空导致content为字符 if m.tool_calls: for tool_call in m.tool_calls: msg_dict['content'].append( From 27cee0a4e1218377f54427bc28913a53f49232d5 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Tue, 12 Aug 2025 11:19:27 +0800 Subject: [PATCH 084/107] fix: in the adapter.py func reply_message_chunk agr message_id alter bot_message,and in dingtalk.py lark.py telegram.py webchat.py agr alter --- pkg/platform/adapter.py | 2 +- pkg/platform/sources/dingtalk.py | 10 ++++++---- pkg/platform/sources/lark.py | 12 ++++++------ pkg/platform/sources/telegram.py | 4 ++-- pkg/platform/sources/webchat.py | 4 ++-- 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/pkg/platform/adapter.py b/pkg/platform/adapter.py index 3412be3c..e064ef80 100644 --- a/pkg/platform/adapter.py +++ b/pkg/platform/adapter.py @@ -64,7 +64,7 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): async def reply_message_chunk( self, message_source: platform_events.MessageEvent, - message_id: int, + bot_message: dict, message: platform_message.MessageChain, quote_origin: bool = False, is_final: bool = False, diff --git a/pkg/platform/sources/dingtalk.py b/pkg/platform/sources/dingtalk.py index cfd9a0ca..f991f71c 100644 --- a/pkg/platform/sources/dingtalk.py +++ b/pkg/platform/sources/dingtalk.py @@ -147,7 +147,7 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): async def reply_message_chunk( self, message_source: platform_events.MessageEvent, - message_id: int, + bot_message, message: platform_message.MessageChain, quote_origin: bool = False, is_final: bool = False, @@ -158,17 +158,19 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): # incoming_message = event.incoming_message # msg_id = incoming_message.message_id + message_id = bot_message.resp_message_id + self.seq += 1 + if (self.seq - 1) % 8 == 0 or is_final: - self.seq += 1 content, at = await DingTalkMessageConverter.yiri2target(message) card_instance, card_instance_id = self.card_instance_id_dict[message_id] # print(card_instance_id) await self.bot.send_card_message(card_instance, card_instance_id, content, is_final) - if is_final: + if is_final and bot_message.tool_calls is None: self.seq = 1 # 消息回复结束之后重置seq - # self.card_instance_id_dict.pop(message_id) # 消息回复结束之后删除卡片实例id + self.card_instance_id_dict.pop(message_id) # 消息回复结束之后删除卡片实例id async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): content = await DingTalkMessageConverter.yiri2target(message) diff --git a/pkg/platform/sources/lark.py b/pkg/platform/sources/lark.py index d170d388..dd4b45fe 100644 --- a/pkg/platform/sources/lark.py +++ b/pkg/platform/sources/lark.py @@ -535,7 +535,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): async def reply_message_chunk( self, message_source: platform_events.MessageEvent, - message_id: str, + bot_message, message: platform_message.MessageChain, quote_origin: bool = False, is_final: bool = False, @@ -543,10 +543,10 @@ class LarkAdapter(adapter.MessagePlatformAdapter): """ 回复消息变成更新卡片消息 """ + self.seq += 1 + message_id = bot_message.resp_message_id + if self.seq % 8 == 0 or is_final: - - if (self.seq - 1) % 8 == 0 or is_final: - self.seq += 1 lark_message = await self.message_converter.yiri2target(message, self.api_client) @@ -576,9 +576,9 @@ class LarkAdapter(adapter.MessagePlatformAdapter): .build() ) - if is_final: + if is_final and bot_message.tool_calls is None: self.seq = 1 # 消息回复结束之后重置seq - # self.card_id_dict.pop(message_id) # 清理已经使用过的卡片 + self.card_id_dict.pop(message_id) # 清理已经使用过的卡片 # 发起请求 response: ContentCardElementResponse = self.api_client.cardkit.v1.card_element.content(request) diff --git a/pkg/platform/sources/telegram.py b/pkg/platform/sources/telegram.py index 3c81fd6b..f113ec26 100644 --- a/pkg/platform/sources/telegram.py +++ b/pkg/platform/sources/telegram.py @@ -211,7 +211,7 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): async def reply_message_chunk( self, message_source: platform_events.MessageEvent, - message_id: int, + bot_message, message: platform_message.MessageChain, quote_origin: bool = False, is_final: bool = False, @@ -263,7 +263,7 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): args['parse_mode'] = 'MarkdownV2' await self.bot.edit_message_text(**args) - if is_final: + if is_final and bot_message.tool_calls is None: self.seq = 1 # 消息回复结束之后重置seq self.msg_stream_id.pop(message_id) # 消息回复结束之后删除流式消息id diff --git a/pkg/platform/sources/webchat.py b/pkg/platform/sources/webchat.py index fce28bc2..52fc9294 100644 --- a/pkg/platform/sources/webchat.py +++ b/pkg/platform/sources/webchat.py @@ -118,7 +118,7 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): async def reply_message_chunk( self, message_source: platform_events.MessageEvent, - message_id: int, + bot_message, message: platform_message.MessageChain, quote_origin: bool = False, is_final: bool = False, @@ -146,7 +146,7 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): # queue = self.webchat_person_session.resp_queues[message_source.message_chain.message_id] # elif isinstance(message_source, platform_events.GroupMessage): # queue = self.webchat_group_session.resp_queues[message_source.message_chain.message_id] - if is_final: + if is_final and bot_message.tool_calls is None: message_data.is_final = True # print(message_data) await queue.put(message_data) From 9f22b8b585095c893f379b2adf2d6cee2e89e62d Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Tue, 12 Aug 2025 11:21:08 +0800 Subject: [PATCH 085/107] fix: be adapter.py func reply_message_chunk agr message_id alter bot_message,and in pipelinemgr.py respback.py agr alter --- pkg/pipeline/pipelinemgr.py | 3 +-- pkg/pipeline/respback/respback.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pkg/pipeline/pipelinemgr.py b/pkg/pipeline/pipelinemgr.py index 79bd4ec6..abf80e16 100644 --- a/pkg/pipeline/pipelinemgr.py +++ b/pkg/pipeline/pipelinemgr.py @@ -94,10 +94,9 @@ class RuntimePipeline: ): result.user_notice.insert(0, platform_message.At(query.message_event.sender.id)) if await query.adapter.is_stream_output_supported(): - print(query.resp_messages[-1]) await query.adapter.reply_message_chunk( message_source=query.message_event, - message_id=str(query.resp_messages[-1].resp_message_id), + bot_message=query.resp_messages[-1], message=result.user_notice, quote_origin=query.pipeline_config['output']['misc']['quote-origin'], is_final=[msg.is_final for msg in query.resp_messages][0] diff --git a/pkg/pipeline/respback/respback.py b/pkg/pipeline/respback/respback.py index 5683ce92..d83302ed 100644 --- a/pkg/pipeline/respback/respback.py +++ b/pkg/pipeline/respback/respback.py @@ -41,10 +41,9 @@ class SendResponseBackStage(stage.PipelineStage): # TODO 命令与流式的兼容性问题 if await query.adapter.is_stream_output_supported(): is_final = [msg.is_final for msg in query.resp_messages][0] - print(query.resp_messages[-1]) await query.adapter.reply_message_chunk( message_source=query.message_event, - message_id=query.resp_messages[-1].resp_message_id, + bot_message=query.resp_messages[-1], message=query.resp_message_chain[-1], quote_origin=quote_origin, is_final=is_final, From e744e9c4ef4cf6ca9199c0d78990f049b3defcaf Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Tue, 12 Aug 2025 11:25:37 +0800 Subject: [PATCH 086/107] fix: in the localagent.py yield MessageChunk add agr tool_calls,and After calling the "tool_calls", the first returned body data will be concatenated. --- pkg/provider/modelmgr/requesters/modelscopechatcmpl.py | 1 + pkg/provider/runners/localagent.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py index d45c5d9e..3a3bb5b0 100644 --- a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py +++ b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py @@ -248,6 +248,7 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): if hasattr(chunk, 'choices') and chunk.choices: choice = chunk.choices[0] delta = choice.delta.model_dump() if hasattr(choice, 'delta') else {} + print(delta) finish_reason = getattr(choice, 'finish_reason', None) else: delta = {} diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index d8732eac..49e3d46d 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -155,6 +155,7 @@ class LocalAgentRunner(runner.RequestRunner): yield llm_entities.MessageChunk( role=last_role, content=accumulated_content, # 输出所有累积内容 + tool_calls=list(tool_calls_map.values()) if (tool_calls_map and msg.is_final) else None, is_final=msg.is_final, ) @@ -166,6 +167,7 @@ class LocalAgentRunner(runner.RequestRunner): ) pending_tool_calls = final_msg.tool_calls + first_content = final_msg.content req_messages.append(final_msg) @@ -222,6 +224,10 @@ class LocalAgentRunner(runner.RequestRunner): if msg.role: last_role = msg.role + # 第一次请求工具调用时的内容 + if msg_idx == 1: + accumulated_content =first_content if first_content is not None else accumulated_content + # 累积内容 if msg.content: accumulated_content += msg.content From 051fffd41e16813bcf91aa8dfb784e9fac8145ab Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Thu, 7 Aug 2025 21:56:40 +0800 Subject: [PATCH 087/107] fix: stash --- .../controller/groups/pipelines/webchat.py | 3 +- pkg/platform/sources/webchat.py | 39 ++++++++++++------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/pkg/api/http/controller/groups/pipelines/webchat.py b/pkg/api/http/controller/groups/pipelines/webchat.py index ae201934..7eea471a 100644 --- a/pkg/api/http/controller/groups/pipelines/webchat.py +++ b/pkg/api/http/controller/groups/pipelines/webchat.py @@ -48,8 +48,7 @@ class WebChatDebugRouterGroup(group.RouterGroup): } return quart.Response(stream_generator(generator), mimetype='text/event-stream',headers=headers) - else: - # result = await webchat_adapter.send_webchat_message(pipeline_uuid, session_type, message_chain_obj) + else: # non-stream result = None async for message in webchat_adapter.send_webchat_message( pipeline_uuid, session_type, message_chain_obj diff --git a/pkg/platform/sources/webchat.py b/pkg/platform/sources/webchat.py index 52fc9294..2c39afbb 100644 --- a/pkg/platform/sources/webchat.py +++ b/pkg/platform/sources/webchat.py @@ -26,7 +26,7 @@ class WebChatSession: id: str message_lists: dict[str, list[WebChatMessage]] = {} resp_waiters: dict[int, asyncio.Future[WebChatMessage]] - resp_queues = dict[int, asyncio.Queue[WebChatMessage]] + resp_queues: dict[int, asyncio.Queue[WebChatMessage]] def __init__(self, id: str): self.id = id @@ -109,9 +109,9 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): # notify waiter if isinstance(message_source, platform_events.FriendMessage): - self.webchat_person_session.resp_waiters[message_source.message_chain.message_id].set_result(message_data) + self.webchat_person_session.resp_queues[message_source.message_chain.message_id].put(message_data) elif isinstance(message_source, platform_events.GroupMessage): - self.webchat_group_session.resp_waiters[message_source.message_chain.message_id].set_result(message_data) + self.webchat_group_session.resp_queues[message_source.message_chain.message_id].put(message_data) return message_data.model_dump() @@ -205,9 +205,8 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): message_id = len(use_session.get_message_list(pipeline_uuid)) + 1 - if is_stream: - use_session.resp_queues[message_id] = asyncio.Queue() - logger.debug(f'Initialized queue for message_id: {message_id}') + use_session.resp_queues[message_id] = asyncio.Queue() + logger.debug(f'Initialized queue for message_id: {message_id}') use_session.get_message_list(pipeline_uuid).append( WebChatMessage( @@ -242,6 +241,7 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): self.ap.platform_mgr.webchat_proxy_bot.bot_entity.use_pipeline_uuid = pipeline_uuid + # trigger pipeline if event.__class__ in self.listeners: await self.listeners[event.__class__](event, self) @@ -257,20 +257,31 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): yield resp_message.model_dump() break yield resp_message.model_dump() + use_session.resp_queues.pop(message_id) - else: + else: # non-stream # set waiter - waiter = asyncio.Future[WebChatMessage]() - use_session.resp_waiters[message_id] = waiter - waiter.add_done_callback(lambda future: use_session.resp_waiters.pop(message_id)) + # waiter = asyncio.Future[WebChatMessage]() + # use_session.resp_waiters[message_id] = waiter + # # waiter.add_done_callback(lambda future: use_session.resp_waiters.pop(message_id)) - resp_message = await waiter + # resp_message = await waiter - resp_message.id = len(use_session.get_message_list(pipeline_uuid)) + 1 + # resp_message.id = len(use_session.get_message_list(pipeline_uuid)) + 1 - use_session.get_message_list(pipeline_uuid).append(resp_message) + # use_session.get_message_list(pipeline_uuid).append(resp_message) - yield resp_message.model_dump() + # yield resp_message.model_dump() + queue = use_session.resp_queues[message_id] + while True: + resp_message = await queue.get() + resp_message.id = msg_id + if resp_message.is_final: + resp_message.id = msg_id + use_session.get_message_list(pipeline_uuid).append(resp_message) + yield resp_message.model_dump() + break + yield resp_message.model_dump() def get_webchat_messages(self, pipeline_uuid: str, session_type: str) -> list[dict]: """获取调试消息历史""" From b6d47187f5ab65c693758daef268fd6ae9c72dcf Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Tue, 12 Aug 2025 19:39:41 +0800 Subject: [PATCH 088/107] perf: prettier --- web/src/app/infra/http/HttpClient.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/app/infra/http/HttpClient.ts b/web/src/app/infra/http/HttpClient.ts index 35c18680..4573edd0 100644 --- a/web/src/app/infra/http/HttpClient.ts +++ b/web/src/app/infra/http/HttpClient.ts @@ -444,8 +444,8 @@ class HttpClient { return; } if (data.type === 'start') { - console,log(data.type) - } + console.log(data.type); + } if (data.message) { // 处理消息数据 From 4668db716a5fa934bf85dcab7e261911670346b7 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Tue, 12 Aug 2025 20:54:47 +0800 Subject: [PATCH 089/107] fix: fix command reply_message error bug,del some print --- pkg/pipeline/respback/respback.py | 7 +++-- pkg/platform/sources/webchat.py | 28 +++++++++---------- .../modelmgr/requesters/modelscopechatcmpl.py | 3 +- pkg/provider/runners/localagent.py | 2 +- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/pkg/pipeline/respback/respback.py b/pkg/pipeline/respback/respback.py index d83302ed..ece4e392 100644 --- a/pkg/pipeline/respback/respback.py +++ b/pkg/pipeline/respback/respback.py @@ -7,6 +7,9 @@ import asyncio from ...platform.types import events as platform_events from ...platform.types import message as platform_message +from ...provider import entities as llm_entities + + from .. import stage, entities from ...core import entities as core_entities @@ -37,9 +40,9 @@ class SendResponseBackStage(stage.PipelineStage): quote_origin = query.pipeline_config['output']['misc']['quote-origin'] - # has_chunks = any(isinstance(msg, llm_entities.MessageChunk) for msg in query.resp_messages) + has_chunks = any(isinstance(msg, llm_entities.MessageChunk) for msg in query.resp_messages) # TODO 命令与流式的兼容性问题 - if await query.adapter.is_stream_output_supported(): + if await query.adapter.is_stream_output_supported() and has_chunks: is_final = [msg.is_final for msg in query.resp_messages][0] await query.adapter.reply_message_chunk( message_source=query.message_event, diff --git a/pkg/platform/sources/webchat.py b/pkg/platform/sources/webchat.py index 2c39afbb..dfe8fc30 100644 --- a/pkg/platform/sources/webchat.py +++ b/pkg/platform/sources/webchat.py @@ -109,9 +109,9 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): # notify waiter if isinstance(message_source, platform_events.FriendMessage): - self.webchat_person_session.resp_queues[message_source.message_chain.message_id].put(message_data) + await self.webchat_person_session.resp_queues[message_source.message_chain.message_id].put(message_data) elif isinstance(message_source, platform_events.GroupMessage): - self.webchat_group_session.resp_queues[message_source.message_chain.message_id].put(message_data) + await self.webchat_group_session.resp_queues[message_source.message_chain.message_id].put(message_data) return message_data.model_dump() @@ -264,24 +264,22 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): # waiter = asyncio.Future[WebChatMessage]() # use_session.resp_waiters[message_id] = waiter # # waiter.add_done_callback(lambda future: use_session.resp_waiters.pop(message_id)) - + # # resp_message = await waiter - + # # resp_message.id = len(use_session.get_message_list(pipeline_uuid)) + 1 - + # # use_session.get_message_list(pipeline_uuid).append(resp_message) - + # # yield resp_message.model_dump() + msg_id = len(use_session.get_message_list(pipeline_uuid)) + 1 + queue = use_session.resp_queues[message_id] - while True: - resp_message = await queue.get() - resp_message.id = msg_id - if resp_message.is_final: - resp_message.id = msg_id - use_session.get_message_list(pipeline_uuid).append(resp_message) - yield resp_message.model_dump() - break - yield resp_message.model_dump() + resp_message = await queue.get() + resp_message.id = msg_id + resp_message.is_final = True + + yield resp_message.model_dump() def get_webchat_messages(self, pipeline_uuid: str, session_type: str) -> list[dict]: """获取调试消息历史""" diff --git a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py index 3a3bb5b0..7ef3ab58 100644 --- a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py +++ b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py @@ -36,7 +36,7 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): self, args: dict, extra_body: dict = {}, - remove_think:bool = False, + remove_think: bool = False, ) -> chat_completion.ChatCompletion: args['stream'] = True @@ -291,7 +291,6 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): # 处理工具调用增量 if delta.get('tool_calls'): - print(delta.get('tool_calls')) for tool_call in delta['tool_calls']: if tool_call['id'] != '': tool_id = tool_call['id'] diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index 49e3d46d..8466a9c4 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -226,7 +226,7 @@ class LocalAgentRunner(runner.RequestRunner): # 第一次请求工具调用时的内容 if msg_idx == 1: - accumulated_content =first_content if first_content is not None else accumulated_content + accumulated_content = first_content if first_content is not None else accumulated_content # 累积内容 if msg.content: From dbf0200cca3eb0df8d3b1dad8b344e32078dda90 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Tue, 12 Aug 2025 22:36:42 +0800 Subject: [PATCH 090/107] feat:add More attractive card templates --- pkg/platform/sources/lark.py | 152 +++++++++++++++++++++++++++++++++-- 1 file changed, 145 insertions(+), 7 deletions(-) diff --git a/pkg/platform/sources/lark.py b/pkg/platform/sources/lark.py index dd4b45fe..c4e5b339 100644 --- a/pkg/platform/sources/lark.py +++ b/pkg/platform/sources/lark.py @@ -432,12 +432,150 @@ class LarkAdapter(adapter.MessagePlatformAdapter): try: self.ap.logger.debug('飞书支持stream输出,创建卡片......') - card_data = { - 'schema': '2.0', - 'header': {'title': {'content': 'bot', 'tag': 'plain_text'}}, - 'body': {'elements': [{'tag': 'markdown', 'content': '[思考中.....]', 'element_id': 'markdown_1'}]}, - 'config': {'streaming_mode': True, 'streaming_config': {'print_strategy': 'delay'}}, - } # delay / fast 创建卡片模板,delay 延迟打印,fast 实时打印,可以自定义更好看的消息模板 + card_data = {"schema": "2.0", "config": {"update_multi": True, "streaming_mode": True, + "streaming_config": {"print_step": {"default": 1}, + "print_frequency_ms": {"default": 70}, + "print_strategy": "fast"}}, + "body": {"direction": "vertical", "padding": "12px 12px 12px 12px", "elements": [{"tag": "div", + "text": { + "tag": "plain_text", + "content": "Langbot", + "text_size": "normal", + "text_align": "left", + "text_color": "default"}, + "icon": { + "tag": "custom_icon", + "img_key": "img_v3_02p3_05c65d5d-9bad-440a-a2fb-c89571bfd5bg"}}, + { + "tag": "markdown", + "content": "", + "text_align": "left", + "text_size": "normal", + "margin": "0px 0px 0px 0px", + "element_id": "streaming_txt"}, + { + "tag": "markdown", + "content": "", + "text_align": "left", + "text_size": "normal", + "margin": "0px 0px 0px 0px"}, + { + "tag": "column_set", + "horizontal_spacing": "8px", + "horizontal_align": "left", + "columns": [ + { + "tag": "column", + "width": "weighted", + "elements": [ + { + "tag": "markdown", + "content": "", + "text_align": "left", + "text_size": "normal", + "margin": "0px 0px 0px 0px"}, + { + "tag": "markdown", + "content": "", + "text_align": "left", + "text_size": "normal", + "margin": "0px 0px 0px 0px"}, + { + "tag": "markdown", + "content": "", + "text_align": "left", + "text_size": "normal", + "margin": "0px 0px 0px 0px"}], + "padding": "0px 0px 0px 0px", + "direction": "vertical", + "horizontal_spacing": "8px", + "vertical_spacing": "2px", + "horizontal_align": "left", + "vertical_align": "top", + "margin": "0px 0px 0px 0px", + "weight": 1}], + "margin": "0px 0px 0px 0px"}, + {"tag": "hr", + "margin": "0px 0px 0px 0px"}, + { + "tag": "column_set", + "horizontal_spacing": "12px", + "horizontal_align": "right", + "columns": [ + { + "tag": "column", + "width": "weighted", + "elements": [ + { + "tag": "markdown", + "content": "以上内容由 AI 生成,仅供参考。更多详细、准确信息可点击引用链接查看", + "text_align": "left", + "text_size": "notation", + "margin": "4px 0px 0px 0px", + "icon": { + "tag": "standard_icon", + "token": "robot_outlined", + "color": "grey"}}], + "padding": "0px 0px 0px 0px", + "direction": "vertical", + "horizontal_spacing": "8px", + "vertical_spacing": "8px", + "horizontal_align": "left", + "vertical_align": "top", + "margin": "0px 0px 0px 0px", + "weight": 1}, + { + "tag": "column", + "width": "20px", + "elements": [ + { + "tag": "button", + "text": { + "tag": "plain_text", + "content": ""}, + "type": "text", + "width": "fill", + "size": "medium", + "icon": { + "tag": "standard_icon", + "token": "thumbsup_outlined"}, + "hover_tips": { + "tag": "plain_text", + "content": "有帮助"}, + "margin": "0px 0px 0px 0px"}], + "padding": "0px 0px 0px 0px", + "direction": "vertical", + "horizontal_spacing": "8px", + "vertical_spacing": "8px", + "horizontal_align": "left", + "vertical_align": "top", + "margin": "0px 0px 0px 0px"}, + { + "tag": "column", + "width": "30px", + "elements": [ + { + "tag": "button", + "text": { + "tag": "plain_text", + "content": ""}, + "type": "text", + "width": "default", + "size": "medium", + "icon": { + "tag": "standard_icon", + "token": "thumbdown_outlined"}, + "hover_tips": { + "tag": "plain_text", + "content": "无帮助"}, + "margin": "0px 0px 0px 0px"}], + "padding": "0px 0px 0px 0px", + "vertical_spacing": "8px", + "horizontal_align": "left", + "vertical_align": "top", + "margin": "0px 0px 0px 0px"}], + "margin": "0px 0px 4px 0px"}]}} + # delay / fast 创建卡片模板,delay 延迟打印,fast 实时打印,可以自定义更好看的消息模板 request: CreateCardRequest = ( CreateCardRequest.builder() @@ -565,7 +703,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): request: ContentCardElementRequest = ( ContentCardElementRequest.builder() .card_id(self.card_id_dict[message_id]) - .element_id('markdown_1') + .element_id('streaming_txt') .request_body( ContentCardElementRequestBody.builder() # .uuid("a0d69e20-1dd1-458b-k525-dfeca4015204") From 99fcde158635f3765ae04938cc662379de6713b8 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Tue, 12 Aug 2025 23:20:41 +0800 Subject: [PATCH 091/107] fix: in the MessageChunk add msg_sequence ,And obtain the usage in the adapter. --- pkg/platform/sources/dingtalk.py | 8 ++++---- pkg/platform/sources/lark.py | 11 ++++++----- pkg/platform/sources/telegram.py | 8 ++++---- pkg/provider/entities.py | 4 ++++ pkg/provider/runners/localagent.py | 11 ++++++++++- 5 files changed, 28 insertions(+), 14 deletions(-) diff --git a/pkg/platform/sources/dingtalk.py b/pkg/platform/sources/dingtalk.py index f991f71c..71c7b0d0 100644 --- a/pkg/platform/sources/dingtalk.py +++ b/pkg/platform/sources/dingtalk.py @@ -108,7 +108,7 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): self.ap = ap self.logger = logger self.card_instance_id_dict = {} - self.seq = 1 + # self.seq = 1 required_keys = [ 'client_id', 'client_secret', @@ -159,9 +159,9 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): # msg_id = incoming_message.message_id message_id = bot_message.resp_message_id - self.seq += 1 + msg_seq = bot_message.msg_sequence - if (self.seq - 1) % 8 == 0 or is_final: + if (msg_seq - 1) % 8 == 0 or is_final: content, at = await DingTalkMessageConverter.yiri2target(message) @@ -169,7 +169,7 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): # print(card_instance_id) await self.bot.send_card_message(card_instance, card_instance_id, content, is_final) if is_final and bot_message.tool_calls is None: - self.seq = 1 # 消息回复结束之后重置seq + # self.seq = 1 # 消息回复结束之后重置seq self.card_instance_id_dict.pop(message_id) # 消息回复结束之后删除卡片实例id async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): diff --git a/pkg/platform/sources/lark.py b/pkg/platform/sources/lark.py index c4e5b339..975730b5 100644 --- a/pkg/platform/sources/lark.py +++ b/pkg/platform/sources/lark.py @@ -439,7 +439,7 @@ class LarkAdapter(adapter.MessagePlatformAdapter): "body": {"direction": "vertical", "padding": "12px 12px 12px 12px", "elements": [{"tag": "div", "text": { "tag": "plain_text", - "content": "Langbot", + "content": "LangBot", "text_size": "normal", "text_align": "left", "text_color": "default"}, @@ -681,9 +681,10 @@ class LarkAdapter(adapter.MessagePlatformAdapter): """ 回复消息变成更新卡片消息 """ - self.seq += 1 + # self.seq += 1 message_id = bot_message.resp_message_id - if self.seq % 8 == 0 or is_final: + msg_seq = bot_message.msg_sequence + if msg_seq % 8 == 0 or is_final: lark_message = await self.message_converter.yiri2target(message, self.api_client) @@ -708,14 +709,14 @@ class LarkAdapter(adapter.MessagePlatformAdapter): ContentCardElementRequestBody.builder() # .uuid("a0d69e20-1dd1-458b-k525-dfeca4015204") .content(text_message) - .sequence(self.seq) + .sequence(msg_seq) .build() ) .build() ) if is_final and bot_message.tool_calls is None: - self.seq = 1 # 消息回复结束之后重置seq + # self.seq = 1 # 消息回复结束之后重置seq self.card_id_dict.pop(message_id) # 清理已经使用过的卡片 # 发起请求 response: ContentCardElementResponse = self.api_client.cardkit.v1.card_element.content(request) diff --git a/pkg/platform/sources/telegram.py b/pkg/platform/sources/telegram.py index f113ec26..42279317 100644 --- a/pkg/platform/sources/telegram.py +++ b/pkg/platform/sources/telegram.py @@ -158,7 +158,7 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): self.ap = ap self.logger = logger self.msg_stream_id = {} - self.seq = 1 + # self.seq = 1 async def telegram_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): if update.message.from_user.is_bot: @@ -216,8 +216,8 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): quote_origin: bool = False, is_final: bool = False, ): - self.seq += 1 - if (self.seq - 1) % 8 == 0 or is_final: + msg_seq = bot_message.msg_sequence + if (msg_seq - 1) % 8 == 0 or is_final: assert isinstance(message_source.source_platform_object, Update) components = await TelegramMessageConverter.yiri2target(message, self.bot) @@ -264,7 +264,7 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): await self.bot.edit_message_text(**args) if is_final and bot_message.tool_calls is None: - self.seq = 1 # 消息回复结束之后重置seq + # self.seq = 1 # 消息回复结束之后重置seq self.msg_stream_id.pop(message_id) # 消息回复结束之后删除流式消息id async def is_stream_output_supported(self) -> bool: diff --git a/pkg/provider/entities.py b/pkg/provider/entities.py index 9dcaffcd..4c4a65c1 100644 --- a/pkg/provider/entities.py +++ b/pkg/provider/entities.py @@ -149,6 +149,10 @@ class MessageChunk(pydantic.BaseModel): tool_call_id: typing.Optional[str] = None is_final: bool = False + """是否是结束""" + + msg_sequence: int = 0 + """消息迭代次数""" def readable_str(self) -> str: if self.content is not None: diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index 8466a9c4..f7f17dae 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -115,7 +115,7 @@ class LocalAgentRunner(runner.RequestRunner): msg_idx = 0 accumulated_content = '' # 从开始累积的所有内容 last_role = 'assistant' - + msg_sequence = 1 async for msg in query.use_llm_model.requester.invoke_llm_stream( query, query.use_llm_model, @@ -152,11 +152,13 @@ class LocalAgentRunner(runner.RequestRunner): # continue # 每8个chunk或最后一个chunk时,输出所有累积的内容 if msg_idx % 8 == 0 or msg.is_final: + msg_sequence += 1 yield llm_entities.MessageChunk( role=last_role, content=accumulated_content, # 输出所有累积内容 tool_calls=list(tool_calls_map.values()) if (tool_calls_map and msg.is_final) else None, is_final=msg.is_final, + msg_sequence=msg_sequence, ) # 创建最终消息用于后续处理 @@ -164,10 +166,12 @@ class LocalAgentRunner(runner.RequestRunner): role=last_role, content=accumulated_content, tool_calls=list(tool_calls_map.values()) if tool_calls_map else None, + msg_sequence=msg_sequence, ) pending_tool_calls = final_msg.tool_calls first_content = final_msg.content + first_end_sequence = final_msg.msg_sequence req_messages.append(final_msg) @@ -209,6 +213,7 @@ class LocalAgentRunner(runner.RequestRunner): msg_idx = 0 accumulated_content = '' # 从开始累积的所有内容 last_role = 'assistant' + msg_sequence = first_end_sequence async for msg in query.use_llm_model.requester.invoke_llm_stream( query, @@ -249,17 +254,21 @@ class LocalAgentRunner(runner.RequestRunner): # 每8个chunk或最后一个chunk时,输出所有累积的内容 if msg_idx % 8 == 0 or msg.is_final: + msg_sequence += 1 yield llm_entities.MessageChunk( role=last_role, content=accumulated_content, # 输出所有累积内容 tool_calls=list(tool_calls_map.values()) if (tool_calls_map and msg.is_final) else None, is_final=msg.is_final, + msg_sequence=msg_sequence, ) final_msg = llm_entities.MessageChunk( role=last_role, content=accumulated_content, tool_calls=list(tool_calls_map.values()) if tool_calls_map else None, + msg_sequence=msg_sequence, + ) else: # 处理完所有调用,再次请求 From cc83ddbe210a668d78b577f083b705552e9f9907 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Tue, 12 Aug 2025 23:29:32 +0800 Subject: [PATCH 092/107] fix: del print --- pkg/provider/modelmgr/requesters/modelscopechatcmpl.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py index 7ef3ab58..e8fe89bf 100644 --- a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py +++ b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py @@ -248,7 +248,6 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): if hasattr(chunk, 'choices') and chunk.choices: choice = chunk.choices[0] delta = choice.delta.model_dump() if hasattr(choice, 'delta') else {} - print(delta) finish_reason = getattr(choice, 'finish_reason', None) else: delta = {} From 8fd21e76f217d3c9896ae9b7c86357959ce513e8 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Wed, 13 Aug 2025 00:00:10 +0800 Subject: [PATCH 093/107] fix: Only when messagechunk is present, will msg_sequence be assigned to the subsequent tool calls. --- pkg/provider/runners/localagent.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index f7f17dae..b61b02e4 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -171,7 +171,9 @@ class LocalAgentRunner(runner.RequestRunner): pending_tool_calls = final_msg.tool_calls first_content = final_msg.content - first_end_sequence = final_msg.msg_sequence + if isinstance(final_msg, llm_entities.MessageChunk): + + first_end_sequence = final_msg.msg_sequence req_messages.append(final_msg) From 85f97860c5618d61909d48c9a40184ab91c6f3fc Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Wed, 13 Aug 2025 01:55:06 +0800 Subject: [PATCH 094/107] fix: Fixed the errors in modelscopechatcmpl.py when in pseudo-non-streaming mode, regarding the display of main content and tool calls. --- .../modelmgr/requesters/modelscopechatcmpl.py | 87 +++++++------------ 1 file changed, 32 insertions(+), 55 deletions(-) diff --git a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py index e8fe89bf..72e6dd58 100644 --- a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py +++ b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import json import typing import openai @@ -34,10 +35,11 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): async def _req( self, + query: core_entities.Query, args: dict, extra_body: dict = {}, remove_think: bool = False, - ) -> chat_completion.ChatCompletion: + ) -> list[dict[str, typing.Any]]: args['stream'] = True chunk = None @@ -51,12 +53,15 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): chunk_idx = 0 thinking_started = False thinking_ended = False + tool_id = '' + tool_name = '' + message_delta = {} async for chunk in resp_gen: - # print(chunk) if not chunk or not chunk.id or not chunk.choices or not chunk.choices[0] or not chunk.choices[0].delta: continue - reasoning_content = chunk.choices[0].delta.reasoning_content + delta = chunk.choices[0].delta.model_dump() if hasattr(chunk.choices[0], 'delta') else {} + reasoning_content = delta.get('reasoning_content') # 处理 reasoning_content if reasoning_content: # accumulated_reasoning += reasoning_content @@ -72,78 +77,50 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): else: # 继续输出 reasoning_content pending_content += reasoning_content - elif thinking_started and not thinking_ended and chunk.choices[0].delta.content: + elif thinking_started and not thinking_ended and delta.get('content'): # reasoning_content 结束,normal content 开始,添加 结束标签 thinking_ended = True - pending_content += '\n\n' + chunk.choices[0].delta.content + pending_content += '\n\n' + delta.get('content') - if chunk.choices[0].delta.content is not None: - pending_content += chunk.choices[0].delta.content + if delta.get('content') is not None: + pending_content += delta.get('content') - if chunk.choices[0].delta.tool_calls is not None: - for tool_call in chunk.choices[0].delta.tool_calls: - if tool_call.function.arguments is None: + if delta.get('tool_calls') is not None: + for tool_call in delta.get('tool_calls'): + if tool_call['id'] != '': + tool_id = tool_call['id'] + if tool_call['function']['name'] is not None: + tool_name = tool_call['function']['name'] + if tool_call['function']['arguments'] is None: continue + tool_call['id'] = tool_id + tool_call['name'] = tool_name for tc in tool_calls: - if tc.index == tool_call.index: - tc.function.arguments += tool_call.function.arguments + if tc['index'] == tool_call['index']: + tc['function']['arguments'] += tool_call['function']['arguments'] break else: tool_calls.append(tool_call) if chunk.choices[0].finish_reason is not None: break + message_delta['content'] = pending_content + message_delta['role'] = 'assistant' - real_tool_calls = [] - - for tc in tool_calls: - function = chat_completion_message_tool_call.Function( - name=tc.function.name, arguments=tc.function.arguments - ) - real_tool_calls.append( - chat_completion_message_tool_call.ChatCompletionMessageToolCall( - id=tc.id, function=function, type='function' - ) - ) - - return ( - chat_completion.ChatCompletion( - id=chunk.id, - object='chat.completion', - created=chunk.created, - choices=[ - chat_completion.Choice( - index=0, - message=chat_completion.ChatCompletionMessage( - role='assistant', - content=pending_content, - tool_calls=real_tool_calls if len(real_tool_calls) > 0 else None, - ), - finish_reason=chunk.choices[0].finish_reason - if hasattr(chunk.choices[0], 'finish_reason') and chunk.choices[0].finish_reason is not None - else 'stop', - logprobs=chunk.choices[0].logprobs, - ) - ], - model=chunk.model, - service_tier=chunk.service_tier if hasattr(chunk, 'service_tier') else None, - system_fingerprint=chunk.system_fingerprint if hasattr(chunk, 'system_fingerprint') else None, - usage=chunk.usage if hasattr(chunk, 'usage') else None, - ) - if chunk - else None - ) + message_delta['tool_calls'] = tool_calls if tool_calls else None + # print(message_delta) + return [message_delta] async def _make_msg( self, - chat_completion: chat_completion.ChatCompletion, + chat_completion: list[dict[str, typing.Any]], ) -> llm_entities.Message: - chatcmpl_message = chat_completion.choices[0].message.dict() + chatcmpl_message = chat_completion[0] # 确保 role 字段存在且不为 None if 'role' not in chatcmpl_message or chatcmpl_message['role'] is None: chatcmpl_message['role'] = 'assistant' - + print(chatcmpl_message) message = llm_entities.Message(**chatcmpl_message) return message @@ -183,7 +160,7 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): args['messages'] = messages # 发送请求 - resp = await self._req(args, extra_body=extra_args, remove_think=remove_think) + resp = await self._req(query, args, extra_body=extra_args, remove_think=remove_think) # 处理请求结果 message = await self._make_msg(resp) From 13dd6fcee3649c1d645a39674b5974311c098890 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Thu, 14 Aug 2025 22:29:42 +0800 Subject: [PATCH 095/107] fix: in the webchat non-stream not save resp_message in message_lists --- pkg/platform/sources/webchat.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/platform/sources/webchat.py b/pkg/platform/sources/webchat.py index dfe8fc30..c43c4628 100644 --- a/pkg/platform/sources/webchat.py +++ b/pkg/platform/sources/webchat.py @@ -276,6 +276,7 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): queue = use_session.resp_queues[message_id] resp_message = await queue.get() + use_session.get_message_list(pipeline_uuid).append(resp_message) resp_message.id = msg_id resp_message.is_final = True From b8b9a378250f5306f400f636d535921e238beebc Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Thu, 14 Aug 2025 22:32:22 +0800 Subject: [PATCH 096/107] fix: in the dify non-stream remove_think lgic --- pkg/provider/runners/difysvapi.py | 220 +++++++++++------------------- 1 file changed, 79 insertions(+), 141 deletions(-) diff --git a/pkg/provider/runners/difysvapi.py b/pkg/provider/runners/difysvapi.py index 8c1307a5..51fddf7b 100644 --- a/pkg/provider/runners/difysvapi.py +++ b/pkg/provider/runners/difysvapi.py @@ -62,6 +62,39 @@ class DifyServiceAPIRunner(runner.RequestRunner): content_text = re.sub(pattern, '', resp_text, flags=re.DOTALL) return f'{thinking_text.group(1)}\n{content_text}' + def _process_thinking_content( + self, + content: str, + ) -> tuple[str, str]: + """处理思维链内容 + + Args: + content: 原始内容 + Returns: + (处理后的内容, 提取的思维链内容) + """ + remove_think = self.pipeline_config['output'].get('misc', '').get('remove-think') + thinking_content = '' + # 从 content 中提取 标签内容 + if content and '' in content and '' in content: + import re + + think_pattern = r'(.*?)' + think_matches = re.findall(think_pattern, content, re.DOTALL) + if think_matches: + thinking_content = '\n'.join(think_matches) + # 移除 content 中的 标签 + content = re.sub(think_pattern, '', content, flags=re.DOTALL).strip() + + # 3. 根据 remove_think 参数决定是否保留思维链 + if remove_think: + return content, '' + else: + # 如果有思维链内容,将其以 格式添加到 content 开头 + if thinking_content: + content = f'\n{thinking_content}\n\n{content}'.strip() + return content, thinking_content + async def _preprocess_user_message(self, query: core_entities.Query) -> tuple[str, list[str]]: """预处理用户消息,提取纯文本,并将图片上传到 Dify 服务 @@ -95,11 +128,6 @@ class DifyServiceAPIRunner(runner.RequestRunner): cov_id = query.session.using_conversation.uuid or '' query.variables['conversation_id'] = cov_id - try: - is_stream = await query.adapter.is_stream_output_supported() - except AttributeError: - is_stream = False - plain_text, image_ids = await self._preprocess_user_message(query) files = [ @@ -113,11 +141,7 @@ class DifyServiceAPIRunner(runner.RequestRunner): mode = 'basic' # 标记是基础编排还是工作流编排 - stream_output_pending_chunk = '' - - batch_pending_max_size = 8 # 积累一定量的消息更新消息一次 - - batch_pending_index = 0 + basic_mode_pending_chunk = '' inputs = {} @@ -135,65 +159,28 @@ class DifyServiceAPIRunner(runner.RequestRunner): ): self.ap.logger.debug('dify-chat-chunk: ' + str(chunk)) - # 查询异常情况 - if chunk['event'] == 'error': - yield llm_entities.Message( - role='assistant', - content=f"查询异常: [{chunk['code']}]. {chunk['message']}.\n请重试,如果还报错,请用 **!reset** 命令重置对话再尝试。", - ) - if chunk['event'] == 'workflow_started': mode = 'workflow' if mode == 'workflow': if chunk['event'] == 'node_finished': - if not is_stream: - if chunk['data']['node_type'] == 'answer': - yield llm_entities.Message( - role='assistant', - content=self._try_convert_thinking(chunk['data']['outputs']['answer']), - ) - else: - if chunk['data']['node_type'] == 'answer': - yield llm_entities.MessageChunk( - role='assistant', - content=self._try_convert_thinking(chunk['data']['outputs']['answer']), - is_final=True, - ) - elif chunk['event'] == 'message': - stream_output_pending_chunk += chunk['answer'] - if is_stream: - # 消息数超过量就输出,从而达到streaming的效果 - batch_pending_index += 1 - if batch_pending_index >= batch_pending_max_size: - yield llm_entities.MessageChunk( - role='assistant', - content=self._try_convert_thinking(stream_output_pending_chunk), - ) - batch_pending_index = 0 + if chunk['data']['node_type'] == 'answer': + content, _ = self._process_thinking_content(chunk['data']['outputs']['answer']) + + yield llm_entities.Message( + role='assistant', + content=content, + ) elif mode == 'basic': - if chunk['event'] == 'message' or chunk['event'] == 'message_end': - if chunk['event'] == 'message_end': - is_final = True - if is_stream and batch_pending_index % batch_pending_max_size == 0: - # 消息数超过量就输出,从而达到streaming的效果 - batch_pending_index += 1 - # if batch_pending_index >= batch_pending_max_size: - yield llm_entities.MessageChunk( - role='assistant', - content=self._try_convert_thinking(stream_output_pending_chunk), - is_final=is_final, - ) - # batch_pending_index = 0 - elif not is_stream: - yield llm_entities.Message( - role='assistant', - content=self._try_convert_thinking(stream_output_pending_chunk), - ) - stream_output_pending_chunk = '' - else: - stream_output_pending_chunk += chunk['answer'] - is_final = False + if chunk['event'] == 'message': + basic_mode_pending_chunk += chunk['answer'] + elif chunk['event'] == 'message_end': + content, _ = self._process_thinking_content(basic_mode_pending_chunk) + yield llm_entities.Message( + role='assistant', + content=content, + ) + basic_mode_pending_chunk = '' if chunk is None: raise errors.DifyAPIError('Dify API 没有返回任何响应,请检查网络连接和API配置') @@ -207,13 +194,6 @@ class DifyServiceAPIRunner(runner.RequestRunner): cov_id = query.session.using_conversation.uuid or '' query.variables['conversation_id'] = cov_id - try: - is_stream = await query.adapter.is_stream_output_supported() - except AttributeError: - is_stream = False - - batch_pending_index = 0 - plain_text, image_ids = await self._preprocess_user_message(query) files = [ @@ -248,66 +228,39 @@ class DifyServiceAPIRunner(runner.RequestRunner): if chunk['event'] in ignored_events: continue - batch_pending_index += 1 - - if chunk['event'] == 'agent_message' or chunk['event'] == 'message_end': - if chunk['event'] == 'message_end': - # break - is_final = True - else: - is_final = False - pending_agent_message += chunk['answer'] - if is_stream: - if batch_pending_index % 64 == 0 or is_final: - yield llm_entities.MessageChunk( - role='assistant', - content=self._try_convert_thinking(pending_agent_message), - is_final=is_final, - ) + if chunk['event'] == 'agent_message' or chunk['event'] == 'message': + pending_agent_message += chunk['answer'] else: - if pending_agent_message.strip() != '' and not is_stream: + if pending_agent_message.strip() != '': pending_agent_message = pending_agent_message.replace('Action:', '') + content, _ = self._process_thinking_content(pending_agent_message) yield llm_entities.Message( role='assistant', - content=self._try_convert_thinking(pending_agent_message), + content=content, ) + pending_agent_message = '' if chunk['event'] == 'agent_thought': if chunk['tool'] != '' and chunk['observation'] != '': # 工具调用结果,跳过 continue if chunk['tool']: - if is_stream: - msg = llm_entities.MessageChunk( - role='assistant', - tool_calls=[ - llm_entities.ToolCall( - id=chunk['id'], - type='function', - function=llm_entities.FunctionCall( - name=chunk['tool'], - arguments=json.dumps({}), - ), - ) - ], - ) - else: - msg = llm_entities.Message( - role='assistant', - tool_calls=[ - llm_entities.ToolCall( - id=chunk['id'], - type='function', - function=llm_entities.FunctionCall( - name=chunk['tool'], - arguments=json.dumps({}), - ), - ) - ], - ) + msg = llm_entities.Message( + role='assistant', + tool_calls=[ + llm_entities.ToolCall( + id=chunk['id'], + type='function', + function=llm_entities.FunctionCall( + name=chunk['tool'], + arguments=json.dumps({}), + ), + ) + ], + ) yield msg - elif chunk['event'] == 'message_file': + if chunk['event'] == 'message_file': if chunk['type'] == 'image' and chunk['belongs_to'] == 'assistant': base_url = self.dify_client.base_url @@ -315,20 +268,13 @@ class DifyServiceAPIRunner(runner.RequestRunner): base_url = base_url[:-3] image_url = base_url + chunk['url'] - if is_stream: - yield llm_entities.MessageChunk( - role='assistant', - content=[llm_entities.ContentElement.from_image_url(image_url)], - ) - else: - yield llm_entities.Message( - role='assistant', - content=[llm_entities.ContentElement.from_image_url(image_url)], - ) - elif chunk['event'] == 'error': + + yield llm_entities.Message( + role='assistant', + content=[llm_entities.ContentElement.from_image_url(image_url)], + ) + if chunk['event'] == 'error': raise errors.DifyAPIError('dify 服务错误: ' + chunk['message']) - else: - pending_agent_message = '' if chunk is None: raise errors.DifyAPIError('Dify API 没有返回任何响应,请检查网络连接和API配置') @@ -343,15 +289,6 @@ class DifyServiceAPIRunner(runner.RequestRunner): query.variables['conversation_id'] = query.session.using_conversation.uuid - try: - is_stream = await query.adapter.is_stream_output_supported() - except AttributeError: - is_stream = False - - _ = is_stream - - # batch_pending_index = 0 - plain_text, image_ids = await self._preprocess_user_message(query) files = [ @@ -408,10 +345,11 @@ class DifyServiceAPIRunner(runner.RequestRunner): elif chunk['event'] == 'workflow_finished': if chunk['data']['error']: raise errors.DifyAPIError(chunk['data']['error']) + content, _ = self._process_thinking_content(chunk['data']['outputs']['summary']) msg = llm_entities.Message( role='assistant', - content=chunk['data']['outputs']['summary'], + content=content, ) yield msg @@ -430,4 +368,4 @@ class DifyServiceAPIRunner(runner.RequestRunner): else: raise errors.DifyAPIError( f'不支持的 Dify 应用类型: {self.pipeline_config["ai"]["dify-service-api"]["app-type"]}' - ) + ) \ No newline at end of file From 8c87a47f5af3b7b602350de1d2ab876fdce43258 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Thu, 14 Aug 2025 22:35:30 +0800 Subject: [PATCH 097/107] fix: in the ollamachat.py func _closure add remove_think agr --- pkg/provider/modelmgr/requesters/ollamachat.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/provider/modelmgr/requesters/ollamachat.py b/pkg/provider/modelmgr/requesters/ollamachat.py index 0a8943c0..42203650 100644 --- a/pkg/provider/modelmgr/requesters/ollamachat.py +++ b/pkg/provider/modelmgr/requesters/ollamachat.py @@ -44,6 +44,7 @@ class OllamaChatCompletions(requester.ProviderAPIRequester): use_model: requester.RuntimeLLMModel, use_funcs: list[tools_entities.LLMFunction] = None, extra_args: dict[str, typing.Any] = {}, + remove_think: bool = False, ) -> llm_entities.Message: args = extra_args.copy() args['model'] = use_model.model_entity.name From 2351193c5109b186663da741976d683a5f71c8ac Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Fri, 15 Aug 2025 00:50:32 +0800 Subject: [PATCH 098/107] fix: in the difysvapi.py add stream , and remove_think on chunk --- pkg/provider/runners/difysvapi.py | 350 +++++++++++++++++++++++++++++- 1 file changed, 338 insertions(+), 12 deletions(-) diff --git a/pkg/provider/runners/difysvapi.py b/pkg/provider/runners/difysvapi.py index 51fddf7b..3b072bc2 100644 --- a/pkg/provider/runners/difysvapi.py +++ b/pkg/provider/runners/difysvapi.py @@ -354,18 +354,344 @@ class DifyServiceAPIRunner(runner.RequestRunner): yield msg + + async def _chat_messages_chunk(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.MessageChunk, None]: + """调用聊天助手""" + cov_id = query.session.using_conversation.uuid or '' + query.variables['conversation_id'] = cov_id + + plain_text, image_ids = await self._preprocess_user_message(query) + + files = [ + { + 'type': 'image', + 'transfer_method': 'local_file', + 'upload_file_id': image_id, + } + for image_id in image_ids + ] + + mode = 'basic' # 标记是基础编排还是工作流编排 + + basic_mode_pending_chunk = '' + + inputs = {} + + inputs.update(query.variables) + message_idx = 0 + + chunk = None # 初始化chunk变量,防止在没有响应时引用错误 + + is_final = False + think_start = False + think_end = False + + remove_think = self.pipeline_config['output'].get('misc', '').get('remove-think') + + async for chunk in self.dify_client.chat_messages( + inputs=inputs, + query=plain_text, + user=f'{query.session.launcher_type.value}_{query.session.launcher_id}', + conversation_id=cov_id, + files=files, + timeout=120, + ): + self.ap.logger.debug('dify-chat-chunk: ' + str(chunk)) + + # if chunk['event'] == 'workflow_started': + # mode = 'workflow' + # if mode == 'workflow': + # elif mode == 'basic': + # 因为都只是返回的 message也没有工具调用什么的,暂时不分类 + if chunk['event'] == 'message': + message_idx += 1 + if remove_think: + if message_idx == 1: + think_start = True + continue + if '' in chunk['answer'] and not think_end: + import re + content = re.sub(r'^\n', '', chunk['answer']) + basic_mode_pending_chunk += content + think_end = True + elif think_end: + basic_mode_pending_chunk += chunk['answer'] + if think_start: + continue + + else: + basic_mode_pending_chunk += chunk['answer'] + + if chunk['event'] == 'message_end': + is_final = True + + if is_final or message_idx % 8 == 0: + # content, _ = self._process_thinking_content(basic_mode_pending_chunk) + yield llm_entities.MessageChunk( + role='assistant', + content=basic_mode_pending_chunk, + is_final=is_final, + ) + + + if chunk is None: + raise errors.DifyAPIError('Dify API 没有返回任何响应,请检查网络连接和API配置') + + query.session.using_conversation.uuid = chunk['conversation_id'] + + + async def _agent_chat_messages_chunk( + self, query: core_entities.Query + ) -> typing.AsyncGenerator[llm_entities.Message, None]: + """调用聊天助手""" + cov_id = query.session.using_conversation.uuid or '' + query.variables['conversation_id'] = cov_id + + plain_text, image_ids = await self._preprocess_user_message(query) + + files = [ + { + 'type': 'image', + 'transfer_method': 'local_file', + 'upload_file_id': image_id, + } + for image_id in image_ids + ] + + ignored_events = [] + + inputs = {} + + inputs.update(query.variables) + + pending_agent_message = '' + + chunk = None # 初始化chunk变量,防止在没有响应时引用错误 + message_idx = 0 + is_final = False + think_start = False + think_end = False + + remove_think = self.pipeline_config['output'].get('misc', '').get('remove-think') + + async for chunk in self.dify_client.chat_messages( + inputs=inputs, + query=plain_text, + user=f'{query.session.launcher_type.value}_{query.session.launcher_id}', + response_mode='streaming', + conversation_id=cov_id, + files=files, + timeout=120, + ): + self.ap.logger.debug('dify-agent-chunk: ' + str(chunk)) + + if chunk['event'] in ignored_events: + continue + + if chunk['event'] == 'agent_message': + message_idx += 1 + if remove_think: + if '' in chunk['answer'] and not think_start: + think_start = True + continue + if '' in chunk['answer'] and not think_end: + import re + content = re.sub(r'^\n', '', chunk['answer']) + pending_agent_message += content + think_end = True + elif think_end: + pending_agent_message += chunk['answer'] + if think_start: + continue + + else: + pending_agent_message += chunk['answer'] + elif chunk['event'] == 'message_end': + is_final = True + else: + + if chunk['event'] == 'agent_thought': + if chunk['tool'] != '' and chunk['observation'] != '': # 工具调用结果,跳过 + continue + message_idx += 1 + if chunk['tool']: + msg = llm_entities.MessageChunk( + role='assistant', + tool_calls=[ + llm_entities.ToolCall( + id=chunk['id'], + type='function', + function=llm_entities.FunctionCall( + name=chunk['tool'], + arguments=json.dumps({}), + ), + ) + ], + ) + yield msg + if chunk['event'] == 'message_file': + message_idx += 1 + if chunk['type'] == 'image' and chunk['belongs_to'] == 'assistant': + base_url = self.dify_client.base_url + + if base_url.endswith('/v1'): + base_url = base_url[:-3] + + image_url = base_url + chunk['url'] + + yield llm_entities.MessageChunk( + role='assistant', + content=[llm_entities.ContentElement.from_image_url(image_url)], + is_final=is_final, + + ) + + if chunk['event'] == 'error': + raise errors.DifyAPIError('dify 服务错误: ' + chunk['message']) + if message_idx % 8 == 0 or is_final: + yield llm_entities.MessageChunk( + role='assistant', + content=pending_agent_message, + is_final=is_final, + ) + + if chunk is None: + raise errors.DifyAPIError('Dify API 没有返回任何响应,请检查网络连接和API配置') + + query.session.using_conversation.uuid = chunk['conversation_id'] + + async def _workflow_messages_chunk(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: + """调用工作流""" + + if not query.session.using_conversation.uuid: + query.session.using_conversation.uuid = str(uuid.uuid4()) + + query.variables['conversation_id'] = query.session.using_conversation.uuid + + plain_text, image_ids = await self._preprocess_user_message(query) + + files = [ + { + 'type': 'image', + 'transfer_method': 'local_file', + 'upload_file_id': image_id, + } + for image_id in image_ids + ] + + ignored_events = ['workflow_started'] + + inputs = { # these variables are legacy variables, we need to keep them for compatibility + 'langbot_user_message_text': plain_text, + 'langbot_session_id': query.variables['session_id'], + 'langbot_conversation_id': query.variables['conversation_id'], + 'langbot_msg_create_time': query.variables['msg_create_time'], + } + + inputs.update(query.variables) + messsage_idx = 0 + is_final = False + think_start = False + think_end = False + workflow_contents = '' + + remove_think = self.pipeline_config['output'].get('misc', '').get('remove-think') + async for chunk in self.dify_client.workflow_run( + inputs=inputs, + user=f'{query.session.launcher_type.value}_{query.session.launcher_id}', + files=files, + timeout=120, + ): + self.ap.logger.debug('dify-workflow-chunk: ' + str(chunk)) + if chunk['event'] in ignored_events: + continue + if chunk['event'] == 'workflow_finished': + is_final = True + if chunk['data']['error']: + raise errors.DifyAPIError(chunk['data']['error']) + + if chunk['event'] == 'text_chunk': + messsage_idx += 1 + if remove_think: + if '' in chunk['data']['text'] and not think_start: + think_start = True + continue + if '' in chunk['data']['text'] and not think_end: + import re + content = re.sub(r'^\n', '', chunk['data']['text']) + workflow_contents += content + think_end = True + elif think_end: + workflow_contents += chunk['data']['text'] + if think_start: + continue + + else: + workflow_contents += chunk['data']['text'] + + if chunk['event'] == 'node_started': + if chunk['data']['node_type'] == 'start' or chunk['data']['node_type'] == 'end': + continue + messsage_idx += 1 + msg = llm_entities.MessageChunk( + role='assistant', + content=None, + tool_calls=[ + llm_entities.ToolCall( + id=chunk['data']['node_id'], + type='function', + function=llm_entities.FunctionCall( + name=chunk['data']['title'], + arguments=json.dumps({}), + ), + ) + ], + ) + + yield msg + + + if messsage_idx % 8 == 0 or is_final: + yield llm_entities.MessageChunk( + role='assistant', + content=workflow_contents, + is_final=is_final, + ) + async def run(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: """运行请求""" - if self.pipeline_config['ai']['dify-service-api']['app-type'] == 'chat': - async for msg in self._chat_messages(query): - yield msg - elif self.pipeline_config['ai']['dify-service-api']['app-type'] == 'agent': - async for msg in self._agent_chat_messages(query): - yield msg - elif self.pipeline_config['ai']['dify-service-api']['app-type'] == 'workflow': - async for msg in self._workflow_messages(query): - yield msg + if await query.adapter.is_stream_output_supported(): + msg_idx = 0 + if self.pipeline_config['ai']['dify-service-api']['app-type'] == 'chat': + async for msg in self._chat_messages_chunk(query): + msg_idx += 1 + msg.msg_sequence = msg_idx + yield msg + elif self.pipeline_config['ai']['dify-service-api']['app-type'] == 'agent': + async for msg in self._agent_chat_messages_chunk(query): + msg_idx += 1 + msg.msg_sequence = msg_idx + yield msg + elif self.pipeline_config['ai']['dify-service-api']['app-type'] == 'workflow': + async for msg in self._workflow_messages_chunk(query): + msg_idx += 1 + msg.msg_sequence = msg_idx + yield msg + else: + raise errors.DifyAPIError( + f'不支持的 Dify 应用类型: {self.pipeline_config["ai"]["dify-service-api"]["app-type"]}' + ) else: - raise errors.DifyAPIError( - f'不支持的 Dify 应用类型: {self.pipeline_config["ai"]["dify-service-api"]["app-type"]}' - ) \ No newline at end of file + if self.pipeline_config['ai']['dify-service-api']['app-type'] == 'chat': + async for msg in self._chat_messages(query): + yield msg + elif self.pipeline_config['ai']['dify-service-api']['app-type'] == 'agent': + async for msg in self._agent_chat_messages(query): + yield msg + elif self.pipeline_config['ai']['dify-service-api']['app-type'] == 'workflow': + async for msg in self._workflow_messages(query): + yield msg + else: + raise errors.DifyAPIError( + f'不支持的 Dify 应用类型: {self.pipeline_config["ai"]["dify-service-api"]["app-type"]}' + ) \ No newline at end of file From 8f863cf530555689eaac3e4783920cfb01ba4358 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Fri, 15 Aug 2025 00:55:39 +0800 Subject: [PATCH 099/107] fix: remove_think bug --- pkg/provider/runners/difysvapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/provider/runners/difysvapi.py b/pkg/provider/runners/difysvapi.py index 3b072bc2..1a443a16 100644 --- a/pkg/provider/runners/difysvapi.py +++ b/pkg/provider/runners/difysvapi.py @@ -406,7 +406,7 @@ class DifyServiceAPIRunner(runner.RequestRunner): if chunk['event'] == 'message': message_idx += 1 if remove_think: - if message_idx == 1: + if '' in chunk['answer'] and not think_start: think_start = True continue if '' in chunk['answer'] and not think_end: From 46fbfbefeaabaa99630da7b89b4e19fa22333bcc Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Sat, 16 Aug 2025 02:13:45 +0800 Subject: [PATCH 100/107] fix: in the dashscopeapi.py stream and non-stream remove_think logic --- pkg/provider/runners/dashscopeapi.py | 38 +++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/pkg/provider/runners/dashscopeapi.py b/pkg/provider/runners/dashscopeapi.py index 7f66e6f0..f488d23c 100644 --- a/pkg/provider/runners/dashscopeapi.py +++ b/pkg/provider/runners/dashscopeapi.py @@ -99,8 +99,14 @@ class DashScopeAPIRunner(runner.RequestRunner): plain_text = '' # 用户输入的纯文本信息 image_ids = [] # 用户输入的图片ID列表 (暂不支持) - plain_text, image_ids = await self._preprocess_user_message(query) + think_start = False + think_end = False + plain_text, image_ids = await self._preprocess_user_message(query) + has_thoughts = True # 获取思考过程 + remove_think = self.pipeline_config['output'].get('misc', '').get('remove-think') + if remove_think: + has_thoughts = False # 发送对话请求 response = dashscope.Application.call( api_key=self.api_key, # 智能体应用的API Key @@ -109,6 +115,7 @@ class DashScopeAPIRunner(runner.RequestRunner): stream=True, # 流式输出 incremental_output=True, # 增量输出,使用流式输出需要开启增量输出 session_id=query.session.using_conversation.uuid, # 会话ID用于,多轮对话 + has_thoughts=has_thoughts, # rag_options={ # 主要用于文件交互,暂不支持 # "session_file_ids": ["FILE_ID1"], # FILE_ID1 替换为实际的临时文件ID,逗号隔开多个 # } @@ -131,6 +138,17 @@ class DashScopeAPIRunner(runner.RequestRunner): idx_chunk += 1 # 获取流式传输的output stream_output = chunk.get('output', {}) + stream_think = stream_output.get('thoughts', []) + if stream_think[0].get('thought'): + if not think_start: + think_start = True + pending_content += f"\n{stream_think[0].get('thought')}" + else: + # 继续输出 reasoning_content + pending_content += stream_think[0].get('thought') + elif stream_think[0].get('thought') == "" and not think_end: + think_end = True + pending_content += "\n\n" if stream_output.get('text') is not None: pending_content += stream_output.get('text') # 是否是流式最后一个chunk @@ -167,6 +185,17 @@ class DashScopeAPIRunner(runner.RequestRunner): idx_chunk += 1 # 获取流式传输的output stream_output = chunk.get('output', {}) + stream_think = stream_output.get('thoughts', []) + if stream_think[0].get('thought'): + if not think_start: + think_start = True + pending_content += f"\n{stream_think[0].get('thought')}" + else: + # 继续输出 reasoning_content + pending_content += stream_think[0].get('thought') + elif stream_think[0].get('thought') == "" and not think_end: + think_end = True + pending_content += "\n\n" if stream_output.get('text') is not None: pending_content += stream_output.get('text') @@ -302,11 +331,18 @@ class DashScopeAPIRunner(runner.RequestRunner): async def run(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: """运行""" + msg_seq = 0 if self.app_type == 'agent': async for msg in self._agent_messages(query): + if isinstance(msg, llm_entities.MessageChunk): + msg_seq += 1 + msg.msg_sequence = msg_seq yield msg elif self.app_type == 'workflow': async for msg in self._workflow_messages(query): + if isinstance(msg, llm_entities.MessageChunk): + msg_seq += 1 + msg.msg_sequence = msg_seq yield msg else: raise DashscopeAPIError(f'不支持的 Dashscope 应用类型: {self.app_type}') From 8ccda10045406db74ba708de5b3a430e954eb623 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Sat, 16 Aug 2025 12:11:00 +0800 Subject: [PATCH 101/107] fix: in the dashscopeapi.py workflow stream bug --- pkg/provider/runners/dashscopeapi.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/provider/runners/dashscopeapi.py b/pkg/provider/runners/dashscopeapi.py index f488d23c..b646a45d 100644 --- a/pkg/provider/runners/dashscopeapi.py +++ b/pkg/provider/runners/dashscopeapi.py @@ -243,6 +243,7 @@ class DashScopeAPIRunner(runner.RequestRunner): incremental_output=True, # 增量输出,使用流式输出需要开启增量输出 session_id=query.session.using_conversation.uuid, # 会话ID用于,多轮对话 biz_params=biz_params, # 工作流应用的自定义输入参数传递 + flow_stream_mode="message_format" # 消息模式,输出/结束节点的流式结果 # rag_options={ # 主要用于文件交互,暂不支持 # "session_file_ids": ["FILE_ID1"], # FILE_ID1 替换为实际的临时文件ID,逗号隔开多个 # } @@ -267,8 +268,10 @@ class DashScopeAPIRunner(runner.RequestRunner): idx_chunk += 1 # 获取流式传输的output stream_output = chunk.get('output', {}) - if stream_output.get('text') is not None: - pending_content += stream_output.get('text') + if stream_output.get('workflow_message') is not None: + pending_content += stream_output.get('workflow_message').get('message').get('content') + # if stream_output.get('text') is not None: + # pending_content += stream_output.get('text') is_final = False if stream_output.get('finish_reason', False) == 'null' else True @@ -283,7 +286,7 @@ class DashScopeAPIRunner(runner.RequestRunner): # 将参考资料替换到文本中 pending_content = self._replace_references(pending_content, references_dict) - if is_final: + if idx_chunk % 8 == 0 or is_final: yield llm_entities.MessageChunk( role='assistant', content=pending_content, From 4bbfa2f1d72414ce2c4e8a12e0d0e2a35f1de1c4 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 17 Aug 2025 13:52:02 +0800 Subject: [PATCH 102/107] fix: telegram adapter gracefully stop --- pkg/platform/sources/telegram.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/platform/sources/telegram.py b/pkg/platform/sources/telegram.py index 42279317..8aee12d7 100644 --- a/pkg/platform/sources/telegram.py +++ b/pkg/platform/sources/telegram.py @@ -218,7 +218,6 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): ): msg_seq = bot_message.msg_sequence if (msg_seq - 1) % 8 == 0 or is_final: - assert isinstance(message_source.source_platform_object, Update) components = await TelegramMessageConverter.yiri2target(message, self.bot) args = {} @@ -295,8 +294,12 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): self.bot_account_id = (await self.bot.get_me()).username await self.application.updater.start_polling(allowed_updates=Update.ALL_TYPES) await self.application.start() + await self.logger.info('Telegram adapter running') async def kill(self) -> bool: if self.application.running: await self.application.stop() + if self.application.updater: + await self.application.updater.stop() + await self.logger.info('Telegram adapter stopped') return True From e931d5eb888c416b8fb2746b196729ed7a2e59dc Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 17 Aug 2025 13:52:40 +0800 Subject: [PATCH 103/107] chore: remove print --- .../modelmgr/requesters/anthropicmsgs.py | 68 ++++++++----------- 1 file changed, 29 insertions(+), 39 deletions(-) diff --git a/pkg/provider/modelmgr/requesters/anthropicmsgs.py b/pkg/provider/modelmgr/requesters/anthropicmsgs.py index e9adf390..cb0c7ce1 100644 --- a/pkg/provider/modelmgr/requesters/anthropicmsgs.py +++ b/pkg/provider/modelmgr/requesters/anthropicmsgs.py @@ -91,9 +91,8 @@ class AnthropicMessages(requester.ProviderAPIRequester): { 'type': 'tool_result', 'tool_use_id': tool_call_id, - 'is_error':False, - 'content': [{"type": "text", - "text": m.content}], + 'is_error': False, + 'content': [{'type': 'text', 'text': m.content}], } ], } @@ -138,10 +137,7 @@ class AnthropicMessages(requester.ProviderAPIRequester): args['messages'] = req_messages if 'thinking' in args: - args['thinking'] = { - "type": "enabled", - "budget_tokens": 10000 - } + args['thinking'] = {'type': 'enabled', 'budget_tokens': 10000} if funcs: tools = await self.ap.tool_mgr.generate_tools_for_anthropic(funcs) @@ -185,21 +181,20 @@ class AnthropicMessages(requester.ProviderAPIRequester): else: raise errors.RequesterError(f'请求地址无效: {e.message}') - async def invoke_llm_stream( - self, - query: core_entities.Query, - model: requester.RuntimeLLMModel, - messages: typing.List[llm_entities.Message], - funcs: typing.List[tools_entities.LLMFunction] = None, - extra_args: dict[str, typing.Any] = {}, - remove_think: bool = False, + self, + query: core_entities.Query, + model: requester.RuntimeLLMModel, + messages: typing.List[llm_entities.Message], + funcs: typing.List[tools_entities.LLMFunction] = None, + extra_args: dict[str, typing.Any] = {}, + remove_think: bool = False, ) -> llm_entities.Message: self.client.api_key = model.token_mgr.get_token() args = extra_args.copy() args['model'] = model.model_entity.name - args['stream'] = True + args['stream'] = True # 处理消息 @@ -231,9 +226,10 @@ class AnthropicMessages(requester.ProviderAPIRequester): { 'type': 'tool_result', 'tool_use_id': tool_call_id, - 'is_error':False, # 暂时直接写false - 'content': [{"type": "text", - "text": m.content}], # 这里要是list包裹,应该是多个返回的情况?type类型好像也可以填其他的,暂时只写text + 'is_error': False, # 暂时直接写false + 'content': [ + {'type': 'text', 'text': m.content} + ], # 这里要是list包裹,应该是多个返回的情况?type类型好像也可以填其他的,暂时只写text } ], } @@ -276,10 +272,7 @@ class AnthropicMessages(requester.ProviderAPIRequester): req_messages.append(msg_dict) if 'thinking' in args: - args['thinking'] = { - "type": "enabled", - "budget_tokens": 10000 - } + args['thinking'] = {'type': 'enabled', 'budget_tokens': 10000} args['messages'] = req_messages @@ -298,18 +291,17 @@ class AnthropicMessages(requester.ProviderAPIRequester): content = '' tool_name = '' tool_id = '' - tool_calls = [] async for chunk in await self.client.messages.create(**args): - tool_call = {"id":None, 'function': {"name": None, "arguments": None},'type':'function'} - if isinstance(chunk, anthropic.types.raw_content_block_start_event.RawContentBlockStartEvent): # 记录开始 + tool_call = {'id': None, 'function': {'name': None, 'arguments': None}, 'type': 'function'} + if isinstance( + chunk, anthropic.types.raw_content_block_start_event.RawContentBlockStartEvent + ): # 记录开始 if chunk.content_block.type == 'tool_use': - if chunk.content_block.name is not None: tool_name = chunk.content_block.name if chunk.content_block.id is not None: tool_id = chunk.content_block.id - tool_call['function']['name'] = tool_name tool_call['function']['arguments'] = '' tool_call['id'] = tool_id @@ -321,7 +313,7 @@ class AnthropicMessages(requester.ProviderAPIRequester): think_ended = True continue elif isinstance(chunk, anthropic.types.raw_content_block_delta_event.RawContentBlockDeltaEvent): - if chunk.delta.type == "thinking_delta": + if chunk.delta.type == 'thinking_delta': if think_started: think_started = False content = '\n' + chunk.delta.thinking @@ -329,34 +321,33 @@ class AnthropicMessages(requester.ProviderAPIRequester): continue else: content = chunk.delta.thinking - elif chunk.delta.type == "text_delta": + elif chunk.delta.type == 'text_delta': if think_ended: think_ended = False content = '\n\n' + chunk.delta.text else: content = chunk.delta.text - elif chunk.delta.type == "input_json_delta": - tool_call['function']["arguments"] = chunk.delta.partial_json + elif chunk.delta.type == 'input_json_delta': + tool_call['function']['arguments'] = chunk.delta.partial_json tool_call['function']['name'] = tool_name tool_call['id'] = tool_id elif isinstance(chunk, anthropic.types.raw_content_block_stop_event.RawContentBlockStopEvent): continue # 记录raw_content_block结束的 elif isinstance(chunk, anthropic.types.raw_message_delta_event.RawMessageDeltaEvent): - if chunk.delta.stop_reason == "end_turn": + if chunk.delta.stop_reason == 'end_turn': finish_reason = True elif isinstance(chunk, anthropic.types.raw_message_stop_event.RawMessageStopEvent): - continue # 这个好像是完全结束 + continue # 这个好像是完全结束 else: - print(chunk) + # print(chunk) + self.ap.logger.debug(f'anthropic chunk: {chunk}') continue - - args = { 'content': content, 'role': role, - "is_final": finish_reason, + 'is_final': finish_reason, 'tool_calls': None if tool_call['id'] is None else [tool_call], } # if chunk_idx == 0: @@ -365,7 +356,6 @@ class AnthropicMessages(requester.ProviderAPIRequester): # assert type(chunk) is anthropic.types.message.Chunk - yield llm_entities.MessageChunk(**args) # return llm_entities.Message(**args) From 3049aa7a96cf2f613e8d360419d019429737f428 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 17 Aug 2025 21:18:41 +0800 Subject: [PATCH 104/107] feat: add migration for pipeline `remove-think` --- .../dbm005_pipeline_remove_cot_config.py | 38 +++++++++++++++++++ pkg/utils/constants.py | 2 +- 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 pkg/persistence/migrations/dbm005_pipeline_remove_cot_config.py diff --git a/pkg/persistence/migrations/dbm005_pipeline_remove_cot_config.py b/pkg/persistence/migrations/dbm005_pipeline_remove_cot_config.py new file mode 100644 index 00000000..14f0beec --- /dev/null +++ b/pkg/persistence/migrations/dbm005_pipeline_remove_cot_config.py @@ -0,0 +1,38 @@ +from .. import migration + +import sqlalchemy + +from ...entity.persistence import pipeline as persistence_pipeline + + +@migration.migration_class(5) +class DBMigratePipelineRemoveCotConfig(migration.DBMigration): + """Pipeline remove cot config""" + + async def upgrade(self): + """Upgrade""" + # read all pipelines + pipelines = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline)) + + for pipeline in pipelines: + serialized_pipeline = self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline) + + config = serialized_pipeline['config'] + + if 'remove-think' not in config['output']['misc']: + config['output']['misc']['remove-think'] = True + + await self.ap.persistence_mgr.execute_async( + sqlalchemy.update(persistence_pipeline.LegacyPipeline) + .where(persistence_pipeline.LegacyPipeline.uuid == serialized_pipeline['uuid']) + .values( + { + 'config': config, + 'for_version': self.ap.ver_mgr.get_current_version(), + } + ) + ) + + async def downgrade(self): + """Downgrade""" + pass diff --git a/pkg/utils/constants.py b/pkg/utils/constants.py index e13958d9..28d6e3e5 100644 --- a/pkg/utils/constants.py +++ b/pkg/utils/constants.py @@ -1,6 +1,6 @@ semantic_version = 'v4.1.2' -required_database_version = 4 +required_database_version = 5 """Tag the version of the database schema, used to check if the database needs to be migrated""" debug_mode = False From da890d30748495157e39a17dc50ee3c77f5cc7dc Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 17 Aug 2025 21:20:32 +0800 Subject: [PATCH 105/107] chore: remove `fix.MD` --- fix.MD | 47 ----------------------------------------------- 1 file changed, 47 deletions(-) delete mode 100644 fix.MD diff --git a/fix.MD b/fix.MD deleted file mode 100644 index 51927eb9..00000000 --- a/fix.MD +++ /dev/null @@ -1,47 +0,0 @@ -## 底层模型请求器 - -- pkg/provider/modelmgr/requesters/... - -给 invoke_llm 加个 stream: bool 参数,并允许 invoke_llm 返回两种参数:原来的 llm_entities.Message(非流式)和 返回 llm_entities.MessageChunk(流式,需要新增这个实体)的 AsyncGenerator - -## Runner - -- pkg/provider/runners/... - -每个runner的run方法也允许传入stream: bool。 - -现在的run方法本身就是生成器(AsyncGenerator),因为agent是有多回合的,会生成多条Message。但现在需要支持文本消息可以分段。 - -现在run方法应该返回 AsyncGenerator[ Union[ Message, AsyncGenerator[MessageChunk] ] ]。 - -对于 local agent 的实现上,调用模型invoke_llm时,传入stream,当发现模型返回的是Message时,即按照现在的写法操作Message;当返回的是 AsyncGenerator 时,需要 yield MessageChunk 给上层,同时需要注意判断工具调用。 - -## 流水线 - -- pkg/pipeline/process/handlers/chat.py - -之前这里就已经有一个生成器写法了,用于处理 AsyncGenerator[Message],但现在需要加上一个判断,如果yield出来的是 Message 则按照现在的处理;如果yield出来的是 AsyncGenerator,那么就需要再 async for 一层; - -因为流水线是基于责任链模式设计的,这里的生成结果只需要放入 Query 对象中,供下一层处理。 - -所以需要在 Query 对象中支持存入MessageChunk,现在只支持存 Message 到 resp_messages,这里得设计一下。 - -## 回复阶段 - -最终会在 pkg/pipeline/respback/respback.py 中检出 query 中的信息并发回,这里也要改成支持 MessagChunk 的。 - -这里应该判断适配器是否支持流式,若不支持,应该等待所有 MessageChunk 生成,拼接成 Message 再转换成 MessageChain 调用 send_message(); - -若支持,则uuid生成一个message id,使用该message id调用适配器的 reply_message_chunk 方法。 - -## 机器人适配器 - -因为机器人可能会由于用户配置项不同而表现为对流式的支持性不同,比如飞书默认不支持流式,需要用户额外配置卡片。 - -所以需要新增一个方法 `is_stream_output_supported() -> bool`,这个让每个适配器来判断并返回是否支持流式; - -在发送时,得加两个方法 `send_message_chunk(target_type: str, target_id: str, message_id: , message: MessageChain)` - -message_id 确定同一条消息,由调用方生成; - -`reply_message_chunk(message_source: MessageEvent, message: MessageChain)` \ No newline at end of file From a534c02d7569d3741087347644c057297f39a267 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Sun, 17 Aug 2025 23:34:01 +0800 Subject: [PATCH 106/107] fix:remove print --- pkg/provider/modelmgr/requesters/modelscopechatcmpl.py | 2 -- pkg/provider/runners/dashscopeapi.py | 2 -- pkg/provider/runners/localagent.py | 1 - 3 files changed, 5 deletions(-) diff --git a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py index 72e6dd58..82d8df70 100644 --- a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py +++ b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py @@ -108,7 +108,6 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): message_delta['role'] = 'assistant' message_delta['tool_calls'] = tool_calls if tool_calls else None - # print(message_delta) return [message_delta] async def _make_msg( @@ -120,7 +119,6 @@ class ModelScopeChatCompletions(requester.ProviderAPIRequester): # 确保 role 字段存在且不为 None if 'role' not in chatcmpl_message or chatcmpl_message['role'] is None: chatcmpl_message['role'] = 'assistant' - print(chatcmpl_message) message = llm_entities.Message(**chatcmpl_message) return message diff --git a/pkg/provider/runners/dashscopeapi.py b/pkg/provider/runners/dashscopeapi.py index b646a45d..737bc312 100644 --- a/pkg/provider/runners/dashscopeapi.py +++ b/pkg/provider/runners/dashscopeapi.py @@ -122,7 +122,6 @@ class DashScopeAPIRunner(runner.RequestRunner): ) idx_chunk = 0 try: - # print(await query.adapter.is_stream_output_supported()) is_stream = await query.adapter.is_stream_output_supported() except AttributeError: @@ -251,7 +250,6 @@ class DashScopeAPIRunner(runner.RequestRunner): # 处理API返回的流式输出 try: - # print(await query.adapter.is_stream_output_supported()) is_stream = await query.adapter.is_stream_output_supported() except AttributeError: diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index 124d405f..2500b363 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -148,7 +148,6 @@ class LocalAgentRunner(runner.RequestRunner): if tool_call.function and tool_call.function.arguments: # 流式处理中,工具调用参数可能分多个chunk返回,需要追加而不是覆盖 tool_calls_map[tool_call.id].function.arguments += tool_call.function.arguments - # print(list(tool_calls_map.values()) if (tool_calls_map and msg.is_final) else None) # continue # 每8个chunk或最后一个chunk时,输出所有累积的内容 if msg_idx % 8 == 0 or msg.is_final: From 67b622d5a648e85a0065141dadeec7c064075ff6 Mon Sep 17 00:00:00 2001 From: Dong_master <2213070223@qq.com> Date: Sun, 17 Aug 2025 23:34:19 +0800 Subject: [PATCH 107/107] fix:Some adjustments to the return types --- pkg/provider/runners/difysvapi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/provider/runners/difysvapi.py b/pkg/provider/runners/difysvapi.py index 1a443a16..b527a3bf 100644 --- a/pkg/provider/runners/difysvapi.py +++ b/pkg/provider/runners/difysvapi.py @@ -442,7 +442,7 @@ class DifyServiceAPIRunner(runner.RequestRunner): async def _agent_chat_messages_chunk( self, query: core_entities.Query - ) -> typing.AsyncGenerator[llm_entities.Message, None]: + ) -> typing.AsyncGenerator[llm_entities.MessageChunk, None]: """调用聊天助手""" cov_id = query.session.using_conversation.uuid or '' query.variables['conversation_id'] = cov_id @@ -560,7 +560,7 @@ class DifyServiceAPIRunner(runner.RequestRunner): query.session.using_conversation.uuid = chunk['conversation_id'] - async def _workflow_messages_chunk(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: + async def _workflow_messages_chunk(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.MessageChunk, None]: """调用工作流""" if not query.session.using_conversation.uuid: