2929from .ui import (
3030 console , display_tool_call , display_tool_result , display_executed_tool ,
3131 display_code_execution_result , display_info , display_warning ,
32- display_key_rotation_notice , display_model_fallback_notice , display_provider_exhausted_notice
32+ display_key_rotation_notice , display_model_fallback_notice , display_provider_exhausted_notice ,
33+ display_assistant_response
3334)
3435from .logger import log_error , log_api_error , log_tool_error , log_debug
3536from .history import history_manager
@@ -402,6 +403,20 @@ def _save_conversation(self):
402403 """Save the current conversation state"""
403404 history_manager .update_conversation (self .messages )
404405
406+ def _filter_valid_tool_calls (self , tool_calls : List [Any ]) -> List [Any ]:
407+ """
408+ Filter out tool calls for unknown/invalid tools.
409+ Some models (especially Llama) may hallucinate tool names like 'commentary'.
410+ """
411+ valid_calls = []
412+ for tc in tool_calls :
413+ tool_name = tc .name if hasattr (tc , 'name' ) else tc .get ('name' , '' )
414+ if tool_name in TOOLS :
415+ valid_calls .append (tc )
416+ else :
417+ log_debug (f"Ignoring unknown tool call: { tool_name } " )
418+ return valid_calls
419+
405420 def _parse_tool_calls_from_text (self , text : str ) -> List [ToolCall ]:
406421 """
407422 Parse tool calls that appear as text in the response.
@@ -668,8 +683,7 @@ def _process_tool_calls_with_retry(
668683 follow_up_response = ""
669684 pending_tool_calls = []
670685 has_content = False
671- live_display = None
672- chunk_count = 0 # Buffer: only update display every N chunks.
686+ chunk_count = 0 # Buffer counter for streaming.
673687
674688 # Stream with Live panel for smooth updates.
675689 for chunk in client .stream_chat (
@@ -680,32 +694,26 @@ def _process_tool_calls_with_retry(
680694 if chunk .content :
681695 if not has_content :
682696 has_content = True
683- # Stop spinner and start Live display
697+ # Stop spinner when streaming starts
684698 self ._update_status ("streaming" , "" )
685- live_display = Live (
686- Panel (Markdown (follow_up_response or "..." ), title = "Assistant" , title_align = "left" , border_style = COLORS ['secondary' ], box = ROUNDED ),
687- console = console ,
688- refresh_per_second = 4 ,
689- transient = True
690- )
691- live_display .start ()
692699
693700 follow_up_response += chunk .content
694701 chunk_count += 1
695- # Update the live panel only every 5 chunks to reduce re-renders.
696- if live_display and chunk_count % 5 == 0 : live_display .update (Panel (Markdown (follow_up_response ), title = "Assistant" , title_align = "left" , border_style = COLORS ['secondary' ], box = ROUNDED ))
697702
698703 if chunk .tool_calls :
699- pending_tool_calls .extend (chunk .tool_calls )
704+ # Filter out invalid/unknown tool calls
705+ valid_calls = self ._filter_valid_tool_calls (chunk .tool_calls )
706+ pending_tool_calls .extend (valid_calls )
700707
701- # Stop live display and show final panel.
702- if live_display : live_display . stop ()
703- if has_content and follow_up_response : console . print ( Panel ( Markdown ( follow_up_response ), title = "Assistant" , title_align = "left" , border_style = COLORS [ 'secondary' ], box = ROUNDED ) )
708+ # Show final response using centralized display
709+ if has_content and follow_up_response :
710+ display_assistant_response ( follow_up_response )
704711
705712 # Check for tool calls in text response
706713 if not pending_tool_calls and follow_up_response :
707714 text_tool_calls = self ._parse_tool_calls_from_text (follow_up_response )
708- if text_tool_calls : pending_tool_calls .extend (text_tool_calls )
715+ if text_tool_calls :
716+ pending_tool_calls .extend (text_tool_calls )
709717
710718 # If the model wants to use more tools, process them (for multi-step tasks)
711719 if pending_tool_calls and allow_more_tools :
@@ -783,8 +791,7 @@ def chat(self, user_input: str, _retry_count: int = 0) -> str:
783791 # Get all tools including MCP tools.
784792 all_tools = get_all_tool_definitions ()
785793 has_started_streaming = False
786- live_display = None
787- chunk_count = 0 # Buffer: only update display every N chunks.
794+ chunk_count = 0 # Buffer counter for streaming.
788795
789796 # Update status to generating.
790797 self ._update_status ("generating" , "" )
@@ -798,28 +805,16 @@ def chat(self, user_input: str, _retry_count: int = 0) -> str:
798805 if chunk .content :
799806 if not has_started_streaming :
800807 has_started_streaming = True
801- # Stop spinner before showing content
808+ # Stop spinner when streaming starts
802809 self ._update_status ("streaming" , "" )
803- # Start Live display for streaming
804- live_display = Live (
805- Panel (Markdown (response_text or "..." ), title = "Assistant" , title_align = "left" , border_style = COLORS ['secondary' ], box = ROUNDED ),
806- console = console ,
807- refresh_per_second = 4 ,
808- transient = True
809- )
810- live_display .start ()
811810
812811 response_text += chunk .content
813812 chunk_count += 1
814- # Update the live panel only every 5 chunks to reduce re-renders
815- if live_display and chunk_count % 5 == 0 :
816- live_display .update (
817- Panel (Markdown (response_text ), title = "Assistant" , title_align = "left" , border_style = COLORS ['secondary' ], box = ROUNDED )
818- )
819813
820- # Collect tool calls
814+ # Collect tool calls (filter out unknown tools)
821815 if chunk .tool_calls :
822- pending_tool_calls .extend (chunk .tool_calls )
816+ valid_calls = self ._filter_valid_tool_calls (chunk .tool_calls )
817+ pending_tool_calls .extend (valid_calls )
823818
824819 # Collect executed tools (from Groq built-in tools like code_interpreter)
825820 if chunk .executed_tools :
@@ -829,11 +824,10 @@ def chat(self, user_input: str, _retry_count: int = 0) -> str:
829824 if chunk .reasoning :
830825 log_debug (f"Model reasoning: { chunk .reasoning [:200 ]} ..." )
831826
832- # Stop live display and show final panel
833- if live_display :
834- live_display .stop ()
835- if has_started_streaming and response_text :
836- console .print (Panel (Markdown (response_text ), title = "Assistant" , title_align = "left" , border_style = COLORS ['secondary' ], box = ROUNDED ))
827+ # Show final response using centralized display (only if we have content and no tool calls)
828+ # If there are tool calls, the response will be shown after tool execution
829+ if has_started_streaming and response_text and not pending_tool_calls :
830+ display_assistant_response (response_text )
837831
838832 # Display executed tools (code execution, web search, etc.)
839833 if executed_tools_list :
@@ -849,6 +843,7 @@ def chat(self, user_input: str, _retry_count: int = 0) -> str:
849843 text_tool_calls = self ._parse_tool_calls_from_text (response_text )
850844 if text_tool_calls :
851845 log_debug (f"Found { len (text_tool_calls )} tool call(s) in text response" )
846+ # Already filtered by _parse_tool_calls_from_text
852847 pending_tool_calls .extend (text_tool_calls )
853848
854849 # Process tool calls if any
0 commit comments