@@ -323,8 +323,8 @@ def _generate_ollama_tool_summary(self, tool_results: List[Any], response_text:
323
323
if not (self ._is_ollama_provider () and tool_results ):
324
324
return None
325
325
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 ) :
328
328
return None
329
329
330
330
# Build tool summary efficiently
@@ -349,6 +349,212 @@ def _format_ollama_tool_result_message(self, function_name: str, tool_result: An
349
349
"content" : f"The { function_name } function returned: { tool_result_str } "
350
350
}
351
351
352
+ def _analyze_tool_chain_context (self , tool_results : List [Any ], response_text : str ) -> dict :
353
+ """
354
+ Analyze tool execution context to understand the current state of the tool chain.
355
+
356
+ Returns a context dictionary with confidence scores and patterns.
357
+ """
358
+ context = {
359
+ 'tool_count' : len (tool_results ) if tool_results else 0 ,
360
+ 'has_sequential_tools' : False ,
361
+ 'has_final_computation' : False ,
362
+ 'has_data_retrieval' : False ,
363
+ 'confidence_score' : 0.0 ,
364
+ 'tool_names' : [],
365
+ 'tool_success_rate' : 1.0
366
+ }
367
+
368
+ if not tool_results :
369
+ return context
370
+
371
+ # Extract tool names and analyze patterns
372
+ successful_tools = 0
373
+ for result in tool_results :
374
+ if isinstance (result , dict ) and 'function_name' in result :
375
+ tool_name = result ['function_name' ].lower ()
376
+ context ['tool_names' ].append (tool_name )
377
+
378
+ # Check if tool execution was successful
379
+ if 'error' not in result or not result .get ('error' ):
380
+ successful_tools += 1
381
+
382
+ # Analyze tool types for patterns
383
+ if any (pattern in tool_name for pattern in ['get' , 'fetch' , 'search' , 'retrieve' , 'find' ]):
384
+ context ['has_data_retrieval' ] = True
385
+
386
+ if any (pattern in tool_name for pattern in ['calculate' , 'compute' , 'multiply' , 'add' , 'sum' , 'process' ]):
387
+ context ['has_final_computation' ] = True
388
+
389
+ # Calculate success rate
390
+ if tool_results :
391
+ context ['tool_success_rate' ] = successful_tools / len (tool_results )
392
+
393
+ # Detect sequential tool usage patterns
394
+ if len (set (context ['tool_names' ])) > 1 :
395
+ context ['has_sequential_tools' ] = True
396
+
397
+ # Calculate confidence score based on tool chain analysis
398
+ confidence = 0.0
399
+
400
+ # Sequential tools with final computation suggest completion
401
+ if context ['has_sequential_tools' ] and context ['has_final_computation' ]:
402
+ confidence += 0.4
403
+
404
+ # Data retrieval followed by processing
405
+ if context ['has_data_retrieval' ] and context ['has_final_computation' ]:
406
+ confidence += 0.3
407
+
408
+ # High success rate adds confidence
409
+ confidence += context ['tool_success_rate' ] * 0.2
410
+
411
+ # Multiple tools executed successfully
412
+ if context ['tool_count' ] >= 2 and context ['tool_success_rate' ] > 0.8 :
413
+ confidence += 0.1
414
+
415
+ context ['confidence_score' ] = min (confidence , 1.0 )
416
+ return context
417
+
418
+ def _assess_response_quality (self , response_text : str , tool_results : List [Any ]) -> dict :
419
+ """
420
+ Assess the quality and completeness of a response based on content analysis.
421
+
422
+ Returns quality metrics and confidence scores.
423
+ """
424
+ quality = {
425
+ 'length' : len (response_text .strip ()) if response_text else 0 ,
426
+ 'has_tool_references' : False ,
427
+ 'has_conclusion_indicators' : False ,
428
+ 'contains_results' : False ,
429
+ 'quality_score' : 0.0
430
+ }
431
+
432
+ if not response_text :
433
+ return quality
434
+
435
+ response_lower = response_text .lower ().strip ()
436
+
437
+ # Check for tool result integration
438
+ if tool_results :
439
+ tool_result_strings = [str (result ) for result in tool_results if result ]
440
+ for tool_result in tool_result_strings :
441
+ if tool_result and any (part in response_lower for part in str (tool_result ).lower ().split () if len (part ) > 3 ):
442
+ quality ['has_tool_references' ] = True
443
+ break
444
+
445
+ # Check for conclusion indicators (dynamic pattern matching)
446
+ conclusion_indicators = ['therefore' , 'so' , 'result' , 'answer' , 'conclusion' , 'final' , 'total' , 'summary' ]
447
+ quality ['has_conclusion_indicators' ] = any (indicator in response_lower for indicator in conclusion_indicators )
448
+
449
+ # Check if response contains actual results/data
450
+ if any (char .isdigit () for char in response_text ) or '$' in response_text :
451
+ quality ['contains_results' ] = True
452
+
453
+ # Calculate quality score
454
+ score = 0.0
455
+
456
+ # Response length contributes to quality
457
+ if quality ['length' ] > 20 :
458
+ score += 0.2
459
+ if quality ['length' ] > 50 :
460
+ score += 0.1
461
+ if quality ['length' ] > 100 :
462
+ score += 0.1
463
+
464
+ # Content quality indicators
465
+ if quality ['has_tool_references' ]:
466
+ score += 0.3
467
+ if quality ['has_conclusion_indicators' ]:
468
+ score += 0.2
469
+ if quality ['contains_results' ]:
470
+ score += 0.1
471
+
472
+ quality ['quality_score' ] = min (score , 1.0 )
473
+ return quality
474
+
475
+ def _should_generate_tool_summary (self , tool_results : List [Any ], response_text : str , iteration_count : int ) -> bool :
476
+ """
477
+ Dynamically determine if a tool summary should be generated based on context analysis.
478
+
479
+ This replaces the hardcoded iteration_count >= 5 check with intelligent analysis.
480
+ """
481
+ # Analyze tool execution context
482
+ tool_context = self ._analyze_tool_chain_context (tool_results , response_text )
483
+
484
+ # Assess response quality
485
+ response_quality = self ._assess_response_quality (response_text , tool_results )
486
+
487
+ # Decision logic based on dynamic analysis
488
+ should_generate = False
489
+
490
+ # High confidence that tool chain is complete
491
+ if tool_context ['confidence_score' ] >= 0.7 :
492
+ should_generate = True
493
+
494
+ # Good tool chain with quality response
495
+ elif tool_context ['confidence_score' ] >= 0.5 and response_quality ['quality_score' ] >= 0.6 :
496
+ should_generate = True
497
+
498
+ # Sequential tools with final computation and good response
499
+ elif (tool_context ['has_sequential_tools' ] and
500
+ tool_context ['has_final_computation' ] and
501
+ response_quality ['quality_score' ] >= 0.4 ):
502
+ should_generate = True
503
+
504
+ # Safety fallback - prevent infinite loops (increased threshold)
505
+ elif iteration_count >= 7 :
506
+ should_generate = True
507
+
508
+ return should_generate
509
+
510
+ def _is_final_answer (self , response_text : str , has_tool_calls : bool , tool_results : List [Any ]) -> bool :
511
+ """
512
+ Determine if a response is a final answer or intermediate acknowledgment.
513
+
514
+ This method provides intelligent differentiation using dynamic analysis
515
+ instead of hardcoded patterns.
516
+
517
+ Args:
518
+ response_text: The text response from the LLM
519
+ has_tool_calls: Whether the response contains tool calls
520
+ tool_results: Results from executed tools
521
+
522
+ Returns:
523
+ True if this is a final answer, False if intermediate
524
+ """
525
+ if not response_text or not response_text .strip ():
526
+ return False
527
+
528
+ # If response contains tool calls, it's likely not a final answer
529
+ if has_tool_calls :
530
+ return False
531
+
532
+ # For Ollama, use dynamic analysis instead of hardcoded patterns
533
+ if self ._is_ollama_provider () and tool_results :
534
+ # Analyze tool chain context
535
+ tool_context = self ._analyze_tool_chain_context (tool_results , response_text )
536
+
537
+ # Assess response quality
538
+ response_quality = self ._assess_response_quality (response_text , tool_results )
539
+
540
+ # Dynamic decision based on context and quality
541
+ # If we have a complete tool chain with quality response, it's likely final
542
+ if (tool_context ['confidence_score' ] >= 0.6 and
543
+ response_quality ['quality_score' ] >= 0.5 ):
544
+ return True
545
+
546
+ # If response is very short and we have tool results, likely intermediate
547
+ if response_quality ['length' ] < 20 :
548
+ return False
549
+
550
+ # If response doesn't reference tool results, likely intermediate
551
+ if not response_quality ['has_tool_references' ] and response_quality ['length' ] < 80 :
552
+ return False
553
+
554
+ # For other providers, maintain existing behavior
555
+ # Substantial content (>10 chars) is considered final
556
+ return len (response_text .strip ()) > 10
557
+
352
558
def _process_stream_delta (self , delta , response_text : str , tool_calls : List [Dict ], formatted_tools : Optional [List ] = None ) -> tuple :
353
559
"""
354
560
Process a streaming delta chunk to extract content and tool calls.
@@ -1102,17 +1308,19 @@ def get_response(
1102
1308
continue
1103
1309
1104
1310
# 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 :
1311
+ # Use intelligent differentiation between intermediate and final responses
1312
+ if self . _is_final_answer ( response_text , bool ( tool_calls ), tool_results ) :
1107
1313
# LLM provided a final answer after tool execution, don't continue
1108
1314
final_response_text = response_text .strip ()
1109
1315
break
1110
1316
1111
1317
# 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
1318
+ # Use dynamic analysis instead of hardcoded iteration count
1319
+ if self ._is_ollama_provider () and self ._should_generate_tool_summary (tool_results , response_text , iteration_count ):
1320
+ tool_summary = self ._generate_ollama_tool_summary (tool_results , response_text )
1321
+ if tool_summary :
1322
+ final_response_text = tool_summary
1323
+ break
1116
1324
1117
1325
# Otherwise, continue the loop to check if more tools are needed
1118
1326
iteration_count += 1
@@ -1851,17 +2059,19 @@ async def get_response_async(
1851
2059
stored_reasoning_content = reasoning_content
1852
2060
1853
2061
# 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 :
2062
+ # Use intelligent differentiation between intermediate and final responses
2063
+ if self . _is_final_answer ( response_text , bool ( tool_calls ), tool_results ) :
1856
2064
# LLM provided a final answer after tool execution, don't continue
1857
2065
final_response_text = response_text .strip ()
1858
2066
break
1859
2067
1860
2068
# 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
2069
+ # Use dynamic analysis instead of hardcoded iteration count
2070
+ if self ._is_ollama_provider () and self ._should_generate_tool_summary (tool_results , response_text , iteration_count ):
2071
+ tool_summary = self ._generate_ollama_tool_summary (tool_results , response_text )
2072
+ if tool_summary :
2073
+ final_response_text = tool_summary
2074
+ break
1865
2075
1866
2076
# Continue the loop to check if more tools are needed
1867
2077
iteration_count += 1
0 commit comments