diff --git a/agent/component/agent_with_tools.py b/agent/component/agent_with_tools.py
index 56f23afe35..d59d8eb804 100644
--- a/agent/component/agent_with_tools.py
+++ b/agent/component/agent_with_tools.py
@@ -145,7 +145,8 @@ class Agent(LLM, ToolBase):
self._param.function_name = self._id.split("-->")[-1]
m = super().get_meta()
if hasattr(self._param, "user_prompt") and self._param.user_prompt:
- m["function"]["parameters"]["properties"]["user_prompt"] = self._param.user_prompt
+ # Keep the JSON schema valid; user_prompt is a string field, not a schema node.
+ m["function"]["parameters"]["properties"]["user_prompt"]["default"] = self._param.user_prompt
return m
def get_input_form(self) -> dict[str, dict]:
diff --git a/agent/tools/base.py b/agent/tools/base.py
index f5a42de4d1..194b47fcee 100644
--- a/agent/tools/base.py
+++ b/agent/tools/base.py
@@ -67,6 +67,19 @@ class LLMToolPluginCallSession(ToolCallSession):
else:
resp = await thread_pool_exec(tool_obj.invoke, **arguments)
+ if resp is None and hasattr(tool_obj, "output") and callable(tool_obj.output):
+ try:
+ fallback_output = tool_obj.output()
+ if isinstance(fallback_output, dict) and fallback_output.get("content") not in (None, ""):
+ resp = fallback_output["content"]
+ elif fallback_output not in (None, ""):
+ resp = fallback_output
+ else:
+ resp = fallback_output
+ logging.warning(f"[ToolCall] resp is None, fallback to output name={name} output_keys={list(fallback_output.keys()) if isinstance(fallback_output, dict) else type(fallback_output).__name__}")
+ except Exception as e:
+ logging.warning(f"[ToolCall] resp is None and output fallback failed name={name} err={e}")
+
elapsed = timer() - st
logging.info(f"[ToolCall] done name={name} elapsed={elapsed:.2f}s result={str(resp)[:200]}")
self.callback(name, arguments, resp, elapsed_time=elapsed)
diff --git a/rag/llm/chat_model.py b/rag/llm/chat_model.py
index a58e8450c0..3aa13d03d8 100644
--- a/rag/llm/chat_model.py
+++ b/rag/llm/chat_model.py
@@ -1322,6 +1322,9 @@ class LiteLLMBase(ABC):
gen_conf.pop("max_tokens", None)
return gen_conf
+ def _need_reasoning_content_back(self) -> bool:
+ return self.provider == SupportedLiteLLMProvider.DeepSeek
+
async def async_chat(self, system, history, gen_conf, **kwargs):
hist = list(history) if history else []
if system:
@@ -1456,23 +1459,24 @@ class LiteLLMBase(ABC):
def _verbose_tool_use(self, name, args, res):
return "" + json.dumps({"name": name, "args": args, "result": res}, ensure_ascii=False, indent=2) + ""
- def _append_history(self, hist, tool_call, tool_res):
- hist.append(
- {
- "role": "assistant",
- "tool_calls": [
- {
- "index": tool_call.index,
- "id": tool_call.id,
- "function": {
- "name": tool_call.function.name,
- "arguments": tool_call.function.arguments,
- },
- "type": "function",
+ def _append_history(self, hist, tool_call, tool_res, reasoning_content=None):
+ assistant_msg = {
+ "role": "assistant",
+ "tool_calls": [
+ {
+ "index": tool_call.index,
+ "id": tool_call.id,
+ "function": {
+ "name": tool_call.function.name,
+ "arguments": tool_call.function.arguments,
},
- ],
- }
- )
+ "type": "function",
+ },
+ ],
+ }
+ if reasoning_content:
+ assistant_msg["reasoning_content"] = reasoning_content
+ hist.append(assistant_msg)
try:
if isinstance(tool_res, dict):
tool_res = json.dumps(tool_res, ensure_ascii=False)
@@ -1480,13 +1484,13 @@ class LiteLLMBase(ABC):
hist.append({"role": "tool", "tool_call_id": tool_call.id, "content": str(tool_res)})
return hist
- def _append_history_batch(self, hist, results):
+ def _append_history_batch(self, hist, results, reasoning_content=None):
"""
Append a batch of tool calls to history following the OpenAI protocol:
one assistant message containing all tool_calls, followed by one tool message per call.
results: list of (tool_call, name, args, result, error)
"""
- hist.append({
+ assistant_msg = {
"role": "assistant",
"tool_calls": [
{
@@ -1497,7 +1501,10 @@ class LiteLLMBase(ABC):
}
for tc, _, _, _, _ in results
],
- })
+ }
+ if reasoning_content:
+ assistant_msg["reasoning_content"] = reasoning_content
+ hist.append(assistant_msg)
for tc, _, _, result, err in results:
if err:
content = str(err)
@@ -1542,11 +1549,13 @@ class LiteLLMBase(ABC):
raise Exception(f"500 response structure error. Response: {response}")
message = response.choices[0].message
+ reasoning_content = None
+ if self._need_reasoning_content_back():
+ reasoning_content = getattr(message, "reasoning_content", None) or getattr(message, "reasoning", None)
if not hasattr(message, "tool_calls") or not message.tool_calls:
- _reasoning = getattr(message, "reasoning_content", None) or getattr(message, "reasoning", None)
- if _reasoning:
- ans += f"{_reasoning}"
+ if reasoning_content:
+ ans += f"{reasoning_content}"
ans += message.content or ""
if response.choices[0].finish_reason == "length":
ans = self._length_stop(ans)
@@ -1567,7 +1576,11 @@ class LiteLLMBase(ABC):
logging.info(f"Response tool_calls={message.tool_calls}")
results = await asyncio.gather(*[_exec_tool(tc) for tc in message.tool_calls])
- history = self._append_history_batch(history, results)
+ history = self._append_history_batch(
+ history,
+ results,
+ reasoning_content=reasoning_content if self._need_reasoning_content_back() else None,
+ )
for tc, name, args, result, err in results:
ans += self._verbose_tool_use(name, args, err if err else result)
@@ -1600,6 +1613,7 @@ class LiteLLMBase(ABC):
try:
for _round in range(self.max_rounds + 1):
reasoning_start = False
+ reasoning_content = ""
logging.info(f"[ToolLoop] round={_round} model={self.model_name} tools={[t['function']['name'] for t in tools]}")
completion_args = self._construct_completion_args(history=history, stream=True, tools=True, **gen_conf)
@@ -1634,6 +1648,8 @@ class LiteLLMBase(ABC):
_reasoning = getattr(delta, "reasoning_content", None) or getattr(delta, "reasoning", None)
if _reasoning:
+ if self._need_reasoning_content_back():
+ reasoning_content += _reasoning
ans = ""
if not reasoning_start:
reasoning_start = True
@@ -1682,7 +1698,11 @@ class LiteLLMBase(ABC):
args = {}
yield self._verbose_tool_use(tc.function.name, args, "Begin to call...")
results = await asyncio.gather(*[_exec_tool(tc) for tc in tcs])
- history = self._append_history_batch(history, results)
+ history = self._append_history_batch(
+ history,
+ results,
+ reasoning_content=reasoning_content if self._need_reasoning_content_back() else None,
+ )
for tc, name, args, result, err in results:
yield self._verbose_tool_use(name, args, err if err else result)