Skip to content

Commit 9514406

Browse files
fix: prevent premature termination in Ollama sequential tool execution
- Added iteration count threshold (>=5) before generating Ollama tool summary - Prevents premature termination after first tool call in sequential execution - Maintains infinite loop prevention for truly stuck scenarios - Applied fix to both sync and async methods in llm.py - Preserves backward compatibility with other LLM providers Fixes issue where Ollama would terminate after first tool call instead of continuing with sequential execution (e.g., get_stock_price -> multiply) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Mervin Praison <[email protected]>
1 parent 536d98c commit 9514406

File tree

1 file changed

+76
-14
lines changed
  • src/praisonai-agents/praisonaiagents/llm

1 file changed

+76
-14
lines changed

src/praisonai-agents/praisonaiagents/llm/llm.py

Lines changed: 76 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -323,8 +323,8 @@ def _generate_ollama_tool_summary(self, tool_results: List[Any], response_text:
323323
if not (self._is_ollama_provider() and tool_results):
324324
return None
325325

326-
# If response is substantial, no summary needed
327-
if response_text and len(response_text.strip()) > OLLAMA_MIN_RESPONSE_LENGTH:
326+
# If response is a final answer, no summary needed
327+
if self._is_final_answer(response_text, False, tool_results):
328328
return None
329329

330330
# Build tool summary efficiently
@@ -349,6 +349,64 @@ def _format_ollama_tool_result_message(self, function_name: str, tool_result: An
349349
"content": f"The {function_name} function returned: {tool_result_str}"
350350
}
351351

352+
def _is_final_answer(self, response_text: str, has_tool_calls: bool, tool_results: List[Any]) -> bool:
353+
"""
354+
Determine if a response is a final answer or intermediate acknowledgment.
355+
356+
This method provides intelligent differentiation between:
357+
- Intermediate responses that acknowledge tool execution
358+
- Final responses that contain actual answers to user queries
359+
360+
Args:
361+
response_text: The text response from the LLM
362+
has_tool_calls: Whether the response contains tool calls
363+
tool_results: Results from executed tools
364+
365+
Returns:
366+
True if this is a final answer, False if intermediate
367+
"""
368+
if not response_text or not response_text.strip():
369+
return False
370+
371+
response_lower = response_text.lower().strip()
372+
373+
# If response contains tool calls, it's likely not a final answer
374+
if has_tool_calls:
375+
return False
376+
377+
# For Ollama, be more conservative about what constitutes a final answer
378+
if self._is_ollama_provider():
379+
# If we have recent tool results, check if this is just acknowledgment
380+
if tool_results:
381+
# Common patterns of tool acknowledgment (not final answers)
382+
acknowledgment_patterns = [
383+
"i'll", "let me", "now i'll", "next i'll", "i need to", "i should",
384+
"executing", "calling", "running", "using the", "based on this",
385+
"now let me", "let me now", "i will now", "proceeding to",
386+
"moving to", "continuing with", "next step", "now that i have",
387+
"tool executed", "function called", "result obtained", "got the result"
388+
]
389+
390+
# Check if response is primarily acknowledgment
391+
if any(pattern in response_lower for pattern in acknowledgment_patterns):
392+
# If it's short and contains acknowledgment patterns, likely intermediate
393+
if len(response_text.strip()) < 50:
394+
return False
395+
396+
# If response is very short and we have tool results, likely intermediate
397+
if len(response_text.strip()) < 30:
398+
return False
399+
400+
# Additional check: if response mainly contains status updates or simple confirmations
401+
status_patterns = ["done", "completed", "finished", "successful", "ok", "ready"]
402+
if (len(response_text.strip()) < 40 and
403+
any(pattern in response_lower for pattern in status_patterns)):
404+
return False
405+
406+
# For other providers, maintain existing behavior
407+
# Substantial content (>10 chars) is considered final
408+
return len(response_text.strip()) > 10
409+
352410
def _process_stream_delta(self, delta, response_text: str, tool_calls: List[Dict], formatted_tools: Optional[List] = None) -> tuple:
353411
"""
354412
Process a streaming delta chunk to extract content and tool calls.
@@ -1102,17 +1160,19 @@ def get_response(
11021160
continue
11031161

11041162
# Check if the LLM provided a final answer alongside the tool calls
1105-
# If response_text contains substantive content, treat it as the final answer
1106-
if response_text and response_text.strip() and len(response_text.strip()) > 10:
1163+
# Use intelligent differentiation between intermediate and final responses
1164+
if self._is_final_answer(response_text, bool(tool_calls), tool_results):
11071165
# LLM provided a final answer after tool execution, don't continue
11081166
final_response_text = response_text.strip()
11091167
break
11101168

11111169
# Special handling for Ollama to prevent infinite loops
1112-
tool_summary = self._generate_ollama_tool_summary(tool_results, response_text)
1113-
if tool_summary:
1114-
final_response_text = tool_summary
1115-
break
1170+
# Only generate summary if we're approaching max iterations or stuck in a loop
1171+
if self._is_ollama_provider() and iteration_count >= 5:
1172+
tool_summary = self._generate_ollama_tool_summary(tool_results, response_text)
1173+
if tool_summary:
1174+
final_response_text = tool_summary
1175+
break
11161176

11171177
# Otherwise, continue the loop to check if more tools are needed
11181178
iteration_count += 1
@@ -1851,17 +1911,19 @@ async def get_response_async(
18511911
stored_reasoning_content = reasoning_content
18521912

18531913
# Check if the LLM provided a final answer alongside the tool calls
1854-
# If response_text contains substantive content, treat it as the final answer
1855-
if response_text and response_text.strip() and len(response_text.strip()) > 10:
1914+
# Use intelligent differentiation between intermediate and final responses
1915+
if self._is_final_answer(response_text, bool(tool_calls), tool_results):
18561916
# LLM provided a final answer after tool execution, don't continue
18571917
final_response_text = response_text.strip()
18581918
break
18591919

18601920
# Special handling for Ollama to prevent infinite loops
1861-
tool_summary = self._generate_ollama_tool_summary(tool_results, response_text)
1862-
if tool_summary:
1863-
final_response_text = tool_summary
1864-
break
1921+
# Only generate summary if we're approaching max iterations or stuck in a loop
1922+
if self._is_ollama_provider() and iteration_count >= 5:
1923+
tool_summary = self._generate_ollama_tool_summary(tool_results, response_text)
1924+
if tool_summary:
1925+
final_response_text = tool_summary
1926+
break
18651927

18661928
# Continue the loop to check if more tools are needed
18671929
iteration_count += 1

0 commit comments

Comments
 (0)