Skip to content

Commit 80fd511

Browse files
authored
feat: add support for showing tool call results in agent execution (AstrBotDevs#5388)
closes: AstrBotDevs#5329
1 parent 5af5ad9 commit 80fd511

5 files changed

Lines changed: 117 additions & 16 deletions

File tree

astrbot/core/astr_agent_run_util.py

Lines changed: 92 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,77 @@ def _should_stop_agent(astr_event) -> bool:
2424
return astr_event.is_stopped() or bool(astr_event.get_extra("agent_stop_requested"))
2525

2626

27+
def _truncate_tool_result(text: str, limit: int = 70) -> str:
28+
if limit <= 0:
29+
return ""
30+
if len(text) <= limit:
31+
return text
32+
if limit <= 3:
33+
return text[:limit]
34+
return f"{text[: limit - 3]}..."
35+
36+
37+
def _extract_chain_json_data(msg_chain: MessageChain) -> dict | None:
38+
if not msg_chain.chain:
39+
return None
40+
first_comp = msg_chain.chain[0]
41+
if isinstance(first_comp, Json) and isinstance(first_comp.data, dict):
42+
return first_comp.data
43+
return None
44+
45+
46+
def _record_tool_call_name(
47+
tool_info: dict | None, tool_name_by_call_id: dict[str, str]
48+
) -> None:
49+
if not isinstance(tool_info, dict):
50+
return
51+
tool_call_id = tool_info.get("id")
52+
tool_name = tool_info.get("name")
53+
if tool_call_id is None or tool_name is None:
54+
return
55+
tool_name_by_call_id[str(tool_call_id)] = str(tool_name)
56+
57+
58+
def _build_tool_call_status_message(tool_info: dict | None) -> str:
59+
if tool_info:
60+
return f"🔨 调用工具: {tool_info.get('name', 'unknown')}"
61+
return "🔨 调用工具..."
62+
63+
64+
def _build_tool_result_status_message(
65+
msg_chain: MessageChain, tool_name_by_call_id: dict[str, str]
66+
) -> str:
67+
tool_name = "unknown"
68+
tool_result = ""
69+
70+
result_data = _extract_chain_json_data(msg_chain)
71+
if result_data:
72+
tool_call_id = result_data.get("id")
73+
if tool_call_id is not None:
74+
tool_name = tool_name_by_call_id.pop(str(tool_call_id), "unknown")
75+
tool_result = str(result_data.get("result", ""))
76+
77+
if not tool_result:
78+
tool_result = msg_chain.get_plain_text(with_other_comps_mark=True)
79+
tool_result = _truncate_tool_result(tool_result, 70)
80+
81+
status_msg = f"🔨 调用工具: {tool_name}"
82+
if tool_result:
83+
status_msg = f"{status_msg}\n📎 返回结果: {tool_result}"
84+
return status_msg
85+
86+
2787
async def run_agent(
2888
agent_runner: AgentRunner,
2989
max_step: int = 30,
3090
show_tool_use: bool = True,
91+
show_tool_call_result: bool = False,
3192
stream_to_general: bool = False,
3293
show_reasoning: bool = False,
3394
) -> AsyncGenerator[MessageChain | None, None]:
3495
step_idx = 0
3596
astr_event = agent_runner.run_context.context.event
97+
tool_name_by_call_id: dict[str, str] = {}
3698
while step_idx < max_step + 1:
3799
step_idx += 1
38100

@@ -90,32 +152,36 @@ async def run_agent(
90152
continue
91153
if astr_event.get_platform_id() == "webchat":
92154
await astr_event.send(msg_chain)
155+
elif show_tool_use and show_tool_call_result:
156+
status_msg = _build_tool_result_status_message(
157+
msg_chain, tool_name_by_call_id
158+
)
159+
await astr_event.send(
160+
MessageChain(type="tool_call").message(status_msg)
161+
)
93162
# 对于其他情况,暂时先不处理
94163
continue
95164
elif resp.type == "tool_call":
96165
if agent_runner.streaming:
97166
# 用来标记流式响应需要分节
98167
yield MessageChain(chain=[], type="break")
99168

100-
tool_info = None
101-
102-
if resp.data["chain"].chain:
103-
json_comp = resp.data["chain"].chain[0]
104-
if isinstance(json_comp, Json):
105-
tool_info = json_comp.data
106-
astr_event.trace.record(
107-
"agent_tool_call",
108-
tool_name=tool_info if tool_info else "unknown",
109-
)
169+
tool_info = _extract_chain_json_data(resp.data["chain"])
170+
astr_event.trace.record(
171+
"agent_tool_call",
172+
tool_name=tool_info if tool_info else "unknown",
173+
)
174+
_record_tool_call_name(tool_info, tool_name_by_call_id)
110175

111176
if astr_event.get_platform_name() == "webchat":
112177
await astr_event.send(resp.data["chain"])
113178
elif show_tool_use:
114-
if tool_info:
115-
m = f"🔨 调用工具: {tool_info.get('name', 'unknown')}"
116-
else:
117-
m = "🔨 调用工具..."
118-
chain = MessageChain(type="tool_call").message(m)
179+
if show_tool_call_result and isinstance(tool_info, dict):
180+
# Delay tool status notification until tool_call_result.
181+
continue
182+
chain = MessageChain(type="tool_call").message(
183+
_build_tool_call_status_message(tool_info)
184+
)
119185
await astr_event.send(chain)
120186
continue
121187

@@ -202,6 +268,7 @@ async def run_live_agent(
202268
tts_provider: TTSProvider | None = None,
203269
max_step: int = 30,
204270
show_tool_use: bool = True,
271+
show_tool_call_result: bool = False,
205272
show_reasoning: bool = False,
206273
) -> AsyncGenerator[MessageChain | None, None]:
207274
"""Live Mode 的 Agent 运行器,支持流式 TTS
@@ -211,6 +278,7 @@ async def run_live_agent(
211278
tts_provider: TTS Provider 实例
212279
max_step: 最大步数
213280
show_tool_use: 是否显示工具使用
281+
show_tool_call_result: 是否显示工具返回结果
214282
show_reasoning: 是否显示推理过程
215283
216284
Yields:
@@ -222,6 +290,7 @@ async def run_live_agent(
222290
agent_runner,
223291
max_step=max_step,
224292
show_tool_use=show_tool_use,
293+
show_tool_call_result=show_tool_call_result,
225294
stream_to_general=False,
226295
show_reasoning=show_reasoning,
227296
):
@@ -250,7 +319,12 @@ async def run_live_agent(
250319
# 1. 启动 Agent Feeder 任务:负责运行 Agent 并将文本分句喂给 text_queue
251320
feeder_task = asyncio.create_task(
252321
_run_agent_feeder(
253-
agent_runner, text_queue, max_step, show_tool_use, show_reasoning
322+
agent_runner,
323+
text_queue,
324+
max_step,
325+
show_tool_use,
326+
show_tool_call_result,
327+
show_reasoning,
254328
)
255329
)
256330

@@ -336,6 +410,7 @@ async def _run_agent_feeder(
336410
text_queue: asyncio.Queue,
337411
max_step: int,
338412
show_tool_use: bool,
413+
show_tool_call_result: bool,
339414
show_reasoning: bool,
340415
) -> None:
341416
"""运行 Agent 并将文本输出分句放入队列"""
@@ -345,6 +420,7 @@ async def _run_agent_feeder(
345420
agent_runner,
346421
max_step=max_step,
347422
show_tool_use=show_tool_use,
423+
show_tool_call_result=show_tool_call_result,
348424
stream_to_general=False,
349425
show_reasoning=show_reasoning,
350426
):

astrbot/core/config/default.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@
100100
"dequeue_context_length": 1,
101101
"streaming_response": False,
102102
"show_tool_use_status": False,
103+
"show_tool_call_result": False,
103104
"sanitize_context_by_modalities": False,
104105
"max_quoted_fallback_images": 20,
105106
"quoted_message_parser": {
@@ -2306,6 +2307,9 @@ class ChatProviderTemplate(TypedDict):
23062307
"show_tool_use_status": {
23072308
"type": "bool",
23082309
},
2310+
"show_tool_call_result": {
2311+
"type": "bool",
2312+
},
23092313
"unsupported_streaming_strategy": {
23102314
"type": "string",
23112315
},
@@ -2994,6 +2998,15 @@ class ChatProviderTemplate(TypedDict):
29942998
"provider_settings.agent_runner_type": "local",
29952999
},
29963000
},
3001+
"provider_settings.show_tool_call_result": {
3002+
"description": "输出函数调用返回结果",
3003+
"type": "bool",
3004+
"hint": "仅在输出函数调用状态启用时生效,展示结果前 70 个字符。",
3005+
"condition": {
3006+
"provider_settings.agent_runner_type": "local",
3007+
"provider_settings.show_tool_use_status": True,
3008+
},
3009+
},
29973010
"provider_settings.sanitize_context_by_modalities": {
29983011
"description": "按模型能力清理历史上下文",
29993012
"type": "bool",

astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ async def initialize(self, ctx: PipelineContext) -> None:
5454
if isinstance(self.max_step, bool): # workaround: #2622
5555
self.max_step = 30
5656
self.show_tool_use: bool = settings.get("show_tool_use_status", True)
57+
self.show_tool_call_result: bool = settings.get("show_tool_call_result", False)
5758
self.show_reasoning = settings.get("display_reasoning_text", False)
5859
self.sanitize_context_by_modalities: bool = settings.get(
5960
"sanitize_context_by_modalities",
@@ -240,6 +241,7 @@ async def process(
240241
tts_provider,
241242
self.max_step,
242243
self.show_tool_use,
244+
self.show_tool_call_result,
243245
show_reasoning=self.show_reasoning,
244246
),
245247
),
@@ -269,6 +271,7 @@ async def process(
269271
agent_runner,
270272
self.max_step,
271273
self.show_tool_use,
274+
self.show_tool_call_result,
272275
show_reasoning=self.show_reasoning,
273276
),
274277
),
@@ -297,6 +300,7 @@ async def process(
297300
agent_runner,
298301
self.max_step,
299302
self.show_tool_use,
303+
self.show_tool_call_result,
300304
stream_to_general,
301305
show_reasoning=self.show_reasoning,
302306
):

dashboard/src/i18n/locales/en-US/features/config-metadata.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,10 @@
251251
"show_tool_use_status": {
252252
"description": "Output Function Call Status"
253253
},
254+
"show_tool_call_result": {
255+
"description": "Output Tool Call Results",
256+
"hint": "Only takes effect when \"Output Function Call Status\" is enabled, and shows at most 70 characters."
257+
},
254258
"sanitize_context_by_modalities": {
255259
"description": "Sanitize History by Modalities",
256260
"hint": "When enabled, sanitizes contexts before each LLM request by removing image blocks and tool-call structures that the current provider's modalities do not support (this changes what the model sees)."

dashboard/src/i18n/locales/zh-CN/features/config-metadata.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,10 @@
254254
"show_tool_use_status": {
255255
"description": "输出函数调用状态"
256256
},
257+
"show_tool_call_result": {
258+
"description": "输出函数调用返回结果",
259+
"hint": "仅在启用“输出函数调用状态”时生效,且最多展示 70 个字符。"
260+
},
257261
"sanitize_context_by_modalities": {
258262
"description": "按模型能力清理历史上下文",
259263
"hint": "开启后,在每次请求 LLM 前会按当前模型提供商中所选择的模型能力删除对话中不支持的图片/工具调用结构(会改变模型看到的历史)"

0 commit comments

Comments
 (0)