# Configuration

RoohAI uses YAML configuration at two levels: a global `config.yaml` for the default pipeline, and per-agent configs in the `agents/` directory.

## Global Config (`config.yaml`)

The root config file defines the default pipeline and model parameters:

```yaml
pipeline:
  stt: whisper-tiny
  tts: piper
  llm: bedrock-claude
  vad: silero

models:
  whisper-tiny:
    type: stt
    class: whisper
    model_id: openai/whisper-tiny
    language: en

  piper:
    type: tts
    class: piper

  bedrock-claude:
    type: llm
    class: bedrock
    model_id: anthropic.claude-3-haiku-20240307-v1:0
    region: us-east-1

  silero:
    type: vad
    class: silero

audio:
  sample_rate: 16000
  channels: 1

server:
  host: 0.0.0.0
  port: 8000
```

### Sections

#### `pipeline`

Maps component types to model names defined in the `models` section:

```yaml
pipeline:
  stt: <model-name>    # Speech-to-Text
  llm: <model-name>    # Language Model
  tts: <model-name>    # Text-to-Speech
  vad: <model-name>    # Voice Activity Detection
```

#### `models`

Each model entry requires `type` and `class`, plus model-specific parameters:

```yaml
models:
  <model-name>:
    type: stt | tts | llm | vad
    class: whisper | wav2vec2 | deepgram | speecht5 | bark | piper | cartesia | bedrock | local | silero
    # ... model-specific parameters
```

The `class` field maps to a registry name. The `type` field determines which base class the model must implement.

#### `audio`

```yaml
audio:
  sample_rate: 16000   # Internal processing sample rate
  channels: 1          # Mono audio
```

#### `server`

```yaml
server:
  host: 0.0.0.0
  port: 8000
```

## Agent Configs (`agents/*.yaml`)

Each agent is a self-contained configuration file stored as `~/.roohai/agents/{name}.yaml`.

### Agent Naming Rules

The agent `name` is used as a URL path segment (`/api/agents/{name}/activate`), a filename (`{name}.yaml`), and an environment variable prefix (`ROOHAI_SECRET_{NAME}_`), so it must be safe for all three:

- **Allowed characters:** letters (`A-Z`, `a-z`), digits (`0-9`), hyphens (`-`), underscores (`_`)
- **Must start with** a letter or digit
- **Length:** 1–64 characters

| Valid | Invalid | Why |
|-------|---------|-----|
| `my-agent` | `my agent` | spaces not allowed |
| `Weather_Bot` | `../etc` | slashes, dots not allowed |
| `Agent-v2` | `.hidden` | must start with letter or digit |
| `a` | `` | empty string |

### Example

```yaml
name: my-cloud-agent
created_at: '2026-03-01T09:11:39.289009+00:00'
status: active      # Only one agent can be "active" at a time
llm_streaming: true # Stream LLM tokens (default), false for batch
system_prompt: You are a helpful voice assistant. Be concise and friendly.

pipeline:
  stt: deepgram
  llm: bedrock-claude
  tts: cartesia
  vad: silero

models:
  deepgram:
    class: deepgram
    type: stt
    model: nova-3
    language: en

  bedrock-claude:
    class: bedrock-claude
    type: llm
    model_id: anthropic.claude-3-haiku-20240307-v1:0
    region: us-east-1
    auth_mode: environment

  cartesia:
    class: cartesia
    type: tts
    model_id: sonic-2
    voice_id: a0e99841-438c-4a64-b679-ae501e7d6091
    language: en
    sample_rate: '24000'

  silero:
    class: silero
    type: vad
```

### LLM Streaming

The `llm_streaming` field controls whether the LLM streams tokens incrementally or waits for the full response:

```yaml
llm_streaming: true    # default — stream tokens, start TTS on first sentence
llm_streaming: false   # wait for full LLM response, then synthesize all at once
```

| Mode | Behavior | Latency | Frontend Display |
|------|----------|---------|------------------|
| `true` (streaming) | Tokens arrive progressively; TTS starts on first completed sentence while LLM is still generating | Lower perceived latency | Text appears word-by-word in chat bubble (`llm_response_chunk` messages) |
| `false` (batch) | Full response arrives at once; TTS starts after LLM finishes | Higher latency | Full text appears at once (`llm_response` message) |

Streaming requires the LLM model to implement `chat_stream()`. The built-in `BedrockLLM` supports streaming. Models that don't override `chat_stream()` (like `LocalLLM`) fall back to yielding the full `chat()` result as one chunk, which behaves identically to batch mode regardless of this setting.

If omitted, defaults to `true`.

### Transport

The `transport` field controls how audio is exchanged between the browser and server:

```yaml
transport: websocket   # default — AudioWorklet + base64 JSON
transport: webrtc      # RTCPeerConnection + Opus codec
```

| Transport | Audio Encoding | Latency | Browser Support |
|-----------|---------------|---------|-----------------|
| `websocket` | PCM16 base64 over JSON | ~50-100ms | All modern browsers |
| `webrtc` | Opus via RTCPeerConnection | ~20-40ms | All modern browsers |

If omitted, defaults to `"websocket"` for backward compatibility.

### System Prompt

Every agent requires a `system_prompt` field. This is the instruction passed to the LLM on every conversation turn, defining the agent's persona and behavior:

```yaml
system_prompt: >
  You are a pirate captain. Always respond in pirate speak.
  Keep your answers short and entertaining.
```

The system prompt is:
- **Required** when creating an agent (via the API or the wizard UI)
- Stored in the agent's YAML config
- Loaded into the pipeline when the agent is activated
- Passed to the LLM's `chat()` method on every call along with the connection's `session_id`

### Agent Status

- `active` — This agent's models are loaded in the pipeline
- `inactive` — Saved but not in use

Only one agent can be active at a time. Activating an agent sets all others to `inactive`.

### Creating Agents Programmatically

```python
from roohai.agents import save_agent, get_agent, list_agents, delete_agent

# Create
config = {
    "name": "my-agent",
    "status": "inactive",
    "system_prompt": "You are a helpful voice assistant.",
    "pipeline": {"stt": "deepgram", "llm": "bedrock-claude", "tts": "piper", "vad": "silero"},
    "models": {
        "deepgram": {"class": "deepgram", "type": "stt", "model": "nova-3"},
        "bedrock-claude": {"class": "bedrock-claude", "type": "llm"},
        "piper": {"class": "piper", "type": "tts", "voice": "en_US-lessac-medium"},
        "silero": {"class": "silero", "type": "vad"},
    },
}
save_agent(config)

# List
agents = list_agents()  # Returns list of dicts

# Read
agent = get_agent("my-agent")  # Returns dict or None

# Delete
delete_agent("my-agent")  # Returns True if existed
```

## Secrets Management

API keys and credentials are stored separately from agent configs in `~/.roohai/secrets.yaml` with `0600` file permissions.

```python
from roohai.secrets import set_agent_secrets, get_agent_secrets, delete_agent_secrets

# Store secrets for an agent
set_agent_secrets("my-agent", {
    "deepgram": {"api_key": "dg-xxxx"},
    "cartesia": {"api_key": "sk-xxxx"},
})

# Retrieve
secrets = get_agent_secrets("my-agent")
# {"deepgram": {"api_key": "dg-xxxx"}, "cartesia": {"api_key": "sk-xxxx"}}

# Clean up
delete_agent_secrets("my-agent")
```

**How secrets merge into configs:** When an agent is activated, the server reads the agent's secrets and merges them into the model configs before creating model instances. This means the agent YAML files never contain sensitive data.

## Environment Variables

Cloud models check for environment variables as fallbacks when no API key is provided in config:

| Variable | Used By |
|----------|---------|
| `AWS_ACCESS_KEY_ID` | BedrockLLM |
| `AWS_SECRET_ACCESS_KEY` | BedrockLLM |
| `AWS_DEFAULT_REGION` | BedrockLLM |
| `DEEPGRAM_API_KEY` | DeepgramSTT |
| `CARTESIA_API_KEY` | CartesiaTTS |
| `ROOHAI_CORS_ORIGINS` | Server CORS policy |

## Security

### CORS (Cross-Origin Resource Sharing)

By default, the RoohAI server allows requests from **all origins** (`*`). To restrict access to specific domains, use the `--cors-origins` CLI flag or the `ROOHAI_CORS_ORIGINS` environment variable.

#### CLI flag

```bash
roohai --cors-origins "https://myapp.com,https://staging.myapp.com"
```

#### Environment variable

```bash
export ROOHAI_CORS_ORIGINS="https://myapp.com,https://staging.myapp.com"
roohai
```

This is useful for Docker, systemd, or CI/CD deployments where CLI flags are less convenient.

#### Behavior

| Setting | Effect |
|---------|--------|
| Not set / `"*"` | All origins allowed (default, suitable for local development) |
| Comma-separated URLs | Only listed origins can make cross-origin requests |

The CLI flag takes precedence when both the flag and environment variable are set.

#### Examples

```bash
# Allow only production domain
roohai --cors-origins "https://myapp.com"

# Allow multiple domains
roohai --cors-origins "https://myapp.com,https://admin.myapp.com"

# Restrict in Docker
docker run -e ROOHAI_CORS_ORIGINS="https://myapp.com" roohai
```

## TTS Chunking

Long LLM responses are split for TTS to avoid synthesis failures. The `TTS_MAX_CHARS` constant (250 characters) controls the maximum text per synthesis call. Text is split at sentence boundaries and short fragments are merged with the previous sentence.
