Repository: agno-agi/agno
Pull request: show_tool_calls in seperate block
## Description

- **Summary of changes**: `show_tool_calls` in seperate blocks instead of in the Response block
- **Related issues**: Mention if this PR fixes or is connected to any issues.
- **Motivation and context**: Explain the reason for the changes and the problem they solve.
- **Environment or dependencies**: Specify any changes in dependencies or environment configurations required for this update.
- **Impact on metrics**: (If applicable) Describe changes in any metrics or performance benchmarks.

Fixes # (issue)
Loom- Async, stream, and async with structured output etc
https://www.loom.com/share/b10ce79868d1453ba06d93d7f6612173?sid=a0d180d3-f190-4bc9-8c72-3e9f92e8d703

Demo SS:
- With basic flow:
```py
from agno.agent import Agent, RunResponse  # noqa
from agno.models.openai import OpenAIChat
from agno.tools.yfinance import YFinanceTools
from agno.tools.wikipedia import WikipediaTools

agent = Agent(
    model=OpenAIChat(id="gpt-4o"), markdown=True, tools=[YFinanceTools(), WikipediaTools()], show_tool_calls=True
)

# Get the response in a variable
# run: RunResponse = agent.run("Share a 2 sentence horror story")
# print(run.content)

# Print the response in the terminal
agent.print_response("What is current prince of stock TSLA? Also give brief history of company tesla")

agent.run_response.metrics
```
<img width="1121" alt="image" src="https://github.com/user-attachments/assets/1c8afd05-3d80-4c94-9b51-8e119dafa7a9" />

- with `structured_output` flow:
```py
t List, Optional

from pydantic import BaseModel, Field
from rich.pretty import pprint  # noqa

from agno.agent import Agent, RunResponse  # noqa
from agno.models.openai import OpenAIChat
from agno.tools.wikipedia import WikipediaTools
from agno.tools.yfinance import YFinanceTools


class CompanyStockInfo(BaseModel):
    """Information about a company."""

    company_name: str = Field(..., description="The full name of the company")
    company_history: str = Field(..., description="Brief history of the company (3-5 sentences)")
    founded_year: Optional[int] = Field(None, description="The year the company was founded")
    # ceo: Optional[str] = Field(None, description="Current CEO of the company")


# Create an agent that combines stock data with company information
stock_info_agent = Agent(
    model=OpenAIChat(id="gpt-4o"),
    description="You are a web crawler that gets info about various companies like history, founded year, etc.",
    instructions=[
        "Use Wikipedia to research the company's history and key information.",
        "Format the response according to the CompanyStockInfo schema.",
        "Make sure all numerical data is accurate and up-to-date.",
        "If a field is truly unknown, leave it as null.",
    ],
    # Use both YFinance and Wikipedia tools
    tools=[WikipediaTools()],
    show_tool_calls=True,
    response_model=CompanyStockInfo,
)

# Print the response in the terminal
stock_info_agent.print_response("Give me information about Tesla Company")
```
<img width="1121" alt="image" src="https://github.com/user-attachments/assets/92f34aed-74e1-438d-9435-e8c29a73dde0" />


---

## Type of change

Please check the options that are relevant:

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [x] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Model update (Addition or modification of models)
- [ ] Other (please describe):

---

## Checklist

- [ ] Adherence to standards: Code complies with Agno’s style guidelines and best practices.
- [ ] Formatting and validation: You have run `./scripts/format.sh` and `./scripts/validate.sh` to ensure code is formatted and linted.
- [ ] Self-review completed: A thorough review has been performed by the contributor(s).
- [ ] Documentation: Docstrings and comments have been added or updated for any complex logic.
- [ ] Examples and guides: Relevant cookbook examples have been included or updated (if applicable).
- [ ] Tested in a clean environment: Changes have been tested in a clean environment to confirm expected behavior.
- [ ] Tests (optional): Tests have been added or updated to cover any new or changed functionality.

---

## Additional Notes

Include any deployment notes, performance implications, security considerations, or other relevant information (e.g., screenshots or logs if applicable).


Problem statement:
[Bug]Failed to load resource: the server responded with a status of 404 ()
# Description
**Failed to load resource: the server responded with a status of 404 ()**

## Steps to Reproduce
just run demo  AI Finance Agent Team with Web Access

## Agent Configuration (if applicable)
Provide relevant agent configuration.

## Expected Behavior
What did you expect to happen?

## Actual Behavior
What actually happened instead?

## Screenshots or Logs (if applicable)
Include any relevant screenshots or error logs that demonstrate the issue.

## Environment
- OS: Cloud Shell Editor
- Browser (if relevant): (e.g. Chrome 108, Firefox 107)
- Agno Version: (e.g. v1.0.0)
- External Dependency Versions: (e.g., yfinance 0.2.52)
- Additional Environment Details:ngrok

## Possible Solutions (optional)
Suggest any ideas you might have to fix or address the issue.

## Additional Context
Add any other context or details about the problem here.

Hints from issue discussion:

Commit message: fix formatting

Patch to review:
diff --git a/libs/agno/agno/agent/agent.py b/libs/agno/agno/agent/agent.py
index 22bd6de1a0..5663b9860b 100644
--- a/libs/agno/agno/agent/agent.py
+++ b/libs/agno/agno/agent/agent.py
@@ -674,6 +674,11 @@ def _run(
         else:
             # Get the model response
             model_response = self.model.response(messages=run_messages.messages)
+            # Format tool calls if they exist
+            if model_response.tool_calls:
+                model_response.format_tool_calls()
+                self.run_response.formatted_tool_calls = model_response.formatted_tool_calls
+
             # Handle structured outputs
             if self.response_model is not None and model_response.parsed is not None:
                 # We get native structured outputs from the model
@@ -887,9 +892,8 @@ def run(
             try:
                 # If a response_model is set, return the response as a structured output
                 if self.response_model is not None and self.parse_response:
-                    # Set show_tool_calls=False if we have response_model
-                    self.show_tool_calls = False
-                    logger.debug("Setting show_tool_calls=False as response_model is set")
+                    # Save original show_tool_calls value
+                    original_show_tool_calls = self.show_tool_calls
 
                     # Set stream=False and run the agent
                     logger.debug("Setting stream=False as response_model is set")
@@ -910,6 +914,8 @@ def run(
 
                     # Do a final check confirming the content is in the response_model format
                     if isinstance(run_response.content, self.response_model):
+                        # Restore original show_tool_calls value
+                        self.show_tool_calls = original_show_tool_calls
                         return run_response
 
                     # Otherwise convert the response to the structured format
@@ -930,6 +936,9 @@ def run(
                             logger.warning(f"Failed to convert response to output model: {e}")
                     else:
                         logger.warning("Something went wrong. Run response content is not a string")
+
+                    # Restore original show_tool_calls value
+                    self.show_tool_calls = original_show_tool_calls
                     return run_response
                 else:
                     if stream and self.is_streamable:
@@ -1182,6 +1191,11 @@ async def _arun(
         else:
             # Get the model response
             model_response = await self.model.aresponse(messages=run_messages.messages)
+            # Format tool calls if they exist
+            if model_response.tool_calls:
+                model_response.format_tool_calls()
+                self.run_response.formatted_tool_calls = model_response.formatted_tool_calls
+
             # Handle structured outputs
             if self.response_model is not None and model_response.parsed is not None:
                 # We get native structured outputs from the model
@@ -2060,13 +2074,18 @@ def get_system_message(self) -> Optional[Message]:
                 sys_message_content = self.format_message_with_state_variables(sys_message_content)
 
             # Add the JSON output prompt if response_model is provided and structured_outputs is False
-            if self.response_model is not None and self.model and (
-                self.model.supports_native_structured_outputs
-                and (self.response_format == "json" or self.structured_outputs is False)
+            if (
+                self.response_model is not None
+                and self.model
+                and (
+                    self.model.supports_native_structured_outputs
+                    and (self.response_format == "json" or self.structured_outputs is False)
+                )
             ):
                 sys_message_content += f"\n{self.get_json_output_prompt()}"
 
-            return Message(role=self.system_message_role, content=sys_message_content)  # type: ignore
+            # type: ignore
+            return Message(role=self.system_message_role, content=sys_message_content)
 
         # 2. If create_default_system_message is False, return None.
         if not self.create_default_system_message:
@@ -3627,6 +3646,7 @@ def print_response(
         **kwargs: Any,
     ) -> None:
         import json
+        import re
 
         from rich.console import Group
         from rich.json import JSON
@@ -3647,6 +3667,7 @@ def print_response(
             _response_content: str = ""
             _response_thinking: str = ""
             reasoning_steps: List[ReasoningStep] = []
+            formatted_tool_calls: List[str] = []
 
             with Live(console=console) as live_log:
                 status = Status("Thinking...", spinner="aesthetic", speed=0.4, refresh_per_second=10)
@@ -3691,11 +3712,11 @@ def print_response(
                             reasoning_steps = resp.extra_data.reasoning_steps
 
                     response_content_stream: Union[str, Markdown] = _response_content
+
                     # Escape special tags before markdown conversion
                     if self.markdown:
                         escaped_content = self.escape_markdown_tags(_response_content, tags_to_include_in_markdown)
                         response_content_stream = Markdown(escaped_content)
-
                     panels = [status]
 
                     if message and show_message:
@@ -3753,6 +3774,21 @@ def print_response(
                     if render:
                         live_log.update(Group(*panels))
 
+                    # Add tool calls panel if available
+                    if self.show_tool_calls and formatted_tool_calls:
+                        render = True
+                        # Create bullet points for each tool call
+                        tool_calls_content = Text()
+                        for tool_call in formatted_tool_calls:
+                            tool_calls_content.append(f"• {tool_call}\n")
+
+                        tool_calls_panel = self.create_panel(
+                            content=tool_calls_content,
+                            title="Tool Calls",
+                            border_style="yellow",
+                        )
+                        panels.append(tool_calls_panel)
+
                     if len(_response_content) > 0:
                         render = True
                         # Create panel for response
@@ -3863,9 +3899,30 @@ def print_response(
                     panels.append(thinking_panel)
                     live_log.update(Group(*panels))
 
+                # Add tool calls panel if available
+                if self.show_tool_calls and isinstance(run_response, RunResponse) and run_response.formatted_tool_calls:
+                    # Create bullet points for each tool call
+                    tool_calls_content = Text()
+                    for tool_call in run_response.formatted_tool_calls:
+                        tool_calls_content.append(f"• {tool_call}\n")
+
+                    tool_calls_panel = self.create_panel(
+                        content=tool_calls_content,
+                        title="Tool Calls",
+                        border_style="yellow",
+                    )
+                    panels.append(tool_calls_panel)
+                    live_log.update(Group(*panels))
+
                 response_content_batch: Union[str, JSON, Markdown] = ""
                 if isinstance(run_response, RunResponse):
                     if isinstance(run_response.content, str):
+                        # Clean up response content to remove "Running:" section
+                        if isinstance(run_response.content, str):
+                            run_response.content = re.sub(
+                                r"Running:\s*\n\s*\n(\s*• [^\n]+\n)+\s*\n", "", run_response.content
+                            )
+
                         if self.markdown:
                             escaped_content = self.escape_markdown_tags(
                                 run_response.content, tags_to_include_in_markdown
@@ -3936,10 +3993,7 @@ async def aprint_response(
         tags_to_include_in_markdown: Set[str] = {"think", "thinking"},
         **kwargs: Any,
     ) -> None:
-        import json
-
         from rich.console import Group
-        from rich.json import JSON
         from rich.live import Live
         from rich.markdown import Markdown
         from rich.status import Status
@@ -3957,6 +4011,7 @@ async def aprint_response(
             _response_content: str = ""
             _response_thinking: str = ""
             reasoning_steps: List[ReasoningStep] = []
+            formatted_tool_calls: List[str] = []
 
             with Live(console=console) as live_log:
                 status = Status("Thinking...", spinner="aesthetic", speed=0.4, refresh_per_second=10)
@@ -3981,7 +4036,7 @@ async def aprint_response(
                 if render:
                     live_log.update(Group(*panels))
 
-                _arun_generator = await self.arun(
+                async for resp in await self.arun(
                     message=message,
                     messages=messages,
                     audio=audio,
@@ -3990,8 +4045,7 @@ async def aprint_response(
                     files=files,
                     stream=True,
                     **kwargs,
-                )
-                async for resp in _arun_generator:
+                ):
                     if isinstance(resp, RunResponse):
                         if resp.event == RunEvent.run_response:
                             if isinstance(resp.content, str):
@@ -4000,6 +4054,9 @@ async def aprint_response(
                                 _response_thinking += resp.thinking
                         if resp.extra_data is not None and resp.extra_data.reasoning_steps is not None:
                             reasoning_steps = resp.extra_data.reasoning_steps
+                        # Capture formatted tool calls
+                        if resp.formatted_tool_calls:
+                            formatted_tool_calls = resp.formatted_tool_calls
 
                     response_content_stream: Union[str, Markdown] = _response_content
                     # Escape special tags before markdown conversion
@@ -4012,7 +4069,7 @@ async def aprint_response(
                     if message and show_message:
                         render = True
                         # Convert message to a panel
-                        message_content = get_text_from_message(self.format_message_with_state_variables(message))
+                        message_content = get_text_from_message(message)
                         message_panel = self.create_panel(
                             content=Text(message_content, style="green"),
                             title="Message",
@@ -4022,7 +4079,7 @@ async def aprint_response(
                     if render:
                         live_log.update(Group(*panels))
 
-                    if len(reasoning_steps) > 0 and (show_reasoning or show_full_reasoning):
+                    if len(reasoning_steps) > 0 and show_reasoning:
                         render = True
                         # Create panels for reasoning steps
                         for i, step in enumerate(reasoning_steps, 1):
@@ -4064,6 +4121,21 @@ async def aprint_response(
                     if render:
                         live_log.update(Group(*panels))
 
+                    # Add tool calls panel if available
+                    if self.show_tool_calls and formatted_tool_calls:
+                        render = True
+                        # Create bullet points for each tool call
+                        tool_calls_content = Text()
+                        for tool_call in formatted_tool_calls:
+                            tool_calls_content.append(f"• {tool_call}\n")
+
+                        tool_calls_panel = self.create_panel(
+                            content=tool_calls_content,
+                            title="Tool Calls",
+                            border_style="yellow",
+                        )
+                        panels.append(tool_calls_panel)
+
                     if len(_response_content) > 0:
                         render = True
                         # Create panel for response
@@ -4122,6 +4194,7 @@ async def aprint_response(
                     audio=audio,
                     images=images,
                     videos=videos,
+                    files=files,
                     stream=False,
                     **kwargs,
                 )
@@ -4173,32 +4246,24 @@ async def aprint_response(
                     panels.append(thinking_panel)
                     live_log.update(Group(*panels))
 
-                response_content_batch: Union[str, JSON, Markdown] = ""
-                if isinstance(run_response, RunResponse):
-                    if isinstance(run_response.content, str):
-                        if self.markdown:
-                            escaped_content = self.escape_markdown_tags(
-                                run_response.content, tags_to_include_in_markdown
-                            )
-                            response_content_batch = Markdown(escaped_content)
-                        else:
-                            response_content_batch = run_response.get_content_as_string(indent=4)
-                    elif self.response_model is not None and isinstance(run_response.content, BaseModel):
-                        try:
-                            response_content_batch = JSON(
-                                run_response.content.model_dump_json(exclude_none=True), indent=2
-                            )
-                        except Exception as e:
-                            logger.warning(f"Failed to convert response to JSON: {e}")
-                    else:
-                        try:
-                            response_content_batch = JSON(json.dumps(run_response.content), indent=4)
-                        except Exception as e:
-                            logger.warning(f"Failed to convert response to JSON: {e}")
+                # Add tool calls panel if available
+                if self.show_tool_calls and isinstance(run_response, RunResponse) and run_response.formatted_tool_calls:
+                    # Create bullet points for each tool call
+                    tool_calls_content = Text()
+                    for tool_call in run_response.formatted_tool_calls:
+                        tool_calls_content.append(f"• {tool_call}\n")
+
+                    tool_calls_panel = self.create_panel(
+                        content=tool_calls_content,
+                        title="Tool Calls",
+                        border_style="yellow",
+                    )
+                    panels.append(tool_calls_panel)
+                    live_log.update(Group(*panels))
 
                 # Create panel for response
                 response_panel = self.create_panel(
-                    content=response_content_batch,
+                    content=run_response.content,
                     title=f"Response ({response_timer.elapsed:.1f}s)",
                     border_style="blue",
                 )
diff --git a/libs/agno/agno/models/base.py b/libs/agno/agno/models/base.py
index 82bf9c9fef..9e52332867 100644
--- a/libs/agno/agno/models/base.py
+++ b/libs/agno/agno/models/base.py
@@ -990,21 +990,22 @@ def _show_tool_calls(self, function_calls_to_run: List[FunctionCall], model_resp
         """
         Show tool calls in the model response.
         """
+        # Only add to content for regular text responses
         if len(function_calls_to_run) == 1:
             if model_response.content and len(model_response.content) > 0 and model_response.content[-1] != "\n":
                 model_response.content += "\n\n"
             else:
                 model_response.content = ""
-            model_response.content += f" - Running: {function_calls_to_run[0].get_call_str()}\n\n"
+            model_response.content += f"Running: \n\n • {function_calls_to_run[0].get_call_str()}\n\n"
         elif len(function_calls_to_run) > 1:
             if model_response.content and len(model_response.content) > 0 and model_response.content[-1] != "\n":
                 model_response.content += "\n\n"
             else:
                 model_response.content = ""
-            model_response.content += "Running:"
+            model_response.content += "Running:\n\n"
             for _f in function_calls_to_run:
-                model_response.content += f"\n - {_f.get_call_str()}"
-            model_response.content += "\n\n"
+                model_response.content += f" • {_f.get_call_str()}\n"
+            model_response.content += "\n"
 
     def _show_stream_tool_calls(self, function_calls_to_run: List[FunctionCall]) -> Iterator[ModelResponse]:
         if len(function_calls_to_run) == 1:
diff --git a/libs/agno/agno/models/response.py b/libs/agno/agno/models/response.py
index da9d50dfee..46bcb14017 100644
--- a/libs/agno/agno/models/response.py
+++ b/libs/agno/agno/models/response.py
@@ -25,6 +25,7 @@ class ModelResponse:
     parsed: Optional[Any] = None
     audio: Optional[AudioResponse] = None
     tool_calls: List[Dict[str, Any]] = field(default_factory=list)
+    formatted_tool_calls: List[str] = field(default_factory=list)
     event: str = ModelResponseEvent.assistant_response.value
 
     provider_data: Optional[Dict[str, Any]] = None
@@ -41,6 +42,15 @@ class ModelResponse:
 
     extra: Optional[Dict[str, Any]] = None
 
+    def format_tool_calls(self) -> None:
+        """Format tool calls for display in a readable format."""
+        self.formatted_tool_calls = []
+        for tool_call in self.tool_calls:
+            if "tool_name" in tool_call and "tool_args" in tool_call:
+                tool_name = tool_call["tool_name"]
+                args_str = ", ".join(f"{k}={v}" for k, v in tool_call["tool_args"].items())
+                self.formatted_tool_calls.append(f"{tool_name}({args_str})")
+
 
 class FileType(str, Enum):
     MP4 = "mp4"
diff --git a/libs/agno/agno/run/response.py b/libs/agno/agno/run/response.py
index 29eede6fee..0dba328d63 100644
--- a/libs/agno/agno/run/response.py
+++ b/libs/agno/agno/run/response.py
@@ -66,6 +66,7 @@ class RunResponse:
     session_id: Optional[str] = None
     workflow_id: Optional[str] = None
     tools: Optional[List[Dict[str, Any]]] = None
+    formatted_tool_calls: Optional[List[str]] = None
     images: Optional[List[ImageArtifact]] = None  # Images attached to the response
     videos: Optional[List[VideoArtifact]] = None  # Videos attached to the response
     audio: Optional[List[AudioArtifact]] = None  # Audio attached to the response


Code review comments:
diff_hunk: @@ -990,21 +990,22 @@ def _show_tool_calls(self, function_calls_to_run: List[FunctionCall], model_resp
         """
         Show tool calls in the model response.
         """
+        # Only add to content for regular text responses
         if len(function_calls_to_run) == 1:
             if model_response.content and len(model_response.content) > 0 and model_response.content[-1] != "\n":
                 model_response.content += "\n\n"
             else:
                 model_response.content = ""
-            model_response.content += f" - Running: {function_calls_to_run[0].get_call_str()}\n\n"
+            model_response.content += f"Running: \n\n • {function_calls_to_run[0].get_call_str()}\n\n"
path: libs/agno/agno/models/base.py
text: @user1:
Should we not already here store it on a different field on the model_response? Then you don't have to check for tool_calls, you can just check for that field. That could be `formatted_tools`

@user1:
Oh wait I see what you did, in that case should we not remove this completely? 

@author:
oh yes, i can just do 
```py
def _show_tool_calls(self, function_calls_to_run: List[FunctionCall], model_response: ModelResponse):
        """
        Show tool calls in the model response.
        """
        pass
```

that also works, since we no longer need to append the tool call info in response content

diff_hunk: @@ -1182,6 +1191,11 @@ async def _arun(
         else:
             # Get the model response
             model_response = await self.model.aresponse(messages=run_messages.messages)
+            # Format tool calls if they exist
+            if model_response.tool_calls:
+                model_response.format_tool_calls()
+                self.run_response.formatted_tool_calls = model_response.formatted_tool_calls
path: libs/agno/agno/agent/agent.py
text: @user1:
Do you need to have `formatted_tool_calls` on the model_response as well ?

@author:
that's where I'm storing the extracted tool call info in this fashion `get_current_stock_price(symbol=TSLA)`
extracted using (tool_name and args) using the function in `response.py`
```py
def format_tool_calls(self) -> None:
        """Format tool calls for display in a readable format."""
        self.formatted_tool_calls = []
        for tool_call in self.tool_calls:
            if "tool_name" in tool_call and "tool_args" in tool_call:
                tool_name = tool_call["tool_name"]
                args_str = ", ".join(f"{k}={v}" for k, v in tool_call["tool_args"].items())
                self.formatted_tool_calls.append(f"{tool_name}({args_str})")
```

@user1:
Instead of storing it, how about that function just returns the formatted tool calls and you set it directly on the run_response? So no intermediate storage. The less we store on the message the better, because it will be serialized, etc. And takes up memory, etc

@author:
done

diff_hunk: @@ -910,6 +914,8 @@ def run(
 
                     # Do a final check confirming the content is in the response_model format
                     if isinstance(run_response.content, self.response_model):
+                        # Restore original show_tool_calls value
+                        self.show_tool_calls = original_show_tool_calls
path: libs/agno/agno/agent/agent.py
text: @user1:
We probably don't need to do this? We don't need to set it to false anymore with structured output