Metadata-Version: 2.4
Name: vaani-sdk
Version: 0.2.0
Summary: Python SDK for the Vaani External API
License: MIT
Keywords: ai,calls,sdk,vaani,voice
Requires-Python: >=3.9
Requires-Dist: httpx>=0.24.0
Requires-Dist: pydantic>=2.0.0
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: respx>=0.20; extra == 'dev'
Description-Content-Type: text/markdown

# vaani-sdk

Python SDK for the [Vaani](https://vaanivoice.ai) API. Authenticate once with your API key and call all available endpoints from clean, typed Python methods — both synchronous and asynchronous.

## Installation

```bash
pip install vaani-sdk
```

Or install directly from source:

```bash
git clone https://github.com/your-org/vaani-sdk
cd vaani-sdk
pip install -e .
```

## Quick Start

```python
from vaani_sdk import VaaniClient

client = VaaniClient(api_key="YOUR_API_KEY")

# Trigger an outbound call
result = client.trigger_call(
    agent_id="8cf3373e-eb6f-4b4c-9f3c-324a56a91147",
    contact_number="+919876543210",
    name="Nishank",
)
print(result.call_id)  # "outbound-1776774443-863aa698"

client.close()
```

Using the client as a **context manager** (recommended — ensures the HTTP connection is closed):

```python
with VaaniClient(api_key="YOUR_API_KEY") as client:
    history = client.get_call_history(page=1, page_size=10)
    for call in history.data:
        print(call.call_id)
```

## Configuration

| Parameter  | Description                      | Default                     |
| ---------- | -------------------------------- | --------------------------- |
| `api_key`  | Your Vaani API key (`X-API-Key`) | _(required)_                |
| `base_url` | Base URL of the Vaani API        | `https://api.vaanivoice.ai` |
| `timeout`  | Request timeout in seconds       | `120.0`                     |

```python
client = VaaniClient(
    api_key="YOUR_API_KEY",
    base_url="https://api.vaanivoice.ai",
    timeout=60.0,
)
```

### Getting your API key

1. Log in to **[app.vaanivoice.ai](https://app.vaanivoice.ai)**
2. Navigate to **Settings > API Keys**
3. Click **Generate API Key**, give it a descriptive name, and copy the key immediately

Your key is shown only once at creation time. Store it securely in an environment variable or secrets manager — never commit it to version control.

```python
import os
from vaani_sdk import VaaniClient

client = VaaniClient(api_key=os.getenv("VAANI_API_KEY"))
```

---

## API Reference

### `client.health()`

Check the health of the Vaani API.

```python
resp = client.health()
print(resp.status)   # "healthy"
print(resp.service)  # "vaani-external-api"
```

---

### `client.trigger_call(...)`

Create an outbound call dispatch.

| Parameter         | Type | Required | Description                                                                                   |
| ----------------- | ---- | -------- | --------------------------------------------------------------------------------------------- |
| `agent_id`        | str  | Yes      | Agent UUID from the Agent Config page on [app.vaanivoice.ai](https://app.vaanivoice.ai)       |
| `contact_number`  | str  | Yes      | Phone number in E.164 format (e.g. `+919876543210`)                                           |
| `name`            | str  | Yes      | Customer name                                                                                 |
| `voice`           | str  | No       | Override the agent's default voice for this call                                              |
| `metadata`        | dict | No       | Template variables configured in the agent's prompt, injected at call time                    |
| `outbound_number` | str  | No       | Override the caller ID (E.164 format). Must be a number configured in your Telephony settings |

```python
result = client.trigger_call(
    agent_id="8cf3373e-eb6f-4b4c-9f3c-324a56a91147",
    contact_number="+919876543210",
    name="Nishank",
    metadata={
        "customer_name": "Rahul Sharma",
        "property_name": "Prateek Laurel",
    },
    outbound_number="+919873446283",
)
print(result.call_id)  # "outbound-1776774443-863aa698"
print(result.raw)      # full API response dict
```

Success response shape:

```json
{
  "success": true,
  "message": "Call initiated successfully",
  "output": {
    "agent_name": "my-sales-agent",
    "call_id": "outbound-1776774443-863aa698"
  },
  "error": null
}
```

---

### `client.get_call_history(page, page_size)`

Retrieve paginated call history for the authenticated user's workspace.

| Parameter   | Type | Default | Description                |
| ----------- | ---- | ------- | -------------------------- |
| `page`      | int  | 1       | Page number (1-indexed)    |
| `page_size` | int  | 50      | Records per page (max 200) |

```python
history = client.get_call_history(page=1, page_size=20)

# Pagination info
print(history.pagination.total)        # e.g. 1028
print(history.pagination.total_pages)  # e.g. 21

# Individual calls
for call in history.data:
    print(call.call_id, call.raw.get("call_status"), call.raw.get("duration_ms"))
```

Each call record includes `call_id`, `call_type`, `call_status`, `agent_name`, `from_number`, `to_number`, `Start_time`, `End_time`, `duration_ms`, `call_cost`, `call_summary`, `recording_api`, `call_transcription`, and more.

---

### `client.get_transcript(call_id)`

Retrieve the full conversation transcript for a completed call, with speaker labels (`AGENT:` / `USER:`).

```python
transcript = client.get_transcript("outbound-1776774443-863aa698")
print(transcript.transcript)
```

Returns `"Transcript not found in Azure Blob Storage"` if the call is still in progress or the transcript hasn't been generated yet.

---

### `client.get_call_details(call_id)`

Get detailed call information including transcript, extracted entities, conversation evaluation, and AI-generated summary.

```python
details = client.get_call_details("outbound-1776774443-863aa698")
print(details.raw.get("summary"))
print(details.raw.get("entity"))
print(details.raw.get("call_eval_tag"))
```

Response includes `transcription`, `entity` (extracted entities like callback requests), `conversation_eval`, `summary`, and `call_eval_tag`.

---

### `client.stream_audio(call_id)`

Download the audio recording for a call. Returns `audio/ogg` or `audio/mpeg` binary data.

```python
audio = client.stream_audio("outbound-1776774443-863aa698")
print(audio.content_type)  # e.g. "audio/ogg"

with open("recording.ogg", "wb") as f:
    f.write(audio.content)
```

For **large files**, stream in chunks to avoid loading everything into memory:

```python
with open("recording.ogg", "wb") as f:
    for chunk in client.stream_audio_iter("outbound-1776774443-863aa698"):
        f.write(chunk)
```

---

### `client.create_agent(agent_display_name, config)`

Create a new agent associated with your client.

| Parameter            | Type | Required | Description                                          |
| -------------------- | ---- | -------- | ---------------------------------------------------- |
| `agent_display_name` | str  | Yes      | Display name for the new agent                       |
| `config`             | dict | No       | Optional initial agent configuration (persona, etc.) |

```python
result = client.create_agent(
    agent_display_name="my-support-bot",
    config={
        "persona": {
            "identity": {
                "system_prompt": "You are a helpful support agent."
            }
        }
    },
)
print(result.agent_id)    # "33ac5907-0e44-43c4-934e-08e335cca6ee"
print(result.agent_name)  # "vaani123my-support-bot"
```

---

### `client.webrtc_token(...)`

Generate a WebRTC token for voice chat with an agent via LiveKit.

| Parameter               | Type  | Default  | Description                                    |
| ----------------------- | ----- | -------- | ---------------------------------------------- |
| `agent_id`              | str   | None     | Agent UUID (optional if agent_name provided)   |
| `agent_name`            | str   | None     | Agent name (optional if agent_id provided)     |
| `voice_gender`          | str   | "female" | Voice gender ('male' or 'female')              |
| `primary_language`      | str   | "hi"     | Primary language code (e.g. 'en', 'hi')        |
| `secondary_language`    | str   | "en"     | Secondary language code                        |
| `welcome_message`       | str   | None     | Custom welcome message (optional)              |
| `welcome_interruptible` | bool  | True     | Whether welcome message can be interrupted     |
| `bg_noise_enabled`      | bool  | False    | Enable background noise                        |
| `bg_noise_volume`       | int   | 60       | Background noise volume (0-100)                |
| `voice_speed`           | float | 1.0      | Voice speed multiplier (0.6-1.4)               |
| `metadata`              | dict  | None     | Arbitrary key-value pairs (optional)           |

```python
result = client.webrtc_token(
    agent_id="33ac5907-0e44-43c4-934e-08e335cca6ee",
    voice_gender="female",
    primary_language="en",
    welcome_message="Hi! How can I help you today?",
)
print(result.token)     # "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
print(result.room_url)  # "wss://livekit.vaanivoice.ai"
```

---

### `client.update_agent_persona(agent_id, persona)`

Partially update the **persona** section of an agent's configuration. Deep-merges the provided payload with the existing persona config.

| Parameter   | Type | Required | Description                                                                    |
| ----------- | ---- | -------- | ------------------------------------------------------------------------------ |
| `agent_id`  | str  | Yes      | Agent UUID                                                                     |
| `persona`   | dict | Yes      | Persona config dict (identity, senses_capabilities, actions, memories, etc.)   |

```python
result = client.update_agent_persona(
    agent_id="33ac5907-0e44-43c4-934e-08e335cca6ee",
    persona={
        "identity": {
            "system_prompt": "You are a helpful customer service agent.",
            "greeting_message": {
                "agent_message": "Hi! How can I help you today?",
                "interruptible": True,
            }
        },
        "senses_capabilities": {
            "language": "English",
            "brain": {
                "llm": {
                    "primary": {
                        "provider": "google",
                        "model": "gemini-2.5-flash",
                        "parameters": {
                            "temperature": 0.7,
                            "max_tokens": 1000
                        }
                    }
                }
            }
        }
    },
)
print(result.agent_display_name)  # Updated agent name
print(result.raw["persona"])      # Full merged persona config
```

---

### `client.update_agent_training(agent_id, training)`

Partially update the **training** section (knowledge base, FAQ, guardrails) of an agent's configuration.

| Parameter  | Type | Required | Description                                 |
| ---------- | ---- | -------- | ------------------------------------------- |
| `agent_id` | str  | Yes      | Agent UUID                                  |
| `training` | dict | Yes      | Training config dict (knowledge, know_how)  |

```python
result = client.update_agent_training(
    agent_id="33ac5907-0e44-43c4-934e-08e335cca6ee",
    training={
        "know_how": {
            "faq": {
                "What are your hours?": "We are open 9 AM to 5 PM.",
                "How do I reset my password?": "Click 'Forgot Password' on the login page."
            },
            "guardrails": {
                "no_personal_info": "Never ask for credit card numbers or passwords."
            }
        }
    },
)
print(result.agent_display_name)
```

---

### `client.update_agent_experience(agent_id, experience)`

Partially update the **experience** section (conversational settings, idle/end-call behavior) of an agent's configuration.

| Parameter    | Type | Required | Description                                                      |
| ------------ | ---- | -------- | ---------------------------------------------------------------- |
| `agent_id`   | str  | Yes      | Agent UUID                                                       |
| `experience` | dict | Yes      | Experience config dict (conversational_experience, settings)     |

```python
result = client.update_agent_experience(
    agent_id="33ac5907-0e44-43c4-934e-08e335cca6ee",
    experience={
        "conversational_experience": {
            "bg_noise": {
                "enabled": True,
                "volume": 0.3,
                "sound": "https://example.com/office-ambience.mp3"
            },
            "filler_words": {
                "filler_words_frequency": 0.5,
                "filler_words_list": ["umm", "uhh", "ok"]
            }
        },
        "settings": {
            "call_settings": {
                "max_call_duration": 30
            },
            "idle_conversation_settings": {
                "idle_call_hangup_timeout": 20,
                "idle_call_warning_message": "Are you still there?"
            }
        }
    },
)
print(result.agent_display_name)
```

---

### `client.update_agent_analysis(agent_id, analysis)`

Partially update the **analysis** section (dispositions, evaluations, data extraction) of an agent's configuration.

| Parameter  | Type | Required | Description                                     |
| ---------- | ---- | -------- | ----------------------------------------------- |
| `agent_id` | str  | Yes      | Agent UUID                                      |
| `analysis` | dict | Yes      | Analysis config dict (evaluations, extraction)  |

```python
result = client.update_agent_analysis(
    agent_id="33ac5907-0e44-43c4-934e-08e335cca6ee",
    analysis={
        "evaluations": {
            "dispositions": {
                "enabled": True,
                "prompt_based": [
                    {
                        "name": "purchase_intent",
                        "type": "bool",
                        "prompt": "Did the user express intent to purchase?",
                        "list_of_tags": {
                            "Yes": "User is interested",
                            "No": "User declined"
                        }
                    }
                ]
            },
            "conversation_evaluation": {
                "enabled": True
            }
        },
        "extraction": {
            "data_collection": {
                "enabled": True,
                "data_points": [
                    {
                        "name": "customer_email",
                        "prompt": "Extract the customer's email address",
                        "nullable": True
                    }
                ]
            }
        }
    },
)
print(result.agent_display_name)
```

---

### `client.update_agent_deployment(agent_id, deployment)`

Partially update the **deployment** section (phone call types, inbound/outbound numbers) of an agent's configuration.

| Parameter    | Type | Required | Description                             |
| ------------ | ---- | -------- | --------------------------------------- |
| `agent_id`   | str  | Yes      | Agent UUID                              |
| `deployment` | dict | Yes      | Deployment config dict (phone settings) |

```python
result = client.update_agent_deployment(
    agent_id="33ac5907-0e44-43c4-934e-08e335cca6ee",
    deployment={
        "deployment": {
            "phone": {
                "call_type": {
                    "Inbound": "",
                    "Outbound": ["+919876543210", "+919876543211"]
                }
            }
        }
    },
)
print(result.agent_display_name)
print(result.raw["deployment_config"])  # Full merged deployment config
```

---

## WebRTC Sessions

The `client.sessions` namespace provides the full session-based WebRTC flow, which is the recommended approach for embedding voice chat into your own UI.

### WebRTC Flow

1. Create a session with `client.sessions.create(...)` — returns a `session_token` and embeddable `session_url`.
2. Call `client.sessions.connect(session_token)` when the user is ready to join — returns LiveKit credentials.
3. Pass `creds.lk_url` and `creds.lk_token` to the **LiveKit client SDK** in your browser or mobile app.
4. Optionally watch real-time events with `client.sessions.ws_url(session_token)`.
5. End the session with `client.sessions.disconnect(session_token)`.

```python
from vaani_sdk import VaaniClient

with VaaniClient(api_key="YOUR_API_KEY") as client:
    # 1. Create a session
    session = client.sessions.create(
        agent_id="33ac5907-0e44-43c4-934e-08e335cca6ee",
        ui_type="widget",
        grace_period=60,
        session_ttl=1800,
        initial_message="User is asking about order #1234",
        metadata={
            "user_id": "user_123",
            "ticket_id": "T-1234",
        },
    )
    print(session.session_token)  # JWT session token
    print(session.session_url)    # embeddable/shareable URL
    print(session.expires_at)     # expiry datetime

    # 2. Exchange token for LiveKit credentials
    creds = client.sessions.connect(session.session_token)
    print(creds.lk_url)    # "wss://livekit.vaanivoice.ai"
    print(creds.lk_token)  # LiveKit participant JWT
    print(creds.room_name) # LiveKit room name

    # 3. Check session status
    status = client.sessions.get(session.session_token)
    print(status.status)  # "created" | "agent_ready" | "active" | "reconnecting" | "ended"

    # 4. End the session (with grace period for reconnection)
    client.sessions.disconnect(session.session_token)

    # Or end immediately with no grace period
    client.sessions.disconnect(session.session_token, explicit_end=True)
```

### Session Events via WebSocket

Use `client.sessions.ws_url(token)` to build a WebSocket URL and listen to server-side push events:

```python
import asyncio
import json
import websockets
from vaani_sdk import VaaniClient

client = VaaniClient(api_key="YOUR_API_KEY")

async def listen(session_token: str) -> None:
    url = client.sessions.ws_url(session_token)
    async with websockets.connect(url) as ws:
        async for message in ws:
            event = json.loads(message)
            if event["event"] == "agent_ready":
                print("Agent has joined the room")
            elif event["event"] == "agent_timeout":
                print("Agent failed to join:", event.get("reason"))
            elif event["event"] == "session_ended":
                print("Session ended:", event.get("reason"))

asyncio.run(listen("session-token-here"))
```

### Sessions Reference

| Method | Description | Returns |
| --- | --- | --- |
| `client.sessions.create(agent_id, ...)` | Create a session and return a token + embeddable URL | `WebRTCSession` |
| `client.sessions.connect(token)` | Exchange session token for LiveKit credentials | `WebRTCSessionConnect` |
| `client.sessions.get(token)` | Fetch current session state | `WebRTCSessionStatus` |
| `client.sessions.disconnect(token, explicit_end=False)` | Mark disconnect or end the session | `WebRTCSessionStatus` |
| `client.sessions.ws_url(token)` | Build WebSocket URL for real-time session events | `str` |

**`sessions.create()` parameters:**

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `agent_id` | str | required | Agent UUID or name |
| `ui_type` | str | `"fullpage"` | Display mode: `"fullpage"` or `"widget"` |
| `grace_period` | int | None | Reconnection window in seconds (0–3600) |
| `session_ttl` | int | None | Session lifetime in seconds (60–86400) |
| `initial_message` | str | None | Message injected into agent context at session start |
| `metadata` | dict | None | Arbitrary key-value data forwarded to the agent |

---

## Resource Namespaces

### `client.calls`

Manage calls via the resource namespace (an alternative to the flat `trigger_call`, `get_transcript`, etc. methods):

```python
with VaaniClient(api_key="YOUR_API_KEY") as client:
    # Trigger an outbound call
    call = client.calls.trigger(
        agent_id="33ac5907-0e44-43c4-934e-08e335cca6ee",
        contact_number="+919876543210",
        name="Rahul",
        metadata={"ticket_id": "T-1234"},
    )
    print(call.call_id)    # "outbound-1776774443-863aa698"
    print(call.status)     # "in_progress"

    # Get a specific call
    call = client.calls.get(call.call_id)

    # Get call transcript
    transcript = client.calls.get_transcript(call.call_id)

    # List calls (with optional agent filter and pagination)
    calls = client.calls.list(agent_id="33ac5907-0e44-43c4-934e-08e335cca6ee", page=1, page_size=50)
    for c in calls:
        print(c.call_id, c.status)
```

### `client.agents`

List and inspect agents via the resource namespace:

```python
with VaaniClient(api_key="YOUR_API_KEY") as client:
    # List all agents
    agents = client.agents.list()
    for agent in agents:
        print(agent.id, agent.agent_name)

    # Get a specific agent
    agent = client.agents.get("33ac5907-0e44-43c4-934e-08e335cca6ee")
    print(agent.agent_name)
    print(agent.agent_language)
    print(agent.created_at)
```

---

## Async Usage

Every method on `VaaniClient` has an exact async equivalent on `AsyncVaaniClient`, including all resource namespaces.

```python
import asyncio
from vaani_sdk import AsyncVaaniClient

async def main():
    async with AsyncVaaniClient(api_key="YOUR_API_KEY") as client:
        # Flat methods
        result = await client.trigger_call(
            agent_id="8cf3373e-eb6f-4b4c-9f3c-324a56a91147",
            contact_number="+919876543210",
            name="Nishank",
        )
        print(result.call_id)

        history = await client.get_call_history(page=1, page_size=5)
        for call in history.data:
            print(call.call_id)

        transcript = await client.get_transcript(history.data[0].call_id)
        print(transcript.transcript)

        details = await client.get_call_details(history.data[0].call_id)
        print(details.raw)

        # Stream audio asynchronously
        with open("recording.ogg", "wb") as f:
            async for chunk in client.stream_audio_iter(history.data[0].call_id):
                f.write(chunk)

        # Resource namespaces
        session = await client.sessions.create(agent_id="8cf3373e-eb6f-4b4c-9f3c-324a56a91147")
        creds = await client.sessions.connect(session.session_token)
        print(creds.lk_url, creds.lk_token)

        calls = await client.calls.list(page=1, page_size=10)
        agents = await client.agents.list()

asyncio.run(main())
```

---

## Error Handling

All exceptions inherit from `VaaniError` and carry a `status_code` attribute.

| Exception | HTTP status | Cause |
| --- | --- | --- |
| `AuthenticationError` | 401 | Invalid or missing API key |
| `ForbiddenError` | 403 | Operation not allowed for this key |
| `NotFoundError` | 404 | Resource does not exist |
| `SessionExpiredError` | 410 | WebRTC session has ended or expired |
| `InsufficientBalanceError` | 402 | Wallet balance too low |
| `ValidationError` | 422 | Request parameters failed validation |
| `RateLimitError` | 429 | Too many requests |
| `ServerError` | 5xx | Vaani server-side error |
| `TimeoutError` | --- | Request timed out |
| `ConnectionError` | --- | Could not reach the API |

```python
from vaani_sdk import (
    VaaniClient,
    AuthenticationError,
    ForbiddenError,
    InsufficientBalanceError,
    NotFoundError,
    SessionExpiredError,
    VaaniError,
)

with VaaniClient(api_key="YOUR_API_KEY") as client:
    try:
        session = client.sessions.create(agent_id="my-agent")
        creds = client.sessions.connect(session.session_token)
    except AuthenticationError:
        print("Bad API key — check X-API-Key header value")
    except ForbiddenError:
        print("Agent not owned by this API key")
    except InsufficientBalanceError:
        print("Wallet balance is too low")
    except SessionExpiredError:
        print("Session has already ended")
    except NotFoundError:
        print("Agent not found")
    except VaaniError as exc:
        print(f"API error {exc.status_code}: {exc.message}")
```

---

## Development

```bash
# Install with dev extras
pip install -e ".[dev]"

# Run tests
pytest tests/
```

