Skip to content

Commit 13aa8a8

Browse files
authored
Merge branch 'AstrBotDevs:master' into cmd_suggestion
2 parents 59ddcf4 + dcc99e6 commit 13aa8a8

27 files changed

Lines changed: 293 additions & 161 deletions

File tree

astrbot/core/agent/runners/tool_loop_agent_runner.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -816,8 +816,9 @@ async def step(self):
816816
# 如果有工具调用,还需处理工具调用
817817
if llm_resp.tools_call_name:
818818
if self.tool_schema_mode == "skills_like":
819-
llm_resp, _ = await self._resolve_tool_exec(llm_resp)
820-
if not llm_resp.tools_call_name:
819+
requery_resp, _ = await self._resolve_tool_exec(llm_resp)
820+
if not requery_resp.tools_call_name:
821+
llm_resp = requery_resp
821822
logger.warning(
822823
"skills_like tool re-query returned no tool calls; fallback to assistant response."
823824
)
@@ -845,6 +846,10 @@ async def step(self):
845846

846847
await self._complete_with_assistant_response(llm_resp)
847848
return
849+
else:
850+
llm_resp.tools_call_name = requery_resp.tools_call_name
851+
llm_resp.tools_call_args = requery_resp.tools_call_args
852+
llm_resp.tools_call_ids = requery_resp.tools_call_ids
848853

849854
tool_call_result_blocks = []
850855
cached_images = [] # Collect cached images for LLM visibility

astrbot/core/provider/sources/openai_source.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -669,10 +669,14 @@ async def _query_stream(
669669
# Gemini and some OpenAI-compatible proxies omit this field
670670
if not hasattr(tc, "index") or tc.index is None:
671671
tc.index = idx
672-
try:
673-
state.handle_chunk(chunk)
674-
except Exception as e:
675-
logger.error("Saving chunk state error: " + str(e))
672+
# 跳过 delta=None 的 chunk,避免 SDK 内部 _convert_initial_chunk_into_snapshot
673+
# 第 747 行 choice.delta.to_dict() 抛出 NoneType 错误。
674+
# refs: AstrBot#6689 / openai-python#5069 / #5047
675+
if delta is not None:
676+
try:
677+
state.handle_chunk(chunk)
678+
except Exception as e:
679+
logger.error("Saving chunk state error: " + str(e))
676680
# logger.debug(f"chunk delta: {delta}")
677681
# handle the content delta
678682
reasoning = self._extract_reasoning_content(chunk)
@@ -700,10 +704,14 @@ async def _query_stream(
700704
if _y:
701705
yield llm_response
702706

703-
final_completion = state.get_final_completion()
704-
llm_response = await self._parse_openai_completion(final_completion, tools)
705-
706-
yield llm_response
707+
try:
708+
final_completion = state.get_final_completion()
709+
llm_response = await self._parse_openai_completion(final_completion, tools)
710+
yield llm_response
711+
except Exception as e:
712+
logger.error("get_final_completion error: " + str(e))
713+
# 流式内容已通过 yield 发出,记录错误后正常结束即可
714+
return
707715

708716
def _extract_reasoning_content(
709717
self,

astrbot/core/utils/t2i/network_strategy.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,11 @@ async def render_custom_template(
156156
if options:
157157
default_options |= options
158158

159-
if SHIKI_RUNTIME_TEMPLATE_PATTERN.search(tmpl_str):
160-
tmpl_data = {"shiki_runtime": get_shiki_runtime()} | tmpl_data
161-
tmpl_str = inject_shiki_runtime(tmpl_str)
159+
# 在线程池中执行 Shiki 注入,避免 1.2MB JS 处理阻塞事件循环
160+
loop = asyncio.get_running_loop()
161+
tmpl_str, tmpl_data = await loop.run_in_executor(
162+
None, self._prepare_template_sync, tmpl_str, tmpl_data
163+
)
162164
post_data = {
163165
"tmpl": tmpl_str,
164166
"json": return_url,
@@ -219,3 +221,11 @@ async def render(
219221
},
220222
return_url,
221223
)
224+
225+
@staticmethod
226+
def _prepare_template_sync(tmpl_str: str, tmpl_data: dict) -> tuple[str, dict]:
227+
"""在线程池中执行的同步模板预处理(避免阻塞事件循环)"""
228+
if SHIKI_RUNTIME_TEMPLATE_PATTERN.search(tmpl_str):
229+
tmpl_data = {"shiki_runtime": get_shiki_runtime()} | tmpl_data
230+
tmpl_str = inject_shiki_runtime(tmpl_str)
231+
return tmpl_str, tmpl_data

docs/.vitepress/config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ export default defineConfig({
287287
collapsed: false,
288288
items: [
289289
{ text: "Package Manager", link: "/astrbot/package" },
290+
{ text: "Desktop Client", link: "/astrbot/desktop" },
290291
{ text: "One-click Launcher", link: "/astrbot/launcher" },
291292
{ text: "Docker", link: "/astrbot/docker" },
292293
{ text: "Kubernetes", link: "/astrbot/kubernetes" },

docs/en/deploy/astrbot/desktop.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Deploy with AstrBot Desktop Client
2+
3+
`AstrBot-desktop` is designed for quick local deployment of AstrBot on your personal computer, supporting Windows, macOS, and Linux.
4+
5+
Among the various deployment options, the desktop client is best suited for personal local use. It is not recommended for long-term server operation or production environments. For production deployments, consider [Docker](/en/deploy/astrbot/docker) or [Kubernetes](/en/deploy/astrbot/kubernetes) instead.
6+
7+
Compared to command-line or container-based solutions, the desktop client offers an out-of-the-box experience, ideal for users who want to get started without dealing with environment setup.
8+
9+
Repository: [AstrBotDevs/AstrBot-desktop](https://github.com/AstrBotDevs/AstrBot-desktop)
10+
11+
## Who Is It For
12+
13+
- Users who want quick local deployment with a graphical interface.
14+
- Beginners who don't want to manually manage Docker / Python environments.
15+
- Personal devices that stay online, primarily for individual or small team daily use.
16+
17+
## Key Features
18+
19+
- Multi-platform installers, ready to use after download.
20+
- GUI-based configuration, lowering the barrier for first-time deployment.
21+
- Suitable as a locally resident client.
22+
23+
## Download and Install
24+
25+
1. Open [AstrBot-desktop Releases](https://github.com/AstrBotDevs/AstrBot-desktop/releases).
26+
2. Download the installer for your operating system (e.g. `.exe`, `.dmg`, `.rpm`, `.deb`).
27+
3. Launch the desktop client after installation and follow the setup wizard to complete initialization.
28+
29+
## Difference from Launcher Deployment
30+
31+
- Desktop client: focuses on an out-of-the-box GUI experience.
32+
- Launcher deployment: focuses on automated script-based startup, suitable for users who prefer a traditional deployment workflow.
33+
- See [Launcher Deployment](/en/deploy/astrbot/launcher).

docs/en/dev/openapi.md

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,113 @@ X-API-Key: abk_xxx
2626
- `POST /api/v1/chat`: request body must include `username`
2727
- `GET /api/v1/chat/sessions`: query params must include `username`
2828

29+
## Scope Permissions
30+
31+
When creating an API Key, you can configure `scopes`. Each scope controls the range of accessible endpoints:
32+
33+
| Scope | Purpose | Accessible Endpoints |
34+
| --- | --- | --- |
35+
| `chat` | Access chat capabilities and query sessions | `POST /api/v1/chat`, `GET /api/v1/chat/sessions` |
36+
| `config` | Retrieve available config file list | `GET /api/v1/configs` |
37+
| `file` | Upload attachment files and get `attachment_id` | `POST /api/v1/file` |
38+
| `im` | Send proactive IM messages, query bot/platform list | `POST /api/v1/im/message`, `GET /api/v1/im/bots` |
39+
40+
If the API Key does not include the required scope for the target endpoint, the request will return `403 Insufficient API key scope`.
41+
2942
## Common Endpoints
3043

44+
**Chat**
45+
46+
Interact with AstrBot's built-in Agent. Supports plugin calls, tool calls, and other capabilities — consistent with IM-side chat.
47+
3148
- `POST /api/v1/chat`: send chat message (SSE stream, server generates UUID when `session_id` is omitted)
3249
- `GET /api/v1/chat/sessions`: list sessions for a specific `username` with pagination
3350
- `GET /api/v1/configs`: list available config files
51+
52+
**File Upload**
53+
3454
- `POST /api/v1/file`: upload attachment
35-
- `POST /api/v1/im/message`: proactive message via UMO
55+
56+
**Proactive IM Messages**
57+
58+
- `POST /api/v1/im/message`: send a proactive message via UMO
3659
- `GET /api/v1/im/bots`: list bot/platform IDs
3760

61+
## `message` Field Format (Important)
62+
63+
The `message` field in `POST /api/v1/chat` and `POST /api/v1/im/message` supports two formats:
64+
65+
1. String: plain text message
66+
2. Array: message segments (message chain)
67+
68+
### 1. Plain Text Format
69+
70+
```json
71+
{
72+
"message": "Hello"
73+
}
74+
```
75+
76+
### 2. Message Segment Array Format
77+
78+
```json
79+
{
80+
"message": [
81+
{ "type": "plain", "text": "Please see this file" },
82+
{ "type": "file", "attachment_id": "9a2f8c72-e7af-4c0e-b352-111111111111" }
83+
]
84+
}
85+
```
86+
87+
Supported `type` values:
88+
89+
| type | Required Fields | Optional Fields | Description |
90+
| --- | --- | --- | --- |
91+
| `plain` | `text` | - | Text segment |
92+
| `reply` | `message_id` | `selected_text` | Quote-reply a message |
93+
| `image` | `attachment_id` | - | Image attachment segment |
94+
| `record` | `attachment_id` | - | Audio attachment segment |
95+
| `file` | `attachment_id` | - | Generic file segment |
96+
| `video` | `attachment_id` | - | Video attachment segment |
97+
98+
* The `reply` segment is currently only supported for `/api/v1/chat`, not for `POST /api/v1/im/message`.
99+
100+
Notes:
101+
102+
- `attachment_id` comes from the upload result of `POST /api/v1/file`.
103+
- `reply` cannot be the only segment; at least one content segment (e.g. `plain/image/file/...`) is required.
104+
- A request with only `reply` or empty content will return an error.
105+
106+
### `message` Usage in Chat API
107+
108+
`POST /api/v1/chat` additionally requires `username`, with optional `session_id` (a UUID is auto-generated if omitted).
109+
110+
```json
111+
{
112+
"username": "alice",
113+
"session_id": "my_session_001",
114+
"message": [
115+
{ "type": "plain", "text": "Please summarize this PDF" },
116+
{ "type": "file", "attachment_id": "9a2f8c72-e7af-4c0e-b352-111111111111" }
117+
],
118+
"enable_streaming": true
119+
}
120+
```
121+
122+
### `message` Usage in IM Message API
123+
124+
`POST /api/v1/im/message` requires `umo` + `message`.
125+
126+
```json
127+
{
128+
"umo": "webchat:FriendMessage:openapi_probe",
129+
"message": [
130+
{ "type": "plain", "text": "This is a proactive message" },
131+
{ "type": "image", "attachment_id": "9a2f8c72-e7af-4c0e-b352-222222222222" }
132+
]
133+
}
134+
```
135+
38136
## Example
39137

40138
```bash

docs/en/dev/star/guides/ai.md

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -157,13 +157,21 @@ In the example below, we define a Main Agent responsible for delegating tasks to
157157
Define Tools:
158158

159159
```py
160+
from astrbot.api import logger
161+
from astrbot.core.agent.run_context import ContextWrapper
162+
from astrbot.core.agent.tool import FunctionTool, ToolExecResult, ToolSet
163+
from astrbot.core.astr_agent_context import AstrAgentContext
164+
from pydantic import Field
165+
from pydantic.dataclasses import dataclass
166+
167+
160168
@dataclass
161169
class AssignAgentTool(FunctionTool[AstrAgentContext]):
162170
"""Main agent uses this tool to decide which sub-agent to delegate a task to."""
163171

164172
name: str = "assign_agent"
165173
description: str = "Assign an agent to a task based on the given query"
166-
parameters: dict = field(
174+
parameters: dict = Field(
167175
default_factory=lambda: {
168176
"type": "object",
169177
"properties": {
@@ -178,7 +186,7 @@ class AssignAgentTool(FunctionTool[AstrAgentContext]):
178186

179187
async def call(
180188
self, context: ContextWrapper[AstrAgentContext], **kwargs
181-
) -> str | CallToolResult:
189+
) -> ToolExecResult:
182190
# Here you would implement the actual agent assignment logic.
183191
# For demonstration purposes, we'll return a dummy response.
184192
return "Based on the query, you should assign agent 1."
@@ -190,7 +198,7 @@ class WeatherTool(FunctionTool[AstrAgentContext]):
190198

191199
name: str = "weather"
192200
description: str = "Get weather information for a location"
193-
parameters: dict = field(
201+
parameters: dict = Field(
194202
default_factory=lambda: {
195203
"type": "object",
196204
"properties": {
@@ -205,7 +213,7 @@ class WeatherTool(FunctionTool[AstrAgentContext]):
205213

206214
async def call(
207215
self, context: ContextWrapper[AstrAgentContext], **kwargs
208-
) -> str | CallToolResult:
216+
) -> ToolExecResult:
209217
city = kwargs["city"]
210218
# Here you would implement the actual weather fetching logic.
211219
# For demonstration purposes, we'll return a dummy response.
@@ -218,7 +226,7 @@ class SubAgent1(FunctionTool[AstrAgentContext]):
218226

219227
name: str = "subagent1_name"
220228
description: str = "subagent1_description"
221-
parameters: dict = field(
229+
parameters: dict = Field(
222230
default_factory=lambda: {
223231
"type": "object",
224232
"properties": {
@@ -233,7 +241,7 @@ class SubAgent1(FunctionTool[AstrAgentContext]):
233241

234242
async def call(
235243
self, context: ContextWrapper[AstrAgentContext], **kwargs
236-
) -> str | CallToolResult:
244+
) -> ToolExecResult:
237245
ctx = context.context.context
238246
event = context.context.event
239247
logger.info(f"the llm context messages: {context.messages}")
@@ -255,7 +263,7 @@ class SubAgent2(FunctionTool[AstrAgentContext]):
255263

256264
name: str = "subagent2_name"
257265
description: str = "subagent2_description"
258-
parameters: dict = field(
266+
parameters: dict = Field(
259267
default_factory=lambda: {
260268
"type": "object",
261269
"properties": {
@@ -270,7 +278,7 @@ class SubAgent2(FunctionTool[AstrAgentContext]):
270278

271279
async def call(
272280
self, context: ContextWrapper[AstrAgentContext], **kwargs
273-
) -> str | CallToolResult:
281+
) -> ToolExecResult:
274282
return "I am useless :(, you shouldn't call me :("
275283
```
276284

@@ -335,6 +343,32 @@ class Conversation:
335343

336344
:::
337345

346+
### Quickly Adding LLM Records to a Conversation `add_message_pair`
347+
348+
```py
349+
from astrbot.core.agent.message import (
350+
AssistantMessageSegment,
351+
UserMessageSegment,
352+
TextPart,
353+
)
354+
355+
conv_mgr = self.context.conversation_manager
356+
provider_id = await self.context.get_current_chat_provider_id(event.unified_msg_origin)
357+
curr_cid = await conv_mgr.get_curr_conversation_id(event.unified_msg_origin)
358+
user_msg = UserMessageSegment(content=[TextPart(text="hi")])
359+
llm_resp = await self.context.llm_generate(
360+
chat_provider_id=provider_id, # Chat model ID
361+
contexts=[user_msg], # When prompt is not specified, contexts is used as input; if both prompt and contexts are provided, prompt is appended to the end of the LLM input
362+
)
363+
await conv_mgr.add_message_pair(
364+
cid=curr_cid,
365+
user_message=user_msg,
366+
assistant_message=AssistantMessageSegment(
367+
content=[TextPart(text=llm_resp.completion_text)]
368+
),
369+
)
370+
```
371+
338372
### Main Methods
339373

340374
#### `new_conversation`

0 commit comments

Comments
 (0)