# Morpheus — Plugin & Hook Manager

> "What is real? How do you define 'real'?" — Morpheus, The Matrix

Discovers plugins, loads hooks, and executes hook pipelines.

## Core Concepts

- **Plugin**: A directory with hooks, config, and optional dependencies
- **Hook**: A decorated function that participates in a named pipeline
- **Priority**: Higher priority hooks execute first (default: 1)

## Plugin Discovery

```python
# Automatic on RAG2F.create()
rag2f = await RAG2F.create(plugins_folder="./plugins/")

# Manual refresh
await rag2f.morpheus.find_plugins()
```

### Discovery Sources (Priority Order)

1. **Entry points** (installed packages) — highest precedence
2. **Filesystem** (`plugins/` folder) — local development

## Hook Decorator

```python
from rag2f.core.morpheus.decorators import hook

# With explicit name
@hook("my_hook_name", priority=10)
def handler(piped_value, *args, rag2f):
    return modified_value

# Using function name as hook name
@hook(priority=5)
def my_hook_name(piped_value, *args, rag2f):
    return modified_value

# Default priority (1)
@hook
def another_hook(piped_value, *args, rag2f):
    return modified_value
```

## Hook Execution

```python
result = morpheus.execute_hook(
    "hook_name",   # Hook pipeline name
    initial_value, # First arg is "piped" through hooks
    *other_args,   # Additional args passed to all hooks
    rag2f=rag2f    # Required keyword arg
)
```

### Pipeline Flow

Hooks execute in descending priority order (higher priority first):

1. `initial_value` passed to first hook (highest priority)
2. Hook returns modified value (or `None` to keep previous)
3. Returned value pipes to next hook
4. Final hook's return becomes `execute_hook()` result

### Return Value Rules

- Hook returns `None` → previous value continues
- Hook returns value → that value pipes to next hook
- Final returned value becomes `execute_hook()` result

## Plugin Structure

```
my_plugin/
├── __init__.py
├── plugin.json           # Optional plugin metadata (recommended)
├── pyproject.toml        # Optional packaging metadata (recommended for distributable plugins)
├── hooks.py              # Functions decorated with @hook
├── plugin_overrides.py   # Optional lifecycle overrides via @plugin (activated/deactivated)
├── requirements.txt      # Plugin-specific dependencies (optional)
└── nested/
    └── more_hooks.py     # Hooks in subdirectories are discovered
```

### plugin.json (recommended)

`plugin_id` is the plugin folder name. `plugin.json` is optional metadata.

```json
{
    "name": "My Plugin",
    "version": "1.0.0",
    "description": "What this plugin does",
    "keywords": ["rag2f", "plugin"],
    "author": {"name": "Your Name", "email": "you@example.com"},
    "urls": {"Repository": "https://example.com/repo"},
    "license": "MIT",
    "min_rag2f_version": "0.0.0",
    "max_rag2f_version": "9.9.9"
}
```

### pyproject.toml (packaging metadata)

RAG2F reads standard `[project]` metadata.

## Entry Point Registration

For distributable plugins, register via entry points:

```toml
# pyproject.toml
[project.entry-points."rag2f.plugins"]
my_plugin = "my_plugin:get_plugin_path"
```

```python
# my_plugin/__init__.py
from pathlib import Path

def get_plugin_path() -> str:
    return str(Path(__file__).parent)
```

## Built-in Hooks

| Hook Name | Module | Purpose |
|-----------|--------|---------|
| `get_id_input_text` | Johnny5 | Generate ID for input |
| `check_duplicated_input_text` | Johnny5 | Check duplicate |
| `handle_text_foreground` | Johnny5 | Process input |
| `indiana_jones_retrieve` | IndianaJones | Retrieval |
| `indiana_jones_synthesize` | IndianaJones | Synthesize response from retrieved items |
| `agent_collect_prompt_fragments` | ATeam | Collect prompt fragments before final prompt assembly |
| `agent_finalize_prompt` | ATeam | Finalize or rewrite the resolved prompt |

## Hook Implementation Examples

### Plugin Lifecycle Override (Activation)

There is no built-in `rag2f_bootstrap_*` hook. If a plugin needs to register
embedders or repositories, do it in the plugin lifecycle override `activated`.

```python
from rag2f.core.morpheus.decorators.plugin_decorator import plugin

@plugin
def activated(plugin, rag2f_instance):
    plugin_id = plugin.id  # ✅ use this in @plugin overrides
    config = rag2f_instance.spock.get_plugin_config(plugin_id) or {}

    # Example: register an embedder
    embedder = MyEmbedder(api_key=config.get("api_key"))
    # The registry key is chosen by the plugin (often a stable name or plugin_id).
    # If you plan to use get_default(), set rag2f.embedder_default to this key.
    rag2f_instance.optimus_prime.register(plugin_id, embedder)

    # Example: register a repository
    # rag2f_instance.xfiles.execute_register(f"{plugin_id}_repo", repo, meta={...})
```

### Processing Hook (Piped Value)

```python
@hook("handle_text_foreground", priority=5)
def my_processor(done: bool, track_id: str, text: str, *, rag2f):
    if done:
        return done  # Already handled
    
    # Do processing...
    success = process_text(track_id, text)
    return success
```

### Chain Modification Hook

```python
@hook("morpheus_test_hook_message", priority=3)
def add_to_message(message: str, *, rag2f):
    return message + " priority 3"
```

### ATeam Prompt Hook

```python
from rag2f.core.dto import PromptFragment


@hook("agent_collect_prompt_fragments", priority=20)
def add_fragments(fragments, context, rag2f):
    if context.purpose == "draft":
        fragments.append(
            PromptFragment(
                text="Write a concise technical draft.",
                plugin_id=context.plugin_id,
            )
        )
    return fragments
```

ATeam prompt hooks are useful when prompt logic must stay plugin-local and composable.
Use them for:
- prompt fragment composition
- prompt variants based on `purpose`
- prompt adjustments based on `caller_hook` or `params`

Do not use them for:
- registering agents
- creating provider clients
- long-lived mutable runtime state

## API Reference

### Morpheus Methods

```python
# Check if plugin exists
morpheus.plugin_exists(plugin_id: str) -> bool

# Get plugin instance
plugin = morpheus.get_plugin(plugin_id: str) -> Plugin

# Get current plugin ID (from within a hook)
plugin_id = morpheus.self_plugin_id() -> str

# Execute hook pipeline
result = morpheus.execute_hook(hook_name: str, *args, rag2f) -> Any

# Register refresh callback
morpheus.on_refresh_callbacks.append(my_callback)
```

---

## Optional: Testing Hooks

See [testing.txt](./testing.txt) for complete testing guide.

### Mock execute_hook

```python
from unittest.mock import MagicMock

mock_rag2f = MagicMock()

def mock_execute_hook(hook_name, *args, **kwargs):
    if hook_name == "my_hook":
        return "mocked_result"
    return args[0] if args else None

mock_rag2f.morpheus.execute_hook.side_effect = mock_execute_hook
```

### Test Hook Priority

```python
def test_hook_priority(morpheus):
    """Hooks execute in descending priority order."""
    result = morpheus.execute_hook("test_hook", "", rag2f=None)
    # Hooks with priority 10, 5, 1 execute in that order
```

### Fresh Morpheus Fixture

```python
@pytest.fixture
def fresh_morpheus(rag2f):
    """Fresh Morpheus instance for isolated testing."""
    return Morpheus(rag2f, plugins_folder="./tests/mocks/plugins/")
```

## Plugin Context

Access plugin ID from within hooks:

```python
@hook("my_hook", priority=5)
def hook_with_context(value, *, rag2f):
    # Get current plugin's ID
    plugin_id = rag2f.morpheus.self_plugin_id()
    
    # Get plugin's config
    config = rag2f.spock.get_plugin_config(plugin_id)
    
    return value

Note:
- Inside `@hook` functions, use `rag2f.morpheus.self_plugin_id()`.
- Inside `@plugin` overrides (`activated`/`deactivated`), use `plugin.id`.
```
