AstrBot 4.6.1__py3-none-any.whl → 4.7.1__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.
Files changed (37) hide show
  1. astrbot/core/agent/mcp_client.py +3 -3
  2. astrbot/core/agent/runners/base.py +7 -4
  3. astrbot/core/agent/runners/coze/coze_agent_runner.py +367 -0
  4. astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py +403 -0
  5. astrbot/core/agent/runners/dify/dify_agent_runner.py +336 -0
  6. astrbot/core/{utils → agent/runners/dify}/dify_api_client.py +51 -13
  7. astrbot/core/agent/runners/tool_loop_agent_runner.py +0 -6
  8. astrbot/core/config/default.py +141 -26
  9. astrbot/core/config/i18n_utils.py +110 -0
  10. astrbot/core/core_lifecycle.py +11 -13
  11. astrbot/core/db/po.py +1 -1
  12. astrbot/core/db/sqlite.py +2 -2
  13. astrbot/core/pipeline/process_stage/method/agent_request.py +48 -0
  14. astrbot/core/pipeline/process_stage/method/{llm_request.py → agent_sub_stages/internal.py} +13 -34
  15. astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +202 -0
  16. astrbot/core/pipeline/process_stage/method/star_request.py +1 -1
  17. astrbot/core/pipeline/process_stage/stage.py +8 -5
  18. astrbot/core/pipeline/result_decorate/stage.py +15 -5
  19. astrbot/core/provider/manager.py +43 -41
  20. astrbot/core/star/session_llm_manager.py +0 -107
  21. astrbot/core/star/session_plugin_manager.py +0 -81
  22. astrbot/core/umop_config_router.py +19 -0
  23. astrbot/core/utils/migra_helper.py +73 -0
  24. astrbot/core/utils/shared_preferences.py +1 -28
  25. astrbot/dashboard/routes/chat.py +13 -1
  26. astrbot/dashboard/routes/config.py +20 -16
  27. astrbot/dashboard/routes/knowledge_base.py +0 -156
  28. astrbot/dashboard/routes/session_management.py +311 -606
  29. {astrbot-4.6.1.dist-info → astrbot-4.7.1.dist-info}/METADATA +1 -1
  30. {astrbot-4.6.1.dist-info → astrbot-4.7.1.dist-info}/RECORD +34 -30
  31. {astrbot-4.6.1.dist-info → astrbot-4.7.1.dist-info}/WHEEL +1 -1
  32. astrbot/core/provider/sources/coze_source.py +0 -650
  33. astrbot/core/provider/sources/dashscope_source.py +0 -207
  34. astrbot/core/provider/sources/dify_source.py +0 -285
  35. /astrbot/core/{provider/sources → agent/runners/coze}/coze_api_client.py +0 -0
  36. {astrbot-4.6.1.dist-info → astrbot-4.7.1.dist-info}/entry_points.txt +0 -0
  37. {astrbot-4.6.1.dist-info → astrbot-4.7.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,207 +0,0 @@
1
- import asyncio
2
- import functools
3
- import re
4
-
5
- from dashscope import Application
6
- from dashscope.app.application_response import ApplicationResponse
7
-
8
- from astrbot.core import logger, sp
9
- from astrbot.core.message.message_event_result import MessageChain
10
-
11
- from .. import Provider
12
- from ..entities import LLMResponse
13
- from ..register import register_provider_adapter
14
- from .openai_source import ProviderOpenAIOfficial
15
-
16
-
17
- @register_provider_adapter("dashscope", "Dashscope APP 适配器。")
18
- class ProviderDashscope(ProviderOpenAIOfficial):
19
- def __init__(
20
- self,
21
- provider_config: dict,
22
- provider_settings: dict,
23
- ) -> None:
24
- Provider.__init__(
25
- self,
26
- provider_config,
27
- provider_settings,
28
- )
29
- self.api_key = provider_config.get("dashscope_api_key", "")
30
- if not self.api_key:
31
- raise Exception("阿里云百炼 API Key 不能为空。")
32
- self.app_id = provider_config.get("dashscope_app_id", "")
33
- if not self.app_id:
34
- raise Exception("阿里云百炼 APP ID 不能为空。")
35
- self.dashscope_app_type = provider_config.get("dashscope_app_type", "")
36
- if not self.dashscope_app_type:
37
- raise Exception("阿里云百炼 APP 类型不能为空。")
38
- self.model_name = "dashscope"
39
- self.variables: dict = provider_config.get("variables", {})
40
- self.rag_options: dict = provider_config.get("rag_options", {})
41
- self.output_reference = self.rag_options.get("output_reference", False)
42
- self.rag_options = self.rag_options.copy()
43
- self.rag_options.pop("output_reference", None)
44
-
45
- self.timeout = provider_config.get("timeout", 120)
46
- if isinstance(self.timeout, str):
47
- self.timeout = int(self.timeout)
48
-
49
- def has_rag_options(self):
50
- """判断是否有 RAG 选项
51
-
52
- Returns:
53
- bool: 是否有 RAG 选项
54
-
55
- """
56
- if self.rag_options and (
57
- len(self.rag_options.get("pipeline_ids", [])) > 0
58
- or len(self.rag_options.get("file_ids", [])) > 0
59
- ):
60
- return True
61
- return False
62
-
63
- async def text_chat(
64
- self,
65
- prompt: str,
66
- session_id=None,
67
- image_urls=None,
68
- func_tool=None,
69
- contexts=None,
70
- system_prompt=None,
71
- model=None,
72
- **kwargs,
73
- ) -> LLMResponse:
74
- if image_urls is None:
75
- image_urls = []
76
- if contexts is None:
77
- contexts = []
78
- # 获得会话变量
79
- payload_vars = self.variables.copy()
80
- # 动态变量
81
- session_var = await sp.session_get(session_id, "session_variables", default={})
82
- payload_vars.update(session_var)
83
-
84
- if (
85
- self.dashscope_app_type in ["agent", "dialog-workflow"]
86
- and not self.has_rag_options()
87
- ):
88
- # 支持多轮对话的
89
- new_record = {"role": "user", "content": prompt}
90
- if image_urls:
91
- logger.warning("阿里云百炼暂不支持图片输入,将自动忽略图片内容。")
92
- contexts_no_img = await self._remove_image_from_context(contexts)
93
- context_query = [*contexts_no_img, new_record]
94
- if system_prompt:
95
- context_query.insert(0, {"role": "system", "content": system_prompt})
96
- for part in context_query:
97
- if "_no_save" in part:
98
- del part["_no_save"]
99
- # 调用阿里云百炼 API
100
- payload = {
101
- "app_id": self.app_id,
102
- "api_key": self.api_key,
103
- "messages": context_query,
104
- "biz_params": payload_vars or None,
105
- }
106
- partial = functools.partial(
107
- Application.call,
108
- **payload,
109
- )
110
- response = await asyncio.get_event_loop().run_in_executor(None, partial)
111
- else:
112
- # 不支持多轮对话的
113
- # 调用阿里云百炼 API
114
- payload = {
115
- "app_id": self.app_id,
116
- "prompt": prompt,
117
- "api_key": self.api_key,
118
- "biz_params": payload_vars or None,
119
- }
120
- if self.rag_options:
121
- payload["rag_options"] = self.rag_options
122
- partial = functools.partial(
123
- Application.call,
124
- **payload,
125
- )
126
- response = await asyncio.get_event_loop().run_in_executor(None, partial)
127
-
128
- assert isinstance(response, ApplicationResponse)
129
-
130
- logger.debug(f"dashscope resp: {response}")
131
-
132
- if response.status_code != 200:
133
- logger.error(
134
- f"阿里云百炼请求失败: request_id={response.request_id}, code={response.status_code}, message={response.message}, 请参考文档:https://help.aliyun.com/zh/model-studio/developer-reference/error-code",
135
- )
136
- return LLMResponse(
137
- role="err",
138
- result_chain=MessageChain().message(
139
- f"阿里云百炼请求失败: message={response.message} code={response.status_code}",
140
- ),
141
- )
142
-
143
- output_text = response.output.get("text", "") or ""
144
- # RAG 引用脚标格式化
145
- output_text = re.sub(r"<ref>\[(\d+)\]</ref>", r"[\1]", output_text)
146
- if self.output_reference and response.output.get("doc_references", None):
147
- ref_parts = []
148
- for ref in response.output.get("doc_references", []) or []:
149
- ref_title = (
150
- ref.get("title", "")
151
- if ref.get("title")
152
- else ref.get("doc_name", "")
153
- )
154
- ref_parts.append(f"{ref['index_id']}. {ref_title}\n")
155
- ref_str = "".join(ref_parts)
156
- output_text += f"\n\n回答来源:\n{ref_str}"
157
-
158
- llm_response = LLMResponse("assistant")
159
- llm_response.result_chain = MessageChain().message(output_text)
160
-
161
- return llm_response
162
-
163
- async def text_chat_stream(
164
- self,
165
- prompt,
166
- session_id=None,
167
- image_urls=...,
168
- func_tool=None,
169
- contexts=...,
170
- system_prompt=None,
171
- tool_calls_result=None,
172
- model=None,
173
- **kwargs,
174
- ):
175
- # raise NotImplementedError("This method is not implemented yet.")
176
- # 调用 text_chat 模拟流式
177
- llm_response = await self.text_chat(
178
- prompt=prompt,
179
- session_id=session_id,
180
- image_urls=image_urls,
181
- func_tool=func_tool,
182
- contexts=contexts,
183
- system_prompt=system_prompt,
184
- tool_calls_result=tool_calls_result,
185
- )
186
- llm_response.is_chunk = True
187
- yield llm_response
188
- llm_response.is_chunk = False
189
- yield llm_response
190
-
191
- async def forget(self, session_id):
192
- return True
193
-
194
- async def get_current_key(self):
195
- return self.api_key
196
-
197
- async def set_key(self, key):
198
- raise Exception("阿里云百炼 适配器不支持设置 API Key。")
199
-
200
- async def get_models(self):
201
- return [self.get_model()]
202
-
203
- async def get_human_readable_context(self, session_id, page, page_size):
204
- raise Exception("暂不支持获得 阿里云百炼 的历史消息记录。")
205
-
206
- async def terminate(self):
207
- pass
@@ -1,285 +0,0 @@
1
- import os
2
-
3
- import astrbot.core.message.components as Comp
4
- from astrbot.core import logger, sp
5
- from astrbot.core.message.message_event_result import MessageChain
6
- from astrbot.core.utils.astrbot_path import get_astrbot_data_path
7
- from astrbot.core.utils.dify_api_client import DifyAPIClient
8
- from astrbot.core.utils.io import download_file, download_image_by_url
9
-
10
- from .. import Provider
11
- from ..entities import LLMResponse
12
- from ..register import register_provider_adapter
13
-
14
-
15
- @register_provider_adapter("dify", "Dify APP 适配器。")
16
- class ProviderDify(Provider):
17
- def __init__(
18
- self,
19
- provider_config,
20
- provider_settings,
21
- ) -> None:
22
- super().__init__(
23
- provider_config,
24
- provider_settings,
25
- )
26
- self.api_key = provider_config.get("dify_api_key", "")
27
- if not self.api_key:
28
- raise Exception("Dify API Key 不能为空。")
29
- api_base = provider_config.get("dify_api_base", "https://api.dify.ai/v1")
30
- self.api_type = provider_config.get("dify_api_type", "")
31
- if not self.api_type:
32
- raise Exception("Dify API 类型不能为空。")
33
- self.model_name = "dify"
34
- self.workflow_output_key = provider_config.get(
35
- "dify_workflow_output_key",
36
- "astrbot_wf_output",
37
- )
38
- self.dify_query_input_key = provider_config.get(
39
- "dify_query_input_key",
40
- "astrbot_text_query",
41
- )
42
- if not self.dify_query_input_key:
43
- self.dify_query_input_key = "astrbot_text_query"
44
- if not self.workflow_output_key:
45
- self.workflow_output_key = "astrbot_wf_output"
46
- self.variables: dict = provider_config.get("variables", {})
47
- self.timeout = provider_config.get("timeout", 120)
48
- if isinstance(self.timeout, str):
49
- self.timeout = int(self.timeout)
50
- self.conversation_ids = {}
51
- """记录当前 session id 的对话 ID"""
52
-
53
- self.api_client = DifyAPIClient(self.api_key, api_base)
54
-
55
- async def text_chat(
56
- self,
57
- prompt: str,
58
- session_id=None,
59
- image_urls=None,
60
- func_tool=None,
61
- contexts=None,
62
- system_prompt=None,
63
- tool_calls_result=None,
64
- model=None,
65
- **kwargs,
66
- ) -> LLMResponse:
67
- if image_urls is None:
68
- image_urls = []
69
- result = ""
70
- session_id = session_id or kwargs.get("user") or "unknown" # 1734
71
- conversation_id = self.conversation_ids.get(session_id, "")
72
-
73
- files_payload = []
74
- for image_url in image_urls:
75
- image_path = (
76
- await download_image_by_url(image_url)
77
- if image_url.startswith("http")
78
- else image_url
79
- )
80
- file_response = await self.api_client.file_upload(
81
- image_path,
82
- user=session_id,
83
- )
84
- logger.debug(f"Dify 上传图片响应:{file_response}")
85
- if "id" not in file_response:
86
- logger.warning(
87
- f"上传图片后得到未知的 Dify 响应:{file_response},图片将忽略。",
88
- )
89
- continue
90
- files_payload.append(
91
- {
92
- "type": "image",
93
- "transfer_method": "local_file",
94
- "upload_file_id": file_response["id"],
95
- },
96
- )
97
-
98
- # 获得会话变量
99
- payload_vars = self.variables.copy()
100
- # 动态变量
101
- session_var = await sp.session_get(session_id, "session_variables", default={})
102
- payload_vars.update(session_var)
103
- payload_vars["system_prompt"] = system_prompt
104
-
105
- try:
106
- match self.api_type:
107
- case "chat" | "agent" | "chatflow":
108
- if not prompt:
109
- prompt = "请描述这张图片。"
110
-
111
- async for chunk in self.api_client.chat_messages(
112
- inputs={
113
- **payload_vars,
114
- },
115
- query=prompt,
116
- user=session_id,
117
- conversation_id=conversation_id,
118
- files=files_payload,
119
- timeout=self.timeout,
120
- ):
121
- logger.debug(f"dify resp chunk: {chunk}")
122
- if (
123
- chunk["event"] == "message"
124
- or chunk["event"] == "agent_message"
125
- ):
126
- result += chunk["answer"]
127
- if not conversation_id:
128
- self.conversation_ids[session_id] = chunk[
129
- "conversation_id"
130
- ]
131
- conversation_id = chunk["conversation_id"]
132
- elif chunk["event"] == "message_end":
133
- logger.debug("Dify message end")
134
- break
135
- elif chunk["event"] == "error":
136
- logger.error(f"Dify 出现错误:{chunk}")
137
- raise Exception(
138
- f"Dify 出现错误 status: {chunk['status']} message: {chunk['message']}",
139
- )
140
-
141
- case "workflow":
142
- async for chunk in self.api_client.workflow_run(
143
- inputs={
144
- self.dify_query_input_key: prompt,
145
- "astrbot_session_id": session_id,
146
- **payload_vars,
147
- },
148
- user=session_id,
149
- files=files_payload,
150
- timeout=self.timeout,
151
- ):
152
- match chunk["event"]:
153
- case "workflow_started":
154
- logger.info(
155
- f"Dify 工作流(ID: {chunk['workflow_run_id']})开始运行。",
156
- )
157
- case "node_finished":
158
- logger.debug(
159
- f"Dify 工作流节点(ID: {chunk['data']['node_id']} Title: {chunk['data'].get('title', '')})运行结束。",
160
- )
161
- case "workflow_finished":
162
- logger.info(
163
- f"Dify 工作流(ID: {chunk['workflow_run_id']})运行结束",
164
- )
165
- logger.debug(f"Dify 工作流结果:{chunk}")
166
- if chunk["data"]["error"]:
167
- logger.error(
168
- f"Dify 工作流出现错误:{chunk['data']['error']}",
169
- )
170
- raise Exception(
171
- f"Dify 工作流出现错误:{chunk['data']['error']}",
172
- )
173
- if (
174
- self.workflow_output_key
175
- not in chunk["data"]["outputs"]
176
- ):
177
- raise Exception(
178
- f"Dify 工作流的输出不包含指定的键名:{self.workflow_output_key}",
179
- )
180
- result = chunk
181
- case _:
182
- raise Exception(f"未知的 Dify API 类型:{self.api_type}")
183
- except Exception as e:
184
- logger.error(f"Dify 请求失败:{e!s}")
185
- return LLMResponse(role="err", completion_text=f"Dify 请求失败:{e!s}")
186
-
187
- if not result:
188
- logger.warning("Dify 请求结果为空,请查看 Debug 日志。")
189
-
190
- chain = await self.parse_dify_result(result)
191
-
192
- return LLMResponse(role="assistant", result_chain=chain)
193
-
194
- async def text_chat_stream(
195
- self,
196
- prompt,
197
- session_id=None,
198
- image_urls=...,
199
- func_tool=None,
200
- contexts=...,
201
- system_prompt=None,
202
- tool_calls_result=None,
203
- model=None,
204
- **kwargs,
205
- ):
206
- # raise NotImplementedError("This method is not implemented yet.")
207
- # 调用 text_chat 模拟流式
208
- llm_response = await self.text_chat(
209
- prompt=prompt,
210
- session_id=session_id,
211
- image_urls=image_urls,
212
- func_tool=func_tool,
213
- contexts=contexts,
214
- system_prompt=system_prompt,
215
- tool_calls_result=tool_calls_result,
216
- )
217
- llm_response.is_chunk = True
218
- yield llm_response
219
- llm_response.is_chunk = False
220
- yield llm_response
221
-
222
- async def parse_dify_result(self, chunk: dict | str) -> MessageChain:
223
- if isinstance(chunk, str):
224
- # Chat
225
- return MessageChain(chain=[Comp.Plain(chunk)])
226
-
227
- async def parse_file(item: dict):
228
- match item["type"]:
229
- case "image":
230
- return Comp.Image(file=item["url"], url=item["url"])
231
- case "audio":
232
- # 仅支持 wav
233
- temp_dir = os.path.join(get_astrbot_data_path(), "temp")
234
- path = os.path.join(temp_dir, f"{item['filename']}.wav")
235
- await download_file(item["url"], path)
236
- return Comp.Image(file=item["url"], url=item["url"])
237
- case "video":
238
- return Comp.Video(file=item["url"])
239
- case _:
240
- return Comp.File(name=item["filename"], file=item["url"])
241
-
242
- output = chunk["data"]["outputs"][self.workflow_output_key]
243
- chains = []
244
- if isinstance(output, str):
245
- # 纯文本输出
246
- chains.append(Comp.Plain(output))
247
- elif isinstance(output, list):
248
- # 主要适配 Dify 的 HTTP 请求结点的多模态输出
249
- for item in output:
250
- # handle Array[File]
251
- if (
252
- not isinstance(item, dict)
253
- or item.get("dify_model_identity", "") != "__dify__file__"
254
- ):
255
- chains.append(Comp.Plain(str(output)))
256
- break
257
- else:
258
- chains.append(Comp.Plain(str(output)))
259
-
260
- # scan file
261
- files = chunk["data"].get("files", [])
262
- for item in files:
263
- comp = await parse_file(item)
264
- chains.append(comp)
265
-
266
- return MessageChain(chain=chains)
267
-
268
- async def forget(self, session_id):
269
- self.conversation_ids[session_id] = ""
270
- return True
271
-
272
- async def get_current_key(self):
273
- return self.api_key
274
-
275
- async def set_key(self, key):
276
- raise Exception("Dify 适配器不支持设置 API Key。")
277
-
278
- async def get_models(self):
279
- return [self.get_model()]
280
-
281
- async def get_human_readable_context(self, session_id, page, page_size):
282
- raise Exception("暂不支持获得 Dify 的历史消息记录。")
283
-
284
- async def terminate(self):
285
- await self.api_client.close()