From 36c0cfc9a94b6d38cfce00ebdd1a6a8e736cbf05 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Sat, 22 Feb 2025 14:08:51 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20feat:=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E9=98=BF=E9=87=8C=E4=BA=91=E7=99=BE=E7=82=BC=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E6=99=BA=E8=83=BD=E4=BD=93=E3=80=81=E5=B7=A5=E4=BD=9C=E6=B5=81?= =?UTF-8?q?=20#552?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/config/default.py | 16 +++ astrbot/core/provider/manager.py | 2 + .../core/provider/sources/dashscope_source.py | 111 ++++++++++++++++++ requirements.txt | 4 +- 4 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 astrbot/core/provider/sources/dashscope_source.py diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index d680cf6c8..68c3146b5 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -529,6 +529,15 @@ CONFIG_METADATA_2 = { "dify_query_input_key": "astrbot_text_query", "timeout": 60, }, + "dashscope": { + "id": "dashscope", + "type": "dashscope", + "enable": True, + "dashscope_app_type": "agent", + "dashscope_api_key": "", + "dashscope_app_id": "", + "timeout": 60, + }, "whisper(API)": { "id": "whisper", "type": "openai_whisper_api", @@ -565,6 +574,13 @@ CONFIG_METADATA_2 = { }, }, "items": { + "dashscope_app_type": { + "description": "应用类型", + "type": "string", + "hint": "阿里云百炼应用的应用类型。", + "options": ["agent", "agent-arrange", "dialog-workflow", "task-workflow"], + "obvious_hint": True, + }, "timeout": { "description": "超时时间", "type": "int", diff --git a/astrbot/core/provider/manager.py b/astrbot/core/provider/manager.py index db8569b83..70a4af389 100644 --- a/astrbot/core/provider/manager.py +++ b/astrbot/core/provider/manager.py @@ -124,6 +124,8 @@ class ProviderManager(): from .sources.llmtuner_source import LLMTunerModelLoader as LLMTunerModelLoader case "dify": from .sources.dify_source import ProviderDify as ProviderDify + case "dashscope": + from .sources.dashscope_source import ProviderDashscope as ProviderDashscope case "googlegenai_chat_completion": from .sources.gemini_source import ProviderGoogleGenAI as ProviderGoogleGenAI case "openai_whisper_api": diff --git a/astrbot/core/provider/sources/dashscope_source.py b/astrbot/core/provider/sources/dashscope_source.py new file mode 100644 index 000000000..56ab79934 --- /dev/null +++ b/astrbot/core/provider/sources/dashscope_source.py @@ -0,0 +1,111 @@ +import asyncio +from typing import List +from .. import Provider, Personality +from ..entites import LLMResponse +from ..func_tool_manager import FuncCall +from astrbot.core.db import BaseDatabase +from ..register import register_provider_adapter +from .openai_source import ProviderOpenAIOfficial +from astrbot.core import logger, sp +from dashscope import Application + +@register_provider_adapter("dashscope", "Dashscope APP 适配器。") +class ProviderDashscope(ProviderOpenAIOfficial): + def __init__( + self, + provider_config: dict, + provider_settings: dict, + db_helper: BaseDatabase, + persistant_history=False, + default_persona: Personality=None + ) -> None: + Provider.__init__( + self, provider_config, provider_settings, persistant_history, db_helper, default_persona + ) + self.api_key = provider_config.get("dashscope_api_key", "") + if not self.api_key: + raise Exception("阿里云百炼 API Key 不能为空。") + self.app_id = provider_config.get("dashscope_app_id", "") + if not self.app_id: + raise Exception("阿里云百炼 APP ID 不能为空。") + self.dashscope_app_type = provider_config.get("dashscope_app_type", "") + if not self.dashscope_app_type: + raise Exception("阿里云百炼 APP 类型不能为空。") + self.model_name = "dashscope" + + self.timeout = provider_config.get("timeout", 120) + if isinstance(self.timeout, str): + self.timeout = int(self.timeout) + + async def text_chat( + self, + prompt: str, + session_id: str = None, + image_urls: List[str] = [], + func_tool: FuncCall = None, + contexts: List = None, + system_prompt: str = None, + **kwargs, + ) -> LLMResponse: + if self.dashscope_app_type in ["agent", "dialog-workflow"]: + # 支持多轮对话的 + new_record = {"role": "user", "content": prompt} + if image_urls: + logger.warning("阿里云百炼暂不支持图片输入,将自动忽略图片内容。") + contexts_no_img = await self._remove_image_from_context(contexts) + context_query = [*contexts_no_img, new_record] + if system_prompt: + context_query.insert(0, {"role": "system", "content": system_prompt}) + for part in context_query: + if '_no_save' in part: + del part['_no_save'] + # 调用阿里云百炼 API + response = await asyncio.get_event_loop().run_in_executor( + None, + Application.call, + self.app_id, + None, + None, + None, + self.api_key, + context_query + ) + else: + # 不支持多轮对话的 + # 调用阿里云百炼 API + response = await asyncio.get_event_loop().run_in_executor( + None, + Application.call, + self.app_id, + prompt, + None, + None, + self.api_key + ) + + logger.debug(f"dashscope resp: {response}") + + if response.status_code != 200: + logger.error(f"阿里云百炼请求失败: request_id={response.request_id}, code={response.status_code}, message={response.message}, 请参考文档:https://help.aliyun.com/zh/model-studio/developer-reference/error-code") + return LLMResponse(role="err", completion_text=f"阿里云百炼请求失败: message={response.message} code={response.status_code}") + + output_text = response.output.get("text", "") + return LLMResponse(role="assistant", completion_text=output_text) + + async def forget(self, session_id): + return True + + async def get_current_key(self): + return self.api_key + + async def set_key(self, key): + raise Exception("阿里云百炼 适配器不支持设置 API Key。") + + async def get_models(self): + return [self.get_model()] + + async def get_human_readable_context(self, session_id, page, page_size): + raise Exception("暂不支持获得 阿里云百炼 的历史消息记录。") + + async def terminate(self): + await self.api_client.close() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e524df84f..39abe351d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,4 +21,6 @@ silk-python lark-oapi ormsgpack -cryptography \ No newline at end of file +cryptography + +dashscope \ No newline at end of file From 466c80b94d173f0d05983fb0ee317777e939839b Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Sat, 22 Feb 2025 14:32:37 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E2=9C=A8=20feat:=20=E9=98=BF=E9=87=8C?= =?UTF-8?q?=E4=BA=91=E7=99=BE=E7=82=BC=E5=BA=94=E7=94=A8=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E6=B5=81=E6=94=AF=E6=8C=81=E8=87=AA=E5=AE=9A=E4=B9=89=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E5=8F=98=E9=87=8F=20#552?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../process_stage/method/dify_request.py | 90 ------------------- .../process_stage/method/llm_request.py | 4 +- astrbot/core/pipeline/process_stage/stage.py | 13 +-- .../core/provider/sources/dashscope_source.py | 35 +++++--- packages/astrbot/main.py | 3 - 5 files changed, 26 insertions(+), 119 deletions(-) delete mode 100644 astrbot/core/pipeline/process_stage/method/dify_request.py diff --git a/astrbot/core/pipeline/process_stage/method/dify_request.py b/astrbot/core/pipeline/process_stage/method/dify_request.py deleted file mode 100644 index 3dca3792d..000000000 --- a/astrbot/core/pipeline/process_stage/method/dify_request.py +++ /dev/null @@ -1,90 +0,0 @@ -''' -Dify 调用 Stage -''' -import traceback -from typing import Union, AsyncGenerator -from ...context import PipelineContext -from ..stage import Stage -from astrbot.core.platform.astr_message_event import AstrMessageEvent -from astrbot.core.message.message_event_result import MessageEventResult, ResultContentType -from astrbot.core.message.components import Image -from astrbot.core import logger -from astrbot.core.utils.metrics import Metric -from astrbot.core.provider.entites import ProviderRequest -from astrbot.core.star.star_handler import star_handlers_registry, EventType - -class DifyRequestSubStage(Stage): - - async def initialize(self, ctx: PipelineContext) -> None: - self.ctx = ctx - - async def process(self, event: AstrMessageEvent) -> Union[None, AsyncGenerator[None, None]]: - req: ProviderRequest = None - - provider = self.ctx.plugin_manager.context.get_using_provider() - - if not provider: - return - - if provider.meta().type != "dify": - return - - if event.get_extra("provider_request"): - req = event.get_extra("provider_request") - assert isinstance(req, ProviderRequest), "provider_request 必须是 ProviderRequest 类型。" - else: - req = ProviderRequest(prompt="", image_urls=[]) - if self.ctx.astrbot_config['provider_settings']['wake_prefix']: - if not event.message_str.startswith(self.ctx.astrbot_config['provider_settings']['wake_prefix']): - return - req.prompt = event.message_str[len(self.ctx.astrbot_config['provider_settings']['wake_prefix']):] - for comp in event.message_obj.message: - if isinstance(comp, Image): - image_url = comp.url if comp.url else comp.file - req.image_urls.append(image_url) - req.session_id = event.session_id - event.set_extra("provider_request", req) - - if not req.prompt: - return - - req.session_id = event.unified_msg_origin - - # 执行请求 LLM 前事件钩子。 - # 装饰 system_prompt 等功能 - handlers = star_handlers_registry.get_handlers_by_event_type(EventType.OnLLMRequestEvent) - for handler in handlers: - try: - await handler.handler(event, req) - except BaseException: - logger.error(traceback.format_exc()) - - try: - logger.debug(f"Dify 请求 Payload: {req.__dict__}") - llm_response = await provider.text_chat(**req.__dict__) # 请求 LLM - await Metric.upload(llm_tick=1, model_name=provider.get_model(), provider_type=provider.meta().type) - - # 执行 LLM 响应后的事件。 - handlers = star_handlers_registry.get_handlers_by_event_type(EventType.OnLLMResponseEvent) - for handler in handlers: - try: - await handler.handler(event, llm_response) - except BaseException: - logger.error(traceback.format_exc()) - - if llm_response.role == 'assistant': - # text completion - event.set_result(MessageEventResult().message(llm_response.completion_text) - .set_result_content_type(ResultContentType.LLM_RESULT)) - return - elif llm_response.role == 'err': - event.set_result(MessageEventResult().message(f"AstrBot 请求失败。\n错误信息: {llm_response.completion_text}")) - return - elif llm_response.role == 'tool': - event.set_result(MessageEventResult().message(f"Dify 暂不支持工具调用。")) - yield - - except BaseException as e: - logger.error(traceback.format_exc()) - event.set_result(MessageEventResult().message("AstrBot 请求 Dify 失败:" + str(e))) - return \ No newline at end of file diff --git a/astrbot/core/pipeline/process_stage/method/llm_request.py b/astrbot/core/pipeline/process_stage/method/llm_request.py index fd50d4423..8d88e13da 100644 --- a/astrbot/core/pipeline/process_stage/method/llm_request.py +++ b/astrbot/core/pipeline/process_stage/method/llm_request.py @@ -54,7 +54,7 @@ class LLMRequestSubStage(Stage): conversation_id = await self.conv_manager.get_curr_conversation_id(event.unified_msg_origin) if not conversation_id: conversation_id = await self.conv_manager.new_conversation(event.unified_msg_origin) - req.session_id = conversation_id + req.session_id = event.unified_msg_origin conversation = await self.conv_manager.get_conversation(event.unified_msg_origin, conversation_id) req.conversation = conversation req.contexts = json.loads(conversation.history) @@ -154,6 +154,6 @@ class LLMRequestSubStage(Stage): contexts_to_save = list(filter(lambda item: '_no_save' not in item, contexts)) await self.conv_manager.update_conversation( event.unified_msg_origin, - req.session_id, + req.conversation.cid, history=contexts_to_save ) \ No newline at end of file diff --git a/astrbot/core/pipeline/process_stage/stage.py b/astrbot/core/pipeline/process_stage/stage.py index 0026e9d6f..f93be8108 100644 --- a/astrbot/core/pipeline/process_stage/stage.py +++ b/astrbot/core/pipeline/process_stage/stage.py @@ -3,7 +3,6 @@ from ..stage import Stage, register_stage from ..context import PipelineContext from .method.llm_request import LLMRequestSubStage from .method.star_request import StarRequestSubStage -from .method.dify_request import DifyRequestSubStage from astrbot.core.platform.astr_message_event import AstrMessageEvent from astrbot.core.star.star_handler import StarHandlerMetadata from astrbot.core.provider.entites import ProviderRequest @@ -21,9 +20,6 @@ class ProcessStage(Stage): self.star_request_sub_stage = StarRequestSubStage() await self.star_request_sub_stage.initialize(ctx) - - self.dify_request_sub_stage = DifyRequestSubStage() - await self.dify_request_sub_stage.initialize(ctx) async def process(self, event: AstrMessageEvent) -> Union[None, AsyncGenerator[None, None]]: '''处理事件 @@ -59,10 +55,5 @@ class ProcessStage(Stage): logger.info("未找到可用的 LLM 提供商,请先前往配置服务提供商。") return - match provider.meta().type: - case "dify": - async for _ in self.dify_request_sub_stage.process(event): - yield - case _: - async for _ in self.llm_request_sub_stage.process(event): - yield \ No newline at end of file + async for _ in self.llm_request_sub_stage.process(event): + yield \ No newline at end of file diff --git a/astrbot/core/provider/sources/dashscope_source.py b/astrbot/core/provider/sources/dashscope_source.py index 56ab79934..a120efbd3 100644 --- a/astrbot/core/provider/sources/dashscope_source.py +++ b/astrbot/core/provider/sources/dashscope_source.py @@ -1,4 +1,5 @@ import asyncio +import functools from typing import List from .. import Provider, Personality from ..entites import LLMResponse @@ -47,6 +48,11 @@ class ProviderDashscope(ProviderOpenAIOfficial): system_prompt: str = None, **kwargs, ) -> LLMResponse: + + # 获得会话变量 + session_vars = sp.get("session_variables", {}) + session_var = session_vars.get(session_id, {}) + if self.dashscope_app_type in ["agent", "dialog-workflow"]: # 支持多轮对话的 new_record = {"role": "user", "content": prompt} @@ -60,27 +66,30 @@ class ProviderDashscope(ProviderOpenAIOfficial): if '_no_save' in part: del part['_no_save'] # 调用阿里云百炼 API + partial = functools.partial( + Application.call, + app_id = self.app_id, + api_key = self.api_key, + messages = context_query, + biz_params = session_var or None, + ) response = await asyncio.get_event_loop().run_in_executor( None, - Application.call, - self.app_id, - None, - None, - None, - self.api_key, - context_query + partial ) else: # 不支持多轮对话的 # 调用阿里云百炼 API + partial = functools.partial( + Application.call, + app_id = self.app_id, + promtp = prompt, + api_key = self.api_key, + biz_params=session_var or None, + ) response = await asyncio.get_event_loop().run_in_executor( None, - Application.call, - self.app_id, - prompt, - None, - None, - self.api_key + partial ) logger.debug(f"dashscope resp: {response}") diff --git a/packages/astrbot/main.py b/packages/astrbot/main.py index 70500c60f..abe5c16b6 100644 --- a/packages/astrbot/main.py +++ b/packages/astrbot/main.py @@ -83,9 +83,6 @@ AstrBot 指令: /tool ls: 函数工具 /key: API Key(op) /websearch: 网页搜索 - -[其他] -/set 变量名 值: 为会话定义变量(Dify 工作流输入) {notice}""" event.set_result(MessageEventResult().message(msg).use_t2i(False)) From 8beb7acdb135515219488a63d4f412399832d843 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Sat, 22 Feb 2025 14:48:18 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E2=9C=A8=20feat:=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E4=B8=BA=20dify=20=E5=92=8C=20dashscope=20=E6=8F=90=E4=BE=9B?= =?UTF-8?q?=E5=95=86=E8=AE=BE=E7=BD=AE=E9=BB=98=E8=AE=A4=E5=9B=BA=E5=AE=9A?= =?UTF-8?q?=E5=8F=98=E9=87=8F=20#552?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/config/default.py | 8 +++ .../core/provider/sources/dashscope_source.py | 72 ++++++++++--------- astrbot/core/provider/sources/dify_source.py | 8 ++- 3 files changed, 54 insertions(+), 34 deletions(-) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 68c3146b5..880e7b404 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -527,6 +527,7 @@ CONFIG_METADATA_2 = { "dify_api_base": "https://api.dify.ai/v1", "dify_workflow_output_key": "", "dify_query_input_key": "astrbot_text_query", + "variables": {}, "timeout": 60, }, "dashscope": { @@ -536,6 +537,7 @@ CONFIG_METADATA_2 = { "dashscope_app_type": "agent", "dashscope_api_key": "", "dashscope_app_id": "", + "variables": {}, "timeout": 60, }, "whisper(API)": { @@ -574,6 +576,12 @@ CONFIG_METADATA_2 = { }, }, "items": { + "variables": { + "description": "工作流固定输入变量", + "type": "object", + "obvious_hint": True, + "hint": "可选。工作流固定输入变量,将会作为工作流的输入。也可以在对话时使用 /set 指令动态设置变量。如果变量名冲突,优先使用动态设置的变量。", + }, "dashscope_app_type": { "description": "应用类型", "type": "string", diff --git a/astrbot/core/provider/sources/dashscope_source.py b/astrbot/core/provider/sources/dashscope_source.py index a120efbd3..eb1db3607 100644 --- a/astrbot/core/provider/sources/dashscope_source.py +++ b/astrbot/core/provider/sources/dashscope_source.py @@ -10,6 +10,7 @@ from .openai_source import ProviderOpenAIOfficial from astrbot.core import logger, sp from dashscope import Application + @register_provider_adapter("dashscope", "Dashscope APP 适配器。") class ProviderDashscope(ProviderOpenAIOfficial): def __init__( @@ -18,10 +19,15 @@ class ProviderDashscope(ProviderOpenAIOfficial): provider_settings: dict, db_helper: BaseDatabase, persistant_history=False, - default_persona: Personality=None + default_persona: Personality = None, ) -> None: Provider.__init__( - self, provider_config, provider_settings, persistant_history, db_helper, default_persona + self, + provider_config, + provider_settings, + persistant_history, + db_helper, + default_persona, ) self.api_key = provider_config.get("dashscope_api_key", "") if not self.api_key: @@ -33,7 +39,8 @@ class ProviderDashscope(ProviderOpenAIOfficial): if not self.dashscope_app_type: raise Exception("阿里云百炼 APP 类型不能为空。") self.model_name = "dashscope" - + self.variables: dict = provider_config.get("variables", {}) + self.timeout = provider_config.get("timeout", 120) if isinstance(self.timeout, str): self.timeout = int(self.timeout) @@ -48,13 +55,15 @@ class ProviderDashscope(ProviderOpenAIOfficial): system_prompt: str = None, **kwargs, ) -> LLMResponse: - # 获得会话变量 + payload_vars = self.variables.copy() + # 动态变量 session_vars = sp.get("session_variables", {}) session_var = session_vars.get(session_id, {}) - + payload_vars.update(session_var) + if self.dashscope_app_type in ["agent", "dialog-workflow"]: - # 支持多轮对话的 + # 支持多轮对话的 new_record = {"role": "user", "content": prompt} if image_urls: logger.warning("阿里云百炼暂不支持图片输入,将自动忽略图片内容。") @@ -63,43 +72,42 @@ class ProviderDashscope(ProviderOpenAIOfficial): if system_prompt: context_query.insert(0, {"role": "system", "content": system_prompt}) for part in context_query: - if '_no_save' in part: - del part['_no_save'] + if "_no_save" in part: + del part["_no_save"] # 调用阿里云百炼 API partial = functools.partial( - Application.call, - app_id = self.app_id, - api_key = self.api_key, - messages = context_query, - biz_params = session_var or None, - ) - response = await asyncio.get_event_loop().run_in_executor( - None, - partial + Application.call, + app_id=self.app_id, + api_key=self.api_key, + messages=context_query, + biz_params=payload_vars or None, ) + response = await asyncio.get_event_loop().run_in_executor(None, partial) else: # 不支持多轮对话的 # 调用阿里云百炼 API partial = functools.partial( - Application.call, - app_id = self.app_id, - promtp = prompt, - api_key = self.api_key, - biz_params=session_var or None, + Application.call, + app_id=self.app_id, + promtp=prompt, + api_key=self.api_key, + biz_params=payload_vars or None, ) - response = await asyncio.get_event_loop().run_in_executor( - None, - partial - ) - + response = await asyncio.get_event_loop().run_in_executor(None, partial) + logger.debug(f"dashscope resp: {response}") if response.status_code != 200: - logger.error(f"阿里云百炼请求失败: request_id={response.request_id}, code={response.status_code}, message={response.message}, 请参考文档:https://help.aliyun.com/zh/model-studio/developer-reference/error-code") - return LLMResponse(role="err", completion_text=f"阿里云百炼请求失败: message={response.message} code={response.status_code}") - + logger.error( + f"阿里云百炼请求失败: request_id={response.request_id}, code={response.status_code}, message={response.message}, 请参考文档:https://help.aliyun.com/zh/model-studio/developer-reference/error-code" + ) + return LLMResponse( + role="err", + completion_text=f"阿里云百炼请求失败: message={response.message} code={response.status_code}", + ) + output_text = response.output.get("text", "") - return LLMResponse(role="assistant", completion_text=output_text) + return LLMResponse(role="assistant", completion_text=output_text) async def forget(self, session_id): return True @@ -117,4 +125,4 @@ class ProviderDashscope(ProviderOpenAIOfficial): raise Exception("暂不支持获得 阿里云百炼 的历史消息记录。") async def terminate(self): - await self.api_client.close() \ No newline at end of file + await self.api_client.close() diff --git a/astrbot/core/provider/sources/dify_source.py b/astrbot/core/provider/sources/dify_source.py index 807520e0e..1127c7561 100644 --- a/astrbot/core/provider/sources/dify_source.py +++ b/astrbot/core/provider/sources/dify_source.py @@ -32,6 +32,7 @@ class ProviderDify(Provider): self.model_name = "dify" self.workflow_output_key = provider_config.get("dify_workflow_output_key", "astrbot_wf_output") self.dify_query_input_key = provider_config.get("dify_query_input_key", "astrbot_text_query") + self.variables: dict = provider_config.get("variables", {}) if not self.dify_query_input_key: self.dify_query_input_key = "astrbot_text_query" self.timeout = provider_config.get("timeout", 120) @@ -72,15 +73,18 @@ class ProviderDify(Provider): logger.warning(f"未知的图片链接:{image_url},图片将忽略。") # 获得会话变量 + payload_vars = self.variables.copy() + # 动态变量 session_vars = sp.get("session_variables", {}) session_var = session_vars.get(session_id, {}) + payload_vars.update(session_var) try: match self.api_type: case "chat" | "agent": async for chunk in self.api_client.chat_messages( inputs={ - **session_var + **payload_vars, }, query=prompt, user=session_id, @@ -101,7 +105,7 @@ class ProviderDify(Provider): inputs={ self.dify_query_input_key: prompt, "astrbot_session_id": session_id, - **session_var + **payload_vars, }, user=session_id, files=files_payload,