Core Features

    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

    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 workflowUse Responses API
    Add compatibility with existing chat clientsUse Chat Completions
    Build a product/manual assistantUse RAG: Product Assistants
    Add spoken input/outputUse Voice Integration
    Add progressive renderingUse 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:

    1. Responses API - Preferred API for agentic workflows, multimodal inputs, and tool-driven responses
    2. Chat Completions - OpenAI-compatible text generation with streaming and tool calling
    3. RAG (Retrieval-Augmented Generation) - Document-grounded Q&A for product assistants
    4. Voice Integration - Voice-to-text and text-to-voice for hands-free interaction
    5. 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 stream is omitted or false, the method returns the normal ChatCompletionResponse
    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:

    ModelUse CaseSpeedCost
    openai/gpt-5.4Default for new agentic and structured-output workflowsBalancedMedium
    openai/gpt-5-miniLower-cost general purpose and JSON tasksFastLow
    google/gemini-2.5-flashFast multimodal and cost-sensitive general useFastLow
    google/gemini-2.5-proComplex reasoning and heavier multimodal tasksSlowerHigher

    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:

    PatternBest ForSDK Building Blocks
    Voice → Text → AI → TextManual helper Q&A, troubleshooting stepsai.voice.listen() + ai.public.chat()
    Voice → Text → AI → VoiceHands-free assistants, accessibilityai.voice.listen() + ai.public.chat() + ai.voice.speak() or ai.tts.generate()
    Real-time VoiceLow-latency spoken conversationai.public.getToken() + Gemini Live client

    Current SDK Support

    • ai.voice.listen() and ai.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() or ai.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:

    1. Capture voice with ai.voice.listen() or your own transcription flow.
    2. Send the transcribed text to ai.public.chat().
    3. Render the text response for readability.
    4. Optionally speak the answer with ai.voice.speak() or ai.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

    VoiceGenderPersonalityBest For
    alloyNeutralBalanced, neutralProfessional podcasts
    echoMaleClear, authoritativeExpert/teacher role
    fableNeutralWarm, storytellingNarrative content
    onyxMaleDeep, engagingMain host, discussions
    novaFemaleFriendly, enthusiasticCo-host, questions
    shimmerFemaleBright, energeticEntertaining 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 ID
    • request (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

    CodeTypeHTTP StatusDescription
    rate_limit_exceededrate_limit_error429User exceeded rate limit
    invalid_requestinvalid_request_error400Invalid parameters
    authentication_errorauthentication_error401Invalid/missing API key
    permission_deniedpermission_error403Insufficient permissions
    not_foundnot_found_error404Resource not found
    document_not_foundnot_found_error404Product document not indexed
    server_errorserver_error500Internal server error
    service_unavailableserver_error503Service 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 TypeDefault LimitWindow
    Public Chat20 requests1 hour
    Token Generation10 requests1 hour
    Admin EndpointsUnlimited*-

    *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:

    PriorityMethodWhen Used
    1Direct prop callbackContainer/widget rendered in the parent React context
    2PostMessage protocolApp rendered in an iframe
    3DOM text fallbackNeither 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


    Support

    For questions or issues: