# `Agent` Class Documentation

Welcome to the documentation of the `Agent` class. This foundational class is central to the **AgentForge** framework, enabling the creation, management, and operation of agents. It provides core functionalities essential for agents, serving as a robust template for both general and specialized implementations.

---

## Overview of the `Agent` Class

The `Agent` class is designed to:

- Serve as the base class for all agents within the **AgentForge** framework.
- Provide essential attributes and methods for agent operation.
- Facilitate seamless integration with various workflows and data structures.
- Simplify the creation of custom agents through method overriding.

By subclassing the `Agent` class, developers can create custom agents that inherit default behaviors and override methods to implement specific functionalities.

---

## Class Definition

### Class Attributes

The `Agent` class utilizes several key attributes:

- **`agent_name`**: The name of the agent, typically set to the class name.
- **`logger`**: A logger instance initialized with the agent’s name for logging messages.
- **`config`**: An instance of the `Config` class that handles configuration loading.
- **`prompt_handling`**: An instance of the `PromptHandling` class for managing prompt templates.
- **`data`**: A dictionary to store data relevant for prompt rendering and inference.
- **`prompt`**: A list of prompts generated for use with the LLM.
- **`result`**: Stores the raw result returned by the LLM.
- **`output`**: Holds the final output after processing the LLM's response.
- **`agent_data`**: Configuration data specific to the agent, including settings and operational parameters.

### Initialization

```python
class Agent:
    def __init__(self):
        """
        Initializes an Agent instance, setting up its name, logger, data attributes, and agent-specific configurations.
        It attempts to load the agent's configuration data and storage settings.
        """
        self.agent_name: str = self.__class__.__name__
        self.logger: Logger = Logger(name=self.agent_name)
        self.config = Config()
        self.prompt_handling = PromptHandling()

        self.data: Dict[str, Any] = {}
        self.prompt: Optional[List[str]] = None
        self.result: Optional[str] = None
        self.output: Optional[str] = None

        if not hasattr(self, 'agent_data'):  # Prevent re-initialization
            self.agent_data: Optional[Dict[str, Any]] = None
```

**Explanation**:

- **`self.agent_name`**: Automatically set to the class name, ensuring consistency between the agent's name and its class.
- **`self.logger`**: Initialized with the agent's name for consistent logging.
- **`self.config`**: Loads the configuration settings for the agent and the system.
- **`self.prompt_handling`**: Manages the rendering and validation of prompt templates.
- **Data Attributes**:
  - **`self.data`**: A dictionary to store all data relevant to the agent's operation.
  - **`self.prompt`**: Will hold the rendered prompts ready for the LLM.
  - **`self.result`**: The raw output from the LLM before any parsing.
  - **`self.output`**: The final processed output from the agent.
- **`self.agent_data`**: Will hold agent-specific configuration data once loaded.

---

## Agent Workflow

The `Agent` class defines a standard workflow executed by the `run` method. This workflow consists of several steps that process data, interact with the LLM, and produce the final output.

### `run` Method

```python
def run(self, **kwargs: Any) -> Optional[str]:
    """
    Orchestrates the execution of the agent's workflow: loading data, processing data, generating prompts,
    running language models, parsing results, saving results, and building the output.

    Parameters:
        **kwargs (Any): Keyword arguments that will form part of the agent's data.

    Returns:
        Optional[str]: The output generated by the agent or None if an error occurred during execution.
    """
    try:
        self.logger.log(f"\n{self.agent_name} - Running...", 'info')
        self.load_data(**kwargs)
        self.process_data()
        self.generate_prompt()
        self.run_llm()
        self.parse_result()
        self.save_to_storage()
        self.build_output()
        self.data = {}
        self.logger.log(f"\n{self.agent_name} - Done!", 'info')
    except Exception as e:
        self.logger.log(f"Agent execution failed: {e}", 'error')
        return None

    return self.output
```

**Workflow Steps**:

1. **Logging Start**: Logs the beginning of the agent's execution.
2. **Data Loading**: Calls `self.load_data(**kwargs)` to load all necessary data into `self.data`.
3. **Data Processing**: Processes the loaded data with `self.process_data()`. This can be customized in subclasses.
4. **Prompt Generation**: Generates prompts for the LLM using `self.generate_prompt()`.
5. **LLM Interaction**: Runs the LLM with `self.run_llm()` and stores the result in `self.result`.
6. **Result Parsing**: Parses the LLM's output with `self.parse_result()`. This can be customized in subclasses.
7. **Saving to Storage**: Saves any necessary data to storage with `self.save_to_storage()`.
8. **Building Output**: Constructs the final output with `self.build_output()`, storing it in `self.output`.
9. **Cleanup**: Clears `self.data` to free up memory.
10. **Logging Completion**: Logs the completion of the agent's execution.
11. **Return Output**: Returns the final output.

---

## Key Concepts

Understanding the following key concepts is essential for effectively utilizing the `Agent` class.

### The `self.data` Variable

#### Overview

- **Purpose**: `self.data` serves as the central repository for all data relevant to the agent's operation.
- **Usage**: It is used throughout the agent's workflow, particularly for rendering prompts and passing parameters to the LLM.

#### Data Aggregation Process

1. **Agent Configuration Data**:
   - Loaded via `self.load_agent_data()`.
   - Includes parameters (`params`) and prompt templates (`prompts`).
   - Example:
     ```python
     self.data.update({
         'params': self.agent_data.get('params').copy(),
         'prompts': self.agent_data['prompts'].copy()
     })
     ```

2. **Persona Data**:
   - Loaded via `self.load_persona_data()`.
   - If personas are enabled (`PersonasEnabled` in system settings), persona attributes are added to `self.data`.
   - Example:
     ```python
     if self.agent_data['settings']['system'].get('PersonasEnabled'):
         persona = self.agent_data.get('persona', {})
         for key in persona:
             self.data[key.lower()] = persona[key]
     ```

3. **Storage Data**:
   - Placeholder method `self.load_from_storage()` for loading data from storage systems.
   - Can be overridden in custom agents to implement specific storage loading logic.

4. **Additional Data**:
   - Loaded via `self.load_additional_data()`.
   - Intended to be overridden in custom agents for loading any extra data needed.

5. **Keyword Arguments (`**kwargs`)**:
   - Passed to `self.run(**kwargs)` and integrated into `self.data` via `self.load_kwargs(**kwargs)`.
   - Allows for dynamic data input at runtime.

### Role in Prompt Rendering

- **Dynamic Prompt Rendering**: `self.data` provides the values for placeholders in prompt templates.
- **Example**:
  - If a prompt template contains `{user_input}`, and `self.data['user_input'] = "Hello, AgentForge!"`, the rendered prompt will replace `{user_input}` with "Hello, AgentForge!".

### Importance

- **Flexibility**: By aggregating data from various sources, `self.data` enables agents to operate dynamically and contextually.
- **Customization**: Developers can manipulate `self.data` within overridden methods to alter agent behavior.

---

## Agent Configuration Data (`self.agent_data`)

- **Purpose**: Stores agent-specific configurations, including settings, parameters, prompt templates, persona data, and LLM instances.
- **Loaded Via**: `self.load_agent_data()`.
- **Contents**:
  - **`settings`**: System and model settings.
  - **`params`**: Parameters for the LLM.
  - **`prompts`**: Prompt templates for the agent.
  - **`persona`**: Persona data if available.
  - **`llm`**: The LLM instance assigned to the agent.
  - **`storage`**: Storage instance if storage is enabled.

**Accessing Configuration Data**:

- Within the agent, you can access configuration settings using `self.agent_data`.
- Example:
  ```python
  storage_enabled = self.agent_data['settings']['system'].get('StorageEnabled')
  ```

---

## Workflow Methods Overview

While the `Agent` class provides default implementations, several methods are intended to be overridden to customize agent behavior.

### Methods Intended for Overriding

- **`load_from_storage(self)`**: For loading data from storage systems.
- **`load_additional_data(self)`**: For loading any additional data required by the agent.
- **`process_data(self)`**: For processing `self.data` before prompt generation.
- **`parse_result(self)`**: For parsing the LLM's output in `self.result`.
- **`save_to_storage(self)`**: For saving data to storage systems.
- **`build_output(self)`**: For constructing the final output from processed data.

**Example of Overriding**:

```python
class CustomAgent(Agent):
    def process_data(self):
        # Custom data processing
        self.data['processed_input'] = self.data['user_input'].upper()

    def build_output(self):
        # Custom output construction
        self.output = f"Processed Result: {self.result}"
```

---

## Interaction with the LLM

### Prompt Generation

- **Method**: `self.generate_prompt()`
- **Process**:
  - Retrieves prompt templates from `self.data['prompts']`.
  - Renders the templates using `self.prompt_handling`, replacing placeholders with values from `self.data`.
  - Stores the rendered prompts in `self.prompt`.

### Running the LLM

- **Method**: `self.run_llm()`
- **Process**:
  - Retrieves the LLM instance from `self.agent_data['llm']`.
  - Uses parameters from `self.agent_data.get('params', {})`.
  - Calls `model.generate_text(self.prompt, **params)`.
  - Stores the result in `self.result`.

---

## Storage Handling

### Resolving Storage

- **Method**: `self.resolve_storage()`
- **Process**:
  - Checks if storage is enabled in system settings.
  - If enabled, initializes the storage instance and stores it in `self.agent_data['storage']`.

### Saving to Storage

- **Method**: `self.save_to_storage()`
- **Usage**:
  - Intended to be overridden to implement specific logic for saving data.
  - Access the storage instance via `self.agent_data['storage']`.

---

## Conclusion

By understanding the `Agent` class and its core components, you can harness the full potential of the **AgentForge** framework to build powerful, customized agents tailored to your specific needs.

- **Key Takeaways**:
  - **Class Attributes**: Familiarize yourself with the attributes used throughout the agent's lifecycle.
  - **Workflow**: Understand the sequence of methods executed in `run` and how they interact.
  - **Data Handling**: Utilize `self.data` effectively to manage and manipulate data within your agent.
  - **Customization**: Override methods as needed to implement custom behaviors.

---

## Next Steps

- **Agent Methods Guide**: For a detailed walkthrough of the main methods you can override, see the [Agent Methods](AgentMethods.md) guide.
- **Creating Custom Agents**: Learn how to create custom agents by subclassing the `Agent` class in the [Custom Agents](CustomAgents.md) documentation.
- **Prompt Handling**: Dive deeper into how agent prompts are rendered within **AgentForge** by visiting the [Agent Prompts](AgentPrompts.md) guide.

---

**Need Help?**

If you have questions or need assistance, feel free to reach out:

- **Email**: [contact@agentforge.net](mailto:contact@agentforge.net)
- **Discord**: Join our [Discord Server](https://discord.gg/ttpXHUtCW6)

---