AstrBot 4.10.2__py3-none-any.whl → 4.10.4__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 (84) hide show
  1. astrbot/builtin_stars/astrbot/long_term_memory.py +186 -0
  2. astrbot/builtin_stars/astrbot/main.py +120 -0
  3. astrbot/builtin_stars/astrbot/metadata.yaml +4 -0
  4. astrbot/builtin_stars/astrbot/process_llm_request.py +245 -0
  5. astrbot/builtin_stars/builtin_commands/commands/__init__.py +31 -0
  6. astrbot/builtin_stars/builtin_commands/commands/admin.py +77 -0
  7. astrbot/builtin_stars/builtin_commands/commands/alter_cmd.py +173 -0
  8. astrbot/builtin_stars/builtin_commands/commands/conversation.py +366 -0
  9. astrbot/builtin_stars/builtin_commands/commands/help.py +88 -0
  10. astrbot/builtin_stars/builtin_commands/commands/llm.py +20 -0
  11. astrbot/builtin_stars/builtin_commands/commands/persona.py +142 -0
  12. astrbot/builtin_stars/builtin_commands/commands/plugin.py +120 -0
  13. astrbot/builtin_stars/builtin_commands/commands/provider.py +329 -0
  14. astrbot/builtin_stars/builtin_commands/commands/setunset.py +36 -0
  15. astrbot/builtin_stars/builtin_commands/commands/sid.py +36 -0
  16. astrbot/builtin_stars/builtin_commands/commands/t2i.py +23 -0
  17. astrbot/builtin_stars/builtin_commands/commands/tool.py +31 -0
  18. astrbot/builtin_stars/builtin_commands/commands/tts.py +36 -0
  19. astrbot/builtin_stars/builtin_commands/commands/utils/rst_scene.py +26 -0
  20. astrbot/builtin_stars/builtin_commands/main.py +237 -0
  21. astrbot/builtin_stars/builtin_commands/metadata.yaml +4 -0
  22. astrbot/builtin_stars/python_interpreter/main.py +536 -0
  23. astrbot/builtin_stars/python_interpreter/metadata.yaml +4 -0
  24. astrbot/builtin_stars/python_interpreter/requirements.txt +1 -0
  25. astrbot/builtin_stars/python_interpreter/shared/api.py +22 -0
  26. astrbot/builtin_stars/reminder/main.py +266 -0
  27. astrbot/builtin_stars/reminder/metadata.yaml +4 -0
  28. astrbot/builtin_stars/session_controller/main.py +114 -0
  29. astrbot/builtin_stars/session_controller/metadata.yaml +5 -0
  30. astrbot/builtin_stars/web_searcher/engines/__init__.py +111 -0
  31. astrbot/builtin_stars/web_searcher/engines/bing.py +30 -0
  32. astrbot/builtin_stars/web_searcher/engines/sogo.py +52 -0
  33. astrbot/builtin_stars/web_searcher/main.py +436 -0
  34. astrbot/builtin_stars/web_searcher/metadata.yaml +4 -0
  35. astrbot/cli/__init__.py +1 -1
  36. astrbot/core/agent/message.py +32 -1
  37. astrbot/core/agent/runners/tool_loop_agent_runner.py +26 -8
  38. astrbot/core/astr_agent_hooks.py +6 -0
  39. astrbot/core/backup/__init__.py +26 -0
  40. astrbot/core/backup/constants.py +77 -0
  41. astrbot/core/backup/exporter.py +477 -0
  42. astrbot/core/backup/importer.py +761 -0
  43. astrbot/core/config/astrbot_config.py +2 -0
  44. astrbot/core/config/default.py +47 -6
  45. astrbot/core/knowledge_base/chunking/recursive.py +10 -2
  46. astrbot/core/log.py +1 -1
  47. astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +184 -174
  48. astrbot/core/pipeline/result_decorate/stage.py +65 -57
  49. astrbot/core/pipeline/waking_check/stage.py +31 -3
  50. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +15 -29
  51. astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +1 -6
  52. astrbot/core/platform/sources/dingtalk/dingtalk_event.py +15 -1
  53. astrbot/core/platform/sources/lark/lark_adapter.py +2 -10
  54. astrbot/core/platform/sources/misskey/misskey_adapter.py +0 -5
  55. astrbot/core/platform/sources/misskey/misskey_utils.py +0 -3
  56. astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +4 -9
  57. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +4 -9
  58. astrbot/core/platform/sources/satori/satori_adapter.py +6 -1
  59. astrbot/core/platform/sources/slack/slack_adapter.py +3 -6
  60. astrbot/core/platform/sources/webchat/webchat_adapter.py +0 -1
  61. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +3 -5
  62. astrbot/core/provider/entities.py +41 -10
  63. astrbot/core/provider/provider.py +3 -1
  64. astrbot/core/provider/sources/anthropic_source.py +140 -30
  65. astrbot/core/provider/sources/fishaudio_tts_api_source.py +14 -6
  66. astrbot/core/provider/sources/gemini_source.py +112 -29
  67. astrbot/core/provider/sources/minimax_tts_api_source.py +4 -1
  68. astrbot/core/provider/sources/openai_source.py +93 -56
  69. astrbot/core/provider/sources/xai_source.py +29 -0
  70. astrbot/core/provider/sources/xinference_stt_provider.py +24 -12
  71. astrbot/core/star/context.py +1 -1
  72. astrbot/core/star/star_manager.py +52 -13
  73. astrbot/core/utils/astrbot_path.py +34 -0
  74. astrbot/core/utils/pip_installer.py +20 -1
  75. astrbot/dashboard/routes/__init__.py +2 -0
  76. astrbot/dashboard/routes/backup.py +1093 -0
  77. astrbot/dashboard/routes/config.py +45 -0
  78. astrbot/dashboard/routes/log.py +44 -10
  79. astrbot/dashboard/server.py +9 -1
  80. {astrbot-4.10.2.dist-info → astrbot-4.10.4.dist-info}/METADATA +1 -1
  81. {astrbot-4.10.2.dist-info → astrbot-4.10.4.dist-info}/RECORD +84 -44
  82. {astrbot-4.10.2.dist-info → astrbot-4.10.4.dist-info}/WHEEL +0 -0
  83. {astrbot-4.10.2.dist-info → astrbot-4.10.4.dist-info}/entry_points.txt +0 -0
  84. {astrbot-4.10.2.dist-info → astrbot-4.10.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,436 @@
1
+ import asyncio
2
+ import random
3
+
4
+ import aiohttp
5
+ from bs4 import BeautifulSoup
6
+ from readability import Document
7
+
8
+ from astrbot.api import AstrBotConfig, llm_tool, logger, star
9
+ from astrbot.api.event import AstrMessageEvent, MessageEventResult, filter
10
+ from astrbot.api.provider import ProviderRequest
11
+ from astrbot.core.provider.func_tool_manager import FunctionToolManager
12
+
13
+ from .engines import HEADERS, USER_AGENTS, SearchResult
14
+ from .engines.bing import Bing
15
+ from .engines.sogo import Sogo
16
+
17
+
18
+ class Main(star.Star):
19
+ TOOLS = [
20
+ "web_search",
21
+ "fetch_url",
22
+ "web_search_tavily",
23
+ "tavily_extract_web_page",
24
+ ]
25
+
26
+ def __init__(self, context: star.Context) -> None:
27
+ self.context = context
28
+ self.tavily_key_index = 0
29
+ self.tavily_key_lock = asyncio.Lock()
30
+
31
+ # 将 str 类型的 key 迁移至 list[str],并保存
32
+ cfg = self.context.get_config()
33
+ provider_settings = cfg.get("provider_settings")
34
+ if provider_settings:
35
+ tavily_key = provider_settings.get("websearch_tavily_key")
36
+ if isinstance(tavily_key, str):
37
+ logger.info(
38
+ "检测到旧版 websearch_tavily_key (字符串格式),自动迁移为列表格式并保存。",
39
+ )
40
+ if tavily_key:
41
+ provider_settings["websearch_tavily_key"] = [tavily_key]
42
+ else:
43
+ provider_settings["websearch_tavily_key"] = []
44
+ cfg.save_config()
45
+
46
+ self.bing_search = Bing()
47
+ self.sogo_search = Sogo()
48
+ self.baidu_initialized = False
49
+
50
+ async def _tidy_text(self, text: str) -> str:
51
+ """清理文本,去除空格、换行符等"""
52
+ return text.strip().replace("\n", " ").replace("\r", " ").replace(" ", " ")
53
+
54
+ async def _get_from_url(self, url: str) -> str:
55
+ """获取网页内容"""
56
+ header = HEADERS
57
+ header.update({"User-Agent": random.choice(USER_AGENTS)})
58
+ async with aiohttp.ClientSession(trust_env=True) as session:
59
+ async with session.get(url, headers=header, timeout=6) as response:
60
+ html = await response.text(encoding="utf-8")
61
+ doc = Document(html)
62
+ ret = doc.summary(html_partial=True)
63
+ soup = BeautifulSoup(ret, "html.parser")
64
+ ret = await self._tidy_text(soup.get_text())
65
+ return ret
66
+
67
+ async def _process_search_result(
68
+ self,
69
+ result: SearchResult,
70
+ idx: int,
71
+ websearch_link: bool,
72
+ ) -> str:
73
+ """处理单个搜索结果"""
74
+ logger.info(f"web_searcher - scraping web: {result.title} - {result.url}")
75
+ try:
76
+ site_result = await self._get_from_url(result.url)
77
+ except BaseException:
78
+ site_result = ""
79
+ site_result = (
80
+ f"{site_result[:700]}..." if len(site_result) > 700 else site_result
81
+ )
82
+
83
+ header = f"{idx}. {result.title} "
84
+
85
+ if websearch_link and result.url:
86
+ header += result.url
87
+
88
+ return f"{header}\n{result.snippet}\n{site_result}\n\n"
89
+
90
+ async def _web_search_default(
91
+ self,
92
+ query,
93
+ num_results: int = 5,
94
+ ) -> list[SearchResult]:
95
+ results = []
96
+ try:
97
+ results = await self.bing_search.search(query, num_results)
98
+ except Exception as e:
99
+ logger.error(f"bing search error: {e}, try the next one...")
100
+ if len(results) == 0:
101
+ logger.debug("search bing failed")
102
+ try:
103
+ results = await self.sogo_search.search(query, num_results)
104
+ except Exception as e:
105
+ logger.error(f"sogo search error: {e}")
106
+ if len(results) == 0:
107
+ logger.debug("search sogo failed")
108
+ return []
109
+
110
+ return results
111
+
112
+ async def _get_tavily_key(self, cfg: AstrBotConfig) -> str:
113
+ """并发安全的从列表中获取并轮换Tavily API密钥。"""
114
+ tavily_keys = cfg.get("provider_settings", {}).get("websearch_tavily_key", [])
115
+ if not tavily_keys:
116
+ raise ValueError("错误:Tavily API密钥未在AstrBot中配置。")
117
+
118
+ async with self.tavily_key_lock:
119
+ key = tavily_keys[self.tavily_key_index]
120
+ self.tavily_key_index = (self.tavily_key_index + 1) % len(tavily_keys)
121
+ return key
122
+
123
+ async def _web_search_tavily(
124
+ self,
125
+ cfg: AstrBotConfig,
126
+ payload: dict,
127
+ ) -> list[SearchResult]:
128
+ """使用 Tavily 搜索引擎进行搜索"""
129
+ tavily_key = await self._get_tavily_key(cfg)
130
+ url = "https://api.tavily.com/search"
131
+ header = {
132
+ "Authorization": f"Bearer {tavily_key}",
133
+ "Content-Type": "application/json",
134
+ }
135
+ async with aiohttp.ClientSession(trust_env=True) as session:
136
+ async with session.post(
137
+ url,
138
+ json=payload,
139
+ headers=header,
140
+ timeout=6,
141
+ ) as response:
142
+ if response.status != 200:
143
+ reason = await response.text()
144
+ raise Exception(
145
+ f"Tavily web search failed: {reason}, status: {response.status}",
146
+ )
147
+ data = await response.json()
148
+ results = []
149
+ for item in data.get("results", []):
150
+ result = SearchResult(
151
+ title=item.get("title"),
152
+ url=item.get("url"),
153
+ snippet=item.get("content"),
154
+ )
155
+ results.append(result)
156
+ return results
157
+
158
+ async def _extract_tavily(self, cfg: AstrBotConfig, payload: dict) -> list[dict]:
159
+ """使用 Tavily 提取网页内容"""
160
+ tavily_key = await self._get_tavily_key(cfg)
161
+ url = "https://api.tavily.com/extract"
162
+ header = {
163
+ "Authorization": f"Bearer {tavily_key}",
164
+ "Content-Type": "application/json",
165
+ }
166
+ async with aiohttp.ClientSession(trust_env=True) as session:
167
+ async with session.post(
168
+ url,
169
+ json=payload,
170
+ headers=header,
171
+ timeout=6,
172
+ ) as response:
173
+ if response.status != 200:
174
+ reason = await response.text()
175
+ raise Exception(
176
+ f"Tavily web search failed: {reason}, status: {response.status}",
177
+ )
178
+ data = await response.json()
179
+ results: list[dict] = data.get("results", [])
180
+ if not results:
181
+ raise ValueError(
182
+ "Error: Tavily web searcher does not return any results.",
183
+ )
184
+ return results
185
+
186
+ @filter.command("websearch")
187
+ async def websearch(self, event: AstrMessageEvent, oper: str | None = None):
188
+ """网页搜索指令(已废弃)"""
189
+ event.set_result(
190
+ MessageEventResult().message(
191
+ "此指令已经被废弃,请在 WebUI 中开启或关闭网页搜索功能。",
192
+ ),
193
+ )
194
+
195
+ @llm_tool(name="web_search")
196
+ async def search_from_search_engine(
197
+ self,
198
+ event: AstrMessageEvent,
199
+ query: str,
200
+ max_results: int = 5,
201
+ ) -> str:
202
+ """搜索网络以回答用户的问题。当用户需要搜索网络以获取即时性的信息时调用此工具。
203
+
204
+ Args:
205
+ query(string): 和用户的问题最相关的搜索关键词,用于在 Google 上搜索。
206
+ max_results(number): 返回的最大搜索结果数量,默认为 5。
207
+
208
+ """
209
+ logger.info(f"web_searcher - search_from_search_engine: {query}")
210
+ cfg = self.context.get_config(umo=event.unified_msg_origin)
211
+ websearch_link = cfg["provider_settings"].get("web_search_link", False)
212
+
213
+ results = await self._web_search_default(query, max_results)
214
+ if not results:
215
+ return "Error: web searcher does not return any results."
216
+
217
+ tasks = []
218
+ for idx, result in enumerate(results, 1):
219
+ task = self._process_search_result(result, idx, websearch_link)
220
+ tasks.append(task)
221
+ processed_results = await asyncio.gather(*tasks, return_exceptions=True)
222
+ ret = ""
223
+ for processed_result in processed_results:
224
+ if isinstance(processed_result, BaseException):
225
+ logger.error(f"Error processing search result: {processed_result}")
226
+ continue
227
+ ret += processed_result
228
+
229
+ if websearch_link:
230
+ ret += "\n\n针对问题,请根据上面的结果分点总结,并且在结尾处附上对应内容的参考链接(如有)。"
231
+
232
+ return ret
233
+
234
+ async def ensure_baidu_ai_search_mcp(self, umo: str | None = None):
235
+ if self.baidu_initialized:
236
+ return
237
+ cfg = self.context.get_config(umo=umo)
238
+ key = cfg.get("provider_settings", {}).get(
239
+ "websearch_baidu_app_builder_key",
240
+ "",
241
+ )
242
+ if not key:
243
+ raise ValueError(
244
+ "Error: Baidu AI Search API key is not configured in AstrBot.",
245
+ )
246
+ func_tool_mgr = self.context.get_llm_tool_manager()
247
+ await func_tool_mgr.enable_mcp_server(
248
+ "baidu_ai_search",
249
+ config={
250
+ "transport": "sse",
251
+ "url": f"http://appbuilder.baidu.com/v2/ai_search/mcp/sse?api_key={key}",
252
+ "headers": {},
253
+ "timeout": 30,
254
+ },
255
+ )
256
+ self.baidu_initialized = True
257
+ logger.info("Successfully initialized Baidu AI Search MCP server.")
258
+
259
+ @llm_tool(name="fetch_url")
260
+ async def fetch_website_content(self, event: AstrMessageEvent, url: str) -> str:
261
+ """Fetch the content of a website with the given web url
262
+
263
+ Args:
264
+ url(string): The url of the website to fetch content from
265
+
266
+ """
267
+ resp = await self._get_from_url(url)
268
+ return resp
269
+
270
+ @llm_tool("web_search_tavily")
271
+ async def search_from_tavily(
272
+ self,
273
+ event: AstrMessageEvent,
274
+ query: str,
275
+ max_results: int = 5,
276
+ search_depth: str = "basic",
277
+ topic: str = "general",
278
+ days: int = 3,
279
+ time_range: str = "",
280
+ start_date: str = "",
281
+ end_date: str = "",
282
+ ) -> str:
283
+ """A web search tool that uses Tavily to search the web for relevant content.
284
+ Ideal for gathering current information, news, and detailed web content analysis.
285
+
286
+ Args:
287
+ query(string): Required. Search query.
288
+ max_results(number): Optional. The maximum number of results to return. Default is 5. Range is 5-20.
289
+ search_depth(string): Optional. The depth of the search, must be one of 'basic', 'advanced'. Default is "basic".
290
+ topic(string): Optional. The topic of the search, must be one of 'general', 'news'. Default is "general".
291
+ days(number): Optional. The number of days back from the current date to include in the search results. Please note that this feature is only available when using the 'news' search topic.
292
+ time_range(string): Optional. The time range back from the current date to include in the search results. This feature is available for both 'general' and 'news' search topics. Must be one of 'day', 'week', 'month', 'year'.
293
+ start_date(string): Optional. The start date for the search results in the format 'YYYY-MM-DD'.
294
+ end_date(string): Optional. The end date for the search results in the format 'YYYY-MM-DD'.
295
+
296
+ """
297
+ logger.info(f"web_searcher - search_from_tavily: {query}")
298
+ cfg = self.context.get_config(umo=event.unified_msg_origin)
299
+ websearch_link = cfg["provider_settings"].get("web_search_link", False)
300
+ if not cfg.get("provider_settings", {}).get("websearch_tavily_key", []):
301
+ raise ValueError("Error: Tavily API key is not configured in AstrBot.")
302
+
303
+ # build payload
304
+ payload = {
305
+ "query": query,
306
+ "max_results": max_results,
307
+ }
308
+ if search_depth not in ["basic", "advanced"]:
309
+ search_depth = "basic"
310
+ payload["search_depth"] = search_depth
311
+
312
+ if topic not in ["general", "news"]:
313
+ topic = "general"
314
+ payload["topic"] = topic
315
+
316
+ if topic == "news":
317
+ payload["days"] = days
318
+
319
+ if time_range in ["day", "week", "month", "year"]:
320
+ payload["time_range"] = time_range
321
+ if start_date:
322
+ payload["start_date"] = start_date
323
+ if end_date:
324
+ payload["end_date"] = end_date
325
+
326
+ results = await self._web_search_tavily(cfg, payload)
327
+ if not results:
328
+ return "Error: Tavily web searcher does not return any results."
329
+
330
+ ret_ls = []
331
+ for result in results:
332
+ ret_ls.append(f"\nTitle: {result.title}")
333
+ ret_ls.append(f"URL: {result.url}")
334
+ ret_ls.append(f"Content: {result.snippet}")
335
+ ret = "\n".join(ret_ls)
336
+
337
+ if websearch_link:
338
+ ret += "\n\n针对问题,请根据上面的结果分点总结,并且在结尾处附上对应内容的参考链接(如有)。"
339
+ return ret
340
+
341
+ @llm_tool("tavily_extract_web_page")
342
+ async def tavily_extract_web_page(
343
+ self,
344
+ event: AstrMessageEvent,
345
+ url: str = "",
346
+ extract_depth: str = "basic",
347
+ ) -> str:
348
+ """Extract the content of a web page using Tavily.
349
+
350
+ Args:
351
+ url(string): Required. An URl to extract content from.
352
+ extract_depth(string): Optional. The depth of the extraction, must be one of 'basic', 'advanced'. Default is "basic".
353
+
354
+ """
355
+ cfg = self.context.get_config(umo=event.unified_msg_origin)
356
+ if not cfg.get("provider_settings", {}).get("websearch_tavily_key", []):
357
+ raise ValueError("Error: Tavily API key is not configured in AstrBot.")
358
+
359
+ if not url:
360
+ raise ValueError("Error: url must be a non-empty string.")
361
+ if extract_depth not in ["basic", "advanced"]:
362
+ extract_depth = "basic"
363
+ payload = {
364
+ "urls": [url],
365
+ "extract_depth": extract_depth,
366
+ }
367
+ results = await self._extract_tavily(cfg, payload)
368
+ ret_ls = []
369
+ for result in results:
370
+ ret_ls.append(f"URL: {result.get('url', 'No URL')}")
371
+ ret_ls.append(f"Content: {result.get('raw_content', 'No content')}")
372
+ ret = "\n".join(ret_ls)
373
+ if not ret:
374
+ return "Error: Tavily web searcher does not return any results."
375
+ return ret
376
+
377
+ @filter.on_llm_request(priority=-10000)
378
+ async def edit_web_search_tools(
379
+ self,
380
+ event: AstrMessageEvent,
381
+ req: ProviderRequest,
382
+ ):
383
+ """Get the session conversation for the given event."""
384
+ cfg = self.context.get_config(umo=event.unified_msg_origin)
385
+ prov_settings = cfg.get("provider_settings", {})
386
+ websearch_enable = prov_settings.get("web_search", False)
387
+ provider = prov_settings.get("websearch_provider", "default")
388
+
389
+ tool_set = req.func_tool
390
+ if isinstance(tool_set, FunctionToolManager):
391
+ req.func_tool = tool_set.get_full_tool_set()
392
+ tool_set = req.func_tool
393
+
394
+ if not tool_set:
395
+ return
396
+
397
+ if not websearch_enable:
398
+ # pop tools
399
+ for tool_name in self.TOOLS:
400
+ tool_set.remove_tool(tool_name)
401
+ return
402
+
403
+ func_tool_mgr = self.context.get_llm_tool_manager()
404
+ if provider == "default":
405
+ web_search_t = func_tool_mgr.get_func("web_search")
406
+ fetch_url_t = func_tool_mgr.get_func("fetch_url")
407
+ if web_search_t:
408
+ tool_set.add_tool(web_search_t)
409
+ if fetch_url_t:
410
+ tool_set.add_tool(fetch_url_t)
411
+ tool_set.remove_tool("web_search_tavily")
412
+ tool_set.remove_tool("tavily_extract_web_page")
413
+ tool_set.remove_tool("AIsearch")
414
+ elif provider == "tavily":
415
+ web_search_tavily = func_tool_mgr.get_func("web_search_tavily")
416
+ tavily_extract_web_page = func_tool_mgr.get_func("tavily_extract_web_page")
417
+ if web_search_tavily:
418
+ tool_set.add_tool(web_search_tavily)
419
+ if tavily_extract_web_page:
420
+ tool_set.add_tool(tavily_extract_web_page)
421
+ tool_set.remove_tool("web_search")
422
+ tool_set.remove_tool("fetch_url")
423
+ tool_set.remove_tool("AIsearch")
424
+ elif provider == "baidu_ai_search":
425
+ try:
426
+ await self.ensure_baidu_ai_search_mcp(event.unified_msg_origin)
427
+ aisearch_tool = func_tool_mgr.get_func("AIsearch")
428
+ if not aisearch_tool:
429
+ raise ValueError("Cannot get Baidu AI Search MCP tool.")
430
+ tool_set.add_tool(aisearch_tool)
431
+ tool_set.remove_tool("web_search")
432
+ tool_set.remove_tool("fetch_url")
433
+ tool_set.remove_tool("web_search_tavily")
434
+ tool_set.remove_tool("tavily_extract_web_page")
435
+ except Exception as e:
436
+ logger.error(f"Cannot Initialize Baidu AI Search MCP Server: {e}")
@@ -0,0 +1,4 @@
1
+ name: astrbot-web-searcher
2
+ desc: 让 LLM 具有网页检索能力
3
+ author: Soulter
4
+ version: 1.14.514
astrbot/cli/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "4.10.2"
1
+ __version__ = "4.10.4"
@@ -12,7 +12,7 @@ class ContentPart(BaseModel):
12
12
 
13
13
  __content_part_registry: ClassVar[dict[str, type["ContentPart"]]] = {}
14
14
 
15
- type: str
15
+ type: Literal["text", "think", "image_url", "audio_url"]
16
16
 
17
17
  def __init_subclass__(cls, **kwargs: Any) -> None:
18
18
  super().__init_subclass__(**kwargs)
@@ -63,6 +63,28 @@ class TextPart(ContentPart):
63
63
  text: str
64
64
 
65
65
 
66
+ class ThinkPart(ContentPart):
67
+ """
68
+ >>> ThinkPart(think="I think I need to think about this.").model_dump()
69
+ {'type': 'think', 'think': 'I think I need to think about this.', 'encrypted': None}
70
+ """
71
+
72
+ type: str = "think"
73
+ think: str
74
+ encrypted: str | None = None
75
+ """Encrypted thinking content, or signature."""
76
+
77
+ def merge_in_place(self, other: Any) -> bool:
78
+ if not isinstance(other, ThinkPart):
79
+ return False
80
+ if self.encrypted:
81
+ return False
82
+ self.think += other.think
83
+ if other.encrypted:
84
+ self.encrypted = other.encrypted
85
+ return True
86
+
87
+
66
88
  class ImageURLPart(ContentPart):
67
89
  """
68
90
  >>> ImageURLPart(image_url="http://example.com/image.jpg").model_dump()
@@ -169,6 +191,15 @@ class Message(BaseModel):
169
191
  )
170
192
  return self
171
193
 
194
+ @model_serializer(mode="wrap")
195
+ def serialize(self, handler):
196
+ data = handler(self)
197
+ if self.tool_calls is None:
198
+ data.pop("tool_calls", None)
199
+ if self.tool_call_id is None:
200
+ data.pop("tool_call_id", None)
201
+ return data
202
+
172
203
 
173
204
  class AssistantMessageSegment(Message):
174
205
  """A message segment from the assistant."""
@@ -13,6 +13,7 @@ from mcp.types import (
13
13
  )
14
14
 
15
15
  from astrbot import logger
16
+ from astrbot.core.agent.message import TextPart, ThinkPart
16
17
  from astrbot.core.message.components import Json
17
18
  from astrbot.core.message.message_event_result import (
18
19
  MessageChain,
@@ -77,10 +78,11 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
77
78
  async def _iter_llm_responses(self) -> T.AsyncGenerator[LLMResponse, None]:
78
79
  """Yields chunks *and* a final LLMResponse."""
79
80
  payload = {
80
- "contexts": self.run_context.messages,
81
+ "contexts": self.run_context.messages, # list[Message]
81
82
  "func_tool": self.req.func_tool,
82
83
  "model": self.req.model, # NOTE: in fact, this arg is None in most cases
83
84
  "session_id": self.req.session_id,
85
+ "extra_user_content_parts": self.req.extra_user_content_parts, # list[ContentPart]
84
86
  }
85
87
 
86
88
  if self.streaming:
@@ -168,13 +170,20 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
168
170
  self.final_llm_resp = llm_resp
169
171
  self._transition_state(AgentState.DONE)
170
172
  self.stats.end_time = time.time()
173
+
171
174
  # record the final assistant message
172
- self.run_context.messages.append(
173
- Message(
174
- role="assistant",
175
- content=llm_resp.completion_text or "*No response*",
176
- ),
177
- )
175
+ parts = []
176
+ if llm_resp.reasoning_content or llm_resp.reasoning_signature:
177
+ parts.append(
178
+ ThinkPart(
179
+ think=llm_resp.reasoning_content,
180
+ encrypted=llm_resp.reasoning_signature,
181
+ )
182
+ )
183
+ parts.append(TextPart(text=llm_resp.completion_text or "*No response*"))
184
+ self.run_context.messages.append(Message(role="assistant", content=parts))
185
+
186
+ # call the on_agent_done hook
178
187
  try:
179
188
  await self.agent_hooks.on_agent_done(self.run_context, llm_resp)
180
189
  except Exception as e:
@@ -213,10 +222,19 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
213
222
  data=AgentResponseData(chain=result),
214
223
  )
215
224
  # 将结果添加到上下文中
225
+ parts = []
226
+ if llm_resp.reasoning_content or llm_resp.reasoning_signature:
227
+ parts.append(
228
+ ThinkPart(
229
+ think=llm_resp.reasoning_content,
230
+ encrypted=llm_resp.reasoning_signature,
231
+ )
232
+ )
233
+ parts.append(TextPart(text=llm_resp.completion_text or "*No response*"))
216
234
  tool_calls_result = ToolCallsResult(
217
235
  tool_calls_info=AssistantMessageSegment(
218
236
  tool_calls=llm_resp.to_openai_to_calls_model(),
219
- content=llm_resp.completion_text,
237
+ content=parts,
220
238
  ),
221
239
  tool_calls_result=tool_call_result_blocks,
222
240
  )
@@ -13,6 +13,12 @@ from astrbot.core.star.star_handler import EventType
13
13
  class MainAgentHooks(BaseAgentRunHooks[AstrAgentContext]):
14
14
  async def on_agent_done(self, run_context, llm_response):
15
15
  # 执行事件钩子
16
+ if llm_response and llm_response.reasoning_content:
17
+ # we will use this in result_decorate stage to inject reasoning content to chain
18
+ run_context.context.event.set_extra(
19
+ "_llm_reasoning_content", llm_response.reasoning_content
20
+ )
21
+
16
22
  await call_event_hook(
17
23
  run_context.context.event,
18
24
  EventType.OnLLMResponseEvent,
@@ -0,0 +1,26 @@
1
+ """AstrBot 备份与恢复模块
2
+
3
+ 提供数据导出和导入功能,支持用户在服务器迁移时一键备份和恢复所有数据。
4
+ """
5
+
6
+ # 从 constants 模块导入共享常量
7
+ from .constants import (
8
+ BACKUP_MANIFEST_VERSION,
9
+ KB_METADATA_MODELS,
10
+ MAIN_DB_MODELS,
11
+ get_backup_directories,
12
+ )
13
+
14
+ # 导入导出器和导入器
15
+ from .exporter import AstrBotExporter
16
+ from .importer import AstrBotImporter, ImportPreCheckResult
17
+
18
+ __all__ = [
19
+ "AstrBotExporter",
20
+ "AstrBotImporter",
21
+ "ImportPreCheckResult",
22
+ "MAIN_DB_MODELS",
23
+ "KB_METADATA_MODELS",
24
+ "get_backup_directories",
25
+ "BACKUP_MANIFEST_VERSION",
26
+ ]
@@ -0,0 +1,77 @@
1
+ """AstrBot 备份模块共享常量
2
+
3
+ 此文件定义了导出器和导入器共享的常量,确保两端配置一致。
4
+ """
5
+
6
+ from sqlmodel import SQLModel
7
+
8
+ from astrbot.core.db.po import (
9
+ Attachment,
10
+ CommandConfig,
11
+ CommandConflict,
12
+ ConversationV2,
13
+ Persona,
14
+ PlatformMessageHistory,
15
+ PlatformSession,
16
+ PlatformStat,
17
+ Preference,
18
+ )
19
+ from astrbot.core.knowledge_base.models import (
20
+ KBDocument,
21
+ KBMedia,
22
+ KnowledgeBase,
23
+ )
24
+ from astrbot.core.utils.astrbot_path import (
25
+ get_astrbot_config_path,
26
+ get_astrbot_plugin_data_path,
27
+ get_astrbot_plugin_path,
28
+ get_astrbot_t2i_templates_path,
29
+ get_astrbot_temp_path,
30
+ get_astrbot_webchat_path,
31
+ )
32
+
33
+ # ============================================================
34
+ # 共享常量 - 确保导出和导入端配置一致
35
+ # ============================================================
36
+
37
+ # 主数据库模型类映射
38
+ MAIN_DB_MODELS: dict[str, type[SQLModel]] = {
39
+ "platform_stats": PlatformStat,
40
+ "conversations": ConversationV2,
41
+ "personas": Persona,
42
+ "preferences": Preference,
43
+ "platform_message_history": PlatformMessageHistory,
44
+ "platform_sessions": PlatformSession,
45
+ "attachments": Attachment,
46
+ "command_configs": CommandConfig,
47
+ "command_conflicts": CommandConflict,
48
+ }
49
+
50
+ # 知识库元数据模型类映射
51
+ KB_METADATA_MODELS: dict[str, type[SQLModel]] = {
52
+ "knowledge_bases": KnowledgeBase,
53
+ "kb_documents": KBDocument,
54
+ "kb_media": KBMedia,
55
+ }
56
+
57
+
58
+ def get_backup_directories() -> dict[str, str]:
59
+ """获取需要备份的目录列表
60
+
61
+ 使用 astrbot_path 模块动态获取路径,支持通过环境变量 ASTRBOT_ROOT 自定义根目录。
62
+
63
+ Returns:
64
+ dict: 键为备份文件中的目录名称,值为目录的绝对路径
65
+ """
66
+ return {
67
+ "plugins": get_astrbot_plugin_path(), # 插件本体
68
+ "plugin_data": get_astrbot_plugin_data_path(), # 插件数据
69
+ "config": get_astrbot_config_path(), # 配置目录
70
+ "t2i_templates": get_astrbot_t2i_templates_path(), # T2I 模板
71
+ "webchat": get_astrbot_webchat_path(), # WebChat 数据
72
+ "temp": get_astrbot_temp_path(), # 临时文件
73
+ }
74
+
75
+
76
+ # 备份清单版本号
77
+ BACKUP_MANIFEST_VERSION = "1.1"