diff --git a/apps/rowboat/app/actions.ts b/apps/rowboat/app/actions.ts index 594944f2..2208681b 100644 --- a/apps/rowboat/app/actions.ts +++ b/apps/rowboat/app/actions.ts @@ -15,7 +15,7 @@ import crypto from 'crypto'; import { SignJWT } from "jose"; import { Claims, getSession } from "@auth0/nextjs-auth0"; import { revalidatePath } from "next/cache"; -import { baseWorkflow } from "./lib/utils"; +import { baseWorkflow, callClientToolWebhook, getAgenticApiResponse } from "./lib/utils"; const crawler = new FirecrawlApp({ apiKey: process.env.FIRECRAWL_API_KEY || '' }); @@ -466,25 +466,8 @@ export async function getAssistantResponse( }> { await projectAuthCheck(projectId); - // call agentic api - const response = await fetch(process.env.AGENTIC_API_URL + '/chat', { - method: 'POST', - body: JSON.stringify(request), - headers: { - 'Content-Type': 'application/json', - }, - }); - if (!response.ok) { - console.error('Failed to call agentic api', response); - throw new Error(`Failed to call agentic api: ${response.statusText}`); - } - const responseJson = await response.json(); - const result: z.infer = responseJson; - return { - messages: convertFromAgenticAPIChatMessages(result.messages), - state: result.state, - rawAPIResponse: result, - }; + const response = await getAgenticApiResponse(request); + return response; } export async function getCopilotResponse( @@ -916,61 +899,6 @@ export async function executeClientTool( ): Promise { await projectAuthCheck(projectId); - const project = await projectsCollection.findOne({ - "_id": projectId, - }); - if (!project) { - throw new Error('Project not found'); - } - - if (!project.webhookUrl) { - throw new Error('Webhook URL not found'); - } - - // prepare request body - const content = JSON.stringify({ - toolCall, - } as z.infer); - const requestId = crypto.randomUUID(); - const bodyHash = crypto - .createHash('sha256') - .update(content, 'utf8') - .digest('hex'); - - // sign request - const jwt = await new SignJWT({ - requestId, - projectId, - bodyHash, - } as z.infer) - .setProtectedHeader({ - alg: 'HS256', - typ: 'JWT', - }) - .setIssuer('rowboat') - .setAudience(project.webhookUrl) - .setSubject(`tool-call-${toolCall.id}`) - .setJti(requestId) - .setIssuedAt() - .setExpirationTime("5 minutes") - .sign(new TextEncoder().encode(project.secret)); - - // make request - const request: z.infer = { - requestId, - content, - }; - const response = await fetch(project.webhookUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-signature-jwt': jwt, - }, - body: JSON.stringify(request), - }); - if (!response.ok) { - throw new Error(`Failed to call webhook: ${response.status}: ${response.statusText}`); - } - const responseBody = await response.json(); - return responseBody; + const result = await callClientToolWebhook(toolCall, projectId); + return result; } \ No newline at end of file diff --git a/apps/rowboat/app/api/v1/chats/[chatId]/close/route.ts b/apps/rowboat/app/api/widget/v1/chats/[chatId]/close/route.ts similarity index 100% rename from apps/rowboat/app/api/v1/chats/[chatId]/close/route.ts rename to apps/rowboat/app/api/widget/v1/chats/[chatId]/close/route.ts diff --git a/apps/rowboat/app/api/v1/chats/[chatId]/messages/route.ts b/apps/rowboat/app/api/widget/v1/chats/[chatId]/messages/route.ts similarity index 100% rename from apps/rowboat/app/api/v1/chats/[chatId]/messages/route.ts rename to apps/rowboat/app/api/widget/v1/chats/[chatId]/messages/route.ts diff --git a/apps/rowboat/app/api/v1/chats/[chatId]/route.ts b/apps/rowboat/app/api/widget/v1/chats/[chatId]/route.ts similarity index 100% rename from apps/rowboat/app/api/v1/chats/[chatId]/route.ts rename to apps/rowboat/app/api/widget/v1/chats/[chatId]/route.ts diff --git a/apps/rowboat/app/api/v1/chats/[chatId]/turn/route.ts b/apps/rowboat/app/api/widget/v1/chats/[chatId]/turn/route.ts similarity index 95% rename from apps/rowboat/app/api/v1/chats/[chatId]/turn/route.ts rename to apps/rowboat/app/api/widget/v1/chats/[chatId]/turn/route.ts index 80ea9b2d..0d437504 100644 --- a/apps/rowboat/app/api/v1/chats/[chatId]/turn/route.ts +++ b/apps/rowboat/app/api/widget/v1/chats/[chatId]/turn/route.ts @@ -4,8 +4,8 @@ import { agentWorkflowsCollection, db, projectsCollection } from "@/app/lib/mong import { z } from "zod"; import { ObjectId, WithId } from "mongodb"; import { authCheck } from "../../../utils"; -import { AgenticAPIChatRequest, convertToAgenticAPIChatMessages, convertToCoreMessages, convertWorkflowToAgenticAPI } from "@/app/lib/types"; -import { executeClientTool, getAssistantResponse } from "@/app/actions"; +import { AgenticAPIChatRequest, convertToAgenticAPIChatMessages, convertWorkflowToAgenticAPI } from "@/app/lib/types"; +import { callClientToolWebhook, getAgenticApiResponse } from "@/app/lib/utils"; const chatsCollection = db.collection>("chats"); const chatMessagesCollection = db.collection>("chatMessages"); @@ -91,7 +91,7 @@ export async function POST( startAgent, }; console.log("turn: sending agentic request", JSON.stringify(request, null, 2)); - const response = await getAssistantResponse(session.projectId, request); + const response = await getAgenticApiResponse(request); state = response.state; if (response.messages.length === 0) { throw new Error("No messages returned from assistant"); @@ -111,7 +111,7 @@ export async function POST( const toolCallResults = await Promise.all(lastMessage.tool_calls.map(async toolCall => { console.log('executing tool call', toolCall); try { - return await executeClientTool(toolCall, session.projectId); + return await callClientToolWebhook(toolCall, session.projectId); } catch (error) { console.error(`Error executing tool call ${toolCall.id}:`, error); return { error: "Tool execution failed" }; diff --git a/apps/rowboat/app/api/v1/chats/route.ts b/apps/rowboat/app/api/widget/v1/chats/route.ts similarity index 100% rename from apps/rowboat/app/api/v1/chats/route.ts rename to apps/rowboat/app/api/widget/v1/chats/route.ts diff --git a/apps/rowboat/app/api/v1/session/guest/route.ts b/apps/rowboat/app/api/widget/v1/session/guest/route.ts similarity index 100% rename from apps/rowboat/app/api/v1/session/guest/route.ts rename to apps/rowboat/app/api/widget/v1/session/guest/route.ts diff --git a/apps/rowboat/app/api/v1/session/user/route.ts b/apps/rowboat/app/api/widget/v1/session/user/route.ts similarity index 100% rename from apps/rowboat/app/api/v1/session/user/route.ts rename to apps/rowboat/app/api/widget/v1/session/user/route.ts diff --git a/apps/rowboat/app/api/v1/utils.ts b/apps/rowboat/app/api/widget/v1/utils.ts similarity index 100% rename from apps/rowboat/app/api/v1/utils.ts rename to apps/rowboat/app/api/widget/v1/utils.ts diff --git a/apps/rowboat/app/lib/utils.ts b/apps/rowboat/app/lib/utils.ts index f48bed8e..8cf23a93 100644 --- a/apps/rowboat/app/lib/utils.ts +++ b/apps/rowboat/app/lib/utils.ts @@ -1,5 +1,9 @@ -import { Workflow } from "@/app/lib/types"; +import { AgenticAPIChatRequest, AgenticAPIChatResponse, ClientToolCallJwt, ClientToolCallRequest, ClientToolCallRequestBody, convertFromAgenticAPIChatMessages, Workflow } from "@/app/lib/types"; import { z } from "zod"; +import { projectsCollection } from "./mongodb"; +import { apiV1 } from "rowboat-shared"; +import { SignJWT } from "jose"; +import crypto from "crypto"; export const baseWorkflow: z.infer = { projectId: "", @@ -100,4 +104,95 @@ You are an helpful customer support assistant }, ], tools: [], -}; \ No newline at end of file +}; + +export async function callClientToolWebhook( + toolCall: z.infer['tool_calls'][number], + projectId: string, +): Promise { + const project = await projectsCollection.findOne({ + "_id": projectId, + }); + if (!project) { + throw new Error('Project not found'); + } + + if (!project.webhookUrl) { + throw new Error('Webhook URL not found'); + } + + // prepare request body + const content = JSON.stringify({ + toolCall, + } as z.infer); + const requestId = crypto.randomUUID(); + const bodyHash = crypto + .createHash('sha256') + .update(content, 'utf8') + .digest('hex'); + + // sign request + const jwt = await new SignJWT({ + requestId, + projectId, + bodyHash, + } as z.infer) + .setProtectedHeader({ + alg: 'HS256', + typ: 'JWT', + }) + .setIssuer('rowboat') + .setAudience(project.webhookUrl) + .setSubject(`tool-call-${toolCall.id}`) + .setJti(requestId) + .setIssuedAt() + .setExpirationTime("5 minutes") + .sign(new TextEncoder().encode(project.secret)); + + // make request + const request: z.infer = { + requestId, + content, + }; + const response = await fetch(project.webhookUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-signature-jwt': jwt, + }, + body: JSON.stringify(request), + }); + if (!response.ok) { + throw new Error(`Failed to call webhook: ${response.status}: ${response.statusText}`); + } + const responseBody = await response.json(); + return responseBody; +} + +export async function getAgenticApiResponse( + request: z.infer, +): Promise<{ + messages: z.infer[], + state: unknown, + rawAPIResponse: unknown, +}> { + // call agentic api + const response = await fetch(process.env.AGENTIC_API_URL + '/chat', { + method: 'POST', + body: JSON.stringify(request), + headers: { + 'Content-Type': 'application/json', + }, + }); + if (!response.ok) { + console.error('Failed to call agentic api', response); + throw new Error(`Failed to call agentic api: ${response.statusText}`); + } + const responseJson = await response.json(); + const result: z.infer = responseJson; + return { + messages: convertFromAgenticAPIChatMessages(result.messages), + state: result.state, + rawAPIResponse: result, + }; +} \ No newline at end of file