Skip to content

Data Management API Reference

Overview

Live Pages use a powerful JavaScript library (pt) that provides database-like operations for managing data. The library is automatically initialized and provides simple CRUD operations with server-side filtering.

Complete API Reference

The PrimeThink API provides a complete set of operations for managing entities and chat context:

Operation Method Description Performance
Create pt.add(entityName, data) Add new entity Fast
Batch Create pt.batchAdd(entityName, dataArray) Create multiple entities in one transaction Fast
Read (one) pt.get(entityId) Get single entity by ID Very Fast (Primary Key)
Read (many) pt.list(options) Query multiple entities with filters Fast (Indexed)
Update pt.edit(entityId, data, merge) Update existing entity Fast
Batch Update pt.batchEdit(items) Update multiple entities in one request Fast
Delete pt.delete(entityId) Remove entity Fast
Batch Delete pt.batchDelete(ids) Delete multiple entities Fast
Members pt.getChatMembers() Get chat members (users & agents) Fast
Chat Info pt.getChatInfo() Get current chat information Fast
Rename Chat pt.renameChat(newName) Rename the current chat Fast
Mentions pt.getAvailableMentions() Get mentionable entities (users, assistants, chats, tasks) Fast
Chat Messages pt.addMessage(message) Add text message to chat Fast
Message Text pt.getMessageText(messageId) Get message text by ID Fast
Message Received pt.onMessageReceived(taskId, callback) Subscribe to AI response for a task Fast
Wait for Message pt.waitForMessageReceived(taskId, options) Promise-based wait for AI response Fast
Wait for All Messages pt.waitForAllMessagesReceived(taskIds, options) Wait for multiple AI responses Fast
Document Changed pt.onDocumentChanged(callback, options) Subscribe to document processing events Fast
Wait for Document pt.waitForDocumentReady(documentId, options) Promise-based wait for document processing Fast
Wait for All Documents pt.waitForAllDocumentsReady(documentIds, options) Wait for multiple documents to be ready Fast
Stop Streaming pt.stopStreamingMessage(streamingTaskId) Stop a streaming AI response Fast
File Upload pt.uploadFiles(form, folder?, documentName?) Upload files with optional folder and custom name Fast
Notifications pt.sendNotification(userId, title, text) Send push notification to user Fast
Document Search pt.searchDocuments(query, scope, documentIds) Search documents and collections using RAG Fast
Document Read pt.getDocumentText(docId, options) Get document text content Fast
Document Status pt.getDocumentStatus(docId) Check document processing status Fast
Document Info pt.getDocumentInfo(documentPath) Get document metadata by path Fast
Document Create pt.saveDocument(filename, format, mimetype, content, folder) Create and save document Fast
Document Delete pt.deleteDocuments(documentIds) Bulk delete documents from chat Fast
Document Download pt.downloadDocuments(documentIds, asZip) Download one or more documents Fast
Directory Navigation pt.listDirectory(path) List contents of a directory path Fast
Collections List pt.listCollections() Get all collections in chat Fast
Collections Docs pt.getDocumentsInCollections(collectionIds) Get documents in specified collections Fast

Available Methods

pt.list(options)

List entities with optional server-side filtering, pagination, and multiple entity types.

Parameters: - entityNames: Array of entity types to include (e.g., ['user', 'product']) - filters: Object with field-value pairs for server-side filtering - limit: Maximum number of results to return (default: 100, max: 1000) - offset: Number of results to skip for pagination (default: 0) - page: Page number for page-based pagination (1-indexed, mutually exclusive with offset) - pageSize: Items per page for page-based pagination (mutually exclusive with limit) - returnMetadata: Set to true to return pagination metadata along with entities

Basic Usage:

// List entities with filters
const users = await pt.list({
    entityNames: ['user', 'product'],
    filters: {
        status: 'active',
        age: { $gte: 18 }
    },
    limit: 10,
    offset: 0
});

// List multiple entity types
const entities = await pt.list({
    entityNames: ['task', 'event', 'user'],
    filters: { status: 'active' }
});

// Process results
const tasks = entities.filter(e => e.entity_name === 'task');
const events = entities.filter(e => e.entity_name === 'event');
const users = entities.filter(e => e.entity_name === 'user');

Response Structure:

// Without metadata (default)
[
    {
        id: 123,
        entity_name: 'task',
        data: { text: 'Buy groceries', completed: false },
        creator_user_id: 456,
        created_at: '2024-03-15T10:30:00+00:00',
        updated_at: '2024-03-15T10:35:00+00:00'
    },
    // ... more entities
]

// With metadata (returnMetadata: true)
{
    entities: [...],    // Array of entity objects
    count: 20,          // Number of items in this response
    pagination: {
        limit: 20,      // Items requested
        offset: 40,     // Starting position
        has_more: true, // true if full page returned (more likely exists)
        page: 3,        // Current page (if page-based)
        page_size: 20   // Items per page (if page-based)
    }
}

pt.get(entityId)

Retrieve a single entity by its ID. This method performs a direct primary key lookup, which is much faster and more semantic than using pt.list() with filters.

Parameters: - entityId: Numeric ID of the entity to retrieve

Usage:

// Get entity by ID
const task = await pt.get(123);
console.log(task.data.text);
console.log(task.created_at);

// Get and edit pattern (using merge mode)
const task = await pt.get(123);
await pt.edit(123, {
    completed: !task.data.completed
}, true);

Error Handling:

try {
    const task = await pt.get(123);
    console.log('Found:', task.data);
} catch (error) {
    console.error('Entity not found or unauthorized:', error);
}

When to use pt.get(): - When you know the entity ID - For direct lookups (much faster than filtering) - Before editing an entity to get current state

pt.add(entityName, data)

Create a new entity.

Parameters: - entityName: String identifying the entity type - data: Object containing the entity data

Usage:

// Add a task
const result = await pt.add('task', {
    text: 'Buy groceries',
    completed: false
});

// Add an event
const event = await pt.add('event', {
    title: 'Team Meeting',
    date: '2024-03-20',
    time: '14:00',
    description: 'Weekly sync'
});

// The creator_user_id is automatically set to the current user
console.log(result.creator_user_id); // Automatically populated

Important Notes: - created_at and updated_at are automatically managed - creator_user_id is automatically set to the current user - Don't include these fields in your data object

pt.batchAdd(entityName, dataArray)

Create multiple entities in a single transaction. All entities succeed or fail together.

Parameters: - entityName: String identifying the entity type for all entities - dataArray: Array of data objects to create

Returns: Array of result objects, one for each item in dataArray:

[
    {
        success: true,
        entity: {
            id: 1,
            entity_name: 'task',
            creator_user_id: 42,
            data: { title: 'Task 1' },
            created_at: '2025-01-20T10:00:00Z',
            updated_at: '2025-01-20T10:00:00Z'
        },
        error: null,
        index: 0
    },
    {
        success: false,
        entity: null,
        error: {
            message: 'Validation failed',
            type: 'ValidationError'
        },
        index: 1
    }
]

Usage:

// Create multiple tasks at once
const results = await pt.batchAdd('task', [
    { text: 'Buy groceries', completed: false },
    { text: 'Call dentist', completed: false },
    { text: 'Review PR', completed: false }
]);

// Check results
const successful = results.filter(r => r.success);
const failed = results.filter(r => !r.success);

console.log(`Created ${successful.length} tasks`);
if (failed.length > 0) {
    console.log(`Failed to create ${failed.length} tasks`);
    failed.forEach(f => console.error(`Item ${f.index}: ${f.error.message}`));
}

// Import data from CSV or API
async function importUsers(userData) {
    const results = await pt.batchAdd('user', userData);

    return {
        imported: results.filter(r => r.success).length,
        failed: results.filter(r => !r.success).length,
        errors: results.filter(r => !r.success).map(r => ({
            index: r.index,
            error: r.error.message
        }))
    };
}

// Bulk data entry
async function bulkAddProducts(products) {
    try {
        const results = await pt.batchAdd('product', products);

        // Get IDs of successfully created products
        const createdIds = results
            .filter(r => r.success)
            .map(r => r.entity.id);

        return createdIds;
    } catch (error) {
        console.error('Batch add failed:', error);
        return [];
    }
}

When to Use: - Importing data from CSV, JSON, or external APIs - Bulk data entry operations - Seeding initial data - Processing form submissions with multiple items - More efficient than multiple pt.add() calls

Important Notes: - All operations happen in a single transaction - If any item fails validation, its success will be false but other items may still succeed - Check the success field for each result - The index field corresponds to the position in the input array - Much more efficient than calling pt.add() multiple times

pt.edit(entityId, data, merge, ifUnchangedSince)

Update an existing entity. By default, this method replaces all data in the entity. Use the merge parameter to preserve existing fields and ifUnchangedSince for optimistic locking.

Parameters: - entityId: Numeric ID of the entity to update - data: Object containing the new entity data - merge: (Optional) If true, merges with existing data; if false, replaces entirely (default: false) - ifUnchangedSince: (Optional) ISO timestamp string for optimistic locking. Update only succeeds if entity hasn't been modified since this timestamp

Returns: - Success: Updated entity object - Conflict (when using ifUnchangedSince): Object with conflict: true and currentEntity showing the current state

Usage:

Replace Mode (default):

// Get current data first to preserve fields
const task = await pt.get(taskId);

// Update with merged data manually
const updated = await pt.edit(taskId, {
    ...task.data,
    completed: true
});

Merge Mode (recommended for partial updates):

// Only update specific fields - other fields are preserved automatically
const updated = await pt.edit(taskId, {
    completed: true
}, true);

// Update multiple fields without fetching first
await pt.edit(taskId, {
    status: 'in_progress',
    priority: 'high'
}, true);

// Toggle a field using merge mode
async function toggleTaskCompletion(taskId) {
    const task = await pt.get(taskId);
    await pt.edit(taskId, {
        completed: !task.data.completed
    }, true);
}

Optimistic Locking (Conflict Prevention):

Use ifUnchangedSince to prevent overwriting changes made by other users:

// Fetch entity with its timestamp
const task = await pt.get(taskId);

// User edits the task...
// Meanwhile, another user might also be editing it

// Try to save with optimistic locking
const result = await pt.edit(
    taskId,
    {
        ...task.data,
        status: 'completed'
    },
    false,  // merge mode
    task.updated_at  // Only update if not modified since this timestamp
);

// Handle conflicts
if (result.conflict) {
    // Another user modified this task
    console.log('Conflict detected!');
    console.log('Your version:', task.data);
    console.log('Current version:', result.currentEntity.data);

    // Show conflict resolution UI to user
    showConflictDialog(task.data, result.currentEntity.data);
} else {
    // Update successful
    console.log('Task updated successfully');
}

// Real-world conflict handling example
async function safeEdit(taskId, updates) {
    const task = await pt.get(taskId);

    try {
        const result = await pt.edit(
            taskId,
            { ...task.data, ...updates },
            false,
            task.updated_at
        );

        if (result.conflict) {
            // Let user decide how to resolve
            const userChoice = await askUser(
                'This task was modified by someone else. What would you like to do?',
                ['Use my changes', 'Use their changes', 'Merge manually']
            );

            if (userChoice === 'Use my changes') {
                // Force update without timestamp check
                return await pt.edit(taskId, { ...task.data, ...updates });
            } else if (userChoice === 'Merge manually') {
                // Show merge interface
                return await showMergeInterface(task.data, result.currentEntity.data, updates);
            } else {
                // Keep their changes
                return result.currentEntity;
            }
        }

        return result;
    } catch (error) {
        console.error('Edit failed:', error);
        throw error;
    }
}

When to use each mode:

Mode Use Case
Replace (merge: false) When you want to completely overwrite all entity data
Merge (merge: true) When updating specific fields while preserving others

When to use optimistic locking: - Multi-user editing scenarios - Long-running edit sessions - Critical data updates - Preventing accidental overwrites

Warning: Without merge mode, always spread existing data to avoid losing fields:

// ❌ BAD: This removes all other fields (replace mode)
await pt.edit(123, { completed: true });

// ✅ GOOD: Use merge mode for partial updates
await pt.edit(123, { completed: true }, true);

// ✅ ALSO GOOD: Manually merge with existing data
const task = await pt.get(123);
await pt.edit(123, {
    ...task.data,
    completed: true
});

pt.batchEdit(items)

Update multiple entities in a single request. Each item can have different update settings.

Parameters: - items: Array of edit item objects

Item Structure:

{
    id: number,                    // Entity ID (required)
    data: object,                  // New data (required)
    merge: boolean,                // Merge mode (optional, default: false)
    if_unchanged_since: string     // ISO timestamp for optimistic locking (optional)
}

Returns: Array of result objects, one for each item:

[
    {
        success: true,
        entity: {
            id: 1,
            entity_name: 'task',
            data: { title: 'Updated Task', completed: true },
            created_at: '2025-01-20T10:00:00Z',
            updated_at: '2025-01-20T11:00:00Z'
        },
        conflict: false
    },
    {
        success: false,
        conflict: true,
        currentEntity: {
            id: 2,
            entity_name: 'task',
            data: { title: 'Already modified by someone else' },
            created_at: '2025-01-20T10:00:00Z',
            updated_at: '2025-01-20T10:45:00Z'  // Changed since our if_unchanged_since
        }
    }
]

Usage:

// Bulk status update
const taskIds = [1, 2, 3, 4, 5];
const results = await pt.batchEdit(
    taskIds.map(id => ({
        id: id,
        data: { completed: true },
        merge: true  // Preserve other fields
    }))
);

console.log(`Updated ${results.filter(r => r.success).length} tasks`);

// Update multiple items with different values
const updates = [
    { id: 1, data: { status: 'completed', priority: 'low' }, merge: true },
    { id: 2, data: { status: 'in_progress', priority: 'high' }, merge: true },
    { id: 3, data: { status: 'pending', priority: 'medium' }, merge: true }
];

const results = await pt.batchEdit(updates);

// Handle conflicts
results.forEach((result, index) => {
    if (result.conflict) {
        console.warn(`Item ${index} has conflict:`, result.currentEntity);
        // Show conflict resolution UI to user
    } else if (!result.success) {
        console.error(`Item ${index} failed to update`);
    }
});

// Optimistic locking for batch updates
async function safeBatchUpdate(entities) {
    const items = entities.map(entity => ({
        id: entity.id,
        data: { status: 'archived' },
        merge: true,
        if_unchanged_since: entity.updated_at
    }));

    const results = await pt.batchEdit(items);

    // Separate successful updates and conflicts
    const successful = results.filter(r => r.success && !r.conflict);
    const conflicts = results.filter(r => r.conflict);

    return { successful, conflicts };
}

// Bulk field update with validation
async function bulkUpdatePriority(taskIds, newPriority) {
    // Fetch all tasks first to preserve other fields
    const tasks = await Promise.all(
        taskIds.map(id => pt.get(id))
    );

    // Prepare updates
    const items = tasks.map(task => ({
        id: task.id,
        data: {
            ...task.data,
            priority: newPriority,
            last_modified_by: 'admin'
        }
    }));

    const results = await pt.batchEdit(items);

    return {
        updated: results.filter(r => r.success).length,
        failed: results.filter(r => !r.success).length
    };
}

When to Use: - Bulk status changes (mark multiple items as complete, archive, etc.) - Mass updates based on filters or selection - Synchronizing data from external sources - Batch operations in admin panels - More efficient than multiple pt.edit() calls

Important Notes: - Each item can have its own merge setting - Use if_unchanged_since for optimistic locking to prevent overwriting changes - Check both success and conflict fields in results - The index field in results corresponds to the input array position - When conflicts occur, currentEntity shows the current database state - More efficient than individual pt.edit() calls

pt.delete(entityId)

Remove an entity from the database.

Parameters: - entityId: Numeric ID of the entity to delete

Usage:

// Simple delete
async function deleteTask(taskId) {
    try {
        await pt.delete(taskId);
        console.log('Task deleted successfully');
    } catch (error) {
        console.error('Error deleting task:', error);
    }
}

// Delete with confirmation
async function deleteTaskSafely(taskId) {
    try {
        const task = await pt.get(taskId);

        if (confirm(`Delete task: "${task.data.text}"?`)) {
            await pt.delete(taskId);
            console.log('Task deleted successfully');
        }
    } catch (error) {
        alert('Task not found or already deleted');
    }
}

// Bulk delete
async function bulkDeleteTasks(taskIds) {
    const results = [];

    for (const id of taskIds) {
        try {
            await pt.delete(id);
            results.push({ id, success: true });
        } catch (error) {
            results.push({ id, success: false, error: error.message });
        }
    }

    return results;
}

pt.batchDelete(ids)

Delete multiple entities in a single request.

Parameters: - ids: Array of entity IDs to delete (numeric IDs)

Returns: Array of result objects, one for each ID:

[
    {
        id: 1,
        success: true,
        error: null
    },
    {
        id: 2,
        success: false,
        error: "Entity not found"
    }
]

Usage:

// Delete multiple tasks
const taskIds = [1, 2, 3, 4, 5];
const results = await pt.batchDelete(taskIds);

// Check results
const deleted = results.filter(r => r.success);
const failed = results.filter(r => !r.success);

console.log(`Deleted ${deleted.length} tasks`);
if (failed.length > 0) {
    console.warn(`Failed to delete ${failed.length} tasks`);
    failed.forEach(f => console.error(`ID ${f.id}: ${f.error}`));
}

// Clean up old completed tasks
async function archiveCompletedTasks() {
    // Get completed tasks older than 30 days
    const oldTasks = await pt.list({
        entityNames: ['task'],
        filters: {
            completed: "true",
            completed_date: { $lt: '2024-12-20' }
        }
    });

    if (oldTasks.length === 0) {
        return { deleted: 0 };
    }

    const ids = oldTasks.map(t => t.id);
    const results = await pt.batchDelete(ids);

    return {
        deleted: results.filter(r => r.success).length,
        failed: results.filter(r => !r.success).length
    };
}

// Delete with user confirmation
async function deleteSelectedItems(selectedIds) {
    if (selectedIds.length === 0) {
        alert('No items selected');
        return;
    }

    if (!confirm(`Delete ${selectedIds.length} items? This cannot be undone.`)) {
        return;
    }

    const results = await pt.batchDelete(selectedIds);

    const successCount = results.filter(r => r.success).length;
    alert(`Deleted ${successCount} of ${selectedIds.length} items`);

    await loadData(); // Refresh the list
}

// Bulk cleanup with error handling
async function bulkDelete(entityName, filters) {
    try {
        // Get entities to delete
        const entities = await pt.list({
            entityNames: [entityName],
            filters: filters
        });

        if (entities.length === 0) {
            return { message: 'No entities found matching criteria' };
        }

        // Delete them
        const ids = entities.map(e => e.id);
        const results = await pt.batchDelete(ids);

        const successful = results.filter(r => r.success);
        const failed = results.filter(r => !r.success);

        return {
            total: entities.length,
            deleted: successful.length,
            failed: failed.length,
            errors: failed.map(f => ({ id: f.id, error: f.error }))
        };
    } catch (error) {
        console.error('Bulk delete failed:', error);
        return { error: error.message };
    }
}

// Usage example
const result = await bulkDelete('task', {
    status: 'archived',
    archived_date: { $lt: '2024-01-01' }
});

console.log(result);
// { total: 150, deleted: 148, failed: 2, errors: [...] }

When to Use: - Bulk cleanup operations - Deleting multiple selected items - Archiving old data - Cascading deletes based on filters - More efficient than multiple pt.delete() calls

Important Notes: - Each delete operation is independent - If one delete fails, others will still be attempted - Check the success field for each result - Failed deletes include an error message explaining why - Always confirm with users before bulk deletes - Consider archiving instead of deleting when possible - More efficient than individual pt.delete() calls

pt.getChatMembers()

Get all members of the current chat (users and AI agents).

Returns: Array of member objects with a simplified, clear structure. Each member is either a user or an AI agent.

Response Structure:

[
  {
    "id": 123,              // Member ID (either user_id or agent_id)
    "type": "user",         // "user" or "agent"
    "name": "John Doe",     // Display name (full name for users, agent name for agents)

    // User-only fields (null for agents):
    "first_name": "John",
    "last_name": "Doe",
    "email": "john@example.com",

    "is_chat_owner": true,  // Created the chat
    "is_logged_user": true  // This is the current user making the request
  },
  {
    "id": 456,
    "type": "agent",
    "name": "AI Assistant",

    // User fields are null for agents
    "first_name": null,
    "last_name": null,
    "email": null,

    "is_chat_owner": false,
    "is_logged_user": false  // Always false for agents
  }
]

Field Reference:

Field Type User Agent Description
id number Member ID
type string "user" or "agent"
name string Display name (full name or agent name)
first_name string? null First name
last_name string? null Last name
email string? null Email address
is_chat_owner boolean Created the chat
is_logged_user boolean True when member is the current user making the request (always false for agents)

Usage Examples:

// Get and display all members
const members = await pt.getChatMembers();
members.forEach(member => {
    const icon = member.type === 'user' ? '👤' : '🤖';
    console.log(`${icon} ${member.name}`);
});

// Filter by type
const users = members.filter(m => m.type === 'user');
const agents = members.filter(m => m.type === 'agent');
console.log(`Users: ${users.length}, Agents: ${agents.length}`);

// Create task assignment dropdown (users only)
async function createAssigneeDropdown() {
    const members = await pt.getChatMembers();
    const users = members.filter(m => m.type === 'user');

    const options = users
        .map(m => `<option value="${m.id}">${m.name}</option>`)
        .join('');

    return `
        <select id="assignee">
            <option value="">Unassigned</option>
            ${options}
        </select>
    `;
}

// Get current user
const members = await pt.getChatMembers();
const currentUser = members.find(m => m.is_logged_user);

pt.getChatInfo()

Get information about the current chat. Returns basic metadata including chat ID, name, and creation timestamp.

Parameters: - None (uses chat context automatically)

Returns: Promise that resolves to a chat information object:

{
  "chat_id": 42,
  "name": "Project Planning",
  "created_at": "2025-01-15T10:30:00Z"
}

Response Structure: - chat_id (number): The chat ID - name (string): Chat name/title - created_at (string): ISO 8601 timestamp of when the chat was created

Usage Examples:

// Get current chat details
const chatInfo = await pt.getChatInfo();
console.log(`Chat: ${chatInfo.name}`);
console.log(`ID: ${chatInfo.chat_id}`);
console.log(`Created: ${chatInfo.created_at}`);

// Display chat header
const info = await pt.getChatInfo();
document.getElementById('chatName').textContent = info.name;
document.getElementById('chatId').textContent = `ID: ${info.chat_id}`;

// Show chat age
const info = await pt.getChatInfo();
const created = new Date(info.created_at);
const age = Math.floor((Date.now() - created) / (1000 * 60 * 60 * 24));
console.log(`Chat is ${age} days old`);

// Build page title with chat name
async function initializePage() {
    const chatInfo = await pt.getChatInfo();
    document.title = `${chatInfo.name} - Dashboard`;

    const header = document.getElementById('pageHeader');
    header.innerHTML = `
        <h1>${chatInfo.name}</h1>
        <p class="text-gray-600">Chat ID: ${chatInfo.chat_id}</p>
    `;
}

// Use chat ID in API calls
const chatInfo = await pt.getChatInfo();
const apiUrl = `/api/v1/chats/${chatInfo.chat_id}/custom-endpoint`;

Common Use Cases: - Display chat name in page header - Show chat metadata in dashboards - Use chat ID for constructing API URLs - Calculate chat age or activity duration - Build breadcrumb navigation - Personalize page titles

pt.renameChat(newName)

Rename the current chat. Updates the chat's display name visible to all members.

Parameters: - newName (string): The new name for the chat. Must be a non-empty string.

Returns: Promise that resolves to an object containing:

{
    "success": true,
    "chat_id": 42,
    "name": "New Chat Name"
}

Response Structure: - success (boolean): Whether the rename operation succeeded - chat_id (number): The chat ID - name (string): The updated chat name

Usage Examples:

// Simple rename
await pt.renameChat('New Chat Name');

// With user input
const newName = prompt('Enter new chat name:');
if (newName) {
    await pt.renameChat(newName);
}

// Rename with confirmation and error handling
async function renameChatWithConfirmation() {
    const chatInfo = await pt.getChatInfo();
    const newName = prompt(`Rename "${chatInfo.name}" to:`, chatInfo.name);

    if (newName && newName !== chatInfo.name) {
        try {
            const result = await pt.renameChat(newName);
            if (result.success) {
                console.log(`Chat renamed to: ${result.name}`);
            }
        } catch (error) {
            console.error('Failed to rename chat:', error);
        }
    }
}

Common Use Cases: - Allow users to customize chat names - Update chat names based on project phases - Rename chats programmatically based on content or context

pt.getAvailableMentions()

Get all mentionable entities available in the chat (users, virtual assistants, other chats, and tasks). Results are filtered based on the current user's permissions.

Parameters: None (uses chat context automatically)

Returns: Promise that resolves to an object containing:

{
    users_mentions: [
        {
            id: 1,
            mention_name: "@john",
            display_name: "John Doe",
            // ... additional user fields
        }
    ],
    virtual_assistants_mentions: [
        {
            id: 5,
            mention_name: "@assistant",
            name: "My Assistant",
            // ... additional assistant fields
        }
    ],
    chats_mentions: [
        {
            id: 10,
            mention_name: "#project-chat",
            chat_name: "Project Chat",
            // ... additional chat fields
        }
    ],
    tasks_mentions: [
        {
            id: 20,
            mention_name: "@task:generate-report",
            // ... additional task fields
        }
    ]
}

Usage:

// Get all available mentions
const mentions = await pt.getAvailableMentions();

// Build a mention picker UI
async function createMentionPicker() {
    const mentions = await pt.getAvailableMentions();

    const html = `
        <div class="mention-picker">
            ${mentions.users_mentions.length > 0 ? `
                <h3>Users</h3>
                ${mentions.users_mentions.map(user => `
                    <div class="mention-item" data-mention="${user.mention_name}">
                        👤 ${user.display_name} (${user.mention_name})
                    </div>
                `).join('')}
            ` : ''}

            ${mentions.virtual_assistants_mentions.length > 0 ? `
                <h3>Virtual Assistants</h3>
                ${mentions.virtual_assistants_mentions.map(va => `
                    <div class="mention-item" data-mention="${va.mention_name}">
                        🤖 ${va.name} (${va.mention_name})
                    </div>
                `).join('')}
            ` : ''}

            ${mentions.chats_mentions.length > 0 ? `
                <h3>Other Chats</h3>
                ${mentions.chats_mentions.map(chat => `
                    <div class="mention-item" data-mention="${chat.mention_name}">
                        💬 ${chat.chat_name} (${chat.mention_name})
                    </div>
                `).join('')}
            ` : ''}

            ${mentions.tasks_mentions.length > 0 ? `
                <h3>Tasks</h3>
                ${mentions.tasks_mentions.map(task => `
                    <div class="mention-item" data-mention="${task.mention_name}">
${task.mention_name}
                    </div>
                `).join('')}
            ` : ''}
        </div>
    `;

    return html;
}

// Auto-complete mentions in textarea
async function setupMentionAutocomplete(textareaId) {
    const mentions = await pt.getAvailableMentions();
    const textarea = document.getElementById(textareaId);

    // Flatten all mentions
    const allMentions = [
        ...mentions.users_mentions.map(m => ({...m, type: 'user'})),
        ...mentions.virtual_assistants_mentions.map(m => ({...m, type: 'assistant'})),
        ...mentions.chats_mentions.map(m => ({...m, type: 'chat'})),
        ...mentions.tasks_mentions.map(m => ({...m, type: 'task'}))
    ];

    textarea.addEventListener('input', (e) => {
        const value = e.target.value;
        const atIndex = value.lastIndexOf('@');
        const hashIndex = value.lastIndexOf('#');
        const mentionStart = Math.max(atIndex, hashIndex);

        if (mentionStart >= 0) {
            const searchText = value.substring(mentionStart).toLowerCase();
            const matches = allMentions.filter(m =>
                m.mention_name.toLowerCase().includes(searchText)
            );

            if (matches.length > 0) {
                showMentionSuggestions(matches, mentionStart);
            }
        }
    });
}

// Send message with mentions
async function sendMessageWithMention(userId) {
    const mentions = await pt.getAvailableMentions();
    const user = mentions.users_mentions.find(u => u.id === userId);

    if (user) {
        await pt.addMessage(`Hey ${user.mention_name}, can you review this?`);
    }
}

// Check what entities user can mention
async function checkMentionPermissions() {
    const mentions = await pt.getAvailableMentions();

    return {
        canMentionUsers: mentions.users_mentions.length > 0,
        canMentionAssistants: mentions.virtual_assistants_mentions.length > 0,
        canMentionChats: mentions.chats_mentions.length > 0,
        canMentionTasks: mentions.tasks_mentions.length > 0
    };
}

Permission Filtering: - Results are automatically filtered by user permissions: - MENTION_USERS_IN_CHATS - For users_mentions - MENTION_OTHER_CHATS_IN_CHATS - For chats_mentions - EXECUTE_TASK_ACTION - For tasks_mentions - Empty arrays are returned for categories the user lacks permission for

Common Use Cases: - Building mention pickers/autocomplete - Creating collaboration interfaces - Implementing @mentions in message composers - Task delegation with user mentions - Cross-chat references

pt.initDb()

Initialize the database for the current group. This is usually called automatically, but can be called manually if needed.

Usage:

await pt.initDb();

pt.reload()

Reload the current Live Page.

Usage:

// Reload the page
pt.reload();

// Example: Reload after a major update
async function refreshApplication() {
    await pt.initDb();
    pt.reload();
}

pt.navigate(chatId)

Navigate to a different Live Page.

Parameters: - chatId: ID of the chat/Live Page to navigate to

Usage:

// Navigate to another Live Page
pt.navigate(456);

// Example: Navigation menu
function createNavigation(livePages) {
    return livePages.map(page => `
        <button onclick="pt.navigate(${page.id})">
            ${page.title}
        </button>
    `).join('');
}

pt.addMessage() - Unified Message Method

Add a message to the current chat from your Live Page. This method automatically handles both text-only messages and messages with file attachments.

Syntax:

Mode 1: Text-only message

pt.addMessage(message, options?)

Mode 2: Message with files

pt.addMessage(formOrFormData, message, options?)

Parameters:

Parameter Type Description
message string (Mode 1) Message text
formOrFormData HTMLFormElement | FormData (Mode 2) Form or FormData with files
message string (Mode 2) Message text (can be empty)
options object Optional configuration
options.hidden boolean Hide message from UI (default: false)
options.awaitResponse boolean Wait for AI response (default: false)
options.folder string Target folder path where file attachments will be stored (e.g., 'reports/2024', 'invoices/january'). Only applies when uploading files.

Returns: Promise that resolves to an object containing: - success: Boolean indicating if the operation succeeded - action: String "add_message" - result: Object containing: - success: Boolean indicating message success - user_message: Object with your sent message - id: Message ID - message: The message text you sent - task_id: String UUID for tracking socket stream events (e.g., "c1680787-eac6-47ab-a8fd-09efadb74406") - ai_responses: Array of AI response objects (if AI replied) - id: Response message ID - message: AI's response text - files_count: Number of files uploaded (when using Mode 2) - attachments: Array of attachment objects (when files are uploaded) - created_at: ISO timestamp of attachment creation - from_user_id: ID of user who attached the file - indexed: Boolean indicating if file has been indexed - name: File name - status: Attachment status (e.g., "attached") - type: Attachment type (e.g., "document") - path: File path in the system - document: Object containing document details - id: Document ID - uuid: Document UUID - name: Document name - mimetype: MIME type of the document - status: Processing status (e.g., "Processed") - extracted_text_size: Size of extracted text in bytes - download_url: URL to download the document - short_description: Preview of document content - details: Additional status message

Downloading Attached Files:

When files are attached via addMessage, the response includes the download_url for each document. You can also access files using the simpler path-based File Download API:

For live apps: /api/v1/live_apps/{chat_id}/{file_path}

The file path can be extracted from the document.name or the attachment path fields. See File Download API for complete details on downloading files by path.

Usage:

Text-only messages:

// Simple text message
async function sendNotification() {
    try {
        const response = await pt.addMessage('Task completed successfully!');
        console.log('Message sent with ID:', response.result.user_message.id);
        console.log('Task ID for tracking:', response.result.task_id);

        // Check if AI responded
        if (response.result.ai_responses && response.result.ai_responses.length > 0) {
            console.log('AI replied:', response.result.ai_responses[0].message);
        }
    } catch (error) {
        console.error('Error sending message:', error);
    }
}

// With options
await pt.addMessage('Processing...', {
    hidden: true,
    awaitResponse: false
});

// Wait for AI response
const result = await pt.addMessage('What is 2+2?', {
    awaitResponse: true
});
console.log('AI says:', result.ai_responses[0].message);

// Send notification after an action
async function completeTask(taskId) {
    const task = await pt.get(taskId);
    await pt.edit(taskId, { completed: true }, true);  // merge mode

    // Notify the chat
    await pt.addMessage(`Task "${task.data.text}" has been completed!`);
}

Messages with files:

// From HTML form
const form = document.getElementById('uploadForm');
await pt.addMessage(form, 'Check out these files!');

// From FormData
const formData = new FormData();
formData.append('files', file1);
formData.append('files', file2);
await pt.addMessage(formData, 'Here are the documents');

// Files only, no message text
await pt.addMessage(form, '');

// Files with options
const result = await pt.addMessage(formData, 'Analyze these files', {
    awaitResponse: true,
    hidden: false
});
console.log(`Uploaded ${result.result.files_count} files`);

// Access attachment information
if (result.result.attachments) {
    result.result.attachments.forEach(attachment => {
        console.log('File:', attachment.name);
        console.log('Status:', attachment.document.status);
        console.log('Download URL:', attachment.document.download_url);
        console.log('Preview:', attachment.document.short_description);
    });
}

// Access AI responses
if (result.result.ai_responses) {
    result.result.ai_responses.forEach(response => {
        console.log('AI:', response.message);
    });
}

// Hidden message with files
await pt.addMessage(form, 'Internal processing...', {
    hidden: true
});

// Upload files to a specific folder
const form = document.getElementById('uploadForm');
await pt.addMessage(form, 'Monthly report', { folder: 'reports/january' });

// Combine folder with other options
await pt.addMessage(formData, 'Analyze these invoices', {
    folder: 'invoices/2024',
    awaitResponse: true
});

Example Response with Attachments:

{
  "success": true,
  "action": "add_message",
  "result": {
    "success": true,
    "user_message": {
      "id": 18378,
      "message": "translate to italian"
    },
    "task_id": "c1680787-eac6-47ab-a8fd-09efadb74406",
    "files_count": 1,
    "attachments": [
      {
        "created_at": "2026-01-24T11:01:50.793389+00:00",
        "from_user_id": 1,
        "indexed": true,
        "name": "rfp_test.xlsx",
        "status": "attached",
        "type": "document",
        "path": "/",
        "document": {
          "id": 1335,
          "uuid": "cd10cd40-62ed-43b0-a2a0-8ad99bed9d09",
          "name": "rfp_test.xlsx",
          "mimetype": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
          "status": "Processed",
          "extracted_text_size": 48,
          "download_url": "http://localhost:8001/api/v1/documents/uuid/cd10cd40-62ed-43b0-a2a0-8ad99bed9d09/download",
          "short_description": "## Sheet1\n| Question |\n| --- |\n| Test question |"
        }
      }
    ],
    "details": "Message has been queued"
  }
}

Dynamic usage:

// Determine at runtime whether to send text or files
async function sendMessage(textOrForm, message = '') {
    if (typeof textOrForm === 'string') {
        // Text only
        await pt.addMessage(textOrForm);
    } else {
        // Files with message
        await pt.addMessage(textOrForm, message);
    }
}

// Usage
await sendMessage('Hello'); // Text
await sendMessage(form, 'Check this'); // Files

Common Use Cases: - Notify users when an action is completed - Send alerts or warnings - Provide feedback from form submissions - Log important events to the chat - Create interactive chat-based workflows - Upload files with context or instructions for AI processing - Combine file upload with immediate AI analysis

AI-Powered Database Operations:

When you send a message to the chat, the AI assistant can process your instructions and automatically manage database entities. This enables powerful automation workflows where you describe what you want, and the AI handles the database operations.

Best Practice: Be Explicit About Tool Usage

To ensure the AI executes your desired operation, explicitly mention which database tool to use: - Use chatdb_add to create new entities - Use chatdb_list to search/query entities - Use chatdb_get to retrieve a specific entity by ID - Use chatdb_edit to update existing entities - Use chatdb_delete to remove entities

// Example 1: Extract data and store it (explicit tool usage)
await pt.addMessage(`
Please analyze the sales data from last quarter and use the tool 'chatdb_add' to create database entries:
- entity_name: "sales_record"
- data format: { month: string, revenue: number, region: string, growth_percentage: number }

Extract all quarterly sales information and add each month as a separate record.
`);

// Example 2: Process and categorize information (explicit tool usage)
await pt.addMessage(`
Search for the top 5 competitors in the AI assistant market and use the tool 'chatdb_add' to store each one:
- entity_name: "competitor"
- data: { name: string, website: string, key_features: array, pricing_model: string }
`);

// Example 3: Query and transform existing data
await pt.addMessage(`
Use 'chatdb_list' to find all tasks with status "pending" and high priority.
For each one, use 'chatdb_add' to create a new entity:
- entity_name: "urgent_action"
- data: { task_id: number, title: string, due_date: string, estimated_hours: number }
`);

// Example 4: Update existing entities
await pt.addMessage(`
Use 'chatdb_list' to find all expenses from last month with category "uncategorized".
For each expense, use 'chatdb_edit' to update it with the appropriate category based on the merchant name.
`);

// Example 5: Clean up old data
await pt.addMessage(`
Use 'chatdb_list' to find all tasks with status "completed" that were finished more than 90 days ago.
Use 'chatdb_delete' to remove them from the database.
`);

Available Database Tools:

Tool Purpose When to Use
chatdb_add Create new entities Storing new data, adding records
chatdb_list Query/search entities Finding records with filters, getting multiple items
chatdb_get Get single entity by ID Retrieving a specific record when you know its ID
chatdb_edit Update existing entities Modifying data, changing status, updating fields
chatdb_delete Remove entities Cleaning up, removing old data
chatdb_init Initialize database schema First-time setup (rarely needed)

Critical: Demand Actual Tool Execution

The AI may summarize or simulate tool calls instead of actually executing them if instructions are too high-level. Use explicit phrases to ensure actual execution:

// ❌ BAD: AI might only simulate this
await pt.addMessage(`
Extract IP tasks from the document. Use chatdb_list to check for duplicates.
If not found, use chatdb_add to insert each task.
`);
// Result: AI returns summary but doesn't actually call the tools

// ✅ GOOD: Explicit execution instructions
await pt.addMessage(`
Extract all IP tasks from the document. FOR EACH task:
1. ACTUALLY CALL chatdb_list with filters: {unique_key: "<hash>"}
2. If not found, ACTUALLY CALL chatdb_add to insert the task
3. Show all tool call results

IMPORTANT: You must ACTUALLY EXECUTE the tools for each task.
`);
// Result: AI actually executes chatdb_list and chatdb_add

Key phrases for actual execution: - "ACTUALLY CALL the tool 'chatdb_list'" - "ACTUALLY USE the tool 'chatdb_add'" - "FOR EACH item, use the tool..." - "Show all tool call results" - "You must ACTUALLY EXECUTE..."

Why this matters: - Without explicit execution instructions, AI may treat prompts as conceptual workflows - AI might return a summary of what "would happen" without updating the database - This is critical for multi-step operations with loops (e.g., processing multiple items) - Always verify in the chat that tool calls were actually executed

This feature is particularly powerful because: - The AI understands natural language instructions - It can process complex requirements and extract structured data - Explicit tool mentions ensure the correct operations are executed - It handles the database operations automatically - You can combine data processing with storage in one request

Tracking AI Responses with Socket Events

When you send a message using pt.addMessage(), the response includes a task_id that you can use to track real-time AI processing events through WebSocket connections. This allows you to build responsive UIs that show AI thinking, streaming responses, and completion status.

Task ID Format:

The task_id returned from pt.addMessage() is a UUID string:

const response = await pt.addMessage('Hello');
console.log(response.result.task_id); // "c1680787-eac6-47ab-a8fd-09efadb74406"

Socket Stream Events:

When connected to the PrimeThink WebSocket, you'll receive events with the following structure:

Event Types:

  1. stream_reasoning_token - AI thinking/reasoning step

    {
        task_id: "1c7bf9f6-75ce-4dfa-9648-991765bc04f2:18434",
        chat_id: 2131,
        chat_uuid: "a48f5a26-14f7-4737-ae7c-19651b981aaf",
        ai_message_id: 18434,
        chunk: '{"id":null,"label":"Thinking...","right_text":null,"description_md":null,"emoji":null,"step_type":"reasoning","order":null,"metadata":null,"children":null,"created_at":"2026-01-26T20:16:04.785761Z"}',
        initial: true
    }
    

  2. stream_partial_token - Streaming AI response text

    {
        task_id: "1c7bf9f6-75ce-4dfa-9648-991765bc04f2:18434",
        chat_id: 2131,
        chat_uuid: "a48f5a26-14f7-4737-ae7c-19651b981aaf",
        ai_message_id: 18434,
        agent_id: 195,
        chunk: "Hello, Demetrio",
        chunk_idx: 0
    }
    

  3. stream_completed - AI response finished

    {
        task_id: "1c7bf9f6-75ce-4dfa-9648-991765bc04f2:18434",
        chat_id: 2131,
        chat_uuid: "a48f5a26-14f7-4737-ae7c-19651b981aaf",
        ai_message_id: 18434
    }
    

Implementation Example:

// Store task IDs for tracking
const activeTaskIds = new Set();

// Send message and track task
async function sendTrackedMessage(message) {
    const response = await pt.addMessage(message);
    const taskId = response.result.task_id;

    // Store for tracking
    activeTaskIds.add(taskId);

    // Show loading state
    showLoadingIndicator(taskId);

    return { taskId, messageId: response.result.user_message.id };
}

// WebSocket event listener
socket.on('message', (event) => {
    const data = JSON.parse(event.data);

    // Check if this event is for one of our tracked tasks
    const baseTaskId = data.task_id?.split(':')[0];
    if (!activeTaskIds.has(baseTaskId)) return;

    switch(data.type) {
        case 'stream_reasoning_token':
            handleReasoningStep(data);
            break;

        case 'stream_partial_token':
            handleStreamingText(data);
            break;

        case 'stream_completed':
            handleCompletion(data);
            activeTaskIds.delete(baseTaskId);
            break;
    }
});

// Handle reasoning steps
function handleReasoningStep(data) {
    const step = JSON.parse(data.chunk);
    console.log(`AI is ${step.label}...`);

    // Update UI
    const indicator = document.getElementById(`task-${data.task_id}`);
    if (indicator) {
        indicator.textContent = step.label;
        indicator.className = 'reasoning-step';
    }
}

// Handle streaming response
function handleStreamingText(data) {
    const container = document.getElementById(`response-${data.ai_message_id}`);
    if (!container) {
        // Create container for streaming text
        const div = document.createElement('div');
        div.id = `response-${data.ai_message_id}`;
        div.className = 'ai-response streaming';
        document.getElementById('messages').appendChild(div);
    }

    // Append chunk
    container.textContent += data.chunk;
}

// Handle completion
function handleCompletion(data) {
    console.log(`Task ${data.task_id} completed`);

    // Remove loading indicator
    const indicator = document.getElementById(`task-${data.task_id}`);
    if (indicator) {
        indicator.remove();
    }

    // Mark response as complete
    const response = document.getElementById(`response-${data.ai_message_id}`);
    if (response) {
        response.classList.remove('streaming');
        response.classList.add('complete');
    }
}

// Example: Send message with full tracking
async function sendMessageWithTracking(message) {
    try {
        const { taskId, messageId } = await sendTrackedMessage(message);

        console.log('Message sent:', messageId);
        console.log('Tracking task:', taskId);

        // UI will update automatically via socket events

    } catch (error) {
        console.error('Failed to send message:', error);
    }
}

Use Cases: - Show "AI is thinking..." indicators - Display streaming AI responses in real-time - Build chat interfaces with live updates - Track multiple concurrent AI tasks - Provide progress feedback to users - Implement typing indicators - Create responsive conversational UIs

Important Notes: - The task_id format is {uuid}:{message_id} for socket events - Store the base UUID (before the colon) for matching events - Socket events arrive in real-time as the AI processes - Multiple tasks can be tracked simultaneously - Always clean up task IDs after completion to prevent memory leaks

pt.stopStreamingMessage(streamingTaskId)

Stop a streaming AI response that is currently being generated. Use this when the user wants to halt a long-running generation or no longer needs the response.

Parameters: - streamingTaskId (string, required): The unique identifier of the streaming task to stop

Returns: Promise that resolves to:

{
    success: boolean,
    message: string  // "Streaming task has been stopped."
}

Backend Action: - Action name: delete_streaming_task - Required parameter: streaming_task_id - Sets a Redis cancellation flag (cancel:{task_id}) and aborts the ARQ job

Usage:

// Basic usage - stop a streaming message
const taskId = 'abc123-task-id';
const result = await pt.stopStreamingMessage(taskId);
console.log(result.message); // "Streaming task has been stopped."

// With error handling
try {
    await pt.stopStreamingMessage(currentTaskId);
    console.log('Message generation stopped');
} catch (error) {
    console.error('Failed to stop:', error.message);
}

// Stop button implementation
document.getElementById('stopBtn').addEventListener('click', async () => {
    if (activeStreamingTaskId) {
        await pt.stopStreamingMessage(activeStreamingTaskId);
        activeStreamingTaskId = null;
        updateUI('stopped');
    }
});

Complete Example with Streaming Control:

// Track active streaming tasks
let activeStreamingTaskId = null;

// Send message and track the task
async function sendMessageWithTracking(message) {
    const response = await pt.addMessage(message);
    activeStreamingTaskId = response.result.task_id;

    // Show stop button
    document.getElementById('stopBtn').style.display = 'block';

    return response;
}

// Stop the current streaming response
async function stopCurrentStream() {
    if (!activeStreamingTaskId) {
        console.log('No active streaming task');
        return;
    }

    try {
        const result = await pt.stopStreamingMessage(activeStreamingTaskId);
        console.log(result.message);

        // Clean up
        activeStreamingTaskId = null;
        document.getElementById('stopBtn').style.display = 'none';

        // Update UI to show stopped state
        const responseContainer = document.getElementById('aiResponse');
        responseContainer.innerHTML += '<span class="stopped">[Generation stopped]</span>';

    } catch (error) {
        console.error('Failed to stop streaming:', error);
    }
}

// Handle stream completion (clean up task ID)
socket.on('stream_completed', (data) => {
    if (data.task_id.startsWith(activeStreamingTaskId)) {
        activeStreamingTaskId = null;
        document.getElementById('stopBtn').style.display = 'none';
    }
});

Common Use Cases: - User wants to cancel a long-running AI response - Response is going in an unwanted direction - User found the answer before generation completed - Implementing "Stop generating" buttons in chat interfaces - Resource management for expensive AI operations

pt.getMessageText(messageId)

Get the text content of a chat message by its ID. Returns the message text as a plain string.

Parameters: - messageId (number, required): The ID of the chat message

Returns: Promise that resolves to a string containing the message text (empty string if message not found)

Usage Examples:

// Get message text
const text = await pt.getMessageText(12345);
console.log('Message:', text);

// Display message in UI
const messageId = 67890;
const content = await pt.getMessageText(messageId);
document.getElementById('messageDisplay').textContent = content;

// Copy message to clipboard
const msgText = await pt.getMessageText(111);
await navigator.clipboard.writeText(msgText);
alert('Message copied to clipboard!');

// Build message history viewer
async function displayRecentMessages(messageIds) {
    const messages = await Promise.all(
        messageIds.map(id => pt.getMessageText(id))
    );

    const html = messages
        .map((text, i) => `
            <div class="message">
                <div class="message-id">Message #${messageIds[i]}</div>
                <div class="message-text">${text}</div>
            </div>
        `)
        .join('');

    document.getElementById('messageHistory').innerHTML = html;
}

// Search for keyword in message
async function checkMessageContains(messageId, keyword) {
    const text = await pt.getMessageText(messageId);
    return text.toLowerCase().includes(keyword.toLowerCase());
}

// Extract and process message content
async function analyzeMessage(messageId) {
    const text = await pt.getMessageText(messageId);

    if (!text) {
        console.log('Message not found or empty');
        return null;
    }

    return {
        length: text.length,
        wordCount: text.split(/\s+/).length,
        hasLinks: text.includes('http'),
        hasEmail: text.includes('@')
    };
}

// Compare two messages
async function compareMessages(id1, id2) {
    const [text1, text2] = await Promise.all([
        pt.getMessageText(id1),
        pt.getMessageText(id2)
    ]);

    return {
        message1: text1,
        message2: text2,
        areSame: text1 === text2,
        lengthDiff: Math.abs(text1.length - text2.length)
    };
}

// Get message for AI processing
async function sendMessageToAI(messageId) {
    const text = await pt.getMessageText(messageId);

    if (text) {
        await pt.addMessage(`Please analyze this message: "${text}"`);
    } else {
        console.error('Could not retrieve message');
    }
}

Common Use Cases: - Display message content in custom UI - Copy message text to clipboard - Search and filter messages by content - Build message history or conversation viewers - Extract message data for processing - Compare or analyze message content - Forward message text to other systems - Create message archives or exports

Related Methods: - Use pt.addMessage() to send new messages to the chat - Use pt.getChatMembers() to get information about message senders - Message IDs are typically obtained from database queries or other API responses

pt.onMessageReceived(taskId, callback)

Subscribe to receive the AI response message for a specific task. When you send a message with awaitResponse: false, the message is queued for processing and you receive a task_id. This method allows you to listen for the completed AI response via Socket.IO events.

Why Use This?

When sending messages without awaiting (awaitResponse: false), the API returns immediately with a task_id:

const result = await pt.addMessage('Hello there', { awaitResponse: false });
// Returns immediately:
// {
//   success: true,
//   user_message: { id: 18695, message: "hello there" },
//   details: "Message has been queued",
//   task_id: "bc45778e-cbe0-4b5e-9071-a5f89033ca10"
// }

The AI then processes the message in the background and streams the response via Socket.IO. The onMessageReceived method lets you capture that response when it's complete.

Socket Events Flow:

When a message is processed, the following Socket.IO events are emitted:

  1. message - User message created (id: 18695)
  2. message - AI message placeholder created (id: 18696)
  3. stream_reasoning_token - Reasoning/thinking tokens (if model supports it)
  4. stream_partial_token - Response tokens as they're generated
  5. stream_completed - Streaming finished for task
  6. message - Final AI message with complete content

The onMessageReceived handler waits for stream_completed and then delivers the final message.

Parameters:

Parameter Type Description
taskId string The task_id returned from addMessage() when awaitResponse is false
callback function Callback function called with the complete AI message object

Returns: function - Unsubscribe function to remove the listener

Message Object Structure:

The callback receives a message object with these properties:

Property Type Description
id number Message ID
message string Full message text (automatically fetched if truncated)
message_is_truncated boolean Always false after processing (full text is fetched)
user_type string 'assistant' for AI responses
type string Message type (e.g., 'message')
created_at string ISO timestamp
chat_uuid string UUID of the chat
reasoning_steps array\|null Array of reasoning steps (if available)
attachments array Array of attached documents
from_virtual_assistant_id number ID of the responding AI agent

Truncated Message Handling:

Long AI responses may be truncated during Socket.IO transmission (messages over ~10KB). The onMessageReceived handler automatically:

  1. Detects if message_is_truncated is true
  2. Calls pt.getMessageText(messageId) to fetch the full text
  3. Replaces the truncated message with the complete text
  4. Sets message_is_truncated to false

This ensures your callback always receives the complete message content.

Basic Usage:

// Send message without waiting
const result = await pt.addMessage('Explain quantum computing', { awaitResponse: false });
const taskId = result.task_id;

// Subscribe to receive the AI response
const unsubscribe = pt.onMessageReceived(taskId, (message) => {
    console.log('AI Response:', message.message);
    console.log('Message ID:', message.id);

    // Display in UI
    document.getElementById('response').textContent = message.message;

    // Clean up listener
    unsubscribe();
});

Promise-Based Wrapper:

// Wrap in a Promise for async/await usage
function sendAndWaitForResponse(text) {
    return new Promise(async (resolve, reject) => {
        try {
            const result = await pt.addMessage(text, { awaitResponse: false });
            const taskId = result.task_id;

            const unsubscribe = pt.onMessageReceived(taskId, (message) => {
                unsubscribe();
                resolve(message);
            });

            // Optional: Add timeout
            setTimeout(() => {
                unsubscribe();
                reject(new Error('Response timeout'));
            }, 60000); // 60 second timeout
        } catch (error) {
            reject(error);
        }
    });
}

// Usage
const response = await sendAndWaitForResponse('What is 2+2?');
console.log('Answer:', response.message);

Multiple Concurrent Messages:

// Track multiple pending responses
const pendingResponses = new Map();

async function sendMessage(text, onResponse) {
    const result = await pt.addMessage(text, { awaitResponse: false });
    const taskId = result.task_id;

    const unsubscribe = pt.onMessageReceived(taskId, (message) => {
        pendingResponses.delete(taskId);
        unsubscribe();
        onResponse(message);
    });

    pendingResponses.set(taskId, { unsubscribe, text });
    return taskId;
}

// Cancel all pending
function cancelAllPending() {
    pendingResponses.forEach(({ unsubscribe }) => unsubscribe());
    pendingResponses.clear();
}

// Usage
sendMessage('Question 1', (msg) => console.log('Answer 1:', msg.message));
sendMessage('Question 2', (msg) => console.log('Answer 2:', msg.message));
sendMessage('Question 3', (msg) => console.log('Answer 3:', msg.message));

With Loading State:

async function askQuestion(question) {
    // Show loading
    const loadingEl = document.getElementById('loading');
    const responseEl = document.getElementById('response');

    loadingEl.style.display = 'block';
    responseEl.textContent = '';

    const result = await pt.addMessage(question, { awaitResponse: false });

    pt.onMessageReceived(result.task_id, (message) => {
        // Hide loading, show response
        loadingEl.style.display = 'none';
        responseEl.textContent = message.message;

        // Show reasoning if available
        if (message.reasoning_steps && message.reasoning_steps.length > 0) {
            const reasoningEl = document.getElementById('reasoning');
            reasoningEl.innerHTML = message.reasoning_steps
                .map(step => `<div class="step">${step.label}: ${step.content}</div>`)
                .join('');
        }
    });
}

Cancel Button:

let currentUnsubscribe = null;

async function sendMessage(text) {
    // Cancel previous if still pending
    if (currentUnsubscribe) {
        currentUnsubscribe();
        currentUnsubscribe = null;
    }

    const result = await pt.addMessage(text, { awaitResponse: false });

    currentUnsubscribe = pt.onMessageReceived(result.task_id, (message) => {
        currentUnsubscribe = null;
        displayResponse(message);
    });
}

// Cancel button handler
document.getElementById('cancelBtn').onclick = () => {
    if (currentUnsubscribe) {
        currentUnsubscribe();
        currentUnsubscribe = null;
        showMessage('Response cancelled');
    }
};

Handling Long Responses:

// Long responses are automatically handled
const result = await pt.addMessage('Write a 5000 word essay about AI', { awaitResponse: false });

pt.onMessageReceived(result.task_id, (message) => {
    // message.message contains the FULL text, even if it was
    // truncated during streaming. The handler automatically
    // fetches the complete text via pt.getMessageText()

    console.log('Full response length:', message.message.length);
    console.log('Was truncated:', message.message_is_truncated); // Always false

    // Safe to use the complete message
    document.getElementById('essay').innerHTML = marked.parse(message.message);
});

Comparison: awaitResponse vs onMessageReceived

Aspect awaitResponse: true awaitResponse: false + onMessageReceived
API Response Waits for AI completion Returns immediately with task_id
UI Blocking Blocks until complete Non-blocking, can show loading state
Streaming UI No streaming feedback Can show streaming tokens via onSocketEvent
Cancellation Cannot cancel Can unsubscribe anytime
Multiple Messages Sequential only Can send multiple in parallel
Error Handling In try/catch In callback or timeout

Best Practices:

  1. Always unsubscribe when done to prevent memory leaks
  2. Add timeouts for production code to handle edge cases
  3. Track pending responses if sending multiple messages
  4. Use with streaming UI - combine with onSocketEvent to show tokens as they arrive
  5. Handle errors gracefully - the callback may never fire if processing fails

Related Methods: - pt.addMessage() - Send messages to the chat - pt.onSocketEvent() - Subscribe to all socket events (for streaming tokens) - pt.getMessageText() - Fetch full message text by ID - pt.stopStreamingMessage() - Stop a streaming response in progress

pt.waitForMessageReceived(taskId, options)

Wait for the AI response message for a specific task. Returns a Promise that resolves when the AI has finished generating its response. This is a convenience wrapper around onMessageReceived for async/await usage.

Parameters:

Parameter Type Default Description
taskId string required The task_id returned from addMessage()
options.timeout number 120000 Timeout in milliseconds (default: 2 minutes)

Returns: Promise<object> - Resolves with the complete AI message object

Throws: Error - If timeout is reached before response arrives

Basic Usage:

// Simple one-liner pattern
const { task_id } = await pt.addMessage('Explain quantum computing');
const { message } = await pt.waitForMessageReceived(task_id);
document.getElementById('answer').textContent = message;

Helper Function Pattern:

// Create a reusable helper
async function ask(question, options = {}) {
    const result = await pt.addMessage(question, options);
    return pt.waitForMessageReceived(result.task_id);
}

// Usage becomes very clean
const response = await ask('What is machine learning?');
console.log(response.message);

With Custom Timeout:

// For complex requests that may take longer
const result = await pt.addMessage('Write a detailed 5000 word essay about AI history');
const response = await pt.waitForMessageReceived(result.task_id, {
    timeout: 300000  // 5 minutes
});

Parallel Questions:

// Ask multiple questions simultaneously
const questions = ['What is AI?', 'What is ML?', 'What is DL?'];

const results = await Promise.all(
    questions.map(q => pt.addMessage(q))
);

const responses = await Promise.all(
    results.map(r => pt.waitForMessageReceived(r.task_id))
);

responses.forEach((r, i) => {
    console.log(`Q: ${questions[i]}`);
    console.log(`A: ${r.message}\n`);
});

Error Handling:

try {
    const result = await pt.addMessage('Complex analysis request');
    const response = await pt.waitForMessageReceived(result.task_id, {
        timeout: 60000
    });
    displayResponse(response.message);
} catch (error) {
    if (error.message.includes('Timeout')) {
        showError('Response is taking too long. Please try again.');
    } else {
        showError(error.message);
    }
}

Comparison: waitForMessageReceived vs onMessageReceived

Aspect waitForMessageReceived onMessageReceived
Style Promise/async-await Callback
Cancellation Not directly Easy with unsubscribe
Code simplicity ✅ Very simple More boilerplate
Error handling try/catch Manual
Best for Most use cases Cancellation, streaming UI

Related Methods: - pt.addMessage() - Send messages to the chat - pt.onMessageReceived() - Callback-based alternative for cancellation support - pt.waitForAllMessagesReceived() - Wait for multiple AI responses - pt.onSocketEvent() - Subscribe to all socket events (for streaming tokens)

pt.waitForAllMessagesReceived(taskIds, options)

Wait for multiple AI responses to complete. Useful when sending multiple messages in parallel and waiting for all responses. Each task is tracked independently, so responses can arrive in any order.

Parameters:

Parameter Type Default Description
taskIds string[] required Array of task_ids returned from addMessage()
options.timeout number 120000 Timeout in milliseconds for ALL responses (default: 2 minutes)
options.failFast boolean true If true, reject immediately on first timeout
options.onProgress function - Callback (completed, total, message) called as each response arrives

Returns: Promise<object[]> - Resolves with array of message objects in same order as taskIds

Basic Usage:

// Send multiple questions and wait for all answers
const questions = ['What is AI?', 'What is ML?', 'What is DL?'];
const results = await Promise.all(questions.map(q => pt.addMessage(q)));
const taskIds = results.map(r => r.task_id);

const responses = await pt.waitForAllMessagesReceived(taskIds);
responses.forEach((r, i) => {
    console.log(`Q: ${questions[i]}`);
    console.log(`A: ${r.message}`);
});

With Progress Tracking:

const responses = await pt.waitForAllMessagesReceived(taskIds, {
    timeout: 180000,
    onProgress: (completed, total, message) => {
        updateProgressBar(completed / total * 100);
        console.log(`${completed}/${total} responses received`);
    }
});

Graceful Degradation (failFast: false):

// Continue even if some timeout
const responses = await pt.waitForAllMessagesReceived(taskIds, {
    failFast: false,
    timeout: 60000
});
// responses may contain null for timed-out tasks
const successful = responses.filter(r => r !== null);

Batch Helper Function:

async function askAll(questions, options = {}) {
    const results = await Promise.all(questions.map(q => pt.addMessage(q)));
    return pt.waitForAllMessagesReceived(
        results.map(r => r.task_id),
        options
    );
}

const answers = await askAll(['Q1?', 'Q2?', 'Q3?']);

Related Methods: - pt.addMessage() - Send messages to the chat - pt.waitForMessageReceived() - Wait for single AI response - pt.onMessageReceived() - Callback-based alternative

pt.onDocumentChanged(callback, options)

Subscribe to document change events. This method listens for document_change socket events and calls the callback whenever a document's status, extracted text, or indexing state changes. Useful for tracking document processing progress after uploads.

Parameters:

Parameter Type Description
callback function Called with document object on each change
options.documentId number Filter to specific document ID
options.documentIds number[] Filter to multiple document IDs
options.status string Filter to specific status ('Added', 'Ready', 'Error')
options.statuses string[] Filter to multiple statuses

Returns: function - Unsubscribe function to remove the listener

Document Change Object Structure:

Property Type Description
id number Document ID
uuid string Document UUID
name string Document filename
mimetype string MIME type (e.g., 'application/pdf')
status string 'Added', 'Ready', or 'Error'
extracted_text_size number\|null Characters extracted (null if not ready)
indexed string 'True' or 'False'
path string Document path in folder structure
chat_uuid string UUID of the chat
download_url string URL to download the document

Basic Usage:

// Subscribe to all document changes
const unsubscribe = pt.onDocumentChanged((doc) => {
    console.log(`Document ${doc.name} changed:`);
    console.log(`  Status: ${doc.status}`);
    console.log(`  Indexed: ${doc.indexed}`);
});

Track Specific Document:

const result = await pt.uploadFiles(form);
const docId = result.documents[0].id;

const unsubscribe = pt.onDocumentChanged((doc) => {
    console.log(`Document ${doc.name}: ${doc.status}`);
    if (doc.status === 'Ready') {
        console.log('Document ready! Text size:', doc.extracted_text_size);
        unsubscribe();
    }
}, { documentId: docId });

Track Multiple Documents:

const result = await pt.uploadFiles(form);
const docIds = result.documents.map(d => d.id);
const readyDocs = new Set();

const unsubscribe = pt.onDocumentChanged((doc) => {
    if (doc.status === 'Ready') {
        readyDocs.add(doc.id);
        updateProgress(readyDocs.size, docIds.length);

        if (readyDocs.size === docIds.length) {
            console.log('All documents ready!');
            unsubscribe();
        }
    }
}, { documentIds: docIds });

Filter by Status:

// Only listen for 'Ready' status
pt.onDocumentChanged((doc) => {
    console.log(`${doc.name} is now ready for RAG search`);
}, { status: 'Ready' });

// Listen for errors
pt.onDocumentChanged((doc) => {
    showError(`Failed to process ${doc.name}`);
}, { status: 'Error' });

pt.waitForDocumentReady(documentId, options)

Wait for a document to reach 'Ready' status. Returns a Promise that resolves when the document is fully processed, text extracted, and indexed for RAG search. Useful after uploading files when you need to wait before accessing the extracted text.

Parameters:

Parameter Type Default Description
documentId number required Document ID to wait for
options.timeout number 60000 Timeout in milliseconds (default: 60 seconds)
options.rejectOnError boolean true If true, reject promise on 'Error' status

Returns: Promise<object> - Resolves with the document object when ready

Basic Usage:

// Upload and wait for document to be ready
const result = await pt.uploadFiles(form);
const docId = result.documents[0].id;

try {
    const doc = await pt.waitForDocumentReady(docId);
    console.log('Document ready! Text size:', doc.extracted_text_size);

    // Now safe to get the extracted text
    const text = await pt.getDocumentText(docId);
    console.log('Extracted text:', text);
} catch (error) {
    console.error('Document processing failed:', error.message);
}

With Custom Timeout:

// For large files that take longer to process
const doc = await pt.waitForDocumentReady(docId, { 
    timeout: 300000  // 5 minutes
});

Wait for Multiple Documents:

const result = await pt.uploadFiles(form);
const docIds = result.documents.map(d => d.id);

const readyDocs = await Promise.all(
    docIds.map(id => pt.waitForDocumentReady(id))
);
console.log('All documents ready:', readyDocs.map(d => d.name));

Handle Errors Gracefully:

// Don't reject on error, handle manually
const doc = await pt.waitForDocumentReady(docId, { rejectOnError: false });
if (doc.status === 'Error') {
    console.log('Document failed, but we can handle it');
} else {
    console.log('Document ready');
}

Related Methods: - pt.uploadFiles() - Upload files to chat - pt.onDocumentChanged() - Callback-based alternative for multiple documents - pt.waitForAllDocumentsReady() - Wait for multiple documents to be ready - pt.getDocumentText() - Get extracted text content - pt.getDocumentStatus() - Check processing status

pt.waitForAllDocumentsReady(documentIds, options)

Wait for multiple documents to reach 'Ready' status. Useful after uploading multiple files when you need all of them processed before continuing. Each document is tracked independently via socket events.

Parameters:

Parameter Type Default Description
documentIds number[] required Array of document IDs to wait for
options.timeout number 120000 Timeout in milliseconds for ALL documents (default: 2 minutes)
options.failFast boolean true If true, reject immediately on first Error
options.rejectOnError boolean true If true, reject on document processing errors
options.onProgress function - Callback (completed, total, document) called as each document becomes ready

Returns: Promise<object[]> - Resolves with array of document objects in same order as documentIds

Basic Usage:

// Upload multiple files and wait for all to be ready
const result = await pt.uploadFiles(form);
const docIds = result.documents.map(d => d.id);

const readyDocs = await pt.waitForAllDocumentsReady(docIds);
console.log('All documents ready:', readyDocs.map(d => d.name));

With Progress Tracking:

const readyDocs = await pt.waitForAllDocumentsReady(docIds, {
    timeout: 180000,
    onProgress: (completed, total, doc) => {
        updateProgressBar(completed / total * 100);
        console.log(`${doc.name} ready (${completed}/${total})`);
    }
});

Graceful Error Handling (failFast: false):

// Continue even if some fail
const docs = await pt.waitForAllDocumentsReady(docIds, {
    failFast: false,
    rejectOnError: false
});
const successful = docs.filter(d => d && d.status === 'Ready');
const failed = docs.filter(d => d && d.status === 'Error');
console.log(`${successful.length} ready, ${failed.length} failed`);

Upload and Wait Helper:

async function uploadAndWait(formData, folder) {
    const result = await pt.uploadFiles(formData, folder);
    const docIds = result.documents.map(d => d.id);
    return pt.waitForAllDocumentsReady(docIds);
}

const docs = await uploadAndWait(form, 'reports');

Related Methods: - pt.uploadFiles() - Upload files to chat - pt.waitForDocumentReady() - Wait for single document - pt.onDocumentChanged() - Callback-based alternative - pt.getDocumentText() - Get extracted text content

pt.uploadFiles() - Silent Document Upload

Upload files directly to the chat's document library without creating messages or triggering AI processing. This is ideal for building document libraries, organizing files, or programmatic uploads.

Syntax:

pt.uploadFiles(formOrFormData, folder?, documentName?)

Parameters:

Parameter Type Description
formOrFormData HTMLFormElement | FormData Form or FormData with files
folder string? Optional folder path (e.g., "reports/2024")
documentName string? Optional custom name for the uploaded file. If not provided, the original filename is used. Useful for single file uploads where you want to rename the document.

Returns: Promise that resolves to an object containing:

{
    success: boolean;
    message: string;
    folder: string;
    documents: Array<{
        id: number;
        uuid: string;
        name: string;
        mimetype: string;
        size_bytes: number;
        status: 'pending' | 'processing' | 'ready' | 'failed';
        created_at: string;
        updated_at: string;
    }>;
}

Downloading Uploaded Files:

After uploading files with uploadFiles, you can access them using the File Download API:

For live apps: /api/v1/live_apps/{chat_id}/{file_path}

The file_path is the combination of the folder parameter you provided and the document name. For example, if you uploaded to reports/2024, the file path would be reports/2024/filename.pdf. See File Download API for complete details.

Usage:

Basic upload:

const form = document.getElementById('uploadForm');
const result = await pt.uploadFiles(form);

console.log(`Uploaded ${result.documents.length} files`);
console.log(`Saved to: ${result.folder}`);

result.documents.forEach(doc => {
    console.log(`- ${doc.name} (ID: ${doc.id}, Status: ${doc.status})`);
});

Upload to specific folder:

const fileInput = document.getElementById('fileInput');
const formData = new FormData();
for (const file of fileInput.files) {
    formData.append('files', file);
}

const result = await pt.uploadFiles(formData, 'reports/monthly/2024');
console.log(`Files uploaded to: ${result.folder}`);

Upload with custom document name:

const formData = new FormData();
formData.append('files', myFile);

// Rename the uploaded file to a custom name
const result = await pt.uploadFiles(formData, 'documents', 'quarterly-report-2026.pdf');
console.log(`Uploaded as: ${result.documents[0].name}`);

Get document IDs for later use:

// Upload files
const result = await pt.uploadFiles(form, 'images/2024');

// Extract document IDs
const docIds = result.documents.map(doc => doc.id);

// Use IDs to search within these documents
const searchResults = await pt.searchDocuments(
    'keyword',
    'DOCUMENTS_ONLY',
    docIds
);

Check upload status:

const result = await pt.uploadFiles(form);

// Check if all uploads succeeded
const allReady = result.documents.every(doc =>
    doc.status === 'ready' || doc.status === 'pending'
);

if (!allReady) {
    console.error('Some uploads failed');
    const failed = result.documents.filter(doc => doc.status === 'failed');
    failed.forEach(doc => console.error(`Failed: ${doc.name}`));
}

Organize uploads by type:

async function uploadByType(files) {
    const formData = new FormData();

    for (const file of files) {
        formData.append('files', file);
    }

    // Determine folder based on file type
    const firstFile = files[0];
    let folder = 'documents';

    if (firstFile.type.startsWith('image/')) {
        folder = 'images';
    } else if (firstFile.type === 'application/pdf') {
        folder = 'pdfs';
    }

    return await pt.uploadFiles(formData, folder);
}

Complete example with HTML:

<form id="uploadForm">
    <input type="file" name="files" multiple required>
    <button type="submit">Upload Files</button>
</form>

<script>
document.getElementById('uploadForm').addEventListener('submit', async (e) => {
    e.preventDefault();
    const form = e.target;

    try {
        const result = await pt.uploadFiles(form, 'documents/uploads');

        alert(`Uploaded ${result.documents.length} file(s) successfully!`);
        console.log('Document details:', result.documents);

        form.reset();
    } catch (error) {
        alert('Upload failed: ' + error.message);
    }
});
</script>

Important Notes: - No message is created in the chat - No AI processing is triggered - Files are added directly to the document library - Returns detailed document information including IDs and UUIDs - Supports multiple file uploads in a single request - File size limits and restrictions apply based on system configuration

Text Extraction Workflow:

When files are uploaded, PrimeThink automatically extracts text content in the background. This is an asynchronous process that takes a few seconds depending on file size and complexity.

Workflow for retrieving extracted text:

  1. Upload - Use pt.uploadFiles() to upload documents (optionally specify a folder)
  2. Wait - Poll document status using pt.getDocumentStatus(docId) until extraction completes
  3. Retrieve - Use pt.getDocumentText(docId) to get the extracted Markdown text

Status Indicators: - status: "Added" = Extraction not yet complete (keep waiting) - status: "Error" = Extraction failed - Any other status ("Processed", "Ready", "Indexed", etc.) = Extraction complete, text available

Complete Example with Text Extraction:

// Upload documents and wait for text extraction
async function uploadAndExtractText(files) {
    // Step 1: Upload files
    const formData = new FormData();
    files.forEach(file => formData.append('files', file));

    const uploadResult = await pt.uploadFiles(formData, 'documents/analysis');
    console.log(`Uploaded ${uploadResult.documents.length} files`);

    // Step 2: Wait for extraction to complete
    const extractedDocs = [];

    for (const doc of uploadResult.documents) {
        console.log(`Waiting for extraction: ${doc.name}...`);

        // Poll status until extraction completes
        let status = await pt.getDocumentStatus(doc.id);

        while (status.document_status === 'Added') {
            // Wait 2 seconds before checking again
            await new Promise(resolve => setTimeout(resolve, 2000));
            status = await pt.getDocumentStatus(doc.id);
        }

        if (status.document_status === 'Error') {
            console.error(`Extraction failed for ${doc.name}`);
            continue;
        }

        // Step 3: Retrieve extracted text
        console.log(`Extraction complete for ${doc.name}, status: ${status.document_status}`);
        const textResponse = await pt.getDocumentText(doc.id);

        extractedDocs.push({
            id: doc.id,
            name: doc.name,
            text: textResponse.result.text,
            size: textResponse.result.text.length
        });
    }

    return extractedDocs;
}

// Usage
const files = document.getElementById('fileInput').files;
const extractedDocs = await uploadAndExtractText(files);

extractedDocs.forEach(doc => {
    console.log(`${doc.name}: ${doc.size} characters extracted`);
    console.log('First 200 chars:', doc.text.substring(0, 200));
});

Optimized Polling with Timeout:

async function waitForExtraction(docId, maxWaitMs = 60000) {
    const startTime = Date.now();

    while (Date.now() - startTime < maxWaitMs) {
        const status = await pt.getDocumentStatus(docId);

        // Extraction complete
        if (status.document_status !== 'Added' && status.document_status !== 'Error') {
            return { success: true, status: status.document_status };
        }

        // Extraction failed
        if (status.document_status === 'Error') {
            return { success: false, error: 'Extraction failed' };
        }

        // Wait before next check
        await new Promise(resolve => setTimeout(resolve, 2000));
    }

    return { success: false, error: 'Timeout waiting for extraction' };
}

// Usage
const result = await pt.uploadFiles(formData);
const docId = result.documents[0].id;

const extractionResult = await waitForExtraction(docId);

if (extractionResult.success) {
    const response = await pt.getDocumentText(docId);
    console.log('Text extracted:', response.result.text);
} else {
    console.error('Extraction failed:', extractionResult.error);
}

Common Use Cases: - Building document libraries - Programmatic file organization - Background file uploads - Document management systems - Batch file processing - API integrations

When to Use Which Method:

Method Purpose Creates Message Triggers AI Returns
pt.addMessage(form, message) Upload with message/AI processing Yes Yes Message info + file count
pt.uploadFiles(form, folder?, documentName?) Silent document library upload No No Detailed document info

Migrating from Old API:

If you were using the old pt.uploadFiles(form, message, hidden, awaitResponse) signature:

// OLD API (deprecated)
await pt.uploadFiles(form, 'Check these files', false, true);

// NEW API - Use pt.addMessage() instead
await pt.addMessage(form, 'Check these files', {
    awaitResponse: true
});

pt.sendNotification(userId, title, text)

Send push notifications to specific users in the chat.

Parameters: - userId (number, required): The ID of the user to notify - title (string, required): Notification title - text (string, required): Notification message text

Returns: Promise that resolves to an object with message field indicating success

Usage:

// Send notification to a specific user
const members = await pt.getChatMembers();
const targetUser = members.find(m => m.email === 'john@example.com');

await pt.sendNotification(
    targetUser.id,
    'Task Assigned',
    'You have been assigned a new task'
);

// Send notification after task assignment
async function assignTask(taskId, userId) {
    const task = await pt.get(taskId);
    await pt.edit(taskId, { assigned_to: userId }, true);  // merge mode

    // Notify the user
    await pt.sendNotification(
        userId,
        'New Task',
        `You've been assigned: ${task.data.title}`
    );
}

// Notify user when task is completed
async function completeTask(taskId) {
    const task = await pt.get(taskId);
    await pt.edit(taskId, { completed: true }, true);  // merge mode

    if (task.data.assigned_to) {
        await pt.sendNotification(
            task.data.assigned_to,
            'Task Completed',
            `Task "${task.data.title}" has been marked as complete`
        );
    }
}

Common Use Cases: - Task assignment notifications - Alert users about important updates - Remind users of pending actions - Notify collaborators of changes

pt.searchDocuments(query, scope, documentIds)

Perform semantic search across documents and collections using RAG (Retrieval-Augmented Generation).

Parameters: - query (string, required): Natural language search query - scope (string, optional): Search scope - "ALL", "DOCUMENTS_ONLY", or "COLLECTIONS_ONLY" (default: "ALL") - documentIds (array of numbers, optional): Limit search to specific document IDs only

Returns: Promise that resolves to an object with results field containing XML-formatted search results

Usage:

// Search all documents and collections
const result = await pt.searchDocuments('quarterly financial results Q3 2024');
console.log(result.results);

// Search only documents
const docsOnly = await pt.searchDocuments('project requirements', 'DOCUMENTS_ONLY');

// Search only collections
const collectionsOnly = await pt.searchDocuments('company policies', 'COLLECTIONS_ONLY');

// Search specific documents only (NEW)
const specificDocs = await pt.searchDocuments(
    'budget analysis',
    'DOCUMENTS_ONLY',
    [156, 157, 158]  // Only search these document IDs
);

// Search documents in a folder
async function searchInFolder(query, folderPath) {
    // Get documents in folder
    const folderContents = await pt.listDirectory(folderPath);
    const documentIds = folderContents.entries
        .filter(e => e.type === 'file')
        .map(e => e.id);

    // Search only those documents
    return await pt.searchDocuments(query, 'DOCUMENTS_ONLY', documentIds);
}

// Build a search interface
async function searchKnowledgeBase(query) {
    try {
        const result = await pt.searchDocuments(query);
        displaySearchResults(result.results);
    } catch (error) {
        console.error('Search failed:', error);
    }
}

// Search with user input
async function handleSearch() {
    const query = document.getElementById('searchInput').value;
    const scope = document.getElementById('scopeSelect').value;

    if (!query) {
        alert('Please enter a search query');
        return;
    }

    const results = await pt.searchDocuments(query, scope);
    document.getElementById('resultsArea').textContent = results.results;
}

Common Use Cases: - Knowledge base search - Find relevant documents - Research and discovery - Content recommendation

pt.getDocumentText(docId, options)

Retrieve the text content from a document.

Parameters: - docId (number, required): Document ID - options (object, optional): Options object with: - from (number): Start character position (default: 0) - to (number): End character position (default: null for full text)

Returns: Promise that resolves to an object containing: - success (boolean): Whether the operation succeeded - action (string): Action name ("get_document_text") - result (object): Result object containing: - document_id (number): The document ID - text (string | null): Document text content in Markdown format (or null if not ready) - full_text (boolean, optional): true if returning complete document text (present when text is available) - status (string, optional): Processing status if document not ready (e.g., "DocumentStatus.added") - message (string, optional): Status message if document not ready

Response Examples:

When document is not ready:

{
    "success": true,
    "action": "get_document_text",
    "result": {
        "document_id": 1339,
        "status": "DocumentStatus.added",
        "message": "Document not ready. Current status: DocumentStatus.added",
        "text": null
    }
}

When document is ready:

{
    "success": true,
    "action": "get_document_text",
    "result": {
        "document_id": 1339,
        "text": "# Document Title\n\nDocument content in Markdown format...",
        "full_text": true
    }
}

About Text Extraction:

When documents are uploaded, PrimeThink automatically extracts text content asynchronously in the background. The extraction process:

  • Is Asynchronous: Takes a few seconds depending on file size and complexity
  • Returns Markdown: Extracted text is formatted as Markdown, preserving structure, tables, and formatting
  • Requires Status Check: Before calling this method, ensure the document status is no longer "Added" by using pt.getDocumentStatus()

Best Practice: After uploading a document with pt.uploadFiles() or pt.addMessage(), wait for the extraction to complete:

// 1. Upload document
const upload = await pt.uploadFiles(formData, 'folder');
const docId = upload.documents[0].id;

// 2. Wait for extraction
let status = await pt.getDocumentStatus(docId);
while (status.document_status === 'Added') {
    await new Promise(r => setTimeout(r, 2000)); // Wait 2 seconds
    status = await pt.getDocumentStatus(docId);
}

// 3. Retrieve text (only after status is no longer "Added")
const response = await pt.getDocumentText(docId);
console.log('Extracted text:', response.result.text);

Usage:

// Get full document text
const response = await pt.getDocumentText(123);
console.log(response.result.text);

// Get first 1000 characters (preview)
const preview = await pt.getDocumentText(123, { from: 0, to: 1000 });
console.log(preview.result.text);

// Get text starting from character 500
const partial = await pt.getDocumentText(123, { from: 500 });
console.log(partial.result.text);

// Build a document viewer with status handling
async function viewDocument(docId) {
    try {
        const response = await pt.getDocumentText(docId);
        const result = response.result;

        if (result.text) {
            // Document is ready
            document.getElementById('documentContent').textContent = result.text;

            if (result.full_text) {
                console.log('Showing complete document');
            } else {
                console.log('Showing partial document text');
            }
        } else if (result.status) {
            // Document is still processing
            alert(`Document not ready. Status: ${result.status}\n${result.message}`);

            // Optionally retry after a delay
            setTimeout(() => viewDocument(docId), 5000);
        } else {
            alert('No text available');
        }
    } catch (error) {
        console.error('Error loading document:', error);
    }
}

// Load document preview
async function loadPreview(docId) {
    const response = await pt.getDocumentText(docId, { to: 500 });
    const result = response.result;

    if (result.text) {
        document.getElementById('preview').textContent = result.text + '...';
    } else if (result.status) {
        document.getElementById('preview').textContent = '⏳ Document is being processed...';
    }
}

// Handle document status with polling
async function loadDocumentWithRetry(docId, maxRetries = 5) {
    for (let i = 0; i < maxRetries; i++) {
        const response = await pt.getDocumentText(docId);
        const result = response.result;

        if (result.text) {
            // Document ready
            return result;
        }

        if (result.status) {
            console.log(`Document still processing, attempt ${i + 1}/${maxRetries}`);
            console.log(`Status: ${result.status}`);
            await new Promise(resolve => setTimeout(resolve, 3000)); // Wait 3 seconds
        } else {
            // Document has an error or is unavailable
            throw new Error(result.message || 'Document unavailable');
        }
    }

    throw new Error('Document processing timeout');
}

Common Use Cases: - Document preview - Content analysis - Text extraction - Partial content loading for large documents

pt.saveDocument(filename, format, mimetype, content, folder)

Create and save content as a document in the chat.

Parameters: - filename (string, required): Filename with extension - format (string, required): Format type - "TXT", "MD", "HTML", "DOCX", "PDF", "CSV", "XLSX", or "CUSTOM" - mimetype (string, required): MIME type (e.g., "text/plain", "application/pdf") - content (string, required): Document content - folder (string, optional): Destination folder path (e.g., "reports", "exports/monthly")

Storage Locations:

The folder parameter supports special prefixes for different access levels: - @public - Public access (no authentication required). Use for files that should be publicly accessible. - @liveapp - Group-level access (authentication + group membership required). Use for files shared across the group. - No prefix - Chat-specific access (authentication + chat membership required). Default and most secure option.

Examples: - @public - Save to public folder - @liveapp - Save to group-wide live app folder - reports - Save to chat-specific "reports" folder - exports/monthly - Save to chat-specific nested folder

For detailed information about file storage hierarchy and access control, see File Storage Hierarchy.

Format Guidelines: - TXT: Plain text (no Markdown) - MD/MARKDOWN: Markdown text - HTML: HTML content (not Markdown) - DOCX: Use Markdown text (will be converted to Word format) - PDF: Use Markdown text (will be converted to PDF) - CSV: Plain CSV format text - XLSX: Plain CSV format text (will be converted to Excel) - CUSTOM: Any non-binary plain text format

Returns: Promise that resolves to an object containing: - success: Boolean indicating if the operation succeeded - action: String "save_document" - result: Object containing: - success: Boolean indicating document save success - message: Success message (e.g., "Document(s) saved") - filename: Name of the saved file - format: Format type used (e.g., "PDF") - documents: Array of document objects, each containing: - id: Document ID (number) - uuid: Document UUID (string) - name: Document filename (string) - mimetype: MIME type (string) - status: Status (e.g., "Added") - extracted_text_size: Size of extracted text in bytes (number) - download_url: URL to download the document (string)

Response Structure:

{
  "success": true,
  "action": "save_document",
  "result": {
    "success": true,
    "message": "Document(s) saved",
    "filename": "Morzine_Expense_Report_2026-01-20.pdf",
    "format": "PDF",
    "documents": [
      {
        "id": 2301,
        "uuid": "592474de-a7e6-48ce-b61c-098e9369e34d",
        "name": "Morzine_Expense_Report_2026-01-20.pdf",
        "mimetype": "application/pdf",
        "status": "Added",
        "extracted_text_size": 3715,
        "download_url": "https://api.primethink.ai/api/v1/documents/uuid/592474de-a7e6-48ce-b61c-098e9369e34d/download"
      }
    ]
  }
}

Downloading Saved Files:

After saving a document, you can download it using the file path and the File Download API. The download_url in the response provides a UUID-based download link, but you can also use the simpler path-based download endpoint.

For live apps, use: /api/v1/live_apps/{chat_id}/{file_path}

Where file_path is the folder and filename you used when saving. See File Download API for details.

Usage:

// Save a plain text document
const result = await pt.saveDocument(
    'notes.txt',
    'TXT',
    'text/plain',
    'These are my meeting notes from today.'
);

// Access document details
const savedDoc = result.result.documents[0];
console.log('Document ID:', savedDoc.id);
console.log('Document UUID:', savedDoc.uuid);
console.log('Download URL:', savedDoc.download_url);

// Save a Markdown document
await pt.saveDocument(
    'report.md',
    'MD',
    'text/markdown',
    '# Project Report\n\n## Summary\n\nThe project is progressing well.'
);

// Save as Word document (using Markdown)
await pt.saveDocument(
    'proposal.docx',
    'DOCX',
    'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
    '# Business Proposal\n\n## Executive Summary\n\nThis proposal outlines...'
);

// Save as PDF (using Markdown)
const pdfResult = await pt.saveDocument(
    'invoice.pdf',
    'PDF',
    'application/pdf',
    '# Invoice #12345\n\n**Date:** 2025-01-15\n**Amount:** $1,250.00'
);

// Access document details from the response
const pdfDoc = pdfResult.result.documents[0];
console.log('PDF saved:', pdfResult.result.filename);
console.log('Document ID:', pdfDoc.id);
console.log('UUID:', pdfDoc.uuid);
console.log('Download URL:', pdfDoc.download_url);
console.log('Extracted text size:', pdfDoc.extracted_text_size, 'bytes');

// Save CSV data
await pt.saveDocument(
    'data.csv',
    'CSV',
    'text/csv',
    'Name,Email,Age\nJohn Doe,john@example.com,30\nJane Smith,jane@example.com,28'
);

// Save to a specific folder
await pt.saveDocument(
    'monthly-report.pdf',
    'PDF',
    'application/pdf',
    '# Monthly Report\n\n## Q1 2025\n\nRevenue increased by 15%.',
    'reports/2025/Q1'  // Folder path
);

// Organize documents by type
await pt.saveDocument(
    'customer-invoice.pdf',
    'PDF',
    'application/pdf',
    invoiceContent,
    'invoices'
);

await pt.saveDocument(
    'meeting-notes.md',
    'MD',
    'text/markdown',
    notesContent,
    'meetings/2025-01'
);

// Save to public folder (accessible without authentication)
await pt.saveDocument(
    'brochure.pdf',
    'PDF',
    'application/pdf',
    '# Company Brochure\n\nWelcome to our company!',
    '@public'
);

// Save to live app folder (group-wide access)
await pt.saveDocument(
    'template.html',
    'HTML',
    'text/html',
    '<div>Reusable template</div>',
    '@liveapp'
);

// Generate and save a report with document details
async function generateReport(data) {
    const content = `# Monthly Report

## Summary
Total items: ${data.length}

## Details
${data.map(item => `- ${item.name}: ${item.value}`).join('\n')}
`;

    const result = await pt.saveDocument(
        'monthly-report.pdf',
        'PDF',
        'application/pdf',
        content
    );

    // Access document details directly from JSON response
    const doc = result.result.documents[0];

    const documentDetails = {
        id: doc.id,
        uuid: doc.uuid,
        name: doc.name,
        downloadUrl: doc.download_url,
        size: doc.extracted_text_size
    };

    alert(`Report generated successfully!\nDocument ID: ${documentDetails.id}\nUUID: ${documentDetails.uuid}`);
    return documentDetails;
}

// Helper function for common MIME types
function getMimeType(format) {
    const types = {
        'TXT': 'text/plain',
        'MD': 'text/markdown',
        'HTML': 'text/html',
        'DOCX': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        'PDF': 'application/pdf',
        'CSV': 'text/csv',
        'XLSX': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
    };
    return types[format] || 'text/plain';
}

// Save with helper
await pt.saveDocument('report.pdf', 'PDF', getMimeType('PDF'), content);

// Working with document response
async function saveAndProcessDocument(content, filename) {
    const result = await pt.saveDocument(
        filename,
        'PDF',
        'application/pdf',
        content
    );

    if (result.success && result.result.success) {
        // Access the saved document details
        const doc = result.result.documents[0];

        // You can now use the document details
        console.log(`Document saved successfully!`);
        console.log(`- ID: ${doc.id}`);
        console.log(`- UUID: ${doc.uuid}`);
        console.log(`- Name: ${doc.name}`);
        console.log(`- MIME Type: ${doc.mimetype}`);
        console.log(`- Status: ${doc.status}`);
        console.log(`- Text Size: ${doc.extracted_text_size} bytes`);
        console.log(`- Download: ${doc.download_url}`);

        // Store document ID for later use
        await pt.add('document_metadata', {
            document_id: doc.id,
            document_uuid: doc.uuid,
            filename: doc.name,
            created_date: new Date().toISOString()
        });

        return doc;
    } else {
        console.error('Failed to save document');
        return null;
    }
}

// Document organization pattern
async function saveOrganizedDocument(type, data, date = new Date()) {
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, '0');

    const folders = {
        'invoice': `invoices/${year}/${month}`,
        'report': `reports/${year}/Q${Math.ceil((date.getMonth() + 1) / 3)}`,
        'meeting': `meetings/${year}/${month}`,
        'contract': `contracts/${year}`
    };

    const folder = folders[type] || 'documents';

    return await pt.saveDocument(
        data.filename,
        data.format,
        getMimeType(data.format),
        data.content,
        folder
    );
}

// Usage: Automatically organized by date and type
await saveOrganizedDocument('invoice', {
    filename: 'invoice-12345.pdf',
    format: 'PDF',
    content: invoiceMarkdown
});
// Saves to: invoices/2025/01/invoice-12345.pdf

await saveOrganizedDocument('report', {
    filename: 'quarterly-summary.pdf',
    format: 'PDF',
    content: reportMarkdown
});
// Saves to: reports/2025/Q1/quarterly-summary.pdf

MIME Type Reference: - Plain text: text/plain - Markdown: text/markdown - HTML: text/html - Word: application/vnd.openxmlformats-officedocument.wordprocessingml.document - PDF: application/pdf - CSV: text/csv - Excel: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet

Common Use Cases: - Report generation - Export data to various formats - Create documentation - Save user-generated content - Generate invoices and forms

pt.listDirectory(path)

List directory contents at a path in the chat's document structure (Unix ls-like navigation).

Parameters: - path (string, required): Path to list (e.g., /, /reports, /reports/2024)

Special Folders: The system includes special folders with different access levels: - / (root) - Lists chat-specific files and folders - Public files are stored in a special @public location (accessible without authentication) - Group-wide files are stored in a special @liveapp location (accessible to all group members)

See File Storage Hierarchy for details on access control and storage locations.

Returns: Promise resolving to:

{
    path: "/reports",
    entries: [
        {
            type: "dir",
            id: 42,
            name: "2024",
            custom_name: null,
            status: null,
            has_children: true,
            path: "reports.2024"
        },
        {
            type: "file",
            id: 156,
            name: "summary.pdf",
            custom_name: "Q4 Summary",
            status: "search",
            has_children: null,
            path: "reports"
        }
    ]
}

Usage:

// List root directory
const root = await pt.listDirectory('/');
console.log('Root folders:', root.entries.filter(e => e.type === 'dir'));

// List specific folder
const reports = await pt.listDirectory('/reports');
console.log('Documents in reports:', reports.entries.filter(e => e.type === 'file'));

// Build file browser
async function buildFileBrowser(path = '/') {
    const contents = await pt.listDirectory(path);

    return contents.entries.map(entry => {
        if (entry.type === 'dir') {
            return `📁 ${entry.name} (${entry.has_children ? 'has items' : 'empty'})`;
        } else {
            return `📄 ${entry.custom_name || entry.name} [${entry.status}]`;
        }
    }).join('\n');
}

pt.getDocumentStatus(docId)

Check the current processing/availability status of a document.

Parameters: - docId (number, required): The document ID to check

Returns:

{
    document_id: 156,
    document_status: "ready"  // or "Added", "Processed", "Ready", "Indexed", "Error", etc.
}

Understanding Status for Text Extraction:

When documents are uploaded, PrimeThink automatically extracts text content asynchronously. The status indicates where the document is in the extraction pipeline:

  • "Added" - Document uploaded, text extraction not yet complete
  • "Error" - Text extraction failed
  • Any other status ("Processed", "Ready", "Indexed", etc.) - Text extraction complete, content available

Use this method to poll for extraction completion after uploading documents with pt.uploadFiles() or pt.addMessage(). Once the status is no longer "Added", you can safely retrieve the extracted text using pt.getDocumentText().

Usage:

// Check if document is ready
const status = await pt.getDocumentStatus(156);
if (status.document_status === 'ready') {
    const text = await pt.getDocumentText(156);
} else {
    console.log(`Document status: ${status.document_status}`);
}

// Poll until ready
async function waitForDocument(docId, maxWait = 60000) {
    const start = Date.now();
    while (Date.now() - start < maxWait) {
        const status = await pt.getDocumentStatus(docId);
        if (status.document_status === 'ready') return true;
        if (status.document_status === 'failed') throw new Error('Document processing failed');
        await new Promise(r => setTimeout(r, 2000));
    }
    throw new Error('Document processing timeout');
}

pt.getDocumentInfo(documentPath)

Get detailed information about a document by its path. Returns a DocumentInChat object with the full Document object nested inside, providing complete metadata about both the document-in-chat relationship and the underlying document.

Parameters: - documentPath (string, required): Path to the document - Supports @public prefix for public files - Supports @liveapp prefix for live app files - Supports chat-relative paths like "folder/file.txt" or "/folder/file.txt" - Simple filenames like "file.txt" search in order: @public → @liveapp → chat root

Returns: Promise that resolves to a DocumentInChat object with nested document:

{
  "created_at": "2026-02-01T19:15:17.618597+00:00",  // When added to chat
  "from_user_id": 1,                                 // User who uploaded
  "indexed": true,                                   // Whether indexed for RAG search
  "name": "translation_1900.pdf",                    // Display name in chat
  "status": "search",                                // Document-in-chat status
  "type": "document",                                // Type identifier
  "path": "/translations",                           // Folder path in chat
  "document": {                                      // Full Document object
    "id": 2368,                                      // Document ID
    "uuid": "eeb36af1-5875-42e0-9b43-4d2d47dc20f7", // Document UUID
    "name": "translation_1900.pdf",                  // Original filename
    "mimetype": "application/pdf",                   // MIME type
    "status": "Ready",                               // Processing status
    "extracted_text_size": 1175,                     // Size of extracted text in chars
    "download_url": "https://api.primethink.ai/...", // URL to download
    "short_description": "Brief description...",     // AI-generated short description
    "summary": "Detailed summary...",                // AI-generated summary
    "keywords": "keyword1, keyword2, ...",           // AI-generated keywords
    "keypoints": "Point 1, Point 2, ..."             // AI-generated key points
  }
}

Response Structure:

DocumentInChat fields: - created_at (string): When the document was added to the chat (ISO 8601) - from_user_id (number): ID of the user who uploaded the document - indexed (boolean): Whether the document is indexed for RAG search - name (string): Display name in chat - status (string): Document-in-chat status ("search", "hidden", etc.) - type (string): Type identifier (typically "document") - path (string): Folder path in chat's document hierarchy

Nested document object: - id (number): Document ID - uuid (string): Document UUID - name (string): Original filename - mimetype (string): MIME type - status (string): Processing status ("Added", "Ready", "Error") - extracted_text_size (number): Size of extracted text in characters (0 if text not yet extracted) - download_url (string): URL to download the document - short_description (string?): AI-generated short description (null or absent if analysis not finished) - summary (string?): AI-generated summary (null or absent if analysis not finished) - keywords (string?): AI-generated keywords (null or absent if analysis not finished) - keypoints (string?): AI-generated key points (null or absent if analysis not finished)

Processing States:

Documents go through multiple processing stages:

  1. Text Extraction (extracted_text_size):
  2. 0 = Text not yet extracted
  3. > 0 = Text extraction complete

  4. AI Analysis (short_description, summary, keywords, keypoints):

  5. null or absent = Analysis not yet complete
  6. Present with values = Analysis complete

These stages happen sequentially: text is extracted first, then AI analysis runs on the extracted text.

Usage Examples:

// Get info about a live app file
const docInfo = await pt.getDocumentInfo('@liveapp/config.json');
console.log('Document ID:', docInfo.document.id);
console.log('Status:', docInfo.document.status);
console.log('MIME type:', docInfo.document.mimetype);

// Check if document extraction is ready
const docInfo = await pt.getDocumentInfo('reports/annual.pdf');
if (docInfo.document.status === 'Ready') {
    console.log('Document text is ready');
    const text = await pt.getDocumentText(docInfo.document.id);
    console.log('Extracted text:', text.text);
} else {
    console.log('Document is still processing, status:', docInfo.document.status);
}

// Get document metadata for display
const docInfo = await pt.getDocumentInfo('@public/demo.pdf');
document.getElementById('fileName').textContent = docInfo.name;
document.getElementById('fileType').textContent = docInfo.document.mimetype;
document.getElementById('status').textContent = docInfo.document.status;

// Build file information card
async function showFileInfo(path) {
    const info = await pt.getDocumentInfo(path);
    const doc = info.document;

    return `
        <div class="file-card">
            <h3>${info.name}</h3>
            <ul>
                <li>Type: ${doc.mimetype}</li>
                <li>Status: ${doc.status}</li>
                <li>Indexed: ${info.indexed ? 'Yes' : 'No'}</li>
                <li>Created: ${new Date(info.created_at).toLocaleDateString()}</li>
            </ul>
            <a href="${doc.download_url}">Download</a>
        </div>
    `;
}

// Validate document exists before processing
async function processDocument(path) {
    try {
        const docInfo = await pt.getDocumentInfo(path);

        if (docInfo.document.status === 'Error') {
            throw new Error('Document processing failed');
        }

        if (docInfo.document.status === 'Added') {
            console.log('Waiting for document extraction...');
            // Wait for extraction to complete
            await pt.waitForDocumentReady(docInfo.document.id);
        }

        // Process document
        const text = await pt.getDocumentText(docInfo.document.id);
        return analyzeText(text.text);

    } catch (error) {
        console.error('Document not found or inaccessible:', error);
        return null;
    }
}

// Get document by various path formats
const publicDoc = await pt.getDocumentInfo('@public/brochure.pdf');
const liveAppDoc = await pt.getDocumentInfo('@liveapp/template.json');
const chatDoc = await pt.getDocumentInfo('/reports/2025/Q1.xlsx');
const simpleDoc = await pt.getDocumentInfo('readme.txt'); // Searches @public → @liveapp → chat root

Common Use Cases: - Validate document exists before processing - Check document extraction status - Display document metadata in UI - Get document ID for use with other API methods - Build file browsers and document managers - Verify file size before downloading - Show document processing status to users

Related Methods: - Use pt.getDocumentText(docId) to retrieve the extracted text content - Use pt.getDocumentStatus(docId) to check only the processing status - See File Download API for downloading documents by path - See File Storage Hierarchy for details about @public and @liveapp paths

pt.deleteDocuments(documentIds)

Bulk delete documents from the chat.

Parameters: - documentIds (array of numbers, required): List of document IDs to delete

Returns:

{
    document_ids: [156, 157, 158],
    deleted: true
}

Usage:

// Delete multiple documents
await pt.deleteDocuments([156, 157, 158]);

// Delete with confirmation
async function deleteSelectedDocs(docIds) {
    if (!confirm(`Delete ${docIds.length} documents?`)) return;
    await pt.deleteDocuments(docIds);
    await loadDocumentList();
}

Note: Only deletes documents owned by the current user.

pt.downloadDocuments(documentIds, asZip)

Download one or more documents.

Parameters: - documentIds (array of numbers, required): Document IDs to download - asZip (boolean, optional): Force ZIP output (default: false, auto-ZIP for multiple files)

Returns: StreamingResponse with file download

Usage:

// Download single document
await pt.downloadDocuments([156]);

// Download multiple as ZIP
await pt.downloadDocuments([156, 157, 158], true);

// Download button handler
async function handleDownload(docIds) {
    try {
        await pt.downloadDocuments(docIds, docIds.length > 1);
    } catch (error) {
        alert('Download failed: ' + error.message);
    }
}

pt.listCollections()

Returns all collections associated with the chat.

Returns: Array of collection objects:

[
    {
        id: 10,
        created_at: "2024-01-15T10:30:00Z",
        last_updated_at: "2024-01-20T14:00:00Z",
        name: "Research Papers",
        uuid: "550e8400-e29b-41d4-a716-446655440000",
        user_id: 1,
        description: "Collection of research papers",
        group_id: 5,
        public: false,
        tags: [{ id: 1, name: "research" }],
        status: "active"
    }
]

Usage:

// Get all collections
const collections = await pt.listCollections();

// Display collections
collections.forEach(col => {
    console.log(`${col.name}: ${col.description}`);
});

// Build collection selector
function createCollectionSelector(collections) {
    return collections.map(col => `
        <option value="${col.id}">${col.name}</option>
    `).join('');
}

pt.getDocumentsInCollections(collectionIds)

Returns documents within specified collections, grouped by collection ID.

Parameters: - collectionIds (array of numbers, required): List of collection IDs

Returns: Object keyed by collection ID (as strings):

{
    "10": [
        { id: 200, name: "paper1.pdf", custom_name: "ML Paper", ... },
        { id: 201, name: "paper2.pdf", ... }
    ],
    "15": [
        { id: 300, name: "report.docx", ... }
    ]
}

Usage:

// Get documents in specific collections
const docs = await pt.getDocumentsInCollections([10, 15]);

// Display by collection
Object.entries(docs).forEach(([collId, documents]) => {
    console.log(`Collection ${collId}: ${documents.length} documents`);
});

// Combine all documents
const allDocs = Object.values(docs).flat();

Entity Structure

All entities follow this consistent structure:

{
    // System fields (managed automatically)
    id: 123,                    // Unique entity ID
    entity_name: 'task',        // Type of entity
    creator_user_id: 456,       // User ID who created this entity
    created_at: '2024-03-15T10:30:00+00:00',  // Auto-managed
    updated_at: '2024-03-15T10:35:00+00:00',  // Auto-managed

    // Your data (in the 'data' property)
    data: {
        text: 'Buy groceries',
        completed: false,
        priority: 'high',
        // ... any custom fields
    }
}

System-Managed Fields: - id: Unique identifier for the entity - entity_name: Type/category of the entity - creator_user_id: ID of the user who created the entity (automatic) - created_at: Timestamp when entity was created (automatic) - updated_at: Timestamp when entity was last updated (automatic)

Important: - Never add created_at, updated_at, or creator_user_id to your data object - These fields are managed at the entity level, not in your data - Your custom fields go inside the data object

Best Practices

1. Use pt.get() for Single Entities

When you know the entity ID, always use pt.get() instead of pt.list() with filters:

// ✅ GOOD: Fast primary key lookup
const task = await pt.get(123);

// ❌ AVOID: Slower filtering when ID is known
const tasks = await pt.list({
    entityNames: ['task'],
    filters: { id: 123 }
});
const task = tasks[0];

2. Use Merge Mode for Partial Updates

// ✅ BEST: Use merge mode for partial updates (simplest)
await pt.edit(taskId, { completed: true }, true);

// ✅ GOOD: Manually preserve existing fields
const task = await pt.get(taskId);
await pt.edit(taskId, {
    ...task.data,
    completed: true
});

// ❌ BAD: Lose all other fields (without merge mode)
await pt.edit(taskId, { completed: true });

3. Handle Errors Gracefully

async function robustOperation() {
    try {
        const entity = await pt.get(123);
        return entity;
    } catch (error) {
        console.error('Operation failed:', error);
        return null;
    }
}

4. Cache getChatMembers() Results

// ✅ GOOD: Cache members at app initialization
let allMembers = [];

async function initApp() {
    allMembers = await pt.getChatMembers();
    await loadTasks();
}

function getMemberName(userId) {
    const member = allMembers.find(m => m.id === userId);
    return member ? member.name : 'Unknown';
}

// ❌ AVOID: Calling getChatMembers() repeatedly
async function displayTask(task) {
    const members = await pt.getChatMembers(); // Called for every task!
    const creator = members.find(m => m.id === task.creator_user_id);
    return creator.name;
}

5. Use Appropriate Limits

// ✅ GOOD: Reasonable limits
const recentTasks = await pt.list({
    entityNames: ['task'],
    limit: 50
});

// ❌ AVOID: Requesting too much data
const allTasks = await pt.list({
    entityNames: ['task'],
    limit: 10000 // Too large
});

Performance Tips

  1. Primary key lookups: Use pt.get() when you know the ID (fastest)
  2. Server-side filtering: Use filters in pt.list() instead of client-side filtering
  3. Pagination: Use limit and offset for large datasets
  4. Caching: Cache getChatMembers() and other static data
  5. Batch operations: Use Promise.all() for multiple independent operations
// Batch operations example (using merge mode - no need to fetch first!)
async function batchUpdate(taskIds, updates) {
    const promises = taskIds.map(id => pt.edit(id, updates, true));
    await Promise.all(promises);
}

Next Steps