from __future__ import annotations

from collections import ChainMap, defaultdict, deque
from dataclasses import dataclass
from os import getenv
from textwrap import dedent
from typing import (
    Any,
    AsyncIterator,
    Callable,
    Dict,
    Iterator,
    List,
    Literal,
    Optional,
    Sequence,
    Type,
    Union,
    cast,
    overload,
)
from uuid import uuid4

from pydantic import BaseModel

from agno.exceptions import AgentRunException, StopAgentRun
from agno.knowledge.agent import AgentKnowledge
from agno.media import Audio, AudioArtifact, Image, ImageArtifact, Video, VideoArtifact
from agno.memory.agent import AgentMemory, AgentRun
from agno.models.base import Model
from agno.models.message import Message, MessageReferences
from agno.models.response import ModelResponse, ModelResponseEvent
from agno.reasoning.step import NextAction, ReasoningStep, ReasoningSteps
from agno.run.messages import RunMessages
from agno.run.response import RunEvent, RunResponse, RunResponseExtraData
from agno.storage.agent.base import AgentStorage
from agno.storage.agent.session import AgentSession
from agno.tools.function import Function
from agno.tools.toolkit import Toolkit
from agno.utils.log import logger, set_log_level_to_debug, set_log_level_to_info
from agno.utils.message import get_text_from_message
from agno.utils.safe_formatter import SafeFormatter
from agno.utils.timer import Timer


@dataclass(init=False, slots=True)  # type: ignore
class Agent:
    # --- Agent settings ---
    # Model for this Agent
    model: Optional[Model] = None
    # Agent name
    name: Optional[str] = None
    # Agent UUID (autogenerated if not set)
    agent_id: Optional[str] = None
    # Agent introduction. This is added to the message history when a run is started.
    introduction: Optional[str] = None

    # --- User settings ---
    # ID of the user interacting with this agent
    user_id: Optional[str] = None

    # --- Session settings ---
    # Session UUID (autogenerated if not set)
    session_id: Optional[str] = None
    # Session name
    session_name: Optional[str] = None
    # Session state stored in the database
    session_state: Optional[Dict[str, Any]] = None

    # --- Agent Context ---
    # Context available for tools and prompt functions
    context: Optional[Dict[str, Any]] = None
    # If True, add the context to the user prompt
    add_context: bool = False
    # If True, resolve the context (i.e. call any functions in the context) before running the agent
    resolve_context: bool = True

    # --- Agent Memory ---
    memory: Optional[AgentMemory] = None
    # add_history_to_messages=true adds messages from the chat history to the messages list sent to the Model.
    add_history_to_messages: bool = False
    # Number of historical responses to add to the messages.
    num_history_responses: int = 3

    # --- Agent Knowledge ---
    knowledge: Optional[AgentKnowledge] = None
    # Enable RAG by adding references from AgentKnowledge to the user prompt.
    add_references: bool = False
    # Function to get references to add to the user_message
    # This function, if provided, is called when add_references is True
    # Signature:
    # def retriever(agent: Agent, query: str, num_documents: Optional[int], **kwargs) -> Optional[list[dict]]:
    #     ...
    retriever: Optional[Callable[..., Optional[List[Dict]]]] = None
    references_format: Literal["json", "yaml"] = "json"

    # --- Agent Storage ---
    storage: Optional[AgentStorage] = None
    # Extra data stored with this agent
    extra_data: Optional[Dict[str, Any]] = None

    # --- Agent Tools ---
    # A list of tools provided to the Model.
    # Tools are functions the model may generate JSON inputs for.
    # If you provide a dict, it is not called by the model.
    tools: Optional[List[Union[Toolkit, Callable, Function, Dict]]] = None
    # Show tool calls in Agent response.
    show_tool_calls: bool = False
    # Maximum number of tool calls allowed.
    tool_call_limit: Optional[int] = None
    # Controls which (if any) tool is called by the model.
    # "none" means the model will not call a tool and instead generates a message.
    # "auto" means the model can pick between generating a message or calling a tool.
    # Specifying a particular function via {"type: "function", "function": {"name": "my_function"}}
    #   forces the model to call that tool.
    # "none" is the default when no tools are present. "auto" is the default if tools are present.
    tool_choice: Optional[Union[str, Dict[str, Any]]] = None

    # --- Agent Reasoning ---
    # Enable reasoning by working through the problem step by step.
    reasoning: bool = False
    reasoning_model: Optional[Model] = None
    reasoning_agent: Optional[Agent] = None
    reasoning_min_steps: int = 1
    reasoning_max_steps: int = 10

    # --- Default tools ---
    # Add a tool that allows the Model to read the chat history.
    read_chat_history: bool = False
    # Add a tool that allows the Model to search the knowledge base (aka Agentic RAG)
    # Added only if knowledge is provided.
    search_knowledge: bool = True
    # Add a tool that allows the Model to update the knowledge base.
    update_knowledge: bool = False
    # Add a tool that allows the Model to get the tool call history.
    read_tool_call_history: bool = False

    # --- System message settings ---
    # Provide the system message as a string or function
    system_message: Optional[Union[str, Callable, Message]] = None
    # Role for the system message
    system_message_role: str = "system"
    # If True, create a default system message using agent settings and use that
    create_default_system_message: bool = True

    # --- Settings for building the default system message ---
    # A description of the Agent that is added to the start of the system message.
    description: Optional[str] = None
    # The goal of this task
    goal: Optional[str] = None
    # List of instructions for the agent.
    instructions: Optional[Union[str, List[str], Callable]] = None
    # Provide the expected output from the Agent.
    expected_output: Optional[str] = None
    # Additional context added to the end of the system message.
    additional_context: Optional[str] = None
    # If markdown=true, add instructions to format the output using markdown
    markdown: bool = False
    # If True, add the agent name to the instructions
    add_name_to_instructions: bool = False
    # If True, add the current datetime to the instructions to give the agent a sense of time
    # This allows for relative times like "tomorrow" to be used in the prompt
    add_datetime_to_instructions: bool = False
    # If True, add the session state variables in the user and system messages
    add_state_in_messages: bool = False

    # --- Extra Messages ---
    # A list of extra messages added after the system message and before the user message.
    # Use these for few-shot learning or to provide additional context to the Model.
    # Note: these are not retained in memory, they are added directly to the messages sent to the model.
    add_messages: Optional[List[Union[Dict, Message]]] = None

    # --- User message settings ---
    # Provide the user message as a string, list, dict, or function
    # Note: this will ignore the message sent to the run function
    user_message: Optional[Union[List, Dict, str, Callable, Message]] = None
    # Role for the user message
    user_message_role: str = "user"
    # If True, create a default user message using references and chat history
    create_default_user_message: bool = True

    # --- Agent Response Settings ---
    # Number of retries to attempt
    retries: int = 0
    # Delay between retries
    delay_between_retries: int = 1
    # Exponential backoff: if True, the delay between retries is doubled each time
    exponential_backoff: bool = False
    # Provide a response model to get the response as a Pydantic model
    response_model: Optional[Type[BaseModel]] = None
    # If True, the response from the Model is converted into the response_model
    # Otherwise, the response is returned as a JSON string
    parse_response: bool = True
    # Use model enforced structured_outputs if supported (e.g. OpenAIChat)
    structured_outputs: bool = False
    # Save the response to a file
    save_response_to_file: Optional[str] = None

    # --- Agent Streaming ---
    # Stream the response from the Agent
    stream: Optional[bool] = None
    # Stream the intermediate steps from the Agent
    stream_intermediate_steps: bool = False

    # --- Agent Team ---
    # The team of agents that this agent can transfer tasks to.
    team: Optional[List[Agent]] = None
    team_data: Optional[Dict[str, Any]] = None
    # --- If this Agent is part of a team ---
    # If this Agent is part of a team, this is the role of the agent in the team
    role: Optional[str] = None
    # If this Agent is part of a team, this member agent will respond directly to the user
    # instead of passing the response to the leader agent
    respond_directly: bool = False
    # --- Transfer instructions ---
    # Add instructions for transferring tasks to team members
    add_transfer_instructions: bool = True
    # Separator between responses from the team
    team_response_separator: str = "\n"

    # --- Debug & Monitoring ---
    # Enable debug logs
    debug_mode: bool = False
    # monitoring=True logs Agent information to agno.com for monitoring
    monitoring: bool = False
    # telemetry=True logs minimal telemetry for analytics
    # This helps us improve the Agent and provide better support
    telemetry: bool = True

    # --- Run Info: DO NOT SET ---
    run_id: Optional[str] = None
    run_input: Optional[Union[str, List, Dict, Message]] = None
    run_messages: Optional[RunMessages] = None
    run_response: Optional[RunResponse] = None
    # Images generated during this session
    images: Optional[List[ImageArtifact]] = None
    # Videos generated during this session
    videos: Optional[List[VideoArtifact]] = None
    # Audio generated during this session
    audio: Optional[List[AudioArtifact]] = None
    # Agent session
    agent_session: Optional[AgentSession] = None

    _formatter: Optional[SafeFormatter] = None

    def __init__(
        self,
        *,
        model: Optional[Model] = None,
        name: Optional[str] = None,
        agent_id: Optional[str] = None,
        introduction: Optional[str] = None,
        user_id: Optional[str] = None,
        session_id: Optional[str] = None,
        session_name: Optional[str] = None,
        session_state: Optional[Dict[str, Any]] = None,
        context: Optional[Dict[str, Any]] = None,
        add_context: bool = False,
        resolve_context: bool = True,
        memory: Optional[AgentMemory] = None,
        add_history_to_messages: bool = False,
        num_history_responses: int = 3,
        knowledge: Optional[AgentKnowledge] = None,
        add_references: bool = False,
        retriever: Optional[Callable[..., Optional[List[Dict]]]] = None,
        references_format: Literal["json", "yaml"] = "json",
        storage: Optional[AgentStorage] = None,
        extra_data: Optional[Dict[str, Any]] = None,
        tools: Optional[List[Union[Toolkit, Callable, Function, Dict]]] = None,
        show_tool_calls: bool = False,
        tool_call_limit: Optional[int] = None,
        tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
        reasoning: bool = False,
        reasoning_model: Optional[Model] = None,
        reasoning_agent: Optional[Agent] = None,
        reasoning_min_steps: int = 1,
        reasoning_max_steps: int = 10,
        read_chat_history: bool = False,
        search_knowledge: bool = True,
        update_knowledge: bool = False,
        read_tool_call_history: bool = False,
        system_message: Optional[Union[str, Callable, Message]] = None,
        system_message_role: str = "system",
        create_default_system_message: bool = True,
        description: Optional[str] = None,
        goal: Optional[str] = None,
        instructions: Optional[Union[str, List[str], Callable]] = None,
        expected_output: Optional[str] = None,
        additional_context: Optional[str] = None,
        markdown: bool = False,
        add_name_to_instructions: bool = False,
        add_datetime_to_instructions: bool = False,
        add_state_in_messages: bool = False,
        add_messages: Optional[List[Union[Dict, Message]]] = None,
        user_message: Optional[Union[List, Dict, str, Callable, Message]] = None,
        user_message_role: str = "user",
        create_default_user_message: bool = True,
        retries: int = 0,
        delay_between_retries: int = 1,
        exponential_backoff: bool = False,
        response_model: Optional[Type[BaseModel]] = None,
        parse_response: bool = True,
        structured_outputs: bool = False,
        save_response_to_file: Optional[str] = None,
        stream: Optional[bool] = None,
        stream_intermediate_steps: bool = False,
        team: Optional[List[Agent]] = None,
        team_data: Optional[Dict[str, Any]] = None,
        role: Optional[str] = None,
        respond_directly: bool = False,
        add_transfer_instructions: bool = True,
        team_response_separator: str = "\n",
        debug_mode: bool = False,
        monitoring: bool = False,
        telemetry: bool = True,
    ):
        self.model = model
        self.name = name
        self.agent_id = agent_id
        self.introduction = introduction

        self.user_id = user_id

        self.session_id = session_id
        self.session_name = session_name
        self.session_state = session_state

        self.context = context
        self.add_context = add_context
        self.resolve_context = resolve_context

        self.memory = memory
        self.add_history_to_messages = add_history_to_messages
        self.num_history_responses = num_history_responses

        self.knowledge = knowledge
        self.add_references = add_references
        self.retriever = retriever
        self.references_format = references_format

        self.storage = storage
        self.extra_data = extra_data

        self.tools = tools
        self.show_tool_calls = show_tool_calls
        self.tool_call_limit = tool_call_limit
        self.tool_choice = tool_choice

        self.reasoning = reasoning
        self.reasoning_model = reasoning_model
        self.reasoning_agent = reasoning_agent
        self.reasoning_min_steps = reasoning_min_steps
        self.reasoning_max_steps = reasoning_max_steps

        self.read_chat_history = read_chat_history
        self.search_knowledge = search_knowledge
        self.update_knowledge = update_knowledge
        self.read_tool_call_history = read_tool_call_history

        self.system_message = system_message
        self.system_message_role = system_message_role
        self.create_default_system_message = create_default_system_message

        self.description = description
        self.goal = goal
        self.instructions = instructions
        self.expected_output = expected_output
        self.additional_context = additional_context
        self.markdown = markdown
        self.add_name_to_instructions = add_name_to_instructions
        self.add_datetime_to_instructions = add_datetime_to_instructions
        self.add_state_in_messages = add_state_in_messages
        self.add_messages = add_messages

        self.user_message = user_message
        self.user_message_role = user_message_role
        self.create_default_user_message = create_default_user_message

        self.retries = retries
        self.delay_between_retries = delay_between_retries
        self.exponential_backoff = exponential_backoff
        self.response_model = response_model
        self.parse_response = parse_response
        self.structured_outputs = structured_outputs
        self.save_response_to_file = save_response_to_file

        self.stream = stream
        self.stream_intermediate_steps = stream_intermediate_steps

        self.team = team
        self.team_data = team_data
        self.role = role
        self.respond_directly = respond_directly
        self.add_transfer_instructions = add_transfer_instructions
        self.team_response_separator = team_response_separator

        self.debug_mode = debug_mode
        self.monitoring = monitoring
        self.telemetry = telemetry

        self.run_id = None
        self.run_input = None
        self.run_messages = None
        self.run_response = None
        self.images = None
        self.videos = None
        self.audio = None

        self.agent_session = None
        self._formatter = None

    def set_agent_id(self) -> str:
        if self.agent_id is None:
            self.agent_id = str(uuid4())
        logger.debug(f"*********** Agent ID: {self.agent_id} ***********")
        return self.agent_id

    def set_session_id(self) -> str:
        if self.session_id is None or self.session_id == "":
            self.session_id = str(uuid4())
        logger.debug(f"*********** Session ID: {self.session_id} ***********")
        return self.session_id

    def set_debug(self) -> None:
        if self.debug_mode or getenv("AGNO_DEBUG", "false").lower() == "true":
            self.debug_mode = True
            set_log_level_to_debug()
        else:
            set_log_level_to_info()

    def set_monitoring(self) -> None:
        if self.monitoring or getenv("AGNO_MONITOR", "false").lower() == "true":
            self.monitoring = True
        else:
            self.monitoring = False

        if self.telemetry or getenv("AGNO_TELEMETRY", "true").lower() == "true":
            self.telemetry = True
        else:
            self.telemetry = False

    def initialize_agent(self) -> None:
        self.set_debug()
        self.set_agent_id()
        self.set_session_id()
        if self._formatter is None:
            self._formatter = SafeFormatter()
        if self.memory is None:
            self.memory = AgentMemory()

    @property
    def is_streamable(self) -> bool:
        return self.response_model is None

    @property
    def has_team(self) -> bool:
        return self.team is not None and len(self.team) > 0

    def _run(
        self,
        message: Optional[Union[str, List, Dict, Message]] = None,
        *,
        stream: bool = False,
        audio: Optional[Sequence[Audio]] = None,
        images: Optional[Sequence[Image]] = None,
        videos: Optional[Sequence[Video]] = None,
        messages: Optional[Sequence[Union[Dict, Message]]] = None,
        stream_intermediate_steps: bool = False,
        **kwargs: Any,
    ) -> Iterator[RunResponse]:
        """Run the Agent and yield the RunResponse.

        Steps:
        1. Prepare the Agent for the run
        2. Update the Model and resolve context
        3. Read existing session from storage
        4. Prepare run messages
        5. Prepare run steps
        6. Start the Run by yielding a RunStarted event
        7. Run Agent Steps
        8. Update RunResponse
        9. Update Agent Memory
        10. Save session to storage
        11. Save output to file if save_response_to_file is set
        """
        # 1. Prepare the Agent for the run
        # 1.1 Initialize the Agent
        self.initialize_agent()
        self.memory = cast(AgentMemory, self.memory)
        # 1.2 Set streaming and stream intermediate steps
        self.stream = self.stream or (stream and self.is_streamable)
        self.stream_intermediate_steps = self.stream_intermediate_steps or (stream_intermediate_steps and self.stream)
        # 1.3 Create a run_id and RunResponse
        self.run_id = str(uuid4())
        self.run_response = RunResponse(run_id=self.run_id, session_id=self.session_id, agent_id=self.agent_id)

        logger.debug(f"*********** Agent Run Start: {self.run_response.run_id} ***********")

        # 2. Update the Model and resolve context
        self.update_model()
        self.run_response.model = self.model.id if self.model is not None else None
        if self.context is not None and self.resolve_context:
            self.resolve_run_context()

        # 3. Read existing session from storage
        self.read_from_storage()

        # 4. Prepare run messages
        run_messages: RunMessages = self.get_run_messages(
            message=message, audio=audio, images=images, videos=videos, messages=messages, **kwargs
        )
        self.run_messages = run_messages

        # 4. Reason about the task if reasoning is enabled
        if self.reasoning or self.reasoning_model is not None:
            reasoning_generator = self.reason(run_messages=run_messages)

            if self.stream:
                yield from reasoning_generator
            else:
                # Consume the generator without yielding
                deque(reasoning_generator, maxlen=0)

        # Get the index of the last "user" message in messages_for_run
        # We track this, so we can add messages after this index to the RunResponse and Memory
        index_of_last_user_message = len(run_messages.messages)

        # 6. Start the Run by yielding a RunStarted event
        if self.stream_intermediate_steps:
            yield self.create_run_response("Run started", event=RunEvent.run_started)

        # 5. Generate a response from the Model (includes running function calls)
        model_response: ModelResponse
        self.model = cast(Model, self.model)
        if self.stream:
            model_response = ModelResponse(content="")
            for model_response_chunk in self.model.response_stream(messages=run_messages.messages):
                # If the model response is an assistant_response, yield a RunResponse with the content
                if model_response_chunk.event == ModelResponseEvent.assistant_response.value:
                    if model_response_chunk.content is not None and model_response.content is not None:
                        model_response.content += model_response_chunk.content
                        # Update the run_response with the content
                        self.run_response.content = model_response_chunk.content
                        self.run_response.created_at = model_response_chunk.created_at
                        yield self.create_run_response(
                            content=model_response_chunk.content, created_at=model_response_chunk.created_at
                        )
                # If the model response is a tool_call_started, add the tool call to the run_response
                elif model_response_chunk.event == ModelResponseEvent.tool_call_started.value:
                    # Add tool calls to the run_response
                    tool_calls_list = model_response_chunk.tool_calls
                    if tool_calls_list is not None:
                        # Add tool calls to the agent.run_response
                        if self.run_response.tools is None:
                            self.run_response.tools = tool_calls_list
                        else:
                            self.run_response.tools.extend(tool_calls_list)

                    # If the agent is streaming intermediate steps, yield a RunResponse with the tool_call_started event
                    if self.stream_intermediate_steps:
                        yield self.create_run_response(
                            content=model_response_chunk.content,
                            event=RunEvent.tool_call_started,
                        )

                # If the model response is a tool_call_completed, update the existing tool call in the run_response
                elif model_response_chunk.event == ModelResponseEvent.tool_call_completed.value:
                    tool_calls_list = model_response_chunk.tool_calls
                    if tool_calls_list is not None:
                        # Update the existing tool call in the run_response
                        if self.run_response.tools:
                            # Create a mapping of tool_call_id to index
                            tool_call_index_map = {
                                tc["tool_call_id"]: i for i, tc in enumerate(self.run_response.tools)
                            }
                            # Process tool calls
                            for tool_call_dict in tool_calls_list:
                                tool_call_id = tool_call_dict["tool_call_id"]
                                index = tool_call_index_map.get(tool_call_id)
                                if index is not None:
                                    self.run_response.tools[index] = tool_call_dict
                        else:
                            self.run_response.tools = tool_calls_list

                        if self.stream_intermediate_steps:
                            yield self.create_run_response(
                                content=model_response_chunk.content,
                                event=RunEvent.tool_call_completed,
                            )
        else:
            # Get the model response
            model_response = self.model.response(messages=run_messages.messages)
            # Handle structured outputs
            if self.response_model is not None and self.structured_outputs and model_response.parsed is not None:
                # Update the run_response content with the structured output
                self.run_response.content = model_response.parsed
                # Update the run_response content_type with the structured output class name
                self.run_response.content_type = self.response_model.__name__
            else:
                # Update the run_response content with the model response content
                self.run_response.content = model_response.content

            # Update the run_response tools with the model response tools
            if model_response.tool_calls is not None:
                if self.run_response.tools is None:
                    self.run_response.tools = model_response.tool_calls
                else:
                    self.run_response.tools.extend(model_response.tool_calls)

            # Update the run_response audio with the model response audio
            if model_response.audio is not None:
                self.run_response.response_audio = model_response.audio

            # Update the run_response messages with the messages
            self.run_response.messages = run_messages.messages
            # Update the run_response created_at with the model response created_at
            self.run_response.created_at = model_response.created_at

        # 8. Update RunResponse
        # Build a list of messages that should be added to the RunResponse
        messages_for_run_response = [m for m in run_messages.messages if m.add_to_agent_memory]
        # Update the RunResponse messages
        self.run_response.messages = messages_for_run_response
        # Update the RunResponse metrics
        self.run_response.metrics = self.aggregate_metrics_from_messages(messages_for_run_response)

        # Update the run_response content if streaming as run_response will only contain the last chunk
        if self.stream:
            self.run_response.content = model_response.content
            if model_response.audio is not None:
                self.run_response.response_audio = model_response.audio

        # 9. Update Agent Memory
        # Add the system message to the memory
        if run_messages.system_message is not None:
            self.memory.add_system_message(
                run_messages.system_message, system_message_role=self.get_system_message_role()
            )

        # Build a list of messages that should be added to the AgentMemory
        messages_for_memory: List[Message] = (
            [run_messages.user_message] if run_messages.user_message is not None else []
        )
        # Add messages from messages_for_run after the last user message
        for _rm in run_messages.messages[index_of_last_user_message:]:
            if _rm.add_to_agent_memory:
                messages_for_memory.append(_rm)
        if len(messages_for_memory) > 0:
            self.memory.add_messages(messages=messages_for_memory)

        # Yield UpdatingMemory event
        if self.stream_intermediate_steps:
            yield self.create_run_response(
                content="Memory updated",
                event=RunEvent.updating_memory,
            )

        # Create an AgentRun object to add to memory
        agent_run = AgentRun(response=self.run_response)
        agent_run.message = run_messages.user_message
        # Update the memories with the user message if needed
        if (
            self.memory.create_user_memories
            and self.memory.update_user_memories_after_run
            and run_messages.user_message is not None
        ):
            self.memory.update_memory(input=run_messages.user_message.get_content_string())
        if messages is not None and len(messages) > 0:
            for _im in messages:
                # Parse the message and convert to a Message object if possible
                mp = None
                if isinstance(_im, Message):
                    mp = _im
                elif isinstance(_im, dict):
                    try:
                        mp = Message(**_im)
                    except Exception as e:
                        logger.warning(f"Failed to validate message: {e}")
                else:
                    logger.warning(f"Unsupported message type: {type(_im)}")
                    continue

                # Add the message to the AgentRun
                if mp:
                    if agent_run.messages is None:
                        agent_run.messages = []
                    agent_run.messages.append(mp)
                    if self.memory.create_user_memories and self.memory.update_user_memories_after_run:
                        self.memory.update_memory(input=mp.get_content_string())
                else:
                    logger.warning("Unable to add message to memory")
        # Add AgentRun to memory
        self.memory.add_run(agent_run)
        # Update the session summary if needed
        if self.memory.create_session_summary and self.memory.update_session_summary_after_run:
            self.memory.update_summary()

        # 10. Save session to storage
        self.write_to_storage()

        # 11. Save output to file if save_response_to_file is set
        self.save_run_response_to_file(message=message)

        # Set run_input
        if message is not None:
            if isinstance(message, str):
                self.run_input = message
            elif isinstance(message, Message):
                self.run_input = message.to_dict()
            else:
                self.run_input = message
        elif messages is not None:
            self.run_input = [m.to_dict() if isinstance(m, Message) else m for m in messages]

        # Log Agent Run
        self.log_agent_run()

        logger.debug(f"*********** Agent Run End: {self.run_response.run_id} ***********")
        if self.stream_intermediate_steps:
            yield self.create_run_response(
                content=self.run_response.content,
                event=RunEvent.run_completed,
            )

        # Yield final response if not streaming so that run() can get the response
        if not self.stream:
            yield self.run_response

    @overload
    def run(
        self,
        message: Optional[Union[str, List, Dict, Message]] = None,
        *,
        stream: Literal[False] = False,
        audio: Optional[Sequence[Audio]] = None,
        images: Optional[Sequence[Image]] = None,
        videos: Optional[Sequence[Video]] = None,
        messages: Optional[Sequence[Union[Dict, Message]]] = None,
        stream_intermediate_steps: bool = False,
        retries: Optional[int] = None,
        **kwargs: Any,
    ) -> RunResponse: ...

    @overload
    def run(
        self,
        message: Optional[Union[str, List, Dict, Message]] = None,
        *,
        stream: Literal[True] = True,
        audio: Optional[Sequence[Audio]] = None,
        images: Optional[Sequence[Image]] = None,
        videos: Optional[Sequence[Video]] = None,
        messages: Optional[Sequence[Union[Dict, Message]]] = None,
        stream_intermediate_steps: bool = False,
        retries: Optional[int] = None,
        **kwargs: Any,
    ) -> Iterator[RunResponse]: ...

    def run(
        self,
        message: Optional[Union[str, List, Dict, Message]] = None,
        *,
        stream: bool = False,
        audio: Optional[Sequence[Audio]] = None,
        images: Optional[Sequence[Image]] = None,
        videos: Optional[Sequence[Any]] = None,
        messages: Optional[Sequence[Union[Dict, Message]]] = None,
        stream_intermediate_steps: bool = False,
        retries: Optional[int] = None,
        **kwargs: Any,
    ) -> Union[RunResponse, Iterator[RunResponse]]:
        """Run the Agent and return the response."""

        # If no retries are set, use the agent's default retries
        if retries is None:
            retries = self.retries

        last_exception = None
        num_attempts = retries + 1
        for attempt in range(num_attempts):
            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")

                    # Set stream=False and run the agent
                    logger.debug("Setting stream=False as response_model is set")
                    self.stream = False
                    run_response: RunResponse = next(
                        self._run(
                            message=message,
                            stream=False,
                            audio=audio,
                            images=images,
                            videos=videos,
                            messages=messages,
                            stream_intermediate_steps=stream_intermediate_steps,
                            **kwargs,
                        )
                    )

                    # If the model natively supports structured outputs, the content is already in the structured format
                    if self.structured_outputs:
                        # Do a final check confirming the content is in the response_model format
                        if isinstance(run_response.content, self.response_model):
                            return run_response

                    # Otherwise convert the response to the structured format
                    if isinstance(run_response.content, str):
                        try:
                            from pydantic import ValidationError

                            structured_output = None
                            try:
                                structured_output = self.response_model.model_validate_json(run_response.content)
                            except ValidationError:
                                # Check if response starts with ```json
                                if run_response.content.startswith("```json"):
                                    run_response.content = run_response.content.replace("```json\n", "").replace(
                                        "\n```", ""
                                    )
                                    try:
                                        structured_output = self.response_model.model_validate_json(
                                            run_response.content
                                        )
                                    except Exception as e:
                                        logger.warning(f"Failed to convert response to pydantic model: {e}")

                            # Update RunResponse
                            if structured_output is not None:
                                run_response.content = structured_output
                                run_response.content_type = self.response_model.__name__
                                if self.run_response is not None:
                                    self.run_response.content = structured_output
                                    self.run_response.content_type = self.response_model.__name__
                            else:
                                logger.warning("Failed to convert response to response_model")
                        except Exception as e:
                            logger.warning(f"Failed to convert response to output model: {e}")
                    else:
                        logger.warning("Something went wrong. Run response content is not a string")
                    return run_response
                else:
                    if stream and self.is_streamable:
                        resp = self._run(
                            message=message,
                            stream=True,
                            audio=audio,
                            images=images,
                            videos=videos,
                            messages=messages,
                            stream_intermediate_steps=stream_intermediate_steps,
                            **kwargs,
                        )
                        return resp
                    else:
                        resp = self._run(
                            message=message,
                            stream=False,
                            audio=audio,
                            images=images,
                            videos=videos,
                            messages=messages,
                            stream_intermediate_steps=stream_intermediate_steps,
                            **kwargs,
                        )
                        return next(resp)
            except AgentRunException as e:
                logger.warning(f"Attempt {attempt + 1}/{num_attempts} failed: {str(e)}")
                if isinstance(e, StopAgentRun):
                    raise e
                last_exception = e
                if attempt < num_attempts - 1:  # Don't sleep on the last attempt
                    if self.exponential_backoff:
                        delay = 2**attempt * self.delay_between_retries
                    else:
                        delay = self.delay_between_retries
                    import time

                    time.sleep(delay)

        # If we get here, all retries failed
        raise Exception(f"Failed after {num_attempts} attempts. Last error: {str(last_exception)}")

    async def _arun(
        self,
        message: Optional[Union[str, List, Dict, Message]] = None,
        *,
        stream: bool = False,
        audio: Optional[Sequence[Audio]] = None,
        images: Optional[Sequence[Image]] = None,
        videos: Optional[Sequence[Video]] = None,
        messages: Optional[Sequence[Union[Dict, Message]]] = None,
        stream_intermediate_steps: bool = False,
        **kwargs: Any,
    ) -> AsyncIterator[RunResponse]:
        """Run the Agent and yield the RunResponse.

        Steps:
        1. Prepare the Agent for the run
        2. Update the Model and resolve context
        3. Read existing session from storage
        4. Prepare run messages
        5. Prepare run steps
        6. Start the Run by yielding a RunStarted event
        7. Run Agent Steps
        8. Update RunResponse
        9. Update Agent Memory
        10. Save session to storage
        11. Save output to file if save_response_to_file is set
        """

        # 1. Prepare the Agent for the run
        # 1.1 Initialize the Agent
        self.initialize_agent()
        self.memory = cast(AgentMemory, self.memory)
        # 1.2 Set streaming and stream intermediate steps
        self.stream = self.stream or (stream and self.is_streamable)
        self.stream_intermediate_steps = self.stream_intermediate_steps or (stream_intermediate_steps and self.stream)
        # 1.3 Create a run_id and RunResponse
        self.run_id = str(uuid4())
        self.run_response = RunResponse(run_id=self.run_id, session_id=self.session_id, agent_id=self.agent_id)

        logger.debug(f"*********** Async Agent Run Start: {self.run_response.run_id} ***********")

        # 2. Update the Model and resolve context
        self.update_model()
        self.run_response.model = self.model.id if self.model is not None else None
        if self.context is not None and self.resolve_context:
            self.resolve_run_context()

        # 3. Read existing session from storage
        self.read_from_storage()

        # 4. Prepare run messages
        run_messages: RunMessages = self.get_run_messages(
            message=message, audio=audio, images=images, videos=videos, messages=messages, **kwargs
        )
        self.run_messages = run_messages

        # 4. Reason about the task if reasoning is enabled
        if self.reasoning or self.reasoning_model is not None:
            areason_generator = self.areason(run_messages=run_messages)
            if self.stream:
                async for item in areason_generator:
                    yield item
            else:
                # Consume the generator without yielding
                async for _ in areason_generator:
                    pass

        # Get the index of the last "user" message in messages_for_run
        # We track this so we can add messages after this index to the RunResponse and Memory
        index_of_last_user_message = len(run_messages.messages)

        # 6. Start the Run by yielding a RunStarted event
        if self.stream_intermediate_steps:
            yield self.create_run_response("Run started", event=RunEvent.run_started)

        # 5. Generate a response from the Model (includes running function calls)
        model_response: ModelResponse
        self.model = cast(Model, self.model)
        if stream and self.is_streamable:
            model_response = ModelResponse(content="")
            model_response_stream = self.model.aresponse_stream(messages=run_messages.messages)  # type: ignore
            async for model_response_chunk in model_response_stream:  # type: ignore
                # If the model response is an assistant_response, yield a RunResponse with the content
                if model_response_chunk.event == ModelResponseEvent.assistant_response.value:
                    if model_response_chunk.content is not None and model_response.content is not None:
                        model_response.content += model_response_chunk.content
                        # Update the run_response with the content
                        self.run_response.content = model_response_chunk.content
                        self.run_response.created_at = model_response_chunk.created_at
                        yield self.create_run_response(
                            content=model_response_chunk.content, created_at=model_response_chunk.created_at
                        )
                # If the model response is a tool_call_started, add the tool call to the run_response
                elif model_response_chunk.event == ModelResponseEvent.tool_call_started.value:
                    # Add tool calls to the run_response
                    tool_calls_list = model_response_chunk.tool_calls
                    if tool_calls_list is not None:
                        # Add tool calls to the agent.run_response
                        if self.run_response.tools is None:
                            self.run_response.tools = tool_calls_list
                        else:
                            self.run_response.tools.extend(tool_calls_list)

                    # If the agent is streaming intermediate steps, yield a RunResponse with the tool_call_started event
                    if self.stream_intermediate_steps:
                        yield self.create_run_response(
                            content=model_response_chunk.content,
                            event=RunEvent.tool_call_started,
                        )
                # If the model response is a tool_call_completed, update the existing tool call in the run_response
                elif model_response_chunk.event == ModelResponseEvent.tool_call_completed.value:
                    tool_calls_list = model_response_chunk.tool_calls
                    if tool_calls_list is not None:
                        # Update the existing tool call in the run_response
                        if self.run_response.tools:
                            # Create a mapping of tool_call_id to index
                            tool_call_index_map = {
                                tc["tool_call_id"]: i for i, tc in enumerate(self.run_response.tools)
                            }
                            # Process tool calls
                            for tool_call_dict in tool_calls_list:
                                tool_call_id = tool_call_dict["tool_call_id"]
                                index = tool_call_index_map.get(tool_call_id)
                                if index is not None:
                                    self.run_response.tools[index] = tool_call_dict
                        else:
                            self.run_response.tools = tool_calls_list

                    if self.stream_intermediate_steps:
                        yield self.create_run_response(
                            content=model_response_chunk.content,
                            event=RunEvent.tool_call_completed,
                        )
        else:
            # Get the model response
            model_response = await self.model.aresponse(messages=run_messages.messages)
            # Handle structured outputs
            if self.response_model is not None and self.structured_outputs and model_response.parsed is not None:
                # Update the run_response content with the structured output
                self.run_response.content = model_response.parsed
                # Update the run_response content_type with the structured output class name
                self.run_response.content_type = self.response_model.__name__
            else:
                # Update the run_response content with the model response content
                self.run_response.content = model_response.content
            # Update the run_response tools with the model response tools
            if model_response.tool_calls is not None:
                if self.run_response.tools is None:
                    self.run_response.tools = model_response.tool_calls
                else:
                    self.run_response.tools.extend(model_response.tool_calls)
            # Update the run_response audio with the model response audio
            if model_response.audio is not None:
                self.run_response.response_audio = model_response.audio

            # Update the run_response messages with the messages
            self.run_response.messages = run_messages.messages
            # Update the run_response created_at with the model response created_at
            self.run_response.created_at = model_response.created_at

        # 8. Update RunResponse
        # Build a list of messages that should be added to the RunResponse
        messages_for_run_response = [m for m in run_messages.messages if m.add_to_agent_memory]
        # Update the RunResponse messages
        self.run_response.messages = messages_for_run_response
        # Update the RunResponse metrics
        self.run_response.metrics = self.aggregate_metrics_from_messages(messages_for_run_response)

        # Update the run_response content if streaming as run_response will only contain the last chunk
        if self.stream:
            self.run_response.content = model_response.content
            if model_response.audio is not None:
                self.run_response.response_audio = model_response.audio

        # 9. Update Agent Memory
        # Add the system message to the memory
        if run_messages.system_message is not None:
            self.memory.add_system_message(
                run_messages.system_message, system_message_role=self.get_system_message_role()
            )

        # Build a list of messages that should be added to the AgentMemory
        messages_for_memory: List[Message] = (
            [run_messages.user_message] if run_messages.user_message is not None else []
        )
        # Add messages from messages_for_run after the last user message
        for _rm in run_messages.messages[index_of_last_user_message:]:
            if _rm.add_to_agent_memory:
                messages_for_memory.append(_rm)
        if len(messages_for_memory) > 0:
            self.memory.add_messages(messages=messages_for_memory)

        # Yield UpdatingMemory event
        if self.stream_intermediate_steps:
            yield self.create_run_response(
                content="Memory updated",
                event=RunEvent.updating_memory,
            )

        # Create an AgentRun object to add to memory
        agent_run = AgentRun(response=self.run_response)
        agent_run.message = run_messages.user_message
        # Update the memories with the user message if needed
        if (
            self.memory.create_user_memories
            and self.memory.update_user_memories_after_run
            and run_messages.user_message is not None
        ):
            await self.memory.aupdate_memory(input=run_messages.user_message.get_content_string())
        if messages is not None and len(messages) > 0:
            for _im in messages:
                # Parse the message and convert to a Message object if possible
                mp = None
                if isinstance(_im, Message):
                    mp = _im
                elif isinstance(_im, dict):
                    try:
                        mp = Message(**_im)
                    except Exception as e:
                        logger.warning(f"Failed to validate message: {e}")
                else:
                    logger.warning(f"Unsupported message type: {type(_im)}")
                    continue

                # Add the message to the AgentRun
                if mp:
                    if agent_run.messages is None:
                        agent_run.messages = []
                    agent_run.messages.append(mp)
                    if self.memory.create_user_memories and self.memory.update_user_memories_after_run:
                        await self.memory.aupdate_memory(input=mp.get_content_string())
                else:
                    logger.warning("Unable to add message to memory")
        # Add AgentRun to memory
        self.memory.add_run(agent_run)
        # Update the session summary if needed
        if self.memory.create_session_summary and self.memory.update_session_summary_after_run:
            await self.memory.aupdate_summary()

        # 10. Save session to storage
        self.write_to_storage()

        # 11. Save output to file if save_response_to_file is set
        self.save_run_response_to_file(message=message)

        # Set run_input
        if message is not None:
            if isinstance(message, str):
                self.run_input = message
            elif isinstance(message, Message):
                self.run_input = message.to_dict()
            else:
                self.run_input = message
        elif messages is not None:
            self.run_input = [m.to_dict() if isinstance(m, Message) else m for m in messages]

        # Log Agent Run
        await self.alog_agent_run()

        logger.debug(f"*********** Agent Run End: {self.run_response.run_id} ***********")
        if self.stream_intermediate_steps:
            yield self.create_run_response(
                content=self.run_response.content,
                event=RunEvent.run_completed,
            )

        # Yield final response if not streaming so that run() can get the response
        if not self.stream:
            yield self.run_response

    async def arun(
        self,
        message: Optional[Union[str, List, Dict, Message]] = None,
        *,
        stream: bool = False,
        audio: Optional[Sequence[Audio]] = None,
        images: Optional[Sequence[Image]] = None,
        videos: Optional[Sequence[Video]] = None,
        messages: Optional[Sequence[Union[Dict, Message]]] = None,
        stream_intermediate_steps: bool = False,
        retries: Optional[int] = None,
        **kwargs: Any,
    ) -> Any:
        """Async Run the Agent and return the response."""

        # If no retries are set, use the agent's default retries
        if retries is None:
            retries = self.retries

        last_exception = None
        num_attempts = retries + 1
        for attempt in range(num_attempts):
            logger.debug(f"Attempt {attempt + 1}/{num_attempts}")
            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")

                    # Set stream=False and run the agent
                    logger.debug("Setting stream=False as response_model is set")
                    run_response = await self._arun(
                        message=message,
                        stream=False,
                        audio=audio,
                        images=images,
                        videos=videos,
                        messages=messages,
                        stream_intermediate_steps=stream_intermediate_steps,
                        **kwargs,
                    ).__anext__()

                    # If the model natively supports structured outputs, the content is already in the structured format
                    if self.structured_outputs:
                        # Do a final check confirming the content is in the response_model format
                        if isinstance(run_response.content, self.response_model):
                            return run_response

                    # Otherwise convert the response to the structured format
                    if isinstance(run_response.content, str):
                        try:
                            from pydantic import ValidationError

                            structured_output = None
                            try:
                                structured_output = self.response_model.model_validate_json(run_response.content)
                            except ValidationError:
                                # Check if response starts with ```json
                                if run_response.content.startswith("```json"):
                                    run_response.content = run_response.content.replace("```json\n", "").replace(
                                        "\n```", ""
                                    )
                                    try:
                                        structured_output = self.response_model.model_validate_json(
                                            run_response.content
                                        )
                                    except Exception as e:
                                        logger.warning(f"Failed to convert response to pydantic model: {e}")

                            # Update RunResponse
                            if structured_output is not None:
                                run_response.content = structured_output
                                run_response.content_type = self.response_model.__name__
                                if self.run_response is not None:
                                    self.run_response.content = structured_output
                                    self.run_response.content_type = self.response_model.__name__
                            else:
                                logger.warning("Failed to convert response to response_model")
                        except Exception as e:
                            logger.warning(f"Failed to convert response to output model: {e}")
                    else:
                        logger.warning("Something went wrong. Run response content is not a string")
                    return run_response
                else:
                    if stream and self.is_streamable:
                        resp = self._arun(
                            message=message,
                            stream=True,
                            audio=audio,
                            images=images,
                            videos=videos,
                            messages=messages,
                            stream_intermediate_steps=stream_intermediate_steps,
                            **kwargs,
                        )
                        return resp
                    else:
                        resp = self._arun(
                            message=message,
                            stream=False,
                            audio=audio,
                            images=images,
                            videos=videos,
                            messages=messages,
                            stream_intermediate_steps=stream_intermediate_steps,
                            **kwargs,
                        )
                        return await resp.__anext__()
            except AgentRunException as e:
                logger.warning(f"Attempt {attempt + 1}/{num_attempts} failed: {str(e)}")
                if isinstance(e, StopAgentRun):
                    raise e
                last_exception = e
                if attempt < num_attempts - 1:  # Don't sleep on the last attempt
                    if self.exponential_backoff:
                        delay = 2**attempt * self.delay_between_retries
                    else:
                        delay = self.delay_between_retries
                    import time

                    time.sleep(delay)

        # If we get here, all retries failed
        raise Exception(f"Failed after {num_attempts} attempts. Last error: {str(last_exception)}")

    def create_run_response(
        self,
        content: Optional[Any] = None,
        *,
        event: RunEvent = RunEvent.run_response,
        content_type: Optional[str] = None,
        created_at: Optional[int] = None,
    ) -> RunResponse:
        self.run_response = cast(RunResponse, self.run_response)
        rr = RunResponse(
            run_id=self.run_id,
            session_id=self.session_id,
            agent_id=self.agent_id,
            content=content,
            tools=self.run_response.tools,
            images=self.run_response.images,
            videos=self.run_response.videos,
            model=self.run_response.model,
            messages=self.run_response.messages,
            extra_data=self.run_response.extra_data,
            event=event.value,
        )
        if content_type is not None:
            rr.content_type = content_type
        if created_at is not None:
            rr.created_at = created_at
        return rr

    def get_tools(self) -> Optional[List[Union[Toolkit, Callable, Dict, Function]]]:
        self.memory = cast(AgentMemory, self.memory)
        tools: List[Union[Toolkit, Callable, Dict, Function]] = []

        # Add provided tools
        if self.tools is not None:
            for tool in self.tools:
                tools.append(tool)

        # Add tools for accessing memory
        if self.read_chat_history:
            tools.append(self.get_chat_history)
        if self.read_tool_call_history:
            tools.append(self.get_tool_call_history)
        if self.memory and self.memory.create_user_memories:
            tools.append(self.update_memory)

        # Add tools for accessing knowledge
        if self.knowledge is not None:
            if self.search_knowledge:
                tools.append(self.search_knowledge_base)
            if self.update_knowledge:
                tools.append(self.add_to_knowledge)

        # Add transfer tools
        if self.team is not None and len(self.team) > 0:
            for agent_index, agent in enumerate(self.team):
                tools.append(self.get_transfer_function(agent, agent_index))

        return tools

    def update_model(self) -> None:
        # Use the default Model (OpenAIChat) if no model is provided
        if self.model is None:
            try:
                from agno.models.openai import OpenAIChat
            except ModuleNotFoundError as e:
                logger.exception(e)
                logger.error(
                    "Agno agents use `openai` as the default model provider. "
                    "Please provide a `model` or install `openai`."
                )
                exit(1)
            self.model = OpenAIChat(id="gpt-4o")

        # Set response_format if it is not set on the Model
        if self.response_model is not None and self.model.response_format is None:
            if self.structured_outputs and self.model.supports_structured_outputs:
                logger.debug("Setting Model.response_format to Agent.response_model")
                self.model.response_format = self.response_model
                self.model.structured_outputs = True
            else:
                self.model.response_format = {"type": "json_object"}

        # Add tools to the Model
        agent_tools = self.get_tools()
        if agent_tools is not None:
            for tool in agent_tools:
                if (
                    self.response_model is not None
                    and self.structured_outputs
                    and self.model.supports_structured_outputs
                ):
                    self.model.add_tool(tool=tool, strict=True, agent=self)
                else:
                    self.model.add_tool(tool=tool, agent=self)

        # Set show_tool_calls if it is not set on the Model
        if self.model.show_tool_calls is None and self.show_tool_calls is not None:
            self.model.show_tool_calls = self.show_tool_calls

        # Set tool_choice to auto if it is not set on the Model
        if self.model.tool_choice is None and self.tool_choice is not None:
            self.model.tool_choice = self.tool_choice

        # Set tool_call_limit if set on the agent
        if self.tool_call_limit is not None:
            self.model.tool_call_limit = self.tool_call_limit

        # Add session_id to the Model
        if self.session_id is not None:
            self.model.session_id = self.session_id

    def resolve_run_context(self) -> None:
        from inspect import signature

        logger.debug("Resolving context")
        if self.context is not None:
            for ctx_key, ctx_value in self.context.items():
                if callable(ctx_value):
                    try:
                        sig = signature(ctx_value)
                        resolved_ctx_value = None
                        if "agent" in sig.parameters:
                            resolved_ctx_value = ctx_value(agent=self)
                        else:
                            resolved_ctx_value = ctx_value()
                        if resolved_ctx_value is not None:
                            self.context[ctx_key] = resolved_ctx_value
                    except Exception as e:
                        logger.warning(f"Failed to resolve context for {ctx_key}: {e}")
                else:
                    self.context[ctx_key] = ctx_value

    def load_user_memories(self) -> None:
        self.memory = cast(AgentMemory, self.memory)
        if self.memory and self.memory.create_user_memories:
            if self.user_id is not None:
                self.memory.user_id = self.user_id

            self.memory.load_user_memories()
            if self.user_id is not None:
                logger.debug(f"Memories loaded for user: {self.user_id}")
            else:
                logger.debug("Memories loaded")

    def get_agent_data(self) -> Dict[str, Any]:
        agent_data: Dict[str, Any] = {}
        if self.name is not None:
            agent_data["name"] = self.name
        if self.agent_id is not None:
            agent_data["agent_id"] = self.agent_id
        if self.model is not None:
            agent_data["model"] = self.model.to_dict()
        return agent_data

    def get_session_data(self) -> Dict[str, Any]:
        session_data: Dict[str, Any] = {}
        if self.session_name is not None:
            session_data["session_name"] = self.session_name
        if self.session_state is not None and len(self.session_state) > 0:
            session_data["session_state"] = self.session_state
        if self.team_data is not None:
            session_data["team_data"] = self.team_data
        if self.images is not None:
            session_data["images"] = [img.model_dump() for img in self.images]  # type: ignore
        if self.videos is not None:
            session_data["videos"] = [vid.model_dump() for vid in self.videos]  # type: ignore
        if self.audio is not None:
            session_data["audio"] = [aud.model_dump() for aud in self.audio]  # type: ignore
        return session_data

    def get_agent_session(self) -> AgentSession:
        """Get an AgentSession object, which can be saved to the database"""
        self.memory = cast(AgentMemory, self.memory)
        self.session_id = cast(str, self.session_id)
        self.agent_id = cast(str, self.agent_id)
        return AgentSession(
            session_id=self.session_id,
            agent_id=self.agent_id,
            user_id=self.user_id,
            memory=self.memory.to_dict() if self.memory is not None else None,
            agent_data=self.get_agent_data(),
            session_data=self.get_session_data(),
            extra_data=self.extra_data,
        )

    def load_agent_session(self, session: AgentSession):
        """Load the existing Agent from an AgentSession (from the database)"""
        from agno.memory.memory import Memory
        from agno.memory.summary import SessionSummary
        from agno.utils.merge_dict import merge_dictionaries

        # Get the agent_id, user_id and session_id from the database
        if self.agent_id is None and session.agent_id is not None:
            self.agent_id = session.agent_id
        if self.user_id is None and session.user_id is not None:
            self.user_id = session.user_id
        if self.session_id is None and session.session_id is not None:
            self.session_id = session.session_id

        # Read agent_data from the database
        if session.agent_data is not None:
            # Get name from database and update the agent name if not set
            if self.name is None and "name" in session.agent_data:
                self.name = session.agent_data.get("name")

            # Get model data from the database and update the model
            if "model" in session.agent_data:
                model_data = session.agent_data.get("model")
                # Update model metrics from the database
                if model_data is not None and isinstance(model_data, dict):
                    model_metrics_from_db = model_data.get("metrics")
                    if model_metrics_from_db is not None and isinstance(model_metrics_from_db, dict) and self.model:
                        try:
                            self.model.metrics = model_metrics_from_db
                        except Exception as e:
                            logger.warning(f"Failed to load model from AgentSession: {e}")

        # Read session_data from the database
        if session.session_data is not None:
            # Get the session_name from database and update the current session_name if not set
            if self.session_name is None and "session_name" in session.session_data:
                self.session_name = session.session_data.get("session_name")

            # Get the session_state from database and update the current session_state
            if "session_state" in session.session_data:
                session_state_from_db = session.session_data.get("session_state")
                if (
                    session_state_from_db is not None
                    and isinstance(session_state_from_db, dict)
                    and len(session_state_from_db) > 0
                ):
                    # If the session_state is already set, merge the session_state from the database with the current session_state
                    if self.session_state is not None and len(self.session_state) > 0:
                        # This updates session_state_from_db
                        merge_dictionaries(session_state_from_db, self.session_state)
                    # Update the current session_state
                    self.session_state = session_state_from_db

            # Get images, videos, and audios from the database
            if "images" in session.session_data:
                images_from_db = session.session_data.get("images")
                if images_from_db is not None and isinstance(images_from_db, list):
                    if self.images is None:
                        self.images = []
                    self.images.extend([ImageArtifact.model_validate(img) for img in images_from_db])
            if "videos" in session.session_data:
                videos_from_db = session.session_data.get("videos")
                if videos_from_db is not None and isinstance(videos_from_db, list):
                    if self.videos is None:
                        self.videos = []
                    self.videos.extend([VideoArtifact.model_validate(vid) for vid in videos_from_db])
            if "audio" in session.session_data:
                audio_from_db = session.session_data.get("audio")
                if audio_from_db is not None and isinstance(audio_from_db, list):
                    if self.audio is None:
                        self.audio = []
                    self.audio.extend([AudioArtifact.model_validate(aud) for aud in audio_from_db])

        # Read extra_data from the database
        if session.extra_data is not None:
            # If extra_data is set in the agent, update the database extra_data with the agent's extra_data
            if self.extra_data is not None:
                # Updates agent_session.extra_data in place
                merge_dictionaries(session.extra_data, self.extra_data)
            # Update the current extra_data with the extra_data from the database which is updated in place
            self.extra_data = session.extra_data

        if self.memory is None:
            self.memory = session.memory  # type: ignore

        if not isinstance(self.memory, AgentMemory):
            if isinstance(self.memory, dict):
                # Convert dict to AgentMemory
                self.memory = AgentMemory(**self.memory)
            else:
                raise TypeError(f"Expected memory to be a dict or AgentMemory, but got {type(self.memory)}")

        if session.memory is not None:
            try:
                if "runs" in session.memory:
                    try:
                        self.memory.runs = [AgentRun(**m) for m in session.memory["runs"]]
                    except Exception as e:
                        logger.warning(f"Failed to load runs from memory: {e}")
                if "messages" in session.memory:
                    try:
                        self.memory.messages = [Message(**m) for m in session.memory["messages"]]
                    except Exception as e:
                        logger.warning(f"Failed to load messages from memory: {e}")
                if "summary" in session.memory:
                    try:
                        self.memory.summary = SessionSummary(**session.memory["summary"])
                    except Exception as e:
                        logger.warning(f"Failed to load session summary from memory: {e}")
                if "memories" in session.memory:
                    try:
                        self.memory.memories = [Memory(**m) for m in session.memory["memories"]]
                    except Exception as e:
                        logger.warning(f"Failed to load user memories: {e}")
            except Exception as e:
                logger.warning(f"Failed to load AgentMemory: {e}")
        logger.debug(f"-*- AgentSession loaded: {session.session_id}")

    def read_from_storage(self) -> Optional[AgentSession]:
        """Load the AgentSession from storage

        Returns:
            Optional[AgentSession]: The loaded AgentSession or None if not found.
        """
        if self.storage is not None and self.session_id is not None:
            self.agent_session = self.storage.read(session_id=self.session_id)
            if self.agent_session is not None:
                self.load_agent_session(session=self.agent_session)
            self.load_user_memories()
        return self.agent_session

    def write_to_storage(self) -> Optional[AgentSession]:
        """Save the AgentSession to storage

        Returns:
            Optional[AgentSession]: The saved AgentSession or None if not saved.
        """
        if self.storage is not None:
            self.agent_session = self.storage.upsert(session=self.get_agent_session())
        return self.agent_session

    def add_introduction(self, introduction: str) -> None:
        """Add an introduction to the chat history"""

        self.memory = cast(AgentMemory, self.memory)
        if introduction is not None:
            # Add an introduction as the first response from the Agent
            if len(self.memory.runs) == 0:
                self.memory.add_run(
                    AgentRun(
                        response=RunResponse(
                            content=introduction, messages=[Message(role="assistant", content=introduction)]
                        )
                    )
                )

    def load_session(self, force: bool = False) -> Optional[str]:
        """Load an existing session from the database and return the session_id.
        If a session does not exist, create a new session.

        - If a session exists in the database, load the session.
        - If a session does not exist in the database, create a new session.
        """
        # If an agent_session is already loaded, return the session_id from the agent_session
        #   if the session_id matches the session_id from the agent_session
        if self.agent_session is not None and not force:
            if self.session_id is not None and self.agent_session.session_id == self.session_id:
                return self.agent_session.session_id

        # Load an existing session or create a new session
        if self.storage is not None:
            # Load existing session if session_id is provided
            logger.debug(f"Reading AgentSession: {self.session_id}")
            self.read_from_storage()

            # Create a new session if it does not exist
            if self.agent_session is None:
                logger.debug("-*- Creating new AgentSession")
                # Initialize the agent_id and session_id if they are not set
                if self.agent_id is None or self.session_id is None:
                    self.initialize_agent()
                if self.introduction is not None:
                    self.add_introduction(self.introduction)
                # write_to_storage() will create a new AgentSession
                # and populate self.agent_session with the new session
                self.write_to_storage()
                if self.agent_session is None:
                    raise Exception("Failed to create new AgentSession in storage")
                logger.debug(f"-*- Created AgentSession: {self.agent_session.session_id}")
                self.log_agent_session()
        return self.session_id

    def new_session(self) -> None:
        """Create a new Agent session

        - Clear the model
        - Clear the memory
        - Create a new session_id
        - Load the new session
        """
        self.agent_session = None
        if self.model is not None:
            self.model.clear()
        if self.memory is not None:
            self.memory.clear()
        self.session_id = str(uuid4())
        self.load_session(force=True)

    def get_json_output_prompt(self) -> str:
        """Return the JSON output prompt for the Agent.

        This is added to the system prompt when the response_model is set and structured_outputs is False.
        """
        import json

        json_output_prompt = "Provide your output as a JSON containing the following fields:"
        if self.response_model is not None:
            if isinstance(self.response_model, str):
                json_output_prompt += "\n<json_fields>"
                json_output_prompt += f"\n{self.response_model}"
                json_output_prompt += "\n</json_fields>"
            elif isinstance(self.response_model, list):
                json_output_prompt += "\n<json_fields>"
                json_output_prompt += f"\n{json.dumps(self.response_model)}"
                json_output_prompt += "\n</json_fields>"
            elif issubclass(self.response_model, BaseModel):
                json_schema = self.response_model.model_json_schema()
                if json_schema is not None:
                    response_model_properties = {}
                    json_schema_properties = json_schema.get("properties")
                    if json_schema_properties is not None:
                        for field_name, field_properties in json_schema_properties.items():
                            formatted_field_properties = {
                                prop_name: prop_value
                                for prop_name, prop_value in field_properties.items()
                                if prop_name != "title"
                            }
                            response_model_properties[field_name] = formatted_field_properties
                    json_schema_defs = json_schema.get("$defs")
                    if json_schema_defs is not None:
                        response_model_properties["$defs"] = {}
                        for def_name, def_properties in json_schema_defs.items():
                            def_fields = def_properties.get("properties")
                            formatted_def_properties = {}
                            if def_fields is not None:
                                for field_name, field_properties in def_fields.items():
                                    formatted_field_properties = {
                                        prop_name: prop_value
                                        for prop_name, prop_value in field_properties.items()
                                        if prop_name != "title"
                                    }
                                    formatted_def_properties[field_name] = formatted_field_properties
                            if len(formatted_def_properties) > 0:
                                response_model_properties["$defs"][def_name] = formatted_def_properties

                    if len(response_model_properties) > 0:
                        json_output_prompt += "\n<json_fields>"
                        json_output_prompt += (
                            f"\n{json.dumps([key for key in response_model_properties.keys() if key != '$defs'])}"
                        )
                        json_output_prompt += "\n</json_fields>"
                        json_output_prompt += "\n\nHere are the properties for each field:"
                        json_output_prompt += "\n<json_field_properties>"
                        json_output_prompt += f"\n{json.dumps(response_model_properties, indent=2)}"
                        json_output_prompt += "\n</json_field_properties>"
            else:
                logger.warning(f"Could not build json schema for {self.response_model}")
        else:
            json_output_prompt += "Provide the output as JSON."

        json_output_prompt += "\nStart your response with `{` and end it with `}`."
        json_output_prompt += "\nYour output will be passed to json.loads() to convert it to a Python object."
        json_output_prompt += "\nMake sure it only contains valid JSON."
        return json_output_prompt

    def format_message_with_state_variables(self, msg: Any) -> Any:
        """Format a message with the session state variables."""
        if not isinstance(msg, str):
            return msg

        format_variables = ChainMap(
            self.session_state or {},
            self.context or {},
            self.extra_data or {},
            {"user_id": self.user_id} if self.user_id is not None else {},
        )
        return self._formatter.format(msg, **format_variables)  # type: ignore

    def get_system_message_role(self) -> str:
        """Return the role for the system message
        The role may be updated by the model if override_system_role is True.
        """
        self.model = cast(Model, self.model)
        if self.model.override_system_role and self.system_message_role == "system":
            return self.model.system_message_role
        return self.system_message_role

    def get_system_message(self) -> Optional[Message]:
        """Return the system message for the Agent.

        1. If the system_message is provided, use that.
        2. If create_default_system_message is False, return None.
        3. Build and return the default system message for the Agent.
        """
        self.memory = cast(AgentMemory, self.memory)

        # 1. If the system_message is provided, use that.
        if self.system_message is not None:
            if isinstance(self.system_message, Message):
                return self.system_message

            sys_message_content: str = ""
            if isinstance(self.system_message, str):
                sys_message_content = self.system_message
            elif callable(self.system_message):
                sys_message_content = self.system_message(agent=self)
                if not isinstance(sys_message_content, str):
                    raise Exception("system_message must return a string")

            # Format the system message with the session state variables
            if self.add_state_in_messages:
                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 not self.structured_outputs:
                sys_message_content += f"\n{self.get_json_output_prompt()}"

            return Message(role=self.get_system_message_role(), content=sys_message_content)

        # 2. If create_default_system_message is False, return None.
        if not self.create_default_system_message:
            return None

        if self.model is None:
            raise Exception("model not set")

        # 3. Build and return the default system message for the Agent.
        # 3.1 Build the list of instructions for the system message
        instructions: List[str] = []
        if self.instructions is not None:
            _instructions = self.instructions
            if callable(self.instructions):
                _instructions = self.instructions(agent=self)

            if isinstance(_instructions, str):
                instructions.append(_instructions)
            elif isinstance(_instructions, list):
                instructions.extend(_instructions)
        # 3.1.1 Add instructions from the Model
        _model_instructions = self.model.get_instructions_for_model()
        if _model_instructions is not None:
            instructions.extend(_model_instructions)

        # 3.2 Build a list of additional information for the system message
        additional_information: List[str] = []
        # 3.2.1 Add instructions for using markdown
        if self.markdown and self.response_model is None:
            additional_information.append("Use markdown to format your answers.")
        # 3.2.2 Add the current datetime
        if self.add_datetime_to_instructions:
            from datetime import datetime

            additional_information.append(f"The current time is {datetime.now()}")
        # 3.2.3 Add agent name if provided
        if self.name is not None and self.add_name_to_instructions:
            additional_information.append(f"Your name is: {self.name}.")

        # 3.3 Build the default system message for the Agent.
        system_message_content: str = ""
        # 3.3.1 First add the Agent description if provided
        if self.description is not None:
            system_message_content += f"{self.description}\n\n"
        # 3.3.2 Then add the Agent goal if provided
        if self.goal is not None:
            system_message_content += f"<your_goal>\n{self.goal}\n</your_goal>\n\n"
        # 3.3.3 Then add the Agent role if provided
        if self.role is not None:
            system_message_content += f"<your_role>\n{self.role}\n</your_role>\n\n"
        # 3.3.4 Then add instructions for transferring tasks to team members
        if self.has_team and self.add_transfer_instructions:
            system_message_content += (
                "<agent_team>\n"
                "You are the leader of a team of AI Agents:\n"
                "- You can either respond directly or transfer tasks to other Agents in your team depending on the tools available to them.\n"
                "- If you transfer a task to another Agent, make sure to include:\n"
                "  - task_description (str): A clear description of the task.\n"
                "  - expected_output (str): The expected output.\n"
                "  - additional_information (str): Additional information that will help the Agent complete the task.\n"
                "- You must always validate the output of the other Agents before responding to the user.\n"
                "- You can re-assign the task if you are not satisfied with the result.\n"
                "</agent_team>\n\n"
            )
        # 3.3.5 Then add instructions for the Agent
        if len(instructions) > 0:
            system_message_content += "<instructions>"
            if len(instructions) > 1:
                for _upi in instructions:
                    system_message_content += f"\n- {_upi}"
            else:
                system_message_content += "\n" + instructions[0]
            system_message_content += "\n</instructions>\n\n"
        # 3.3.6 Add additional information
        if len(additional_information) > 0:
            system_message_content += "<additional_information>"
            for _ai in additional_information:
                system_message_content += f"\n- {_ai}"
            system_message_content += "\n</additional_information>\n\n"

        # Format the system message with the session state variables
        if self.add_state_in_messages:
            system_message_content = self.format_message_with_state_variables(system_message_content)

        # 3.3.7 Then add the system message from the Model
        system_message_from_model = self.model.get_system_message_for_model()
        if system_message_from_model is not None:
            system_message_content += system_message_from_model
        # 3.3.8 Then add the expected output
        if self.expected_output is not None:
            system_message_content += f"<expected_output>\n{self.expected_output.strip()}\n</expected_output>\n\n"
        # 3.3.9 Then add additional context
        if self.additional_context is not None:
            system_message_content += f"{self.additional_context.strip()}\n"
        # 3.3.10 Then add information about the team members
        if self.has_team and self.add_transfer_instructions:
            system_message_content += (
                f"<transfer_instructions>\n{self.get_transfer_instructions().strip()}\n</transfer_instructions>\n\n"
            )
        # 3.3.11 Then add memories to the system prompt
        if self.memory.create_user_memories:
            if self.memory.memories and len(self.memory.memories) > 0:
                system_message_content += (
                    "You have access to memories from previous interactions with the user that you can use:\n\n"
                )
                system_message_content += "<memories_from_previous_interactions>"
                for _memory in self.memory.memories:
                    system_message_content += f"\n- {_memory.memory}"
                system_message_content += "\n</memories_from_previous_interactions>\n\n"
                system_message_content += (
                    "Note: this information is from previous interactions and may be updated in this conversation. "
                    "You should always prefer information from this conversation over the past memories.\n\n"
                )
            else:
                system_message_content += (
                    "You have the capability to retain memories from previous interactions with the user, "
                    "but have not had any interactions with the user yet.\n"
                    "If the user asks about previous memories, you can let them know that you dont have any memory about the user because you haven't had any interactions yet.\n\n"
                )
            system_message_content += (
                "You can add new memories using the `update_memory` tool.\n"
                "If you use the `update_memory` tool, remember to pass on the response to the user.\n\n"
            )
        # 3.3.12 Then add a summary of the interaction to the system prompt
        if self.memory.create_session_summary:
            if self.memory.summary is not None:
                system_message_content += "Here is a brief summary of your previous interactions if it helps:\n\n"
                system_message_content += "<summary_of_previous_interactions>\n"
                system_message_content += str(self.memory.summary)
                system_message_content += "\n</summary_of_previous_interactions>\n\n"
                system_message_content += (
                    "Note: this information is from previous interactions and may be outdated. "
                    "You should ALWAYS prefer information from this conversation over the past summary.\n\n"
                )

        # Add the JSON output prompt if response_model is provided and structured_outputs is False
        if self.response_model is not None and not self.structured_outputs:
            system_message_content += f"{self.get_json_output_prompt()}"

        # Return the system message
        return (
            Message(role=self.get_system_message_role(), content=system_message_content.strip())
            if system_message_content
            else None
        )

    def get_user_message(
        self,
        *,
        message: Optional[Union[str, List]],
        audio: Optional[Sequence[Audio]] = None,
        images: Optional[Sequence[Image]] = None,
        videos: Optional[Sequence[Video]] = None,
        **kwargs: Any,
    ) -> Optional[Message]:
        """Return the user message for the Agent.

        1. If the user_message is provided, use that.
        2. If create_default_user_message is False or if the message is a list, return the message as is.
        3. Build the default user message for the Agent
        """
        # Get references from the knowledge base to use in the user message
        references = None
        self.run_response = cast(RunResponse, self.run_response)
        if self.add_references and message:
            message_str: str
            if isinstance(message, str):
                message_str = message
            elif callable(message):
                message_str = message(agent=self)
            else:
                raise Exception("message must be a string or a callable when add_references is True")

            retrieval_timer = Timer()
            retrieval_timer.start()
            docs_from_knowledge = self.get_relevant_docs_from_knowledge(query=message_str, **kwargs)
            if docs_from_knowledge is not None:
                references = MessageReferences(
                    query=message_str, references=docs_from_knowledge, time=round(retrieval_timer.elapsed, 4)
                )
                # Add the references to the run_response
                if self.run_response.extra_data is None:
                    self.run_response.extra_data = RunResponseExtraData()
                if self.run_response.extra_data.references is None:
                    self.run_response.extra_data.references = []
                self.run_response.extra_data.references.append(references)
            retrieval_timer.stop()
            logger.debug(f"Time to get references: {retrieval_timer.elapsed:.4f}s")

        # 1. If the user_message is provided, use that.
        if self.user_message is not None:
            if isinstance(self.user_message, Message):
                return self.user_message

            user_message_content = self.user_message
            if callable(self.user_message):
                user_message_kwargs = {"agent": self, "message": message, "references": references}
                user_message_content = self.user_message(**user_message_kwargs)
                if not isinstance(user_message_content, str):
                    raise Exception("user_message must return a string")

            if self.add_state_in_messages:
                user_message_content = self.format_message_with_state_variables(user_message_content)

            return Message(
                role=self.user_message_role,
                content=user_message_content,
                audio=audio,
                images=images,
                videos=videos,
                **kwargs,
            )

        # 2. If create_default_user_message is False or message is a list, return the message as is.
        if not self.create_default_user_message or isinstance(message, list):
            return Message(
                role=self.user_message_role, content=message, images=images, audio=audio, videos=videos, **kwargs
            )

        # 3. Build the default user message for the Agent
        # If the message is None, return None
        if message is None:
            return None

        user_msg_content = message
        # Format the message with the session state variables
        if self.add_state_in_messages:
            user_msg_content = self.format_message_with_state_variables(message)
        # 4.1 Add references to user message
        if (
            self.add_references
            and references is not None
            and references.references is not None
            and len(references.references) > 0
        ):
            user_msg_content += "\n\nUse the following references from the knowledge base if it helps:\n"
            user_msg_content += "<references>\n"
            user_msg_content += self.convert_documents_to_string(references.references) + "\n"
            user_msg_content += "</references>"
        # 4.2 Add context to user message
        if self.add_context and self.context is not None:
            user_msg_content += "\n\n<context>\n"
            user_msg_content += self.convert_context_to_string(self.context) + "\n"
            user_msg_content += "</context>"

        # Return the user message
        return Message(
            role=self.user_message_role,
            content=user_msg_content,
            audio=audio,
            images=images,
            videos=videos,
            **kwargs,
        )

    def get_run_messages(
        self,
        *,
        message: Optional[Union[str, List, Dict, Message]] = None,
        audio: Optional[Sequence[Audio]] = None,
        images: Optional[Sequence[Image]] = None,
        videos: Optional[Sequence[Video]] = None,
        messages: Optional[Sequence[Union[Dict, Message]]] = None,
        **kwargs: Any,
    ) -> RunMessages:
        """This function returns a RunMessages object with the following attributes:
            - system_message: The system message for this run
            - user_message: The user message for this run
            - messages: List of messages to send to the model

        To build the RunMessages object:
        1. Add system message to run_messages
        2. Add extra messages to run_messages if provided
        3. Add history to run_messages
        4. Add user message to run_messages
        5. Add messages to run_messages if provided

        Returns:
            RunMessages object with the following attributes:
                - system_message: The system message for this run
                - user_message: The user message for this run
                - messages: List of all messages to send to the model

        Typical usage:
        run_messages = self.get_run_messages(
            message=message, audio=audio, images=images, videos=videos, messages=messages, **kwargs
        )
        """
        # Initialize the RunMessages object
        run_messages = RunMessages()
        self.memory = cast(AgentMemory, self.memory)
        self.run_response = cast(RunResponse, self.run_response)

        # 1. Add system message to run_messages
        system_message = self.get_system_message()
        if system_message is not None:
            run_messages.system_message = system_message
            run_messages.messages.append(system_message)

        # 2. Add extra messages to run_messages if provided
        if self.add_messages is not None:
            messages_to_add_to_run_response: List[Message] = []
            if run_messages.extra_messages is None:
                run_messages.extra_messages = []

            for _m in self.add_messages:
                if isinstance(_m, Message):
                    messages_to_add_to_run_response.append(_m)
                    run_messages.messages.append(_m)
                    run_messages.extra_messages.append(_m)
                elif isinstance(_m, dict):
                    try:
                        _m_parsed = Message.model_validate(_m)
                        messages_to_add_to_run_response.append(_m_parsed)
                        run_messages.messages.append(_m_parsed)
                        run_messages.extra_messages.append(_m_parsed)
                    except Exception as e:
                        logger.warning(f"Failed to validate message: {e}")
            # Add the extra messages to the run_response
            if len(messages_to_add_to_run_response) > 0:
                logger.debug(f"Adding {len(messages_to_add_to_run_response)} extra messages")
                if self.run_response.extra_data is None:
                    self.run_response.extra_data = RunResponseExtraData(add_messages=messages_to_add_to_run_response)
                else:
                    if self.run_response.extra_data.add_messages is None:
                        self.run_response.extra_data.add_messages = messages_to_add_to_run_response
                    else:
                        self.run_response.extra_data.add_messages.extend(messages_to_add_to_run_response)

        # 3. Add history to run_messages
        if self.add_history_to_messages:
            history: List[Message] = self.memory.get_messages_from_last_n_runs(
                last_n=self.num_history_responses, skip_role=self.get_system_message_role()
            )
            if len(history) > 0:
                logger.debug(f"Adding {len(history)} messages from history")
                if self.run_response.extra_data is None:
                    self.run_response.extra_data = RunResponseExtraData(history=history)
                else:
                    if self.run_response.extra_data.history is None:
                        self.run_response.extra_data.history = history
                    else:
                        self.run_response.extra_data.history.extend(history)
                run_messages.messages += history

        # 4.Add user message to run_messages
        user_message: Optional[Message] = None
        # 4.1 Build user message if message is None, str or list
        if message is None or isinstance(message, str) or isinstance(message, list):
            user_message = self.get_user_message(message=message, audio=audio, images=images, videos=videos, **kwargs)
        # 4.2 If message is provided as a Message, use it directly
        elif isinstance(message, Message):
            user_message = message
        # 4.3 If message is provided as a dict, try to validate it as a Message
        elif isinstance(message, dict):
            try:
                user_message = Message.model_validate(message)
            except Exception as e:
                logger.warning(f"Failed to validate message: {e}")
        # Add user message to run_messages
        if user_message is not None:
            run_messages.user_message = user_message
            run_messages.messages.append(user_message)

        # 5. Add messages to run_messages if provided
        if messages is not None and len(messages) > 0:
            for _m in messages:
                if isinstance(_m, Message):
                    run_messages.messages.append(_m)
                    if run_messages.extra_messages is None:
                        run_messages.extra_messages = []
                    run_messages.extra_messages.append(_m)
                elif isinstance(_m, dict):
                    try:
                        run_messages.messages.append(Message.model_validate(_m))
                        if run_messages.extra_messages is None:
                            run_messages.extra_messages = []
                        run_messages.extra_messages.append(Message.model_validate(_m))
                    except Exception as e:
                        logger.warning(f"Failed to validate message: {e}")

        return run_messages

    def deep_copy(self, *, update: Optional[Dict[str, Any]] = None) -> Agent:
        """Create and return a deep copy of this Agent, optionally updating fields.

        Args:
            update (Optional[Dict[str, Any]]): Optional dictionary of fields for the new Agent.

        Returns:
            Agent: A new Agent instance.
        """
        from dataclasses import fields

        # Do not copy agent_session and session_name to the new agent
        excluded_fields = ["agent_session", "session_name", "memory"]
        # Extract the fields to set for the new Agent
        fields_for_new_agent: Dict[str, Any] = {}

        for f in fields(self):
            if f.name in excluded_fields:
                continue
            field_value = getattr(self, f.name)
            if field_value is not None:
                fields_for_new_agent[f.name] = self._deep_copy_field(f.name, field_value)

        # Update fields if provided
        if update:
            fields_for_new_agent.update(update)
        # Create a new Agent
        new_agent = self.__class__(**fields_for_new_agent)
        logger.debug(f"Created new {self.__class__.__name__}")
        return new_agent

    def _deep_copy_field(self, field_name: str, field_value: Any) -> Any:
        """Helper method to deep copy a field based on its type."""
        from copy import copy, deepcopy

        # For memory and reasoning_agent, use their deep_copy methods
        if field_name in ("memory", "reasoning_agent"):
            return field_value.deep_copy()

        # For storage, model and reasoning_model, use a deep copy
        elif field_name in ("storage", "model", "reasoning_model"):
            try:
                return deepcopy(field_value)
            except Exception as e:
                logger.warning(f"Failed to deepcopy field: {field_name} - {e}")
                try:
                    return copy(field_value)
                except Exception as e:
                    logger.warning(f"Failed to copy field: {field_name} - {e}")
                    return field_value

        # For compound types, attempt a deep copy
        elif isinstance(field_value, (list, dict, set)):
            try:
                return deepcopy(field_value)
            except Exception as e:
                logger.warning(f"Failed to deepcopy field: {field_name} - {e}")
                try:
                    return copy(field_value)
                except Exception as e:
                    logger.warning(f"Failed to copy field: {field_name} - {e}")
                    return field_value

        # For pydantic models, attempt a model_copy
        elif isinstance(field_value, BaseModel):
            try:
                return field_value.model_copy(deep=True)
            except Exception as e:
                logger.warning(f"Failed to deepcopy field: {field_name} - {e}")
                try:
                    return field_value.model_copy(deep=False)
                except Exception as e:
                    logger.warning(f"Failed to copy field: {field_name} - {e}")
                    return field_value

        # For other types, attempt a shallow copy first
        try:
            from copy import copy

            return copy(field_value)
        except Exception:
            # If copy fails, return as is
            return field_value

    def get_transfer_function(self, member_agent: Agent, index: int) -> Function:
        def _transfer_task_to_agent(
            task_description: str, expected_output: str, additional_information: Optional[str] = None
        ) -> Iterator[str]:
            if member_agent.team_data is None:
                member_agent.team_data = {}

            # Update the member agent team_data to include leader_session_id, leader_agent_id and leader_run_id
            member_agent.team_data["leader_session_id"] = self.session_id
            member_agent.team_data["leader_agent_id"] = self.agent_id
            member_agent.team_data["leader_run_id"] = self.run_id

            # -*- Run the agent
            member_agent_task = f"{task_description}\n\n<expected_output>\n{expected_output}\n</expected_output>"
            try:
                if additional_information is not None and additional_information.strip() != "":
                    member_agent_task += (
                        f"\n\n<additional_information>\n{additional_information}\n</additional_information>"
                    )
            except Exception as e:
                logger.warning(f"Failed to add additional information to the member agent: {e}")

            member_agent_session_id = member_agent.session_id
            member_agent_agent_id = member_agent.agent_id

            # Create a dictionary with member_session_id and member_agent_id
            member_agent_info = {
                "session_id": member_agent_session_id,
                "agent_id": member_agent_agent_id,
            }
            # Update the leader agent team_data to include member_agent_info
            if self.team_data is None:
                self.team_data = {}
            if "members" not in self.team_data:
                self.team_data["members"] = [member_agent_info]
            else:
                # Check if member_agent_info is already in the list
                if member_agent_info not in self.team_data["members"]:
                    self.team_data["members"].append(member_agent_info)

            if self.stream and member_agent.is_streamable:
                member_agent_run_response_stream = member_agent.run(member_agent_task, stream=True)
                for member_agent_run_response_chunk in member_agent_run_response_stream:
                    yield member_agent_run_response_chunk.content  # type: ignore
            else:
                member_agent_run_response: RunResponse = member_agent.run(member_agent_task, stream=False)
                if member_agent_run_response.content is None:
                    yield "No response from the member agent."
                elif isinstance(member_agent_run_response.content, str):
                    yield member_agent_run_response.content
                elif issubclass(type(member_agent_run_response.content), BaseModel):
                    try:
                        yield member_agent_run_response.content.model_dump_json(indent=2)
                    except Exception as e:
                        yield str(e)
                else:
                    try:
                        import json

                        yield json.dumps(member_agent_run_response.content, indent=2)
                    except Exception as e:
                        yield str(e)
            yield self.team_response_separator

        # Give a name to the member agent
        agent_name = member_agent.name.replace(" ", "_").lower() if member_agent.name else f"agent_{index}"
        if member_agent.name is None:
            member_agent.name = agent_name

        transfer_function = Function.from_callable(_transfer_task_to_agent)
        transfer_function.name = f"transfer_task_to_{agent_name}"
        transfer_function.description = dedent(f"""\
        Use this function to transfer a task to {agent_name}
        You must provide a clear and concise description of the task the agent should achieve AND the expected output.
        Args:
            task_description (str): A clear and concise description of the task the agent should achieve.
            expected_output (str): The expected output from the agent.
            additional_information (Optional[str]): Additional information that will help the agent complete the task.
        Returns:
            str: The result of the delegated task.
        """)

        # If the member agent is set to respond directly, show the result of the function call and stop the model execution
        if member_agent.respond_directly:
            transfer_function.show_result = True
            transfer_function.stop_after_tool_call = True

        return transfer_function

    def get_transfer_instructions(self) -> str:
        if self.team and len(self.team) > 0:
            transfer_instructions = "You can transfer tasks to the following Agents in your team:\n"
            for agent_index, agent in enumerate(self.team):
                transfer_instructions += f"\nAgent {agent_index + 1}:\n"
                if agent.name:
                    transfer_instructions += f"Name: {agent.name}\n"
                if agent.role:
                    transfer_instructions += f"Role: {agent.role}\n"
                if agent.tools is not None:
                    _tools = []
                    for _tool in agent.tools:
                        if isinstance(_tool, Toolkit):
                            _tools.extend(list(_tool.functions.keys()))
                        elif isinstance(_tool, Function):
                            _tools.append(_tool.name)
                        elif callable(_tool):
                            _tools.append(_tool.__name__)
                    transfer_instructions += f"Available tools: {', '.join(_tools)}\n"
            return transfer_instructions
        return ""

    def get_relevant_docs_from_knowledge(
        self, query: str, num_documents: Optional[int] = None, **kwargs
    ) -> Optional[List[Dict[str, Any]]]:
        """Return a list of references from the knowledge base"""
        from agno.document import Document

        if self.retriever is not None:
            retriever_kwargs = {"agent": self, "query": query, "num_documents": num_documents, **kwargs}
            return self.retriever(**retriever_kwargs)

        if self.knowledge is None:
            return None

        relevant_docs: List[Document] = self.knowledge.search(query=query, num_documents=num_documents, **kwargs)
        if len(relevant_docs) == 0:
            return None
        return [doc.to_dict() for doc in relevant_docs]

    def convert_documents_to_string(self, docs: List[Dict[str, Any]]) -> str:
        if docs is None or len(docs) == 0:
            return ""

        if self.references_format == "yaml":
            import yaml

            return yaml.dump(docs)

        import json

        return json.dumps(docs, indent=2)

    def convert_context_to_string(self, context: Dict[str, Any]) -> str:
        """Convert the context dictionary to a string representation.

        Args:
            context: Dictionary containing context data

        Returns:
            String representation of the context, or empty string if conversion fails
        """
        if context is None:
            return ""

        try:
            import json

            return json.dumps(context, indent=2, default=str)
        except (TypeError, ValueError, OverflowError) as e:
            logger.warning(f"Failed to convert context to JSON: {e}")
            # Attempt a fallback conversion for non-serializable objects
            sanitized_context = {}
            for key, value in context.items():
                try:
                    # Try to serialize each value individually
                    json.dumps({key: value}, default=str)
                    sanitized_context[key] = value
                except Exception:
                    # If serialization fails, convert to string representation
                    sanitized_context[key] = str(value)

            try:
                return json.dumps(sanitized_context, indent=2)
            except Exception as e:
                logger.error(f"Failed to convert sanitized context to JSON: {e}")
                return str(context)

    def save_run_response_to_file(self, message: Optional[Union[str, List, Dict, Message]] = None) -> None:
        if self.save_response_to_file is not None and self.run_response is not None:
            message_str = None
            if message is not None:
                if isinstance(message, str):
                    message_str = message
                else:
                    logger.warning("Did not use message in output file name: message is not a string")
            try:
                from pathlib import Path

                fn = self.save_response_to_file.format(
                    name=self.name,
                    session_id=self.session_id,
                    user_id=self.user_id,
                    message=message_str,
                    run_id=self.run_id,
                )
                fn_path = Path(fn)
                if not fn_path.parent.exists():
                    fn_path.parent.mkdir(parents=True, exist_ok=True)
                if isinstance(self.run_response.content, str):
                    fn_path.write_text(self.run_response.content)
                else:
                    import json

                    fn_path.write_text(json.dumps(self.run_response.content, indent=2))
            except Exception as e:
                logger.warning(f"Failed to save output to file: {e}")

    def update_run_response_with_reasoning(
        self, reasoning_steps: List[ReasoningStep], reasoning_agent_messages: List[Message]
    ) -> None:
        self.run_response = cast(RunResponse, self.run_response)
        if self.run_response.extra_data is None:
            self.run_response.extra_data = RunResponseExtraData()

        extra_data = self.run_response.extra_data

        # Update reasoning_steps
        if extra_data.reasoning_steps is None:
            extra_data.reasoning_steps = reasoning_steps
        else:
            extra_data.reasoning_steps.extend(reasoning_steps)

        # Update reasoning_messages
        if extra_data.reasoning_messages is None:
            extra_data.reasoning_messages = reasoning_agent_messages
        else:
            extra_data.reasoning_messages.extend(reasoning_agent_messages)

    def aggregate_metrics_from_messages(self, messages: List[Message]) -> Dict[str, Any]:
        aggregated_metrics: Dict[str, Any] = defaultdict(list)

        # Use a defaultdict(list) to collect all values for each assisntant message
        for m in messages:
            if m.role == "assistant" and m.metrics is not None:
                for k, v in m.metrics.items():
                    aggregated_metrics[k].append(v)
        return aggregated_metrics

    def rename(self, name: str) -> None:
        """Rename the Agent and save to storage"""

        # -*- Read from storage
        self.read_from_storage()
        # -*- Rename Agent
        self.name = name
        # -*- Save to storage
        self.write_to_storage()
        # -*- Log Agent session
        self.log_agent_session()

    def rename_session(self, session_name: str) -> None:
        """Rename the current session and save to storage"""

        # -*- Read from storage
        self.read_from_storage()
        # -*- Rename session
        self.session_name = session_name
        # -*- Save to storage
        self.write_to_storage()
        # -*- Log Agent session
        self.log_agent_session()

    def generate_session_name(self) -> str:
        """Generate a name for the session using the first 6 messages from the memory"""

        if self.model is None:
            raise Exception("Model not set")

        gen_session_name_prompt = "Conversation\n"
        messages_for_generating_session_name = []
        self.memory = cast(AgentMemory, self.memory)
        try:
            message_pairs = self.memory.get_message_pairs()
            for message_pair in message_pairs[:3]:
                messages_for_generating_session_name.append(message_pair[0])
                messages_for_generating_session_name.append(message_pair[1])
        except Exception as e:
            logger.warning(f"Failed to generate name: {e}")

        for message in messages_for_generating_session_name:
            gen_session_name_prompt += f"{message.role.upper()}: {message.content}\n"

        gen_session_name_prompt += "\n\nConversation Name: "

        system_message = Message(
            role=self.get_system_message_role(),
            content="Please provide a suitable name for this conversation in maximum 5 words. "
            "Remember, do not exceed 5 words.",
        )
        user_message = Message(role=self.user_message_role, content=gen_session_name_prompt)
        generate_name_messages = [system_message, user_message]
        generated_name = self.model.response(messages=generate_name_messages)
        content = generated_name.content
        if content is None:
            logger.error("Generated name is None. Trying again.")
            return self.generate_session_name()
        if len(content.split()) > 15:
            logger.error("Generated name is too long. Trying again.")
            return self.generate_session_name()
        return content.replace('"', "").strip()

    def auto_rename_session(self) -> None:
        """Automatically rename the session and save to storage"""

        # -*- Read from storage
        self.read_from_storage()
        # -*- Generate name for session
        generated_session_name = self.generate_session_name()
        logger.debug(f"Generated Session Name: {generated_session_name}")
        # -*- Rename thread
        self.session_name = generated_session_name
        # -*- Save to storage
        self.write_to_storage()
        # -*- Log Agent Session
        self.log_agent_session()

    def delete_session(self, session_id: str):
        """Delete the current session and save to storage"""
        if self.storage is None:
            return
        # -*- Delete session
        self.storage.delete_session(session_id=session_id)

    ###########################################################################
    # Handle images, videos and audio
    ###########################################################################

    def add_image(self, image: ImageArtifact) -> None:
        if self.images is None:
            self.images = []
        self.images.append(image)
        if self.run_response is not None:
            if self.run_response.images is None:
                self.run_response.images = []
            self.run_response.images.append(image)

    def add_video(self, video: VideoArtifact) -> None:
        if self.videos is None:
            self.videos = []
        self.videos.append(video)
        if self.run_response is not None:
            if self.run_response.videos is None:
                self.run_response.videos = []
            self.run_response.videos.append(video)

    def add_audio(self, audio: AudioArtifact) -> None:
        if self.audio is None:
            self.audio = []
        self.audio.append(audio)
        if self.run_response is not None:
            if self.run_response.audio is None:
                self.run_response.audio = []
            self.run_response.audio.append(audio)

    def get_images(self) -> Optional[List[ImageArtifact]]:
        return self.images

    def get_videos(self) -> Optional[List[VideoArtifact]]:
        return self.videos

    def get_audio(self) -> Optional[List[AudioArtifact]]:
        return self.audio

    ###########################################################################
    # Reasoning
    ###########################################################################

    def reason(self, run_messages: RunMessages) -> Iterator[RunResponse]:
        # Yield a reasoning started event
        if self.stream_intermediate_steps:
            yield self.create_run_response(content="Reasoning started", event=RunEvent.reasoning_started)

        # Get the reasoning model
        reasoning_model: Optional[Model] = self.reasoning_model
        if reasoning_model is None and self.model is not None:
            reasoning_model = self.model.__class__(id=self.model.id)
        if reasoning_model is None:
            logger.warning("Reasoning error. Reasoning model is None, continuing regular session...")
            return

        # Use DeepSeek for reasoning
        if reasoning_model.__class__.__name__ == "DeepSeek" and reasoning_model.id == "deepseek-reasoner":
            from agno.reasoning.deepseek import get_deepseek_reasoning, get_deepseek_reasoning_agent

            ds_reasoning_agent = self.reasoning_agent or get_deepseek_reasoning_agent(
                reasoning_model=reasoning_model, monitoring=self.monitoring
            )
            ds_reasoning_message: Optional[Message] = get_deepseek_reasoning(
                reasoning_agent=ds_reasoning_agent, messages=run_messages.get_input_messages()
            )
            if ds_reasoning_message is None:
                logger.warning("Reasoning error. Reasoning response is None, continuing regular session...")
                return
            run_messages.messages.append(ds_reasoning_message)
            # Add reasoning step to the Agent's run_response
            self.update_run_response_with_reasoning(
                reasoning_steps=[ReasoningStep(result=ds_reasoning_message.content)],
                reasoning_agent_messages=[ds_reasoning_message],
            )
        # Use Groq for reasoning
        if reasoning_model.__class__.__name__ == "Groq" and "deepseek" in reasoning_model.id:
            from agno.reasoning.groq import get_groq_reasoning, get_groq_reasoning_agent

            groq_reasoning_agent = self.reasoning_agent or get_groq_reasoning_agent(
                reasoning_model=reasoning_model, monitoring=self.monitoring
            )
            groq_reasoning_message: Optional[Message] = get_groq_reasoning(
                reasoning_agent=groq_reasoning_agent, messages=run_messages.get_input_messages()
            )
            if groq_reasoning_message is None:
                logger.warning("Reasoning error. Reasoning response is None, continuing regular session...")
                return
            run_messages.messages.append(groq_reasoning_message)
            # Add reasoning step to the Agent's run_response
            self.update_run_response_with_reasoning(
                reasoning_steps=[ReasoningStep(result=groq_reasoning_message.content)],
                reasoning_agent_messages=[groq_reasoning_message],
            )
        # Get default reasoning
        else:
            from agno.reasoning.default import get_default_reasoning_agent
            from agno.reasoning.helpers import get_next_action, update_messages_with_reasoning

            # Get default reasoning agent
            reasoning_agent: Optional[Agent] = self.reasoning_agent
            if reasoning_agent is None:
                reasoning_agent = get_default_reasoning_agent(
                    reasoning_model=reasoning_model,
                    min_steps=self.reasoning_min_steps,
                    max_steps=self.reasoning_max_steps,
                    tools=self.tools,
                    structured_outputs=self.structured_outputs,
                    monitoring=self.monitoring,
                )

            # Validate reasoning agent
            if reasoning_agent is None:
                logger.warning("Reasoning error. Reasoning agent is None, continuing regular session...")
                return
            # Ensure the reasoning agent response model is ReasoningSteps
            if reasoning_agent.response_model is not None and not isinstance(reasoning_agent.response_model, type):
                if not issubclass(reasoning_agent.response_model, ReasoningSteps):
                    logger.warning(
                        "Reasoning agent response model should be `ReasoningSteps`, continuing regular session..."
                    )
                return
            # Ensure the reasoning model and agent do not show tool calls
            reasoning_agent.show_tool_calls = False
            reasoning_agent.model.show_tool_calls = False  # type: ignore

            step_count = 1
            next_action = NextAction.CONTINUE
            reasoning_messages: List[Message] = []
            all_reasoning_steps: List[ReasoningStep] = []
            logger.debug("==== Starting Reasoning ====")
            while next_action == NextAction.CONTINUE and step_count < self.reasoning_max_steps:
                logger.debug(f"==== Step {step_count} ====")
                step_count += 1
                try:
                    # Run the reasoning agent
                    reasoning_agent_response: RunResponse = reasoning_agent.run(
                        messages=run_messages.get_input_messages()
                    )
                    if reasoning_agent_response.content is None or reasoning_agent_response.messages is None:
                        logger.warning("Reasoning error. Reasoning response is empty, continuing regular session...")
                        break

                    if reasoning_agent_response.content.reasoning_steps is None:
                        logger.warning("Reasoning error. Reasoning steps are empty, continuing regular session...")
                        break

                    reasoning_steps: List[ReasoningStep] = reasoning_agent_response.content.reasoning_steps
                    all_reasoning_steps.extend(reasoning_steps)
                    # Yield reasoning steps
                    if self.stream_intermediate_steps:
                        for reasoning_step in reasoning_steps:
                            yield self.create_run_response(
                                content=reasoning_step,
                                content_type=reasoning_step.__class__.__name__,
                                event=RunEvent.reasoning_step,
                            )

                    # Find the index of the first assistant message
                    first_assistant_index = next(
                        (i for i, m in enumerate(reasoning_agent_response.messages) if m.role == "assistant"),
                        len(reasoning_agent_response.messages),
                    )
                    # Extract reasoning messages starting from the message after the first assistant message
                    reasoning_messages = reasoning_agent_response.messages[first_assistant_index:]

                    # Add reasoning step to the Agent's run_response
                    self.update_run_response_with_reasoning(
                        reasoning_steps=reasoning_steps, reasoning_agent_messages=reasoning_agent_response.messages
                    )

                    # Get the next action
                    next_action = get_next_action(reasoning_steps[-1])
                    if next_action == NextAction.FINAL_ANSWER:
                        break
                except Exception as e:
                    logger.error(f"Reasoning error: {e}")
                    break

            logger.debug(f"Total Reasoning steps: {len(all_reasoning_steps)}")
            logger.debug("==== Reasoning finished====")

            # Update the messages_for_model to include reasoning messages
            update_messages_with_reasoning(
                run_messages=run_messages,
                reasoning_messages=reasoning_messages,
            )

        # Yield the final reasoning completed event
        if self.stream_intermediate_steps:
            yield self.create_run_response(
                content=ReasoningSteps(reasoning_steps=all_reasoning_steps),
                content_type=ReasoningSteps.__class__.__name__,
                event=RunEvent.reasoning_completed,
            )

    async def areason(self, run_messages: RunMessages) -> Any:
        # Yield a reasoning started event
        if self.stream_intermediate_steps:
            yield self.create_run_response(content="Reasoning started", event=RunEvent.reasoning_started)

        # Get the reasoning model
        reasoning_model: Optional[Model] = self.reasoning_model
        if reasoning_model is None and self.model is not None:
            reasoning_model = self.model.__class__(id=self.model.id)
        if reasoning_model is None:
            logger.warning("Reasoning error. Reasoning model is None, continuing regular session...")
            return

        # Use DeepSeek for reasoning
        if reasoning_model.__class__.__name__ == "DeepSeek" and reasoning_model.id == "deepseek-reasoner":
            from agno.reasoning.deepseek import aget_deepseek_reasoning, get_deepseek_reasoning_agent

            ds_reasoning_agent = self.reasoning_agent or get_deepseek_reasoning_agent(
                reasoning_model=reasoning_model, monitoring=self.monitoring
            )
            ds_reasoning_message: Optional[Message] = await aget_deepseek_reasoning(
                reasoning_agent=ds_reasoning_agent, messages=run_messages.get_input_messages()
            )
            if ds_reasoning_message is None:
                logger.warning("Reasoning error. Reasoning response is None, continuing regular session...")
                return
            run_messages.messages.append(ds_reasoning_message)
            # Add reasoning step to the Agent's run_response
            self.update_run_response_with_reasoning(
                reasoning_steps=[ReasoningStep(result=ds_reasoning_message.content)],
                reasoning_agent_messages=[ds_reasoning_message],
            )
        # Use Groq for reasoning
        if reasoning_model.__class__.__name__ == "Groq" and "deepseek" in reasoning_model.id:
            from agno.reasoning.groq import aget_groq_reasoning, get_groq_reasoning_agent

            groq_reasoning_agent = self.reasoning_agent or get_groq_reasoning_agent(
                reasoning_model=reasoning_model, monitoring=self.monitoring
            )
            groq_reasoning_message: Optional[Message] = await aget_groq_reasoning(
                reasoning_agent=groq_reasoning_agent, messages=run_messages.get_input_messages()
            )
            if groq_reasoning_message is None:
                logger.warning("Reasoning error. Reasoning response is None, continuing regular session...")
                return
            run_messages.messages.append(groq_reasoning_message)
            # Add reasoning step to the Agent's run_response
            self.update_run_response_with_reasoning(
                reasoning_steps=[ReasoningStep(result=groq_reasoning_message.content)],
                reasoning_agent_messages=[groq_reasoning_message],
            )
        # Get default reasoning
        else:
            from agno.reasoning.default import get_default_reasoning_agent
            from agno.reasoning.helpers import get_next_action, update_messages_with_reasoning

            # Get default reasoning agent
            reasoning_agent: Optional[Agent] = self.reasoning_agent
            if reasoning_agent is None:
                reasoning_agent = get_default_reasoning_agent(
                    reasoning_model=reasoning_model,
                    min_steps=self.reasoning_min_steps,
                    max_steps=self.reasoning_max_steps,
                    tools=self.tools,
                    structured_outputs=self.structured_outputs,
                    monitoring=self.monitoring,
                )

            # Validate reasoning agent
            if reasoning_agent is None:
                logger.warning("Reasoning error. Reasoning agent is None, continuing regular session...")
                return
            # Ensure the reasoning agent response model is ReasoningSteps
            if reasoning_agent.response_model is not None and not isinstance(reasoning_agent.response_model, type):
                if not issubclass(reasoning_agent.response_model, ReasoningSteps):
                    logger.warning(
                        "Reasoning agent response model should be `ReasoningSteps`, continuing regular session..."
                    )
                return
            # Ensure the reasoning model and agent do not show tool calls
            reasoning_agent.show_tool_calls = False
            reasoning_agent.model.show_tool_calls = False  # type: ignore

            step_count = 1
            next_action = NextAction.CONTINUE
            reasoning_messages: List[Message] = []
            all_reasoning_steps: List[ReasoningStep] = []
            logger.debug("==== Starting Reasoning ====")
            while next_action == NextAction.CONTINUE and step_count < self.reasoning_max_steps:
                logger.debug(f"==== Step {step_count} ====")
                step_count += 1
                try:
                    # Run the reasoning agent
                    reasoning_agent_response: RunResponse = await reasoning_agent.arun(
                        messages=run_messages.get_input_messages()
                    )
                    if reasoning_agent_response.content is None or reasoning_agent_response.messages is None:
                        logger.warning("Reasoning error. Reasoning response is empty, continuing regular session...")
                        break

                    if reasoning_agent_response.content.reasoning_steps is None:
                        logger.warning("Reasoning error. Reasoning steps are empty, continuing regular session...")
                        break

                    reasoning_steps: List[ReasoningStep] = reasoning_agent_response.content.reasoning_steps
                    all_reasoning_steps.extend(reasoning_steps)
                    # Yield reasoning steps
                    if self.stream_intermediate_steps:
                        for reasoning_step in reasoning_steps:
                            yield self.create_run_response(
                                content=reasoning_step,
                                content_type=reasoning_step.__class__.__name__,
                                event=RunEvent.reasoning_step,
                            )

                    # Find the index of the first assistant message
                    first_assistant_index = next(
                        (i for i, m in enumerate(reasoning_agent_response.messages) if m.role == "assistant"),
                        len(reasoning_agent_response.messages),
                    )
                    # Extract reasoning messages starting from the message after the first assistant message
                    reasoning_messages = reasoning_agent_response.messages[first_assistant_index:]

                    # Add reasoning step to the Agent's run_response
                    self.update_run_response_with_reasoning(
                        reasoning_steps=reasoning_steps, reasoning_agent_messages=reasoning_agent_response.messages
                    )

                    # Get the next action
                    next_action = get_next_action(reasoning_steps[-1])
                    if next_action == NextAction.FINAL_ANSWER:
                        break
                except Exception as e:
                    logger.error(f"Reasoning error: {e}")
                    break

            logger.debug(f"Total Reasoning steps: {len(all_reasoning_steps)}")
            logger.debug("==== Reasoning finished====")

            # Update the messages_for_model to include reasoning messages
            update_messages_with_reasoning(
                run_messages=run_messages,
                reasoning_messages=reasoning_messages,
            )

        # Yield the final reasoning completed event
        if self.stream_intermediate_steps:
            yield self.create_run_response(
                content=ReasoningSteps(reasoning_steps=all_reasoning_steps),
                content_type=ReasoningSteps.__class__.__name__,
                event=RunEvent.reasoning_completed,
            )

    ###########################################################################
    # Default Tools
    ###########################################################################

    def get_chat_history(self, num_chats: Optional[int] = None) -> str:
        """Use this function to get the chat history between the user and agent.

        Args:
            num_chats: The number of chats to return.
                Each chat contains 2 messages. One from the user and one from the agent.
                Default: None

        Returns:
            str: A JSON of a list of dictionaries representing the chat history.

        Example:
            - To get the last chat, use num_chats=1.
            - To get the last 5 chats, use num_chats=5.
            - To get all chats, use num_chats=None.
            - To get the first chat, use num_chats=None and pick the first message.
        """
        import json

        history: List[Dict[str, Any]] = []
        self.memory = cast(AgentMemory, self.memory)
        all_chats = self.memory.get_message_pairs()
        if len(all_chats) == 0:
            return ""

        chats_added = 0
        for chat in all_chats[::-1]:
            history.insert(0, chat[1].to_dict())
            history.insert(0, chat[0].to_dict())
            chats_added += 1
            if num_chats is not None and chats_added >= num_chats:
                break
        return json.dumps(history)

    def get_tool_call_history(self, num_calls: int = 3) -> str:
        """Use this function to get the tools called by the agent in reverse chronological order.

        Args:
            num_calls: The number of tool calls to return.
                Default: 3

        Returns:
            str: A JSON of a list of dictionaries representing the tool call history.

        Example:
            - To get the last tool call, use num_calls=1.
            - To get all tool calls, use num_calls=None.
        """
        import json

        self.memory = cast(AgentMemory, self.memory)
        tool_calls = self.memory.get_tool_calls(num_calls)
        if len(tool_calls) == 0:
            return ""
        logger.debug(f"tool_calls: {tool_calls}")
        return json.dumps(tool_calls)

    def search_knowledge_base(self, query: str) -> str:
        """Use this function to search the knowledge base for information about a query.

        Args:
            query: The query to search for.

        Returns:
            str: A string containing the response from the knowledge base.
        """

        # Get the relevant documents from the knowledge base
        self.run_response = cast(RunResponse, self.run_response)
        retrieval_timer = Timer()
        retrieval_timer.start()
        docs_from_knowledge = self.get_relevant_docs_from_knowledge(query=query)
        if docs_from_knowledge is not None:
            references = MessageReferences(
                query=query, references=docs_from_knowledge, time=round(retrieval_timer.elapsed, 4)
            )
            # Add the references to the run_response
            if self.run_response.extra_data is None:
                self.run_response.extra_data = RunResponseExtraData()
            if self.run_response.extra_data.references is None:
                self.run_response.extra_data.references = []
            self.run_response.extra_data.references.append(references)
        retrieval_timer.stop()
        logger.debug(f"Time to get references: {retrieval_timer.elapsed:.4f}s")

        if docs_from_knowledge is None:
            return "No documents found"
        return self.convert_documents_to_string(docs_from_knowledge)

    def add_to_knowledge(self, query: str, result: str) -> str:
        """Use this function to add information to the knowledge base for future use.

        Args:
            query: The query to add.
            result: The result of the query.

        Returns:
            str: A string indicating the status of the addition.
        """
        import json

        from agno.document import Document

        if self.knowledge is None:
            return "Knowledge base not available"
        document_name = self.name
        if document_name is None:
            document_name = query.replace(" ", "_").replace("?", "").replace("!", "").replace(".", "")
        document_content = json.dumps({"query": query, "result": result})
        logger.info(f"Adding document to knowledge base: {document_name}: {document_content}")
        self.knowledge.load_document(
            document=Document(
                name=document_name,
                content=document_content,
            )
        )
        return "Successfully added to knowledge base"

    def update_memory(self, task: str) -> str:
        """Use this function to update the Agent's memory. Describe the task in detail.

        Args:
            task: The task to update the memory with.

        Returns:
            str: A string indicating the status of the task.
        """
        self.memory = cast(AgentMemory, self.memory)
        try:
            return self.memory.update_memory(input=task, force=True) or "Memory updated successfully"
        except Exception as e:
            return f"Failed to update memory: {e}"

    ###########################################################################
    # Api functions
    ###########################################################################

    def log_agent_session(self):
        if not (self.telemetry or self.monitoring):
            return

        from agno.api.agent import AgentSessionCreate, create_agent_session

        try:
            agent_session: AgentSession = self.agent_session or self.get_agent_session()
            create_agent_session(
                session=AgentSessionCreate(
                    session_id=agent_session.session_id,
                    agent_data=agent_session.monitoring_data() if self.monitoring else agent_session.telemetry_data(),
                ),
                monitor=self.monitoring,
            )
        except Exception as e:
            logger.debug(f"Could not create agent monitor: {e}")

    async def alog_agent_session(self):
        if not (self.telemetry or self.monitoring):
            return

        from agno.api.agent import AgentSessionCreate, acreate_agent_session

        try:
            agent_session: AgentSession = self.agent_session or self.get_agent_session()
            await acreate_agent_session(
                session=AgentSessionCreate(
                    session_id=agent_session.session_id,
                    agent_data=agent_session.monitoring_data() if self.monitoring else agent_session.telemetry_data(),
                ),
                monitor=self.monitoring,
            )
        except Exception as e:
            logger.debug(f"Could not create agent monitor: {e}")

    def _create_run_data(self) -> Dict[str, Any]:
        """Create and return the run data dictionary."""
        run_response_format = "text"
        self.run_response = cast(RunResponse, self.run_response)
        if self.response_model is not None:
            run_response_format = "json"
        elif self.markdown:
            run_response_format = "markdown"

        functions = {}
        if self.model is not None and self.model._functions is not None:
            functions = {
                f_name: func.to_dict() for f_name, func in self.model._functions.items() if isinstance(func, Function)
            }

        run_data: Dict[str, Any] = {
            "functions": functions,
            "metrics": self.run_response.metrics,
        }

        if self.monitoring:
            run_data.update(
                {
                    "run_input": self.run_input,
                    "run_response": self.run_response.to_dict(),
                    "run_response_format": run_response_format,
                }
            )

        return run_data

    def log_agent_run(self) -> None:
        self.set_monitoring()

        if not (self.telemetry or self.monitoring):
            return

        from agno.api.agent import AgentRunCreate, create_agent_run

        try:
            run_data = self._create_run_data()
            agent_session: AgentSession = self.agent_session or self.get_agent_session()

            create_agent_run(
                run=AgentRunCreate(
                    run_id=self.run_id,
                    run_data=run_data,
                    session_id=agent_session.session_id,
                    agent_data=agent_session.monitoring_data() if self.monitoring else agent_session.telemetry_data(),
                ),
                monitor=self.monitoring,
            )
        except Exception as e:
            logger.debug(f"Could not create agent event: {e}")

    async def alog_agent_run(self) -> None:
        self.set_monitoring()

        if not (self.telemetry or self.monitoring):
            return

        from agno.api.agent import AgentRunCreate, acreate_agent_run

        try:
            run_data = self._create_run_data()
            agent_session: AgentSession = self.agent_session or self.get_agent_session()

            await acreate_agent_run(
                run=AgentRunCreate(
                    run_id=self.run_id,
                    run_data=run_data,
                    session_id=agent_session.session_id,
                    agent_data=agent_session.monitoring_data() if self.monitoring else agent_session.telemetry_data(),
                ),
                monitor=self.monitoring,
            )
        except Exception as e:
            logger.debug(f"Could not create agent event: {e}")

    ###########################################################################
    # Print Response
    ###########################################################################

    def create_panel(self, content, title, border_style="blue"):
        from rich.box import HEAVY
        from rich.panel import Panel

        return Panel(
            content, title=title, title_align="left", border_style=border_style, box=HEAVY, expand=True, padding=(1, 1)
        )

    def print_response(
        self,
        message: Optional[Union[List, Dict, str, Message]] = None,
        *,
        messages: Optional[List[Union[Dict, Message]]] = None,
        audio: Optional[Sequence[Audio]] = None,
        images: Optional[Sequence[Image]] = None,
        videos: Optional[Sequence[Video]] = None,
        stream: bool = False,
        markdown: bool = False,
        show_message: bool = True,
        show_reasoning: bool = True,
        show_full_reasoning: bool = False,
        console: Optional[Any] = None,
        **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
        from rich.text import Text

        if markdown:
            self.markdown = True

        if self.response_model is not None:
            markdown = False
            self.markdown = False
            stream = False

        if stream:
            _response_content: str = ""
            reasoning_steps: List[ReasoningStep] = []
            with Live(console=console) as live_log:
                status = Status("Thinking...", spinner="aesthetic", speed=0.4, refresh_per_second=10)
                live_log.update(status)
                response_timer = Timer()
                response_timer.start()
                # Flag which indicates if the panels should be rendered
                render = False
                # Panels to be rendered
                panels = [status]
                # First render the message panel if the message is not None
                if message and show_message:
                    render = True
                    # Convert message to a panel
                    message_content = get_text_from_message(message)
                    message_panel = self.create_panel(
                        content=Text(message_content, style="green"),
                        title="Message",
                        border_style="cyan",
                    )
                    panels.append(message_panel)
                if render:
                    live_log.update(Group(*panels))

                for resp in self.run(
                    message=message, messages=messages, audio=audio, images=images, videos=videos, stream=True, **kwargs
                ):
                    if isinstance(resp, RunResponse) and isinstance(resp.content, str):
                        if resp.event == RunEvent.run_response:
                            _response_content += resp.content
                        if resp.extra_data is not None and resp.extra_data.reasoning_steps is not None:
                            reasoning_steps = resp.extra_data.reasoning_steps

                    response_content_stream = Markdown(_response_content) if self.markdown else _response_content

                    panels = [status]

                    if message and show_message:
                        render = True
                        # Convert message to a panel
                        message_content = get_text_from_message(message)
                        message_panel = self.create_panel(
                            content=Text(message_content, style="green"),
                            title="Message",
                            border_style="cyan",
                        )
                        panels.append(message_panel)
                    if render:
                        live_log.update(Group(*panels))

                    if len(reasoning_steps) > 0 and show_reasoning:
                        render = True
                        # Create panels for reasoning steps
                        for i, step in enumerate(reasoning_steps, 1):
                            # Build step content
                            step_content = Text.assemble()
                            if step.title is not None:
                                step_content.append(f"{step.title}\n", "bold")
                            if step.action is not None:
                                step_content.append(f"{step.action}\n", "dim")
                            if step.result is not None:
                                step_content.append(Text.from_markup(step.result, style="dim"))

                            if show_full_reasoning:
                                # Add detailed reasoning information if available
                                if step.reasoning is not None:
                                    step_content.append(
                                        Text.from_markup(f"\n[bold]Reasoning:[/bold] {step.reasoning}", style="dim")
                                    )
                                if step.confidence is not None:
                                    step_content.append(
                                        Text.from_markup(f"\n[bold]Confidence:[/bold] {step.confidence}", style="dim")
                                    )
                            reasoning_panel = self.create_panel(
                                content=step_content, title=f"Reasoning step {i}", border_style="green"
                            )
                            panels.append(reasoning_panel)
                    if render:
                        live_log.update(Group(*panels))

                    if len(_response_content) > 0:
                        render = True
                        # Create panel for response
                        response_panel = self.create_panel(
                            content=response_content_stream,
                            title=f"Response ({response_timer.elapsed:.1f}s)",
                            border_style="blue",
                        )
                        panels.append(response_panel)
                    if render:
                        live_log.update(Group(*panels))
                response_timer.stop()

                # Final update to remove the "Thinking..." status
                panels = [p for p in panels if not isinstance(p, Status)]
                live_log.update(Group(*panels))
        else:
            with Live(console=console) as live_log:
                status = Status("Thinking...", spinner="aesthetic", speed=0.4, refresh_per_second=10)
                live_log.update(status)
                response_timer = Timer()
                response_timer.start()
                # Flag which indicates if the panels should be rendered
                render = False
                # Panels to be rendered
                panels = [status]
                # First render the message panel if the message is not None
                if message and show_message:
                    # Convert message to a panel
                    message_content = get_text_from_message(message)
                    message_panel = self.create_panel(
                        content=Text(message_content, style="green"),
                        title="Message",
                        border_style="cyan",
                    )
                    panels.append(message_panel)
                if render:
                    live_log.update(Group(*panels))

                # Run the agent
                run_response = self.run(
                    message=message,
                    messages=messages,
                    audio=audio,
                    images=images,
                    videos=videos,
                    stream=False,
                    **kwargs,
                )
                response_timer.stop()

                reasoning_steps = []
                if (
                    isinstance(run_response, RunResponse)
                    and run_response.extra_data is not None
                    and run_response.extra_data.reasoning_steps is not None
                ):
                    reasoning_steps = run_response.extra_data.reasoning_steps

                if len(reasoning_steps) > 0 and show_reasoning:
                    render = True
                    # Create panels for reasoning steps
                    for i, step in enumerate(reasoning_steps, 1):
                        # Build step content
                        step_content = Text.assemble()
                        if step.title is not None:
                            step_content.append(f"{step.title}\n", "bold")
                        if step.action is not None:
                            step_content.append(f"{step.action}\n", "dim")
                        if step.result is not None:
                            step_content.append(Text.from_markup(step.result, style="dim"))

                        if show_full_reasoning:
                            # Add detailed reasoning information if available
                            if step.reasoning is not None:
                                step_content.append(
                                    Text.from_markup(f"\n[bold]Reasoning:[/bold] {step.reasoning}", style="dim")
                                )
                            if step.confidence is not None:
                                step_content.append(
                                    Text.from_markup(f"\n[bold]Confidence:[/bold] {step.confidence}", style="dim")
                                )
                        reasoning_panel = self.create_panel(
                            content=step_content, title=f"Reasoning step {i}", border_style="green"
                        )
                        panels.append(reasoning_panel)
                    if render:
                        live_log.update(Group(*panels))

                response_content_batch: Union[str, JSON, Markdown] = ""
                if isinstance(run_response, RunResponse):
                    if isinstance(run_response.content, str):
                        response_content_batch = (
                            Markdown(run_response.content)
                            if self.markdown
                            else 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}")

                # Create panel for response
                response_panel = self.create_panel(
                    content=response_content_batch,
                    title=f"Response ({response_timer.elapsed:.1f}s)",
                    border_style="blue",
                )
                panels.append(response_panel)

                # Final update to remove the "Thinking..." status
                panels = [p for p in panels if not isinstance(p, Status)]
                live_log.update(Group(*panels))

    async def aprint_response(
        self,
        message: Optional[Union[List, Dict, str, Message]] = None,
        *,
        messages: Optional[List[Union[Dict, Message]]] = None,
        audio: Optional[Sequence[Audio]] = None,
        images: Optional[Sequence[Image]] = None,
        videos: Optional[Sequence[Video]] = None,
        stream: bool = False,
        markdown: bool = False,
        show_message: bool = True,
        show_reasoning: bool = True,
        show_full_reasoning: bool = False,
        console: Optional[Any] = None,
        **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
        from rich.text import Text

        if markdown:
            self.markdown = True

        if self.response_model is not None:
            markdown = False
            self.markdown = False
            stream = False

        if stream:
            _response_content: str = ""
            reasoning_steps: List[ReasoningStep] = []
            with Live(console=console) as live_log:
                status = Status("Thinking...", spinner="aesthetic", speed=0.4, refresh_per_second=10)
                live_log.update(status)
                response_timer = Timer()
                response_timer.start()
                # Flag which indicates if the panels should be rendered
                render = False
                # Panels to be rendered
                panels = [status]
                # First render the message panel if the message is not None
                if message and show_message:
                    render = True
                    # Convert message to a panel
                    message_content = get_text_from_message(message)
                    message_panel = self.create_panel(
                        content=Text(message_content, style="green"),
                        title="Message",
                        border_style="cyan",
                    )
                    panels.append(message_panel)
                if render:
                    live_log.update(Group(*panels))

                _arun_generator = await self.arun(
                    message=message, messages=messages, audio=audio, images=images, videos=videos, stream=True, **kwargs
                )
                async for resp in _arun_generator:
                    if isinstance(resp, RunResponse) and isinstance(resp.content, str):
                        if resp.event == RunEvent.run_response:
                            _response_content += resp.content
                        if resp.extra_data is not None and resp.extra_data.reasoning_steps is not None:
                            reasoning_steps = resp.extra_data.reasoning_steps
                    response_content_stream = Markdown(_response_content) if self.markdown else _response_content

                    panels = [status]

                    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_panel = self.create_panel(
                            content=Text(message_content, style="green"),
                            title="Message",
                            border_style="cyan",
                        )
                        panels.append(message_panel)
                    if render:
                        live_log.update(Group(*panels))

                    if len(reasoning_steps) > 0 and (show_reasoning or show_full_reasoning):
                        render = True
                        # Create panels for reasoning steps
                        for i, step in enumerate(reasoning_steps, 1):
                            # Build step content
                            step_content = Text.assemble()
                            if step.title is not None:
                                step_content.append(f"{step.title}\n", "bold")
                            if step.action is not None:
                                step_content.append(f"{step.action}\n", "dim")
                            if step.result is not None:
                                step_content.append(Text.from_markup(step.result, style="dim"))

                            if show_full_reasoning:
                                # Add detailed reasoning information if available
                                if step.reasoning is not None:
                                    step_content.append(
                                        Text.from_markup(f"\n[bold]Reasoning:[/bold] {step.reasoning}", style="dim")
                                    )
                                if step.confidence is not None:
                                    step_content.append(
                                        Text.from_markup(f"\n[bold]Confidence:[/bold] {step.confidence}", style="dim")
                                    )
                            reasoning_panel = self.create_panel(
                                content=step_content, title=f"Reasoning step {i}", border_style="green"
                            )
                            panels.append(reasoning_panel)
                    if render:
                        live_log.update(Group(*panels))

                    if len(_response_content) > 0:
                        render = True
                        # Create panel for response
                        response_panel = self.create_panel(
                            content=response_content_stream,
                            title=f"Response ({response_timer.elapsed:.1f}s)",
                            border_style="blue",
                        )
                        panels.append(response_panel)
                    if render:
                        live_log.update(Group(*panels))
                response_timer.stop()

                # Final update to remove the "Thinking..." status
                panels = [p for p in panels if not isinstance(p, Status)]
                live_log.update(Group(*panels))
        else:
            with Live(console=console) as live_log:
                status = Status("Thinking...", spinner="aesthetic", speed=0.4, refresh_per_second=10)
                live_log.update(status)
                response_timer = Timer()
                response_timer.start()
                # Flag which indicates if the panels should be rendered
                render = False
                # Panels to be rendered
                panels = [status]
                # First render the message panel if the message is not None
                if message and show_message:
                    # Convert message to a panel
                    message_content = get_text_from_message(message)
                    message_panel = self.create_panel(
                        content=Text(message_content, style="green"),
                        title="Message",
                        border_style="cyan",
                    )
                    panels.append(message_panel)
                if render:
                    live_log.update(Group(*panels))

                # Run the agent
                run_response = await self.arun(
                    message=message,
                    messages=messages,
                    audio=audio,
                    images=images,
                    videos=videos,
                    stream=False,
                    **kwargs,
                )
                response_timer.stop()

                reasoning_steps = []
                if (
                    isinstance(run_response, RunResponse)
                    and run_response.extra_data is not None
                    and run_response.extra_data.reasoning_steps is not None
                ):
                    reasoning_steps = run_response.extra_data.reasoning_steps

                if len(reasoning_steps) > 0 and show_reasoning:
                    render = True
                    # Create panels for reasoning steps
                    for i, step in enumerate(reasoning_steps, 1):
                        # Build step content
                        step_content = Text.assemble()
                        if step.title is not None:
                            step_content.append(f"{step.title}\n", "bold")
                        if step.action is not None:
                            step_content.append(f"{step.action}\n", "dim")
                        if step.result is not None:
                            step_content.append(Text.from_markup(step.result, style="dim"))

                        if show_full_reasoning:
                            # Add detailed reasoning information if available
                            if step.reasoning is not None:
                                step_content.append(
                                    Text.from_markup(f"\n[bold]Reasoning:[/bold] {step.reasoning}", style="dim")
                                )
                            if step.confidence is not None:
                                step_content.append(
                                    Text.from_markup(f"\n[bold]Confidence:[/bold] {step.confidence}", style="dim")
                                )
                        reasoning_panel = self.create_panel(
                            content=step_content, title=f"Reasoning step {i}", border_style="green"
                        )
                        panels.append(reasoning_panel)
                    if render:
                        live_log.update(Group(*panels))

                response_content_batch: Union[str, JSON, Markdown] = ""
                if isinstance(run_response, RunResponse):
                    if isinstance(run_response.content, str):
                        response_content_batch = (
                            Markdown(run_response.content)
                            if self.markdown
                            else 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}")

                # Create panel for response
                response_panel = self.create_panel(
                    content=response_content_batch,
                    title=f"Response ({response_timer.elapsed:.1f}s)",
                    border_style="blue",
                )
                panels.append(response_panel)

                # Final update to remove the "Thinking..." status
                panels = [p for p in panels if not isinstance(p, Status)]
                live_log.update(Group(*panels))

    def cli_app(
        self,
        message: Optional[str] = None,
        user: str = "User",
        emoji: str = ":sunglasses:",
        stream: bool = False,
        markdown: bool = False,
        exit_on: Optional[List[str]] = None,
        **kwargs: Any,
    ) -> None:
        from rich.prompt import Prompt

        if message:
            self.print_response(message=message, stream=stream, markdown=markdown, **kwargs)

        _exit_on = exit_on or ["exit", "quit", "bye"]
        while True:
            message = Prompt.ask(f"[bold] {emoji} {user} [/bold]")
            if message in _exit_on:
                break

            self.print_response(message=message, stream=stream, markdown=markdown, **kwargs)
