Skip to content

Commit d3cee00

Browse files
committed
merge
1 parent 03deaac commit d3cee00

7 files changed

Lines changed: 528 additions & 193 deletions

File tree

astrbot/core/agent/runners/tool_loop_agent_runner.py

Lines changed: 116 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,18 @@
1616
TextContent,
1717
TextResourceContents,
1818
)
19+
from tenacity import (
20+
AsyncRetrying,
21+
retry_if_exception_type,
22+
stop_after_attempt,
23+
wait_exponential,
24+
)
1925

2026
from astrbot import logger
2127
from astrbot.core.agent.message import ImageURLPart, TextPart, ThinkPart
2228
from astrbot.core.agent.tool import ToolSet
2329
from astrbot.core.agent.tool_image_cache import tool_image_cache
30+
from astrbot.core.exceptions import EmptyModelOutputError
2431
from astrbot.core.message.components import Json
2532
from astrbot.core.message.message_event_result import (
2633
MessageChain,
@@ -150,6 +157,32 @@ def _get_persona_custom_error_message(self) -> str | None:
150157
event = getattr(self.run_context.context, "event", None)
151158
return extract_persona_custom_error_message_from_event(event)
152159

160+
async def _complete_with_assistant_response(self, llm_resp: LLMResponse) -> None:
161+
"""Finalize the current step as a plain assistant response with no tool calls."""
162+
self.final_llm_resp = llm_resp
163+
self._transition_state(AgentState.DONE)
164+
self.stats.end_time = time.time()
165+
166+
parts = []
167+
if llm_resp.reasoning_content or llm_resp.reasoning_signature:
168+
parts.append(
169+
ThinkPart(
170+
think=llm_resp.reasoning_content,
171+
encrypted=llm_resp.reasoning_signature,
172+
)
173+
)
174+
if llm_resp.completion_text:
175+
parts.append(TextPart(text=llm_resp.completion_text))
176+
if len(parts) == 0:
177+
logger.warning("LLM returned empty assistant message with no tool calls.")
178+
self.run_context.messages.append(Message(role="assistant", content=parts))
179+
180+
try:
181+
await self.agent_hooks.on_agent_done(self.run_context, llm_resp)
182+
except Exception as e:
183+
logger.error(f"Error in on_agent_done hook: {e}", exc_info=True)
184+
self._resolve_unconsumed_follow_ups()
185+
153186
@override
154187
async def reset(
155188
self,
@@ -305,31 +338,61 @@ async def _iter_llm_responses_with_fallback(
305338
candidate_id,
306339
)
307340
self.provider = candidate
308-
has_stream_output = False
309341
try:
310-
async for resp in self._iter_llm_responses(include_model=idx == 0):
311-
if resp.is_chunk:
312-
has_stream_output = True
313-
yield resp
314-
continue
315-
316-
if (
317-
resp.role == "err"
318-
and not has_stream_output
319-
and (not is_last_candidate)
320-
):
321-
last_err_response = resp
322-
logger.warning(
323-
"Chat Model %s returns error response, trying fallback to next provider.",
324-
candidate_id,
325-
)
326-
break
327-
328-
yield resp
329-
return
342+
retrying = AsyncRetrying(
343+
retry=retry_if_exception_type(EmptyModelOutputError),
344+
stop=stop_after_attempt(self.EMPTY_OUTPUT_RETRY_ATTEMPTS),
345+
wait=wait_exponential(
346+
multiplier=1,
347+
min=self.EMPTY_OUTPUT_RETRY_WAIT_MIN_S,
348+
max=self.EMPTY_OUTPUT_RETRY_WAIT_MAX_S,
349+
),
350+
reraise=True,
351+
)
330352

331-
if has_stream_output:
332-
return
353+
async for attempt in retrying:
354+
has_stream_output = False
355+
with attempt:
356+
try:
357+
async for resp in self._iter_llm_responses(
358+
include_model=idx == 0
359+
):
360+
if resp.is_chunk:
361+
has_stream_output = True
362+
yield resp
363+
continue
364+
365+
if (
366+
resp.role == "err"
367+
and not has_stream_output
368+
and (not is_last_candidate)
369+
):
370+
last_err_response = resp
371+
logger.warning(
372+
"Chat Model %s returns error response, trying fallback to next provider.",
373+
candidate_id,
374+
)
375+
break
376+
377+
yield resp
378+
return
379+
380+
if has_stream_output:
381+
return
382+
except EmptyModelOutputError:
383+
if has_stream_output:
384+
logger.warning(
385+
"Chat Model %s returned empty output after streaming started; skipping empty-output retry.",
386+
candidate_id,
387+
)
388+
else:
389+
logger.warning(
390+
"Chat Model %s returned empty output on attempt %s/%s.",
391+
candidate_id,
392+
attempt.retry_state.attempt_number,
393+
self.EMPTY_OUTPUT_RETRY_ATTEMPTS,
394+
)
395+
raise
333396
except Exception as exc: # noqa: BLE001
334397
last_exception = exc
335398
logger.warning(
@@ -540,35 +603,7 @@ async def step(self):
540603
return
541604

542605
if not llm_resp.tools_call_name:
543-
# 如果没有工具调用,转换到完成状态
544-
self.final_llm_resp = llm_resp
545-
self._transition_state(AgentState.DONE)
546-
self.stats.end_time = time.time()
547-
548-
# record the final assistant message
549-
parts = []
550-
551-
if llm_resp.reasoning_content or llm_resp.reasoning_signature:
552-
parts.append(
553-
ThinkPart(
554-
think=llm_resp.reasoning_content,
555-
encrypted=llm_resp.reasoning_signature,
556-
)
557-
)
558-
if llm_resp.completion_text:
559-
parts.append(TextPart(text=llm_resp.completion_text))
560-
if len(parts) == 0:
561-
logger.warning(
562-
"LLM returned empty assistant message with no tool calls."
563-
)
564-
self.run_context.messages.append(Message(role="assistant", content=parts))
565-
566-
# call the on_agent_done hook
567-
try:
568-
await self.agent_hooks.on_agent_done(self.run_context, llm_resp)
569-
except Exception as e:
570-
logger.error(f"Error in on_agent_done hook: {e}", exc_info=True)
571-
self._resolve_unconsumed_follow_ups()
606+
await self._complete_with_assistant_response(llm_resp)
572607

573608
# 返回 LLM 结果
574609
if llm_resp.result_chain:
@@ -588,6 +623,24 @@ async def step(self):
588623
if llm_resp.tools_call_name:
589624
if self.tool_schema_mode == "skills_like":
590625
llm_resp, _ = await self._resolve_tool_exec(llm_resp)
626+
if not llm_resp.tools_call_name:
627+
logger.warning(
628+
"skills_like tool re-query returned no tool calls; fallback to assistant response."
629+
)
630+
if llm_resp.result_chain:
631+
yield AgentResponse(
632+
type="llm_result",
633+
data=AgentResponseData(chain=llm_resp.result_chain),
634+
)
635+
elif llm_resp.completion_text:
636+
yield AgentResponse(
637+
type="llm_result",
638+
data=AgentResponseData(
639+
chain=MessageChain().message(llm_resp.completion_text),
640+
),
641+
)
642+
await self._complete_with_assistant_response(llm_resp)
643+
return
591644

592645
tool_call_result_blocks = []
593646
cached_images = [] # Collect cached images for LLM visibility
@@ -1040,7 +1093,9 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None:
10401093
)
10411094

10421095
def _build_tool_requery_context(
1043-
self, tool_names: list[str]
1096+
self,
1097+
tool_names: list[str],
1098+
extra_instruction: str | None = None,
10441099
) -> list[dict[str, T.Any]]:
10451100
"""Build contexts for re-querying LLM with param-only tool schemas."""
10461101
contexts: list[dict[str, T.Any]] = []
@@ -1052,13 +1107,20 @@ def _build_tool_requery_context(
10521107
instruction = self.SKILLS_LIKE_REQUERY_INSTRUCTION_TEMPLATE.format(
10531108
tool_names=", ".join(tool_names)
10541109
)
1110+
if extra_instruction:
1111+
instruction = f"{instruction}\n{extra_instruction}"
10551112
if contexts and contexts[0].get("role") == "system":
10561113
content = contexts[0].get("content") or ""
10571114
contexts[0]["content"] = f"{content}\n{instruction}"
10581115
else:
10591116
contexts.insert(0, {"role": "system", "content": instruction})
10601117
return contexts
10611118

1119+
@staticmethod
1120+
def _has_meaningful_assistant_reply(llm_resp: LLMResponse) -> bool:
1121+
text = (llm_resp.completion_text or "").strip()
1122+
return bool(text)
1123+
10621124
def _build_tool_subset(self, tool_set: ToolSet, tool_names: list[str]) -> ToolSet:
10631125
"""Build a subset of tools from the given tool set based on tool names."""
10641126
subset = ToolSet()
@@ -1096,6 +1158,7 @@ async def _resolve_tool_exec(
10961158
model=self.req.model,
10971159
session_id=self.req.session_id,
10981160
extra_user_content_parts=self.req.extra_user_content_parts,
1161+
tool_choice="required",
10991162
abort_signal=self._abort_signal,
11001163
)
11011164
if requery_resp:

astrbot/core/astr_agent_tool_exec.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import asyncio
2-
import datetime
32
import inspect
43
import json
54
import time
@@ -352,6 +351,7 @@ async def _execute_handoff(
352351

353352
# 构建子代理的 system_prompt,添加 skills 提示词和公共上下文
354353
subagent_system_prompt = tool.agent.instructions or ""
354+
subagent_system_prompt = f"# Role\nYour name is {agent_name}(used for tool calling)\n{subagent_system_prompt}\n"
355355
if agent_name:
356356
try:
357357
from astrbot.core.dynamic_subagent_manager import DynamicSubAgentManager
@@ -362,7 +362,7 @@ async def _execute_handoff(
362362
umo, agent_name, runtime
363363
)
364364
if skills_prompt:
365-
subagent_system_prompt += f"\n\n# Available Skills\n{skills_prompt}"
365+
subagent_system_prompt += f"{skills_prompt}" + "\n"
366366
logger.info(f"[SubAgentSkills] Injected skills for {agent_name}")
367367

368368
# 注入公共上下文
@@ -376,10 +376,18 @@ async def _execute_handoff(
376376
)
377377

378378
# 注入时间信息
379-
current_time = (
380-
datetime.datetime.now().astimezone().strftime("%Y-%m-%d %H:%M (%Z)")
379+
time_prompt = DynamicSubAgentManager.build_time_prompt(umo)
380+
subagent_system_prompt += time_prompt
381+
382+
# 注入工作目录
383+
workdir_prompt = DynamicSubAgentManager.build_workdir_prompt(
384+
umo, agent_name
381385
)
382-
subagent_system_prompt += f"Current datetime: {current_time}"
386+
subagent_system_prompt += workdir_prompt
387+
388+
# 注入行为规范
389+
rule_prompt = DynamicSubAgentManager.build_rule_prompt(umo)
390+
subagent_system_prompt += rule_prompt
383391

384392
except Exception:
385393
pass

astrbot/core/astr_main_agent.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,6 +1042,7 @@ def _apply_enhanced_subagent_tools(
10421042
dynamic_subagent_prompt = DynamicSubAgentManager.build_dynamic_subagent_prompt(
10431043
session_id
10441044
)
1045+
10451046
req.system_prompt = f"{req.system_prompt or ''}\n{dynamic_subagent_prompt}\n"
10461047
# Register existing handoff tools from config
10471048
plugin_context = getattr(event, "_plugin_context", None)

astrbot/core/computer/booters/local.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,16 @@ def _run() -> dict[str, Any]:
159159
[os.environ.get("PYTHON", sys.executable), "-c", code],
160160
timeout=timeout,
161161
capture_output=True,
162-
text=True,
162+
# text=True,
163+
)
164+
# stdout = "" if silent else result.stdout
165+
# stderr = result.stderr if result.returncode != 0 else ""
166+
stdout = "" if silent else _decode_shell_output(result.stdout)
167+
stderr = (
168+
_decode_shell_output(result.stderr)
169+
if result.returncode != 0
170+
else ""
163171
)
164-
stdout = "" if silent else result.stdout
165-
stderr = result.stderr if result.returncode != 0 else ""
166172
return {
167173
"data": {
168174
"output": {"text": stdout, "images": []},

astrbot/core/config/default.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,6 @@
195195
),
196196
"agents": [],
197197
},
198-
# 增强版动态SubAgent配置(独立于subagent_orchestrator)
199198
"enhanced_subagent": {
200199
"enabled": False,
201200
"log_level": "debug",
@@ -2537,17 +2536,17 @@ class ChatProviderTemplate(TypedDict):
25372536
"mimo-tts-style-prompt": {
25382537
"description": "风格提示词",
25392538
"type": "string",
2540-
"hint": "用于控制生成语音的说话风格、语气或情绪,例如温柔、活泼、沉稳等。可留空。",
2539+
"hint": "会以 <style>...</style> 标签形式添加到待合成文本开头,用于控制语速、情绪、角色或风格,例如 开心、变快、孙悟空、悄悄话。可留空。",
25412540
},
25422541
"mimo-tts-dialect": {
25432542
"description": "方言",
25442543
"type": "string",
2545-
"hint": "指定生成语音时使用的方言或口音,例如四川话、粤语口音等。可留空。",
2544+
"hint": "会与风格提示词一起写入开头的 <style>...</style> 标签中,例如 东北话、四川话、河南话、粤语。可留空。",
25462545
},
25472546
"mimo-tts-seed-text": {
25482547
"description": "种子文本",
25492548
"type": "string",
2550-
"hint": "用于引导音色和说话方式的参考文本,会影响生成语音的表达风格。",
2549+
"hint": "作为可选的 user 消息发送,用于辅助调节语气和风格,不会拼接到待合成文本中。",
25512550
},
25522551
"fishaudio-tts-character": {
25532552
"description": "character",

0 commit comments

Comments
 (0)