From 6ebe9f9d6f8d344399386741ba74ee81e52a5775 Mon Sep 17 00:00:00 2001 From: Nicholas Mirigliani Date: Fri, 25 Jul 2025 11:57:56 -0400 Subject: [PATCH 1/2] first pass open source changes --- index.ts | 56 +++++ lib/agent.ts | 93 ++++--- src/agents/clerk-agent/tools.ts | 231 ------------------ src/agents/smartlead-agent/index.ts | 122 ++++----- .../smartlead-agent/smartlead-helpers.ts | 75 ------ 5 files changed, 163 insertions(+), 414 deletions(-) delete mode 100644 src/agents/clerk-agent/tools.ts delete mode 100644 src/agents/smartlead-agent/smartlead-helpers.ts diff --git a/index.ts b/index.ts index a216664..b28d704 100644 --- a/index.ts +++ b/index.ts @@ -24,6 +24,13 @@ if (!process.env.AGENTUITY_API_KEY && !process.env.AGENTUITY_SDK_KEY) { process.exit(1); } +if (!process.env.AGENTUITY_PROJECT_KEY) { + console.error( + "\x1b[31m[ERROR] AGENTUITY_PROJECT_KEY is not set. Please set this environment variable.\x1b[0m" + ); + process.exit(1); +} + if (!process.env.AGENTUITY_TRANSPORT_URL) { console.warn( "\x1b[31m[WARN] You are running this agent outside of the Agentuity environment. Any automatic Agentuity features will be disabled.\x1b[0m" @@ -40,6 +47,55 @@ if (!process.env.AGENTUITY_TRANSPORT_URL) { } } +if (!process.env.ATTIO_AUTH_TOKEN) { + console.error( + "\x1b[31m[ERROR] ATTIO_AUTH_TOKEN is not set. Please set this environment variable.\x1b[0m" + ); + process.exit(1); +} + +if (!process.env.SLACK_WEBHOOK) { + console.error( + "\x1b[31m[ERROR] SLACK_WEBHOOK is not set. Please set this environment variable.\x1b[0m" + ); + process.exit(1); +} + +if (!process.env.SMARTLEAD_API_KEY) { + console.error( + "\x1b[31m[ERROR] SMARTLEAD_API_KEY is not set. Please set this environment variable.\x1b[0m" + ); + process.exit(1); +} + +if (!process.env.STRIPE_SIGNING_SECRET) { + console.error( + "\x1b[31m[ERROR] STRIPE_SIGNING_SECRET is not set. Please set this environment variable.\x1b[0m" + ); + process.exit(1); +} + +if (!process.env.STRIPE_API_KEY) { + console.error( + "\x1b[31m[ERROR] STRIPE_API_KEY is not set. Please set this environment variable.\x1b[0m" + ); + process.exit(1); +} + +if (!process.env.COMPOSIO_API_KEY) { + console.error( + "\x1b[31m[ERROR] COMPOSIO_API_KEY is not set. Please set this environment variable.\x1b[0m" + ); + process.exit(1); +} + +if (!process.env.COMPOSIO_USER_ID) { + console.error( + "\x1b[31m[ERROR] COMPOSIO_USER_ID is not set. Please set this environment variable.\x1b[0m" + ); + process.exit(1); +} + runner(true, import.meta.dirname).catch((err) => { if (err instanceof Error) { console.error(err.message); diff --git a/lib/agent.ts b/lib/agent.ts index 6a5e3de..2c4790e 100644 --- a/lib/agent.ts +++ b/lib/agent.ts @@ -3,6 +3,13 @@ import { Composio } from "@composio/core"; import { AnthropicProvider } from "@composio/anthropic"; import { Anthropic } from "@anthropic-ai/sdk"; +const client = new Anthropic(); + +const composio = new Composio({ + apiKey: process.env.COMPOSIO_API_KEY, + provider: new AnthropicProvider(), +}); + export const createAgent = ( prompt: string, extraTools: any[] = [], @@ -19,46 +26,33 @@ export const createAgent = ( resp: AgentResponse, ctx: AgentContext ) { - const rawBody = await req.data.text(); - if (verifyWebhook && !verifyWebhook(rawBody, req, resp, ctx)) { + const payload = await req.data.json(); + if ( + verifyWebhook && + !verifyWebhook(JSON.stringify(payload), req, resp, ctx) + ) { return resp.json({ success: false, error: "Webhook verification failed.", }); } - const client = new Anthropic(); - - const composio = new Composio({ - apiKey: process.env.COMPOSIO_API_KEY, - provider: new AnthropicProvider(), - }); + const userId = process.env.COMPOSIO_USER_ID; + if (!userId) { + throw new Error("COMPOSIO_USER_ID is not set"); + } - const REQUIRED_TOOLS = [ - "ATTIO_FIND_RECORD", - "ATTIO_UPDATE_RECORD", - "ATTIO_CREATE_RECORD", - "ATTIO_LIST_RECORDS", - "ATTIO_GET_OBJECT", - "SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL", - "getOrgIdFromCustomer", - "latestAttioNumber", - ]; - - const allTools = await composio.tools.get("joel", { - toolkits: ["ATTIO", "SLACK"], + const tools = await composio.tools.get(userId, { + tools: [ + "ATTIO_FIND_RECORD", + "ATTIO_UPDATE_RECORD", + "ATTIO_CREATE_RECORD", + "ATTIO_LIST_RECORDS", + "ATTIO_GET_OBJECT", + "SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL", + ], }); - const tools = allTools.filter((t) => REQUIRED_TOOLS.includes(t.name)); - //console.log("tools: ", tools); - - const payload = JSON.parse(rawBody); // parse it here because we read it as text for verification - - // Note: Need to specify attribute names for the tool call: the default for email is "email" but it should be "email_addresses" - - // Create a set of custom tool names for quick lookup - const customToolNames = new Set(extraTools.map((tool) => tool.name)); - const maxIterations = 10; let iteration = 0; let previousToolCallResults: any[] = []; @@ -105,7 +99,7 @@ ${ toolCalls, null, 2 - )}` + )}\nReason: ${rejectReason}` : "" } @@ -126,10 +120,9 @@ ${ // Claude returns a list of content blocks in `response.content` toolCalls = response.content.filter((block) => block.type === "tool_use"); - if (toolCalls.length) { - // console.log("Tool calls", toolCalls); - } else { - console.log("No tool calls, done."); + // If there are no tool calls, we're done. + if (!toolCalls.length) { + ctx.logger.info("No tool calls detected, finishing up."); const textBlock = response.content.find( (block) => block.type === "text" ); @@ -139,7 +132,6 @@ ${ //JUDGE THE TOOL CALLS HERE const judgeResponse = await client.messages.create({ model: "claude-3-5-haiku-20241022", // Using cheaper Haiku for judge too - // Removed tools - judge should only return text, not make tool calls max_tokens: 1000, stream: false, messages: [ @@ -178,32 +170,38 @@ Respond ONLY with the JSON decision object, no other text: try { judgeDecision = JSON.parse(judgeBlock.text); } catch (error) { - console.log("Judge response that failed to parse:", judgeBlock.text); + ctx.logger.error( + "Judge response that failed to parse:", + judgeBlock.text + ); return resp.text( `Judge response could not be parsed. Raw response: ${judgeBlock.text}` ); } } else { - console.log( + ctx.logger.info( "Full judge response:", JSON.stringify(judgeResponse.content, null, 2) ); return resp.text("No judge response found."); } + if (!judgeDecision) return resp.text("No judge decision."); + if (judgeDecision.decision === "reject") { justRejected = true; - console.log("Rejected tool calls:", toolCalls, judgeDecision); + ctx.logger.info("Rejected tool calls:", toolCalls, judgeDecision); rejectReason = judgeDecision.reason; iteration++; continue; } - // If we get here, the Judge approved the tool calls. + // Judge approved the tool calls justRejected = false; rejectReason = ""; - // Separate custom tools from composio tools + const customToolNames = new Set(extraTools.map((tool) => tool.name)); + const customToolCalls = toolCalls.filter((call: any) => customToolNames.has(call.name) ); @@ -215,14 +213,14 @@ Respond ONLY with the JSON decision object, no other text: // Execute custom tools if (customToolCalls.length > 0) { - console.log("Executing custom tools:", customToolCalls); + ctx.logger.info("Executing custom tools:", customToolCalls); const customResults: any[] = []; for (const toolCall of customToolCalls) { const executor = customToolExecutors[toolCall.name]; if (executor) { try { - const result = await executor(toolCall.input); + const result = await executor(toolCall.input); // This is where the tool call is executed based on its executor. customResults.push({ tool_call_id: toolCall.id, type: "tool_result", @@ -246,18 +244,17 @@ Respond ONLY with the JSON decision object, no other text: } } + // If there are no composio tools, return the custom results - otherwise, merge them later if (composioToolCalls.length === 0) { - // Only custom tools, return custom results toolCallResult = customResults; } else { - // Both custom and composio tools, we'll merge results toolCallResult.customResults = customResults; } } // Execute composio tools if any if (composioToolCalls.length > 0) { - console.log("Executing composio tools:", composioToolCalls); + ctx.logger.info("Executing composio tools:", composioToolCalls); // Create a response-like object with only composio tool calls const composioResponse = { @@ -276,7 +273,7 @@ Respond ONLY with the JSON decision object, no other text: }; const composioResult = await composio.provider.handleToolCalls( - "joel", + userId, composioResponse ); diff --git a/src/agents/clerk-agent/tools.ts b/src/agents/clerk-agent/tools.ts deleted file mode 100644 index 24c424a..0000000 --- a/src/agents/clerk-agent/tools.ts +++ /dev/null @@ -1,231 +0,0 @@ -// import { parseOrgIdString, addOrgToOrgIdString, updateOrgNameInOrgIdString } from "../../../lib/helpers"; - -// // Custom tools in Anthropic format for clerk-agent -// // These handle business logic that composio's basic ATTIO tools don't cover -// export const clerkExtraTools = [ -// { -// name: "addOrgToCompany", -// description: "Add an organization to a company's orgId field using the pipe-delimited format (Name:id|Name2:id2). Automatically handles duplicate prevention.", -// input_schema: { -// type: "object", -// properties: { -// companyId: { -// type: "string", -// description: "The Attio record ID of the company" -// }, -// orgName: { -// type: "string", -// description: "The name of the organization to add" -// }, -// orgId: { -// type: "string", -// description: "The Clerk organization ID" -// } -// }, -// required: ["companyId", "orgName", "orgId"] -// } -// }, -// { -// name: "getCompaniesByOrgId", -// description: "Find all companies that contain a specific organization ID in their orgId field", -// input_schema: { -// type: "object", -// properties: { -// orgId: { -// type: "string", -// description: "The organization ID to search for" -// } -// }, -// required: ["orgId"] -// } -// }, -// { -// name: "updateOrgNameInCompany", -// description: "Update an organization's name in a company's orgId field based on org ID", -// input_schema: { -// type: "object", -// properties: { -// companyId: { -// type: "string", -// description: "The Attio record ID of the company" -// }, -// orgId: { -// type: "string", -// description: "The organization ID to update" -// }, -// newOrgName: { -// type: "string", -// description: "The new organization name" -// } -// }, -// required: ["companyId", "orgId", "newOrgName"] -// } -// } -// ]; - -// // Tool executors for the custom tools -// // These will be called by composio when the tools are invoked -// export const clerkToolExecutors: Record = { -// addOrgToCompany: async ({ -// companyId, -// orgName, -// orgId, -// }: { -// companyId: string; -// orgName: string; -// orgId: string; -// }) => { -// // This function will be called by composio, so we need to make direct API calls -// // First get the current company to check existing orgId -// const response = await fetch(`https://api.attio.com/v2/objects/companies/records/${companyId}`, { -// method: "GET", -// headers: { -// Authorization: `Bearer ${process.env.ATTIO_AUTH_TOKEN}`, -// "Content-Type": "application/json", -// }, -// }); - -// if (!response.ok) { -// throw new Error(`Failed to get company: ${response.status}`); -// } - -// const company = await response.json() as any; - -// // Extract the actual string value from Attio's attribute structure -// const currentOrgId = company?.data?.values?.org_id?.[0]?.value || null; - -// // Add the new org to the string using our helper -// const updatedOrgId = addOrgToOrgIdString(currentOrgId, orgName, orgId); - -// // Update the company with the new orgId string -// const updateResponse = await fetch(`https://api.attio.com/v2/objects/companies/records/${companyId}`, { -// method: "PATCH", -// headers: { -// Authorization: `Bearer ${process.env.ATTIO_AUTH_TOKEN}`, -// "Content-Type": "application/json", -// }, -// body: JSON.stringify({ -// data: { -// values: { -// org_id: updatedOrgId -// } -// } -// }) -// }); - -// if (!updateResponse.ok) { -// throw new Error(`Failed to update company: ${updateResponse.status}`); -// } - -// return await updateResponse.json(); -// }, - -// getCompaniesByOrgId: async ({ orgId }: { orgId: string }) => { -// // Get all companies since Attio doesn't support substring search on orgId field -// const response = await fetch("https://api.attio.com/v2/objects/companies/records/query", { -// method: "POST", -// headers: { -// Authorization: `Bearer ${process.env.ATTIO_AUTH_TOKEN}`, -// "Content-Type": "application/json", -// }, -// body: JSON.stringify({}) // No filter to get all companies -// }); - -// if (!response.ok) { -// throw new Error(`Failed to query companies: ${response.status}`); -// } - -// const search = await response.json() as any; - -// if (!search?.data) return []; - -// // Filter companies that have the orgId in their concatenated orgId string -// const matchingCompanies = search.data.filter((company: any) => { -// const orgIdValue = company?.values?.org_id?.[0]?.value; -// if (!orgIdValue || typeof orgIdValue !== 'string') return false; - -// // Parse the orgId string and check if any org has the target ID -// const orgs = parseOrgIdString(orgIdValue); -// return orgs.some(org => org.id === orgId); -// }); - -// // Return full company records -// const results = []; -// for (const company of matchingCompanies) { -// const recordId = company.id?.record_id; -// if (recordId) { -// const fullCompanyResponse = await fetch(`https://api.attio.com/v2/objects/companies/records/${recordId}`, { -// method: "GET", -// headers: { -// Authorization: `Bearer ${process.env.ATTIO_AUTH_TOKEN}`, -// "Content-Type": "application/json", -// }, -// }); - -// if (fullCompanyResponse.ok) { -// const fullCompany = await fullCompanyResponse.json(); -// results.push(fullCompany); -// } -// } -// } - -// return results; -// }, - -// updateOrgNameInCompany: async ({ -// companyId, -// orgId, -// newOrgName, -// }: { -// companyId: string; -// orgId: string; -// newOrgName: string; -// }) => { -// // First get the current company to check existing orgId -// const response = await fetch(`https://api.attio.com/v2/objects/companies/records/${companyId}`, { -// method: "GET", -// headers: { -// Authorization: `Bearer ${process.env.ATTIO_AUTH_TOKEN}`, -// "Content-Type": "application/json", -// }, -// }); - -// if (!response.ok) { -// throw new Error(`Failed to get company: ${response.status}`); -// } - -// const company = await response.json() as any; - -// // Extract the actual string value from Attio's attribute structure -// const currentOrgId = company?.data?.values?.org_id?.[0]?.value || null; - -// // Update the org name in the string using our helper -// const updatedOrgId = updateOrgNameInOrgIdString(currentOrgId, orgId, newOrgName); - -// if (updatedOrgId === null) { -// throw new Error(`Organization with ID ${orgId} not found in company ${companyId}`); -// } - -// // Update the company with the new orgId string -// const updateResponse = await fetch(`https://api.attio.com/v2/objects/companies/records/${companyId}`, { -// method: "PATCH", -// headers: { -// Authorization: `Bearer ${process.env.ATTIO_AUTH_TOKEN}`, -// "Content-Type": "application/json", -// }, -// body: JSON.stringify({ -// data: { -// values: { -// org_id: updatedOrgId -// } -// } -// }) -// }); - -// if (!updateResponse.ok) { -// throw new Error(`Failed to update company: ${updateResponse.status}`); -// } - -// return await updateResponse.json(); -// }, -// }; \ No newline at end of file diff --git a/src/agents/smartlead-agent/index.ts b/src/agents/smartlead-agent/index.ts index dd461a4..de47cea 100644 --- a/src/agents/smartlead-agent/index.ts +++ b/src/agents/smartlead-agent/index.ts @@ -28,13 +28,13 @@ You MUST fill out all parameters for each tool call. If the event_type is LEAD_CATEGORY_UPDATED, you should: 1. call the ATTIO_FIND_RECORD tool with input: - { - "object_id": "people", - "limit": 1, - "attributes": { - "email_addresses": "" + { + "object_id": "people", + "limit": 1, + "attributes": { + "email_addresses": "" + } } - } 1a. If the lead is not found, call the ATTIO_CREATE_RECORD tool with input: { "object_type": "people", @@ -48,76 +48,78 @@ If the event_type is LEAD_CATEGORY_UPDATED, you should: } After Step 1 (or 1a) you should have access to the person record id. 2. call the ATTIO_FIND_RECORD tool with input: - { - "object_id": "companies", - "limit": 1, - "attributes": { - "name": "" + { + "object_id": "companies", + "limit": 1, + "attributes": { + "name": "" + } } - } 2a. If there is no company with that name, you must create one. Call the ATTIO_CREATE_RECORD tool with input: - { - "object_type": "companies", - "values": { - "name": "" - } - } + { + "object_type": "companies", + "values": { + "name": "" + } + } After Step 2 (or 2a), you should have access to the company record id. 3. call the ATTIO_LIST_RECORDS tool with input: - { - "object_type": "deals", - "limit": 100, - } - You may or may not find a deal with the current lead's company. If - 3a. If there is no deal with the current lead's company, call the ATTIO_CREATE_RECORD tool with input: { "object_type": "deals", - "values": { - "name": "Deal with ", - "stage": "Lead", - "owner": "nmirigliani@agentuity.com", - "value": 0, - "associated_people": [personRecordId], - "associated_company": companyRecordId, - } + "limit": 100, } + You may or may not find a deal with the current lead's company. + 3a. If there is no deal with the current lead's company, call the ATTIO_CREATE_RECORD tool with input: + { + "object_type": "deals", + "values": { + "name": "Deal with ", + "stage": "Lead", + "owner": "nmirigliani@agentuity.com", + "value": 0, + "associated_people": [personRecordId], + "associated_company": companyRecordId, + } + } You should recieve the record that you created. - 3b. If there **is** a deal with the current lead's company, call the ATTIO_UPDATE_RECORD tool with input: - { - "object_type": "deals", - "record_id": " (from Step 3)", - "values": { - "associated_people": [...existingAssociatedPeople (from Step 3), personRecordId], - } - The goal is to add the person to the existing deal. + 3b. If there **is** a deal with the current lead's company, call the ATTIO_UPDATE_RECORD tool with input: + { + "object_type": "deals", + "record_id": " (from Step 3)", + "values": { + "associated_people": [...existingAssociatedPeople (from Step 3), personRecordId], + } + } + The goal is to add the person to the existing deal. 4. Finally, call the SMARTLEAD_SET_LEAD_STATUS_POSITIVE with input: - { - "email": "" - } + { + "email": "" + } + Once you have done this, you should not make any more tool calls and stop completely. If the event_type is EMAIL_REPLY, you should: 1. call the SMARTLEAD_GET_LEAD_STATUS tool with input: - { - "email": "" - } - 1a. If the lead status is "positive", call the SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL tool. - The message should be *exactly*: + { + "email": "" + } + 1a. If the lead status is "positive", call the SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL tool. + The message should be *exactly*: - "<@ID>, you have an email to look at in your inbox () from ()." + "<@ID>, you have an email to look at in your inbox () from ()." - where ID is the user id of the person who should receive the message. You must determine this to be either Matthew Congrove, Jeff Haynie, or Rick Blalock based on the from_email. - The ids are: - - Matthew Congrove: U08A0FWLM24 - - Jeff Haynie: U08993W8V0T - - Rick Blalock: U088UL77GDV - You must keep the ids in the format <@ID> including the "<@" and ">". - { - "channel": "C091N1Z5Q3Y", - "text": "" - } - 1b. If the lead status is not "positive" (including empty reply or nothing), do nothing. + where ID is the user id of the person who should receive the message. You must determine this to be either Matthew Congrove, Jeff Haynie, or Rick Blalock based on the from_email. + The ids are: + - Matthew Congrove: U08A0FWLM24 + - Jeff Haynie: U08993W8V0T + - Rick Blalock: U088UL77GDV + You must keep the ids in the format <@ID> including the "<@" and ">". + { + "channel": "C091N1Z5Q3Y", + "text": "" + } + 1b. If the lead status is not "positive" (including empty reply or nothing), do nothing. After Step 1, you should have sent a message to the appropriate person. Once you have done this, you should stop. `; diff --git a/src/agents/smartlead-agent/smartlead-helpers.ts b/src/agents/smartlead-agent/smartlead-helpers.ts deleted file mode 100644 index 04440fb..0000000 --- a/src/agents/smartlead-agent/smartlead-helpers.ts +++ /dev/null @@ -1,75 +0,0 @@ -// // SmartLead API helpers - -// export interface SmartLeadResponse { -// id?: string; -// lead_campaign_data?: { -// campaign_id?: string; -// }[]; -// custom_fields?: { -// custom_lead_status?: string; -// }; -// } - -// export async function getFromSmartLead( -// url: string -// ): Promise { -// const api_key = process.env.SMARTLEAD_API_KEY; -// if (!api_key) { -// throw new Error("SMARTLEAD_API_KEY environment variable is not set"); -// } -// const new_url = url + `&api_key=${api_key}`; -// console.log("new_url:", new_url); -// const response = await fetch(new_url); -// if (!response.ok) { -// throw new Error("Failed to get data from SmartLead"); -// } -// const data = (await response.json()) as SmartLeadResponse; -// return data; -// } - -// export async function getLeadStatusByEmail(email: string) { -// const smartlead_api_key = process.env.SMARTLEAD_API_KEY; -// if (!smartlead_api_key) { -// throw new Error("SMARTLEAD_API_KEY environment variable is not set"); -// } -// const response = await fetch( -// `https://server.smartlead.ai/api/v1/leads/?api_key=${smartlead_api_key}&email=${email}` -// ); -// if (!response.ok) { -// throw new Error("Failed to get data from SmartLead"); -// } -// const data = await response.json(); -// return data; -// } - -// export async function updateSmartLeadStatus( -// lead_email: string, -// custom_lead_status: string -// ) { -// // Get lead info -// let smartlead_response = await getFromSmartLead( -// `https://server.smartlead.ai/api/v1/leads/?email=${lead_email}` -// ); -// if (smartlead_response) { -// let lead_id = smartlead_response?.id; -// let campaign_id = smartlead_response?.lead_campaign_data?.[0]?.campaign_id; -// if (lead_id && campaign_id) { -// const api_key = process.env.SMARTLEAD_API_KEY; -// const url = `https://server.smartlead.ai/api/v1/campaigns/${campaign_id}/leads/${lead_id}?api_key=${api_key}`; -// const lead_input = { -// email: lead_email, -// custom_fields: { -// custom_lead_status, -// }, -// }; -// const response = await fetch(url, { -// method: "POST", -// headers: { "Content-Type": "application/json" }, -// body: JSON.stringify(lead_input), -// }); -// const result = await response.json(); -// return result; -// } -// } -// throw new Error("Person not found in SmartLead or missing campaign/lead id."); -// } From 2136da72ae6e5bae366e90e8b6cae94257daf427 Mon Sep 17 00:00:00 2001 From: Nicholas Mirigliani Date: Mon, 28 Jul 2025 10:11:45 -0500 Subject: [PATCH 2/2] model change --- lib/agent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/agent.ts b/lib/agent.ts index 2c4790e..4ce6779 100644 --- a/lib/agent.ts +++ b/lib/agent.ts @@ -63,7 +63,7 @@ export const createAgent = ( while (iteration < maxIterations) { const response = await client.messages.create({ - model: "claude-3-5-haiku-20241022", + model: "claude-3-7-sonnet-latest", tools: [...tools, ...extraTools], max_tokens: 1000, stream: false,