# ToothFairyAI Python SDK

Official Python SDK for the ToothFairyAI API - A comprehensive toolkit for building AI-powered applications with chat, document processing, agent management, and more.

**Status**: ✅ Production Ready | **Version**: 0.5.9

[![Tests](https://img.shields.io/badge/tests-passing-brightgreen)](https://github.com/toothfairyai/python-sdk)
[![Python](https://img.shields.io/badge/python-3.8%2B-blue)](https://www.python.org/)
[![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)

```
========================= test session starts ==========================
platform darwin -- Python 3.13.1, pytest-9.0.1

tests/debug/test_agent_functions.py     ✓✓✓✓     4 passed
tests/debug/test_agent_update.py        ✓✓✓✓     4 passed
tests/debug/test_authorisations.py      ✓✓✓✓✓✓✓  7 passed
tests/debug/test_members.py             ✓✓       2 passed
tests/debug/test_secrets.py             ✓        1 passed

========================= 18 passed in 35.2s ===========================
```

## Installation

```bash
pip install toothfairyai
```

## Quick Start

```python
from toothfairyai import ToothFairyClient

# Initialize the client
client = ToothFairyClient(
    api_key="your-api-key",
    workspace_id="your-workspace-id"
)

# Send a message to an agent (non-streaming)
response = client.chat.send_to_agent(
    message="Hello, how can you help me?",
    agent_id="your-agent-id"
)
print(response.agent_response)
```

## Table of Contents

- [SDK Overview](#sdk-overview)
- [Client Configuration](#client-configuration)
- [Core Features](#core-features)
- [Validation Models](#validation-models)
- [Complete API Reference](#complete-api-reference)
  - [Chat & Messaging](#chat--messaging)
  - [Agents](#agents)
  - [Documents](#documents)
  - [Entities](#entities)
  - [Folders](#folders)
  - [Prompts](#prompts)
  - [Agent Functions](#agent-functions)
  - [Authorisations](#authorisations)
  - [Channels](#channels)
  - [Connections](#connections)
  - [Members](#members)
  - [Sites](#sites)
  - [Benchmarks](#benchmarks)
  - [Hooks](#hooks)
  - [Scheduled Jobs](#scheduled-jobs)
  - [Secrets](#secrets)
  - [Dictionary](#dictionary)
  - [Embeddings](#embeddings)
  - [Settings](#settings)
  - [Billing](#billing)
  - [Streams](#streams)
  - [Request Logs](#request-logs)
- [Streaming Responses](#streaming-responses)
- [Error Handling](#error-handling)
- [Best Practices](#best-practices)

## SDK Overview

The ToothFairyAI SDK provides a Pythonic interface to all 95 API endpoints, organized into 18 specialized managers:

| Manager | Purpose | Endpoints |
|---------|---------|-----------|
| `client.chat` | Chat conversations & messages | 7 |
| `client.agents` | AI agent management | 5 |
| `client.documents` | Document upload & management | 6 |
| `client.entities` | Topics, intents, NER entities | 5 |
| `client.folders` | Content organization | 5 |
| `client.prompts` | Prompt templates | 5 |
| `client.agent_functions` | Agent function calls | 5 |
| `client.authorisations` | Auth configurations | 5 |
| `client.channels` | Communication channels | 5 |
| `client.connections` | Database/API connections | 3 |
| `client.members` | Workspace members | 4 |
| `client.sites` | Website crawling | 4 |
| `client.benchmarks` | Performance testing | 5 |
| `client.hooks` | Custom code execution | 5 |
| `client.scheduled_jobs` | Cron jobs & scheduling | 5 |
| `client.secrets` | Secret management | 2 |
| `client.dictionary` | Translation dictionary | 2 |
| `client.embeddings` | Document embeddings | 1 |
| `client.charting_settings` | Charting configuration | 2 |
| `client.embeddings_settings` | Embeddings configuration | 2 |
| `client.billing` | Usage & costs | 1 |
| `client.streams` | Output streams | 2 |
| `client.request_logs` | Request logging | 2 |
| `client.streaming` | Real-time streaming | - |

## Client Configuration

```python
from toothfairyai import ToothFairyClient

client = ToothFairyClient(
    api_key="your-api-key",           # Required: Your API key
    workspace_id="your-workspace-id", # Required: Your workspace ID
    base_url="https://api.toothfairyai.com",  # Optional: API base URL
    ai_url="https://ai.toothfairyai.com",     # Optional: AI endpoint URL
    ai_stream_url="https://ais.toothfairyai.com",  # Optional: Streaming URL
    timeout=120  # Optional: Request timeout in seconds (default: 120)
)
```

### Getting Your Credentials

1. **API Key**: Log in to ToothFairyAI Admin → API Integration → Generate API Key
2. **Workspace ID**: Found in your workspace settings or API dashboard

## Core Features

### 1. Chat & Messaging
Create conversations, send messages, and manage chat history with AI agents.

### 2. Agent Management
Create, configure, and manage AI agents with different modes (retriever, chatter, planner, etc.).

### 3. Document Processing
Upload, search, and manage documents for knowledge base integration.

### 4. Entity Management
Organize content with topics, intents, and named entity recognition.

### 5. Streaming
Real-time streaming responses with an iterator pattern similar to OpenAI's SDK.

### 6. Validation Models (NEW)
Pre-validate API payloads before sending requests using Pydantic models. Catch errors early and get clear error messages.

## Complete API Reference

### Chat & Messaging

#### Create a Chat Session

```python
# Create a new chat
chat = client.chat.create(
    name="Customer Support",
    customer_id="customer-123",
    customer_info={"name": "John Doe", "email": "john@example.com"},
    primary_role="user",
    external_participant_id="john@example.com",
    channel_settings={
        "sms": {"isEnabled": True, "recipient": "+1234567890"},
        "email": {"isEnabled": True, "recipient": "john@example.com"}
    }
)
print(f"Chat ID: {chat.id}")
```

#### Update a Chat

```python
chat = client.chat.update(
    chat_id="chat-id",
    name="Updated Chat Name",
    customer_id="new-customer-id"
)
```

#### Get Chat Details

```python
chat = client.chat.get(chat_id="chat-id")
print(f"Chat: {chat.name}")
print(f"Primary Role: {chat.primary_role}")
```

#### List Chats

```python
chats = client.chat.list(limit=10, offset=0)
for chat in chats.items:
    print(f"{chat.name} (ID: {chat.id})")
```

#### Delete a Chat

```python
client.chat.delete(chat_id="chat-id")
```

#### Create a Message

```python
message = client.chat.create_message(
    chat_id="chat-id",
    text="Hello, I need help with my account",
    role="user",
    user_id="user-123",
    images=["https://example.com/image.jpg"],
    files=["https://example.com/document.pdf"]
)
```

#### Update a Message

```python
message = client.chat.update_message(
    message_id="message-id",
    text="Updated message text",
    role="user"
)
```

#### Get a Message

```python
message = client.chat.get_message(message_id="message-id")
print(f"Message: {message.text}")
```

#### List Messages in a Chat

```python
messages = client.chat.list_messages(
    chat_id="chat-id",
    limit=20,
    offset=0
)
for msg in messages.items:
    print(f"{msg.role}: {msg.text}")
```

#### Send Message to Agent (Non-Streaming)

```python
response = client.chat.send_to_agent(
    message="Hello, how can you help me?",
    agent_id="agent-id",
    chat_id="existing-chat-id",  # Optional - creates new chat if not provided
    phone_number="+1234567890",  # Optional
    customer_id="customer-123",   # Optional
    provider_id="provider-id",    # Optional
    customer_info={"name": "John"},  # Optional
    attachments={
        "images": ["https://example.com/image.jpg"],
        "files": ["https://example.com/document.pdf"],
        "audios": ["https://example.com/audio.mp3"],
        "videos": ["https://example.com/video.mp4"]
    }
)
print(f"Response: {response.agent_response}")
print(f"Chat ID: {response.chat_id}")
print(f"Message ID: {response.message_id}")
```

### Agents

#### Create an Agent

```python
agent = client.agents.create(
    label="Customer Support Agent",
    mode="retriever",  # retriever, coder, chatter, planner, computer, voice
    interpolation_string="You are a helpful customer support assistant...",
    goals="Help customers resolve issues and answer questions",
    temperature=0.7,
    max_tokens=2000,
    max_history=10,
    top_k=10,
    doc_top_k=5,
    description="Specialized agent for customer support",
    has_memory=True,
    show_citations=True,
    allowed_topics=["topic-1", "topic-2"],
    static_docs=["doc-1", "doc-2"]
)
print(f"Agent ID: {agent.id}")
```

#### Create an Agent with Structured JSON Output

```python
# Define JSON schema for structured responses
json_schema = {
    "type": "object",
    "properties": {
        "summary": {"type": "string"},
        "key_points": {"type": "array", "items": {"type": "string"}},
        "sentiment": {"type": "string", "enum": ["positive", "neutral", "negative"]}
    },
    "required": ["summary", "key_points", "sentiment"]
}

agent = client.agents.create(
    label="Analysis Agent",
    mode="retriever",  # works with retriever or chatter mode
    interpolation_string="You are an assistant that returns structured JSON output.",
    goals="Analyze text and return structured analysis.",
    temperature=0.3,
    max_tokens=1000,
    max_history=5,
    top_k=5,
    doc_top_k=3,
    enable_json_output=True,
    json_output_structure=json_schema
)
# Agent will always respond with JSON matching the schema
```

#### Update an Agent

```python
agent = client.agents.update(
    agent_id="agent-id",
    label="Updated Agent Name",
    temperature=0.8,
    max_tokens=3000,
    description="Updated description",
    charting=False  # Required field, defaults to False
)
```

#### Get Agent Details

```python
agent = client.agents.get(agent_id="agent-id")
print(f"Agent: {agent.label}")
print(f"Mode: {agent.mode}")
print(f"Goals: {agent.goals}")
```

#### List Agents

```python
agents = client.agents.list(limit=10, offset=0)
for agent in agents.items:
    print(f"{agent.label} ({agent.mode})")
```

#### Delete an Agent

```python
client.agents.delete(agent_id="agent-id")
```

### Documents

#### Upload a Document

```python
# Upload with progress callback
result = client.documents.upload(
    file_path="./document.pdf",
    folder_id="folder-id",
    on_progress=lambda percent, loaded, total: print(f"Progress: {percent}%")
)
print(f"Uploaded: {result.filename}")
print(f"Size: {result.size_in_mb:.2f} MB")
```

#### Upload from Base64

```python
import base64

with open("document.pdf", "rb") as f:
    base64_data = base64.b64encode(f.read()).decode()

result = client.documents.upload_from_base64(
    base64_data=base64_data,
    filename="document.pdf",
    content_type="application/pdf",
    folder_id="folder-id"
)
```

#### Create Document from URL

```python
doc = client.documents.create_from_path(
    file_path="https://example.com/document.pdf",
    user_id="user-123",
    title="External Document",
    folder_id="folder-id",
    topics=["topic-1", "topic-2"],
    status="published"
)
```

#### Create Document Record

```python
doc = client.documents.create(
    user_id="user-123",
    title="My Document",
    doc_type="readComprehensionFile",  # readComprehensionUrl, readComprehensionPdf
    topics=["topic-1"],
    folder_id="folder-id",
    external_path="https://example.com/doc.pdf",
    source="external",
    status="published",
    scope="training"
)
```

#### Get Document

```python
doc = client.documents.get(document_id="doc-id")
print(f"Title: {doc.title}")
print(f"Type: {doc.doc_type}")
print(f"Status: {doc.status}")
```

#### Update Document

```python
doc = client.documents.update(
    document_id="doc-id",
    user_id="user-123",
    title="Updated Title",
    topics=["new-topic"],
    folder_id="new-folder-id",
    status="published"
)
```

#### List Documents

```python
docs = client.documents.list(
    limit=20,
    offset=0,
    folder_id="folder-id",  # Optional filter
    status="published"      # Optional filter
)
for doc in docs.items:
    print(f"{doc.title} (ID: {doc.id})")
```

#### Delete Document

```python
client.documents.delete(document_id="doc-id")
```

#### Search Documents

```python
results = client.documents.search(
    text="product features",
    top_k=10,
    metadata={"category": "procedures"}
)
for result in results:
    print(f"Score: {result.get('score')}")
    print(f"Content: {result.get('content')}")
```

#### Download Document

```python
result = client.documents.download(
    filename="document.pdf",
    output_path="./downloads/document.pdf",
    context="documents",
    on_progress=lambda p, l, t: print(f"Download: {p}%")
)
print(f"Downloaded: {result.filename}")
```

### Entities

#### Create an Entity

```python
entity = client.entities.create(
    user_id="user-123",
    label="Product Support",
    entity_type="topic",  # topic, intent, ner
    description="Customer product support inquiries",
    emoji="💡",
    parent_entity="parent-topic-id",
    background_color="#FF5733"
)
```

#### Get Entity

```python
entity = client.entities.get(entity_id="entity-id")
print(f"Label: {entity.label}")
print(f"Type: {entity.entity_type}")
```

#### Update Entity

```python
entity = client.entities.update(
    entity_id="entity-id",
    label="Updated Label",
    description="New description",
    emoji="🔧"
)
```

#### Delete Entity

```python
client.entities.delete(entity_id="entity-id")
```

#### List Entities

```python
entities = client.entities.list(limit=20, entity_type="topic")
for entity in entities.items:
    print(f"{entity.label} ({entity.entity_type})")
```

#### Get Entities by Type

```python
topics = client.entities.get_by_type("topic")
intents = client.entities.get_by_type("intent")
ner_entities = client.entities.get_by_type("ner")
```

#### Search Entities

```python
results = client.entities.search("cleaning", entity_type="topic")
for entity in results:
    print(f"{entity.label}")
```

### Folders

#### Create a Folder

```python
folder = client.folders.create(
    user_id="user-123",
    name="Procedures",
    description="Medical procedures documentation",
    emoji="📁",
    status="active",
    parent="parent-folder-id"  # Optional - for subfolders
)
```

#### Get Folder

```python
folder = client.folders.get(folder_id="folder-id")
print(f"Name: {folder.name}")
print(f"Parent: {folder.parent}")
```

#### Update Folder

```python
folder = client.folders.update(
    folder_id="folder-id",
    name="Updated Name",
    description="New description",
    status="active"
)
```

#### Delete Folder

```python
client.folders.delete(folder_id="folder-id")
```

#### List Folders

```python
folders = client.folders.list(
    status="active",
    limit=20,
    offset=0
)
for folder in folders.items:
    print(f"{folder.name}")
```

#### Get Root Folders

```python
root_folders = client.folders.get_root_folders()
for folder in root_folders:
    print(f"Root: {folder.name}")
```

#### Get Subfolders

```python
subfolders = client.folders.get_subfolders(parent_id="folder-id")
for folder in subfolders:
    print(f"Subfolder: {folder.name}")
```

#### Get Folder Tree

```python
tree = client.folders.get_tree()
def print_tree(nodes, level=0):
    for node in nodes:
        print("  " * level + f"📁 {node.name}")
        print_tree(node.children, level + 1)

print_tree(tree)
```

#### Search Folders

```python
results = client.folders.search("procedures")
for folder in results:
    print(f"{folder.name}")
```

### Prompts

#### Create a Prompt

```python
prompt = client.prompts.create(
    user_id="user-123",
    label="Greeting",
    prompt_type="greeting",
    interpolation_string="Hello {{name}}, how can I help you today?",
    scope="global",
    style="friendly",
    domain="customer-support",
    prompt_placeholder="Enter greeting message",
    available_to_agents=["agent-1", "agent-2"]
)
```

#### Get Prompt

```python
prompt = client.prompts.get(prompt_id="prompt-id")
print(f"Label: {prompt.label}")
print(f"Template: {prompt.interpolation_string}")
```

#### Update Prompt

```python
prompt = client.prompts.update(
    prompt_id="prompt-id",
    label="Updated Label",
    interpolation_string="New template string",
    available_to_agents=["agent-3"]
)
```

#### Delete Prompt

```python
client.prompts.delete(prompt_id="prompt-id")
```

#### List Prompts

```python
prompts = client.prompts.list(
    prompt_type="greeting",
    limit=20,
    offset=0
)
for prompt in prompts.items:
    print(f"{prompt.label}")
```

#### Get Prompts by Type

```python
greetings = client.prompts.get_by_type("greeting")
```

#### Get Prompts by Agent

```python
agent_prompts = client.prompts.get_by_agent("agent-id")
```

#### Get Prompts by Scope

```python
global_prompts = client.prompts.get_by_scope("global")
```

#### Search Prompts

```python
results = client.prompts.search("greeting", prompt_type="greeting")
```

#### Clone Prompt

```python
cloned = client.prompts.clone(
    prompt_id="prompt-id",
    user_id="user-123",
    label="Cloned Greeting",
    interpolation_string="Modified template"
)
```

### Agent Functions

#### Create an Agent Function

```python
# Create a simple GET function
function = client.agent_functions.create(
    name="Weather API",
    description="Get current weather data for a location",  # Required
    url="https://api.weather.com/current",
    request_type="GET",
    authorisation_type="bearer",
    authorisation_key="your-token",
    parameters=[
        {"name": "location", "type": "string", "required": True}
    ]
)
print(f"Function ID: {function.id}")

# Create a POST function with headers
function = client.agent_functions.create(
    name="Create Order",
    description="Create a new order in the system",  # Required
    url="https://api.example.com/orders",
    request_type="POST",
    authorisation_type="apikey",
    headers=[{"key": "Content-Type", "value": "application/json"}],
    static_args=[{"key": "source", "value": "chatbot"}]
)
```

#### Update Agent Function

```python
function = client.agent_functions.update(
    agent_function_id="function-id",
    name="Updated Function Name",
    url="https://new-url.com"
)
```

#### Delete Agent Function

```python
client.agent_functions.delete(agent_function_id="function-id")
```

#### Get Agent Function

```python
function = client.agent_functions.get(agent_function_id="function-id")
print(f"Name: {function.name}")
print(f"URL: {function.url}")
```

#### List Agent Functions

```python
functions = client.agent_functions.list(limit=20)
for func in functions.items:
    print(f"{func.name}")

# Search by name
results = client.agent_functions.search("weather")
```

### Authorisations

#### Create an Authorisation

```python
# API Key authorisation
auth = client.authorisations.create(
    name="External API Key",
    auth_type="apikey",  # apikey, bearer, oauth, basic, none, username_and_password, env
    description="API key for external service",
    token_secret="your-api-key-value"
)
print(f"Auth ID: {auth.id}")

# Bearer token authorisation
auth = client.authorisations.create(
    name="Service Token",
    auth_type="bearer",
    token_secret="your-bearer-token"
)

# OAuth authorisation
auth = client.authorisations.create(
    name="GitHub OAuth",
    auth_type="oauth",
    description="GitHub OAuth for API access",
    scope="repo,user",
    grant_type="authorization_code",
    client_id="github-client-id",
    client_secret="encrypted-secret",
    authorization_base_url="https://github.com/login/oauth/authorize",
    token_base_url="https://github.com/login/oauth/access_token"
)

# Basic auth
auth = client.authorisations.create(
    name="Basic Auth",
    auth_type="basic",
    description="Basic authentication credentials"
)
```

#### Update Authorisation

```python
# Note: auth_type cannot be changed after creation
auth = client.authorisations.update(
    authorisation_id="auth-id",
    name="Updated Auth Name",
    description="New description"
)
```

#### Delete Authorisation

```python
client.authorisations.delete(authorisation_id="auth-id")
```

#### Get Authorisation

```python
auth = client.authorisations.get(authorisation_id="auth-id")
print(f"Name: {auth.name}")
print(f"Type: {auth.type}")
```

#### List Authorisations

```python
auths = client.authorisations.list(limit=20)
for auth in auths.items:
    print(f"{auth.name} ({auth.type})")

# Get authorisations by type
api_keys = client.authorisations.get_by_type("apikey")
oauth_auths = client.authorisations.get_by_type("oauth")

# Search by name
results = client.authorisations.search("github")
```

### Channels

#### Create a Channel

```python
channel = client.channels.create(
    name="SMS Channel",
    channel="sms",
    provider="twilio",
    senderid="+1234567890",
    description="SMS notifications via Twilio",
    is_active=True
)
print(f"Channel ID: {channel.id}")
```

#### Update Channel

```python
channel = client.channels.update(
    channel_id="channel-id",
    name="Updated Channel",
    is_active=False
)
```

#### Delete Channel

```python
client.channels.delete(channel_id="channel-id")
```

#### Get Channel

```python
channel = client.channels.get(channel_id="channel-id")
print(f"Name: {channel.name}")
print(f"Provider: {channel.provider}")
```

#### List Channels

```python
channels = client.channels.list(limit=20)
for channel in channels.items:
    print(f"{channel.name} ({channel.provider})")
```

### Connections

#### Get Connection

```python
connection = client.connections.get(connection_id="connection-id")
print(f"Name: {connection.name}")
print(f"Type: {connection.type}")
print(f"Host: {connection.host}")
```

#### List Connections

```python
connections = client.connections.list(limit=20)
for conn in connections.items:
    print(f"{conn.name} ({conn.type})")
```

#### Delete Connection

```python
client.connections.delete(connection_id="connection-id")
```

### Members

#### Get Member

```python
member = client.members.get(member_id="member-id")
print(f"User ID: {member.user_id}")
print(f"Role: {member.role}")
print(f"Status: {member.status}")
```

#### List Members

```python
members = client.members.list(limit=20)
for member in members.items:
    print(f"{member.user_id} ({member.role})")
```

#### Update Member

```python
member = client.members.update(
    member_id="member-id",
    role="admin",
    status="active"
)
```

#### Delete Member

```python
client.members.delete(member_id="member-id")
```

### Sites

#### Get Site

```python
site = client.sites.get(site_id="site-id")
print(f"Name: {site.name}")
print(f"URL: {site.url}")
print(f"Status: {site.status}")
```

#### List Sites

```python
sites = client.sites.list(limit=20)
for site in sites.items:
    print(f"{site.name} - {site.url}")
```

#### Update Site

```python
site = client.sites.update(
    site_id="site-id",
    name="Updated Site",
    status="active",
    allowed_paths=["/docs", "/blog"]
)
```

#### Delete Site

```python
client.sites.delete(site_id="site-id")
```

### Benchmarks

#### Create a Benchmark

```python
benchmark = client.benchmarks.create(
    name="Customer Support Test",
    description="Test agent performance on customer support queries",
    questions=[
        {
            "question": "How do I reset my password?",
            "answer": "Go to Settings > Security > Reset Password",
            "reasoning": "Standard password reset flow",
            "context": "Account management"
        }
    ],
    files=["doc-1", "doc-2"]
)
print(f"Benchmark ID: {benchmark.id}")
```

#### Update Benchmark

```python
benchmark = client.benchmarks.update(
    benchmark_id="benchmark-id",
    name="Updated Benchmark",
    description="New description"
)
```

#### Delete Benchmark

```python
client.benchmarks.delete(benchmark_id="benchmark-id")
```

#### Get Benchmark

```python
benchmark = client.benchmarks.get(benchmark_id="benchmark-id")
print(f"Name: {benchmark.name}")
print(f"Questions: {len(benchmark.questions)}")
```

#### List Benchmarks

```python
benchmarks = client.benchmarks.list(limit=20)
for benchmark in benchmarks.items:
    print(f"{benchmark.name}")
```

### Hooks

#### Create a Hook

```python
hook = client.hooks.create(
    name="Data Processing Hook",
    function_name="process_data",
    custom_execution_code="def process_data(data): return data.upper()",
    custom_execution_instructions="Process incoming data",
    available_libraries="pandas,numpy",
    allow_external_api=True,
    hardcoded_script=False
)
print(f"Hook ID: {hook.id}")
```

#### Update Hook

```python
hook = client.hooks.update(
    hook_id="hook-id",
    name="Updated Hook",
    custom_execution_code="def process_data(data): return data.lower()"
)
```

#### Delete Hook

```python
client.hooks.delete(hook_id="hook-id")
```

#### Get Hook

```python
hook = client.hooks.get(hook_id="hook-id")
print(f"Name: {hook.name}")
print(f"Function: {hook.function_name}")
```

#### List Hooks

```python
hooks = client.hooks.list(limit=20)
for hook in hooks.items:
    print(f"{hook.name}")
```

### Scheduled Jobs

#### Create a Scheduled Job

```python
job = client.scheduled_jobs.create(
    name="Daily Report",
    description="Generate daily report at 9 AM",
    agent_id="agent-id",
    custom_prompt_id="prompt-id",
    forced_prompt="Generate a daily summary report",
    schedule={
        "frequency": "DAILY",
        "hour": 9,
        "minute": 0
    },
    timezone="UTC",
    is_active=True,
    status="ACTIVE"
)
print(f"Job ID: {job.id}")
```

#### Update Scheduled Job

```python
job = client.scheduled_jobs.update(
    scheduled_job_id="job-id",
    name="Updated Job",
    is_active=False,
    schedule={"frequency": "WEEKLY", "dayOfWeek": 1, "hour": 9}
)
```

#### Delete Scheduled Job

```python
client.scheduled_jobs.delete(scheduled_job_id="job-id")
```

#### Get Scheduled Job

```python
job = client.scheduled_jobs.get(scheduled_job_id="job-id")
print(f"Name: {job.name}")
print(f"Status: {job.status}")
print(f"Schedule: {job.schedule}")
```

#### List Scheduled Jobs

```python
jobs = client.scheduled_jobs.list(limit=20)
for job in jobs.items:
    print(f"{job.name} ({job.status})")
```

### Secrets

Secrets are linked to authorisations. You must first create an authorisation, then create a secret for it.

#### Create a Secret

```python
# First, create an authorisation
auth = client.authorisations.create(
    name="External Service Auth",
    auth_type="apikey",
    description="Authorisation for external service"
)

# Then create a secret linked to that authorisation
secret = client.secrets.create(
    authorisation_id=auth.id,
    password_secret_value="your-secret-api-key-12345"
)
print(f"Secret created: {secret.name}")
```

#### Delete Secret

```python
client.secrets.delete(secret_id="secret-id")
```

### Dictionary

#### Get Dictionary Entry

```python
entry = client.dictionary.get(entry_id="entry-id")
print(f"Source: {entry.source_text}")
print(f"Target: {entry.target_text}")
print(f"Languages: {entry.source_language} -> {entry.target_language}")
```

#### List Dictionary Entries

```python
entries = client.dictionary.list(limit=20)
for entry in entries.items:
    print(f"{entry.source_language} -> {entry.target_language}")
```

### Embeddings

#### Get Document Embeddings

```python
embeddings = client.embeddings.get()
for emb in embeddings:
    print(f"Document: {emb.title}")
    print(f"Chunk: {emb.chunk_id}")
    print(f"Type: {emb.type}")
```

### Settings

#### Charting Settings

```python
# Get charting settings
settings = client.charting_settings.get()
print(f"Primary Color: {settings.primary_color}")
print(f"Secondary Color: {settings.secondary_color}")

# Update charting settings
settings = client.charting_settings.update(
    primary_color="#FF5733",
    secondary_color="#33FF57",
    line_color="#3357FF"
)
```

#### Embeddings Settings

```python
# Get embeddings settings
settings = client.embeddings_settings.get()
print(f"Max Chunk Words: {settings.max_chunk_words}")
print(f"Chunking Strategy: {settings.chunking_strategy}")

# Update embeddings settings
settings = client.embeddings_settings.update(
    max_chunk_words=1000,
    chunking_strategy="summary",
    image_extraction_strategy="safe"
)
```

### Billing

#### Get Monthly Costs

```python
costs = client.billing.get()
print(f"API Usage: {costs.api_usage}")
print(f"Training Usage: {costs.training_usage}")
print(f"Total Cost: ${costs.api_usage.get('totalCostUSD', 0):.2f}")
```

### Streams

#### Get Output Stream

```python
stream = client.streams.get(stream_id="stream-id")
print(f"Content: {stream.content}")
print(f"Type: {stream.type}")
print(f"Status: {stream.status}")
```

#### List Output Streams

```python
streams = client.streams.list(limit=20)
for stream in streams.items:
    print(f"{stream.type} - {stream.status}")
```

### Request Logs

#### Get Request Log

```python
log = client.request_logs.get(log_id="log-id")
print(f"Type: {log.type}")
print(f"Status: {log.status}")
print(f"Tokens: {log.tokens}")
print(f"Words: {log.words}")
```

#### List Request Logs

```python
logs = client.request_logs.list(limit=20)
for log in logs.items:
    print(f"{log.type} - {log.status} ({log.tokens} tokens)")
```

## Streaming Responses

Stream responses with an iterator pattern, similar to OpenAI's Python SDK:

### Simple Streaming

```python
stream = client.streaming.send_to_agent(
    message="Tell me about your services",
    agent_id="your-agent-id"
)

for event in stream:
    print(event.text, end="", flush=True)

print()
print(f"Chat ID: {stream.chat_id}")
print(f"Message ID: {stream.message_id}")
```

### Streaming with Event Types

```python
stream = client.streaming.send_to_agent("Hello", "agent-id")

for event in stream:
    if event.is_token:
        # Token events contain streaming text
        print(event.text, end="", flush=True)
    elif event.is_complete:
        # Stream is complete
        print("\nDone!")
    elif event.is_error:
        # Handle errors
        print(f"Error: {event.data}")
    elif event.is_status:
        # Status updates
        print(f"Status: {event.data}")
```

### Collect Full Response

```python
stream = client.streaming.send_to_agent("Hello", "agent-id")
full_response = stream.collect()  # Blocks until complete
print(full_response)
```

### Continue a Conversation

```python
# First message creates a new chat
stream1 = client.streaming.send_to_agent("Hello", "agent-id")
for event in stream1:
    print(event.text, end="")

# Continue in the same chat
stream2 = client.streaming.send_to_agent(
    message="Tell me more",
    agent_id="agent-id",
    chat_id=stream1.chat_id  # Use the chat ID from first stream
)
for event in stream2:
    print(event.text, end="")
```

### Streaming with Attachments

```python
stream = client.streaming.send_to_agent(
    message="What's in this image?",
    agent_id="agent-id",
    attachments={
        "images": ["https://example.com/image.jpg"]
    }
)

for event in stream:
    print(event.text, end="")
```

## Error Handling

```python
from toothfairyai import ToothFairyClient, ToothFairyError

client = ToothFairyClient(api_key="...", workspace_id="...")

try:
    response = client.chat.send_to_agent("Hello", "agent-id")
except ToothFairyError as e:
    print(f"Error: {e.message}")
    print(f"Code: {e.code}")
    print(f"Status: {e.status_code}")
    if e.response:
        print(f"Response: {e.response}")
```

### Specific Error Types

```python
from toothfairyai import (
    ToothFairyError,
    ApiError,
    NetworkError,
    MissingApiKeyError,
    MissingWorkspaceIdError,
    ValidationError,
    FileSizeError
)

try:
    client = ToothFairyClient(api_key="", workspace_id="")
except MissingApiKeyError:
    print("API key is required")
except MissingWorkspaceIdError:
    print("Workspace ID is required")

try:
    result = client.documents.upload("large_file.pdf")
except FileSizeError as e:
    print(f"File too large: {e.size_mb:.2f}MB (max: {e.max_size_mb}MB)")
except ValidationError as e:
    print(f"Validation error: {e.message}")
except NetworkError as e:
    print(f"Network error: {e.message}")
except ApiError as e:
    print(f"API error: {e.message} (status: {e.status_code})")
```

## Connection Testing

```python
# Test connection
is_connected = client.test_connection()
print(f"Connected: {is_connected}")

# Get health status
health = client.get_health()
print(f"Status: {health.get('status', 'unknown')}")
```

## Validation Models

The SDK includes optional Pydantic validation models to catch payload errors before making API requests. This is especially useful when integrating with AI agents (MCP tools) that need to construct payloads.

### Why Use Validation Models?

- **Catch errors early**: Validate before sending to API
- **Clear error messages**: Know exactly what's wrong with your payload
- **Required fields**: Never forget required fields like `interpolation_string` (min 128 chars) for agents
- **Type safety**: Ensure correct types for all fields
- **Enum validation**: Prevent invalid enum values (e.g., `CronJobFrequency.DAILY` not `daily`)

### Basic Usage

```python
from toothfairyai import ToothFairyClient
from toothfairyai.models import AgentCreateModel, AgentFunctionCreateModel

client = ToothFairyClient(api_key="...", workspace_id="...")

# Validate before creating
agent_data = AgentCreateModel(
    label="Support Agent",
    mode="retriever",
    interpolation_string="You are a helpful customer support assistant. " * 10,  # Min 128 chars
    goals="Help customers resolve issues",
    temperature=0.7
)

# Convert to dict and pass to SDK
agent = client.agents.create(**agent_data.model_dump(exclude_none=True))
```

### Validation with Error Handling

```python
from toothfairyai.models import ScheduledJobCreateModel, ValidationError as ModelValidationError
from pydantic import ValidationError as PydanticValidationError

try:
    job_data = ScheduledJobCreateModel(
        name="Daily Report",
        agent_id="agent-123",
        custom_prompt_id="prompt-456",  # REQUIRED for scheduled jobs
        schedule={"frequency": "DAILY", "hour": 9, "minute": 0}
    )
except PydanticValidationError as e:
    print(f"Validation failed: {e}")
    # Shows exactly which fields are missing or invalid
```

### Available Models

| Model | Use Case | Key Requirements |
|-------|----------|------------------|
| `AgentCreateModel` | Create agents | `interpolation_string` ≥ 128 chars |
| `AgentUpdateModel` | Update agents | `charting` field required |
| `AgentFunctionCreateModel` | Create functions | `description` required |
| `ScheduledJobCreateModel` | Create scheduled jobs | `custom_prompt_id` required |
| `DocumentCreateModel` | Create documents | `user_id` required |
| `ConnectionCreateModel` | Create connections | `name` required |
| `FolderCreateModel` | Create folders | Uses `name` (not `label`) |
| `EntityCreateModel` | Create entities | `background_color` hex format |
| `MemberCreateModel` | Update members | Uses `user_id` (not `email`) |
| `PromptCreateModel` | Create prompts | `interpolation_string` required |
| `AuthorisationCreateModel` | Create auths | `auth_type` required |
| `HookCreateModel` | Create hooks | `function_name` required |
| `BenchmarkCreateModel` | Create benchmarks | `questions` list required |
| `ChatCreateModel` | Create chats | `name` required |
| `ChannelCreateModel` | Create channels | `channel`, `provider` required |
| `SecretCreateModel` | Create secrets | `authorisation_id`, `password_secret_value` required |

### Enum Types

```python
from toothfairyai.models import CronJobFrequency, RequestType, AuthType

# CronJobFrequency uses UPPERCASE values
schedule = {"frequency": CronJobFrequency.DAILY, "hour": 9}

# RequestType for agent functions
function = AgentFunctionCreateModel(
    name="API Call",
    description="Fetch data from API",
    url="https://api.example.com",
    request_type=RequestType.GET  # Optional, defaults to None
)

# AuthType for authorisations
auth = AuthorisationCreateModel(
    name="API Key Auth",
    auth_type=AuthType.APIKEY
)
```

### MCP Tool Integration

When building MCP tools or AI agents that create entities, use validation models to prevent payload errors:

```python
def create_agent_tool(label: str, mode: str, interpolation_string: str, **kwargs):
    """MCP tool to create an agent with validation."""
    from toothfairyai.models import AgentCreateModel
    from pydantic import ValidationError
    
    try:
        # Validate the payload
        agent_data = AgentCreateModel(
            label=label,
            mode=mode,
            interpolation_string=interpolation_string,
            **kwargs
        )
        
        # If validation passes, create the agent
        client = get_client()
        return client.agents.create(**agent_data.model_dump(exclude_none=True))
        
    except ValidationError as e:
        # Return clear error message to AI
        return {"error": str(e), "validation_failed": True}
```

### Integration with Existing Code

Validation models are **optional** and **non-breaking**. Existing code continues to work:

```python
# Old code still works
agent = client.agents.create(
    label="Agent",
    mode="retriever",
    interpolation_string="..."  # Still works
)

# New code with validation
agent_data = AgentCreateModel(label="Agent", mode="retriever", interpolation_string="...")
agent = client.agents.create(**agent_data.model_dump(exclude_none=True))
```

## Best Practices

### 1. Use Environment Variables for Credentials

```python
import os
from toothfairyai import ToothFairyClient

client = ToothFairyClient(
    api_key=os.getenv("TOOTHFAIRY_API_KEY"),
    workspace_id=os.getenv("TOOTHFAIRY_WORKSPACE_ID")
)
```

### 2. Handle Pagination

```python
def get_all_chats():
    all_chats = []
    offset = 0
    limit = 100

    while True:
        response = client.chat.list(limit=limit, offset=offset)
        all_chats.extend(response.items)
        
        if len(response.items) < limit:
            break
        
        offset += limit
    
    return all_chats
```

### 3. Use Streaming for Long Responses

```python
# For long responses, use streaming to show progress
stream = client.streaming.send_to_agent(
    message="Generate a comprehensive report",
    agent_id="agent-id"
)

for event in stream:
    if event.is_token:
        print(event.text, end="", flush=True)
```

### 4. Implement Retry Logic

```python
import time
from toothfairyai import NetworkError

def send_with_retry(message, agent_id, max_retries=3):
    for attempt in range(max_retries):
        try:
            return client.chat.send_to_agent(message, agent_id)
        except NetworkError as e:
            if attempt < max_retries - 1:
                time.sleep(2 ** attempt)  # Exponential backoff
            else:
                raise
```

### 5. Use Chat IDs for Conversations

```python
# Create a chat once
chat = client.chat.create(name="Support Session")

# Use the same chat ID for all messages in the conversation
response1 = client.chat.send_to_agent("Hello", agent_id, chat_id=chat.id)
response2 = client.chat.send_to_agent("Tell me more", agent_id, chat_id=chat.id)
```

### 6. Organize Documents with Folders

```python
# Create folder structure
procedures = client.folders.create(user_id="user-123", name="Procedures")
policies = client.folders.create(user_id="user-123", name="Policies")

# Upload documents to appropriate folders
client.documents.upload("procedure1.pdf", folder_id=procedures.id)
client.documents.upload("policy1.pdf", folder_id=policies.id)
```

### 7. Use Topics for Content Organization

```python
# Create topics
support_topic = client.entities.create(
    user_id="user-123",
    label="FAQ Support",
    entity_type="topic"
)

# Associate documents with topics
doc = client.documents.create_from_path(
    file_path="user_guide.pdf",
    user_id="user-123",
    topics=[support_topic.id]
)
```

### 8. Monitor Usage with Billing

```python
# Check monthly usage
costs = client.billing.get()
api_cost = costs.api_usage.get('totalCostUSD', 0)
training_cost = costs.training_usage.get('totalCharge', 0)

print(f"API Cost: ${api_cost:.2f}")
print(f"Training Cost: ${training_cost:.2f}")
print(f"Total: ${api_cost + training_cost:.2f}")
```

### 9. Use Scheduled Jobs for Automation

```python
# Create a daily report job
job = client.scheduled_jobs.create(
    id="daily-report",
    name="Daily Summary Report",
    agent_id="report-agent-id",
    schedule={
        "frequency": "DAILY",
        "hour": 9,
        "minute": 0
    },
    timezone="UTC",
    is_active=True
)
```

### 10. Secure Secrets Management

```python
# Create an authorisation for the external service
auth = client.authorisations.create(
    name="External Service Auth",
    auth_type="apikey",
    description="API key for external service integration"
)

# Store the actual secret value
secret = client.secrets.create(
    authorisation_id=auth.id,
    password_secret_value="your-actual-api-key"
)

# Reference the authorisation in agent functions
function = client.agent_functions.create(
    name="External API Call",
    description="Call external API with stored credentials",
    url="https://api.example.com/data",
    request_type="GET",
    authorisation_type="apikey",
    authorisation_key=auth.id  # Reference the authorisation
)
```

## License

MIT License - see [LICENSE](LICENSE) for details.

## Support

- **Documentation**: [https://docs.toothfairyai.com](https://docs.toothfairyai.com)
- **Issues**: [GitHub Issues](https://github.com/toothfairyai/python-sdk/issues)
- **Email**: support@toothfairyai.com

## Changelog

See [CHANGELOG.md](CHANGELOG.md) for version history and updates.