# -*- coding: utf-8 -*-
"""
@author:XuMing(xuming624@qq.com)
@description: Agent base class - fields definition and initialization

This module contains the Agent class definition with all fields and initialization logic.
The actual method implementations are mixed in from other modules.
"""
from __future__ import annotations

import json
from datetime import datetime
from textwrap import dedent
from collections import defaultdict
from typing import (
    Any,
    AsyncIterator,
    Callable,
    cast,
    Dict,
    Iterator,
    List,
    Literal,
    Optional,
    overload,
    Sequence,
    Type,
    Union,
)
from uuid import uuid4
from copy import copy, deepcopy
from dataclasses import dataclass, field, fields
from pydantic import BaseModel
from typing import TYPE_CHECKING

from agentica.utils.log import logger, set_log_level_to_debug, set_log_level_to_info
from agentica.model.openai import OpenAIChat
from agentica.tools.base import ModelTool, Tool, Function
from agentica.template import PromptTemplate
from agentica.model.content import Image, Video
from agentica.model.base import Model
from agentica.model.message import Message
from agentica.run_response import RunResponse
from agentica.memory import AgentMemory, Memory, AgentRun
from agentica.compression.manager import CompressionManager
from agentica.db.base import BaseDb

if TYPE_CHECKING:
    from agentica.workspace import Workspace
    from agentica.knowledge.base import Knowledge


@dataclass(init=False)
class Agent:
    """AI Agent with configurable behavior and capabilities.

    Agent supports two memory management approaches:

    1. **Workspace (Recommended)**: File-based configuration and memory
       - Human-readable Markdown files
       - Version control friendly (Git)
       - Easy to edit and share
       - Includes: AGENT.md, PERSONA.md, USER.md, MEMORY.md

    2. **AgentMemory**: Runtime session management
       - Conversation history (messages, runs)
       - Session summaries
       - Note: User memories feature is deprecated, use Workspace instead

    Example - Using Workspace (recommended):
        >>> from agentica import Agent
        >>> agent = Agent(
        ...     workspace_path="~/.agentica/workspace",
        ...     model=OpenAIChat(model="gpt-4o"),
        ... )

    Example - Using session summary:
        >>> from agentica import Agent
        >>> from agentica.memory import AgentMemory
        >>> agent = Agent(
        ...     memory=AgentMemory.with_summary(),
        ...     model=OpenAIChat(model="gpt-4o"),
        ... )
    """
    # -*- Agent settings
    # Model to use 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 chat history when a run is started.
    introduction: Optional[str] = None

    # -*- Agent Data
    # Images associated with this agent
    images: Optional[List[Image]] = None
    # Videos associated with this agent
    videos: Optional[List[Video]] = None
    # Data associated with this agent
    agent_data: Optional[Dict[str, Any]] = None

    # -*- User settings
    # ID of the user interacting with this agent
    user_id: Optional[str] = None
    # Data associated with the user interacting with this agent
    user_data: Optional[Dict[str, Any]] = None

    # -*- Session settings
    # Session UUID (autogenerated if not set)
    session_id: Optional[str] = None
    # Session name
    session_name: Optional[str] = None
    # Session state stored in the session_data
    session_state: Dict[str, Any] = field(default_factory=dict)
    # Data associated with this session
    session_data: Optional[Dict[str, Any]] = None

    # -*- Agent Memory
    memory: AgentMemory = field(default_factory=AgentMemory)
    # add_history_to_messages=true adds the chat history to the messages sent to the Model.
    add_history_to_messages: bool = False
    # Number of historical responses to add to the messages.
    num_history_responses: int = 3
    # DEPRECATED: Use Workspace.save_memory() instead for persistent user memories
    # This feature will be removed in a future version
    enable_user_memories: bool = False

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

    # -*- Agent Database
    db: Optional[BaseDb] = None
    # AgentSession from the database: DO NOT SET MANUALLY
    _agent_session: Optional[Any] = None  # AgentSession type

    # -*- Workspace settings
    # Workspace instance for this agent
    workspace: Optional["Workspace"] = None
    # Path to the workspace directory
    workspace_path: Optional[str] = None
    # If True, load workspace context (AGENT.md, PERSONA.md, etc.) into instructions
    load_workspace_context: bool = True
    # If True, load workspace memory into instructions
    load_workspace_memory: bool = True
    # Number of days of memory to load from workspace
    memory_days: int = 2

    # -*- Agent Tools
    # A list of tools provided to the Model.
    # Tools are functions the model may generate JSON inputs for.
    # If you provide a dict, it is not called by the model.
    tools: Optional[List[Union[ModelTool, Tool, Callable, Dict, Function]]] = None
    # Whether the LLM supports tool calls (function calls)
    support_tool_calls: bool = True
    # Show tool calls in Agent response.
    show_tool_calls: bool = False
    # Maximum number of tool calls allowed.
    tool_call_limit: Optional[int] = None
    # Controls which (if any) tool is called by the model.
    # "none" means the model will not call a tool and instead generates a message.
    # "auto" means the model can pick between generating a message or calling a tool.
    # Specifying a particular function via {"type: "function", "function": {"name": "my_function"}}
    #   forces the model to call that tool.
    # "none" is the default when no tools are present. "auto" is the default if tools are present.
    tool_choice: Optional[Union[str, Dict[str, Any]]] = None
    # Auto-load MCP tools from mcp_config.json/yaml
    # Searches: current dir -> parent dirs -> ~/.agentica/
    # Default False since not all models support tools
    auto_load_mcp: bool = False

    # -*- 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 before running the agent
    resolve_context: bool = True

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

    # -*- Agent Multi-round Strategy Settings
    # Enable multi-round strategy for better search accuracy
    enable_multi_round: bool = False
    # Maximum number of rounds for multi-round strategy
    max_rounds: int = 10
    # Maximum number of tokens to use in the model input
    max_tokens: int = 128000

    # -*- Agentic Prompt Settings, prompt enhancement, like openclaw/claude code
    # Enable agentic prompt enhancement (HEARTBEAT, SOUL modules)
    enable_agentic_prompt: bool = False

    # -*- Compression Settings
    # Enable compression of tool call results to save context space
    compress_tool_results: bool = False
    # CompressionManager instance for managing compression
    compression_manager: Optional[CompressionManager] = None

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

    # -*- System Prompt Settings
    # System prompt: provide the system prompt as a string
    system_prompt: Optional[Union[str, Callable]] = None
    # System prompt template: provide the system prompt as a PromptTemplate
    system_prompt_template: Optional[PromptTemplate] = None
    # If True, build a default system message using agent settings and use that
    use_default_system_message: bool = True
    # Role for the system message
    system_message_role: str = "system"

    # -*- 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 task the agent should achieve.
    task: Optional[str] = None
    # List of instructions for the agent.
    instructions: Optional[Union[str, List[str], Callable]] = None
    # List of guidelines for the agent.
    guidelines: Optional[List[str]] = 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 True, add instructions to return "I dont know" when the agent does not know the answer.
    prevent_hallucinations: bool = False
    # If True, add instructions to prevent prompt leakage
    prevent_prompt_leakage: bool = False
    # If True, add instructions for limiting tool access to the default system prompt if tools are provided
    limit_tool_access: bool = False
    # 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 = True
    # The language to use for output, e.g. "en" for English, "zh" for Chinese, etc.
    output_language: Optional[str] = None

    # -*- User Prompt Settings
    # User prompt template: provide the user prompt as a PromptTemplate
    user_prompt_template: Optional[PromptTemplate] = None
    # If True, build a default user prompt using references and chat history
    use_default_user_message: bool = True
    # Role for the user message
    user_message_role: str = "user"

    # -*- Agent Response Settings
    # Provide a response model to get the response as a Pydantic model
    response_model: Optional[Type[Any]] = None
    # If True, the response from the Model is converted into the response_model
    # Otherwise, the response is returned as a string
    parse_response: bool = True
    # Use the structured_outputs from the Model if available
    structured_outputs: bool = False
    # Save the response to a file
    save_response_to_file: Optional[str] = None

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

    # debug_mode=True enables debug logs
    debug_mode: bool = False
    # monitoring=True logs Agent information
    monitoring: bool = False

    # DO NOT SET THE FOLLOWING FIELDS MANUALLY
    # Run ID: DO NOT SET MANUALLY
    run_id: Optional[str] = None
    # Input to the Agent run: DO NOT SET MANUALLY
    run_input: Optional[Union[str, List, Dict]] = None
    # Response from the Agent run: DO NOT SET MANUALLY
    run_response: RunResponse = field(default_factory=RunResponse)
    # If True, stream the response from the Agent
    stream: Optional[bool] = None
    # If True, stream the intermediate steps from the Agent
    stream_intermediate_steps: bool = False

    def __init__(
            self,
            *,
            # Core settings
            model: Optional[Model] = None,
            name: Optional[str] = None,
            agent_id: Optional[str] = None,
            introduction: Optional[str] = None,

            # Data
            images: Optional[List[Image]] = None,
            videos: Optional[List[Video]] = None,
            agent_data: Optional[Dict[str, Any]] = None,

            # User
            user_id: Optional[str] = None,
            user_data: Optional[Dict[str, Any]] = None,

            # Session
            session_id: Optional[str] = None,
            session_name: Optional[str] = None,
            session_state: Optional[Dict[str, Any]] = None,
            session_data: Optional[Dict[str, Any]] = None,

            # Memory
            memory: Optional[AgentMemory] = None,
            add_history_to_messages: bool = False,
            num_history_responses: int = 3,
            enable_user_memories: bool = False,

            # Knowledge
            knowledge: Optional[Knowledge] = None,
            add_references: bool = False,
            retriever: Optional[Callable[..., Optional[list[dict]]]] = None,
            references_format: Literal["json", "yaml"] = "json",

            # Database
            db: Optional[BaseDb] = None,

            # Workspace
            workspace: Optional["Workspace"] = None,
            workspace_path: Optional[str] = None,
            load_workspace_context: bool = True,
            load_workspace_memory: bool = True,
            memory_days: int = 2,

            # Tools
            tools: Optional[List[Union[ModelTool, Tool, Callable, Dict, Function]]] = None,
            support_tool_calls: bool = True,
            show_tool_calls: bool = False,
            tool_call_limit: Optional[int] = None,
            tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
            auto_load_mcp: bool = False,

            # Context
            context: Optional[Dict[str, Any]] = None,
            add_context: bool = False,
            resolve_context: bool = True,

            # Default tools
            read_chat_history: bool = False,
            search_knowledge: bool = True,
            update_knowledge: bool = False,
            read_tool_call_history: bool = False,

            # Agent Multi-round Strategy Settings
            enable_multi_round: bool = False,
            max_rounds: int = 20,
            max_tokens: int = 128000,
            enable_agentic_prompt: bool = False,
            run_timeout: Optional[float] = None,
            first_token_timeout: Optional[float] = None,

            # Compression Settings
            compress_tool_results: bool = False,
            compression_manager: Optional[Any] = None,

            # Messages
            add_messages: Optional[List[Union[Dict, Message]]] = None,

            # System prompt
            system_prompt: Optional[Union[str, Callable]] = None,
            system_prompt_template: Optional[PromptTemplate] = None,
            use_default_system_message: bool = True,
            system_message_role: str = "system",

            # System message building
            description: Optional[str] = None,
            task: Optional[str] = None,
            instructions: Optional[Union[str, List[str], Callable]] = None,
            guidelines: Optional[List[str]] = None,
            expected_output: Optional[str] = None,
            additional_context: Optional[str] = None,
            prevent_hallucinations: bool = False,
            prevent_prompt_leakage: bool = False,
            limit_tool_access: bool = False,
            markdown: bool = False,
            add_name_to_instructions: bool = False,
            add_datetime_to_instructions: bool = True,
            output_language: Optional[str] = None,

            # User prompt
            user_prompt_template: Optional[PromptTemplate] = None,
            use_default_user_message: bool = True,
            user_message_role: str = "user",

            # Response
            response_model: Optional[Type[Any]] = None,
            parse_response: bool = True,
            structured_outputs: bool = False,
            save_response_to_file: Optional[str] = None,

            # Team
            team: Optional[List['Agent']] = None,
            role: Optional[str] = None,
            respond_directly: bool = False,
            add_transfer_instructions: bool = True,
            team_response_separator: str = "\n",

            # Debug
            debug_mode: bool = False,
            monitoring: bool = False,

            # Aliases for backward compatibility
            llm: Optional[Model] = None,
            knowledge_base: Optional[Knowledge] = None,
            add_chat_history_to_messages: Optional[bool] = None,
            add_knowledge_references_to_prompt: Optional[bool] = None,
            output_model: Optional[Type[Any]] = None,
            output_file: Optional[str] = None,
            debug: Optional[bool] = None,
    ):
        # Handle aliases
        if llm is not None:
            model = llm
        if knowledge_base is not None:
            knowledge = knowledge_base
        if add_chat_history_to_messages is not None:
            add_history_to_messages = add_chat_history_to_messages
        if add_knowledge_references_to_prompt is not None:
            add_references = add_knowledge_references_to_prompt
        if output_model is not None:
            response_model = output_model
        if output_file is not None:
            save_response_to_file = output_file
        if debug is not None:
            debug_mode = debug

        # Initialize fields
        self.model = model
        self.name = name
        self.agent_id = agent_id or str(uuid4())
        self.introduction = introduction

        self.images = images
        self.videos = videos
        self.agent_data = agent_data

        self.user_id = user_id
        self.user_data = user_data

        self.session_id = session_id or str(uuid4())
        self.session_name = session_name
        self.session_state = session_state or {}
        self.session_data = session_data

        self.memory = memory or AgentMemory()
        self.add_history_to_messages = add_history_to_messages
        self.num_history_responses = num_history_responses
        self.enable_user_memories = enable_user_memories

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

        self.db = db
        self._agent_session = None

        # Workspace initialization
        self.workspace = workspace
        self.workspace_path = workspace_path
        self.load_workspace_context = load_workspace_context
        self.load_workspace_memory = load_workspace_memory
        self.memory_days = memory_days

        self.tools = tools
        self.support_tool_calls = support_tool_calls
        self.show_tool_calls = show_tool_calls
        self.tool_call_limit = tool_call_limit
        self.tool_choice = tool_choice
        self.auto_load_mcp = auto_load_mcp

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

        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.enable_multi_round = enable_multi_round
        self.max_rounds = max_rounds
        self.max_tokens = max_tokens
        self.enable_agentic_prompt = enable_agentic_prompt
        self.run_timeout = run_timeout
        self.first_token_timeout = first_token_timeout

        self.compress_tool_results = compress_tool_results
        self.compression_manager = compression_manager

        self.add_messages = add_messages

        self.system_prompt = system_prompt
        self.system_prompt_template = system_prompt_template
        self.use_default_system_message = use_default_system_message
        self.system_message_role = system_message_role

        self.description = description
        self.task = task
        self.instructions = instructions
        self.guidelines = guidelines
        self.expected_output = expected_output
        self.additional_context = additional_context
        self.prevent_hallucinations = prevent_hallucinations
        self.prevent_prompt_leakage = prevent_prompt_leakage
        self.limit_tool_access = limit_tool_access
        self.markdown = markdown
        self.add_name_to_instructions = add_name_to_instructions
        self.add_datetime_to_instructions = add_datetime_to_instructions
        self.output_language = output_language

        self.user_prompt_template = user_prompt_template
        self.use_default_user_message = use_default_user_message
        self.user_message_role = user_message_role

        self.response_model = response_model
        self.parse_response = parse_response
        self.structured_outputs = structured_outputs
        self.save_response_to_file = save_response_to_file

        self.team = team
        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

        # Runtime fields
        self.run_id = None
        self.run_input = None
        self.run_response = RunResponse()
        self.stream = None
        self.stream_intermediate_steps = False

        # Post-init setup
        self._post_init()

    def _post_init(self):
        """Post-initialization setup"""
        # Set log level based on debug mode
        if self.debug_mode:
            set_log_level_to_debug()
            logger.debug("Set Log level: debug")
        else:
            set_log_level_to_info()

        # Initialize workspace if workspace_path is provided
        self._init_workspace()

        # Auto-load MCP tools from config file
        if self.auto_load_mcp:
            self._load_mcp_tools()

        # Collect and merge tool system prompts into instructions
        self._merge_tool_system_prompts()

        # Initialize compression manager if compress_tool_results is enabled
        if self.compress_tool_results and self.compression_manager is None:
            from agentica.compression import CompressionManager
            self.compression_manager = CompressionManager(
                model=self.model,
                compress_tool_results=True,
            )

        # Setup user memories: sync db and user_id between Agent and AgentMemory
        # DEPRECATED: This feature is deprecated, use Workspace instead
        if self.enable_user_memories:
            import warnings
            warnings.warn(
                "enable_user_memories is deprecated. Use Workspace for persistent memory storage. "
                "See: Agent(workspace_path='~/.agentica/workspace')",
                DeprecationWarning,
                stacklevel=3
            )
            self.memory.create_user_memories = True
            self.memory.update_user_memories_after_run = True
        # Sync db from Agent to AgentMemory if not set
        if self.db is not None and self.memory.db is None:
            self.memory.db = self.db
        # Sync user_id from Agent to AgentMemory if not set
        if self.user_id is not None and self.memory.user_id is None:
            self.memory.user_id = self.user_id

    def _init_workspace(self):
        """Initialize workspace from workspace_path if provided"""
        # Create workspace from path if not already provided
        if self.workspace_path and not self.workspace:
            from agentica.workspace import Workspace
            self.workspace = Workspace(self.workspace_path, user_id=self.user_id)

        # Sync user_id to existing workspace
        if self.workspace and self.user_id:
            self.workspace.set_user(self.user_id)

        # Note: workspace context and memory are loaded dynamically in
        # get_system_message() on every run, not statically injected here.
        # This ensures that edits to AGENT.md, USER.md, MEMORY.md etc.
        # take effect on the next conversation turn.

    def get_workspace_context_prompt(self) -> Optional[str]:
        """Dynamically load workspace context for injection into system prompt.

        Called on every run to ensure edits to AGENT.md, PERSONA.md, TOOLS.md,
        USER.md take effect on the next conversation turn (like Claude Code / OpenCode).

        Returns:
            Context prompt string, or None if no context available.
        """
        if not self.workspace or not self.load_workspace_context:
            return None

        if not self.workspace.exists():
            return None

        context = self.workspace.get_context_prompt()
        return context if context else None

    def get_workspace_memory_prompt(self) -> Optional[str]:
        """Dynamically load workspace memory for injection into system prompt.

        Called on every run to ensure newly saved memories are always included.

        Returns:
            Memory prompt string, or None if no memory available.
        """
        if not self.workspace or not self.load_workspace_memory:
            return None

        memory = self.workspace.get_memory_prompt(days=self.memory_days)
        return memory if memory else None

    def _load_mcp_tools(self):
        """Auto-load MCP tools from mcp_config.json/yaml if available.
        
        Searches for config file in:
        1. Current directory and parent directories
        2. ~/.agentica/
        
        Only loads servers with enable=true (default).
        """
        try:
            import asyncio
            from agentica.mcp.config import MCPConfig
            from agentica.tools.mcp_tool import McpTool, CompositeMultiMcpTool
            
            config = MCPConfig()
            if not config.servers:
                return
            
            # Use McpTool.from_config to load all enabled servers
            mcp_tool = McpTool.from_config(config_path=config.config_path)
            
            # Initialize MCP tool(s) to populate functions.
            # Uses async context manager to connect, discover tools, then
            # immediately closes the initial connection. Each subsequent tool
            # call creates its own fresh connection (see create_tool_function).
            async def init_mcp():
                if isinstance(mcp_tool, CompositeMultiMcpTool):
                    await mcp_tool.__aenter__()
                    await mcp_tool.__aexit__(None, None, None)
                else:
                    await mcp_tool.__aenter__()
                    await mcp_tool.__aexit__(None, None, None)
            
            # Run async initialization in sync context
            try:
                loop = asyncio.get_running_loop()
            except RuntimeError:
                loop = None
            
            if loop is not None:
                # Already in async context, run in a separate thread
                import concurrent.futures
                with concurrent.futures.ThreadPoolExecutor() as executor:
                    future = executor.submit(asyncio.run, init_mcp())
                    future.result(timeout=30)
            else:
                # No event loop, safe to use asyncio.run
                asyncio.run(init_mcp())
            
            # Add MCP tool to tools list
            if self.tools is None:
                self.tools = [mcp_tool]
            else:
                self.tools = list(self.tools) + [mcp_tool]
            
            logger.info(f"Auto-loaded MCP tools from: {config.config_path}")
        except ImportError:
            # MCP dependencies not installed, skip silently
            pass
        except Exception as e:
            logger.warning(f"Failed to auto-load MCP tools: {e}")

    def _merge_tool_system_prompts(self) -> None:
        """
        Collect system prompts from all tools and merge them into instructions.
        
        This allows tools to provide their own usage guidance that gets injected
        into the agent's system prompt automatically.
        
        Tool prompts are deduplicated by tool class name to avoid duplicate content
        when multiple instances of the same tool type are present.
        """
        if not self.tools:
            return

        # Use dict to deduplicate by tool class name
        # This prevents duplicate prompts when same tool type appears multiple times
        tool_prompts_map: Dict[str, str] = {}
        
        for tool in self.tools:
            if isinstance(tool, Tool) and hasattr(tool, 'get_system_prompt'):
                prompt = tool.get_system_prompt()
                if prompt:
                    # Use tool class name as key for deduplication
                    tool_class_name = tool.__class__.__name__
                    # Keep the first (or longest) prompt for each tool type
                    if tool_class_name not in tool_prompts_map or len(prompt) > len(tool_prompts_map[tool_class_name]):
                        tool_prompts_map[tool_class_name] = prompt

        if not tool_prompts_map:
            return

        # Build structured tool instructions with clear hierarchy
        tool_sections = []
        for tool_name, prompt in tool_prompts_map.items():
            tool_sections.append(f"<tool_instructions name=\"{tool_name}\">\n{prompt}\n</tool_instructions>")
        
        merged_prompt = "<tool_system_prompts>\n" + "\n\n".join(tool_sections) + "\n</tool_system_prompts>"

        # Merge tool prompts into instructions
        if self.instructions is None:
            self.instructions = [merged_prompt]
        elif isinstance(self.instructions, str):
            self.instructions = [self.instructions, merged_prompt]
        elif isinstance(self.instructions, list):
            self.instructions = list(self.instructions) + [merged_prompt]
        # Note: if instructions is a Callable, we don't modify it

        logger.debug(f"Merged {len(tool_prompts_map)} tool system prompts into instructions: {list(tool_prompts_map.keys())}")

    @property
    def is_streamable(self) -> bool:
        """Determines if the response from the Model is streamable
        For structured outputs we disable streaming.
        """
        return self.response_model is None

    @property
    def identifier(self) -> Optional[str]:
        """Get an identifier for the agent"""
        return self.name or self.agent_id

    @classmethod
    def from_workspace(
        cls,
        workspace_path: str,
        model: Optional["Model"] = None,
        initialize: bool = True,
        **kwargs
    ) -> "Agent":
        """从工作空间创建 Agent

        这是一个工厂方法，用于从工作空间目录创建 Agent 实例。
        工作空间包含 AGENT.md, PERSONA.md 等配置文件，以及 memory/ 和 skills/ 目录。

        Args:
            workspace_path: 工作空间路径
            model: LLM 模型实例
            initialize: 是否初始化工作空间（如果不存在则创建默认文件）
            **kwargs: 其他 Agent 参数

        Returns:
            Agent 实例

        Example:
            >>> agent = Agent.from_workspace(
            ...     workspace_path="~/.agentica/workspace",
            ...     model=ZhipuAI(),
            ...     initialize=True,
            ... )
            >>> agent.run("你好")
        """
        from agentica.workspace import Workspace

        workspace = Workspace(workspace_path)
        if initialize and not workspace.exists():
            workspace.initialize()

        return cls(
            workspace=workspace,
            workspace_path=workspace_path,
            model=model,
            **kwargs
        )

    def save_memory(self, content: str, long_term: bool = False):
        """保存记忆到工作空间

        将内容保存到工作空间的记忆文件中。支持保存到日记忆或长期记忆。

        Args:
            content: 记忆内容
            long_term: True 保存到长期记忆 (MEMORY.md)，False 保存到日记忆 (memory/YYYY-MM-DD.md)

        Example:
            >>> agent.save_memory("用户偏好：喜欢简洁的回答", long_term=True)
            >>> agent.save_memory("今天讨论了项目进度")  # 保存到日记忆
        """
        if self.workspace:
            self.workspace.write_memory(content, to_daily=not long_term)
            logger.debug(f"Saved memory to workspace: long_term={long_term}")
        else:
            logger.warning("No workspace configured, memory not saved")

    def add_instruction(self, instruction: str):
        """动态添加指令到 Agent

        在运行时向 Agent 添加额外的指令，常用于注入技能提示或临时上下文。

        Args:
            instruction: 要添加的指令内容

        Example:
            >>> # 注入技能指令
            >>> skill_prompt = skill_loader.get_skill_prompt("github")
            >>> agent.add_instruction(skill_prompt)
            >>>
            >>> # 添加临时上下文
            >>> agent.add_instruction("当前项目是一个 Python Web 应用")
        """
        if not instruction:
            return

        if self.instructions is None:
            self.instructions = [instruction]
        elif isinstance(self.instructions, str):
            self.instructions = [self.instructions, instruction]
        elif isinstance(self.instructions, list):
            self.instructions = list(self.instructions) + [instruction]
        # Note: if instructions is a Callable, we don't modify it
        else:
            logger.warning(f"Cannot add instruction: instructions is {type(self.instructions)}")
            return

        logger.debug(f"Added instruction to agent: {instruction[:50]}...")

    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.
        """
        # Runtime fields that should not be passed to __init__
        # These are initialized manually in __init__ and marked as "DO NOT SET MANUALLY"
        runtime_fields = {"run_id", "run_input", "run_response", "stream", "stream_intermediate_steps"}
        
        # Method fields that are declared as Callable type annotations (from mixins)
        # These should not be copied as they are methods, not data fields
        method_fields = {
            # From prompts.py
            "get_json_output_prompt", "get_system_message", "get_user_message", 
            "get_messages_for_run", "get_relevant_docs_from_knowledge",
            "convert_documents_to_string", "convert_context_to_string",
            # From runner.py
            "run", "arun", "arun_stream", "_run", "_arun", "_run_single_round",
            "_run_multi_round", "_arun_single_round", "_arun_multi_round",
            "_on_pre_step", "_on_tool_call", "_on_post_step",
            "save_run_response_to_file", "_aggregate_metrics_from_run_messages",
            "generic_run_response",
            # From session.py
            "get_agent_data", "get_session_data", "get_agent_session",
            "from_agent_session", "read_from_storage", "write_to_storage",
            "add_introduction", "load_session", "create_session", "new_session",
            "reset", "load_user_memories", "get_user_memories", "clear_user_memories",
            "rename", "rename_session", "generate_session_name",
            "auto_rename_session", "delete_session",
            # From team.py
            "as_tool", "get_transfer_function", "get_transfer_prompt", "get_tools",
            # From tools.py
            "get_chat_history", "get_tool_call_history", "search_knowledge_base",
            "add_to_knowledge", "update_memory", "_create_run_data",
            # From printer.py
            "print_response", "aprint_response", "cli_app",
            # From media.py
            "add_image", "add_video", "get_images", "get_videos",
        }

        # Extract the fields to set for the new Agent
        fields_for_new_agent = {}

        # Get all dataclass fields instead of model_fields_set
        for field in fields(self):
            field_name = field.name
            # Skip runtime fields and method fields
            if field_name in runtime_fields or field_name in method_fields:
                continue
            field_value = getattr(self, field_name)
            if field_value is not None:
                fields_for_new_agent[field_name] = self._deep_copy_field(field_name, field_value)

        # Update fields if provided
        if update:
            fields_for_new_agent.update(update)

        # Create a new Agent
        new_agent = self.__class__(**fields_for_new_agent)
        logger.debug(f"Created new Agent: agent_id: {new_agent.agent_id} | session_id: {new_agent.session_id}")
        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."""
        # For memory and model, use their deep_copy methods
        if field_name in ("memory", "model"):
            return field_value.deep_copy()

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

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

        # For other types, return as is
        return field_value

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

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

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

    def update_model(self) -> None:
        if self.model is None:
            logger.debug("Model not set, Using OpenAIChat as default")
            self.model = OpenAIChat()
        logger.debug(f"Agent, using model: {self.model}")

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

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

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

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

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

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

        # Add user_id to the Model for Langfuse tracing
        # Default to "default" if not set, required for Langfuse user tracking
        self.model.user_id = self.user_id or "default"

        # Add agent name to the Model for Langfuse tracing
        if self.name is not None:
            self.model.agent_name = self.name

    # Import methods from other modules
    # These will be added via the mixin imports at the end of this file
    
    # From prompts.py
    get_json_output_prompt: Callable
    get_system_message: Callable
    get_user_message: Callable
    get_messages_for_run: Callable
    get_relevant_docs_from_knowledge: Callable
    convert_documents_to_string: Callable
    convert_context_to_string: Callable
    
    # From runner.py
    run: Callable
    arun: Callable
    arun_stream: Callable
    _run: Callable
    _arun: Callable
    _run_single_round: Callable
    _run_multi_round: Callable
    _arun_single_round: Callable
    _arun_multi_round: Callable
    _on_pre_step: Callable
    _on_tool_call: Callable
    _on_post_step: Callable
    save_run_response_to_file: Callable
    _aggregate_metrics_from_run_messages: Callable
    generic_run_response: Callable
    
    # From session.py
    get_agent_data: Callable
    get_session_data: Callable
    get_agent_session: Callable
    from_agent_session: Callable
    read_from_storage: Callable
    write_to_storage: Callable
    add_introduction: Callable
    load_session: Callable
    create_session: Callable
    new_session: Callable
    reset: Callable
    load_user_memories: Callable
    get_user_memories: Callable
    clear_user_memories: Callable
    rename: Callable
    rename_session: Callable
    generate_session_name: Callable
    auto_rename_session: Callable
    delete_session: Callable
    
    # From team.py
    as_tool: Callable
    get_transfer_function: Callable
    get_transfer_prompt: Callable
    get_tools: Callable
    
    # From tools.py
    get_chat_history: Callable
    get_tool_call_history: Callable
    search_knowledge_base: Callable
    add_to_knowledge: Callable
    update_memory: Callable
    _create_run_data: Callable
    
    # From printer.py
    print_response: Callable
    aprint_response: Callable
    cli_app: Callable
    
    # From media.py
    add_image: Callable
    add_video: Callable
    get_images: Callable
    get_videos: Callable


# Import and apply mixins to add method implementations
from agentica.agent.prompts import PromptsMixin
from agentica.agent.runner import RunnerMixin
from agentica.agent.session import SessionMixin
from agentica.agent.team import TeamMixin
from agentica.agent.tools import ToolsMixin
from agentica.agent.printer import PrinterMixin
from agentica.agent.media import MediaMixin


# Apply mixins by copying methods to Agent class
def _apply_mixin(cls, mixin_cls):
    """Apply a mixin class to the Agent class by copying its methods."""
    for name in dir(mixin_cls):
        if not name.startswith('_') or name.startswith('_') and not name.startswith('__'):
            attr = getattr(mixin_cls, name)
            if callable(attr) and not isinstance(attr, type):
                setattr(cls, name, attr)


_apply_mixin(Agent, PromptsMixin)
_apply_mixin(Agent, RunnerMixin)
_apply_mixin(Agent, SessionMixin)
_apply_mixin(Agent, TeamMixin)
_apply_mixin(Agent, ToolsMixin)
_apply_mixin(Agent, PrinterMixin)
_apply_mixin(Agent, MediaMixin)
