AstrBot 3.5.6__py3-none-any.whl → 4.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- astrbot/api/__init__.py +16 -4
- astrbot/api/all.py +2 -1
- astrbot/api/event/__init__.py +5 -6
- astrbot/api/event/filter/__init__.py +37 -34
- astrbot/api/platform/__init__.py +7 -8
- astrbot/api/provider/__init__.py +8 -7
- astrbot/api/star/__init__.py +3 -4
- astrbot/api/util/__init__.py +2 -2
- astrbot/cli/__init__.py +1 -0
- astrbot/cli/__main__.py +18 -197
- astrbot/cli/commands/__init__.py +6 -0
- astrbot/cli/commands/cmd_conf.py +209 -0
- astrbot/cli/commands/cmd_init.py +56 -0
- astrbot/cli/commands/cmd_plug.py +245 -0
- astrbot/cli/commands/cmd_run.py +62 -0
- astrbot/cli/utils/__init__.py +18 -0
- astrbot/cli/utils/basic.py +76 -0
- astrbot/cli/utils/plugin.py +246 -0
- astrbot/cli/utils/version_comparator.py +90 -0
- astrbot/core/__init__.py +17 -19
- astrbot/core/agent/agent.py +14 -0
- astrbot/core/agent/handoff.py +38 -0
- astrbot/core/agent/hooks.py +30 -0
- astrbot/core/agent/mcp_client.py +385 -0
- astrbot/core/agent/message.py +175 -0
- astrbot/core/agent/response.py +14 -0
- astrbot/core/agent/run_context.py +22 -0
- astrbot/core/agent/runners/__init__.py +3 -0
- astrbot/core/agent/runners/base.py +65 -0
- astrbot/core/agent/runners/coze/coze_agent_runner.py +367 -0
- astrbot/core/agent/runners/coze/coze_api_client.py +324 -0
- astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py +403 -0
- astrbot/core/agent/runners/dify/dify_agent_runner.py +336 -0
- astrbot/core/agent/runners/dify/dify_api_client.py +195 -0
- astrbot/core/agent/runners/tool_loop_agent_runner.py +400 -0
- astrbot/core/agent/tool.py +285 -0
- astrbot/core/agent/tool_executor.py +17 -0
- astrbot/core/astr_agent_context.py +19 -0
- astrbot/core/astr_agent_hooks.py +36 -0
- astrbot/core/astr_agent_run_util.py +80 -0
- astrbot/core/astr_agent_tool_exec.py +246 -0
- astrbot/core/astrbot_config_mgr.py +275 -0
- astrbot/core/config/__init__.py +2 -2
- astrbot/core/config/astrbot_config.py +60 -20
- astrbot/core/config/default.py +1972 -453
- astrbot/core/config/i18n_utils.py +110 -0
- astrbot/core/conversation_mgr.py +285 -75
- astrbot/core/core_lifecycle.py +167 -62
- astrbot/core/db/__init__.py +305 -102
- astrbot/core/db/migration/helper.py +69 -0
- astrbot/core/db/migration/migra_3_to_4.py +357 -0
- astrbot/core/db/migration/migra_45_to_46.py +44 -0
- astrbot/core/db/migration/migra_webchat_session.py +131 -0
- astrbot/core/db/migration/shared_preferences_v3.py +48 -0
- astrbot/core/db/migration/sqlite_v3.py +497 -0
- astrbot/core/db/po.py +259 -55
- astrbot/core/db/sqlite.py +773 -528
- astrbot/core/db/vec_db/base.py +73 -0
- astrbot/core/db/vec_db/faiss_impl/__init__.py +3 -0
- astrbot/core/db/vec_db/faiss_impl/document_storage.py +392 -0
- astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +93 -0
- astrbot/core/db/vec_db/faiss_impl/sqlite_init.sql +17 -0
- astrbot/core/db/vec_db/faiss_impl/vec_db.py +204 -0
- astrbot/core/event_bus.py +26 -22
- astrbot/core/exceptions.py +9 -0
- astrbot/core/file_token_service.py +98 -0
- astrbot/core/initial_loader.py +19 -10
- astrbot/core/knowledge_base/chunking/__init__.py +9 -0
- astrbot/core/knowledge_base/chunking/base.py +25 -0
- astrbot/core/knowledge_base/chunking/fixed_size.py +59 -0
- astrbot/core/knowledge_base/chunking/recursive.py +161 -0
- astrbot/core/knowledge_base/kb_db_sqlite.py +301 -0
- astrbot/core/knowledge_base/kb_helper.py +642 -0
- astrbot/core/knowledge_base/kb_mgr.py +330 -0
- astrbot/core/knowledge_base/models.py +120 -0
- astrbot/core/knowledge_base/parsers/__init__.py +13 -0
- astrbot/core/knowledge_base/parsers/base.py +51 -0
- astrbot/core/knowledge_base/parsers/markitdown_parser.py +26 -0
- astrbot/core/knowledge_base/parsers/pdf_parser.py +101 -0
- astrbot/core/knowledge_base/parsers/text_parser.py +42 -0
- astrbot/core/knowledge_base/parsers/url_parser.py +103 -0
- astrbot/core/knowledge_base/parsers/util.py +13 -0
- astrbot/core/knowledge_base/prompts.py +65 -0
- astrbot/core/knowledge_base/retrieval/__init__.py +14 -0
- astrbot/core/knowledge_base/retrieval/hit_stopwords.txt +767 -0
- astrbot/core/knowledge_base/retrieval/manager.py +276 -0
- astrbot/core/knowledge_base/retrieval/rank_fusion.py +142 -0
- astrbot/core/knowledge_base/retrieval/sparse_retriever.py +136 -0
- astrbot/core/log.py +21 -15
- astrbot/core/message/components.py +413 -287
- astrbot/core/message/message_event_result.py +35 -24
- astrbot/core/persona_mgr.py +192 -0
- astrbot/core/pipeline/__init__.py +14 -14
- astrbot/core/pipeline/content_safety_check/stage.py +13 -9
- astrbot/core/pipeline/content_safety_check/strategies/__init__.py +1 -2
- astrbot/core/pipeline/content_safety_check/strategies/baidu_aip.py +13 -14
- astrbot/core/pipeline/content_safety_check/strategies/keywords.py +2 -1
- astrbot/core/pipeline/content_safety_check/strategies/strategy.py +6 -6
- astrbot/core/pipeline/context.py +7 -1
- astrbot/core/pipeline/context_utils.py +107 -0
- astrbot/core/pipeline/preprocess_stage/stage.py +63 -36
- astrbot/core/pipeline/process_stage/method/agent_request.py +48 -0
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +464 -0
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +202 -0
- astrbot/core/pipeline/process_stage/method/star_request.py +26 -32
- astrbot/core/pipeline/process_stage/stage.py +21 -15
- astrbot/core/pipeline/process_stage/utils.py +125 -0
- astrbot/core/pipeline/rate_limit_check/stage.py +34 -36
- astrbot/core/pipeline/respond/stage.py +142 -101
- astrbot/core/pipeline/result_decorate/stage.py +124 -57
- astrbot/core/pipeline/scheduler.py +21 -16
- astrbot/core/pipeline/session_status_check/stage.py +37 -0
- astrbot/core/pipeline/stage.py +11 -76
- astrbot/core/pipeline/waking_check/stage.py +69 -33
- astrbot/core/pipeline/whitelist_check/stage.py +10 -7
- astrbot/core/platform/__init__.py +6 -6
- astrbot/core/platform/astr_message_event.py +107 -129
- astrbot/core/platform/astrbot_message.py +32 -12
- astrbot/core/platform/manager.py +62 -18
- astrbot/core/platform/message_session.py +30 -0
- astrbot/core/platform/platform.py +16 -24
- astrbot/core/platform/platform_metadata.py +9 -4
- astrbot/core/platform/register.py +12 -7
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +136 -60
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +126 -46
- astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +63 -31
- astrbot/core/platform/sources/dingtalk/dingtalk_event.py +30 -26
- astrbot/core/platform/sources/discord/client.py +129 -0
- astrbot/core/platform/sources/discord/components.py +139 -0
- astrbot/core/platform/sources/discord/discord_platform_adapter.py +473 -0
- astrbot/core/platform/sources/discord/discord_platform_event.py +313 -0
- astrbot/core/platform/sources/lark/lark_adapter.py +27 -18
- astrbot/core/platform/sources/lark/lark_event.py +39 -13
- astrbot/core/platform/sources/misskey/misskey_adapter.py +770 -0
- astrbot/core/platform/sources/misskey/misskey_api.py +964 -0
- astrbot/core/platform/sources/misskey/misskey_event.py +163 -0
- astrbot/core/platform/sources/misskey/misskey_utils.py +550 -0
- astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +149 -33
- astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +41 -26
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +36 -17
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_event.py +3 -1
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +14 -8
- astrbot/core/platform/sources/satori/satori_adapter.py +792 -0
- astrbot/core/platform/sources/satori/satori_event.py +432 -0
- astrbot/core/platform/sources/slack/client.py +164 -0
- astrbot/core/platform/sources/slack/slack_adapter.py +416 -0
- astrbot/core/platform/sources/slack/slack_event.py +253 -0
- astrbot/core/platform/sources/telegram/tg_adapter.py +100 -43
- astrbot/core/platform/sources/telegram/tg_event.py +136 -36
- astrbot/core/platform/sources/webchat/webchat_adapter.py +72 -22
- astrbot/core/platform/sources/webchat/webchat_event.py +46 -22
- astrbot/core/platform/sources/webchat/webchat_queue_mgr.py +35 -0
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +926 -0
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py +178 -0
- astrbot/core/platform/sources/wechatpadpro/xml_data_parser.py +159 -0
- astrbot/core/platform/sources/wecom/wecom_adapter.py +169 -27
- astrbot/core/platform/sources/wecom/wecom_event.py +162 -77
- astrbot/core/platform/sources/wecom/wecom_kf.py +279 -0
- astrbot/core/platform/sources/wecom/wecom_kf_message.py +196 -0
- astrbot/core/platform/sources/wecom_ai_bot/WXBizJsonMsgCrypt.py +297 -0
- astrbot/core/platform/sources/wecom_ai_bot/__init__.py +15 -0
- astrbot/core/platform/sources/wecom_ai_bot/ierror.py +19 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +472 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_api.py +417 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +152 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_queue_mgr.py +153 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +168 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_utils.py +209 -0
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +306 -0
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +186 -0
- astrbot/core/platform_message_history_mgr.py +49 -0
- astrbot/core/provider/__init__.py +2 -3
- astrbot/core/provider/entites.py +8 -8
- astrbot/core/provider/entities.py +154 -98
- astrbot/core/provider/func_tool_manager.py +446 -458
- astrbot/core/provider/manager.py +345 -207
- astrbot/core/provider/provider.py +188 -73
- astrbot/core/provider/register.py +9 -7
- astrbot/core/provider/sources/anthropic_source.py +295 -115
- astrbot/core/provider/sources/azure_tts_source.py +224 -0
- astrbot/core/provider/sources/bailian_rerank_source.py +236 -0
- astrbot/core/provider/sources/dashscope_tts.py +138 -14
- astrbot/core/provider/sources/edge_tts_source.py +24 -19
- astrbot/core/provider/sources/fishaudio_tts_api_source.py +58 -13
- astrbot/core/provider/sources/gemini_embedding_source.py +61 -0
- astrbot/core/provider/sources/gemini_source.py +310 -132
- astrbot/core/provider/sources/gemini_tts_source.py +81 -0
- astrbot/core/provider/sources/groq_source.py +15 -0
- astrbot/core/provider/sources/gsv_selfhosted_source.py +151 -0
- astrbot/core/provider/sources/gsvi_tts_source.py +14 -7
- astrbot/core/provider/sources/minimax_tts_api_source.py +159 -0
- astrbot/core/provider/sources/openai_embedding_source.py +40 -0
- astrbot/core/provider/sources/openai_source.py +241 -145
- astrbot/core/provider/sources/openai_tts_api_source.py +18 -7
- astrbot/core/provider/sources/sensevoice_selfhosted_source.py +13 -11
- astrbot/core/provider/sources/vllm_rerank_source.py +71 -0
- astrbot/core/provider/sources/volcengine_tts.py +115 -0
- astrbot/core/provider/sources/whisper_api_source.py +18 -13
- astrbot/core/provider/sources/whisper_selfhosted_source.py +19 -12
- astrbot/core/provider/sources/xinference_rerank_source.py +116 -0
- astrbot/core/provider/sources/xinference_stt_provider.py +197 -0
- astrbot/core/provider/sources/zhipu_source.py +6 -73
- astrbot/core/star/__init__.py +43 -11
- astrbot/core/star/config.py +17 -18
- astrbot/core/star/context.py +362 -138
- astrbot/core/star/filter/__init__.py +4 -3
- astrbot/core/star/filter/command.py +111 -35
- astrbot/core/star/filter/command_group.py +46 -34
- astrbot/core/star/filter/custom_filter.py +6 -5
- astrbot/core/star/filter/event_message_type.py +4 -2
- astrbot/core/star/filter/permission.py +4 -2
- astrbot/core/star/filter/platform_adapter_type.py +45 -12
- astrbot/core/star/filter/regex.py +4 -2
- astrbot/core/star/register/__init__.py +19 -15
- astrbot/core/star/register/star.py +41 -13
- astrbot/core/star/register/star_handler.py +236 -86
- astrbot/core/star/session_llm_manager.py +280 -0
- astrbot/core/star/session_plugin_manager.py +170 -0
- astrbot/core/star/star.py +36 -43
- astrbot/core/star/star_handler.py +47 -85
- astrbot/core/star/star_manager.py +442 -260
- astrbot/core/star/star_tools.py +167 -45
- astrbot/core/star/updator.py +17 -20
- astrbot/core/umop_config_router.py +106 -0
- astrbot/core/updator.py +38 -13
- astrbot/core/utils/astrbot_path.py +39 -0
- astrbot/core/utils/command_parser.py +1 -1
- astrbot/core/utils/io.py +119 -60
- astrbot/core/utils/log_pipe.py +1 -1
- astrbot/core/utils/metrics.py +11 -10
- astrbot/core/utils/migra_helper.py +73 -0
- astrbot/core/utils/path_util.py +63 -62
- astrbot/core/utils/pip_installer.py +37 -15
- astrbot/core/utils/session_lock.py +29 -0
- astrbot/core/utils/session_waiter.py +19 -20
- astrbot/core/utils/shared_preferences.py +174 -34
- astrbot/core/utils/t2i/__init__.py +4 -1
- astrbot/core/utils/t2i/local_strategy.py +386 -238
- astrbot/core/utils/t2i/network_strategy.py +109 -49
- astrbot/core/utils/t2i/renderer.py +29 -14
- astrbot/core/utils/t2i/template/astrbot_powershell.html +184 -0
- astrbot/core/utils/t2i/template_manager.py +111 -0
- astrbot/core/utils/tencent_record_helper.py +115 -1
- astrbot/core/utils/version_comparator.py +10 -13
- astrbot/core/zip_updator.py +112 -65
- astrbot/dashboard/routes/__init__.py +20 -13
- astrbot/dashboard/routes/auth.py +20 -9
- astrbot/dashboard/routes/chat.py +297 -141
- astrbot/dashboard/routes/config.py +652 -55
- astrbot/dashboard/routes/conversation.py +107 -37
- astrbot/dashboard/routes/file.py +26 -0
- astrbot/dashboard/routes/knowledge_base.py +1244 -0
- astrbot/dashboard/routes/log.py +27 -2
- astrbot/dashboard/routes/persona.py +202 -0
- astrbot/dashboard/routes/plugin.py +197 -139
- astrbot/dashboard/routes/route.py +27 -7
- astrbot/dashboard/routes/session_management.py +354 -0
- astrbot/dashboard/routes/stat.py +85 -18
- astrbot/dashboard/routes/static_file.py +5 -2
- astrbot/dashboard/routes/t2i.py +233 -0
- astrbot/dashboard/routes/tools.py +184 -120
- astrbot/dashboard/routes/update.py +59 -36
- astrbot/dashboard/server.py +96 -36
- astrbot/dashboard/utils.py +165 -0
- astrbot-4.7.0.dist-info/METADATA +294 -0
- astrbot-4.7.0.dist-info/RECORD +274 -0
- {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/WHEEL +1 -1
- astrbot/core/db/plugin/sqlite_impl.py +0 -112
- astrbot/core/db/sqlite_init.sql +0 -50
- astrbot/core/pipeline/platform_compatibility/stage.py +0 -56
- astrbot/core/pipeline/process_stage/method/llm_request.py +0 -606
- astrbot/core/platform/sources/gewechat/client.py +0 -806
- astrbot/core/platform/sources/gewechat/downloader.py +0 -55
- astrbot/core/platform/sources/gewechat/gewechat_event.py +0 -255
- astrbot/core/platform/sources/gewechat/gewechat_platform_adapter.py +0 -103
- astrbot/core/platform/sources/gewechat/xml_data_parser.py +0 -110
- astrbot/core/provider/sources/dashscope_source.py +0 -203
- astrbot/core/provider/sources/dify_source.py +0 -281
- astrbot/core/provider/sources/llmtuner_source.py +0 -132
- astrbot/core/rag/embedding/openai_source.py +0 -20
- astrbot/core/rag/knowledge_db_mgr.py +0 -94
- astrbot/core/rag/store/__init__.py +0 -9
- astrbot/core/rag/store/chroma_db.py +0 -42
- astrbot/core/utils/dify_api_client.py +0 -152
- astrbot-3.5.6.dist-info/METADATA +0 -249
- astrbot-3.5.6.dist-info/RECORD +0 -158
- {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/entry_points.txt +0 -0
- {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import typing as T
|
|
5
|
+
|
|
6
|
+
import astrbot.core.message.components as Comp
|
|
7
|
+
from astrbot.core import logger, sp
|
|
8
|
+
from astrbot.core.message.message_event_result import MessageChain
|
|
9
|
+
from astrbot.core.provider.entities import (
|
|
10
|
+
LLMResponse,
|
|
11
|
+
ProviderRequest,
|
|
12
|
+
)
|
|
13
|
+
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
|
14
|
+
from astrbot.core.utils.io import download_file
|
|
15
|
+
|
|
16
|
+
from ...hooks import BaseAgentRunHooks
|
|
17
|
+
from ...response import AgentResponseData
|
|
18
|
+
from ...run_context import ContextWrapper, TContext
|
|
19
|
+
from ..base import AgentResponse, AgentState, BaseAgentRunner
|
|
20
|
+
from .dify_api_client import DifyAPIClient
|
|
21
|
+
|
|
22
|
+
if sys.version_info >= (3, 12):
|
|
23
|
+
from typing import override
|
|
24
|
+
else:
|
|
25
|
+
from typing_extensions import override
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DifyAgentRunner(BaseAgentRunner[TContext]):
|
|
29
|
+
"""Dify Agent Runner"""
|
|
30
|
+
|
|
31
|
+
@override
|
|
32
|
+
async def reset(
|
|
33
|
+
self,
|
|
34
|
+
request: ProviderRequest,
|
|
35
|
+
run_context: ContextWrapper[TContext],
|
|
36
|
+
agent_hooks: BaseAgentRunHooks[TContext],
|
|
37
|
+
provider_config: dict,
|
|
38
|
+
**kwargs: T.Any,
|
|
39
|
+
) -> None:
|
|
40
|
+
self.req = request
|
|
41
|
+
self.streaming = kwargs.get("streaming", False)
|
|
42
|
+
self.final_llm_resp = None
|
|
43
|
+
self._state = AgentState.IDLE
|
|
44
|
+
self.agent_hooks = agent_hooks
|
|
45
|
+
self.run_context = run_context
|
|
46
|
+
|
|
47
|
+
self.api_key = provider_config.get("dify_api_key", "")
|
|
48
|
+
self.api_base = provider_config.get("dify_api_base", "https://api.dify.ai/v1")
|
|
49
|
+
self.api_type = provider_config.get("dify_api_type", "chat")
|
|
50
|
+
self.workflow_output_key = provider_config.get(
|
|
51
|
+
"dify_workflow_output_key",
|
|
52
|
+
"astrbot_wf_output",
|
|
53
|
+
)
|
|
54
|
+
self.dify_query_input_key = provider_config.get(
|
|
55
|
+
"dify_query_input_key",
|
|
56
|
+
"astrbot_text_query",
|
|
57
|
+
)
|
|
58
|
+
self.variables: dict = provider_config.get("variables", {}) or {}
|
|
59
|
+
self.timeout = provider_config.get("timeout", 60)
|
|
60
|
+
if isinstance(self.timeout, str):
|
|
61
|
+
self.timeout = int(self.timeout)
|
|
62
|
+
|
|
63
|
+
self.api_client = DifyAPIClient(self.api_key, self.api_base)
|
|
64
|
+
|
|
65
|
+
@override
|
|
66
|
+
async def step(self):
|
|
67
|
+
"""
|
|
68
|
+
执行 Dify Agent 的一个步骤
|
|
69
|
+
"""
|
|
70
|
+
if not self.req:
|
|
71
|
+
raise ValueError("Request is not set. Please call reset() first.")
|
|
72
|
+
|
|
73
|
+
if self._state == AgentState.IDLE:
|
|
74
|
+
try:
|
|
75
|
+
await self.agent_hooks.on_agent_begin(self.run_context)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
logger.error(f"Error in on_agent_begin hook: {e}", exc_info=True)
|
|
78
|
+
|
|
79
|
+
# 开始处理,转换到运行状态
|
|
80
|
+
self._transition_state(AgentState.RUNNING)
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
# 执行 Dify 请求并处理结果
|
|
84
|
+
async for response in self._execute_dify_request():
|
|
85
|
+
yield response
|
|
86
|
+
except Exception as e:
|
|
87
|
+
logger.error(f"Dify 请求失败:{str(e)}")
|
|
88
|
+
self._transition_state(AgentState.ERROR)
|
|
89
|
+
self.final_llm_resp = LLMResponse(
|
|
90
|
+
role="err", completion_text=f"Dify 请求失败:{str(e)}"
|
|
91
|
+
)
|
|
92
|
+
yield AgentResponse(
|
|
93
|
+
type="err",
|
|
94
|
+
data=AgentResponseData(
|
|
95
|
+
chain=MessageChain().message(f"Dify 请求失败:{str(e)}")
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
finally:
|
|
99
|
+
await self.api_client.close()
|
|
100
|
+
|
|
101
|
+
@override
|
|
102
|
+
async def step_until_done(
|
|
103
|
+
self, max_step: int = 30
|
|
104
|
+
) -> T.AsyncGenerator[AgentResponse, None]:
|
|
105
|
+
while not self.done():
|
|
106
|
+
async for resp in self.step():
|
|
107
|
+
yield resp
|
|
108
|
+
|
|
109
|
+
async def _execute_dify_request(self):
|
|
110
|
+
"""执行 Dify 请求的核心逻辑"""
|
|
111
|
+
prompt = self.req.prompt or ""
|
|
112
|
+
session_id = self.req.session_id or "unknown"
|
|
113
|
+
image_urls = self.req.image_urls or []
|
|
114
|
+
system_prompt = self.req.system_prompt
|
|
115
|
+
|
|
116
|
+
conversation_id = await sp.get_async(
|
|
117
|
+
scope="umo",
|
|
118
|
+
scope_id=session_id,
|
|
119
|
+
key="dify_conversation_id",
|
|
120
|
+
default="",
|
|
121
|
+
)
|
|
122
|
+
result = ""
|
|
123
|
+
|
|
124
|
+
# 处理图片上传
|
|
125
|
+
files_payload = []
|
|
126
|
+
for image_url in image_urls:
|
|
127
|
+
# image_url is a base64 string
|
|
128
|
+
try:
|
|
129
|
+
image_data = base64.b64decode(image_url)
|
|
130
|
+
file_response = await self.api_client.file_upload(
|
|
131
|
+
file_data=image_data,
|
|
132
|
+
user=session_id,
|
|
133
|
+
mime_type="image/png",
|
|
134
|
+
file_name="image.png",
|
|
135
|
+
)
|
|
136
|
+
logger.debug(f"Dify 上传图片响应:{file_response}")
|
|
137
|
+
if "id" not in file_response:
|
|
138
|
+
logger.warning(
|
|
139
|
+
f"上传图片后得到未知的 Dify 响应:{file_response},图片将忽略。"
|
|
140
|
+
)
|
|
141
|
+
continue
|
|
142
|
+
files_payload.append(
|
|
143
|
+
{
|
|
144
|
+
"type": "image",
|
|
145
|
+
"transfer_method": "local_file",
|
|
146
|
+
"upload_file_id": file_response["id"],
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
except Exception as e:
|
|
150
|
+
logger.warning(f"上传图片失败:{e}")
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
# 获得会话变量
|
|
154
|
+
payload_vars = self.variables.copy()
|
|
155
|
+
# 动态变量
|
|
156
|
+
session_var = await sp.get_async(
|
|
157
|
+
scope="umo",
|
|
158
|
+
scope_id=session_id,
|
|
159
|
+
key="session_variables",
|
|
160
|
+
default={},
|
|
161
|
+
)
|
|
162
|
+
payload_vars.update(session_var)
|
|
163
|
+
payload_vars["system_prompt"] = system_prompt
|
|
164
|
+
|
|
165
|
+
# 处理不同的 API 类型
|
|
166
|
+
match self.api_type:
|
|
167
|
+
case "chat" | "agent" | "chatflow":
|
|
168
|
+
if not prompt:
|
|
169
|
+
prompt = "请描述这张图片。"
|
|
170
|
+
|
|
171
|
+
async for chunk in self.api_client.chat_messages(
|
|
172
|
+
inputs={
|
|
173
|
+
**payload_vars,
|
|
174
|
+
},
|
|
175
|
+
query=prompt,
|
|
176
|
+
user=session_id,
|
|
177
|
+
conversation_id=conversation_id,
|
|
178
|
+
files=files_payload,
|
|
179
|
+
timeout=self.timeout,
|
|
180
|
+
):
|
|
181
|
+
logger.debug(f"dify resp chunk: {chunk}")
|
|
182
|
+
if chunk["event"] == "message" or chunk["event"] == "agent_message":
|
|
183
|
+
result += chunk["answer"]
|
|
184
|
+
if not conversation_id:
|
|
185
|
+
await sp.put_async(
|
|
186
|
+
scope="umo",
|
|
187
|
+
scope_id=session_id,
|
|
188
|
+
key="dify_conversation_id",
|
|
189
|
+
value=chunk["conversation_id"],
|
|
190
|
+
)
|
|
191
|
+
conversation_id = chunk["conversation_id"]
|
|
192
|
+
|
|
193
|
+
# 如果是流式响应,发送增量数据
|
|
194
|
+
if self.streaming and chunk["answer"]:
|
|
195
|
+
yield AgentResponse(
|
|
196
|
+
type="streaming_delta",
|
|
197
|
+
data=AgentResponseData(
|
|
198
|
+
chain=MessageChain().message(chunk["answer"])
|
|
199
|
+
),
|
|
200
|
+
)
|
|
201
|
+
elif chunk["event"] == "message_end":
|
|
202
|
+
logger.debug("Dify message end")
|
|
203
|
+
break
|
|
204
|
+
elif chunk["event"] == "error":
|
|
205
|
+
logger.error(f"Dify 出现错误:{chunk}")
|
|
206
|
+
raise Exception(
|
|
207
|
+
f"Dify 出现错误 status: {chunk['status']} message: {chunk['message']}"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
case "workflow":
|
|
211
|
+
async for chunk in self.api_client.workflow_run(
|
|
212
|
+
inputs={
|
|
213
|
+
self.dify_query_input_key: prompt,
|
|
214
|
+
"astrbot_session_id": session_id,
|
|
215
|
+
**payload_vars,
|
|
216
|
+
},
|
|
217
|
+
user=session_id,
|
|
218
|
+
files=files_payload,
|
|
219
|
+
timeout=self.timeout,
|
|
220
|
+
):
|
|
221
|
+
logger.debug(f"dify workflow resp chunk: {chunk}")
|
|
222
|
+
match chunk["event"]:
|
|
223
|
+
case "workflow_started":
|
|
224
|
+
logger.info(
|
|
225
|
+
f"Dify 工作流(ID: {chunk['workflow_run_id']})开始运行。"
|
|
226
|
+
)
|
|
227
|
+
case "node_finished":
|
|
228
|
+
logger.debug(
|
|
229
|
+
f"Dify 工作流节点(ID: {chunk['data']['node_id']} Title: {chunk['data'].get('title', '')})运行结束。"
|
|
230
|
+
)
|
|
231
|
+
case "text_chunk":
|
|
232
|
+
if self.streaming and chunk["data"]["text"]:
|
|
233
|
+
yield AgentResponse(
|
|
234
|
+
type="streaming_delta",
|
|
235
|
+
data=AgentResponseData(
|
|
236
|
+
chain=MessageChain().message(
|
|
237
|
+
chunk["data"]["text"]
|
|
238
|
+
)
|
|
239
|
+
),
|
|
240
|
+
)
|
|
241
|
+
case "workflow_finished":
|
|
242
|
+
logger.info(
|
|
243
|
+
f"Dify 工作流(ID: {chunk['workflow_run_id']})运行结束"
|
|
244
|
+
)
|
|
245
|
+
logger.debug(f"Dify 工作流结果:{chunk}")
|
|
246
|
+
if chunk["data"]["error"]:
|
|
247
|
+
logger.error(
|
|
248
|
+
f"Dify 工作流出现错误:{chunk['data']['error']}"
|
|
249
|
+
)
|
|
250
|
+
raise Exception(
|
|
251
|
+
f"Dify 工作流出现错误:{chunk['data']['error']}"
|
|
252
|
+
)
|
|
253
|
+
if self.workflow_output_key not in chunk["data"]["outputs"]:
|
|
254
|
+
raise Exception(
|
|
255
|
+
f"Dify 工作流的输出不包含指定的键名:{self.workflow_output_key}"
|
|
256
|
+
)
|
|
257
|
+
result = chunk
|
|
258
|
+
case _:
|
|
259
|
+
raise Exception(f"未知的 Dify API 类型:{self.api_type}")
|
|
260
|
+
|
|
261
|
+
if not result:
|
|
262
|
+
logger.warning("Dify 请求结果为空,请查看 Debug 日志。")
|
|
263
|
+
|
|
264
|
+
# 解析结果
|
|
265
|
+
chain = await self.parse_dify_result(result)
|
|
266
|
+
|
|
267
|
+
# 创建最终响应
|
|
268
|
+
self.final_llm_resp = LLMResponse(role="assistant", result_chain=chain)
|
|
269
|
+
self._transition_state(AgentState.DONE)
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
await self.agent_hooks.on_agent_done(self.run_context, self.final_llm_resp)
|
|
273
|
+
except Exception as e:
|
|
274
|
+
logger.error(f"Error in on_agent_done hook: {e}", exc_info=True)
|
|
275
|
+
|
|
276
|
+
# 返回最终结果
|
|
277
|
+
yield AgentResponse(
|
|
278
|
+
type="llm_result",
|
|
279
|
+
data=AgentResponseData(chain=chain),
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
async def parse_dify_result(self, chunk: dict | str) -> MessageChain:
|
|
283
|
+
"""解析 Dify 的响应结果"""
|
|
284
|
+
if isinstance(chunk, str):
|
|
285
|
+
# Chat
|
|
286
|
+
return MessageChain(chain=[Comp.Plain(chunk)])
|
|
287
|
+
|
|
288
|
+
async def parse_file(item: dict):
|
|
289
|
+
match item["type"]:
|
|
290
|
+
case "image":
|
|
291
|
+
return Comp.Image(file=item["url"], url=item["url"])
|
|
292
|
+
case "audio":
|
|
293
|
+
# 仅支持 wav
|
|
294
|
+
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
|
295
|
+
path = os.path.join(temp_dir, f"{item['filename']}.wav")
|
|
296
|
+
await download_file(item["url"], path)
|
|
297
|
+
return Comp.Image(file=item["url"], url=item["url"])
|
|
298
|
+
case "video":
|
|
299
|
+
return Comp.Video(file=item["url"])
|
|
300
|
+
case _:
|
|
301
|
+
return Comp.File(name=item["filename"], file=item["url"])
|
|
302
|
+
|
|
303
|
+
output = chunk["data"]["outputs"][self.workflow_output_key]
|
|
304
|
+
chains = []
|
|
305
|
+
if isinstance(output, str):
|
|
306
|
+
# 纯文本输出
|
|
307
|
+
chains.append(Comp.Plain(output))
|
|
308
|
+
elif isinstance(output, list):
|
|
309
|
+
# 主要适配 Dify 的 HTTP 请求结点的多模态输出
|
|
310
|
+
for item in output:
|
|
311
|
+
# handle Array[File]
|
|
312
|
+
if (
|
|
313
|
+
not isinstance(item, dict)
|
|
314
|
+
or item.get("dify_model_identity", "") != "__dify__file__"
|
|
315
|
+
):
|
|
316
|
+
chains.append(Comp.Plain(str(output)))
|
|
317
|
+
break
|
|
318
|
+
else:
|
|
319
|
+
chains.append(Comp.Plain(str(output)))
|
|
320
|
+
|
|
321
|
+
# scan file
|
|
322
|
+
files = chunk["data"].get("files", [])
|
|
323
|
+
for item in files:
|
|
324
|
+
comp = await parse_file(item)
|
|
325
|
+
chains.append(comp)
|
|
326
|
+
|
|
327
|
+
return MessageChain(chain=chains)
|
|
328
|
+
|
|
329
|
+
@override
|
|
330
|
+
def done(self) -> bool:
|
|
331
|
+
"""检查 Agent 是否已完成工作"""
|
|
332
|
+
return self._state in (AgentState.DONE, AgentState.ERROR)
|
|
333
|
+
|
|
334
|
+
@override
|
|
335
|
+
def get_final_llm_resp(self) -> LLMResponse | None:
|
|
336
|
+
return self.final_llm_resp
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import codecs
|
|
2
|
+
import json
|
|
3
|
+
from collections.abc import AsyncGenerator
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from aiohttp import ClientResponse, ClientSession, FormData
|
|
7
|
+
|
|
8
|
+
from astrbot.core import logger
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def _stream_sse(resp: ClientResponse) -> AsyncGenerator[dict, None]:
|
|
12
|
+
decoder = codecs.getincrementaldecoder("utf-8")()
|
|
13
|
+
buffer = ""
|
|
14
|
+
async for chunk in resp.content.iter_chunked(8192):
|
|
15
|
+
buffer += decoder.decode(chunk)
|
|
16
|
+
while "\n\n" in buffer:
|
|
17
|
+
block, buffer = buffer.split("\n\n", 1)
|
|
18
|
+
if block.strip().startswith("data:"):
|
|
19
|
+
try:
|
|
20
|
+
yield json.loads(block[5:])
|
|
21
|
+
except json.JSONDecodeError:
|
|
22
|
+
logger.warning(f"Drop invalid dify json data: {block[5:]}")
|
|
23
|
+
continue
|
|
24
|
+
# flush any remaining text
|
|
25
|
+
buffer += decoder.decode(b"", final=True)
|
|
26
|
+
if buffer.strip().startswith("data:"):
|
|
27
|
+
try:
|
|
28
|
+
yield json.loads(buffer[5:])
|
|
29
|
+
except json.JSONDecodeError:
|
|
30
|
+
logger.warning(f"Drop invalid dify json data: {buffer[5:]}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DifyAPIClient:
|
|
34
|
+
def __init__(self, api_key: str, api_base: str = "https://api.dify.ai/v1"):
|
|
35
|
+
self.api_key = api_key
|
|
36
|
+
self.api_base = api_base
|
|
37
|
+
self.session = ClientSession(trust_env=True)
|
|
38
|
+
self.headers = {
|
|
39
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async def chat_messages(
|
|
43
|
+
self,
|
|
44
|
+
inputs: dict,
|
|
45
|
+
query: str,
|
|
46
|
+
user: str,
|
|
47
|
+
response_mode: str = "streaming",
|
|
48
|
+
conversation_id: str = "",
|
|
49
|
+
files: list[dict[str, Any]] | None = None,
|
|
50
|
+
timeout: float = 60,
|
|
51
|
+
) -> AsyncGenerator[dict[str, Any], None]:
|
|
52
|
+
if files is None:
|
|
53
|
+
files = []
|
|
54
|
+
url = f"{self.api_base}/chat-messages"
|
|
55
|
+
payload = locals()
|
|
56
|
+
payload.pop("self")
|
|
57
|
+
payload.pop("timeout")
|
|
58
|
+
logger.info(f"chat_messages payload: {payload}")
|
|
59
|
+
async with self.session.post(
|
|
60
|
+
url,
|
|
61
|
+
json=payload,
|
|
62
|
+
headers=self.headers,
|
|
63
|
+
timeout=timeout,
|
|
64
|
+
) as resp:
|
|
65
|
+
if resp.status != 200:
|
|
66
|
+
text = await resp.text()
|
|
67
|
+
raise Exception(
|
|
68
|
+
f"Dify /chat-messages 接口请求失败:{resp.status}. {text}",
|
|
69
|
+
)
|
|
70
|
+
async for event in _stream_sse(resp):
|
|
71
|
+
yield event
|
|
72
|
+
|
|
73
|
+
async def workflow_run(
|
|
74
|
+
self,
|
|
75
|
+
inputs: dict,
|
|
76
|
+
user: str,
|
|
77
|
+
response_mode: str = "streaming",
|
|
78
|
+
files: list[dict[str, Any]] | None = None,
|
|
79
|
+
timeout: float = 60,
|
|
80
|
+
):
|
|
81
|
+
if files is None:
|
|
82
|
+
files = []
|
|
83
|
+
url = f"{self.api_base}/workflows/run"
|
|
84
|
+
payload = locals()
|
|
85
|
+
payload.pop("self")
|
|
86
|
+
payload.pop("timeout")
|
|
87
|
+
logger.info(f"workflow_run payload: {payload}")
|
|
88
|
+
async with self.session.post(
|
|
89
|
+
url,
|
|
90
|
+
json=payload,
|
|
91
|
+
headers=self.headers,
|
|
92
|
+
timeout=timeout,
|
|
93
|
+
) as resp:
|
|
94
|
+
if resp.status != 200:
|
|
95
|
+
text = await resp.text()
|
|
96
|
+
raise Exception(
|
|
97
|
+
f"Dify /workflows/run 接口请求失败:{resp.status}. {text}",
|
|
98
|
+
)
|
|
99
|
+
async for event in _stream_sse(resp):
|
|
100
|
+
yield event
|
|
101
|
+
|
|
102
|
+
async def file_upload(
|
|
103
|
+
self,
|
|
104
|
+
user: str,
|
|
105
|
+
file_path: str | None = None,
|
|
106
|
+
file_data: bytes | None = None,
|
|
107
|
+
file_name: str | None = None,
|
|
108
|
+
mime_type: str | None = None,
|
|
109
|
+
) -> dict[str, Any]:
|
|
110
|
+
"""Upload a file to Dify. Must provide either file_path or file_data.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
user: The user ID.
|
|
114
|
+
file_path: The path to the file to upload.
|
|
115
|
+
file_data: The file data in bytes.
|
|
116
|
+
file_name: Optional file name when using file_data.
|
|
117
|
+
Returns:
|
|
118
|
+
A dictionary containing the uploaded file information.
|
|
119
|
+
"""
|
|
120
|
+
url = f"{self.api_base}/files/upload"
|
|
121
|
+
|
|
122
|
+
form = FormData()
|
|
123
|
+
form.add_field("user", user)
|
|
124
|
+
|
|
125
|
+
if file_data is not None:
|
|
126
|
+
# 使用 bytes 数据
|
|
127
|
+
form.add_field(
|
|
128
|
+
"file",
|
|
129
|
+
file_data,
|
|
130
|
+
filename=file_name or "uploaded_file",
|
|
131
|
+
content_type=mime_type or "application/octet-stream",
|
|
132
|
+
)
|
|
133
|
+
elif file_path is not None:
|
|
134
|
+
# 使用文件路径
|
|
135
|
+
import os
|
|
136
|
+
|
|
137
|
+
with open(file_path, "rb") as f:
|
|
138
|
+
file_content = f.read()
|
|
139
|
+
form.add_field(
|
|
140
|
+
"file",
|
|
141
|
+
file_content,
|
|
142
|
+
filename=os.path.basename(file_path),
|
|
143
|
+
content_type=mime_type or "application/octet-stream",
|
|
144
|
+
)
|
|
145
|
+
else:
|
|
146
|
+
raise ValueError("file_path 和 file_data 不能同时为 None")
|
|
147
|
+
|
|
148
|
+
async with self.session.post(
|
|
149
|
+
url,
|
|
150
|
+
data=form,
|
|
151
|
+
headers=self.headers, # 不包含 Content-Type,让 aiohttp 自动设置
|
|
152
|
+
) as resp:
|
|
153
|
+
if resp.status != 200 and resp.status != 201:
|
|
154
|
+
text = await resp.text()
|
|
155
|
+
raise Exception(f"Dify 文件上传失败:{resp.status}. {text}")
|
|
156
|
+
return await resp.json() # {"id": "xxx", ...}
|
|
157
|
+
|
|
158
|
+
async def close(self):
|
|
159
|
+
await self.session.close()
|
|
160
|
+
|
|
161
|
+
async def get_chat_convs(self, user: str, limit: int = 20):
|
|
162
|
+
# conversations. GET
|
|
163
|
+
url = f"{self.api_base}/conversations"
|
|
164
|
+
payload = {
|
|
165
|
+
"user": user,
|
|
166
|
+
"limit": limit,
|
|
167
|
+
}
|
|
168
|
+
async with self.session.get(url, params=payload, headers=self.headers) as resp:
|
|
169
|
+
return await resp.json()
|
|
170
|
+
|
|
171
|
+
async def delete_chat_conv(self, user: str, conversation_id: str):
|
|
172
|
+
# conversation. DELETE
|
|
173
|
+
url = f"{self.api_base}/conversations/{conversation_id}"
|
|
174
|
+
payload = {
|
|
175
|
+
"user": user,
|
|
176
|
+
}
|
|
177
|
+
async with self.session.delete(url, json=payload, headers=self.headers) as resp:
|
|
178
|
+
return await resp.json()
|
|
179
|
+
|
|
180
|
+
async def rename(
|
|
181
|
+
self,
|
|
182
|
+
conversation_id: str,
|
|
183
|
+
name: str,
|
|
184
|
+
user: str,
|
|
185
|
+
auto_generate: bool = False,
|
|
186
|
+
):
|
|
187
|
+
# /conversations/:conversation_id/name
|
|
188
|
+
url = f"{self.api_base}/conversations/{conversation_id}/name"
|
|
189
|
+
payload = {
|
|
190
|
+
"user": user,
|
|
191
|
+
"name": name,
|
|
192
|
+
"auto_generate": auto_generate,
|
|
193
|
+
}
|
|
194
|
+
async with self.session.post(url, json=payload, headers=self.headers) as resp:
|
|
195
|
+
return await resp.json()
|