from __future__ import annotations

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

from pydantic import BaseModel

from agno.agent.metrics import SessionMetrics
from agno.exceptions import ModelProviderError, StopAgentRun
from agno.knowledge.agent import AgentKnowledge
from agno.media import Audio, AudioArtifact, AudioResponse, File, Image, ImageArtifact, Video, VideoArtifact
from agno.memory.agent import AgentMemory, AgentRun
from agno.memory.v2.memory import Memory, SessionSummary
from agno.models.base import Model
from agno.models.message import Citations, 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.run.team import TeamRunResponse
from agno.storage.base import Storage
from agno.storage.session.agent import AgentSession
from agno.tools.function import Function
from agno.tools.toolkit import Toolkit
from agno.utils.log import (
    log_debug,
    log_error,
    log_exception,
    log_info,
    log_warning,
    set_log_level_to_debug,
    set_log_level_to_info,
)
from agno.utils.message import get_text_from_message
from agno.utils.prompts import get_json_output_prompt
from agno.utils.response import create_panel, escape_markdown_tags, format_tool_calls
from agno.utils.safe_formatter import SafeFormatter
from agno.utils.string import parse_response_model_str
from agno.utils.timer import Timer


@dataclass(init=False)
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 ---
    # Default user_id to use for this agent
    user_id: Optional[str] = None

    # --- Session settings ---
    # Default session_id to use for this agent (autogenerated if not set)
    session_id: Optional[str] = None
    # Session name
    session_name: Optional[str] = None
    # Session state (stored in the database to persist across runs)
    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[Union[AgentMemory, Memory]] = None
    # Enable the agent to manage memories of the user
    enable_agentic_memory: bool = False
    # If True, the agent creates/updates user memories at the end of runs
    enable_user_memories: bool = False
    # If True, the agent adds a reference to the user memories in the response
    add_memory_references: Optional[bool] = None
    # If True, the agent creates/updates session summaries at the end of runs
    enable_session_summaries: bool = False
    # If True, the agent adds a reference to the session summaries in the response
    add_session_summary_references: Optional[bool] = None

    # --- Agent History ---
    # 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
    # Deprecated in favor of num_history_runs: Number of historical responses to add to the messages
    num_history_responses: Optional[int] = None
    # Number of historical runs to include in the messages
    num_history_runs: int = 3

    # --- Agent Knowledge ---
    knowledge: Optional[AgentKnowledge] = None
    # Enable RAG by adding references from AgentKnowledge to the user prompt.

    # Add knowledge_filters to the Agent class attributes
    knowledge_filters: Optional[Dict[str, Any]] = None

    # Let the agent choose the knowledge filters
    enable_agentic_knowledge_filters: Optional[bool] = False

    add_references: bool = False
    # Retrieval function to get references
    # This function, if provided, is used instead of the default search_knowledge function
    # 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[Storage] = 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.
    tools: Optional[List[Union[Toolkit, Callable, Function, Dict]]] = None
    # Show tool calls in Agent response.
    show_tool_calls: bool = True
    # 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

    # A function that acts as middleware and is called around tool calls.
    tool_hooks: Optional[List[Callable]] = 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
    # Allows for custom timezone for datetime instructions following the TZ Database format (e.g. "Etc/UTC")
    timezone_identifier: Optional[str] = None
    # 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 (in seconds)
    delay_between_retries: int = 1
    # Exponential backoff: if True, the delay between retries is doubled each time
    exponential_backoff: bool = False

    # --- Agent Response Model Settings ---
    # 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: Optional[bool] = None
    # If `response_model` is set, sets the response mode of the model, i.e. if the model should explicitly respond with a JSON object instead of a Pydantic model
    use_json_mode: 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"

    # Optional team session ID, set by the team leader agent.
    team_session_id: Optional[str] = None
    # Optional team ID. Indicates this agent is part of a team.
    team_id: Optional[str] = None
    # Optional team session state. Set by the team leader agent.
    team_session_state: Optional[Dict[str, Any]] = None

    # --- 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

    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[Union[AgentMemory, Memory]] = None,
        enable_agentic_memory: bool = False,
        enable_user_memories: bool = False,
        add_memory_references: Optional[bool] = None,
        enable_session_summaries: bool = False,
        add_session_summary_references: Optional[bool] = None,
        add_history_to_messages: bool = False,
        num_history_responses: Optional[int] = None,
        num_history_runs: int = 3,
        knowledge: Optional[AgentKnowledge] = None,
        knowledge_filters: Optional[Dict[str, Any]] = None,
        enable_agentic_knowledge_filters: Optional[bool] = None,
        add_references: bool = False,
        retriever: Optional[Callable[..., Optional[List[Dict]]]] = None,
        references_format: Literal["json", "yaml"] = "json",
        storage: Optional[Storage] = None,
        extra_data: Optional[Dict[str, Any]] = None,
        tools: Optional[List[Union[Toolkit, Callable, Function, Dict]]] = None,
        show_tool_calls: bool = True,
        tool_call_limit: Optional[int] = None,
        tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
        tool_hooks: Optional[List[Callable]] = 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,
        timezone_identifier: Optional[str] = None,
        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: Optional[bool] = None,
        use_json_mode: 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.enable_agentic_memory = enable_agentic_memory
        self.enable_user_memories = enable_user_memories
        self.add_memory_references = add_memory_references
        self.enable_session_summaries = enable_session_summaries
        self.add_session_summary_references = add_session_summary_references

        self.add_history_to_messages = add_history_to_messages
        self.num_history_responses = num_history_responses
        self.num_history_runs = num_history_runs

        self.knowledge = knowledge
        self.knowledge_filters = knowledge_filters
        self.enable_agentic_knowledge_filters = enable_agentic_knowledge_filters
        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.tool_hooks = tool_hooks

        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.timezone_identifier = timezone_identifier
        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.use_json_mode = use_json_mode
        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

        # --- Params not to be set by user ---
        self.session_metrics: Optional[SessionMetrics] = None

        self.run_id: Optional[str] = None
        self.run_input: Optional[Union[str, List, Dict, Message]] = None
        self.run_messages: Optional[RunMessages] = None
        self.run_response: Optional[RunResponse] = None

        # Images generated during this session
        self.images: Optional[List[ImageArtifact]] = None
        # Audio generated during this session
        self.audio: Optional[List[AudioArtifact]] = None
        # Videos generated during this session
        self.videos: Optional[List[VideoArtifact]] = None
        # Agent session
        self.agent_session: Optional[AgentSession] = None

        self._tool_instructions: Optional[List[str]] = None
        self._tools_for_model: Optional[List[Dict[str, Any]]] = None
        self._functions_for_model: Optional[Dict[str, Function]] = None

        self._formatter: Optional[SafeFormatter] = None

    def set_agent_id(self) -> str:
        if self.agent_id is None:
            self.agent_id = str(uuid4())
        return self.agent_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_storage_mode(self) -> None:
        if self.storage is not None:
            if self.storage.mode in ["workflow", "team"]:
                log_warning(f"You shouldn't use storage in multiple modes. Current mode is {self.storage.mode}.")

            self.storage.mode = "agent"

    def set_monitoring(self) -> None:
        """Override monitoring and telemetry settings based on environment variables."""

        # Only override if the environment variable is set
        monitor_env = getenv("AGNO_MONITOR")
        if monitor_env is not None:
            self.monitoring = monitor_env.lower() == "true"

        telemetry_env = getenv("AGNO_TELEMETRY")
        if telemetry_env is not None:
            self.telemetry = telemetry_env.lower() == "true"

    def set_default_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:
                log_exception(e)
                log_error(
                    "Agno agents use `openai` as the default model provider. "
                    "Please provide a `model` or install `openai`."
                )
                exit(1)

            log_info("Setting default model to OpenAI Chat")
            self.model = OpenAIChat(id="gpt-4o")

    def set_defaults(self) -> None:
        if self.add_memory_references is None:
            self.add_memory_references = self.enable_user_memories or self.enable_agentic_memory

        if self.add_session_summary_references is None:
            self.add_session_summary_references = self.enable_session_summaries

        if self.num_history_responses is not None:
            self.num_history_runs = self.num_history_responses

    def initialize_agent(self) -> None:
        self.set_defaults()
        self.set_default_model()
        self.set_storage_mode()
        self.set_debug()
        self.set_agent_id()

        log_debug(f"Agent ID: {self.agent_id}", center=True)

        if self.memory is None:
            self.memory = Memory()

        # Default to the agent's model if no model is provided
        if isinstance(self.memory, Memory):
            if self.memory.model is None and self.model is not None:
                self.memory.set_model(self.model)

        if self._formatter is None:
            self._formatter = SafeFormatter()

    @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,
        session_id: str,
        user_id: Optional[str] = None,
        audio: Optional[Sequence[Audio]] = None,
        images: Optional[Sequence[Image]] = None,
        videos: Optional[Sequence[Video]] = None,
        files: Optional[Sequence[File]] = None,
        messages: Optional[Sequence[Union[Dict, Message]]] = None,
        knowledge_filters: Optional[Dict[str, Any]] = None,
        stream_intermediate_steps: bool = False,
        run_response: RunResponse,
        **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. Reason about the task if reasoning is enabled
        6. Start the Run by yielding a RunStarted event
        7. Generate a response from the Model (includes running function calls)
        8. Update RunResponse
        9. Update Agent Memory
        10. Calculate session metrics
        11. Save session to storage
        12. Save output to file if save_response_to_file is set
        """

        # 1. Prepare the Agent for the run
        if isinstance(self.memory, AgentMemory):
            self.memory = cast(AgentMemory, self.memory)
        else:
            self.memory = cast(Memory, 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())

        log_debug(f"Agent Run Start: {run_response.run_id}", center=True)

        # 2.1 Prepare arguments for the model
        self.set_default_model()
        response_format = self._get_response_format()
        self.model = cast(Model, self.model)

        self.determine_tools_for_model(
            model=self.model,
            session_id=session_id,
            user_id=user_id,
            async_mode=False,
            knowledge_filters=knowledge_filters,
        )
        run_response.model = self.model.id if self.model is not None else None

        # 2.2 Resolve context
        if self.context is not None and self.resolve_context:
            self.resolve_run_context()

        # 3. Read existing session from storage
        self.read_from_storage(session_id=session_id, user_id=user_id)

        # 4. Prepare run messages
        run_messages: RunMessages = self.get_run_messages(
            message=message,
            session_id=session_id,
            user_id=user_id,
            audio=audio,
            images=images,
            videos=videos,
            files=files,
            messages=messages,
            **kwargs,
        )
        if len(run_messages.messages) == 0:
            log_error("No messages to be sent to the model.")

        self.run_messages = run_messages

        # 5. 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, session_id=session_id)

            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", session_id=session_id, event=RunEvent.run_started)

        # 7. Generate a response from the Model (includes running function calls)
        model_response: ModelResponse
        self.model = cast(Model, self.model)
        reasoning_started = False
        reasoning_time_taken = 0.0
        if self.stream:
            model_response = ModelResponse()
            for model_response_chunk in self.model.response_stream(
                messages=run_messages.messages,
                response_format=response_format,
                tools=self._tools_for_model,
                functions=self._functions_for_model,
                tool_choice=self.tool_choice,
                tool_call_limit=self.tool_call_limit,
            ):
                # If the model response is an assistant_response, yield a RunResponse
                if model_response_chunk.event == ModelResponseEvent.assistant_response.value:
                    # Process content and thinking
                    if model_response_chunk.content is not None:
                        model_response.content = (model_response.content or "") + model_response_chunk.content
                        run_response.content = model_response.content

                    if model_response_chunk.thinking is not None:
                        model_response.thinking = (model_response.thinking or "") + model_response_chunk.thinking
                        run_response.thinking = model_response.thinking

                    if model_response_chunk.redacted_thinking is not None:
                        model_response.redacted_thinking = (
                            model_response.redacted_thinking or ""
                        ) + model_response_chunk.redacted_thinking

                        # We only have thinking on response
                        run_response.thinking = model_response.redacted_thinking

                    if model_response_chunk.citations is not None:
                        # We get citations in one chunk
                        run_response.citations = model_response_chunk.citations

                    # Only yield if we have content or thinking to show
                    if (
                        model_response_chunk.content is not None
                        or model_response_chunk.thinking is not None
                        or model_response_chunk.redacted_thinking is not None
                        or model_response_chunk.citations is not None
                    ):
                        yield self.create_run_response(
                            content=model_response_chunk.content,
                            thinking=model_response_chunk.thinking,
                            redacted_thinking=model_response_chunk.redacted_thinking,
                            citations=model_response_chunk.citations,
                            created_at=model_response_chunk.created_at,
                            session_id=session_id,
                        )

                    # Process audio
                    if model_response_chunk.audio is not None:
                        if model_response.audio is None:
                            model_response.audio = AudioResponse(id=str(uuid4()), content="", transcript="")

                        if model_response_chunk.audio.id is not None:
                            model_response.audio.id = model_response_chunk.audio.id  # type: ignore
                        if model_response_chunk.audio.content is not None:
                            model_response.audio.content += model_response_chunk.audio.content  # type: ignore
                        if model_response_chunk.audio.transcript is not None:
                            model_response.audio.transcript += model_response_chunk.audio.transcript  # type: ignore
                        if model_response_chunk.audio.expires_at is not None:
                            model_response.audio.expires_at = model_response_chunk.audio.expires_at  # type: ignore
                        if model_response_chunk.audio.mime_type is not None:
                            model_response.audio.mime_type = model_response_chunk.audio.mime_type  # type: ignore
                        model_response.audio.sample_rate = model_response_chunk.audio.sample_rate
                        model_response.audio.channels = model_response_chunk.audio.channels

                        # Yield the audio and transcript bit by bit
                        run_response.response_audio = AudioResponse(
                            id=model_response_chunk.audio.id,
                            content=model_response_chunk.audio.content,
                            transcript=model_response_chunk.audio.transcript,
                            sample_rate=model_response_chunk.audio.sample_rate,
                            channels=model_response_chunk.audio.channels,
                        )
                        run_response.created_at = model_response_chunk.created_at

                        yield run_response

                    if model_response_chunk.image is not None:
                        self.add_image(model_response_chunk.image)

                        yield run_response

                # 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
                    new_tool_calls_list = model_response_chunk.tool_calls
                    if new_tool_calls_list is not None:
                        # Add tool calls to the agent.run_response
                        if run_response.tools is None:
                            run_response.tools = new_tool_calls_list
                        else:
                            run_response.tools.extend(new_tool_calls_list)

                        # Format tool calls whenever new ones are added during streaming
                        run_response.formatted_tool_calls = format_tool_calls(run_response.tools)

                    # 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,
                            run_response=run_response,
                            session_id=session_id,
                        )

                # 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:
                    reasoning_step: Optional[ReasoningStep] = None

                    new_tool_calls_list = model_response_chunk.tool_calls
                    if new_tool_calls_list is not None:
                        # Update the existing tool call in the run_response
                        if 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(run_response.tools)
                                if tc.get("tool_call_id") is not None
                            }
                            # Process tool calls
                            for tool_call_dict in new_tool_calls_list:
                                tool_call_id = tool_call_dict.get("tool_call_id")
                                index = tool_call_index_map.get(tool_call_id)
                                if index is not None:
                                    run_response.tools[index] = tool_call_dict
                        else:
                            run_response.tools = new_tool_calls_list

                        # Only iterate through new tool calls
                        for tool_call in new_tool_calls_list:
                            tool_name = tool_call.get("tool_name", "")
                            if tool_name.lower() in ["think", "analyze"]:
                                tool_args = tool_call.get("tool_args", {})

                                reasoning_step = self.update_reasoning_content_from_tool_call(tool_name, tool_args)

                                metrics = tool_call.get("metrics")
                                if metrics is not None and metrics.time is not None:
                                    reasoning_time_taken = reasoning_time_taken + float(metrics.time)

                    if self.stream_intermediate_steps:
                        if reasoning_step is not None:
                            if not reasoning_started:
                                yield self.create_run_response(
                                    content="Reasoning started",
                                    event=RunEvent.reasoning_started,
                                )
                                reasoning_started = True

                            yield self.create_run_response(
                                content=reasoning_step,
                                content_type=reasoning_step.__class__.__name__,
                                event=RunEvent.reasoning_step,
                                reasoning_content=run_response.reasoning_content,
                            )

                        yield self.create_run_response(
                            content=model_response_chunk.content,
                            event=RunEvent.tool_call_completed,
                            run_response=run_response,
                            session_id=session_id,
                        )
        else:
            # Get the model response
            model_response = self.model.response(
                messages=run_messages.messages,
                response_format=response_format,
                tools=self._tools_for_model,
                functions=self._functions_for_model,
                tool_choice=self.tool_choice,
                tool_call_limit=self.tool_call_limit,
            )
            # Format tool calls if they exist
            if model_response.tool_calls:
                run_response.formatted_tool_calls = format_tool_calls(model_response.tool_calls)

            # Handle structured outputs
            if self.response_model is not None and model_response.parsed is not None:
                # We get native structured outputs from the model
                if self._model_should_return_structured_output():
                    # Update the run_response content with the structured output
                    run_response.content = model_response.parsed
                    # Update the run_response content_type with the structured output class name
                    run_response.content_type = self.response_model.__name__
            else:
                # Update the run_response content with the model response content
                run_response.content = model_response.content

            # Update the run_response thinking with the model response thinking
            if model_response.thinking is not None:
                run_response.thinking = model_response.thinking
            if model_response.redacted_thinking is not None:
                if run_response.thinking is None:
                    run_response.thinking = model_response.redacted_thinking
                else:
                    run_response.thinking += model_response.redacted_thinking

            # Update the run_response citations with the model response citations
            if model_response.citations is not None:
                run_response.citations = model_response.citations

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

                # For Reasoning/Thinking/Knowledge Tools update reasoning_content in RunResponse
                for tool_call in model_response.tool_calls:
                    tool_name = tool_call.get("tool_name", "")
                    if tool_name.lower() in ["think", "analyze"]:
                        tool_args = tool_call.get("tool_args", {})
                        self.update_reasoning_content_from_tool_call(tool_name, tool_args)

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

            if model_response.image is not None:
                self.add_image(model_response.image)

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

        if self.stream_intermediate_steps and reasoning_started:
            all_reasoning_steps: List[ReasoningStep] = []
            if run_response and run_response.extra_data and hasattr(run_response.extra_data, "reasoning_steps"):
                all_reasoning_steps = cast(List[ReasoningStep], run_response.extra_data.reasoning_steps)

            if all_reasoning_steps:
                self._add_reasoning_metrics_to_extra_data(reasoning_time_taken)
                yield self.create_run_response(
                    content=ReasoningSteps(reasoning_steps=all_reasoning_steps),
                    content_type=ReasoningSteps.__class__.__name__,
                    event=RunEvent.reasoning_completed,
                )

        # 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
        run_response.messages = messages_for_run_response
        # Update the RunResponse metrics
        run_response.metrics = self.aggregate_metrics_from_messages(messages_for_run_response)

        # Update the run_response audio if streaming
        if self.stream and model_response.audio is not None:
            run_response.response_audio = model_response.audio

        # 9. Update Agent Memory
        if isinstance(self.memory, AgentMemory):
            # 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.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)

            # Create an AgentRun object to add to memory
            agent_run = AgentRun(response=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:
                            log_warning(f"Failed to validate message: {e}")
                    else:
                        log_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:
                        log_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. Calculate session metrics
            self.session_metrics = self.calculate_metrics(self.memory.messages)
        elif isinstance(self.memory, Memory):
            # Add AgentRun to memory
            self.memory.add_run(session_id=session_id, run=run_response)

            self._make_memories_and_summaries(run_messages, session_id, user_id, messages)  # type: ignore

            if self.session_metrics is None:
                self.session_metrics = self.calculate_metrics(run_messages.messages)  # Calculate metrics for the run
            else:
                self.session_metrics += self.calculate_metrics(
                    run_messages.messages
                )  # Calculate metrics for the session

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

        # 11. Save session to storage
        self.write_to_storage(user_id=user_id, session_id=session_id)

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

        # 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(user_id=user_id, session_id=session_id)

        log_debug(f"Agent Run End: {run_response.run_id}", center=True, symbol="*")
        if self.stream_intermediate_steps:
            yield self.create_run_response(
                content=run_response.content,
                reasoning_content=run_response.reasoning_content,
                session_id=session_id,
                event=RunEvent.run_completed,
                run_response=run_response,
            )

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

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

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

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

        # Initialize the Agent
        self.initialize_agent()

        effective_filters = knowledge_filters

        # When filters are passed manually
        if self.knowledge_filters or knowledge_filters:
            """
                initialize metadata (specially required in case when load is commented out)
                when load is not called the reader's document_lists won't be called and metadata filters won't be initialized
                so we need to call initialize_valid_filters to make sure the filters are initialized
            """
            if not self.knowledge.valid_metadata_filters:  # type: ignore
                self.knowledge.initialize_valid_filters()  # type: ignore

            effective_filters = self._get_effective_filters(knowledge_filters)

        # Agentic filters are enabled
        if self.enable_agentic_knowledge_filters and not self.knowledge.valid_metadata_filters:  # type: ignore
            # initialize metadata (specially required in case when load is commented out)
            self.knowledge.initialize_valid_filters()  # type: ignore

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

        # Use stream override value when necessary
        if stream is None:
            stream = False if self.stream is None else self.stream

        if stream_intermediate_steps is None:
            stream_intermediate_steps = (
                False if self.stream_intermediate_steps is None else self.stream_intermediate_steps
            )

        # Can't have stream_intermediate_steps if stream is False
        if stream is False:
            stream_intermediate_steps = False

        # Use the default user_id and session_id when necessary
        if user_id is None:
            user_id = self.user_id

        if session_id is None or session_id == "":
            if not (self.session_id is None or self.session_id == ""):
                session_id = self.session_id
            else:
                # Generate a new session_id and store it in the agent
                session_id = str(uuid4())
                self.session_id = session_id

        session_id = cast(str, session_id)

        self._initialize_session_state(user_id=user_id, session_id=session_id)

        log_debug(f"Session ID: {session_id}", center=True)

        last_exception = None
        num_attempts = retries + 1

        # Create a run_id for this specific run
        run_id = str(uuid4())

        for attempt in range(num_attempts):
            try:
                # Create a new run_response for this attempt
                run_response = RunResponse(run_id=run_id, session_id=session_id, agent_id=self.agent_id)

                # for backward compatibility, set self.run_response
                self.run_response = run_response

                # 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 stream=False and run the agent
                    if self.stream and self.stream is True:
                        log_debug("Setting stream=False as response_model is set")
                        self.stream = False
                    rr: RunResponse = next(
                        self._run(
                            message=message,
                            stream=False,
                            user_id=user_id,
                            session_id=session_id,
                            audio=audio,
                            images=images,
                            videos=videos,
                            files=files,
                            messages=messages,
                            stream_intermediate_steps=stream_intermediate_steps,
                            knowledge_filters=effective_filters,
                            run_response=run_response,
                            **kwargs,
                        )
                    )

                    # Do a final check confirming the content is in the response_model format
                    if isinstance(rr.content, self.response_model):
                        return rr

                    # Otherwise convert the response to the structured format
                    if isinstance(rr.content, str):
                        try:
                            structured_output = parse_response_model_str(rr.content, self.response_model)

                            # Update RunResponse
                            if structured_output is not None:
                                rr.content = structured_output
                                rr.content_type = self.response_model.__name__
                            else:
                                log_warning("Failed to convert response to response_model")
                        except Exception as e:
                            log_warning(f"Failed to convert response to output model: {e}")
                    else:
                        log_warning("Something went wrong. Run response content is not a string")
                    return rr
                else:
                    if stream and self.is_streamable:
                        resp = self._run(
                            message=message,
                            stream=True,
                            user_id=user_id,
                            session_id=session_id,
                            audio=audio,
                            images=images,
                            videos=videos,
                            files=files,
                            messages=messages,
                            stream_intermediate_steps=stream_intermediate_steps,
                            knowledge_filters=effective_filters,
                            run_response=run_response,
                            **kwargs,
                        )
                        return resp
                    else:
                        resp = self._run(
                            message=message,
                            stream=False,
                            user_id=user_id,
                            session_id=session_id,
                            audio=audio,
                            images=images,
                            videos=videos,
                            files=files,
                            messages=messages,
                            stream_intermediate_steps=stream_intermediate_steps,
                            knowledge_filters=effective_filters,
                            run_response=run_response,
                            **kwargs,
                        )
                        return next(resp)
            except ModelProviderError as e:
                log_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)
            except KeyboardInterrupt:
                # Create a cancelled response
                cancelled_response = RunResponse(
                    run_id=self.run_id or str(uuid4()),
                    session_id=session_id,
                    agent_id=self.agent_id,
                    content="Operation cancelled by user",
                    event=RunEvent.run_cancelled,
                )
                return cancelled_response

        # If we get here, all retries failed
        if last_exception is not None:
            log_error(
                f"Failed after {num_attempts} attempts. Last error using {last_exception.model_name}({last_exception.model_id})"
            )
            raise last_exception
        else:
            raise Exception(f"Failed after {num_attempts} attempts.")

    async def _arun(
        self,
        message: Optional[Union[str, List, Dict, Message]] = None,
        *,
        stream: bool = False,
        session_id: str,
        user_id: Optional[str] = None,
        audio: Optional[Sequence[Audio]] = None,
        images: Optional[Sequence[Image]] = None,
        videos: Optional[Sequence[Video]] = None,
        files: Optional[Sequence[File]] = None,
        messages: Optional[Sequence[Union[Dict, Message]]] = None,
        stream_intermediate_steps: bool = False,
        knowledge_filters: Optional[Dict[str, Any]] = None,
        run_response: RunResponse,
        **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. Reason about the task if reasoning is enabled
        6. Start the Run by yielding a RunStarted event
        7. Generate a response from the Model (includes running function calls)
        8. Update RunResponse
        9. Update Agent Memory
        10. Calculate session metrics
        11. Save session to storage
        12. Save output to file if save_response_to_file is set
        """

        # 1. Prepare the Agent for the run
        if isinstance(self.memory, AgentMemory):
            self.memory = cast(AgentMemory, self.memory)
        else:
            self.memory = cast(Memory, 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())

        log_debug(f"Async Agent Run Start: {run_response.run_id}", center=True, symbol="*")

        # 2.1 Prepare arguments for the model
        self.set_default_model()
        response_format = self._get_response_format()
        self.model = cast(Model, self.model)
        
        self.determine_tools_for_model(
            model=self.model,
            session_id=session_id,
            user_id=user_id,
            async_mode=True,
            knowledge_filters=knowledge_filters,
        )

        run_response.model = self.model.id if self.model is not None else None

        # 2.2 Resolve context
        if self.context is not None and self.resolve_context:
            await self.aresolve_run_context()

        # 3. Read existing session from storage
        self.read_from_storage(session_id=session_id, user_id=user_id)

        # 4. Prepare run messages
        run_messages: RunMessages = self.get_run_messages(
            message=message,
            session_id=session_id,
            user_id=user_id,
            audio=audio,
            images=images,
            videos=videos,
            files=files,
            messages=messages,
            **kwargs,
        )
        if len(run_messages.messages) == 0:
            log_error("No messages to be sent to the model.")
        self.run_messages = run_messages

        # 5. 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, session_id=session_id)
            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", session_id=session_id, event=RunEvent.run_started)

        # 7. Generate a response from the Model (includes running function calls)
        reasoning_started = False
        reasoning_time_taken = 0.0

        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,
                response_format=response_format,
                tools=self._tools_for_model,
                functions=self._functions_for_model,
                tool_choice=self.tool_choice,
                tool_call_limit=self.tool_call_limit,
            )  # type: ignore
            async for model_response_chunk in model_response_stream:  # type: ignore
                # If the model response is an assistant_response, yield a RunResponse
                if model_response_chunk.event == ModelResponseEvent.assistant_response.value:
                    # Process content and thinking
                    if model_response_chunk.content is not None:
                        model_response.content = (model_response.content or "") + model_response_chunk.content
                        run_response.content = model_response.content

                    if model_response_chunk.thinking is not None:
                        model_response.thinking = (model_response.thinking or "") + model_response_chunk.thinking
                        run_response.thinking = model_response.thinking

                    if model_response_chunk.redacted_thinking is not None:
                        model_response.redacted_thinking = (
                            model_response.redacted_thinking or ""
                        ) + model_response_chunk.redacted_thinking
                        # We only have thinking on response
                        run_response.thinking = model_response.redacted_thinking

                    if model_response_chunk.citations is not None:
                        run_response.citations = model_response_chunk.citations

                    # Only yield if we have content or thinking to show
                    if (
                        model_response_chunk.content is not None
                        or model_response_chunk.thinking is not None
                        or model_response_chunk.redacted_thinking is not None
                        or model_response_chunk.citations is not None
                    ):
                        yield self.create_run_response(
                            content=model_response_chunk.content,
                            thinking=model_response_chunk.thinking,
                            redacted_thinking=model_response_chunk.redacted_thinking,
                            citations=model_response_chunk.citations,
                            created_at=model_response_chunk.created_at,
                            session_id=session_id,
                        )

                    # Process audio
                    if model_response_chunk.audio is not None:
                        if model_response.audio is None:
                            model_response.audio = AudioResponse(id=str(uuid4()), content="", transcript="")

                        if model_response_chunk.audio.id is not None:
                            model_response.audio.id = model_response_chunk.audio.id  # type: ignore
                        if model_response_chunk.audio.content is not None:
                            model_response.audio.content += model_response_chunk.audio.content  # type: ignore
                        if model_response_chunk.audio.transcript is not None:
                            model_response.audio.transcript += model_response_chunk.audio.transcript  # type: ignore
                        if model_response_chunk.audio.expires_at is not None:
                            model_response.audio.expires_at = model_response_chunk.audio.expires_at  # type: ignore
                        if model_response_chunk.audio.mime_type is not None:
                            model_response.audio.mime_type = model_response_chunk.audio.mime_type  # type: ignore
                        model_response.audio.sample_rate = model_response_chunk.audio.sample_rate
                        model_response.audio.channels = model_response_chunk.audio.channels

                        # Yield the audio and transcript bit by bit
                        run_response.response_audio = AudioResponse(
                            id=model_response_chunk.audio.id,
                            content=model_response_chunk.audio.content,
                            transcript=model_response_chunk.audio.transcript,
                            sample_rate=model_response_chunk.audio.sample_rate,
                            channels=model_response_chunk.audio.channels,
                        )
                        run_response.created_at = model_response_chunk.created_at

                        yield run_response

                    if model_response_chunk.image is not None:
                        self.add_image(model_response_chunk.image)

                        yield run_response

                # 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
                    new_tool_calls_list = model_response_chunk.tool_calls
                    if new_tool_calls_list is not None:
                        # Add tool calls to the agent.run_response
                        if run_response.tools is None:
                            run_response.tools = new_tool_calls_list
                        else:
                            run_response.tools.extend(new_tool_calls_list)

                        # Format tool calls whenever new ones are added during streaming
                        run_response.formatted_tool_calls = format_tool_calls(run_response.tools)

                    # 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,
                            run_response=run_response,
                            session_id=session_id,
                        )

                # 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:
                    reasoning_step: Optional[ReasoningStep] = None
                    new_tool_calls_list = model_response_chunk.tool_calls
                    if new_tool_calls_list is not None:
                        # Update the existing tool call in the run_response
                        if 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(run_response.tools)
                                if tc.get("tool_call_id") is not None
                            }
                            # Process tool calls
                            for tool_call_dict in new_tool_calls_list:
                                tool_call_id = tool_call_dict.get("tool_call_id")
                                index = tool_call_index_map.get(tool_call_id)
                                if index is not None:
                                    run_response.tools[index] = tool_call_dict
                        else:
                            run_response.tools = new_tool_calls_list

                        # Only iterate through new tool calls
                        for tool_call in new_tool_calls_list:
                            tool_name = tool_call.get("tool_name", "")
                            if tool_name.lower() in ["think", "analyze"]:
                                tool_args = tool_call.get("tool_args", {})

                                reasoning_step = self.update_reasoning_content_from_tool_call(tool_name, tool_args)

                                metrics = tool_call.get("metrics")
                                if metrics is not None and metrics.time is not None:
                                    reasoning_time_taken = reasoning_time_taken + float(metrics.time)

                    if self.stream_intermediate_steps:
                        if reasoning_step is not None:
                            if not reasoning_started:
                                yield self.create_run_response(
                                    content="Reasoning started",
                                    event=RunEvent.reasoning_started,
                                )
                                reasoning_started = True

                            yield self.create_run_response(
                                content=reasoning_step,
                                content_type=reasoning_step.__class__.__name__,
                                event=RunEvent.reasoning_step,
                                reasoning_content=run_response.reasoning_content,
                            )

                        yield self.create_run_response(
                            content=model_response_chunk.content,
                            event=RunEvent.tool_call_completed,
                            run_response=run_response,
                            session_id=session_id,
                        )
        else:
            # Get the model response
            model_response = await self.model.aresponse(
                messages=run_messages.messages,
                response_format=response_format,
                tools=self._tools_for_model,
                functions=self._functions_for_model,
                tool_choice=self.tool_choice,
                tool_call_limit=self.tool_call_limit,
            )
            # Format tool calls if they exist
            if model_response.tool_calls:
                run_response.formatted_tool_calls = format_tool_calls(model_response.tool_calls)

            # Handle structured outputs
            if self.response_model is not None and model_response.parsed is not None:
                # We get native structured outputs from the model
                if self._model_should_return_structured_output():
                    # Update the run_response content with the structured output
                    run_response.content = model_response.parsed
                    # Update the run_response content_type with the structured output class name
                    run_response.content_type = self.response_model.__name__
            else:
                # Update the run_response content with the model response content
                run_response.content = model_response.content

            # Update the run_response thinking with the model response thinking
            if model_response.thinking is not None:
                run_response.thinking = model_response.thinking
            if model_response.redacted_thinking is not None:
                if run_response.thinking is None:
                    run_response.thinking = model_response.redacted_thinking
                else:
                    run_response.thinking += model_response.redacted_thinking

            if model_response.citations is not None:
                run_response.citations = model_response.citations

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

                # For Reasoning/Thinking/Knowledge Tools update reasoning_content in RunResponse
                for tool_call in model_response.tool_calls:
                    tool_name = tool_call.get("tool_name", "")
                    if tool_name.lower() in ["think", "analyze"]:
                        tool_args = tool_call.get("tool_args", {})
                        self.update_reasoning_content_from_tool_call(tool_name, tool_args)

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

            if model_response.image is not None:
                self.add_image(model_response.image)

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

        if self.stream_intermediate_steps and reasoning_started:
            all_reasoning_steps: List[ReasoningStep] = []
            if run_response and run_response.extra_data and hasattr(run_response.extra_data, "reasoning_steps"):
                all_reasoning_steps = cast(List[ReasoningStep], run_response.extra_data.reasoning_steps)

            if all_reasoning_steps:
                self._add_reasoning_metrics_to_extra_data(reasoning_time_taken)
                yield self.create_run_response(
                    content=ReasoningSteps(reasoning_steps=all_reasoning_steps),
                    content_type=ReasoningSteps.__class__.__name__,
                    event=RunEvent.reasoning_completed,
                )

        # 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
        run_response.messages = messages_for_run_response
        # Update the RunResponse metrics
        run_response.metrics = self.aggregate_metrics_from_messages(messages_for_run_response)

        # Update the run_response audio if streaming
        if self.stream and model_response.audio is not None:
            run_response.response_audio = model_response.audio

        # 9. Update Agent Memory
        if isinstance(self.memory, AgentMemory):
            # 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.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)

            # Create an AgentRun object to add to memory
            agent_run = AgentRun(response=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:
                            log_warning(f"Failed to validate message: {e}")
                    else:
                        log_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:
                        log_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()

            self.session_metrics = self.calculate_metrics(self.memory.messages)
        elif isinstance(self.memory, Memory):
            # Add AgentRun to memory
            self.memory.add_run(session_id=session_id, run=run_response)

            await self._amake_memories_and_summaries(run_messages, session_id, user_id, messages)  # type: ignore

            if self.session_metrics is None:
                self.session_metrics = self.calculate_metrics(run_messages.messages)  # Calculate metrics for the run
            else:
                self.session_metrics += self.calculate_metrics(
                    run_messages.messages
                )  # Calculate metrics for the session

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

        # 11. Save session to storage
        self.write_to_storage(user_id=user_id, session_id=session_id)

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

        # 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(user_id=user_id, session_id=session_id)

        log_debug(f"Agent Run End: {run_response.run_id}", center=True, symbol="*")
        if self.stream_intermediate_steps:
            yield self.create_run_response(
                content=run_response.content,
                reasoning_content=run_response.reasoning_content,
                session_id=session_id,
                event=RunEvent.run_completed,
                run_response=run_response,
            )

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

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

        # Initialize the Agent
        self.initialize_agent()

        effective_filters = knowledge_filters

        # When filters are passed manually
        if self.knowledge_filters or knowledge_filters:
            """
                initialize metadata (specially required in case when load is commented out)
                when load is not called the reader's document_lists won't be called and metadata filters won't be initialized
                so we need to call initialize_valid_filters to make sure the filters are initialized
            """
            if not self.knowledge.valid_metadata_filters:  # type: ignore
                self.knowledge.initialize_valid_filters()  # type: ignore

            effective_filters = self._get_effective_filters(knowledge_filters)

        # Agentic filters are enabled
        if self.enable_agentic_knowledge_filters and not self.knowledge.valid_metadata_filters:  # type: ignore
            # initialize metadata (specially required in case when load is commented out)
            self.knowledge.initialize_valid_filters()  # type: ignore

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

        # Use stream override value when necessary
        if stream is None:
            stream = False if self.stream is None else self.stream

        if stream_intermediate_steps is None:
            stream_intermediate_steps = (
                False if self.stream_intermediate_steps is None else self.stream_intermediate_steps
            )

        # Can't have stream_intermediate_steps if stream is False
        if stream is False:
            stream_intermediate_steps = False

        # Use the default user_id and session_id when necessary
        if user_id is None:
            user_id = self.user_id

        if session_id is None or session_id == "":
            if not (self.session_id is None or self.session_id == ""):
                session_id = self.session_id
            else:
                # Generate a new session_id and store it in the agent
                session_id = str(uuid4())
                self.session_id = session_id

        session_id = cast(str, session_id)

        self._initialize_session_state(user_id=user_id, session_id=session_id)

        log_debug(f"Session ID: {session_id}", center=True)

        last_exception = None
        num_attempts = retries + 1

        # Create a run_id for this specific run
        run_id = str(uuid4())

        for attempt in range(num_attempts):
            try:
                # Create a new run_response for this attempt
                run_response = RunResponse(run_id=run_id, session_id=session_id, agent_id=self.agent_id)

                # for backward compatibility, set self.run_response
                self.run_response = run_response

                # 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 stream=False and run the agent
                    if self.stream and self.stream is True:
                        log_debug("Setting stream=False as response_model is set")
                        self.stream = False

                    resp = await self._arun(
                        message=message,
                        stream=False,
                        user_id=user_id,
                        session_id=session_id,
                        audio=audio,
                        images=images,
                        videos=videos,
                        files=files,
                        messages=messages,
                        stream_intermediate_steps=stream_intermediate_steps,
                        knowledge_filters=effective_filters,
                        run_response=run_response,
                        **kwargs,
                    ).__anext__()

                    # Do a final check confirming the content is in the response_model format
                    if isinstance(resp.content, self.response_model):
                        return resp

                    # Otherwise convert the response to the structured format
                    if isinstance(resp.content, str):
                        try:
                            structured_output = parse_response_model_str(resp.content, self.response_model)

                            # Update RunResponse
                            if structured_output is not None:
                                resp.content = structured_output
                                resp.content_type = self.response_model.__name__
                            else:
                                log_warning("Failed to convert response to response_model")
                        except Exception as e:
                            log_warning(f"Failed to convert response to output model: {e}")
                    else:
                        log_warning("Something went wrong. Run response content is not a string")
                    return resp
                else:
                    # Pass the new run_response to _arun
                    if stream and self.is_streamable:
                        resp = self._arun(
                            message=message,
                            stream=True,
                            user_id=user_id,
                            session_id=session_id,
                            audio=audio,
                            images=images,
                            videos=videos,
                            files=files,
                            messages=messages,
                            stream_intermediate_steps=stream_intermediate_steps,
                            knowledge_filters=effective_filters,
                            run_response=run_response,
                            **kwargs,
                        )  # type: ignore[assignment]
                        return resp
                    else:
                        return await self._arun(
                            message=message,
                            stream=False,
                            user_id=user_id,
                            session_id=session_id,
                            audio=audio,
                            images=images,
                            videos=videos,
                            files=files,
                            messages=messages,
                            stream_intermediate_steps=stream_intermediate_steps,
                            knowledge_filters=effective_filters,
                            run_response=run_response,
                            **kwargs,
                        ).__anext__()  # type: ignore[assignment]
            except ModelProviderError as e:
                log_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)
            except KeyboardInterrupt:
                # Create a cancelled response
                return RunResponse(
                    run_id=run_id,
                    session_id=session_id,
                    agent_id=self.agent_id,
                    content="Operation cancelled by user",
                    event=RunEvent.run_cancelled,
                )

        # If we get here, all retries failed
        if last_exception is not None:
            log_error(
                f"Failed after {num_attempts} attempts. Last error using {last_exception.model_name}({last_exception.model_id})"
            )
            raise last_exception
        else:
            raise Exception(f"Failed after {num_attempts} attempts.")

    def create_run_response(
        self,
        content: Optional[Any] = None,
        *,
        session_id: Optional[str] = None,
        thinking: Optional[str] = None,
        redacted_thinking: Optional[str] = None,
        reasoning_content: Optional[str] = None,
        event: RunEvent = RunEvent.run_response,
        content_type: Optional[str] = None,
        created_at: Optional[int] = None,
        citations: Optional[Citations] = None,
        run_response: Optional[RunResponse] = None,
    ) -> RunResponse:
        thinking_combined = (thinking or "") + (redacted_thinking or "")

        tools = None
        response_audio = None
        audio = None
        images = None
        videos = None
        model = None
        messages = None
        extra_data = None

        if run_response:
            model = run_response.model
            messages = run_response.messages
            extra_data = run_response.extra_data
            content = run_response.content
            content_type = run_response.content_type
            audio = run_response.audio
            images = run_response.images
            videos = run_response.videos
            response_audio = run_response.response_audio
            citations = run_response.citations
            tools = run_response.tools
            reasoning_content = run_response.reasoning_content

        rr = RunResponse(
            run_id=self.run_id,
            session_id=session_id,
            agent_id=self.agent_id,
            content=content,
            thinking=thinking_combined if thinking_combined else None,
            reasoning_content=reasoning_content,
            tools=tools,
            audio=audio,
            images=images,
            videos=videos,
            citations=citations,
            response_audio=response_audio,
            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
        if messages is not None:
            rr.messages = messages
        if extra_data is not None:
            rr.extra_data = extra_data
        if model is not None:
            rr.model = model
        return rr

    def _initialize_session_state(self, user_id: Optional[str] = None, session_id: Optional[str] = None) -> None:
        self.session_state = self.session_state or {}
        if user_id is not None:
            self.session_state["current_user_id"] = user_id
        if session_id is not None:
            self.session_state["current_session_id"] = session_id

    def _make_memories_and_summaries(
        self,
        run_messages: RunMessages,
        session_id: str,
        user_id: Optional[str] = None,
        messages: Optional[List[Message]] = None,
    ) -> None:
        session_messages: List[Message] = []
        self.memory = cast(Memory, self.memory)
        if self.enable_user_memories and run_messages.user_message is not None:
            log_debug("Creating user memories.")
            self.memory.create_user_memories(message=run_messages.user_message.get_content_string(), user_id=user_id)

            # TODO: Possibly do both of these in one step
            if messages is not None and len(messages) > 0:
                parsed_messages = []
                for _im in messages:
                    # Parse the message and convert to a Message object if possible
                    if isinstance(_im, Message):
                        parsed_messages.append(_im)
                    elif isinstance(_im, dict):
                        try:
                            parsed_messages.append(Message(**_im))
                        except Exception as e:
                            log_warning(f"Failed to validate message: {e}")
                    else:
                        log_warning(f"Unsupported message type: {type(_im)}")
                        continue

                if len(parsed_messages) > 0:
                    if session_messages is None:
                        session_messages = []
                    session_messages.extend(parsed_messages)
                    self.memory.create_user_memories(messages=parsed_messages, user_id=user_id)
                else:
                    log_warning("Unable to add messages to memory")

        # Update the session summary if needed
        if self.enable_session_summaries:
            log_debug("Creating session summary.")
            self.memory.create_session_summary(session_id=session_id, user_id=user_id)

    async def _amake_memories_and_summaries(
        self,
        run_messages: RunMessages,
        session_id: str,
        user_id: Optional[str] = None,
        messages: Optional[List[Message]] = None,
    ) -> None:
        self.memory = cast(Memory, self.memory)
        session_messages: List[Message] = []
        if self.enable_user_memories and run_messages.user_message is not None:
            log_debug("Creating user memories.")
            await self.memory.acreate_user_memories(
                message=run_messages.user_message.get_content_string(), user_id=user_id
            )

            # TODO: Possibly do both of these in one step
            if messages is not None and len(messages) > 0:
                parsed_messages = []
                for _im in messages:
                    # Parse the message and convert to a Message object if possible
                    if isinstance(_im, Message):
                        parsed_messages.append(_im)
                    elif isinstance(_im, dict):
                        try:
                            parsed_messages.append(Message(**_im))
                        except Exception as e:
                            log_warning(f"Failed to validate message: {e}")
                    else:
                        log_warning(f"Unsupported message type: {type(_im)}")
                        continue

                if len(parsed_messages) > 0:
                    if session_messages is None:
                        session_messages = []
                    session_messages.extend(parsed_messages)
                    await self.memory.acreate_user_memories(messages=parsed_messages, user_id=user_id)
                else:
                    log_warning("Unable to add messages to memory")

        # Update the session summary if needed
        if self.enable_session_summaries:
            log_debug("Creating session summary.")
            await self.memory.acreate_session_summary(session_id=session_id, user_id=user_id)

    def get_tools(
        self,
        session_id: str,
        async_mode: bool = False,
        user_id: Optional[str] = None,
        knowledge_filters: Optional[Dict[str, Any]] = None,
    ) -> Optional[List[Union[Toolkit, Callable, Function, Dict]]]:
        agent_tools: List[Union[Toolkit, Callable, Function, Dict]] = []

        # Add provided tools
        if self.tools is not None:
            agent_tools.extend(self.tools)

        # Add tools for accessing memory
        if self.read_chat_history:
            agent_tools.append(self.get_chat_history_function(session_id=session_id))
        if self.read_tool_call_history:
            agent_tools.append(self.get_tool_call_history_function(session_id=session_id))

        if isinstance(self.memory, AgentMemory) and self.memory.create_user_memories:
            agent_tools.append(self.update_memory)
        elif isinstance(self.memory, Memory) and self.enable_agentic_memory:
            agent_tools.append(self.get_update_user_memory_function(user_id=user_id, async_mode=async_mode))

        # Add tools for accessing knowledge
        if self.knowledge is not None or self.retriever is not None:
            # Check if retriever is an async function but used in sync mode
            from inspect import iscoroutinefunction

            if not async_mode and iscoroutinefunction(self.retriever):
                log_warning(
                    "Async retriever function is being used with synchronous agent.run() or agent.print_response(). "
                    "It is recommended to use agent.arun() or agent.aprint_response() instead."
                )

            if self.search_knowledge:
                # Use async or sync search based on async_mode
                if self.enable_agentic_knowledge_filters:
                    agent_tools.append(
                        self.search_knowledge_base_with_agentic_filters_function(
                            async_mode=async_mode, knowledge_filters=knowledge_filters
                        )
                    )
                else:
                    agent_tools.append(
                        self.search_knowledge_base_function(async_mode=async_mode, knowledge_filters=knowledge_filters)
                    )
            if self.update_knowledge:
                agent_tools.append(self.add_to_knowledge)

        # Add transfer tools
        if self.has_team and self.team is not None:
            for agent_index, agent in enumerate(self.team):
                agent_tools.append(self.get_transfer_function(agent, agent_index, session_id))

        return agent_tools

    def determine_tools_for_model(
        self,
        model: Model,
        session_id: str,
        user_id: Optional[str] = None,
        async_mode: bool = False,
        knowledge_filters: Optional[Dict[str, Any]] = None,
    ) -> None:
        agent_tools = self.get_tools(
            session_id=session_id, async_mode=async_mode, user_id=user_id, knowledge_filters=knowledge_filters
        )

        if self._tools_for_model is None:
            self._tools_for_model = []
            self._functions_for_model = {}

            # Get Agent tools
            if agent_tools is not None and len(agent_tools) > 0:
                log_debug("Processing tools for model")

                # Check if we need strict mode for the functions for the model
                strict = False
                if (
                    self.response_model is not None
                    and (self.structured_outputs or (not self.use_json_mode))
                    and model.supports_native_structured_outputs
                ):
                    strict = True

                for tool in agent_tools:
                    if isinstance(tool, Dict):
                        # If a dict is passed, it is a builtin tool
                        # that is run by the model provider and not the Agent
                        self._tools_for_model.append(tool)
                        log_debug(f"Included builtin tool {tool}")

                    elif isinstance(tool, Toolkit):
                        # For each function in the toolkit and process entrypoint
                        for name, func in tool.functions.items():
                            # If the function does not exist in self.functions
                            if name not in self._functions_for_model:
                                func._agent = self
                                func.process_entrypoint(strict=strict)
                                if strict and func.strict is None:
                                    func.strict = True
                                if self.tool_hooks is not None:
                                    func.tool_hooks = self.tool_hooks
                                self._functions_for_model[name] = func
                                self._tools_for_model.append({"type": "function", "function": func.to_dict()})
                                log_debug(f"Added tool {name} from {tool.name}")

                        # Add instructions from the toolkit
                        if tool.add_instructions and tool.instructions is not None:
                            if self._tool_instructions is None:
                                self._tool_instructions = []
                            self._tool_instructions.append(tool.instructions)

                    elif isinstance(tool, Function):
                        if tool.name not in self._functions_for_model:
                            tool._agent = self
                            tool.process_entrypoint(strict=strict)
                            if strict and tool.strict is None:
                                tool.strict = True
                            if self.tool_hooks is not None:
                                tool.tool_hooks = self.tool_hooks
                            self._functions_for_model[tool.name] = tool
                            self._tools_for_model.append({"type": "function", "function": tool.to_dict()})
                            log_debug(f"Added tool {tool.name}")

                        # Add instructions from the Function
                        if tool.add_instructions and tool.instructions is not None:
                            if self._tool_instructions is None:
                                self._tool_instructions = []
                            self._tool_instructions.append(tool.instructions)

                    elif callable(tool):
                        try:
                            function_name = tool.__name__
                            if function_name not in self._functions_for_model:
                                func = Function.from_callable(tool, strict=strict)
                                func._agent = self
                                if strict:
                                    func.strict = True
                                if self.tool_hooks is not None:
                                    func.tool_hooks = self.tool_hooks
                                self._functions_for_model[func.name] = func
                                self._tools_for_model.append({"type": "function", "function": func.to_dict()})
                                log_debug(f"Added tool {func.name}")
                        except Exception as e:
                            log_warning(f"Could not add tool {tool}: {e}")

    def _model_should_return_structured_output(self):
        self.model = cast(Model, self.model)
        return bool(
            self.model.supports_native_structured_outputs
            and self.response_model is not None
            and (not self.use_json_mode or self.structured_outputs)
        )

    def _get_response_format(self) -> Optional[Union[Dict, Type[BaseModel]]]:
        self.model = cast(Model, self.model)
        if self.response_model is None:
            return None
        else:
            json_response_format = {"type": "json_object"}

            if self.model.supports_native_structured_outputs:
                if not self.use_json_mode or self.structured_outputs:
                    log_debug("Setting Model.response_format to Agent.response_model")
                    return self.response_model
                else:
                    log_debug(
                        "Model supports native structured outputs but it is not enabled. Using JSON mode instead."
                    )
                    return json_response_format

            elif self.model.supports_json_schema_outputs:
                if self.use_json_mode or (not self.structured_outputs):
                    log_debug("Setting Model.response_format to JSON response mode")
                    return {
                        "type": "json_schema",
                        "json_schema": {
                            "name": self.response_model.__name__,
                            "schema": self.response_model.model_json_schema(),
                        },
                    }
                else:
                    return None

            else:
                log_debug("Model does not support structured or JSON schema outputs.")
                return json_response_format

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

        log_debug("Resolving context")
        if not isinstance(self.context, dict):
            log_warning("Context is not a dict")
            return

        for key, value in self.context.items():
            if callable(value):
                try:
                    sig = signature(value)
                    result = value(agent=self) if "agent" in sig.parameters else value()
                    if result is not None:
                        self.context[key] = result
                except Exception as e:
                    log_warning(f"Failed to resolve context for '{key}': {e}")
            else:
                self.context[key] = value

    async def aresolve_run_context(self) -> None:
        from inspect import iscoroutine, signature

        log_debug("Resolving context (async)")
        if not isinstance(self.context, dict):
            log_warning("Context is not a dict")
            return

        for key, value in self.context.items():
            if not callable(value):
                self.context[key] = value
                continue

            try:
                sig = signature(value)
                result = value(agent=self) if "agent" in sig.parameters else value()

                if iscoroutine(result):
                    result = await result

                self.context[key] = result
            except Exception as e:
                log_warning(f"Failed to resolve context for '{key}': {e}")

    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_session_state is not None and len(self.team_session_state) > 0:
            session_data["team_session_state"] = self.team_session_state
        if self.session_metrics is not None:
            session_data["session_metrics"] = asdict(self.session_metrics) if self.session_metrics is not None else None
        if self.team_data is not None:
            session_data["team_data"] = self.team_data
        if self.images is not None:
            session_data["images"] = [img.to_dict() for img in self.images]  # type: ignore
        if self.videos is not None:
            session_data["videos"] = [vid.to_dict() for vid in self.videos]  # type: ignore
        if self.audio is not None:
            session_data["audio"] = [aud.to_dict() for aud in self.audio]  # type: ignore
        return session_data

    def get_agent_session(self, session_id: str, user_id: Optional[str] = None) -> AgentSession:
        from time import time

        """Get an AgentSession object, which can be saved to the database"""
        if self.memory is not None:
            if isinstance(self.memory, AgentMemory):
                self.memory = cast(AgentMemory, self.memory)
                memory_dict = self.memory.to_dict()
                # We only persist the runs for the current session ID (not all runs in memory)
                memory_dict["runs"] = [
                    agent_run.to_dict()
                    for agent_run in self.memory.runs
                    if agent_run.response is not None and agent_run.response.session_id == session_id
                ]
            else:
                self.memory = cast(Memory, self.memory)
                # We fake the structure on storage, to maintain the interface with the legacy implementation
                run_responses = self.memory.runs.get(session_id, [])  # type: ignore
                memory_dict = self.memory.to_dict()
                memory_dict["runs"] = [rr.to_dict() for rr in run_responses]
        else:
            memory_dict = None

        self.team_session_id = cast(str, self.team_session_id)
        self.agent_id = cast(str, self.agent_id)
        return AgentSession(
            session_id=session_id,
            agent_id=self.agent_id,
            user_id=user_id,
            team_session_id=self.team_session_id,
            memory=memory_dict,
            agent_data=self.get_agent_data(),
            session_data=self.get_session_data(),
            extra_data=self.extra_data,
            created_at=int(time()),
        )

    def load_agent_session(self, session: AgentSession):
        """Load the existing Agent from an AgentSession (from the database)"""

        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")

        # 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 the 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
                        # If there are conflicting keys, values from session_state_from_db will take precedence
                        merge_dictionaries(self.session_state, session_state_from_db)
                    else:
                        # Update the current session_state
                        self.session_state = session_state_from_db

            # Get the team_session_state from the database and update the current team_session_state
            if "team_session_state" in session.session_data:
                team_session_state_from_db = session.session_data.get("team_session_state")
                if (
                    team_session_state_from_db is not None
                    and isinstance(team_session_state_from_db, dict)
                    and len(team_session_state_from_db) > 0
                ):
                    # If the team_session_state is already set, merge the team_session_state from the database with the current team_session_state
                    if self.team_session_state is not None and len(self.team_session_state) > 0:
                        # This updates team_session_state_from_db
                        # If there are conflicting keys, values from team_session_state_from_db will take precedence
                        merge_dictionaries(self.team_session_state, team_session_state_from_db)
                    else:
                        # Update the current team_session_state
                        self.team_session_state = team_session_state_from_db

            # Get the session_metrics from the database
            if "session_metrics" in session.session_data:
                session_metrics_from_db = session.session_data.get("session_metrics")
                if session_metrics_from_db is not None and isinstance(session_metrics_from_db, dict):
                    self.session_metrics = SessionMetrics(**session_metrics_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 we haven't instantiated the memory yet, set it to the memory from the database
        if self.memory is None:
            self.memory = session.memory  # type: ignore

        if not (isinstance(self.memory, AgentMemory) or isinstance(self.memory, Memory)):
            # Is it a dict of `AgentMemory`?
            if isinstance(self.memory, dict) and "create_user_memories" in self.memory:
                # Convert dict to AgentMemory
                self.memory = AgentMemory(**self.memory)
                # Convert dict to Memory
            elif isinstance(self.memory, dict):
                memory_dict = self.memory
                memory_dict.pop("runs")
                self.memory = Memory(**memory_dict)
            else:
                raise TypeError(f"Expected memory to be a dict or AgentMemory, but got {type(self.memory)}")

        if session.memory is not None:
            if isinstance(self.memory, AgentMemory):
                try:
                    if "runs" in session.memory:
                        try:
                            self.memory.runs = []
                            for run in session.memory["runs"]:
                                self.memory.runs.append(AgentRun.model_validate(run))
                        except Exception as e:
                            log_warning(f"Failed to load runs from memory: {e}")
                    if "messages" in session.memory:
                        try:
                            self.memory.messages = [Message.model_validate(m) for m in session.memory["messages"]]
                        except Exception as e:
                            log_warning(f"Failed to load messages from memory: {e}")
                    if "summary" in session.memory:
                        from agno.memory.summary import SessionSummary

                        try:
                            self.memory.summary = SessionSummary.model_validate(session.memory["summary"])
                        except Exception as e:
                            log_warning(f"Failed to load session summary from memory: {e}")
                    if "memories" in session.memory:
                        from agno.memory.memory import Memory as UserMemory

                        try:
                            self.memory.memories = [UserMemory.model_validate(m) for m in session.memory["memories"]]
                        except Exception as e:
                            log_warning(f"Failed to load user memories: {e}")
                    if self.memory.create_user_memories:
                        if self.user_id is not None and self.memory.user_id is None:
                            self.memory.user_id = self.user_id

                        self.memory.load_user_memories()
                        if self.user_id is not None:
                            log_debug(f"Memories loaded for user: {self.user_id}")
                        else:
                            log_debug("Memories loaded")
                except Exception as e:
                    log_warning(f"Failed to load AgentMemory: {e}")
            elif isinstance(self.memory, Memory):
                if "runs" in session.memory:
                    try:
                        if self.memory.runs is None:
                            self.memory.runs = {}
                        self.memory.runs[session.session_id] = []
                        for run in session.memory["runs"]:
                            run_session_id = run["session_id"]
                            if "team_id" in run:
                                self.memory.runs[run_session_id].append(TeamRunResponse.from_dict(run))
                            else:
                                self.memory.runs[run_session_id].append(RunResponse.from_dict(run))
                    except Exception as e:
                        log_warning(f"Failed to load runs from memory: {e}")
                if "memories" in session.memory:
                    from agno.memory.v2.memory import UserMemory as UserMemoryV2

                    try:
                        # If memories are already loaded, use them as is for the current session
                        if self.memory.memories is not None:
                            pass
                        # If memories do not exist, we load them from the session memory for the current user
                        else:
                            self.memory.memories = {
                                user_id: {
                                    memory_id: UserMemoryV2.from_dict(memory)
                                    for memory_id, memory in user_memories.items()
                                }
                                for user_id, user_memories in session.memory["memories"].items()
                            }
                    except Exception as e:
                        log_warning(f"Failed to load user memories: {e}")
                if "summaries" in session.memory:
                    from agno.memory.v2.memory import SessionSummary as SessionSummaryV2

                    try:
                        self.memory.summaries = {
                            user_id: {
                                session_id: SessionSummaryV2.from_dict(summary)
                                for session_id, summary in user_session_summaries.items()
                            }
                            for user_id, user_session_summaries in session.memory["summaries"].items()
                        }
                    except Exception as e:
                        log_warning(f"Failed to load session summaries: {e}")
        log_debug(f"-*- AgentSession loaded: {session.session_id}")

    def read_from_storage(
        self,
        session_id: str,
        user_id: Optional[str] = None,
    ) -> Optional[AgentSession]:
        """Load the AgentSession from storage

        Returns:
            Optional[AgentSession]: The loaded AgentSession or None if not found.
        """
        if self.storage is not None:
            # Get a single session from storage
            self.agent_session = cast(AgentSession, self.storage.read(session_id=session_id))
            if self.agent_session is not None:
                # Load the agent session
                self.load_agent_session(session=self.agent_session)
            else:
                # New session, just reset the state
                self.session_name = None
        return self.agent_session

    def write_to_storage(self, session_id: str, user_id: Optional[str] = None) -> 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 = cast(
                AgentSession,
                self.storage.upsert(session=self.get_agent_session(session_id=session_id, user_id=user_id)),
            )
        return self.agent_session

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

        if isinstance(self.memory, AgentMemory):
            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=self.model.assistant_message_role, content=introduction)  # type: ignore
                                ],
                            )
                        )
                    )

    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
            log_debug(f"Reading AgentSession: {self.session_id}")
            self.read_from_storage(session_id=self.session_id, user_id=self.user_id)  # type: ignore

            # Create a new session if it does not exist
            if self.agent_session is None:
                log_debug("-*- Creating new AgentSession")
                # Initialize the agent_id and session_id if they are not set
                if self.agent_id is None:
                    self.initialize_agent()
                if self.session_id is None or self.session_id == "":
                    self.session_id = str(uuid4())
                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(user_id=self.user_id, session_id=self.session_id)  # type: ignore
                if self.agent_session is None:
                    raise Exception("Failed to create new AgentSession in storage")
                log_debug(f"-*- Created AgentSession: {self.agent_session.session_id}")
                self._log_agent_session(user_id=self.user_id, session_id=self.session_id)  # type: ignore
        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:
            if isinstance(self.memory, AgentMemory):
                self.memory.clear()
            elif isinstance(self.memory, Memory):
                self.memory.clear()
        self.session_id = str(uuid4())
        self.load_session(force=True)

    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(self, session_id: str, user_id: Optional[str] = None) -> 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.
        """

        # 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 the model does not support native structured outputs or JSON schema outputs
            # or if use_json_mode is True
            if (
                self.model is not None
                and self.response_model is not None
                and not (
                    (self.model.supports_native_structured_outputs or self.model.supports_json_schema_outputs)
                    and (not self.use_json_mode or self.structured_outputs is True)
                )
            ):
                sys_message_content += f"\n{get_json_output_prompt(self.response_model)}"  # type: ignore

            # type: ignore
            return Message(role=self.system_message_role, content=sys_message_content)

        # 2. If create_default_system_message is False, return None.
        if not self.create_default_system_message:
            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(self._tools_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

            tz = None

            if self.timezone_identifier:
                try:
                    from zoneinfo import ZoneInfo

                    tz = ZoneInfo(self.timezone_identifier)
                except Exception:
                    log_warning("Invalid timezone identifier")

            time = datetime.now(tz) if tz else datetime.now()

            additional_information.append(f"The current time is {time}.")
        # 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.2.4 Add information about agentic filters if enabled
        if self.knowledge is not None and self.enable_agentic_knowledge_filters:
            valid_filters = getattr(self.knowledge, "valid_metadata_filters", None)
            if valid_filters:
                valid_filters_str = ", ".join(valid_filters)
                additional_information.append(
                    dedent(f"""
                    The knowledge base contains documents with these metadata filters: {valid_filters_str}.
                    Always use filters when the user query indicates specific metadata.

                    Examples:
                    1. If the user asks about a specific person like "Jordan Mitchell", you MUST use the search_knowledge_base tool with the filters parameter set to {{'<valid key like user_id>': '<valid value based on the user query>'}}.
                    2. If the user asks about a specific document type like "contracts", you MUST use the search_knowledge_base tool with the filters parameter set to {{'document_type': 'contract'}}.
                    4. If the user asks about a specific location like "documents from New York", you MUST use the search_knowledge_base tool with the filters parameter set to {{'<valid key like location>': 'New York'}}.

                    General Guidelines:
                    - Always analyze the user query to identify relevant metadata.
                    - Use the most specific filter(s) possible to narrow down results.
                    - If multiple filters are relevant, combine them in the filters parameter (e.g., {{'name': 'Jordan Mitchell', 'document_type': 'contract'}}).
                    - Ensure the filter keys match the valid metadata filters: {valid_filters_str}.

                    You can use the search_knowledge_base tool to search the knowledge base and get the most relevant documents. Make sure to pass the filters as [Dict[str: Any]] to the tool. FOLLOW THIS STRUCTURE STRICTLY.
                """)
                )

        # 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"
        # 3.3.2 Then add the Agent goal if provided
        if self.goal is not None:
            system_message_content += f"\n<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"\n<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"
        # 3.3.7 Then add instructions for the tools
        if self._tool_instructions is not None:
            for _ti in self._tool_instructions:
                system_message_content += f"{_ti}\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 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.8 Then add additional context
        if self.additional_context is not None:
            system_message_content += f"{self.additional_context}\n"
        # 3.3.9 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.10 Then add memories to the system prompt
        if self.memory:
            if isinstance(self.memory, AgentMemory) and 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"
                    )
                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"
                )
            elif isinstance(self.memory, Memory) and self.add_memory_references:
                if not user_id:
                    user_id = "default"
                user_memories = self.memory.get_user_memories(user_id=user_id)  # type: ignore
                if user_memories and len(user_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 user_memories:  # type: ignore
                        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"
                    )
                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 self.enable_agentic_memory:
                    system_message_content += (
                        "\n<updating_user_memories>\n"
                        "- You have access to the `update_user_memory` tool that you can use to add new memories, update existing memories, delete memories, or clear all memories.\n"
                        "- If the user's message includes information that should be captured as a memory, use the `update_user_memory` tool to update your memory database.\n"
                        "- Memories should include details that could personalize ongoing interactions with the user.\n"
                        "- Use this tool to add new memories or update existing memories that you identify in the conversation.\n"
                        "- Use this tool if the user asks to update their memory, delete a memory, or clear all memories.\n"
                        "- If you use the `update_user_memory` tool, remember to pass on the response to the user.\n"
                        "</updating_user_memories>\n\n"
                    )

            # 3.3.11 Then add a summary of the interaction to the system prompt
            if isinstance(self.memory, AgentMemory) and self.memory.create_session_summary:
                if self.memory.summary is not None:
                    system_message_content += "Here is a brief summary of your previous interactions:\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"
                    )
            elif isinstance(self.memory, Memory) and self.add_session_summary_references:
                if not user_id:
                    user_id = "default"
                session_summary: SessionSummary = self.memory.summaries.get(user_id, {}).get(session_id, None)  # type: ignore
                if session_summary is not None:
                    system_message_content += "Here is a brief summary of your previous interactions:\n\n"
                    system_message_content += "<summary_of_previous_interactions>\n"
                    system_message_content += session_summary.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"
                    )

        # 3.3.12 Add the system message from the Model
        system_message_from_model = self.model.get_system_message_for_model(self._tools_for_model)
        if system_message_from_model is not None:
            system_message_content += system_message_from_model

        # 3.3.13 Add the JSON output prompt if response_model is provided and the model does not support native structured outputs or JSON schema outputs
        # or if use_json_mode is True
        if self.response_model is not None and not (
            (self.model.supports_native_structured_outputs or self.model.supports_json_schema_outputs)
            and (not self.use_json_mode or self.structured_outputs is True)
        ):
            system_message_content += f"{get_json_output_prompt(self.response_model)}"  # type: ignore

        # Return the system message
        return (
            Message(role=self.system_message_role, content=system_message_content.strip())  # type: ignore
            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,
        files: Optional[Sequence[File]] = 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()
            log_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,
                files=files,
                **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,
                files=files,
                **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,
            files=files,
            **kwargs,
        )

    def get_run_messages(
        self,
        *,
        message: Optional[Union[str, List, Dict, Message]] = None,
        session_id: str,
        user_id: Optional[str] = None,
        audio: Optional[Sequence[Audio]] = None,
        images: Optional[Sequence[Image]] = None,
        videos: Optional[Sequence[Video]] = None,
        files: Optional[Sequence[File]] = 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, session_id=session_id, user_id=user_id, audio=audio, images=images, videos=videos, files=files, messages=messages, **kwargs
        )
        """

        # Initialize the RunMessages object
        run_messages = RunMessages()
        self.run_response = cast(RunResponse, self.run_response)

        # 1. Add system message to run_messages
        system_message = self.get_system_message(session_id=session_id, user_id=user_id)
        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:
                        log_warning(f"Failed to validate message: {e}")
            # Add the extra messages to the run_response
            if len(messages_to_add_to_run_response) > 0:
                log_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:
            from copy import deepcopy

            history: List[Message] = []
            if isinstance(self.memory, AgentMemory):
                history = self.memory.get_messages_from_last_n_runs(
                    last_n=self.num_history_runs, skip_role=self.system_message_role
                )
            elif isinstance(self.memory, Memory):
                history = self.memory.get_messages_from_last_n_runs(
                    session_id=session_id, last_n=self.num_history_runs, skip_role=self.system_message_role
                )
            if len(history) > 0:
                # Create a deep copy of the history messages to avoid modifying the original messages
                history_copy = [deepcopy(msg) for msg in history]

                # Tag each message as coming from history
                for _msg in history_copy:
                    _msg.from_history = True

                log_debug(f"Adding {len(history_copy)} messages from history")

                run_messages.messages += history_copy

        # 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, files=files, **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:
                log_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:
                        log_warning(f"Failed to validate message: {e}")

        return run_messages

    def get_session_summary(self, session_id: Optional[str] = None, user_id: Optional[str] = None):
        """Get the session summary for the given session ID and user ID."""
        if self.memory is None:
            return None

        session_id = session_id if session_id is not None else self.session_id
        if session_id is None:
            raise ValueError("Session ID is required")

        if isinstance(self.memory, Memory):
            user_id = user_id if user_id is not None else self.user_id
            if user_id is None:
                user_id = "default"
            return self.memory.get_session_summary(session_id=session_id, user_id=user_id)
        elif isinstance(self.memory, AgentMemory):
            return self.memory.summary
        else:
            raise ValueError(f"Memory type {type(self.memory)} not supported")

    def get_user_memories(self, user_id: Optional[str] = None):
        """Get the user memories for the given user ID."""
        if self.memory is None:
            return None
        user_id = user_id if user_id is not None else self.user_id
        if user_id is None:
            user_id = "default"

        if isinstance(self.memory, Memory):
            return self.memory.get_user_memories(user_id=user_id)
        elif isinstance(self.memory, AgentMemory):
            raise ValueError("AgentMemory does not support get_user_memories")
        else:
            raise ValueError(f"Memory type {type(self.memory)} not supported")

    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"]
        # 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)
        log_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:
                try:
                    return copy(field_value)
                except Exception as e:
                    log_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:
                try:
                    return copy(field_value)
                except Exception as e:
                    log_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:
                try:
                    return field_value.model_copy(deep=False)
                except Exception as e:
                    log_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, session_id: Optional[str] = None) -> 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"] = 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:
                log_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 if member_agent.name else f"agent_{index}"
        # Convert non-ascii characters to ascii equivalents and ensure only alphanumeric, underscore and hyphen
        agent_name = "".join(c for c in agent_name if c.isalnum() or c in "_- ").strip()
        agent_name = agent_name.lower().replace(" ", "_")

        if member_agent.name is None:
            member_agent.name = agent_name

        strict = True if (member_agent.response_model is not None and member_agent.model is not None) else False
        transfer_function = Function.from_callable(_transfer_task_to_agent, strict=strict)
        transfer_function.strict = strict
        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, filters: Optional[Dict[str, Any]] = None, **kwargs
    ) -> Optional[List[Dict[str, Any]]]:
        """Get relevant docs from the knowledge base to answer a query.

        Args:
            query (str): The query to search for.
            num_documents (Optional[int]): Number of documents to return.
            filters (Optional[Dict[str, Any]]): Filters to apply to the search.
            **kwargs: Additional keyword arguments.

        Returns:
            Optional[List[Dict[str, Any]]]: List of relevant document dicts.
        """
        from agno.document import Document

        # Validate the filters against known valid filter keys
        if self.knowledge is not None:
            valid_filters, invalid_keys = self.knowledge.validate_filters(filters)  # type: ignore

            # Warn about invalid filter keys
            if invalid_keys:
                # type: ignore
                log_warning(f"Invalid filter keys provided: {invalid_keys}. These filters will be ignored.")
                log_info(f"Valid filter keys are: {self.knowledge.valid_metadata_filters}")  # type: ignore

                # Only use valid filters
                filters = valid_filters
                if not filters:
                    log_warning("No valid filters remain after validation. Search will proceed without filters.")

        if self.retriever is not None and callable(self.retriever):
            from inspect import signature

            try:
                sig = signature(self.retriever)
                retriever_kwargs: Dict[str, Any] = {}
                if "agent" in sig.parameters:
                    retriever_kwargs = {"agent": self}
                if "filters" in sig.parameters:
                    retriever_kwargs["filters"] = filters
                retriever_kwargs.update({"query": query, "num_documents": num_documents, **kwargs})
                return self.retriever(**retriever_kwargs)
            except Exception as e:
                log_warning(f"Retriever failed: {e}")
                return None

        # Use knowledge base search
        try:
            if self.knowledge is None or self.knowledge.vector_db is None:
                return None

            if num_documents is None:
                num_documents = self.knowledge.num_documents

            log_debug(f"Searching knowledge base with filters: {filters}")
            relevant_docs: List[Document] = self.knowledge.search(
                query=query, num_documents=num_documents, filters=filters
            )

            if not relevant_docs or len(relevant_docs) == 0:
                log_debug("No relevant documents found for query")
                return None

            return [doc.to_dict() for doc in relevant_docs]
        except Exception as e:
            log_warning(f"Error searching knowledge base: {e}")
            return None

    async def aget_relevant_docs_from_knowledge(
        self, query: str, num_documents: Optional[int] = None, filters: Optional[Dict[str, Any]] = None, **kwargs
    ) -> Optional[List[Dict[str, Any]]]:
        """Get relevant documents from knowledge base asynchronously."""
        from agno.document import Document

        # Validate the filters against known valid filter keys
        if self.knowledge is not None:
            valid_filters, invalid_keys = self.knowledge.validate_filters(filters)  # type: ignore

            # Warn about invalid filter keys
            if invalid_keys:  # type: ignore
                log_warning(f"Invalid filter keys provided: {invalid_keys}. These filters will be ignored.")
                log_info(f"Valid filter keys are: {self.knowledge.valid_metadata_filters}")  # type: ignore

                # Only use valid filters
                filters = valid_filters
                if not filters:
                    log_warning("No valid filters remain after validation. Search will proceed without filters.")

        if self.retriever is not None and callable(self.retriever):
            from inspect import isawaitable, signature

            try:
                sig = signature(self.retriever)
                retriever_kwargs: Dict[str, Any] = {}
                if "agent" in sig.parameters:
                    retriever_kwargs = {"agent": self}
                if "filters" in sig.parameters:
                    retriever_kwargs["filters"] = filters
                retriever_kwargs.update({"query": query, "num_documents": num_documents, **kwargs})
                result = self.retriever(**retriever_kwargs)

                if isawaitable(result):
                    result = await result

                return result
            except Exception as e:
                log_warning(f"Retriever failed: {e}")
                return None

        # Use knowledge base search
        try:
            if self.knowledge is None or self.knowledge.vector_db is None:
                return None

            if num_documents is None:
                num_documents = self.knowledge.num_documents

            log_debug(f"Searching knowledge base with filters: {filters}")
            relevant_docs: List[Document] = await self.knowledge.async_search(
                query=query, num_documents=num_documents, filters=filters, **kwargs
            )

            if not relevant_docs or len(relevant_docs) == 0:
                log_debug("No relevant documents found for query")
                return None

            return [doc.to_dict() for doc in relevant_docs]
        except Exception as e:
            log_warning(f"Error searching knowledge base: {e}")
            return None

    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, ensure_ascii=False)

    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 ""

        import json

        try:
            return json.dumps(context, indent=2, default=str)
        except (TypeError, ValueError, OverflowError) as e:
            log_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:
                log_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, session_id: Optional[str] = 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:
                    log_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=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:
                log_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)

        # Create and store reasoning_content
        reasoning_content = ""
        for step in reasoning_steps:
            if step.title:
                reasoning_content += f"## {step.title}\n"
            if step.reasoning:
                reasoning_content += f"{step.reasoning}\n"
            if step.action:
                reasoning_content += f"Action: {step.action}\n"
            if step.result:
                reasoning_content += f"Result: {step.result}\n"
            reasoning_content += "\n"

        # Add to existing reasoning_content or set it
        if not self.run_response.reasoning_content:
            self.run_response.reasoning_content = reasoning_content
        else:
            self.run_response.reasoning_content += reasoning_content

    def aggregate_metrics_from_messages(self, messages: List[Message]) -> Dict[str, Any]:
        aggregated_metrics: Dict[str, Any] = defaultdict(list)
        assistant_message_role = self.model.assistant_message_role if self.model is not None else "assistant"
        for m in messages:
            if m.role == assistant_message_role and m.metrics is not None and m.from_history is False:
                for k, v in asdict(m.metrics).items():
                    if k == "timer":
                        continue
                    if v is not None:
                        aggregated_metrics[k].append(v)
        if aggregated_metrics is not None:
            aggregated_metrics = dict(aggregated_metrics)
        return aggregated_metrics

    def calculate_metrics(self, messages: List[Message]) -> SessionMetrics:
        session_metrics = SessionMetrics()
        assistant_message_role = self.model.assistant_message_role if self.model is not None else "assistant"
        for m in messages:
            if m.role == assistant_message_role and m.metrics is not None:
                session_metrics += m.metrics
        return session_metrics

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

        if self.session_id is None and session_id is None:
            raise Exception("Session ID is not set")

        session_id = session_id or self.session_id

        # -*- Read from storage
        self.read_from_storage(session_id=session_id, user_id=self.user_id)  # type: ignore
        # -*- Rename Agent
        self.name = name
        # -*- Save to storage
        self.write_to_storage(user_id=self.user_id, session_id=session_id)  # type: ignore
        # -*- Log Agent session
        self._log_agent_session(user_id=self.user_id, session_id=session_id)  # type: ignore

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

        if self.session_id is None and session_id is None:
            raise Exception("Session ID is not set")

        session_id = session_id or self.session_id

        # -*- Read from storage
        self.read_from_storage(session_id=session_id, user_id=self.user_id)  # type: ignore
        # -*- Rename session
        self.session_name = session_name
        # -*- Save to storage
        self.write_to_storage(user_id=self.user_id, session_id=session_id)  # type: ignore
        # -*- Log Agent session
        self._log_agent_session(user_id=self.user_id, session_id=session_id)  # type: ignore

    def generate_session_name(self, session_id: str) -> 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 = []
        if isinstance(self.memory, AgentMemory):
            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:
                log_warning(f"Failed to generate name: {e}")
        elif isinstance(self.memory, Memory):
            messages_for_generating_session_name = self.memory.get_messages_for_session(session_id=session_id)

        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.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:
            log_error("Generated name is None. Trying again.")
            return self.generate_session_name(session_id=session_id)
        if len(content.split()) > 15:
            log_error("Generated name is too long. Trying again.")
            return self.generate_session_name(session_id=session_id)
        return content.replace('"', "").strip()

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

        if self.session_id is None:
            raise Exception("Session ID is not set")

        # -*- Read from storage
        self.read_from_storage(session_id=self.session_id, user_id=self.user_id)  # type: ignore
        # -*- Generate name for session
        generated_session_name = self.generate_session_name(session_id=self.session_id)
        log_debug(f"Generated Session Name: {generated_session_name}")
        # -*- Rename thread
        self.session_name = generated_session_name
        # -*- Save to storage
        self.write_to_storage(user_id=self.user_id, session_id=self.session_id)  # type: ignore
        # -*- Log Agent Session
        self._log_agent_session(user_id=self.user_id, session_id=self.session_id)  # type: ignore

    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)

    def get_messages_for_session(
        self, session_id: Optional[str] = None, user_id: Optional[str] = None
    ) -> List[Message]:
        """Get messages for a session"""
        _session_id = session_id or self.session_id
        _user_id = user_id or self.user_id
        if _session_id is None:
            log_warning("Session ID is not set, cannot get messages for session")
            return []

        if self.memory is None:
            self.read_from_storage(session_id=_session_id, user_id=_user_id)

        if self.memory is None:
            return []

        if isinstance(self.memory, AgentMemory):
            return self.memory.messages
        elif isinstance(self.memory, Memory):
            return self.memory.get_messages_from_last_n_runs(session_id=_session_id)
        else:
            return []

    ###########################################################################
    # 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 _format_reasoning_step_content(self, reasoning_step: ReasoningStep) -> str:
        """Format content for a reasoning step without changing any existing logic."""
        step_content = ""
        if reasoning_step.title:
            step_content += f"## {reasoning_step.title}\n"
        if reasoning_step.reasoning:
            step_content += f"{reasoning_step.reasoning}\n"
        if reasoning_step.action:
            step_content += f"Action: {reasoning_step.action}\n"
        if reasoning_step.result:
            step_content += f"Result: {reasoning_step.result}\n"
        step_content += "\n"

        # Get the current reasoning_content and append this step
        current_reasoning_content = ""
        if hasattr(self.run_response, "reasoning_content") and self.run_response.reasoning_content:  # type: ignore
            current_reasoning_content = self.run_response.reasoning_content  # type: ignore

        # Create updated reasoning_content
        updated_reasoning_content = current_reasoning_content + step_content

        return updated_reasoning_content

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

        use_default_reasoning = False

        # Get the reasoning model
        reasoning_model: Optional[Model] = self.reasoning_model
        reasoning_model_provided = reasoning_model is not None
        if reasoning_model is None and self.model is not None:
            from copy import deepcopy

            reasoning_model = deepcopy(self.model)
        if reasoning_model is None:
            log_warning("Reasoning error. Reasoning model is None, continuing regular session...")
            return

        # If a reasoning model is provided, use it to generate reasoning
        if reasoning_model_provided:
            from agno.reasoning.azure_ai_foundry import is_ai_foundry_reasoning_model
            from agno.reasoning.deepseek import is_deepseek_reasoning_model
            from agno.reasoning.groq import is_groq_reasoning_model
            from agno.reasoning.helpers import get_reasoning_agent
            from agno.reasoning.ollama import is_ollama_reasoning_model
            from agno.reasoning.openai import is_openai_reasoning_model

            reasoning_agent = self.reasoning_agent or get_reasoning_agent(
                reasoning_model=reasoning_model, monitoring=self.monitoring
            )
            is_deepseek = is_deepseek_reasoning_model(reasoning_model)
            is_groq = is_groq_reasoning_model(reasoning_model)
            is_openai = is_openai_reasoning_model(reasoning_model)
            is_ollama = is_ollama_reasoning_model(reasoning_model)
            is_ai_foundry = is_ai_foundry_reasoning_model(reasoning_model)

            if is_deepseek or is_groq or is_openai or is_ollama or is_ai_foundry:
                reasoning_message: Optional[Message] = None
                if is_deepseek:
                    from agno.reasoning.deepseek import get_deepseek_reasoning

                    log_debug("Starting DeepSeek Reasoning", center=True, symbol="=")
                    reasoning_message = get_deepseek_reasoning(
                        reasoning_agent=reasoning_agent, messages=run_messages.get_input_messages()
                    )
                elif is_groq:
                    from agno.reasoning.groq import get_groq_reasoning

                    log_debug("Starting Groq Reasoning", center=True, symbol="=")
                    reasoning_message = get_groq_reasoning(
                        reasoning_agent=reasoning_agent, messages=run_messages.get_input_messages()
                    )
                elif is_openai:
                    from agno.reasoning.openai import get_openai_reasoning

                    log_debug("Starting OpenAI Reasoning", center=True, symbol="=")
                    reasoning_message = get_openai_reasoning(
                        reasoning_agent=reasoning_agent, messages=run_messages.get_input_messages()
                    )
                elif is_ollama:
                    from agno.reasoning.ollama import get_ollama_reasoning

                    log_debug("Starting Ollama Reasoning", center=True, symbol="=")
                    reasoning_message = get_ollama_reasoning(
                        reasoning_agent=reasoning_agent, messages=run_messages.get_input_messages()
                    )
                elif is_ai_foundry:
                    from agno.reasoning.azure_ai_foundry import get_ai_foundry_reasoning

                    log_debug("Starting Azure AI Foundry Reasoning", center=True, symbol="=")
                    reasoning_message = get_ai_foundry_reasoning(
                        reasoning_agent=reasoning_agent, messages=run_messages.get_input_messages()
                    )

                if reasoning_message is None:
                    log_warning("Reasoning error. Reasoning response is None, continuing regular session...")
                    return
                run_messages.messages.append(reasoning_message)
                # Add reasoning step to the Agent's run_response
                self.update_run_response_with_reasoning(
                    reasoning_steps=[ReasoningStep(result=reasoning_message.content)],
                    reasoning_agent_messages=[reasoning_message],
                )
                if self.stream_intermediate_steps:
                    yield self.create_run_response(
                        content=ReasoningSteps(reasoning_steps=[ReasoningStep(result=reasoning_message.content)]),
                        session_id=session_id,
                        event=RunEvent.reasoning_completed,
                    )
            else:
                log_warning(
                    f"Reasoning model: {reasoning_model.__class__.__name__} is not a native reasoning model, defaulting to manual Chain-of-Thought reasoning"
                )
                use_default_reasoning = True
        # If no reasoning model is provided, use default reasoning
        else:
            use_default_reasoning = True

        if use_default_reasoning:
            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  # type: ignore
            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,
                    use_json_mode=self.use_json_mode,
                    monitoring=self.monitoring,
                    telemetry=self.telemetry,
                    debug_mode=self.debug_mode,
                )

            # Validate reasoning agent
            if reasoning_agent is None:
                log_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):
                    log_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

            step_count = 1
            next_action = NextAction.CONTINUE
            reasoning_messages: List[Message] = []
            all_reasoning_steps: List[ReasoningStep] = []
            log_debug("Starting Reasoning", center=True, symbol="=")
            while next_action == NextAction.CONTINUE and step_count < self.reasoning_max_steps:
                log_debug(f"Step {step_count}", center=True, symbol="=")
                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:
                        log_warning("Reasoning error. Reasoning response is empty, continuing regular session...")
                        break

                    if (
                        reasoning_agent_response.content.reasoning_steps is None
                        or len(reasoning_agent_response.content.reasoning_steps) == 0
                    ):
                        log_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:
                            updated_reasoning_content = self._format_reasoning_step_content(reasoning_step)

                            yield self.create_run_response(
                                content=reasoning_step,
                                content_type=reasoning_step.__class__.__name__,
                                reasoning_content=updated_reasoning_content,
                                event=RunEvent.reasoning_step,
                                session_id=session_id,
                            )

                    # 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:
                    log_error(f"Reasoning error: {e}")
                    break

                step_count += 1

            log_debug(f"Total Reasoning steps: {len(all_reasoning_steps)}")
            log_debug("Reasoning finished", center=True, symbol="=")

            # 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,
                    session_id=session_id,
                )

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

        use_default_reasoning = False

        # Get the reasoning model
        reasoning_model: Optional[Model] = self.reasoning_model
        reasoning_model_provided = reasoning_model is not None
        if reasoning_model is None and self.model is not None:
            from copy import deepcopy

            reasoning_model = deepcopy(self.model)
        if reasoning_model is None:
            log_warning("Reasoning error. Reasoning model is None, continuing regular session...")
            return

        # If a reasoning model is provided, use it to generate reasoning
        if reasoning_model_provided:
            from agno.reasoning.azure_ai_foundry import is_ai_foundry_reasoning_model
            from agno.reasoning.deepseek import is_deepseek_reasoning_model
            from agno.reasoning.groq import is_groq_reasoning_model
            from agno.reasoning.helpers import get_reasoning_agent
            from agno.reasoning.ollama import is_ollama_reasoning_model
            from agno.reasoning.openai import is_openai_reasoning_model

            reasoning_agent = self.reasoning_agent or get_reasoning_agent(
                reasoning_model=reasoning_model, monitoring=self.monitoring
            )
            is_deepseek = is_deepseek_reasoning_model(reasoning_model)
            is_groq = is_groq_reasoning_model(reasoning_model)
            is_openai = is_openai_reasoning_model(reasoning_model)
            is_ollama = is_ollama_reasoning_model(reasoning_model)
            is_ai_foundry = is_ai_foundry_reasoning_model(reasoning_model)

            if is_deepseek or is_groq or is_openai or is_ollama or is_ai_foundry:
                reasoning_message: Optional[Message] = None
                if is_deepseek:
                    from agno.reasoning.deepseek import aget_deepseek_reasoning

                    log_debug("Starting DeepSeek Reasoning", center=True, symbol="=")
                    reasoning_message = await aget_deepseek_reasoning(
                        reasoning_agent=reasoning_agent, messages=run_messages.get_input_messages()
                    )
                elif is_groq:
                    from agno.reasoning.groq import aget_groq_reasoning

                    log_debug("Starting Groq Reasoning", center=True, symbol="=")
                    reasoning_message = await aget_groq_reasoning(
                        reasoning_agent=reasoning_agent, messages=run_messages.get_input_messages()
                    )
                elif is_openai:
                    from agno.reasoning.openai import aget_openai_reasoning

                    log_debug("Starting OpenAI Reasoning", center=True, symbol="=")
                    reasoning_message = await aget_openai_reasoning(
                        reasoning_agent=reasoning_agent, messages=run_messages.get_input_messages()
                    )
                elif is_ollama:
                    from agno.reasoning.ollama import get_ollama_reasoning

                    log_debug("Starting Ollama Reasoning", center=True, symbol="=")
                    reasoning_message = get_ollama_reasoning(
                        reasoning_agent=reasoning_agent, messages=run_messages.get_input_messages()
                    )
                elif is_ai_foundry:
                    from agno.reasoning.azure_ai_foundry import get_ai_foundry_reasoning

                    log_debug("Starting Azure AI Foundry Reasoning", center=True, symbol="=")
                    reasoning_message = get_ai_foundry_reasoning(
                        reasoning_agent=reasoning_agent, messages=run_messages.get_input_messages()
                    )

                if reasoning_message is None:
                    log_warning("Reasoning error. Reasoning response is None, continuing regular session...")
                    return
                run_messages.messages.append(reasoning_message)
                # Add reasoning step to the Agent's run_response
                self.update_run_response_with_reasoning(
                    reasoning_steps=[ReasoningStep(result=reasoning_message.content)],
                    reasoning_agent_messages=[reasoning_message],
                )
                if self.stream_intermediate_steps:
                    yield self.create_run_response(
                        content=ReasoningSteps(reasoning_steps=[ReasoningStep(result=reasoning_message.content)]),
                        session_id=session_id,
                        event=RunEvent.reasoning_completed,
                    )
            else:
                log_warning(
                    f"Reasoning model: {reasoning_model.__class__.__name__} is not a native reasoning model, defaulting to manual Chain-of-Thought reasoning"
                )
                use_default_reasoning = True
        # If no reasoning model is provided, use default reasoning
        else:
            use_default_reasoning = True

        if use_default_reasoning:
            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  # type: ignore
            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,
                    use_json_mode=self.use_json_mode,
                    monitoring=self.monitoring,
                    telemetry=self.telemetry,
                    debug_mode=self.debug_mode,
                )

            # Validate reasoning agent
            if reasoning_agent is None:
                log_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):
                    log_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

            step_count = 1
            next_action = NextAction.CONTINUE
            reasoning_messages: List[Message] = []
            all_reasoning_steps: List[ReasoningStep] = []
            log_debug("Starting Reasoning", center=True, symbol="=")
            while next_action == NextAction.CONTINUE and step_count < self.reasoning_max_steps:
                log_debug(f"Step {step_count}", center=True, symbol="=")
                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:
                        log_warning("Reasoning error. Reasoning response is empty, continuing regular session...")
                        break

                    if reasoning_agent_response.content.reasoning_steps is None:
                        log_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:
                            updated_reasoning_content = self._format_reasoning_step_content(reasoning_step)

                            # Yield the response with the updated reasoning_content
                            yield self.create_run_response(
                                content=reasoning_step,
                                content_type=reasoning_step.__class__.__name__,
                                reasoning_content=updated_reasoning_content,
                                event=RunEvent.reasoning_step,
                                session_id=session_id,
                            )

                    # 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:
                    log_error(f"Reasoning error: {e}")
                    break

            log_debug(f"Total Reasoning steps: {len(all_reasoning_steps)}")
            log_debug("Reasoning finished", center=True, symbol="=")

            # 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,
                    session_id=session_id,
                )

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

    def get_update_user_memory_function(self, user_id: Optional[str] = None, async_mode: bool = False) -> Callable:
        def update_user_memory(task: str) -> str:
            """Use this function to submit a task to modify the Agent's memory.
            Describe the task in detail and be specific.
            The task can include adding a memory, updating a memory, deleting a memory, or clearing all memories.

            Args:
                task: The task to update the memory. Be specific and describe the task in detail.

            Returns:
                str: A string indicating the status of the task.
            """
            self.memory = cast(Memory, self.memory)
            response = self.memory.update_memory_task(task=task, user_id=user_id)

            return response

        async def aupdate_user_memory(task: str) -> str:
            """Use this function to update the Agent's memory of a user.
            Describe the task in detail and be specific.
            The task can include adding a memory, updating a memory, deleting a memory, or clearing all memories.

            Args:
                task: The task to update the memory. Be specific and describe the task in detail.

            Returns:
                str: A string indicating the status of the task.
            """
            self.memory = cast(Memory, self.memory)
            response = await self.memory.aupdate_memory_task(task=task, user_id=user_id)
            return response

        if async_mode:
            return aupdate_user_memory
        else:
            return update_user_memory

    def get_chat_history_function(self, session_id: str) -> Callable:
        def get_chat_history(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]] = []
            if isinstance(self.memory, AgentMemory):
                agent_chats = self.memory.get_message_pairs()

                if len(agent_chats) == 0:
                    return ""

                chats_added = 0
                for chat in agent_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

            elif isinstance(self.memory, Memory):
                all_chats = self.memory.get_messages_for_session(session_id=session_id)

                if len(all_chats) == 0:
                    return ""

                for chat in all_chats[::-1]:  # type: ignore
                    history.insert(0, chat.to_dict())  # type: ignore

                if num_chats is not None:
                    history = history[:num_chats]

            else:
                return ""

            return json.dumps(history)

        return get_chat_history

    def get_tool_call_history_function(self, session_id: str) -> Callable:
        def get_tool_call_history(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

            if isinstance(self.memory, AgentMemory):
                tool_calls = self.memory.get_tool_calls(num_calls=num_calls)
            elif isinstance(self.memory, Memory):
                tool_calls = self.memory.get_tool_calls(session_id=session_id, num_calls=num_calls)
            else:
                return ""
            if len(tool_calls) == 0:
                return ""
            log_debug(f"tool_calls: {tool_calls}")
            return json.dumps(tool_calls)

        return get_tool_call_history

    def search_knowledge_base_function(
        self, knowledge_filters: Optional[Dict[str, Any]] = None, async_mode: bool = False
    ) -> Callable:
        """Factory function to create a search_knowledge_base function with filters."""

        def search_knowledge_base(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, passing filters
            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, filters=knowledge_filters)
            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()
            from agno.utils.log import log_debug

            log_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)

        async def asearch_knowledge_base(query: str) -> str:
            """Use this function to search the knowledge base for information about a query asynchronously.

            Args:
                query: The query to search for.

            Returns:
                str: A string containing the response from the knowledge base.
            """
            self.run_response = cast(RunResponse, self.run_response)
            retrieval_timer = Timer()
            retrieval_timer.start()
            docs_from_knowledge = await self.aget_relevant_docs_from_knowledge(query=query, filters=knowledge_filters)
            if docs_from_knowledge is not None:
                references = MessageReferences(
                    query=query, references=docs_from_knowledge, time=round(retrieval_timer.elapsed, 4)
                )
                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()
            log_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)

        if async_mode:
            return asearch_knowledge_base
        else:
            return search_knowledge_base

    def search_knowledge_base_with_agentic_filters_function(
        self, knowledge_filters: Optional[Dict[str, Any]] = None, async_mode: bool = False
    ) -> Callable:
        """Factory function to create a search_knowledge_base function with filters."""

        def search_knowledge_base(query: str, filters: Optional[Dict[str, Any]] = None) -> str:
            """Use this function to search the knowledge base for information about a query.

            Args:
                query: The query to search for.
                filters: The filters to apply to the search. This is a dictionary of key-value pairs.

            Returns:
                str: A string containing the response from the knowledge base.
            """
            search_filters = self._get_agentic_or_user_search_filters(filters, knowledge_filters)

            # Get the relevant documents from the knowledge base, passing filters
            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, filters=search_filters)
            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()
            from agno.utils.log import log_debug

            log_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)

        async def asearch_knowledge_base(query: str, filters: Optional[Dict[str, Any]] = None) -> str:
            """Use this function to search the knowledge base for information about a query asynchronously.

            Args:
                query: The query to search for.
                filters: The filters to apply to the search. This is a dictionary of key-value pairs.

            Returns:
                str: A string containing the response from the knowledge base.
            """
            search_filters = self._get_agentic_or_user_search_filters(filters, knowledge_filters)

            self.run_response = cast(RunResponse, self.run_response)
            retrieval_timer = Timer()
            retrieval_timer.start()
            docs_from_knowledge = await self.aget_relevant_docs_from_knowledge(query=query, filters=search_filters)
            if docs_from_knowledge is not None:
                references = MessageReferences(
                    query=query, references=docs_from_knowledge, time=round(retrieval_timer.elapsed, 4)
                )
                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()
            log_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)

        if async_mode:
            return asearch_knowledge_base
        else:
            return search_knowledge_base

    def _get_agentic_or_user_search_filters(
        self, filters: Optional[Dict[str, Any]], effective_filters: Optional[Dict[str, Any]]
    ) -> Dict[str, Any]:
        """Helper function to determine the final filters to use for the search.

        Args:
            filters: Filters passed by the agent.
            effective_filters: Filters passed by user.

        Returns:
            Dict[str, Any]: The final filters to use for the search.
        """
        search_filters = {}

        # If agentic filters exist and manual filters (passed by user) do not, use agentic filters
        if filters and not effective_filters:
            search_filters = filters

        # If both agentic filters exist and manual filters (passed by user) exist, use manual filters (give priority to user and override)
        if filters and effective_filters:
            search_filters = effective_filters

        log_info(f"Filters used by Agent: {search_filters}")
        return search_filters

    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})
        log_info(f"Adding document to knowledge base: {document_name}: {document_content}")
        self.knowledge.add_document_to_knowledge_base(
            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, session_id: str, user_id: Optional[str] = None):
        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(
                session_id=session_id, user_id=user_id
            )
            create_agent_session(
                session=AgentSessionCreate(
                    session_id=agent_session.session_id,
                    agent_data=agent_session.to_dict() if self.monitoring else agent_session.telemetry_data(),
                ),
                monitor=self.monitoring,
            )
        except Exception as e:
            log_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._functions_for_model is not None:
            functions = {
                f_name: func.to_dict()
                for f_name, func in self._functions_for_model.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, session_id: str, user_id: Optional[str] = None) -> None:
        self.set_monitoring()

        if not self.telemetry and not 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(
                session_id=session_id, user_id=user_id
            )

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

    async def _alog_agent_run(self, session_id: str, user_id: Optional[str] = None) -> None:
        self.set_monitoring()

        if not self.telemetry and not 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(
                session_id=session_id, user_id=user_id
            )

            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.to_dict() if self.monitoring else agent_session.telemetry_data(),
                    team_session_id=agent_session.team_session_id,
                ),
                monitor=self.monitoring,
            )
        except Exception as e:
            log_debug(f"Could not create agent event: {e}")

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

    def print_response(
        self,
        message: Optional[Union[List, Dict, str, Message]] = None,
        *,
        session_id: Optional[str] = None,
        user_id: Optional[str] = None,
        messages: Optional[List[Union[Dict, Message]]] = None,
        audio: Optional[Sequence[Audio]] = None,
        images: Optional[Sequence[Image]] = None,
        videos: Optional[Sequence[Video]] = None,
        files: Optional[Sequence[File]] = None,
        stream: Optional[bool] = None,
        stream_intermediate_steps: bool = False,
        markdown: bool = False,
        show_message: bool = True,
        show_reasoning: bool = True,
        show_full_reasoning: bool = False,
        console: Optional[Any] = None,
        # Add tags to include in markdown content
        tags_to_include_in_markdown: Set[str] = {"think", "thinking"},
        knowledge_filters: Optional[Dict[str, 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:
            self.markdown = False
            stream = False

        stream_intermediate_steps = stream_intermediate_steps or self.stream_intermediate_steps
        stream = stream or self.stream or False

        if stream:
            _response_content: str = ""
            _response_thinking: 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 = 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,
                    session_id=session_id,
                    user_id=user_id,
                    audio=audio,
                    images=images,
                    videos=videos,
                    files=files,
                    stream=True,
                    stream_intermediate_steps=stream_intermediate_steps,
                    knowledge_filters=knowledge_filters,
                    **kwargs,
                ):
                    if isinstance(resp, RunResponse):
                        if resp.event == RunEvent.run_response:
                            if isinstance(resp.content, str):
                                _response_content += resp.content
                            if resp.thinking is not None:
                                _response_thinking += resp.thinking
                        if resp.extra_data is not None and resp.extra_data.reasoning_steps is not None:
                            reasoning_steps = resp.extra_data.reasoning_steps

                    response_content_stream: Union[str, Markdown] = _response_content

                    # Escape special tags before markdown conversion
                    if self.markdown:
                        escaped_content = escape_markdown_tags(_response_content, tags_to_include_in_markdown)
                        response_content_stream = Markdown(escaped_content)
                    panels = [status]

                    if message and show_message:
                        render = True
                        # Convert message to a panel
                        message_content = get_text_from_message(message)
                        message_panel = 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(
                                    Text.from_markup(f"[bold]Action:[/bold] {step.action}\n", style="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 = 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_thinking) > 0:
                        render = True
                        # Create panel for thinking
                        thinking_panel = create_panel(
                            content=Text(_response_thinking),
                            title=f"Thinking ({response_timer.elapsed:.1f}s)",
                            border_style="green",
                        )
                        panels.append(thinking_panel)
                    if render:
                        live_log.update(Group(*panels))

                    # Add tool calls panel if available
                    if (
                        self.show_tool_calls
                        and self.run_response is not None
                        and self.run_response.formatted_tool_calls
                    ):
                        render = True
                        # Create bullet points for each tool call
                        tool_calls_content = Text()
                        for tool_call in self.run_response.formatted_tool_calls:
                            tool_calls_content.append(f"• {tool_call}\n")

                        tool_calls_panel = create_panel(
                            content=tool_calls_content.plain.rstrip(),
                            title="Tool Calls",
                            border_style="yellow",
                        )
                        panels.append(tool_calls_panel)

                    if len(_response_content) > 0:
                        render = True
                        # Create panel for response
                        response_panel = 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))

                    if isinstance(resp, RunResponse) and resp.citations is not None and resp.citations.urls is not None:
                        md_content = "\n".join(
                            f"{i + 1}. [{citation.title or citation.url}]({citation.url})"
                            for i, citation in enumerate(resp.citations.urls)
                            if citation.url  # Only include citations with valid URLs
                        )
                        if md_content:  # Only create panel if there are citations
                            citations_panel = create_panel(
                                content=Markdown(md_content),
                                title="Citations",
                                border_style="green",
                            )
                            panels.append(citations_panel)
                            live_log.update(Group(*panels))

                if self.memory is not None and isinstance(self.memory, Memory):
                    if self.memory.memory_manager is not None and self.memory.memory_manager.memories_updated:
                        memory_panel = create_panel(
                            content=Text("Memories updated"),
                            title="Memories",
                            border_style="green",
                        )
                        panels.append(memory_panel)
                        live_log.update(Group(*panels))
                        self.memory.memory_manager.memories_updated = False

                    if self.memory.summary_manager is not None and self.memory.summary_manager.summary_updated:
                        summary_panel = create_panel(
                            content=Text("Session summary updated"),
                            title="Session Summary",
                            border_style="green",
                        )
                        panels.append(summary_panel)
                        live_log.update(Group(*panels))
                        self.memory.summary_manager.summary_updated = False

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

                # Run the agent
                run_response = self.run(
                    message=message,
                    messages=messages,
                    session_id=session_id,
                    user_id=user_id,
                    audio=audio,
                    images=images,
                    videos=videos,
                    files=files,
                    stream=False,
                    stream_intermediate_steps=stream_intermediate_steps,
                    knowledge_filters=knowledge_filters,
                    **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:
                    # 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(Text.from_markup(f"[bold]Action:[/bold] {step.action}\n", style="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 = create_panel(
                            content=step_content, title=f"Reasoning step {i}", border_style="green"
                        )
                        panels.append(reasoning_panel)
                    live_log.update(Group(*panels))

                if isinstance(run_response, RunResponse) and run_response.thinking is not None:
                    # Create panel for thinking
                    thinking_panel = create_panel(
                        content=Text(run_response.thinking),
                        title=f"Thinking ({response_timer.elapsed:.1f}s)",
                        border_style="green",
                    )
                    panels.append(thinking_panel)
                    live_log.update(Group(*panels))

                # Add tool calls panel if available
                if self.show_tool_calls and isinstance(run_response, RunResponse) and run_response.formatted_tool_calls:
                    # Create bullet points for each tool call
                    tool_calls_content = Text()
                    for tool_call in run_response.formatted_tool_calls:
                        tool_calls_content.append(f"• {tool_call}\n")

                    tool_calls_panel = create_panel(
                        content=tool_calls_content.plain.rstrip(),
                        title="Tool Calls",
                        border_style="yellow",
                    )
                    panels.append(tool_calls_panel)
                    live_log.update(Group(*panels))

                response_content_batch: Union[str, JSON, Markdown] = ""
                if isinstance(run_response, RunResponse):
                    if isinstance(run_response.content, str):
                        if self.markdown:
                            escaped_content = escape_markdown_tags(run_response.content, tags_to_include_in_markdown)
                            response_content_batch = Markdown(escaped_content)
                        else:
                            response_content_batch = run_response.get_content_as_string(indent=4)
                    elif self.response_model is not None and isinstance(run_response.content, BaseModel):
                        try:
                            response_content_batch = JSON(
                                run_response.content.model_dump_json(exclude_none=True), indent=2
                            )
                        except Exception as e:
                            log_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:
                            log_warning(f"Failed to convert response to JSON: {e}")

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

                if (
                    isinstance(run_response, RunResponse)
                    and run_response.citations is not None
                    and run_response.citations.urls is not None
                ):
                    md_content = "\n".join(
                        f"{i + 1}. [{citation.title or citation.url}]({citation.url})"
                        for i, citation in enumerate(run_response.citations.urls)
                        if citation.url  # Only include citations with valid URLs
                    )
                    if md_content:  # Only create panel if there are citations
                        citations_panel = create_panel(
                            content=Markdown(md_content),
                            title="Citations",
                            border_style="green",
                        )
                        panels.append(citations_panel)
                        live_log.update(Group(*panels))

                if self.memory is not None and isinstance(self.memory, Memory):
                    if self.memory.memory_manager is not None and self.memory.memory_manager.memories_updated:
                        memory_panel = create_panel(
                            content=Text("Memories updated"),
                            title="Memories",
                            border_style="green",
                        )
                        panels.append(memory_panel)
                        live_log.update(Group(*panels))
                        self.memory.memory_manager.memories_updated = False

                    if self.memory.summary_manager is not None and self.memory.summary_manager.summary_updated:
                        summary_panel = create_panel(
                            content=Text("Session summary updated"),
                            title="Session Summary",
                            border_style="green",
                        )
                        panels.append(summary_panel)
                        live_log.update(Group(*panels))
                        self.memory.summary_manager.summary_updated = False

                # 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,
        session_id: Optional[str] = None,
        user_id: Optional[str] = None,
        audio: Optional[Sequence[Audio]] = None,
        images: Optional[Sequence[Image]] = None,
        videos: Optional[Sequence[Video]] = None,
        files: Optional[Sequence[File]] = None,
        stream: Optional[bool] = None,
        stream_intermediate_steps: bool = False,
        markdown: bool = False,
        show_message: bool = True,
        show_reasoning: bool = True,
        show_full_reasoning: bool = False,
        console: Optional[Any] = None,
        # Add tags to include in markdown content
        tags_to_include_in_markdown: Set[str] = {"think", "thinking"},
        knowledge_filters: Optional[Dict[str, 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:
            self.markdown = False
            stream = False

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

                result = await self.arun(
                    message=message,
                    messages=messages,
                    session_id=session_id,
                    user_id=user_id,
                    audio=audio,
                    images=images,
                    videos=videos,
                    files=files,
                    stream=True,
                    stream_intermediate_steps=stream_intermediate_steps,
                    knowledge_filters=knowledge_filters,
                    **kwargs,
                )

                async for resp in result:
                    if isinstance(resp, RunResponse):
                        if resp.event == RunEvent.run_response:
                            if isinstance(resp.content, str):
                                _response_content += resp.content
                            if resp.thinking is not None:
                                _response_thinking += resp.thinking
                        if resp.extra_data is not None and resp.extra_data.reasoning_steps is not None:
                            reasoning_steps = resp.extra_data.reasoning_steps

                    response_content_stream: Union[str, Markdown] = _response_content
                    # Escape special tags before markdown conversion
                    if self.markdown:
                        escaped_content = escape_markdown_tags(_response_content, tags_to_include_in_markdown)
                        response_content_stream = Markdown(escaped_content)

                    panels = [status]

                    if message and show_message:
                        render = True
                        # Convert message to a panel
                        message_content = get_text_from_message(message)
                        message_panel = 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(
                                    Text.from_markup(f"[bold]Action:[/bold] {step.action}\n", style="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 = 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_thinking) > 0:
                        render = True
                        # Create panel for thinking
                        thinking_panel = create_panel(
                            content=Text(_response_thinking),
                            title=f"Thinking ({response_timer.elapsed:.1f}s)",
                            border_style="green",
                        )
                        panels.append(thinking_panel)
                    if render:
                        live_log.update(Group(*panels))

                    # Add tool calls panel if available
                    if (
                        self.show_tool_calls
                        and self.run_response is not None
                        and self.run_response.formatted_tool_calls
                    ):
                        render = True
                        # Create bullet points for each tool call
                        tool_calls_content = Text()
                        for tool_call in self.run_response.formatted_tool_calls:
                            tool_calls_content.append(f"• {tool_call}\n")

                        tool_calls_panel = create_panel(
                            content=tool_calls_content.plain.rstrip(),
                            title="Tool Calls",
                            border_style="yellow",
                        )
                        panels.append(tool_calls_panel)

                    if len(_response_content) > 0:
                        render = True
                        # Create panel for response
                        response_panel = 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))

                    if isinstance(resp, RunResponse) and resp.citations is not None and resp.citations.urls is not None:
                        md_content = "\n".join(
                            f"{i + 1}. [{citation.title or citation.url}]({citation.url})"
                            for i, citation in enumerate(resp.citations.urls)
                            if citation.url  # Only include citations with valid URLs
                        )
                        if md_content:  # Only create panel if there are citations
                            citations_panel = create_panel(
                                content=Markdown(md_content),
                                title="Citations",
                                border_style="green",
                            )
                            panels.append(citations_panel)
                            live_log.update(Group(*panels))

                if self.memory is not None and isinstance(self.memory, Memory):
                    if self.memory.memory_manager is not None and self.memory.memory_manager.memories_updated:
                        memory_panel = create_panel(
                            content=Text("Memories updated"),
                            title="Memories",
                            border_style="green",
                        )
                        panels.append(memory_panel)
                        live_log.update(Group(*panels))
                        self.memory.memory_manager.memories_updated = False

                    if self.memory.summary_manager is not None and self.memory.summary_manager.summary_updated:
                        summary_panel = create_panel(
                            content=Text("Session summary updated"),
                            title="Session Summary",
                            border_style="green",
                        )
                        panels.append(summary_panel)
                        live_log.update(Group(*panels))
                        self.memory.summary_manager.summary_updated = False

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

                # Run the agent
                run_response = await self.arun(
                    message=message,
                    messages=messages,
                    session_id=session_id,
                    user_id=user_id,
                    audio=audio,
                    images=images,
                    videos=videos,
                    files=files,
                    stream=False,
                    stream_intermediate_steps=stream_intermediate_steps,
                    knowledge_filters=knowledge_filters,
                    **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:
                    # 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(Text.from_markup(f"[bold]Action:[/bold] {step.action}\n", style="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 = create_panel(
                            content=step_content, title=f"Reasoning step {i}", border_style="green"
                        )
                        panels.append(reasoning_panel)
                    live_log.update(Group(*panels))

                if isinstance(run_response, RunResponse) and run_response.thinking is not None:
                    # Create panel for thinking
                    thinking_panel = create_panel(
                        content=Text(run_response.thinking),
                        title=f"Thinking ({response_timer.elapsed:.1f}s)",
                        border_style="green",
                    )
                    panels.append(thinking_panel)
                    live_log.update(Group(*panels))

                if self.show_tool_calls and isinstance(run_response, RunResponse) and run_response.formatted_tool_calls:
                    tool_calls_content = Text()
                    for tool_call in run_response.formatted_tool_calls:
                        tool_calls_content.append(f"• {tool_call}\n")

                    tool_calls_panel = create_panel(
                        content=tool_calls_content.plain.rstrip(),
                        title="Tool Calls",
                        border_style="yellow",
                    )
                    panels.append(tool_calls_panel)
                    live_log.update(Group(*panels))

                response_content_batch: Union[str, JSON, Markdown] = ""
                if isinstance(run_response, RunResponse):
                    if isinstance(run_response.content, str):
                        if self.markdown:
                            escaped_content = escape_markdown_tags(run_response.content, tags_to_include_in_markdown)
                            response_content_batch = Markdown(escaped_content)
                        else:
                            response_content_batch = run_response.get_content_as_string(indent=4)
                    elif self.response_model is not None and isinstance(run_response.content, BaseModel):
                        try:
                            response_content_batch = JSON(
                                run_response.content.model_dump_json(exclude_none=True), indent=2
                            )
                        except Exception as e:
                            log_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:
                            log_warning(f"Failed to convert response to JSON: {e}")

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

                if (
                    isinstance(run_response, RunResponse)
                    and run_response.citations is not None
                    and run_response.citations.urls is not None
                ):
                    md_content = "\n".join(
                        f"{i + 1}. [{citation.title or citation.url}]({citation.url})"
                        for i, citation in enumerate(run_response.citations.urls)
                        if citation.url  # Only include citations with valid URLs
                    )
                    if md_content:  # Only create panel if there are citations
                        citations_panel = create_panel(
                            content=Markdown(md_content),
                            title="Citations",
                            border_style="green",
                        )
                        panels.append(citations_panel)
                        live_log.update(Group(*panels))

                if self.memory is not None and isinstance(self.memory, Memory):
                    if self.memory.memory_manager is not None and self.memory.memory_manager.memories_updated:
                        memory_panel = create_panel(
                            content=Text("Memories updated"),
                            title="Memories",
                            border_style="green",
                        )
                        panels.append(memory_panel)
                        live_log.update(Group(*panels))
                        self.memory.memory_manager.memories_updated = False

                    if self.memory.summary_manager is not None and self.memory.summary_manager.summary_updated:
                        summary_panel = create_panel(
                            content=Text("Session summary updated"),
                            title="Session Summary",
                            border_style="green",
                        )
                        panels.append(summary_panel)
                        live_log.update(Group(*panels))
                        self.memory.summary_manager.summary_updated = False

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

    def update_reasoning_content_from_tool_call(
        self, tool_name: str, tool_args: Dict[str, Any]
    ) -> Optional[ReasoningStep]:
        """Update reasoning_content based on tool calls that look like thinking or reasoning tools."""

        # Case 1: ReasoningTools.think (has title, thought, optional action and confidence)
        if tool_name.lower() == "think" and "title" in tool_args and "thought" in tool_args:
            title = tool_args["title"]
            thought = tool_args["thought"]
            action = tool_args.get("action", "")
            confidence = tool_args.get("confidence", None)

            # Create a reasoning step
            reasoning_step = ReasoningStep(
                title=title,
                reasoning=thought,
                action=action,
                next_action=NextAction.CONTINUE,
                confidence=confidence,
            )

            # Add the step to the run response
            self._add_reasoning_step_to_extra_data(reasoning_step)

            formatted_content = f"## {title}\n{thought}\n"
            if action:
                formatted_content += f"Action: {action}\n"
            if confidence is not None:
                formatted_content += f"Confidence: {confidence}\n"
            formatted_content += "\n"

            self._append_to_reasoning_content(formatted_content)
            return reasoning_step

        # Case 2: ReasoningTools.analyze (has title, result, analysis, optional next_action and confidence)
        elif tool_name.lower() == "analyze" and "title" in tool_args:
            title = tool_args["title"]
            result = tool_args.get("result", "")
            analysis = tool_args.get("analysis", "")
            next_action = tool_args.get("next_action", "")
            confidence = tool_args.get("confidence", None)

            # Map string next_action to enum
            next_action_enum = NextAction.CONTINUE
            if next_action.lower() == "validate":
                next_action_enum = NextAction.VALIDATE
            elif next_action.lower() in ["final", "final_answer", "finalize"]:
                next_action_enum = NextAction.FINAL_ANSWER

            # Create a reasoning step
            reasoning_step = ReasoningStep(
                title=title,
                result=result,
                reasoning=analysis,
                next_action=next_action_enum,
                confidence=confidence,
            )

            # Add the step to the run response
            self._add_reasoning_step_to_extra_data(reasoning_step)

            formatted_content = f"## {title}\n"
            if result:
                formatted_content += f"Result: {result}\n"
            if analysis:
                formatted_content += f"{analysis}\n"
            if next_action and next_action.lower() != "continue":
                formatted_content += f"Next Action: {next_action}\n"
            if confidence is not None:
                formatted_content += f"Confidence: {confidence}\n"
            formatted_content += "\n"

            self._append_to_reasoning_content(formatted_content)
            return reasoning_step

        # Case 3: ThinkingTools.think (simple format, just has 'thought')
        elif tool_name.lower() == "think" and "thought" in tool_args:
            thought = tool_args["thought"]
            reasoning_step = ReasoningStep(
                title="Thinking",
                reasoning=thought,
                confidence=None,
            )
            formatted_content = f"## Thinking\n{thought}\n\n"
            self._add_reasoning_step_to_extra_data(reasoning_step)
            self._append_to_reasoning_content(formatted_content)
            return reasoning_step

        return None

    def _append_to_reasoning_content(self, content: str) -> None:
        """Helper to append content to the reasoning_content field."""
        if not hasattr(self.run_response, "reasoning_content") or not self.run_response.reasoning_content:  # type: ignore
            self.run_response.reasoning_content = content  # type: ignore
        else:
            self.run_response.reasoning_content += content  # type: ignore

    def _add_reasoning_step_to_extra_data(self, reasoning_step: ReasoningStep) -> None:
        if hasattr(self, "run_response") and self.run_response is not None:
            if self.run_response.extra_data is None:
                from agno.run.response import RunResponseExtraData

                self.run_response.extra_data = RunResponseExtraData()

            if self.run_response.extra_data.reasoning_steps is None:
                self.run_response.extra_data.reasoning_steps = []

            self.run_response.extra_data.reasoning_steps.append(reasoning_step)

    def _add_reasoning_metrics_to_extra_data(self, reasoning_time_taken: float) -> None:
        try:
            if hasattr(self, "run_response") and self.run_response is not None:
                if self.run_response.extra_data is None:
                    from agno.run.response import RunResponseExtraData

                    self.run_response.extra_data = RunResponseExtraData()

                # Initialize reasoning_messages if it doesn't exist
                if self.run_response.extra_data.reasoning_messages is None:
                    self.run_response.extra_data.reasoning_messages = []

                metrics_message = Message(
                    role="assistant",
                    content=self.run_response.reasoning_content,
                    metrics={"time": reasoning_time_taken},
                )

                # Add the metrics message to the reasoning_messages
                self.run_response.extra_data.reasoning_messages.append(metrics_message)

        except Exception as e:
            # Log the error but don't crash
            from agno.utils.log import log_error

            log_error(f"Failed to add reasoning metrics to extra_data: {str(e)}")

    def _get_effective_filters(self, knowledge_filters: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
        """
        Determine which knowledge filters to use, with priority to run-level filters.

        Args:
            knowledge_filters: Filters passed at run time

        Returns:
            The effective filters to use, with run-level filters taking priority
        """
        effective_filters = None

        # If agent has filters, use those as a base
        if self.knowledge_filters:
            effective_filters = self.knowledge_filters.copy()

        # If run has filters, they override agent filters
        if knowledge_filters:
            if effective_filters:
                # Merge filters, with run filters taking priority
                effective_filters.update(knowledge_filters)
            else:
                effective_filters = knowledge_filters

        if effective_filters:
            log_debug(f"Using knowledge filters: {effective_filters}")

        return effective_filters

    def cli_app(
        self,
        message: Optional[str] = None,
        session_id: Optional[str] = None,
        user_id: Optional[str] = None,
        user: str = "User",
        emoji: str = ":sunglasses:",
        stream: bool = False,
        markdown: bool = False,
        exit_on: Optional[List[str]] = None,
        **kwargs: Any,
    ) -> None:
        """Run an interactive command-line interface to interact with the agent."""

        from inspect import isawaitable

        from rich.prompt import Prompt

        # Ensuring the agent is not using our async MCP tools
        if self.tools is not None:
            for tool in self.tools:
                if isawaitable(tool):
                    raise NotImplementedError("Use `acli_app` to use async tools.")
                if tool.__class__.__name__ in ["MCPTools", "MultiMCPTools"]:
                    raise NotImplementedError("Use `acli_app` to use MCP tools.")

        if message:
            self.print_response(
                message=message, stream=stream, markdown=markdown, user_id=user_id, session_id=session_id, **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, user_id=user_id, session_id=session_id, **kwargs
            )

    async def acli_app(
        self,
        message: Optional[str] = None,
        session_id: Optional[str] = None,
        user_id: Optional[str] = None,
        user: str = "User",
        emoji: str = ":sunglasses:",
        stream: bool = False,
        markdown: bool = False,
        exit_on: Optional[List[str]] = None,
        **kwargs: Any,
    ) -> None:
        """
        Run an interactive command-line interface to interact with the agent.
        Works with agent dependencies requiring async logic.
        """
        from rich.prompt import Prompt

        if message:
            await self.aprint_response(
                message=message, stream=stream, markdown=markdown, user_id=user_id, session_id=session_id, **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

            await self.aprint_response(
                message=message, stream=stream, markdown=markdown, user_id=user_id, session_id=session_id, **kwargs
            )
