AI & Chat Completions
Chat completions, RAG product assistants, voice integration, and podcast generation.
SmartLinks AI
Build AI-powered SmartLinks experiences with a practical SDK guide for responses, chat, product assistants, streaming, voice, and real-world integration patterns.
Table of Contents
- Overview
- Quick Start
- Authentication
- Responses API
- Chat Completions
- RAG: Product Assistants
- Voice Integration
- Podcast Generation
- Type Definitions
- API Reference
- Usage Examples
- Error Handling
- Rate Limiting
- Best Practices
- Providing Content to the AI Assistant
Overview
This guide is written for SDK users building real products, not backend operators. It focuses on the public SDK surface, recommended starting points, and examples you can adapt directly.
Start with the path that matches your job
| If you want to... | Start here |
|---|---|
| Build a new AI workflow | Use Responses API |
| Add compatibility with existing chat clients | Use Chat Completions |
| Build a product/manual assistant | Use RAG: Product Assistants |
| Add spoken input/output | Use Voice Integration |
| Add progressive rendering | Use Streaming Responses or Streaming Chat |
Recommended starting points
- New AI features: start with
ai.chat.responses.create(...) - Product/manual assistants: start with
ai.public.chat(...) - Existing OpenAI-style clients: use
ai.chat.completions.create(...) - Real-time voice: use
ai.public.getToken(...)and your provider's live client
SmartLinks AI provides five main capabilities:
- Responses API - Preferred API for agentic workflows, multimodal inputs, and tool-driven responses
- Chat Completions - OpenAI-compatible text generation with streaming and tool calling
- RAG (Retrieval-Augmented Generation) - Document-grounded Q&A for product assistants
- Voice Integration - Voice-to-text and text-to-voice for hands-free interaction
- Podcast Generation - NotebookLM-style multi-voice conversational podcasts from documents
Key Features
- ✅ Full TypeScript support with type safety
- ✅ Streaming responses with async iterators
- ✅ Automatic rate limit handling
- ✅ Session management for conversations
- ✅ Voice input/output helpers
- ✅ Tool/function calling support
- ✅ OpenAI-style Responses API support
- ✅ Document indexing and retrieval
- ✅ Customizable assistant behavior
Quick Start
If you're only reading one section, start here. The three snippets below cover the most common public SDK use cases.
1. Generate a response
import { initializeApi, ai } from '@proveanything/smartlinks';
// Initialize the SDK
initializeApi({
baseURL: 'https://smartlinks.app/api/v1',
apiKey: process.env.SMARTLINKS_API_KEY // Required for admin endpoints
});
// Preferred: create a response
const response = await ai.chat.responses.create('my-collection', {
model: 'google/gemini-2.5-flash',
input: 'Summarize the key safety steps for descaling a coffee maker.'
});
console.log(response.output_text);
2. Build a product assistant
import { initializeApi, ai } from '@proveanything/smartlinks';
initializeApi({ baseURL: 'https://smartlinks.app/api/v1' });
const answer = await ai.public.chat('my-collection', {
productId: 'coffee-maker-deluxe',
userId: 'user-123',
message: 'How do I descale this machine?'
});
console.log(answer.message);
3. Stream output into your UI
const stream = await ai.chat.responses.create('my-collection', {
input: 'Write a launch checklist for a new product page.',
stream: true
});
for await (const event of stream) {
if (event.type === 'response.output_text.delta') {
updateUi(event.delta);
}
}
Authentication
Admin Endpoints
Admin endpoints require an API key passed during initialization:
initializeApi({
baseURL: 'https://smartlinks.app/api/v1',
apiKey: process.env.SMARTLINKS_API_KEY
});
The SDK automatically includes the API key in the Authorization: Bearer <token> header.
Public Endpoints
Public endpoints don't require an API key but are rate-limited by userId:
// No API key needed
const response = await ai.public.chat('my-collection', {
productId: 'coffee-maker',
userId: 'user-123',
message: 'How do I clean this?'
});
Responses API
The Responses API is the recommended starting point for new integrations. Use it when you want a single endpoint for structured input, tool use, and streaming output.
Basic Response
const response = await ai.chat.responses.create('my-collection', {
model: 'google/gemini-2.5-flash',
input: 'Write a friendly two-sentence welcome for a product assistant.'
});
console.log(response.output_text);
Multimessage Input
const response = await ai.chat.responses.create('my-collection', {
model: 'google/gemini-2.5-flash',
input: [
{
role: 'system',
content: [
{ type: 'input_text', text: 'You are a concise support assistant.' }
]
},
{
role: 'user',
content: [
{ type: 'input_text', text: 'Give me three troubleshooting steps for a grinder that will not start.' }
]
}
]
});
console.log(response.output_text);
Streaming Responses
When you pass stream: true, the SDK returns an AsyncIterable of SSE events instead of a final JSON object. You do not need to parse raw SSE frames yourself — just iterate with for await...of.
const result = await ai.chat.responses.create('my-collection', {
input: 'Summarize the manual',
stream: true
});
for await (const event of result) {
if (event.type === 'response.output_text.delta') {
process.stdout.write(event.delta);
}
}
If you omit stream: true, the same method returns the final ResponsesResult object instead.
const stream = await ai.chat.responses.create('my-collection', {
model: 'google/gemini-2.5-flash',
input: 'Explain how to descale an espresso machine step by step.',
stream: true
});
for await (const event of stream) {
if (event.type === 'response.output_text.delta') {
process.stdout.write(event.delta);
}
}
Tool Calling
const response = await ai.chat.responses.create('my-collection', {
model: 'google/gemini-2.5-flash',
input: 'What is the weather in Paris?',
tools: [
{
type: 'function',
name: 'get_weather',
description: 'Get the current weather for a city',
parameters: {
type: 'object',
properties: {
location: { type: 'string' }
},
required: ['location']
}
}
]
});
console.log(response.output);
Chat Completions
OpenAI-compatible chat completions with streaming and tool calling support. Use this for compatibility with existing Chat Completions integrations; prefer the Responses API for new agentic features.
Basic Chat
const response = await ai.chat.completions.create('my-collection', {
model: 'google/gemini-2.5-flash',
messages: [
{ role: 'system', content: 'You are a helpful assistant.' },
{ role: 'user', content: 'What is the capital of France?' }
]
});
console.log(response.choices[0].message.content);
// Output: "The capital of France is Paris."
Streaming Chat
Stream responses in real-time for better UX:
- Set
stream: true - The SDK returns an
AsyncIterable<ChatCompletionChunk> - Iterate over chunks with
for await...of - Read incremental text from
chunk.choices[0]?.delta?.content - If
streamis omitted orfalse, the method returns the normalChatCompletionResponse
const stream = await ai.chat.completions.create('my-collection', {
model: 'google/gemini-2.5-flash',
messages: [
{ role: 'user', content: 'Write a short poem about coding' }
],
stream: true
});
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || '';
process.stdout.write(content);
}
Tool/Function Calling
Define tools (functions) that the AI can call:
const tools = [
{
type: 'function',
function: {
name: 'get_weather',
description: 'Get the current weather for a location',
parameters: {
type: 'object',
properties: {
location: {
type: 'string',
description: 'City name'
},
unit: {
type: 'string',
enum: ['celsius', 'fahrenheit']
}
},
required: ['location']
}
}
}
];
const response = await ai.chat.completions.create('my-collection', {
model: 'google/gemini-2.5-flash',
messages: [
{ role: 'user', content: 'What\'s the weather in Paris?' }
],
tools
});
const toolCall = response.choices[0].message.tool_calls?.[0];
if (toolCall) {
console.log('Function:', toolCall.function.name);
console.log('Arguments:', JSON.parse(toolCall.function.arguments));
// { location: "Paris", unit: "celsius" }
}
Available Models
// List all available models
const models = await ai.models.list('my-collection');
// Or filter by provider / capability
const openAiModels = await ai.models.list('my-collection', {
provider: 'openai'
});
const visionModels = await ai.models.list('my-collection', {
capability: 'vision'
});
models.data.forEach(model => {
console.log(`${model.name}`);
console.log(` Provider: ${model.provider}`);
console.log(` Context: ${model.contextWindow} tokens`);
console.log(` Pricing: $${model.pricing.input}/1M input tokens`);
});
// Get specific model info
const model = await ai.models.get('my-collection', 'google/gemini-2.5-flash');
console.log(model.capabilities); // ['text', 'vision', 'audio', 'code']
Use ai.models.list(collectionId) as the source of truth for what your collection can use at runtime. The public docs provide recommendations, but actual availability depends on the SmartLinks model catalog exposed to that collection.
Recommended Models:
| Model | Use Case | Speed | Cost |
|---|---|---|---|
openai/gpt-5.4 | Default for new agentic and structured-output workflows | Balanced | Medium |
openai/gpt-5-mini | Lower-cost general purpose and JSON tasks | Fast | Low |
google/gemini-2.5-flash | Fast multimodal and cost-sensitive general use | Fast | Low |
google/gemini-2.5-pro | Complex reasoning and heavier multimodal tasks | Slower | Higher |
If you want a safe default for most new work, start with openai/gpt-5.4. If you want a lower-cost fallback, use openai/gpt-5-mini or google/gemini-2.5-flash depending on your latency and pricing goals.
RAG: Product Assistants
Create intelligent product assistants that answer questions based on product documentation.
Setup: Index Documents
First, index your product documentation:
// Index a product manual from URL
const result = await ai.rag.indexDocument('my-collection', {
productId: 'coffee-maker-deluxe',
documentUrl: 'https://example.com/manuals/coffee-maker.pdf',
chunkSize: 500, // Tokens per chunk
overlap: 50, // Token overlap between chunks
provider: 'openai' // Embedding provider
});
console.log(`Indexed ${result.chunks} chunks`);
console.log(`Dimensions: ${result.metadata.embeddingDimensions}`);
// Or index from text directly
await ai.rag.indexDocument('my-collection', {
productId: 'coffee-maker-deluxe',
text: 'Your product manual content here...',
metadata: {
source: 'manual',
version: '2.0'
}
});
Configure Assistant
Customize the assistant's behavior:
await ai.rag.configureAssistant('my-collection', {
productId: 'coffee-maker-deluxe',
systemPrompt: 'You are a helpful coffee maker assistant. Be concise and friendly.',
model: 'google/gemini-2.5-flash',
temperature: 0.7,
maxTokensPerResponse: 500,
rateLimitPerUser: 20,
allowedTopics: ['usage', 'cleaning', 'troubleshooting'],
customInstructions: {
tone: 'friendly',
additionalRules: 'Always include safety warnings when relevant.'
}
});
Public Chat
Users can chat with the product assistant without authentication:
// First question
const response = await ai.public.chat('my-collection', {
productId: 'coffee-maker-deluxe',
userId: 'user-123',
message: 'How do I descale my coffee maker?'
});
console.log('Answer:', response.message);
console.log('Used', response.context.chunksUsed, 'document sections');
console.log('Top similarity:', response.context.topSimilarity);
Conversation History
Maintain conversation context with sessions:
const sessionId = `session-${Date.now()}`;
// First question
const q1 = await ai.public.chat('my-collection', {
productId: 'coffee-maker-deluxe',
userId: 'user-123',
message: 'How do I clean it?',
sessionId
});
// Follow-up question (uses history)
const q2 = await ai.public.chat('my-collection', {
productId: 'coffee-maker-deluxe',
userId: 'user-123',
message: 'How often should I do that?',
sessionId
});
// Get full conversation history
const session = await ai.public.getSession('my-collection', sessionId);
console.log('Messages:', session.messages);
console.log('Total messages:', session.messageCount);
// Clear session when done
await ai.public.clearSession('my-collection', sessionId);
Session Management
// Get session statistics (admin)
const stats = await ai.sessions.stats('my-collection');
console.log('Total sessions:', stats.totalSessions);
console.log('Active sessions:', stats.activeSessions);
console.log('Total messages:', stats.totalMessages);
console.log('Rate-limited users:', stats.rateLimitedUsers);
Voice Integration
Enable voice input and output for hands-free interaction.
Voice Patterns
The SDK supports three practical voice patterns:
| Pattern | Best For | SDK Building Blocks |
|---|---|---|
| Voice → Text → AI → Text | Manual helper Q&A, troubleshooting steps | ai.voice.listen() + ai.public.chat() |
| Voice → Text → AI → Voice | Hands-free assistants, accessibility | ai.voice.listen() + ai.public.chat() + ai.voice.speak() or ai.tts.generate() |
| Real-time Voice | Low-latency spoken conversation | ai.public.getToken() + Gemini Live client |
Current SDK Support
ai.voice.listen()andai.voice.speak()are browser helpers built on the Web Speech APIs.ai.public.getToken()generates ephemeral tokens for Gemini Live sessions.ai.tts.generate()supports server-side text-to-speech generation.- The SDK does not currently expose a first-class transcription endpoint like Whisper; if you need that flow, implement it as your own backend endpoint and feed the transcribed text into
ai.public.chat()orai.chat.responses.create().
Recommended Approach
For product assistants and RAG-backed support, start with Voice → Text → AI → Text/Voice. It gives you the best control over retrieval, session history, and cost. Use Gemini Live when low-latency spoken conversation matters more than deep document grounding.
Browser Voice Helpers
These helpers are browser-only and rely on native speech recognition / speech synthesis support.
// Check if voice is supported
if (ai.voice.isSupported()) {
// Listen for voice input
const question = await ai.voice.listen('en-US');
console.log('User said:', question);
// Get answer from AI
const response = await ai.public.chat('my-collection', {
productId: 'coffee-maker-deluxe',
userId: 'user-123',
message: question
});
// Speak the answer
await ai.voice.speak(response.message, {
voice: 'alloy',
rate: 1.0
});
}
Voice Assistant Class
Create a complete voice assistant:
class ProductVoiceAssistant {
private collectionId: string;
private productId: string;
private userId: string;
private sessionId: string;
constructor(config: {
collectionId: string;
productId: string;
userId: string;
}) {
this.collectionId = config.collectionId;
this.productId = config.productId;
this.userId = config.userId;
this.sessionId = `voice-${Date.now()}`;
}
async ask(): Promise<string> {
// Listen for question
console.log('Listening...');
const question = await ai.voice.listen();
// Get answer
console.log('Processing...');
const response = await ai.public.chat(this.collectionId, {
productId: this.productId,
userId: this.userId,
message: question,
sessionId: this.sessionId
});
// Speak answer
console.log('Speaking...');
await ai.voice.speak(response.message);
return response.message;
}
async getRemainingQuestions(): Promise<number> {
const status = await ai.public.getRateLimit(this.collectionId, this.userId);
return status.remaining;
}
}
// Usage
const assistant = new ProductVoiceAssistant({
collectionId: 'my-collection',
productId: 'coffee-maker-deluxe',
userId: 'user-123'
});
await assistant.ask(); // Voice question → Voice answer
const remaining = await assistant.getRemainingQuestions();
console.log(`${remaining} questions remaining`);
Gemini Live Integration
Generate ephemeral tokens for Gemini Live (multimodal voice):
Use this path for real-time voice sessions. The SDK only issues the short-lived token; the actual live connection is made with the provider client.
// Generate token for voice session
const token = await ai.public.getToken('my-collection', {
settings: {
ttl: 3600, // 1 hour
voice: 'alloy',
language: 'en-US'
}
});
console.log('Token:', token.token);
console.log('Expires at:', new Date(token.expiresAt));
// Use token with Gemini Live API
// (See Google's Gemini documentation)
Voice + RAG Guidance
For document-grounded assistants, prefer this pattern:
- Capture voice with
ai.voice.listen()or your own transcription flow. - Send the transcribed text to
ai.public.chat(). - Render the text response for readability.
- Optionally speak the answer with
ai.voice.speak()orai.tts.generate().
This is usually a better fit for manuals and procedural guidance than trying to use a live voice session as the primary retrieval layer.
Podcast Generation
Generate NotebookLM-style multi-voice conversational podcasts from product documentation.
Generate a Podcast
const podcast = await ai.podcast.generate('my-collection', {
productId: 'coffee-maker-deluxe',
duration: 5, // Target 5 minutes
style: 'casual', // 'casual' | 'professional' | 'educational' | 'entertaining'
voices: {
host1: 'nova', // Female voice
host2: 'onyx' // Male voice
},
includeAudio: true // Generate audio files
});
console.log('Podcast Title:', podcast.script.title);
console.log('Duration:', podcast.metadata.duration, 'seconds');
console.log('Download:', podcast.audio?.mixedUrl);
Available Voices
| Voice | Gender | Personality | Best For |
|---|---|---|---|
alloy | Neutral | Balanced, neutral | Professional podcasts |
echo | Male | Clear, authoritative | Expert/teacher role |
fable | Neutral | Warm, storytelling | Narrative content |
onyx | Male | Deep, engaging | Main host, discussions |
nova | Female | Friendly, enthusiastic | Co-host, questions |
shimmer | Female | Bright, energetic | Entertaining content |
Recommended Combinations:
- Casual: Nova + Onyx - Friendly and engaging
- Professional: Alloy + Echo - Authoritative and clear
- Educational: Fable + Echo - Teaching style
- Entertaining: Shimmer + Onyx - High energy
Access the Script
// View the generated script
podcast.script.segments.forEach((segment, i) => {
const speaker = segment.speaker === 'host1' ? 'Host 1' : 'Host 2';
console.log(`${speaker}: ${segment.text}`);
});
Check Generation Status
For long-running podcast generation, poll for status:
// Start generation
const podcast = await ai.podcast.generate('my-collection', {
productId: 'coffee-maker-deluxe',
duration: 10,
includeAudio: true
});
// Poll for status
const checkStatus = async () => {
const status = await ai.podcast.getStatus('my-collection', podcast.podcastId);
console.log(`Status: ${status.status} (${status.progress}%)`);
if (status.status === 'completed' && status.result) {
console.log('Podcast ready!');
console.log('Listen:', status.result.audio?.mixedUrl);
return true;
} else if (status.status === 'failed') {
console.error('Generation failed:', status.error);
return true;
}
return false;
};
// Check every 5 seconds
const interval = setInterval(async () => {
const done = await checkStatus();
if (done) clearInterval(interval);
}, 5000);
Text-to-Speech (TTS)
Generate custom audio from text:
const audioBlob = await ai.tts.generate('my-collection', {
text: 'Welcome to our podcast about coffee makers!',
voice: 'nova',
speed: 1.0,
format: 'mp3'
});
// Create audio URL for playback
const audioUrl = URL.createObjectURL(audioBlob);
Type Definitions
Core Types
/**
* Chat message with role and content
*/
interface ChatMessage {
role: 'system' | 'user' | 'assistant' | 'function' | 'tool';
content: string | ContentPart[];
name?: string;
function_call?: FunctionCall;
tool_calls?: ToolCall[];
tool_call_id?: string;
}
/**
* Chat completion request
*/
interface ChatCompletionRequest {
messages: ChatMessage[];
model?: string;
stream?: boolean;
tools?: ToolDefinition[];
tool_choice?: 'none' | 'auto' | 'required' | { type: 'function'; function: { name: string } };
temperature?: number; // 0-2, default: 0.7
max_tokens?: number;
top_p?: number;
frequency_penalty?: number;
presence_penalty?: number;
response_format?: { type: 'text' | 'json_object' };
user?: string;
}
/**
* Chat completion response
*/
interface ChatCompletionResponse {
id: string;
object: 'chat.completion';
created: number;
model: string;
choices: ChatCompletionChoice[];
usage: {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
};
}
/**
* Streaming chunk
*/
interface ChatCompletionChunk {
id: string;
object: 'chat.completion.chunk';
created: number;
model: string;
choices: Array<{
index: number;
delta: Partial<ChatMessage>;
finish_reason: string | null;
}>;
}
RAG Types
/**
* Index document request
*/
interface IndexDocumentRequest {
productId: string;
text?: string; // Either text or documentUrl required
documentUrl?: string;
metadata?: Record<string, any>;
chunkSize?: number; // Default: 500
overlap?: number; // Default: 50
provider?: 'openai' | 'gemini';
}
/**
* Configure assistant request
*/
interface ConfigureAssistantRequest {
productId: string;
systemPrompt?: string;
model?: string;
maxTokensPerResponse?: number;
temperature?: number;
rateLimitPerUser?: number;
allowedTopics?: string[];
customInstructions?: Record<string, any>;
}
/**
* Public chat request
*/
interface PublicChatRequest {
productId: string;
userId: string;
message: string;
sessionId?: string;
stream?: boolean;
}
/**
* Public chat response
*/
interface PublicChatResponse {
message: string;
sessionId: string;
usage: {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
};
context?: {
chunksUsed: number;
topSimilarity: number;
};
}
Podcast Types
/**
* Podcast generation request
*/
interface GeneratePodcastRequest {
productId: string;
documentText?: string; // Optional if document already indexed
duration?: number; // Target duration in minutes (default: 10)
style?: 'casual' | 'professional' | 'educational' | 'entertaining';
voices?: {
host1?: string; // Voice name for first host
host2?: string; // Voice name for second host
};
includeAudio?: boolean; // Generate audio files (default: false)
language?: string; // Default: 'en-US'
customInstructions?: string;
}
/**
* Podcast script segment
*/
interface PodcastSegment {
speaker: 'host1' | 'host2';
text: string;
timestamp?: number; // Start time in seconds
duration?: number; // Segment duration
}
/**
* Podcast script
*/
interface PodcastScript {
title: string;
description: string;
segments: PodcastSegment[];
}
/**
* Podcast generation response
*/
interface GeneratePodcastResponse {
success: boolean;
podcastId: string;
script: PodcastScript;
audio?: {
host1Url?: string; // URL to download host 1 audio
host2Url?: string; // URL to download host 2 audio
mixedUrl?: string; // URL to download mixed podcast
};
metadata: {
duration: number; // Actual duration in seconds
wordCount: number;
generatedAt: string;
};
}
/**
* Podcast status
*/
interface PodcastStatus {
podcastId: string;
status: 'generating_script' | 'generating_audio' | 'mixing' | 'completed' | 'failed';
progress: number; // 0-100
estimatedTimeRemaining?: number; // Seconds
error?: string;
result?: GeneratePodcastResponse; // Available when completed
}
/**
* TTS request
*/
interface TTSRequest {
text: string;
voice?: 'alloy' | 'echo' | 'fable' | 'onyx' | 'nova' | 'shimmer';
speed?: number; // 0.25 - 4.0, default: 1.0
format?: 'mp3' | 'opus' | 'aac' | 'flac'; // Default: mp3
}
Error Types
/**
* API Error response
*/
interface AIError {
error: {
message: string;
type: string;
code: string;
param?: string;
resetAt?: string; // ISO 8601 timestamp (for rate limits)
};
}
/**
* Custom error class
*/
class SmartLinksAIError extends Error {
type: string;
code: string;
statusCode: number;
resetAt?: string;
isRateLimitError(): boolean;
isAuthError(): boolean;
isNotFoundError(): boolean;
}
API Reference
Admin Endpoints
ai.chat.completions.create(collectionId, request)
Create a chat completion (OpenAI-compatible).
Parameters:
collectionId(string) - Collection IDrequest(ChatCompletionRequest) - Request parameters
Returns: Promise<ChatCompletionResponse | AsyncIterable<ChatCompletionChunk>>
Example:
const response = await ai.chat.completions.create('my-collection', {
model: 'google/gemini-2.5-flash',
messages: [{ role: 'user', content: 'Hello!' }]
});
ai.models.list(collectionId)
List available AI models.
Returns: Promise<ModelList>
ai.models.get(collectionId, modelId)
Get specific model information.
Parameters:
modelId(string) - Model identifier (e.g., 'google/gemini-2.5-flash')
Returns: Promise<AIModel>
ai.rag.indexDocument(collectionId, request)
Index a document for RAG.
Parameters:
request(IndexDocumentRequest) - Document and indexing parameters
Returns: Promise<IndexDocumentResponse>
ai.rag.configureAssistant(collectionId, request)
Configure AI assistant behavior.
Parameters:
request(ConfigureAssistantRequest) - Assistant configuration
Returns: Promise<ConfigureAssistantResponse>
ai.sessions.stats(collectionId)
Get session statistics.
Returns: Promise<SessionStatistics>
ai.rateLimit.reset(collectionId, userId)
Reset rate limit for a user.
Returns: Promise<{ success: boolean; userId: string }>
ai.podcast.generate(collectionId, request)
Generate a NotebookLM-style conversational podcast.
Parameters:
request(GeneratePodcastRequest) - Podcast generation parameters
Returns: Promise<GeneratePodcastResponse>
Example:
const podcast = await ai.podcast.generate('my-collection', {
productId: 'coffee-maker-deluxe',
duration: 5,
style: 'casual',
voices: { host1: 'nova', host2: 'onyx' },
includeAudio: true
});
ai.podcast.getStatus(collectionId, podcastId)
Get podcast generation status.
Parameters:
podcastId(string) - Podcast identifier
Returns: Promise<PodcastStatus>
ai.tts.generate(collectionId, request)
Generate text-to-speech audio.
Parameters:
request(TTSRequest) - TTS parameters
Returns: Promise<Blob>
Example:
const audioBlob = await ai.tts.generate('my-collection', {
text: 'Welcome to our podcast!',
voice: 'nova',
speed: 1.0
});
Public Endpoints
ai.public.chat(collectionId, request)
Chat with product assistant (no auth required).
Parameters:
request(PublicChatRequest) - Chat parameters
Returns: Promise<PublicChatResponse>
Rate Limited: Yes (20 requests/hour per userId by default)
ai.public.getSession(collectionId, sessionId)
Get conversation history.
Returns: Promise<Session>
ai.public.clearSession(collectionId, sessionId)
Clear conversation history.
Returns: Promise<{ success: boolean }>
ai.public.getRateLimit(collectionId, userId)
Check rate limit status.
Returns: Promise<RateLimitStatus>
ai.public.getToken(collectionId, request)
Generate ephemeral token for Gemini Live.
Returns: Promise<EphemeralTokenResponse>
Usage Examples
Example 1: Product FAQ Bot
async function createProductFAQ() {
const collectionId = 'my-collection';
const productId = 'coffee-maker-deluxe';
// 1. Index product documentation
await ai.rag.indexDocument(collectionId, {
productId,
documentUrl: 'https://example.com/manual.pdf'
});
// 2. Configure assistant
await ai.rag.configureAssistant(collectionId, {
productId,
systemPrompt: 'You are a coffee maker expert. Provide clear, step-by-step instructions.',
rateLimitPerUser: 30
});
// 3. Answer user questions
const answer = await ai.public.chat(collectionId, {
productId,
userId: 'user-123',
message: 'How do I make espresso?'
});
console.log(answer.message);
}
Example 2: Streaming Chatbot UI
async function streamingChatbot(userMessage: string) {
const stream = await ai.chat.completions.create('my-collection', {
model: 'google/gemini-2.5-flash',
messages: [
{ role: 'system', content: 'You are a helpful assistant.' },
{ role: 'user', content: userMessage }
],
stream: true
});
let fullResponse = '';
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || '';
fullResponse += content;
// Update UI in real-time
updateChatUI(content);
}
return fullResponse;
}
Example 3: Multi-Turn Conversation
async function chatConversation() {
const collectionId = 'my-collection';
const sessionId = `chat-${Date.now()}`;
const userId = 'user-123';
const productId = 'coffee-maker-deluxe';
// Question 1
const a1 = await ai.public.chat(collectionId, {
productId,
userId,
message: 'How do I clean the machine?',
sessionId
});
console.log('A1:', a1.message);
// Question 2 (references previous context)
const a2 = await ai.public.chat(collectionId, {
productId,
userId,
message: 'How often should I do that?',
sessionId
});
console.log('A2:', a2.message);
// Get full history
const session = await ai.public.getSession(collectionId, sessionId);
console.log('Full conversation:', session.messages);
}
Example 4: React Hook for Product Assistant
import { useState, useCallback } from 'react';
import { ai } from '@proveanything/smartlinks';
export function useProductAssistant(
collectionId: string,
productId: string,
userId: string
) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [rateLimit, setRateLimit] = useState({ remaining: 20, limit: 20 });
const ask = useCallback(async (message: string) => {
setLoading(true);
setError(null);
try {
const response = await ai.public.chat(collectionId, {
productId,
userId,
message
});
setRateLimit(prev => ({
...prev,
remaining: prev.remaining - 1
}));
return response.message;
} catch (err: any) {
setError(err.message);
throw err;
} finally {
setLoading(false);
}
}, [collectionId, productId, userId]);
return { ask, loading, error, rateLimit };
}
// Usage in component
function ProductHelp() {
const { ask, loading, rateLimit } = useProductAssistant(
'my-collection',
'coffee-maker',
'user-123'
);
const [answer, setAnswer] = useState('');
const handleAsk = async () => {
const response = await ask('How do I clean this?');
setAnswer(response);
};
return (
<div>
<button onClick={handleAsk} disabled={loading}>
{loading ? 'Asking...' : 'Ask Question'}
</button>
{answer && <p>{answer}</p>}
<p>{rateLimit.remaining} questions remaining</p>
</div>
);
}
Example 5: Voice Q&A
async function voiceQA() {
if (!ai.voice.isSupported()) {
console.error('Voice not supported in this browser');
return;
}
console.log('Speak your question...');
// Listen for voice input
const question = await ai.voice.listen('en-US');
console.log('You asked:', question);
// Get answer from AI
const response = await ai.public.chat('my-collection', {
productId: 'coffee-maker-deluxe',
userId: 'user-123',
message: question
});
// Display answer
console.log('Answer:', response.message);
// Speak answer
await ai.voice.speak(response.message);
}
Example 6: Generate Product Podcast
async function generateProductPodcast() {
// Generate a casual 5-minute podcast about the coffee maker
const podcast = await ai.podcast.generate('my-collection', {
productId: 'coffee-maker-deluxe',
duration: 5, // minutes
style: 'casual', // Conversational style
voices: {
host1: 'nova', // Female voice
host2: 'onyx' // Male voice
},
includeAudio: true
});
console.log('Podcast Title:', podcast.script.title);
console.log('Duration:', podcast.metadata.duration, 'seconds');
// Display script
console.log('\nScript:');
podcast.script.segments.forEach((segment, i) => {
const speaker = segment.speaker === 'host1' ? 'Host 1' : 'Host 2';
console.log(`\n${speaker}: ${segment.text}`);
});
// Download audio
if (podcast.audio?.mixedUrl) {
console.log('\nDownload podcast:', podcast.audio.mixedUrl);
}
}
Example 7: Podcast with Progress Tracking
async function generatePodcastWithProgress() {
// Start podcast generation
const podcast = await ai.podcast.generate('my-collection', {
productId: 'coffee-maker-deluxe',
duration: 10,
style: 'professional',
includeAudio: true
});
const podcastId = podcast.podcastId;
// Poll for status
const checkStatus = async () => {
const status = await ai.podcast.getStatus('my-collection', podcastId);
console.log(`Status: ${status.status} (${status.progress}%)`);
if (status.status === 'completed' && status.result) {
console.log('Podcast ready!');
console.log('Listen:', status.result.audio?.mixedUrl);
return true;
} else if (status.status === 'failed') {
console.error('Generation failed:', status.error);
return true;
}
return false;
};
// Check every 5 seconds
const interval = setInterval(async () => {
const done = await checkStatus();
if (done) clearInterval(interval);
}, 5000);
}
Error Handling
Error Codes
| Code | Type | HTTP Status | Description |
|---|---|---|---|
rate_limit_exceeded | rate_limit_error | 429 | User exceeded rate limit |
invalid_request | invalid_request_error | 400 | Invalid parameters |
authentication_error | authentication_error | 401 | Invalid/missing API key |
permission_denied | permission_error | 403 | Insufficient permissions |
not_found | not_found_error | 404 | Resource not found |
document_not_found | not_found_error | 404 | Product document not indexed |
server_error | server_error | 500 | Internal server error |
service_unavailable | server_error | 503 | Service temporarily unavailable |
Error Handling Pattern
import { SmartLinksAIError } from '@proveanything/smartlinks';
async function robustChat() {
try {
const response = await ai.public.chat('my-collection', {
productId: 'coffee-maker',
userId: 'user-123',
message: 'Help!'
});
return response.message;
} catch (error) {
if (error instanceof SmartLinksAIError) {
switch (error.code) {
case 'rate_limit_exceeded':
console.error('Rate limit exceeded');
console.log('Try again at:', new Date(error.resetAt!));
break;
case 'document_not_found':
console.error('Product manual not indexed yet');
break;
case 'authentication_error':
console.error('Invalid API key');
break;
case 'invalid_request':
console.error('Invalid request:', error.message);
break;
default:
console.error('API error:', error.message);
}
} else {
console.error('Unexpected error:', error);
}
throw error;
}
}
Rate Limit Retry
async function chatWithRetry(
request: PublicChatRequest,
maxRetries = 3
) {
let retries = 0;
while (retries < maxRetries) {
try {
return await ai.public.chat('my-collection', request);
} catch (error) {
if (error instanceof SmartLinksAIError && error.isRateLimitError()) {
if (retries === maxRetries - 1) throw error;
const resetTime = new Date(error.resetAt!).getTime();
const waitTime = resetTime - Date.now();
console.log(`Rate limited. Waiting ${waitTime}ms...`);
await new Promise(resolve => setTimeout(resolve, waitTime));
retries++;
} else {
throw error;
}
}
}
}
Rate Limiting
Rate Limit Overview
Public endpoints are rate-limited per userId:
| Endpoint Type | Default Limit | Window |
|---|---|---|
| Public Chat | 20 requests | 1 hour |
| Token Generation | 10 requests | 1 hour |
| Admin Endpoints | Unlimited* | - |
*Admin endpoints use API key authentication and are not rate-limited by default.
Rate Limit Headers
All API responses include rate limit information:
X-RateLimit-Limit: 20
X-RateLimit-Remaining: 15
X-RateLimit-Reset: 1707300000000
Checking Rate Limit
// Check rate limit status before making requests
const status = await ai.public.getRateLimit('my-collection', 'user-123');
console.log('Used:', status.used);
console.log('Remaining:', status.remaining);
console.log('Resets at:', new Date(status.resetAt));
if (status.remaining > 0) {
// Safe to make request
await ai.public.chat(/* ... */);
} else {
// Show user when they can ask again
console.log('Rate limit reached. Try again at:', status.resetAt);
}
Resetting Rate Limits (Admin)
// Reset rate limit for a specific user
await ai.rateLimit.reset('my-collection', 'user-123');
console.log('Rate limit reset for user-123');
Best Practices
1. Choose the Right Model
// For most new workflows (recommended default)
await ai.chat.responses.create('my-collection', {
model: 'openai/gpt-5.4',
input: 'Create a concise onboarding checklist.'
});
// For lower-cost structured or JSON-oriented work
await ai.chat.responses.create('my-collection', {
model: 'openai/gpt-5-mini',
input: 'Return a color palette as JSON.'
});
// For fast multimodal or cost-sensitive general use
await ai.chat.completions.create('my-collection', {
model: 'google/gemini-2.5-flash',
messages: [...]
});
// When you need the actual available catalog for this collection
const available = await ai.models.list('my-collection');
console.log(available.data.map(model => model.id));
2. Use Streaming for Long Responses
Improve perceived performance with streaming:
// Non-streaming: User waits for full response
const response = await ai.chat.completions.create('my-collection', {
messages: [...]
});
// Streaming: User sees progress immediately
const stream = await ai.chat.completions.create('my-collection', {
stream: true,
messages: [...]
});
for await (const chunk of stream) {
updateUI(chunk.choices[0]?.delta?.content);
}
3. Maintain Session Context
Keep conversations coherent with session IDs:
// Generate unique session ID per conversation
const sessionId = `user-${userId}-product-${productId}`;
// All questions in same conversation use same sessionId
await ai.public.chat('my-collection', {
sessionId,
message: 'First question',
...
});
await ai.public.chat('my-collection', {
sessionId,
message: 'Follow-up question',
...
});
4. Handle Rate Limits Gracefully
Show clear feedback to users:
try {
await ai.public.chat('my-collection', {...});
} catch (error) {
if (error instanceof SmartLinksAIError && error.isRateLimitError()) {
const resetTime = new Date(error.resetAt!);
showNotification(
`You've reached your question limit. ` +
`Try again at ${resetTime.toLocaleTimeString()}`
);
}
}
5. Optimize Voice UX
Provide clear status updates:
async function voiceAssistant() {
try {
showStatus('Listening...');
const question = await ai.voice.listen();
showStatus('Processing...');
const answer = await ai.public.chat('my-collection', {
message: question,
...
});
showStatus('Speaking...');
await ai.voice.speak(answer.message);
showStatus('Ready');
} catch (error) {
showStatus('Error', error.message);
}
}
6. Chunk Large Documents
For better RAG performance, chunk documents appropriately:
// For technical manuals
await ai.rag.indexDocument('my-collection', {
productId: 'coffee-maker',
documentUrl: '...',
chunkSize: 500, // Smaller chunks for precise answers
overlap: 50 // Overlap maintains context
});
// For narrative content
await ai.rag.indexDocument('my-collection', {
productId: 'coffee-maker',
documentUrl: '...',
chunkSize: 1000, // Larger chunks for coherent responses
overlap: 100
});
7. Use System Prompts Effectively
Provide clear instructions:
await ai.chat.completions.create('my-collection', {
messages: [
{
role: 'system',
content: `You are a coffee maker expert assistant.
- Be concise and clear
- Use numbered lists for steps
- Always mention safety precautions
- If unsure, ask for clarification`
},
{ role: 'user', content: 'How do I descale?' }
]
});
8. Monitor Usage and Costs
Track usage for cost management:
const response = await ai.chat.completions.create('my-collection', {
messages: [...]
});
// Log token usage
console.log('Usage:', response.usage);
console.log('Prompt tokens:', response.usage.prompt_tokens);
console.log('Completion tokens:', response.usage.completion_tokens);
console.log('Total tokens:', response.usage.total_tokens);
// Calculate estimated cost
const model = await ai.models.get('my-collection', response.model);
const cost =
(response.usage.prompt_tokens * model.pricing.input / 1_000_000) +
(response.usage.completion_tokens * model.pricing.output / 1_000_000);
console.log('Estimated cost: $', cost.toFixed(4));
Providing Content to the AI Assistant
The portal's built-in AI assistant can discuss the content currently visible to the user. To make this work, your app must supply contextual content when the assistant requests it. There are three extraction methods, tried in priority order:
| Priority | Method | When Used |
|---|---|---|
| 1 | Direct prop callback | Container/widget rendered in the parent React context |
| 2 | PostMessage protocol | App rendered in an iframe |
| 3 | DOM text fallback | Neither of the above responded |
This section covers methods 1 and 2 — the ones you implement in your app.
Method 1: Direct Prop (onRequestAIContent)
When your app is rendered as a container (a direct component in the parent's React tree), the framework passes an onRequestAIContent prop to your exported component. You do not call this prop yourself — the framework calls it when the AI assistant needs context.
Structure your component to make current state accessible when the callback fires:
import { useEffect, useRef } from 'react';
export function PublicContainer(props) {
const { onRequestAIContent, appId, ...rest } = props;
// Keep a ref to the latest content so the callback always returns fresh data
const currentContentRef = useRef(null);
useEffect(() => {
currentContentRef.current = buildCurrentContent();
}, [relevantState]);
return <div data-app-container={appId}>{/* your UI */}</div>;
}
The framework registers the content provider internally via useAIContentExtraction.registerContentProvider(). Your component just needs to respond when the callback is invoked.
Method 2: PostMessage Protocol (Iframes)
For iframe-embedded apps the framework sends a postMessage request and expects a response within 500 ms. If your app doesn't reply in time the framework falls back to DOM text extraction.
Request sent by the framework to your iframe:
{
type: 'smartlinks:request-ai-content',
requestId: 'ai-content-1709834567890-abc123' // unique per request
}
Response your app must send back:
{
type: 'smartlinks:ai-content-response',
requestId: 'ai-content-1709834567890-abc123', // echo back the requestId
content: AIContentResponse
}
Minimal implementation:
window.addEventListener('message', async (event) => {
if (event.data?.type === 'smartlinks:request-ai-content') {
const content = await gatherAIContent();
window.parent.postMessage({
type: 'smartlinks:ai-content-response',
requestId: event.data.requestId,
content,
}, '*');
}
});
async function gatherAIContent(): Promise<AIContentResponse> {
return {
text: 'The user is viewing the warranty registration form...',
contentLabel: 'Warranty Registration',
};
}
The AIContentResponse Interface
interface AIContentResponse {
/**
* Plain text or markdown for the AI to use as context.
* Injected into the system prompt. Max recommended: ~4000 characters.
*/
text: string;
/**
* Optional structured metadata (key-value pairs).
* Not used directly in prompts but available for custom providers.
*/
metadata?: Record<string, unknown>;
/**
* Pre-built RAG configuration. When provided, the assistant can use
* SL.ai.public.chat() to ground answers in indexed product documents.
*/
ragHint?: {
/** The product ID whose indexed documents should be queried */
productId: string;
/** Optional session ID for multi-turn RAG conversations */
sessionId?: string;
/** Optional context hint to help scope the RAG query */
context?: string;
};
/**
* Human-readable label shown in context-update messages
* (e.g. "Product Manual", "FAQ").
*/
contentLabel?: string;
/**
* How the assistant should use this content:
* - 'context' (default): inject text into the system prompt
* - 'rag': use ragHint to query indexed docs via SL.ai.public.chat()
* - 'hybrid': inject text as context AND ground answers via RAG
*/
strategy?: 'context' | 'rag' | 'hybrid';
}
Response Strategies
context (Default)
Return readable text. The assistant injects it into the system prompt as background knowledge. Good for descriptions, summaries, structured data, and FAQs.
return {
text: `
## Wine Details
- **Name**: 2023 Château Margaux
- **Region**: Bordeaux, France
- **Tasting Notes**: Dark fruit, cedar, tobacco
- **Food Pairing**: Lamb, aged cheese
`,
contentLabel: 'Wine Information',
strategy: 'context',
};
rag
Tell the assistant to query pre-indexed documents via SmartLinks RAG. The text field is minimal — the real knowledge comes from the indexed docs. Good for product manuals, large document sets, and technical specs.
return {
text: 'The user is viewing the espresso machine product page.',
contentLabel: 'Product Assistant',
strategy: 'rag',
ragHint: {
productId: 'espresso-machine-pro',
sessionId: `rag-session-${userId}`,
context: 'User is on the troubleshooting section',
},
};
When the assistant receives a rag strategy it routes the question through SL.ai.public.chat():
const response = await SL.ai.public.chat(collectionId, {
productId: ragHint.productId,
userId: currentUserId,
message: userQuestion,
sessionId: ragHint.sessionId,
});
hybrid
Combines both — the text is injected as additional context and the user's questions are also grounded via RAG. Good for museum exhibits, guided experiences, or any scenario with rich metadata alongside large document sets.
return {
text: `
## Exhibit: The Starry Night
- **Artist**: Vincent van Gogh
- **Year**: 1889
- **Current Location**: Gallery 3, East Wing
- **Audio Guide**: Available in 12 languages
`,
contentLabel: 'Museum Exhibit',
strategy: 'hybrid',
ragHint: {
productId: 'starry-night-exhibit',
context: 'Art history and technique questions',
},
};
Content Extraction Examples
Museum Guide App
async function gatherAIContent(): Promise<AIContentResponse> {
const exhibit = getCurrentExhibit();
return {
text: `
Exhibit: "${exhibit.title}" by ${exhibit.artist}
Period: ${exhibit.period}
Medium: ${exhibit.medium}
Description: ${exhibit.curatorNotes}
Related works in this gallery: ${exhibit.relatedWorks.join(', ')}
`.trim(),
contentLabel: `Exhibit: ${exhibit.title}`,
strategy: 'hybrid',
ragHint: {
productId: exhibit.smartlinksProductId,
context: `Art history, technique, and visitor information for ${exhibit.title}`,
},
metadata: {
exhibitId: exhibit.id,
gallery: exhibit.gallery,
audioGuideAvailable: exhibit.hasAudioGuide,
},
};
}
Wine Product App
async function gatherAIContent(): Promise<AIContentResponse> {
const wine = getCurrentWine();
const reviews = await fetchRecentReviews(wine.id, 5);
return {
text: `
Wine: ${wine.name} (${wine.vintage})
Winery: ${wine.winery}
Region: ${wine.region}, ${wine.country}
Grape: ${wine.grape}
ABV: ${wine.abv}%
Price: ${wine.price}
Tasting Notes: ${wine.tastingNotes}
Recent Reviews:
${reviews.map(r => `- "${r.text}" (${r.rating}/5)`).join('\n')}
`.trim(),
contentLabel: 'Wine Details',
strategy: 'context',
};
}
Equipment Manual App (RAG-only)
async function gatherAIContent(): Promise<AIContentResponse> {
const equipment = getCurrentEquipment();
return {
text: `User is viewing: ${equipment.name} (Model: ${equipment.modelNumber})`,
contentLabel: `${equipment.name} Assistant`,
strategy: 'rag',
ragHint: {
productId: equipment.smartlinksProductId,
sessionId: `manual-${equipment.id}-${Date.now()}`,
context: 'Technical manual, troubleshooting, and maintenance',
},
};
}
Timing & Lifecycle
- On navigation: When the user navigates to a new product/proof/app, the assistant automatically requests fresh content and injects a context-update system message.
- On first message: If no content has been gathered yet, the assistant requests it before the first AI call.
- On manual refresh: The assistant can re-request content at any time (e.g. if the user's view within the app has changed).
Content is requested lazily — your handler is only called when the AI assistant is active and needs context. If the user never opens the assistant, your handler is never called.
Method 3: DOM Fallback (Automatic)
If your app doesn't implement either of the above, the framework extracts innerText from the DOM element with data-app-container="{appId}", truncated to ~4000 characters. This is a last resort — content quality is much lower than a structured response. Implementing Method 1 or 2 is strongly recommended.
Related Documentation
- API Summary - Complete API reference
- Widgets - Embedding SmartLinks components
- Realtime - Realtime data updates
- iframe Responder - iframe integration
Support
For questions or issues:
- Documentation: https://smartlinks.app/docs
- GitHub: https://github.com/Prove-Anything/smartlinks
- Email: support@smartlinks.app