# Spock — Configuration Manager

> "Logic is the beginning of wisdom, not the end." — Spock, Star Trek

Manages configuration from JSON files and environment variables.

## Priority Order

```
ENV variables > JSON file > provided config dict > defaults
```

## Configuration Structure

```json
{
  "rag2f": {
    "embedder_default": "azure_openai",
    "setting_key": "value"
  },
  "plugins": {
    "plugin_id": {
      "api_key": "...",
      "endpoint": "...",
      "nested": {
        "deep_key": "value"
      }
    }
  }
}
```

## Initialization

```python
from rag2f.core.rag2f import RAG2F

# Via RAG2F.create()
rag2f = await RAG2F.create(
    config_path="config.json",      # Load from file
    config={"rag2f": {...}}         # Or provide dict directly
)

# Access Spock
spock = rag2f.spock  # or rag2f.config_manager
```

## Environment Variables

Pattern: `RAG2F__<SECTION>__<KEY>__<SUBKEY>`

```bash
# Core settings
export RAG2F__RAG2F__EMBEDDER_DEFAULT=azure_openai

# Plugin settings (secrets should use ENV, not JSON)
export RAG2F__PLUGINS__MY_PLUGIN__API_KEY=sk-xxx
export RAG2F__PLUGINS__MY_PLUGIN__ENDPOINT=https://api.example.com

# Nested keys
export RAG2F__PLUGINS__MY_PLUGIN__NESTED__DEEP_KEY=value
```

## Accessing Configuration

### Core Config

```python
# Get single value
value = spock.get_rag2f_config("embedder_default")
# Returns: "azure_openai" or None

# Get with default
value = spock.get_rag2f_config("missing_key", default="fallback")
# Returns: "fallback"
```

### Plugin Config

```python
# Get entire plugin config
config = spock.get_plugin_config("my_plugin")
# Returns: {"api_key": "...", "endpoint": "...", "nested": {...}}

# Get specific key
api_key = spock.get_plugin_config("my_plugin", "api_key")
# Returns: "sk-xxx"

# Get nested key
deep = spock.get_plugin_config("my_plugin", "nested", "deep_key")
# Returns: "value"

# With default
value = spock.get_plugin_config("my_plugin", "missing", default="fallback")
# Returns: "fallback"
```

## Configuration Examples

### config.example.json

```json
{
  "rag2f": {
    "embedder_default": "azure_openai"
  },
  "plugins": {
    "azure_openai_embedder": {
      "endpoint": "https://your-resource.openai.azure.com/",
      "deployment_name": "text-embedding-ada-002",
      "api_version": "2023-05-15"
    },
    "my_vector_db": {
      "host": "localhost",
      "port": 6333,
      "collection": "documents"
    }
  }
}
```

### Plugin Reading Config

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

@plugin
def activated(plugin, rag2f_instance):
  # Get plugin config (key == plugin.id)
  config = rag2f_instance.spock.get_plugin_config(plugin.id) or {}
    
    # Required: use ENV for secrets
    api_key = config.get("api_key")
    if not api_key:
        raise ValueError("API key not configured")
    
    embedder = AzureOpenAIEmbedder(
        api_key=api_key,
        endpoint=config.get("endpoint"),
        deployment=config.get("deployment_name"),
    )
    
    # Register under a stable key (must match rag2f.embedder_default)
    rag2f_instance.optimus_prime.register("azure_openai", embedder)
```

## Best Practices

### Secrets via ENV

```bash
# Good: secrets in environment
export RAG2F__PLUGINS__MY_PLUGIN__API_KEY=sk-xxx

# Bad: secrets in config.json (don't commit!)
```

### Config Validation in Plugins

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

@plugin
def activated(plugin, rag2f_instance):
  config = rag2f_instance.spock.get_plugin_config(plugin.id) or {}
    
    # Validate required config
    required = ["endpoint", "deployment"]
    missing = [k for k in required if not config.get(k)]
    if missing:
        raise ValueError(f"Missing config: {missing}")
    
    # Validate api_key from ENV
    api_key = config.get("api_key")
    if not api_key:
        raise ValueError(
            "API key required. Set RAG2F__PLUGINS__MY_EMBEDDER__API_KEY"
        )
```

---

## Optional: Testing with Spock

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

### In-Memory Config

```python
import pytest_asyncio
from rag2f.core.rag2f import RAG2F

@pytest_asyncio.fixture
async def rag2f_with_config():
    return await RAG2F.create(
        config={
            "rag2f": {"embedder_default": "test_embedder"},
            "plugins": {
                "test_plugin": {"setting": "value"}
            }
        }
    )

def test_config_access(rag2f_with_config):
    assert rag2f_with_config.spock.get_rag2f_config("embedder_default") == "test_embedder"
    assert rag2f_with_config.spock.get_plugin_config("test_plugin", "setting") == "value"
```

### Temp Config File

```python
import tempfile
import json

def test_with_temp_config():
    config = {"rag2f": {"key": "value"}}
    
    with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
        json.dump(config, f)
        config_path = f.name
    
    # Use config_path in tests
```

### Mocking ENV

```python
import os
from unittest.mock import patch

def test_env_override():
    env = {"RAG2F__RAG2F__EMBEDDER_DEFAULT": "env_embedder"}
    
    with patch.dict(os.environ, env, clear=False):
        spock = Spock()
        spock.load()
        assert spock.get_rag2f_config("embedder_default") == "env_embedder"
```

## API Reference

```python
class Spock:
    def __init__(self, config_path: str | None = None): ...
    
    def load(self, config: dict[str, Any] | None = None) -> None:
        """Load configuration (called automatically by RAG2F.create)."""
    
    def get_rag2f_config(self, key: str, default: Any = None) -> Any:
        """Get core RAG2F configuration value."""
    
    def get_plugin_config(self, plugin_id: str, *keys: str, default: Any = None) -> Any:
        """Get plugin configuration (whole config or nested keys)."""
    
    @staticmethod
    def default_config() -> dict[str, Any]:
        """Return default config structure: {'rag2f': {}, 'plugins': {}}"""
```

---

## Key Features (from SPOCK_README.md)

- **Unified config**: Core + plugin settings in one place
- **Multiple sources**: JSON files and/or environment variables
- **Priority model**: ENV variables override JSON values
- **Instance-based**: Each RAG2F instance has isolated configuration (no global state)
- **Type inference**: ENV values are automatically parsed as int/float/bool/JSON when possible
- **Safe for testing**: No global state makes tests isolated and reproducible

---

## Type Inference in Environment Variables

ENV values are automatically parsed:

```bash
# int
export RAG2F__PLUGINS__MY_PLUGIN__SIZE=1536

# float
export RAG2F__PLUGINS__MY_PLUGIN__TIMEOUT=30.0

# bool
export RAG2F__PLUGINS__MY_PLUGIN__ENABLED=true

# JSON object
export RAG2F__PLUGINS__MY_PLUGIN__HTTP='{"timeout": 30, "retries": 2}'

# JSON array
export RAG2F__PLUGINS__MY_PLUGIN__TAGS='["a", "b"]'

# string (default)
export RAG2F__PLUGINS__MY_PLUGIN__API_KEY=sk-xxx
```

The parser tries int → float → bool → JSON → string in that order.

---

## Troubleshooting

- Check if Spock is loaded: `rag2f.spock.is_loaded`
- Verify config file path: `rag2f.spock.config_path`
- Inspect environment variables: `env | grep RAG2F__`
- Enable debug logging: `logging.basicConfig(level=logging.DEBUG)`

---

## Optional
## Migration from Single-File Plugin Config

**Before** (plugin reads its own config file):

```python
embedder = AzureOpenAIEmbedder(config_file="plugins/azure_openai_embedder/config.json")
```

**After** (plugin reads centralized Spock via RAG2F instance):

```python
@plugin
def activated(plugin, rag2f_instance):
    config = rag2f_instance.spock.get_plugin_config(plugin.id) or {}
    embedder = AzureOpenAIEmbedder(config)
    rag2f_instance.optimus_prime.register(plugin.id, embedder)
```

Move plugin-specific config under `plugins.<plugin_id>` in the main `config.json`, or set via environment variables. This provides:
- Single source of truth for all configuration
- Easier testing (mock Spock or pass config dict to `RAG2F.create()`)
- No file I/O from plugins
- Consistent ENV override semantics
