diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index d680cf6c8..880e7b404 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -527,6 +527,17 @@ 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": { + "id": "dashscope", + "type": "dashscope", + "enable": True, + "dashscope_app_type": "agent", + "dashscope_api_key": "", + "dashscope_app_id": "", + "variables": {}, "timeout": 60, }, "whisper(API)": { @@ -565,6 +576,19 @@ CONFIG_METADATA_2 = { }, }, "items": { + "variables": { + "description": "工作流固定输入变量", + "type": "object", + "obvious_hint": True, + "hint": "可选。工作流固定输入变量,将会作为工作流的输入。也可以在对话时使用 /set 指令动态设置变量。如果变量名冲突,优先使用动态设置的变量。", + }, + "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/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/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..eb1db3607 --- /dev/null +++ b/astrbot/core/provider/sources/dashscope_source.py @@ -0,0 +1,128 @@ +import asyncio +import functools +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.variables: dict = provider_config.get("variables", {}) + + 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: + # 获得会话变量 + 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("阿里云百炼暂不支持图片输入,将自动忽略图片内容。") + 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 + partial = functools.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=payload_vars or None, + ) + 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}", + ) + + 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() 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, 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)) 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