# Tool Observation Formatting

## Overview

Tools in Minion now support **context-aware output formatting**. When a tool is called as the last statement in generated code, its output becomes an "observation" that is passed to the LLM. This new mechanism allows tools to format their output differently for LLM consumption vs. in-code usage.

## The Problem

When tools return data for use in code, they should return raw, processable data. But when the same tool output becomes an observation for the LLM, it should be formatted in a more human/LLM-friendly way.

**Example:**
- `file_read` tool returns raw file content when used in code (so it can be split, parsed, etc.)
- But when shown as observation to LLM, it should include line numbers for easier reference

## The Solution

### `format_for_observation()` Method

Every tool (both `BaseTool` and `AsyncBaseTool`) now has a `format_for_observation()` method that can be overridden to provide custom formatting for observations.

```python
class BaseTool(ABC):
    def format_for_observation(self, output: Any) -> str:
        """
        Format tool output for LLM observation.

        Args:
            output: The raw output from forward() method

        Returns:
            Formatted string suitable for LLM observation

        Default behavior: Convert output to string
        """
        return str(output) if output is not None else ""
```

## Usage

### 1. Define a Tool with Custom Observation Formatting

```python
from minion.tools.base_tool import BaseTool

class FileReadTool(BaseTool):
    name = "file_read"
    description = "Read the contents of a file"
    inputs = {
        "file_path": {
            "type": "string",
            "description": "Path to the file to read"
        }
    }
    output_type = "string"

    def forward(self, file_path: str) -> str:
        """Returns raw file content"""
        with open(file_path, 'r') as f:
            return f.read()

    def format_for_observation(self, output: Any) -> str:
        """Add line numbers for LLM observation"""
        if not isinstance(output, str):
            return str(output)

        lines = output.split('\n')
        formatted_lines = []
        padding = len(str(len(lines)))

        for i, line in enumerate(lines, start=1):
            line_num = str(i).rjust(padding)
            formatted_lines.append(f"{line_num} | {line}")

        return '\n'.join(formatted_lines)
```

### 2. Use the Tool in Code

When used in the middle of code, the tool returns raw output:

```python
# Generated by LLM
content = file_read(file_path="script.py")
lines = content.split('\n')  # Raw string can be processed
first_line = lines[0]
```

Output assigned to `content`:
```
import sys
def main():
    print('Hello')
```

When used as the last statement, it becomes an observation with formatting:

```python
# Generated by LLM
file_read(file_path="script.py")
```

Observation shown to LLM:
```
1 | import sys
2 | def main():
3 |     print('Hello')
```

## Implementation Details

### How It Works

1. **Tool Execution**: `CodeMinion` executes the code using `AsyncPythonExecutor` or `LocalPythonExecutor`
2. **AST Analysis**: `_get_last_tool_from_code()` parses the code to identify if the last statement is a tool call
3. **Conditional Formatting**:
   - If last statement is a tool call → use `tool.format_for_observation(output)`
   - Otherwise → use default string formatting
4. **Observation Creation**: The formatted output is included in the observation message to the LLM

### Key Methods

**In `CodeMinion` (worker.py):**

```python
def _get_last_tool_from_code(self, code: str):
    """Extract the last tool called in code by parsing AST"""
    # Returns tool object if last statement is a tool call

def _format_output_for_observation(self, output: Any, code: str) -> str:
    """Format output using tool's format_for_observation if available"""
    tool = self._get_last_tool_from_code(code)
    if tool and hasattr(tool, 'format_for_observation'):
        return tool.format_for_observation(output)
    return str(output)
```

## Examples

### Example 1: File Reading with Line Numbers

See `minion/tools/file_tools.py` for the complete implementation.

**Use in code:**
```python
content = file_read(file_path="config.json")
data = json.loads(content)  # Process raw content
```

**Use as observation:**
```python
file_read(file_path="config.json")
# LLM sees:
# 1 | {
# 2 |   "setting": "value",
# 3 |   "enabled": true
# 4 | }
```

### Example 2: Search Results with Highlighting

```python
class SearchTool(BaseTool):
    name = "search"
    description = "Search for files matching a pattern"

    def forward(self, pattern: str) -> list:
        # Returns list of file paths
        return glob.glob(pattern)

    def format_for_observation(self, output: Any) -> str:
        """Format search results with counts and formatting"""
        if not output:
            return "No files found."

        result = f"Found {len(output)} files:\n"
        for i, path in enumerate(output, 1):
            result += f"  {i}. {path}\n"
        return result
```

**In code:** Returns `['file1.py', 'file2.py']` (raw list)
**As observation:** Returns formatted string with count and numbering

### Example 3: Calculator with Step-by-Step

```python
class CalculatorTool(BaseTool):
    name = "calculate"
    description = "Perform mathematical calculations"

    def forward(self, expression: str) -> float:
        return eval(expression)  # Simplified

    def format_for_observation(self, output: Any) -> str:
        """Show result with formatting"""
        return f"Result: {output:,.2f}"
```

## Best Practices

1. **Keep `forward()` pure**: Always return raw, processable data from `forward()`
2. **Format for humans in observations**: Use `format_for_observation()` to make output LLM-friendly
3. **Handle errors gracefully**: Don't add formatting to error messages
4. **Consider the context**: Think about what information helps the LLM understand the result
5. **Keep it readable**: Don't over-format; simple is often better

## Benefits

✅ **Single Tool, Dual Purpose**: One tool definition serves both code and observation needs
✅ **Better LLM Understanding**: Formatted observations help LLMs reference specific parts
✅ **Flexible**: Each tool can customize its observation format
✅ **Backward Compatible**: Default implementation just converts to string
✅ **Clean Separation**: Raw data for code, formatted view for LLM

## Testing

See `tests/standalone_test_observation.py` for complete test examples.

```bash
python tests/standalone_test_observation.py
```

## Future Extensions

Potential enhancements:
- **Markdown formatting**: Support rich formatting in observations
- **Syntax highlighting**: Color-coded output for code snippets
- **Truncation**: Smart truncation for large outputs
- **Interactive format**: Allow LLM to request different format views
