mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-06-29 23:41:12 +08:00
Fix: agent toolcall null response & schema validation & DeepSeek think history (#14425)
### What problem does this PR solve? agent toolcall null response & schema validation & DeepSeek think history ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue)
This commit is contained in:
@@ -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]:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 "<tool_call>" + json.dumps({"name": name, "args": args, "result": res}, ensure_ascii=False, indent=2) + "</tool_call>"
|
||||
|
||||
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"<think>{_reasoning}</think>"
|
||||
if reasoning_content:
|
||||
ans += f"<think>{reasoning_content}</think>"
|
||||
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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user