UNPKG

@picahq/ai

Version:

Pica AI SDK for Vercel AI SDK integration

849 lines (751 loc) 29.3 kB
import axios from "axios"; import { z } from "zod"; import { tool } from "ai"; import FormData from 'form-data'; import { getDefaultSystemPrompt } from "./prompts/defaultSystem"; import { getDefaultSystemWithAuthkitPrompt } from "./prompts/defaultSystemWithAuthkit"; import { getKnowledgeAgentSystemPrompt } from "./prompts/knowledgeAgentSystem"; import { getKnowledgeAgentWithAuthkitSystemPrompt } from "./prompts/knowledgeAgentWithAuthkitSystem"; import { normalizeActionId, paginateResults, replacePathVariables } from "./utils"; import { AvailableActions, RequestConfig, ConnectionDefinition, Connection } from "./types/connection"; interface PicaOptions { /** * The descriptor for the Pica client options. * @property connectors - Array of connector IDs to filter available actions * @property actions - Array of action IDs to filter available actions (default: all actions) * @property permissions - Permissions for the Pica client: "read" (GET only), "write" (POST/PUT/PATCH), "admin" (all methods) (default: "admin") * @property serverUrl - Custom server URL for Pica API (defaults to https://api.picaos.com) * @property identity - Identity value for AuthKit token generation * @property identityType - Type of identity for AuthKit ("user", "team", "organization", or "project") * @property authkit - Whether to enable AuthKit integration * @property knowledgeAgent - Whether to enable Knowledge Agent mode * @property knowledgeAgentConfig - Configuration options for Knowledge Agent * @property headers - Additional headers to send with requests */ connectors?: string[]; actions?: string[]; permissions?: "read" | "write" | "admin"; serverUrl?: string; identity?: string; identityType?: "user" | "team" | "organization" | "project"; authkit?: boolean; knowledgeAgent?: boolean; knowledgeAgentConfig?: KnowledgeAgentConfig; headers?: Record<string, string>; } interface KnowledgeAgentConfig { includeEnvironmentVariables: boolean; } export class Pica { private secret: string; private connections: Connection[]; private connectionDefinitions: ConnectionDefinition[]; private systemPromptValue: string; private initialized: Promise<void>; private identity?: string; private identityType?: string; private useAuthkit: boolean; private useKnowledgeAgent: boolean; private knowledgeAgentConfig?: KnowledgeAgentConfig; private options?: PicaOptions; private baseUrl = "https://api.picaos.com"; private getConnectionUrl; private availableActionsUrl; private getConnectionDefinitionsUrl; constructor(secret: string, options?: PicaOptions) { this.secret = secret; this.connections = []; this.connectionDefinitions = []; this.systemPromptValue = "Loading connections..."; this.identity = options?.identity; this.identityType = options?.identityType; this.useAuthkit = options?.authkit || false; this.useKnowledgeAgent = options?.knowledgeAgent || false; this.knowledgeAgentConfig = options?.knowledgeAgentConfig || { includeEnvironmentVariables: true }; this.options = options; if (options?.serverUrl) { this.baseUrl = options.serverUrl; } this.getConnectionUrl = `${this.baseUrl}/v1/vault/connections`; this.availableActionsUrl = `${this.baseUrl}/v1/knowledge`; this.getConnectionDefinitionsUrl = `${this.baseUrl}/v1/available-connectors`; this.initialized = this.initialize() .then(() => { let filteredConnections = this.connections.filter((conn: any) => conn.active); if (!options?.connectors?.length) { filteredConnections = []; } const connectionsInfo = filteredConnections.length > 0 ? '\t* ' + filteredConnections .map((conn: any) => `${conn.platform} - Key: ${conn.key}`) .join('\n\t* ') : 'No connections available'; const availablePlatformsInfo = this.connectionDefinitions.map((def) => `\n\t* ${def.platform} (${def.name})` ).join(''); if (options?.knowledgeAgentConfig && !this.useKnowledgeAgent) { throw new Error("Cannot provide Knowledge Agent configuration when Knowledge Agent is disabled. Please set useKnowledgeAgent to true if you want to use the Knowledge Agent."); } // Choose the appropriate system prompt based on options if (this.useAuthkit && this.useKnowledgeAgent) { this.systemPromptValue = getKnowledgeAgentWithAuthkitSystemPrompt(connectionsInfo, availablePlatformsInfo, this.knowledgeAgentConfig?.includeEnvironmentVariables); } else if (this.useAuthkit) { this.systemPromptValue = getDefaultSystemWithAuthkitPrompt(connectionsInfo, availablePlatformsInfo); } else if (this.useKnowledgeAgent) { this.systemPromptValue = getKnowledgeAgentSystemPrompt(connectionsInfo, availablePlatformsInfo, this.knowledgeAgentConfig?.includeEnvironmentVariables); } else { this.systemPromptValue = getDefaultSystemPrompt(connectionsInfo, availablePlatformsInfo); } }) .catch(error => { console.error('Error during initialization:', error); this.systemPromptValue = "Error loading connections"; }); } async generateSystemPrompt(userSystemPrompt?: string): Promise<string> { await this.waitForInitialization(); const now = new Date(); const prompt = `${userSystemPrompt ? userSystemPrompt + '\n\n' : ''}=== PICA: INTEGRATION ASSISTANT ===\n Everything below is for Pica (picaos.com), your integration assistant that can instantly connect your AI agents to 100+ APIs.\n Current Time: ${now.toLocaleString('en-US', { timeZone: 'GMT' })} (GMT) --- Tools Information --- ${this.system.trim()} `; return prompt; } private async initialize() { await Promise.all([ this.initializeConnections(undefined, this.options?.connectors), this.initializeConnectionDefinitions(), ]); } async waitForInitialization() { await this.initialized; return this.system; } private async initializeConnections(platform?: string, connectionKeys?: string[]) { try { if (!connectionKeys || connectionKeys.length === 0) { this.connections = []; return; } const headers = this.generateHeaders(); let baseUrl = this.getConnectionUrl; let hasQueryParam = false; if (platform) { baseUrl += `?platform=${platform}`; hasQueryParam = true; } if (!connectionKeys.includes("*")) { baseUrl += hasQueryParam ? `&key=${connectionKeys.join(',')}` : `?key=${connectionKeys.join(',')}`; hasQueryParam = true; } if (this.identity) { baseUrl += hasQueryParam ? `&identity=${encodeURIComponent(this.identity)}` : `?identity=${encodeURIComponent(this.identity)}`; hasQueryParam = true; } if (this.identityType) { baseUrl += hasQueryParam ? `&identityType=${encodeURIComponent(this.identityType)}` : `?identityType=${encodeURIComponent(this.identityType)}`; hasQueryParam = true; } const fetchPage = (skip: number, limit: number) => axios.get<{ rows: Connection[], total: number, skip: number, limit: number }>( `${baseUrl}${hasQueryParam ? '&' : '?'}limit=${limit}&skip=${skip}`, { headers } ).then(response => response.data); this.connections = await paginateResults<Connection>(fetchPage); } catch (error) { console.error("Failed to initialize connections:", error); this.connections = []; } } private async initializeConnectionDefinitions() { try { const headers = this.generateHeaders(); let url = this.getConnectionDefinitionsUrl; let hasQueryParam = false; if (this.useAuthkit) { url += `?authkit=true`; hasQueryParam = true; } const fetchPage = (skip: number, limit: number) => axios.get<{ rows: ConnectionDefinition[], total: number, skip: number, limit: number }>( `${url}${hasQueryParam ? '&' : '?'}limit=${limit}&skip=${skip}`, { headers } ).then(response => response.data); this.connectionDefinitions = await paginateResults<ConnectionDefinition>(fetchPage); } catch (error) { console.error("Failed to initialize connection definitions:", error); this.connectionDefinitions = []; } } get system() { return this.systemPromptValue; } private generateHeaders() { return { "Content-Type": "application/json", "x-pica-secret": this.secret, ...this.options?.headers }; } private async getAllAvailableActions(platform: string, actions?: string[]): Promise<AvailableActions[]> { try { const fetchPage = (skip: number, limit: number) => axios.get<{ rows: AvailableActions[], total: number, skip: number, limit: number }>( `${this.availableActionsUrl}?supported=true&connectionPlatform=${platform}&skip=${skip}&limit=${limit}`, { headers: this.generateHeaders() } ).then(response => response.data); const results = await paginateResults<AvailableActions>(fetchPage); // Normalize action IDs in the results const normalizedResults = results.map(action => { if (action._id) { action._id = normalizeActionId(action._id); } return action; }); // Filter actions by permissions let filteredByPermissions = normalizedResults; const permissions = this.options?.permissions; if (permissions === "read") { // Filter for GET methods only filteredByPermissions = normalizedResults.filter(action => { let method = action.method; return method?.toUpperCase() === "GET"; }); } else if (permissions === "write") { // Filter for POST, PUT, PATCH methods filteredByPermissions = normalizedResults.filter(action => { let method = action.method?.toUpperCase(); return method === "POST" || method === "PUT" || method === "PATCH"; }); } // For "admin" or no permissions set, return all actions (no filtering) // Filter actions if actions array is provided if (actions?.length) { return filteredByPermissions.filter(action => actions.includes(action._id) ); } return filteredByPermissions; } catch (error) { console.error("Error fetching all available actions:", error); throw new Error("Failed to fetch all available actions"); } } public async getAvailablePicaConnectors() { await this.initializeConnectionDefinitions(); return this.connectionDefinitions; } public async getAvailableConnectors(platform?: string) { await this.initializeConnections(platform, this.options?.connectors); return this.connections; } private async getSingleAction(actionId: string): Promise<AvailableActions> { try { const normalizedActionId = normalizeActionId(actionId); const response = await axios.get<{ rows: AvailableActions[], total: number, skip: number, limit: number }>( `${this.availableActionsUrl}?_id=${normalizedActionId}`, { headers: this.generateHeaders() } ); if (!response.data.rows || response.data.rows.length === 0) { throw new Error(`Action with ID ${normalizedActionId} not found`); } return response.data.rows[0]; } catch (error) { console.error("Error fetching single action:", error); throw new Error("Failed to fetch action"); } } private async getMethodFromKnowledge(actionId: string): Promise<string> { try { const normalizedActionId = normalizeActionId(actionId); const knowledgeResponse = await axios.get<{ rows: Array<{ method: string; }>; }>( `${this.baseUrl}/v1/knowledge?_id=${normalizedActionId}`, { headers: this.generateHeaders() } ); if (knowledgeResponse.data.rows && knowledgeResponse.data.rows.length > 0) { return knowledgeResponse.data.rows[0].method; } else { throw new Error(`Method not found for action ${actionId}`); } } catch (error) { console.error("Error fetching method from knowledge API:", error); throw new Error(`Failed to fetch method for action ${actionId}`); } } private async getAvailableActions(platform: string) { try { const allActions = await this.getAllAvailableActions(platform, this.options?.actions); return { total: allActions.length, actions: allActions }; } catch (error) { console.error("Error fetching available actions:", error); throw new Error("Failed to fetch available actions"); } } private async executePassthrough( actionId: string, connectionKey: string, data: any, path: string, method?: string, queryParams?: Record<string, string | number | boolean>, headers?: Record<string, string | number | boolean>, isFormData?: boolean, isFormUrlEncoded?: boolean, returnRequestConfigWithoutExecution?: boolean ): Promise<{ executed: boolean; responseData: unknown; requestConfig: RequestConfig; } | { executed: false; requestConfig: RequestConfig; }> { try { const allHeaders = { ...this.generateHeaders(), 'x-pica-connection-key': connectionKey, 'x-pica-action-id': actionId, ...(isFormData ? { 'Content-Type': 'multipart/form-data' } : {}), ...(isFormUrlEncoded ? { 'Content-Type': 'application/x-www-form-urlencoded' } : {}), ...headers }; // Remove Content-Type header if no data is being sent const finalHeaders = !data ? Object.entries(allHeaders) .filter(([key]) => key.toLowerCase() !== 'content-type') .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}) : allHeaders; const url = `${this.baseUrl}/v1/passthrough${path.startsWith('/') ? path : '/' + path}`; const requestConfig: RequestConfig = { url, method, headers: finalHeaders, params: queryParams }; if (method?.toLowerCase() !== 'get') { if (isFormData) { const formData = new FormData(); if (data && typeof data === 'object' && !Array.isArray(data)) { Object.entries(data).forEach(([key, value]) => { if (typeof value === 'object') { formData.append(key, JSON.stringify(value)); } else { formData.append(key, value); } }); } requestConfig.data = formData; Object.assign(requestConfig.headers, formData.getHeaders()); } else if (isFormUrlEncoded) { const params = new URLSearchParams(); if (data && typeof data === 'object' && !Array.isArray(data)) { Object.entries(data).forEach(([key, value]) => { if (typeof value === 'object') { params.append(key, JSON.stringify(value)); } else { params.append(key, String(value)); } }); } requestConfig.data = params; } else { requestConfig.data = data; } } if (returnRequestConfigWithoutExecution) { requestConfig.headers['x-pica-secret'] = "YOUR_PICA_SECRET_KEY_HERE"; return { executed: false, requestConfig }; } const response = await axios(requestConfig); requestConfig.headers['x-pica-secret'] = "****REDACTED****"; return { executed: true, responseData: response.data, requestConfig }; } catch (error) { console.error("Error executing passthrough:", error); throw error; } } private getPromptToConnectPlatformTool() { return { promptToConnectPlatform: tool({ description: "Prompt the user to connect to a platform that they do not currently have access to", inputSchema: z.object({ platformName: z.string(), }), outputSchema: z.object({ response: z.string(), }), execute: async ({ platformName }) => { return { response: platformName } } }) } } get intelligenceTool() { const baseTool = { getAvailableActions: this.oneTool.getAvailableActions, getActionKnowledge: this.oneTool.getActionKnowledge, execute: tool({ description: "Return a request config to the Pica Passthrough API without executing the action. Show the user a typescript code block to make an HTTP request to the Pica Passthrough API using the request config.", inputSchema: z.object({ platform: z.string(), action: z.object({ _id: z.string(), path: z.string() }), method: z.string().optional(), connectionKey: z.string(), data: z.any(), pathVariables: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(), queryParams: z.record(z.string(), z.any()).optional(), headers: z.record(z.string(), z.any()).optional(), isFormData: z.boolean().optional(), isFormUrlEncoded: z.boolean().optional(), }), outputSchema: z.object({ success: z.boolean(), title: z.string(), message: z.string(), raw: z.string() }), execute: async (params: { platform: string; action: { _id: string; path: string; }; method?: string; connectionKey: string; data?: any; pathVariables?: Record<string, string | number | boolean>; queryParams?: Record<string, any>; headers?: Record<string, any>; isFormData?: boolean; isFormUrlEncoded?: boolean; }) => { try { if (!this.connections.some(conn => conn.key === params.connectionKey) && this.useAuthkit) { throw new Error(`Connection not found. Please add a ${params.platform} connection first.`); } // Handle path variables const templateVariables = params.action.path.match(/\{\{([^}]+)\}\}/g); let resolvedPath = params.action.path; if (templateVariables) { const requiredVariables = templateVariables.map(v => v.replace(/\{\{|\}\}/g, '')); const combinedVariables = { ...(Array.isArray(params.data) ? {} : (params.data || {})), ...(params.pathVariables || {}) }; const missingVariables = requiredVariables.filter(v => !combinedVariables[v]); if (missingVariables.length > 0) { throw new Error( `Missing required path variables: ${missingVariables.join(', ')}. ` + `Please provide values for these variables.` ); } // Clean up data object and prepare path variables if (!Array.isArray(params.data)) { requiredVariables.forEach(v => { if (params.data && params.data[v] && (!params.pathVariables || !params.pathVariables[v])) { if (!params.pathVariables) params.pathVariables = {}; params.pathVariables[v] = params.data[v]; delete params.data[v]; } }); } resolvedPath = replacePathVariables(params.action.path, params.pathVariables || {}); } const normalizedActionId = normalizeActionId(params.action._id); // If method is not provided, fetch it from the knowledge API let method = params.method; if (!method) { method = await this.getMethodFromKnowledge(normalizedActionId); } // Execute the passthrough request with all components const result = await this.executePassthrough( normalizedActionId, params.connectionKey, params.data, resolvedPath, method, params.queryParams, params.headers, params.isFormData, params.isFormUrlEncoded, true ); return { success: true, title: "Request config returned", message: "Request config returned without execution", raw: JSON.stringify(result.requestConfig) }; } catch (error: any) { console.error("Error creating request config:", error); return { success: false, title: "Failed to create request config", message: error.message, raw: JSON.stringify(error?.response?.data || error) }; } } }) }; // Add the promptToConnectPlatform tool if authkit is enabled if (this.useAuthkit) { return { ...baseTool, ...this.getPromptToConnectPlatformTool() }; } return baseTool; } get oneTool() { const baseTool = { getAvailableActions: tool({ description: "Get available actions for a specific platform", inputSchema: z.object({ platform: z.string(), }), outputSchema: z.object({ success: z.boolean(), actions: z.array(z.object({ _id: z.string(), title: z.string(), tags: z.array(z.string()), })), platform: z.string(), content: z.string() }), execute: async (params: { platform: string; }) => { try { const availableActions = await this.getAvailableActions(params.platform); const simplifiedActions = availableActions.actions.map(action => ({ _id: action._id, title: action.title, tags: action.tags, })); return { success: true, actions: simplifiedActions, platform: params.platform, content: `Found ${simplifiedActions.length} available actions for ${params.platform}` }; } catch (error: any) { console.error("Error getting available actions:", error); return { success: false, title: "Failed to get available actions", message: error.message, raw: JSON.stringify(error?.response?.data || error) }; } } }), getActionKnowledge: tool({ description: "Get full action details including knowledge documentation for a specific action", inputSchema: z.object({ platform: z.string(), actionId: z.string(), }), outputSchema: z.object({ success: z.boolean(), action: z.object({ _id: z.string(), title: z.string(), connectionPlatform: z.string(), knowledge: z.string(), path: z.string(), baseUrl: z.string(), tags: z.array(z.string()), method: z.string().optional() }), platform: z.string(), content: z.string() }), execute: async (params: { platform: string; actionId: string; }) => { try { const normalizedActionId = normalizeActionId(params.actionId); const action = await this.getSingleAction(normalizedActionId); return { success: true, action, platform: params.platform, content: `Found knowledge for action: ${action.title}` }; } catch (error: any) { console.error("Error getting action knowledge:", error); return { success: false, title: "Failed to get action knowledge", message: error.message, raw: JSON.stringify(error?.response?.data || error) }; } } }), execute: tool({ description: "Execute a specific action using the passthrough API", inputSchema: z.object({ platform: z.string(), action: z.object({ _id: z.string(), path: z.string() }), method: z.string().optional(), connectionKey: z.string(), data: z.any(), pathVariables: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(), queryParams: z.record(z.string(), z.any()).optional(), headers: z.record(z.string(), z.any()).optional(), isFormData: z.boolean().optional(), isFormUrlEncoded: z.boolean().optional(), }), execute: async (params: { platform: string; action: { _id: string; path: string; }; method?: string; connectionKey: string; data?: any; pathVariables?: Record<string, string | number | boolean>; queryParams?: Record<string, any>; headers?: Record<string, any>; isFormData?: boolean; isFormUrlEncoded?: boolean; }) => { try { if (!this.connections.some(conn => conn.key === params.connectionKey)) { throw new Error(`Connection not found. Please add a ${params.platform} connection first.`); } const normalizedActionId = normalizeActionId(params.action._id); const fullAction = await this.getSingleAction(normalizedActionId); // If method is not provided, fetch it from the knowledge API let method = params.method; if (!method) { method = await this.getMethodFromKnowledge(normalizedActionId); } // Handle path variables const templateVariables = params.action.path.match(/\{\{([^}]+)\}\}/g); let resolvedPath = params.action.path; if (templateVariables) { const requiredVariables = templateVariables.map(v => v.replace(/\{\{|\}\}/g, '')); const combinedVariables = { ...(Array.isArray(params.data) ? {} : (params.data || {})), ...(params.pathVariables || {}) }; const missingVariables = requiredVariables.filter(v => !combinedVariables[v]); if (missingVariables.length > 0) { throw new Error( `Missing required path variables: ${missingVariables.join(', ')}. ` + `Please provide values for these variables.` ); } // Clean up data object and prepare path variables if (!Array.isArray(params.data)) { requiredVariables.forEach(v => { if (params.data && params.data[v] && (!params.pathVariables || !params.pathVariables[v])) { if (!params.pathVariables) params.pathVariables = {}; params.pathVariables[v] = params.data[v]; delete params.data[v]; } }); } resolvedPath = replacePathVariables(params.action.path, params.pathVariables || {}); } // Execute the passthrough request with all components const result = await this.executePassthrough( normalizedActionId, params.connectionKey, params.data, resolvedPath, method, params.queryParams, params.headers, params.isFormData, params.isFormUrlEncoded, false ); return { success: true, data: result.executed ? result.responseData : undefined, connectionKey: params.connectionKey, platform: params.platform, action: fullAction.title, requestConfig: result.requestConfig, content: `Executed ${fullAction.title} via ${params.platform}`, }; } catch (error: any) { console.error("Error executing action:", error); return { success: false, title: "Failed to execute action", message: error.message, raw: JSON.stringify(error?.response?.data || error) }; } } }) }; // Add the promptToConnectPlatform tool if authkit is enabled if (this.useAuthkit) { return { ...baseTool, ...this.getPromptToConnectPlatformTool() }; } return baseTool; } }