From 7f1e40ada70e81fac113d63e4ac0707bb3e7da34 Mon Sep 17 00:00:00 2001 From: Michael Liebmann Date: Sun, 27 Oct 2024 20:21:55 +0700 Subject: [PATCH] removed OpenAI call, added assistant instructions, cleaned --- .gitignore | 1 + src/actions/getPipedriveLeadSummary.ts | 258 ---------------------- src/actions/getPipedriveLeadorDealInfo.ts | 254 +++++++++++++++++++++ src/index.ts | 2 +- 4 files changed, 256 insertions(+), 259 deletions(-) delete mode 100644 src/actions/getPipedriveLeadSummary.ts create mode 100644 src/actions/getPipedriveLeadorDealInfo.ts diff --git a/.gitignore b/.gitignore index 50a16cc..70e1e65 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /dist .env .DS_Store +.cursorrules \ No newline at end of file diff --git a/src/actions/getPipedriveLeadSummary.ts b/src/actions/getPipedriveLeadSummary.ts deleted file mode 100644 index 7dade5c..0000000 --- a/src/actions/getPipedriveLeadSummary.ts +++ /dev/null @@ -1,258 +0,0 @@ -import { ActionDefinition, ActionContext, OutputObject } from 'connery'; -import OpenAI from 'openai'; -import axios from 'axios'; - -const actionDefinition: ActionDefinition = { - key: 'getPipedriveLeadOrDealSummary', - name: 'Get Pipedrive Lead or Deal Status or Summary', - description: 'Receive a status or summary from a Pipedrive lead or deal using OpenAI', - type: 'read', - inputParameters: [ - { - key: 'pipedriveCompanyDomain', - name: 'Pipedrive Company Domain', - description: 'Your Pipedrive company domain (e.g. yourcompany.pipedrive.com)', - type: 'string', - validation: { required: true }, - }, - { - key: 'pipedriveApiKey', - name: 'Pipedrive API Key', - description: 'Your Pipedrive API key', - type: 'string', - validation: { required: true }, - }, - { - key: 'openaiApiKey', - name: 'OpenAI API Key', - description: 'Your OpenAI API key', - type: 'string', - validation: { required: true }, - }, - { - key: 'openaiModel', - name: 'OpenAI Model', - description: 'The OpenAI model to use (e.g., gpt-4o)', - type: 'string', - validation: { required: true }, - }, - { - key: 'searchTerm', - name: 'Search Term', - description: 'Company name, contact name, or deal name to search for', - type: 'string', - validation: { required: true }, - }, - ], - operation: { - handler: handler, - }, - outputParameters: [ - { - key: 'textResponse', - name: 'Text Response', - description: 'The summarized lead or deal information', - type: 'string', - validation: { required: true }, - }, - ], -}; - -export default actionDefinition; - -export async function handler({ input }: ActionContext): Promise { - const { pipedriveCompanyDomain, pipedriveApiKey, openaiApiKey, openaiModel, searchTerm } = input; - - //console.log('Handler function started'); - //console.log(`Search Term: ${searchTerm}`); - - const fullDomain = pipedriveCompanyDomain.includes('.pipedrive.com') - ? pipedriveCompanyDomain - : `${pipedriveCompanyDomain}.pipedrive.com`; - - try { - //console.log('Searching for Pipedrive lead or deal...'); - const leadResults = await searchPipedriveLead(pipedriveApiKey, fullDomain, searchTerm); - const dealResults = await searchPipedriveDeal(pipedriveApiKey, fullDomain, searchTerm); - - //console.log('Lead Results:', JSON.stringify(leadResults, null, 2)); - //console.log('Deal Results:', JSON.stringify(dealResults, null, 2)); - - const bestLead = leadResults.data && leadResults.data.items - ? findBestMatch(leadResults.data.items.map((item: any) => item.item), searchTerm) - : null; - const bestDeal = dealResults.data && dealResults.data.items - ? findBestMatch(dealResults.data.items.map((item: any) => item.item), searchTerm) - : null; - - let summaryData; - if (bestLead && (!bestDeal || bestLead.result_score > bestDeal.result_score)) { - summaryData = { type: 'lead', ...bestLead }; - } else if (bestDeal) { - const activities = await getActivitiesForDeal(pipedriveApiKey, fullDomain, bestDeal.id); - summaryData = { type: 'deal', ...bestDeal, activities }; - } else { - throw new Error('No matching leads or deals found'); - } - - //console.log('Generating summary...'); - const summary = await generateSummary(openaiApiKey, openaiModel, summaryData); - - return { - textResponse: summary, - }; - } catch (error) { - console.error('Error:', error instanceof Error ? error.message : String(error)); - throw new Error(`Failed to process request: ${error instanceof Error ? error.message : String(error)}`); - } -} - -async function searchPipedriveLead(apiKey: string, companyDomain: string, searchTerm: string) { - const baseUrl = `https://${companyDomain}/api/v1`; - const headers = { 'x-api-token': apiKey, 'Accept': 'application/json' }; - - try { - //console.log(`Searching lead with URL: ${baseUrl}/leads/search`); - const response = await axios.get(`${baseUrl}/leads/search`, { - headers, - params: { - term: searchTerm, - fields: 'title,custom_fields,notes', - exact_match: false, - limit: 10 - }, - }); - return response.data; - } catch (error) { - if (axios.isAxiosError(error)) { - console.error('Error searching for lead:', error.message); - console.error('Response data:', error.response?.data); - } else { - console.error('Error searching for lead:', error instanceof Error ? error.message : String(error)); - } - return { data: { items: [] } }; - } -} - -async function searchPipedriveDeal(apiKey: string, companyDomain: string, searchTerm: string) { - const baseUrl = `https://${companyDomain}/api/v1`; - const headers = { 'x-api-token': apiKey, 'Accept': 'application/json' }; - - try { - //console.log(`Searching deal with URL: ${baseUrl}/deals/search`); - const response = await axios.get(`${baseUrl}/deals/search`, { - headers, - params: { - term: searchTerm, - fields: 'title,custom_fields,notes', - exact_match: false, - limit: 10 - }, - }); - return response.data; - } catch (error) { - if (axios.isAxiosError(error)) { - console.error('Error searching for deal:', error.message); - console.error('Response data:', error.response?.data); - } else { - console.error('Error searching for deal:', error instanceof Error ? error.message : String(error)); - } - return { data: [] }; - } -} - -function findBestMatch(items: any[], searchTerm: string): any | null { - if (!Array.isArray(items)) { - //console.error('findBestMatch: items is not an array', items); - return null; - } - - const searchTermLower = searchTerm.toLowerCase(); - return items.reduce((best, current) => { - const titleMatch = (current.title || '').toLowerCase().includes(searchTermLower); - const orgMatch = (current.organization?.name || '').toLowerCase().includes(searchTermLower); - const personMatch = (current.person?.name || '').toLowerCase().includes(searchTermLower); - - if (titleMatch || orgMatch || personMatch) { - if (!best || (current.result_score && current.result_score > best.result_score)) { - return current; - } - } - return best; - }, null); -} - -async function getActivitiesForDeal(apiKey: string, companyDomain: string, dealId: number) { - const baseUrl = `https://${companyDomain}/api/v1`; - const headers = { 'x-api-token': apiKey, 'Accept': 'application/json' }; - - try { - const response = await axios.get(`${baseUrl}/deals/${dealId}/activities`, { - headers, - params: { limit: 5, sort: 'due_date DESC' }, - }); - return response.data.data; - } catch (error) { - console.error('Error fetching activities:', error instanceof Error ? error.message : String(error)); - return []; - } -} - -async function generateSummary(apiKey: string, model: string, data: any) { - const openai = new OpenAI({ apiKey }); - - const prompt = ` - Summarize the following Pipedrive ${data.type} information in a concise and well-readable format that fits on one screen: - - ${data.type.charAt(0).toUpperCase() + data.type.slice(1)}: ${JSON.stringify(data)} - - Please adhere to the following guidelines: - - - ${data.type.charAt(0).toUpperCase() + data.type.slice(1)} Overview: Summarize in two lines, including status and source if available. Always add lead/deal source if given, and omit any cryptic IDs. - - Company Information: Present all available company details in one line, emphasizing any size information. - - Contact Details: Provide one line per contact, always including phone and email if available. - - Notes and Activities: Avoid duplicating content. If there's overlap between notes and activities, combine them. Include duration if given. Provide one line per activity. - ${data.type === 'deal' ? `- Activities: List the most recent activities or next steps, one line per activity.` : ''} - ${data.type === 'lead' ? `- Deals Information: State "No deals are currently associated with this lead as it has not been converted to a deal yet."` : ''} - - Overall Next Steps: Include only if explicitly stated in the input; do not make up any information. - - Output should be in plain text without any special formatting. - - Use only the information provided above. Do not add any information that is not present in the given data. - `; - - /* - const prompt = ` - Summarize the following Pipedrive ${data.type} information in a structured and well-readable format: - - ${data.type.charAt(0).toUpperCase() + data.type.slice(1)}: ${JSON.stringify(data)} - - Please include the following sections: - 1. ${data.type.charAt(0).toUpperCase() + data.type.slice(1)} Overview (leave out cryptic IDs) - 2. Company Information (address, location and postal code info in one line, any info on annual spend or company size is important) - 3. Contact Details (include everything that is available, organize all info in onle line for each available contact) - 4. Notes Summary (explicitly include information about source, spend, information on company size, contact details, and next steps, if any) - ${data.type === 'deal' ? `5. Activities (list the most recent activities or next steps)` : ''} - ${data.type === 'lead' ? `5. Deals Information: Explicitly state "No deals are currently associated with this lead as it has not been converted to a deal yet."` : ''} - 6. Overall Next Steps (do not make up anything here, only add if clear from the input. If nothing was found in the input, mention that in your output). - - Use only the information provided above. Do not add any information that is not present in the given data. - `;*/ - - try { - const response = await openai.chat.completions.create({ - model: model, - messages: [ - { role: 'system', content: 'You are a helpful assistant that summarizes Pipedrive lead and deal information.' }, - { role: 'user', content: prompt }, - ], - }); - - const content = response.choices[0]?.message?.content; - if (!content) throw new Error('No summary generated'); - return content.trim(); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - throw new Error(`Failed to generate summary: ${errorMessage}`); - } -} diff --git a/src/actions/getPipedriveLeadorDealInfo.ts b/src/actions/getPipedriveLeadorDealInfo.ts new file mode 100644 index 0000000..a3c243b --- /dev/null +++ b/src/actions/getPipedriveLeadorDealInfo.ts @@ -0,0 +1,254 @@ +import { ActionDefinition, ActionContext, OutputObject } from 'connery'; +import axios from 'axios'; + +const actionDefinition: ActionDefinition = { + key: 'getPipedriveLeadOrDealInfo', + name: 'Get Pipedrive Lead or Deal Information', + description: 'Retrieve comprehensive information about a Pipedrive lead or deal', + type: 'read', + inputParameters: [ + { + key: 'pipedriveCompanyDomain', + name: 'Pipedrive Company Domain', + description: 'Your Pipedrive company domain (e.g. yourcompany.pipedrive.com)', + type: 'string', + validation: { required: true }, + }, + { + key: 'pipedriveApiKey', + name: 'Pipedrive API Key', + description: 'Your Pipedrive API key', + type: 'string', + validation: { required: true }, + }, + { + key: 'instructions', + name: 'Instructions', + description: 'Optional instructions for processing the lead or deal information', + type: 'string', + validation: { required: false }, + }, + { + key: 'searchTerm', + name: 'Search Term', + description: 'Company name, contact name, or deal name to search for', + type: 'string', + validation: { required: true }, + }, + ], + operation: { + handler: handler, + }, + outputParameters: [ + { + key: 'textResponse', + name: 'Text Response', + description: 'The comprehensive lead or deal information', + type: 'string', + validation: { required: true }, + }, + ], +}; + +export default actionDefinition; + +export async function handler({ input }: ActionContext): Promise { + const { pipedriveApiKey, companyDomain, searchTerm, instructions } = input; + const fullDomain = `${companyDomain}.pipedrive.com`; + + try { + const [leadResults, dealResults] = await Promise.all([ + searchPipedriveLead(pipedriveApiKey, fullDomain, searchTerm), + searchPipedriveDeal(pipedriveApiKey, fullDomain, searchTerm) + ]); + + const bestDeal = dealResults.data?.items?.length > 0 + ? findBestMatch(dealResults.data.items, searchTerm) + : null; + const bestLead = leadResults.data?.items?.length > 0 + ? findBestMatch(leadResults.data.items, searchTerm) + : null; + + let info: any = { + searchTerm, + leadsFound: leadResults.data?.items?.length ?? 0, + dealsFound: dealResults.data?.items?.length ?? 0, + }; + + if (bestDeal) { + info.bestMatch = { + type: 'deal', + data: await getDealInfo(pipedriveApiKey, fullDomain, bestDeal.id) + }; + } else if (bestLead) { + info.bestMatch = { + type: 'lead', + data: await getLeadInfo(pipedriveApiKey, fullDomain, bestLead.id) + }; + } else { + info.message = "No exact matches found."; + info.closestMatches = { + deals: dealResults.data.items.slice(0, 3).map((item: any) => ({ + id: item.id, + title: item.title + })), + leads: leadResults.data.items.slice(0, 3).map((item: any) => ({ + id: item.id, + title: item.title + })) + }; + } + + // Function to remove null values + function removeNulls(obj: any): any { + return Object.fromEntries( + Object.entries(obj) + .filter(([_, v]) => v != null) + .map(([k, v]) => [k, typeof v === 'object' ? removeNulls(v) : v]) + ); + } + + let cleanedInfo = removeNulls(info); + let responseJson = JSON.stringify(cleanedInfo, null, 2); + + if (instructions) { + responseJson = `Instructions for the following content: ${instructions}\n\n${responseJson}`; + } + + if (responseJson.length > 90000) { + responseJson = responseJson.substring(0, 90000); + } + + return { + textResponse: responseJson, + }; + } catch (error) { + throw new Error(`Failed to process request: ${error instanceof Error ? error.message : String(error)}`); + } +} + +async function searchPipedriveLead(apiKey: string, companyDomain: string, searchTerm: string) { + const baseUrl = `https://${companyDomain}/api/v1`; + const url = `${baseUrl}/leads/search`; + + const headers = { 'x-api-token': apiKey, 'Accept': 'application/json' }; + const params = { + term: searchTerm, + fields: 'title,custom_fields,notes', + exact_match: false, + limit: 10 + }; + + try { + const response = await axios.get(url, { headers, params }); + return response.data; + } catch (error) { + console.error('Error searching for lead:', error); + if (axios.isAxiosError(error)) { + console.error('Response data:', error.response?.data); + } + return { data: { items: [] } }; + } +} + +async function searchPipedriveDeal(apiKey: string, companyDomain: string, searchTerm: string) { + const baseUrl = `https://${companyDomain}/api/v1`; + const url = `${baseUrl}/deals/search`; + + const headers = { 'x-api-token': apiKey, 'Accept': 'application/json' }; + const params = { + term: searchTerm, + fields: 'title,custom_fields,notes', + exact_match: false, + limit: 10 + }; + + try { + const response = await axios.get(url, { headers, params }); + return response.data; + } catch (error) { + console.error('Error searching for deal:', error); + if (axios.isAxiosError(error)) { + console.error('Response data:', error.response?.data); + } + return { data: [] }; + } +} + +function findBestMatch(items: any[], searchTerm: string): any | null { + if (!items || items.length === 0) return null; + + const searchTermLower = searchTerm.toLowerCase(); + return items.reduce((best, item) => { + const currentItem = item.item || item; + const score = (currentItem.title?.toLowerCase().includes(searchTermLower) ? 3 : 0) + + (currentItem.organization?.name?.toLowerCase().includes(searchTermLower) ? 2 : 0) + + (currentItem.person?.name?.toLowerCase().includes(searchTermLower) ? 1 : 0); + + return (score > best.score || (score === best.score && item.result_score > best.resultScore)) + ? { item: currentItem, score, resultScore: item.result_score } + : best; + }, { item: null, score: -1, resultScore: -1 }).item; +} + +async function getLeadInfo(apiKey: string, companyDomain: string, leadId: string) { + const baseUrl = `https://${companyDomain}/api/v1`; + const headers = { 'x-api-token': apiKey, 'Accept': 'application/json' }; + + try { + const leadInfo = await axios.get(`${baseUrl}/leads/${leadId}`, { headers }); + + let activities = []; + let notes = []; + + try { + const activitiesResponse = await axios.get(`${baseUrl}/leads/${leadId}/activities`, { headers }); + activities = activitiesResponse.data.data || []; + } catch (error) { + console.warn('Failed to fetch lead activities:', error instanceof Error ? error.message : String(error)); + } + + try { + const notesResponse = await axios.get(`${baseUrl}/leads/${leadId}/notes`, { headers }); + notes = notesResponse.data.data || []; + } catch (error) { + console.warn('Failed to fetch lead notes:', error instanceof Error ? error.message : String(error)); + } + + return { + lead: leadInfo.data.data, + activities, + notes, + }; + } catch (error) { + console.error('Error fetching lead info:', error instanceof Error ? error.message : String(error)); + if (axios.isAxiosError(error) && error.response) { + console.error('Response status:', error.response.status); + console.error('Response data:', error.response.data); + } + throw new Error(`Failed to fetch lead info: ${error instanceof Error ? error.message : String(error)}`); + } +} + +async function getDealInfo(apiKey: string, companyDomain: string, dealId: number) { + const baseUrl = `https://${companyDomain}/api/v1`; + const url = `${baseUrl}/deals/${dealId}`; + + const headers = { 'x-api-token': apiKey, 'Accept': 'application/json' }; + const params = { get_all_custom_fields: true }; + + try { + const dealInfo = await axios.get(url, { headers, params }); + const activities = await axios.get(`${baseUrl}/deals/${dealId}/activities`, { headers }); + const notes = await axios.get(`${baseUrl}/deals/${dealId}/notes`, { headers }); + + return { + deal: dealInfo.data.data, + activities: activities.data.data, + notes: notes.data.data, + }; + } catch (error) { + console.error('Error fetching deal info:', error instanceof Error ? error.message : String(error)); + throw new Error(`Failed to fetch deal info: ${error instanceof Error ? error.message : String(error)}`); + } +} diff --git a/src/index.ts b/src/index.ts index d71600a..2b24d4a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ import { PluginDefinition, setupPluginServer } from 'connery'; -import getPipedriveLeadSummary from "./actions/getPipedriveLeadSummary.js"; +import getPipedriveLeadSummary from "./actions/getPipedriveLeadorDealInfo.js"; const pluginDefinition: PluginDefinition = { name: 'Pipedrive',