@agentdb/sdk
Version:
JavaScript SDK for AgentDB database service
780 lines (696 loc) • 25.4 kB
JavaScript
/**
* AgentDB JavaScript SDK
*
* A lightweight JavaScript SDK for the AgentDB database service.
* Uses only the fetch API for HTTP requests.
*/
/**
* Create a Float32 buffer from an array of numbers for use as vector parameters
* @param {number[]} values - Array of numeric values to convert to a vector buffer
* @returns {Buffer|Uint8Array} Buffer containing the vector data as Float32 values
* @throws {ValidationError} If values is not an array or contains non-numeric values
*
* @example
* import { createVectorBuffer } from '@agentdb/sdk';
*
* // Create a vector buffer from an array
* const embedding = createVectorBuffer([0.1, 0.2, 0.3, 0.4]);
*
* // Use in a vector search query
* const result = await connection.execute({
* sql: 'SELECT id, text, vec_distance_cosine(embedding, ?) as distance FROM documents ORDER BY distance LIMIT 5',
* params: [embedding]
* });
*/
function createVectorBuffer(values) {
if (!Array.isArray(values)) {
throw new ValidationError('createVectorBuffer expects an array of numbers');
}
if (values.length === 0) {
throw new ValidationError('createVectorBuffer expects a non-empty array');
}
// Validate that all values are numbers
for (let i = 0; i < values.length; i++) {
if (typeof values[i] !== 'number' || !isFinite(values[i])) {
throw new ValidationError(`createVectorBuffer expects all values to be finite numbers, got ${typeof values[i]} at index ${i}`);
}
}
// Create buffer - use different approaches for Node.js vs browser
if (typeof Buffer !== 'undefined') {
// Node.js environment
const buffer = Buffer.allocUnsafe(values.length * 4);
for (let i = 0; i < values.length; i++) {
buffer.writeFloatLE(values[i], i * 4);
}
return buffer;
} else {
// Browser environment - use ArrayBuffer and DataView
const buffer = new ArrayBuffer(values.length * 4);
const view = new DataView(buffer);
for (let i = 0; i < values.length; i++) {
view.setFloat32(i * 4, values[i], true); // true for little-endian
}
return new Uint8Array(buffer);
}
}
/**
* Custom error classes for AgentDB operations
*/
class AgentDBError extends Error {
constructor(message, statusCode = null, response = null) {
super(message);
this.name = 'AgentDBError';
this.statusCode = statusCode;
this.response = response;
}
}
class AuthenticationError extends AgentDBError {
constructor(message, statusCode = 401, response = null) {
super(message, statusCode, response);
this.name = 'AuthenticationError';
}
}
class ValidationError extends AgentDBError {
constructor(message, statusCode = 400, response = null) {
super(message, statusCode, response);
this.name = 'ValidationError';
}
}
class DatabaseError extends AgentDBError {
constructor(message, statusCode = 500, response = null) {
super(message, statusCode, response);
this.name = 'DatabaseError';
}
}
/**
* Main database service class for listing databases and creating connections
*/
class DatabaseService {
/**
* Create a new DatabaseService instance
* @param {string} baseUrl - The base URL of the AgentDB service (e.g., 'https://api.agentdb.dev')
* @param {string} apiKey - The API key for authentication
* @param {boolean} debug - Enable debug logging (default: false)
*/
constructor(baseUrl, apiKey, debug = false) {
if (!baseUrl) {
throw new ValidationError('baseUrl is required');
}
if (!apiKey) {
throw new ValidationError('apiKey is required');
}
this.baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash
this.apiKey = apiKey;
this.debug = debug;
}
/**
* Log debug messages when debug mode is enabled
* @private
*/
_debugLog(...args) {
if (this.debug) {
console.log('[AgentDB SDK Debug]', ...args);
}
}
/**
* Make an authenticated HTTP request
* @private
*/
async _makeRequest(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const headers = {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
...options.headers
};
// Add debug parameter to query string if debug mode is enabled and it's a GET request
let finalUrl = url;
if (this.debug && options.method === 'GET') {
const separator = url.includes('?') ? '&' : '?';
finalUrl = `${url}${separator}debug=true`;
}
// Add debug parameter to request body if debug mode is enabled and it's not a GET request
let finalOptions = { ...options };
if (this.debug && options.method !== 'GET' && options.body) {
try {
const bodyData = JSON.parse(options.body);
bodyData.debug = true;
finalOptions.body = JSON.stringify(bodyData);
} catch (parseError) {
// If body is not JSON, we can't add debug parameter
this._debugLog('Warning: Could not add debug parameter to non-JSON request body');
}
}
this._debugLog(`Making ${finalOptions.method || 'GET'} request to: ${finalUrl}`);
if (finalOptions.body) {
this._debugLog('Request body:', finalOptions.body);
}
try {
const response = await fetch(finalUrl, {
...finalOptions,
headers
});
let responseData;
try {
responseData = await response.json();
} catch (parseError) {
throw new AgentDBError(`Failed to parse response: ${parseError.message}`, response.status);
}
this._debugLog('Response status:', response.status);
this._debugLog('Response data:', responseData);
// Log debug information from API if available
if (responseData.debugLogs && Array.isArray(responseData.debugLogs)) {
this._debugLog('API Debug Logs:');
responseData.debugLogs.forEach(log => {
this._debugLog(`[${log.level}] ${log.timestamp}: ${log.message}`);
});
}
if (!response.ok) {
const errorMessage = responseData.error || `HTTP ${response.status}: ${response.statusText}`;
if (response.status === 401) {
throw new AuthenticationError(errorMessage, response.status, responseData);
} else if (response.status === 400) {
throw new ValidationError(errorMessage, response.status, responseData);
} else if (response.status >= 500) {
throw new DatabaseError(errorMessage, response.status, responseData);
} else {
throw new AgentDBError(errorMessage, response.status, responseData);
}
}
return responseData;
} catch (error) {
if (error instanceof AgentDBError) {
throw error;
}
// Network or other fetch errors
throw new AgentDBError(`Network error: ${error.message}`);
}
}
/**
* List all databases for a given token
* @param {string} token - The UUID token for database access
* @returns {Promise<Array>} Array of database objects with name, type, fileName, size, and modified properties
*/
async listDatabases(token) {
if (!token) {
throw new ValidationError('token is required');
}
const response = await this._makeRequest(`/db/api/list-databases?token=${encodeURIComponent(token)}`, {
method: 'GET'
});
return response.databases || [];
}
/**
* Create a new database connection
* @param {string} token - The UUID token for database access
* @param {string} dbName - The name of the database
* @param {string} dbType - The type of database ('sqlite' or 'duckdb')
* @returns {DatabaseConnection} A new database connection instance
*/
connect(token, dbName, dbType = 'sqlite') {
return new DatabaseConnection(this.baseUrl, this.apiKey, token, dbName, dbType, this.debug);
}
/**
* Delete a database
* @param {string} token - The UUID token for database access
* @param {string} dbName - The name of the database to delete
* @param {string} dbType - The type of database ('sqlite' or 'duckdb')
* @returns {Promise<Object>} Deletion result with success message and optional debug logs
*/
async deleteDatabase(token, dbName, dbType = 'sqlite') {
if (!token) {
throw new ValidationError('token is required');
}
if (!dbName) {
throw new ValidationError('dbName is required');
}
if (!['sqlite', 'duckdb'].includes(dbType)) {
throw new ValidationError('dbType must be either "sqlite" or "duckdb"');
}
return await this._makeRequest('/db/api/delete-database', {
method: 'DELETE',
body: JSON.stringify({ token, dbName, dbType })
});
}
/**
* Rename a database
* @param {string} token - The UUID token for database access
* @param {string} oldDbName - The current name of the database
* @param {string} newDbName - The new name for the database
* @param {string} dbType - The type of database ('sqlite' or 'duckdb')
* @returns {Promise<Object>} Rename result with success message and optional debug logs
*/
async renameDatabase(token, oldDbName, newDbName, dbType = 'sqlite') {
if (!token) {
throw new ValidationError('token is required');
}
if (!oldDbName) {
throw new ValidationError('oldDbName is required');
}
if (!newDbName) {
throw new ValidationError('newDbName is required');
}
if (!['sqlite', 'duckdb'].includes(dbType)) {
throw new ValidationError('dbType must be either "sqlite" or "duckdb"');
}
return await this._makeRequest('/db/api/rename-database', {
method: 'PUT',
body: JSON.stringify({ token, oldDbName, newDbName, dbType })
});
}
/**
* Copy a database from one location to another
* @param {string} sourceToken - The UUID token for the source database
* @param {string} sourceDbName - The name of the source database
* @param {string} sourceDbType - The type of the source database ('sqlite' or 'duckdb')
* @param {string} destToken - The UUID token for the destination
* @param {string} destDbName - The name for the destination database
* @returns {Promise<Object>} Copy result with success message and optional debug logs
*/
async copyDatabase(sourceToken, sourceDbName, sourceDbType, destToken, destDbName) {
if (!sourceToken) {
throw new ValidationError('sourceToken is required');
}
if (!sourceDbName) {
throw new ValidationError('sourceDbName is required');
}
if (!sourceDbType) {
throw new ValidationError('sourceDbType is required');
}
if (!destToken) {
throw new ValidationError('destToken is required');
}
if (!destDbName) {
throw new ValidationError('destDbName is required');
}
if (!['sqlite', 'duckdb'].includes(sourceDbType)) {
throw new ValidationError('sourceDbType must be either "sqlite" or "duckdb"');
}
return await this._makeRequest('/db/api/copy-database', {
method: 'POST',
body: JSON.stringify({
sourceToken,
sourceDbName,
sourceDbType,
destToken,
destDbName
})
});
}
/**
* Get a presigned URL for uploading a database file
* @param {string} token - The UUID token for database access
* @param {string} dbName - The name for the database
* @param {string} dbType - The type of database ('sqlite' or 'duckdb')
* @returns {Promise<Object>} Upload URL information with uploadUrl, fileName, expiresIn, and optional debug logs
*/
async getUploadUrl(token, dbName, dbType = 'sqlite') {
if (!token) {
throw new ValidationError('token is required');
}
if (!dbName) {
throw new ValidationError('dbName is required');
}
if (!['sqlite', 'duckdb'].includes(dbType)) {
throw new ValidationError('dbType must be either "sqlite" or "duckdb"');
}
return await this._makeRequest('/db/api/upload-url', {
method: 'POST',
body: JSON.stringify({ token, dbName, dbType })
});
}
/**
* Get a presigned URL for downloading a database file
* @param {string} token - The UUID token for database access
* @param {string} dbName - The name of the database
* @param {string} dbType - The type of database ('sqlite' or 'duckdb')
* @returns {Promise<Object>} Download URL information with downloadUrl, fileName, expiresIn, and optional debug logs
*/
async getDownloadUrl(token, dbName, dbType = 'sqlite') {
if (!token) {
throw new ValidationError('token is required');
}
if (!dbName) {
throw new ValidationError('dbName is required');
}
if (!['sqlite', 'duckdb'].includes(dbType)) {
throw new ValidationError('dbType must be either "sqlite" or "duckdb"');
}
return await this._makeRequest('/db/api/download-url', {
method: 'POST',
body: JSON.stringify({ token, dbName, dbType })
});
}
/**
* Create a new database template
* @param {string} templateName - The name for the template
* @param {Array<string>} initializationSql - Array of SQL statements to initialize the template
* @param {string} description - Description of the template
* @returns {Promise<Object>} Created template information
*/
async createTemplate(templateName, initializationSql, description) {
if (!templateName) {
throw new ValidationError('templateName is required');
}
if (!initializationSql || !Array.isArray(initializationSql)) {
throw new ValidationError('initializationSql must be an array of SQL statements');
}
if (!description) {
throw new ValidationError('description is required');
}
return await this._makeRequest('/api/templates', {
method: 'POST',
body: JSON.stringify({ templateName, initializationSql, description })
});
}
/**
* List all available templates
* @returns {Promise<Array>} Array of template objects
*/
async listTemplates() {
const response = await this._makeRequest('/api/templates', {
method: 'GET'
});
return response.templates || [];
}
/**
* Get a specific template by name
* @param {string} templateName - The name of the template to retrieve
* @returns {Promise<Object>} Template information
*/
async getTemplate(templateName) {
if (!templateName) {
throw new ValidationError('templateName is required');
}
return await this._makeRequest(`/api/templates/${encodeURIComponent(templateName)}`, {
method: 'GET'
});
}
/**
* Update an existing template
* @param {string} templateName - The name of the template to update
* @param {Array<string>} initializationSql - Array of SQL statements to initialize the template
* @param {string} description - Description of the template
* @returns {Promise<Object>} Update result with success message and old template info
*/
async updateTemplate(templateName, initializationSql, description) {
if (!templateName) {
throw new ValidationError('templateName is required');
}
if (!initializationSql || !Array.isArray(initializationSql)) {
throw new ValidationError('initializationSql must be an array of SQL statements');
}
if (!description) {
throw new ValidationError('description is required');
}
return await this._makeRequest(`/api/templates/${encodeURIComponent(templateName)}`, {
method: 'PUT',
body: JSON.stringify({ initializationSql, description })
});
}
/**
* Delete a template
* @param {string} templateName - The name of the template to delete
* @returns {Promise<Object>} Deletion result with success message
*/
async deleteTemplate(templateName) {
if (!templateName) {
throw new ValidationError('templateName is required');
}
return await this._makeRequest(`/api/templates/${encodeURIComponent(templateName)}`, {
method: 'DELETE'
});
}
/**
* Get template information for a specific database
* @param {string} templateName - The name of the template
* @param {string} token - The UUID token for database access
* @param {string} dbName - The name of the database
* @param {string} dbType - The type of database ('sqlite' or 'duckdb')
* @returns {Promise<Object>} Template info including schema and application status
*/
async getTemplateInfo(templateName, token, dbName, dbType = 'sqlite') {
if (!templateName) {
throw new ValidationError('templateName is required');
}
if (!token) {
throw new ValidationError('token is required');
}
if (!dbName) {
throw new ValidationError('dbName is required');
}
if (!['sqlite', 'duckdb'].includes(dbType)) {
throw new ValidationError('dbType must be either "sqlite" or "duckdb"');
}
const params = new URLSearchParams({
templateName,
token,
dbName,
dbType
});
return await this._makeRequest(`/db/api/template-info?${params.toString()}`, {
method: 'GET'
});
}
/**
* Get applied templates for a database
* @param {string} token - The UUID token for database access
* @param {string} dbName - The name of the database
* @param {string} dbType - The type of database ('sqlite' or 'duckdb')
* @returns {Promise<Object>} Applied templates information
*/
async getAppliedTemplates(token, dbName, dbType = 'sqlite') {
if (!token) {
throw new ValidationError('token is required');
}
if (!dbName) {
throw new ValidationError('dbName is required');
}
if (!['sqlite', 'duckdb'].includes(dbType)) {
throw new ValidationError('dbType must be either "sqlite" or "duckdb"');
}
const params = new URLSearchParams({
token,
dbName,
dbType
});
return await this._makeRequest(`/db/api/applied-templates?${params.toString()}`, {
method: 'GET'
});
}
/**
* Create a permanent URL slug for MCP server endpoints
* @param {Object} queryParams - Query parameters for the MCP endpoint
* @returns {Promise<Object>} Created slug information with slug and full URL
*/
async createMcpSlug(queryParams) {
if (!queryParams || typeof queryParams !== 'object') {
throw new ValidationError('queryParams is required and must be an object');
}
return await this._makeRequest('/api/mcp/create-slug', {
method: 'POST',
body: JSON.stringify({ queryParams })
});
}
/**
* Get MCP slug information
* @param {string} slug - The slug identifier
* @returns {Promise<Object>} Slug information and query parameters
*/
async getMcpSlug(slug) {
if (!slug) {
throw new ValidationError('slug is required');
}
return await this._makeRequest(`/api/mcp/slug/${encodeURIComponent(slug)}`, {
method: 'GET'
});
}
}
/**
* Database connection class for executing SQL statements
*/
class DatabaseConnection {
/**
* Create a new DatabaseConnection instance
* @param {string} baseUrl - The base URL of the AgentDB service
* @param {string} apiKey - The API key for authentication
* @param {string} token - The UUID token for database access
* @param {string} dbName - The name of the database
* @param {string} dbType - The type of database ('sqlite' or 'duckdb')
* @param {boolean} debug - Enable debug logging (default: false)
*/
constructor(baseUrl, apiKey, token, dbName, dbType = 'sqlite', debug = false) {
if (!baseUrl) {
throw new ValidationError('baseUrl is required');
}
if (!apiKey) {
throw new ValidationError('apiKey is required');
}
if (!token) {
throw new ValidationError('token is required');
}
if (!dbName) {
throw new ValidationError('dbName is required');
}
if (!['sqlite', 'duckdb'].includes(dbType)) {
throw new ValidationError('dbType must be either "sqlite" or "duckdb"');
}
this.baseUrl = baseUrl.replace(/\/$/, '');
this.apiKey = apiKey;
this.token = token;
this.dbName = dbName;
this.dbType = dbType;
this.debug = debug;
}
/**
* Log debug messages when debug mode is enabled
* @private
*/
_debugLog(...args) {
if (this.debug) {
console.log('[AgentDB SDK Debug]', ...args);
}
}
/**
* Make an authenticated HTTP request
* @private
*/
async _makeRequest(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const headers = {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
...options.headers
};
this._debugLog(`Making ${options.method || 'GET'} request to: ${url}`);
if (options.body) {
this._debugLog('Request body:', options.body);
}
try {
const response = await fetch(url, {
...options,
headers
});
let responseData;
try {
responseData = await response.json();
} catch (parseError) {
throw new AgentDBError(`Failed to parse response: ${parseError.message}`, response.status);
}
this._debugLog('Response status:', response.status);
this._debugLog('Response data:', responseData);
// Log debug information from API if available
if (responseData.debugLogs && Array.isArray(responseData.debugLogs)) {
this._debugLog('API Debug Logs:');
responseData.debugLogs.forEach(log => {
this._debugLog(`[${log.level}] ${log.timestamp}: ${log.message}`);
});
}
if (!response.ok) {
const errorMessage = responseData.error || `HTTP ${response.status}: ${response.statusText}`;
if (response.status === 401) {
throw new AuthenticationError(errorMessage, response.status, responseData);
} else if (response.status === 400) {
throw new ValidationError(errorMessage, response.status, responseData);
} else if (response.status >= 500) {
throw new DatabaseError(errorMessage, response.status, responseData);
} else {
throw new AgentDBError(errorMessage, response.status, responseData);
}
}
return responseData;
} catch (error) {
if (error instanceof AgentDBError) {
throw error;
}
// Network or other fetch errors
throw new AgentDBError(`Network error: ${error.message}`);
}
}
/**
* Execute one or more SQL statements
* @param {Array|Object} statements - Array of statement objects or single statement object
* Each statement should have 'sql' and optional 'params' properties
* @returns {Promise<Object>} Execution results with results array and optional debug logs
*/
async execute(statements) {
if (!statements) {
throw new ValidationError('statements is required');
}
// Normalize to array format
const statementsArray = Array.isArray(statements) ? statements : [statements];
// Validate statements format
for (const stmt of statementsArray) {
if (!stmt || typeof stmt !== 'object') {
throw new ValidationError('Each statement must be an object');
}
if (!stmt.sql || typeof stmt.sql !== 'string') {
throw new ValidationError('Each statement must have a "sql" property with a string value');
}
if (stmt.params && !Array.isArray(stmt.params)) {
throw new ValidationError('Statement params must be an array if provided');
}
}
const requestBody = {
token: this.token,
dbName: this.dbName,
dbType: this.dbType,
statements: statementsArray
};
// Add debug parameter if debug mode is enabled
if (this.debug) {
requestBody.debug = true;
}
return await this._makeRequest('/db/api/execute', {
method: 'POST',
body: JSON.stringify(requestBody)
});
}
/**
* Convert natural language to SQL and optionally execute it
* @param {string} query - The natural language query
* @param {Array} conversationHistory - Optional conversation history for context
* @param {string} templateName - Optional template name for schema context
* @returns {Promise<Object>} Natural language conversion results with generated SQL and optional execution results
*/
async naturalLanguageToSql(query, conversationHistory = null, templateName = null) {
if (!query || typeof query !== 'string') {
throw new ValidationError('query is required and must be a string');
}
if (conversationHistory && !Array.isArray(conversationHistory)) {
throw new ValidationError('conversationHistory must be an array if provided');
}
const requestBody = {
query,
token: this.token,
dbName: this.dbName,
dbType: this.dbType
};
if (conversationHistory && conversationHistory.length > 0) {
requestBody.conversationHistory = conversationHistory;
}
if (templateName) {
requestBody.templateName = templateName;
}
// Add debug parameter if debug mode is enabled
if (this.debug) {
requestBody.debug = true;
}
return await this._makeRequest('/db/api/nl-to-sql', {
method: 'POST',
body: JSON.stringify(requestBody)
});
}
}
// Export classes, utility functions, and error types
export {
DatabaseService,
DatabaseConnection,
createVectorBuffer,
AgentDBError,
AuthenticationError,
ValidationError,
DatabaseError
};
// Default export for convenience
export default DatabaseService;