From 10f76ef49f6f14cf9658b49963aef338f6bcd4d2 Mon Sep 17 00:00:00 2001 From: ramnique <30795890+ramnique@users.noreply.github.com> Date: Mon, 13 Jan 2025 15:31:31 +0530 Subject: [PATCH] add rowboat app --- apps/.gitignore | 1 + apps/rowboat/.eslintrc.json | 3 + apps/rowboat/.gitignore | 42 + apps/rowboat/README.md | 36 + apps/rowboat/app/actions.ts | 976 + apps/rowboat/app/android-chrome-192x192.png | Bin 0 -> 2566 bytes apps/rowboat/app/android-chrome-512x512.png | Bin 0 -> 5625 bytes apps/rowboat/app/api/auth/[auth0]/route.ts | 3 + .../app/api/v1/chats/[chatId]/close/route.ts | 40 + .../api/v1/chats/[chatId]/messages/route.ts | 94 + .../app/api/v1/chats/[chatId]/route.ts | 43 + .../app/api/v1/chats/[chatId]/turn/route.ts | 152 + apps/rowboat/app/api/v1/chats/route.ts | 116 + .../rowboat/app/api/v1/session/guest/route.ts | 30 + apps/rowboat/app/api/v1/session/user/route.ts | 55 + apps/rowboat/app/api/v1/utils.ts | 62 + apps/rowboat/app/app.tsx | 64 + apps/rowboat/app/apple-touch-icon.png | Bin 0 -> 2456 bytes apps/rowboat/app/browserconfig.xml | 9 + apps/rowboat/app/favicon-16x16.png | Bin 0 -> 630 bytes apps/rowboat/app/favicon-32x32.png | Bin 0 -> 851 bytes apps/rowboat/app/favicon.ico | Bin 0 -> 7406 bytes apps/rowboat/app/globals.css | 78 + apps/rowboat/app/layout.tsx | 30 + .../app/lib/components/FormStatusButton.tsx | 14 + .../app/lib/components/PageSection.tsx | 18 + .../app/lib/components/datasource-icon.tsx | 16 + .../app/lib/components/editable-field.tsx | 155 + apps/rowboat/app/lib/components/icons.tsx | 39 + .../app/lib/components/markdown-content.tsx | 66 + .../rowboat/app/lib/components/pagination.tsx | 24 + .../rowboat/app/lib/components/typewriter.tsx | 52 + .../app/lib/components/user_button.tsx | 37 + apps/rowboat/app/lib/embedding.ts | 3 + apps/rowboat/app/lib/loadenv.ts | 2 + apps/rowboat/app/lib/mongodb.ts | 14 + apps/rowboat/app/lib/types.ts | 625 + apps/rowboat/app/lib/utils.ts | 114 + apps/rowboat/app/loading.tsx | 7 + apps/rowboat/app/mstile-144x144.png | Bin 0 -> 2061 bytes apps/rowboat/app/mstile-150x150.png | Bin 0 -> 2206 bytes apps/rowboat/app/mstile-310x150.png | Bin 0 -> 2536 bytes apps/rowboat/app/mstile-310x310.png | Bin 0 -> 4738 bytes apps/rowboat/app/mstile-70x70.png | Bin 0 -> 1648 bytes apps/rowboat/app/new-chat-link.tsx | 10 + apps/rowboat/app/page.tsx | 5 + .../app/projects/[projectId]/config/app.tsx | 124 + .../app/projects/[projectId]/config/embed.tsx | 42 + .../app/projects/[projectId]/config/page.tsx | 15 + .../projects/[projectId]/config/secret.tsx | 94 + .../[projectId]/config/webhook-url.tsx | 81 + .../app/projects/[projectId]/layout.tsx | 16 + .../rowboat/app/projects/[projectId]/menu.tsx | 85 + apps/rowboat/app/projects/[projectId]/nav.tsx | 68 + .../rowboat/app/projects/[projectId]/page.tsx | 9 + .../projects/[projectId]/playground/app.tsx | 110 + .../projects/[projectId]/playground/chat.tsx | 319 + .../[projectId]/playground/compose-box.tsx | 69 + .../[projectId]/playground/messages.tsx | 763 + .../[projectId]/playground/scenario-list.tsx | 233 + .../playground/simulation-options.tsx | 107 + .../[projectId]/sources/[sourceId]/delete.tsx | 28 + .../[projectId]/sources/[sourceId]/page.tsx | 17 + .../sources/[sourceId]/source-page.tsx | 215 + .../sources/[sourceId]/web-recrawl.tsx | 26 + .../projects/[projectId]/sources/new/form.tsx | 146 + .../projects/[projectId]/sources/new/page.tsx | 21 + .../app/projects/[projectId]/sources/page.tsx | 16 + .../sources/self-updating-source-status.tsx | 51 + .../[projectId]/sources/source-status.tsx | 63 + .../[projectId]/sources/sources-list.tsx | 106 + .../[projectId]/sources/toggle-source.tsx | 44 + .../[projectId]/workflow/agent_config.tsx | 380 + .../[projectId]/workflow/agents_list.tsx | 93 + .../app/projects/[projectId]/workflow/app.tsx | 111 + .../projects/[projectId]/workflow/copilot.tsx | 497 + .../[projectId]/workflow/copilot_actions.tsx | 298 + .../projects/[projectId]/workflow/page.tsx | 51 + .../projects/[projectId]/workflow/pane.tsx | 48 + .../[projectId]/workflow/preview-modal.tsx | 144 + .../[projectId]/workflow/prompt_config.tsx | 71 + .../[projectId]/workflow/prompts_list.tsx | 74 + .../[projectId]/workflow/published_badge.tsx | 10 + .../[projectId]/workflow/tool_config.tsx | 236 + .../[projectId]/workflow/tools_list.tsx | 71 + .../[projectId]/workflow/workflow_editor.tsx | 860 + .../workflow/workflow_selector.tsx | 161 + apps/rowboat/app/projects/app.tsx | 73 + apps/rowboat/app/projects/layout.tsx | 28 + apps/rowboat/app/projects/new/page.tsx | 22 + apps/rowboat/app/projects/new/submit.tsx | 21 + apps/rowboat/app/projects/page.tsx | 5 + apps/rowboat/app/providers.tsx | 15 + apps/rowboat/app/safari-pinned-tab.svg | 29 + apps/rowboat/app/scripts/crawlUrls.ts | 979 + apps/rowboat/app/site.webmanifest | 19 + apps/rowboat/components.json | 21 + apps/rowboat/components/ui/resizable.tsx | 45 + apps/rowboat/hooks/use-click-away.ts | 23 + apps/rowboat/lib/utils.ts | 6 + apps/rowboat/middleware.ts | 45 + apps/rowboat/next.config.mjs | 4 + apps/rowboat/package-lock.json | 14855 ++++++++++++++++ apps/rowboat/package.json | 63 + apps/rowboat/postcss.config.mjs | 8 + apps/rowboat/public/assistant-icon.png | Bin 0 -> 10045 bytes apps/rowboat/public/eval-avatar.png | Bin 0 -> 3235 bytes apps/rowboat/public/landing-bg.jpg | Bin 0 -> 1328216 bytes apps/rowboat/public/logo-header.png | Bin 0 -> 4833 bytes apps/rowboat/public/menu-bar.png | Bin 0 -> 6214 bytes apps/rowboat/public/next.svg | 1 + apps/rowboat/public/rowboat-avatar.png | Bin 0 -> 5641 bytes apps/rowboat/public/rowboat-logo.png | Bin 0 -> 11337 bytes apps/rowboat/public/user-avatar.png | Bin 0 -> 523804 bytes apps/rowboat/public/vercel.svg | 1 + apps/rowboat/tailwind.config.ts | 73 + apps/rowboat/tsconfig.json | 26 + 117 files changed, 25370 insertions(+) create mode 100644 apps/.gitignore create mode 100644 apps/rowboat/.eslintrc.json create mode 100644 apps/rowboat/.gitignore create mode 100644 apps/rowboat/README.md create mode 100644 apps/rowboat/app/actions.ts create mode 100644 apps/rowboat/app/android-chrome-192x192.png create mode 100644 apps/rowboat/app/android-chrome-512x512.png create mode 100644 apps/rowboat/app/api/auth/[auth0]/route.ts create mode 100644 apps/rowboat/app/api/v1/chats/[chatId]/close/route.ts create mode 100644 apps/rowboat/app/api/v1/chats/[chatId]/messages/route.ts create mode 100644 apps/rowboat/app/api/v1/chats/[chatId]/route.ts create mode 100644 apps/rowboat/app/api/v1/chats/[chatId]/turn/route.ts create mode 100644 apps/rowboat/app/api/v1/chats/route.ts create mode 100644 apps/rowboat/app/api/v1/session/guest/route.ts create mode 100644 apps/rowboat/app/api/v1/session/user/route.ts create mode 100644 apps/rowboat/app/api/v1/utils.ts create mode 100644 apps/rowboat/app/app.tsx create mode 100644 apps/rowboat/app/apple-touch-icon.png create mode 100644 apps/rowboat/app/browserconfig.xml create mode 100644 apps/rowboat/app/favicon-16x16.png create mode 100644 apps/rowboat/app/favicon-32x32.png create mode 100644 apps/rowboat/app/favicon.ico create mode 100644 apps/rowboat/app/globals.css create mode 100644 apps/rowboat/app/layout.tsx create mode 100644 apps/rowboat/app/lib/components/FormStatusButton.tsx create mode 100644 apps/rowboat/app/lib/components/PageSection.tsx create mode 100644 apps/rowboat/app/lib/components/datasource-icon.tsx create mode 100644 apps/rowboat/app/lib/components/editable-field.tsx create mode 100644 apps/rowboat/app/lib/components/icons.tsx create mode 100644 apps/rowboat/app/lib/components/markdown-content.tsx create mode 100644 apps/rowboat/app/lib/components/pagination.tsx create mode 100644 apps/rowboat/app/lib/components/typewriter.tsx create mode 100644 apps/rowboat/app/lib/components/user_button.tsx create mode 100644 apps/rowboat/app/lib/embedding.ts create mode 100644 apps/rowboat/app/lib/loadenv.ts create mode 100644 apps/rowboat/app/lib/mongodb.ts create mode 100644 apps/rowboat/app/lib/types.ts create mode 100644 apps/rowboat/app/lib/utils.ts create mode 100644 apps/rowboat/app/loading.tsx create mode 100644 apps/rowboat/app/mstile-144x144.png create mode 100644 apps/rowboat/app/mstile-150x150.png create mode 100644 apps/rowboat/app/mstile-310x150.png create mode 100644 apps/rowboat/app/mstile-310x310.png create mode 100644 apps/rowboat/app/mstile-70x70.png create mode 100644 apps/rowboat/app/new-chat-link.tsx create mode 100644 apps/rowboat/app/page.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/config/app.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/config/embed.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/config/page.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/config/secret.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/config/webhook-url.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/layout.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/menu.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/nav.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/page.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/playground/app.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/playground/chat.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/playground/compose-box.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/playground/messages.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/playground/scenario-list.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/playground/simulation-options.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/sources/[sourceId]/delete.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/sources/[sourceId]/page.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/sources/[sourceId]/source-page.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/sources/[sourceId]/web-recrawl.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/sources/new/form.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/sources/new/page.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/sources/page.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/sources/self-updating-source-status.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/sources/source-status.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/sources/sources-list.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/sources/toggle-source.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/workflow/agent_config.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/workflow/agents_list.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/workflow/app.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/workflow/copilot.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/workflow/copilot_actions.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/workflow/page.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/workflow/pane.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/workflow/preview-modal.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/workflow/prompt_config.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/workflow/prompts_list.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/workflow/published_badge.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/workflow/tool_config.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/workflow/tools_list.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/workflow/workflow_selector.tsx create mode 100644 apps/rowboat/app/projects/app.tsx create mode 100644 apps/rowboat/app/projects/layout.tsx create mode 100644 apps/rowboat/app/projects/new/page.tsx create mode 100644 apps/rowboat/app/projects/new/submit.tsx create mode 100644 apps/rowboat/app/projects/page.tsx create mode 100644 apps/rowboat/app/providers.tsx create mode 100644 apps/rowboat/app/safari-pinned-tab.svg create mode 100644 apps/rowboat/app/scripts/crawlUrls.ts create mode 100644 apps/rowboat/app/site.webmanifest create mode 100644 apps/rowboat/components.json create mode 100644 apps/rowboat/components/ui/resizable.tsx create mode 100644 apps/rowboat/hooks/use-click-away.ts create mode 100644 apps/rowboat/lib/utils.ts create mode 100644 apps/rowboat/middleware.ts create mode 100644 apps/rowboat/next.config.mjs create mode 100644 apps/rowboat/package-lock.json create mode 100644 apps/rowboat/package.json create mode 100644 apps/rowboat/postcss.config.mjs create mode 100644 apps/rowboat/public/assistant-icon.png create mode 100644 apps/rowboat/public/eval-avatar.png create mode 100644 apps/rowboat/public/landing-bg.jpg create mode 100644 apps/rowboat/public/logo-header.png create mode 100644 apps/rowboat/public/menu-bar.png create mode 100644 apps/rowboat/public/next.svg create mode 100644 apps/rowboat/public/rowboat-avatar.png create mode 100644 apps/rowboat/public/rowboat-logo.png create mode 100644 apps/rowboat/public/user-avatar.png create mode 100644 apps/rowboat/public/vercel.svg create mode 100644 apps/rowboat/tailwind.config.ts create mode 100644 apps/rowboat/tsconfig.json diff --git a/apps/.gitignore b/apps/.gitignore new file mode 100644 index 00000000..e43b0f98 --- /dev/null +++ b/apps/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/apps/rowboat/.eslintrc.json b/apps/rowboat/.eslintrc.json new file mode 100644 index 00000000..bffb357a --- /dev/null +++ b/apps/rowboat/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/apps/rowboat/.gitignore b/apps/rowboat/.gitignore new file mode 100644 index 00000000..b3499047 --- /dev/null +++ b/apps/rowboat/.gitignore @@ -0,0 +1,42 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# crawler script artifacts +chunked.jsonl +crawled.jsonl +embeddings.jsonl +rewritten.jsonl diff --git a/apps/rowboat/README.md b/apps/rowboat/README.md new file mode 100644 index 00000000..c4033664 --- /dev/null +++ b/apps/rowboat/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/apps/rowboat/app/actions.ts b/apps/rowboat/app/actions.ts new file mode 100644 index 00000000..fa47527e --- /dev/null +++ b/apps/rowboat/app/actions.ts @@ -0,0 +1,976 @@ +'use server'; + +import { redirect } from "next/navigation"; +import { SimulationData, EmbeddingDoc, GetInformationToolResult, DataSource, PlaygroundChat, AgenticAPIChatRequest, AgenticAPIChatResponse, convertFromAgenticAPIChatMessages, WebpageCrawlResponse, Workflow, WorkflowAgent, CopilotAPIRequest, CopilotAPIResponse, CopilotMessage, CopilotWorkflow, convertToCopilotWorkflow, convertToCopilotApiMessage, convertToCopilotMessage, CopilotAssistantMessage, CopilotChatContext, convertToCopilotApiChatContext, Scenario, ClientToolCallRequestBody, ClientToolCallJwt, ClientToolCallRequest, WithStringId, Project } from "./lib/types"; +import { ObjectId, WithId } from "mongodb"; +import { generateObject, generateText, tool, embed } from "ai"; +import { dataSourcesCollection, embeddingsCollection, projectsCollection, webpagesCollection, agentWorkflowsCollection, scenariosCollection, projectMembersCollection } from "@/app/lib/mongodb"; +import { z } from 'zod'; +import { openai } from "@ai-sdk/openai"; +import FirecrawlApp, { ScrapeResponse } from '@mendable/firecrawl-js'; +import { embeddingModel } from "./lib/embedding"; +import { apiV1 } from "rowboat-shared"; +import { zodToJsonSchema } from 'zod-to-json-schema'; +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"; + +const crawler = new FirecrawlApp({ apiKey: process.env.FIRECRAWL_API_KEY }); + +export async function authCheck(): Promise { + const { user } = await getSession() || {}; + if (!user) { + throw new Error('User not authenticated'); + } + return user; +} + +export async function projectAuthCheck(projectId: string) { + const user = await authCheck(); + const membership = await projectMembersCollection.findOne({ + projectId, + userId: user.sub, + }); + if (!membership) { + throw new Error('User not a member of project'); + } +} + +export async function createWorkflow(projectId: string): Promise>> { + await projectAuthCheck(projectId); + + // get the next workflow number + const doc = await projectsCollection.findOneAndUpdate({ + _id: projectId, + }, { + $inc: { + nextWorkflowNumber: 1, + }, + }, { + returnDocument: 'after' + }); + if (!doc) { + throw new Error('Project not found'); + } + const nextWorkflowNumber = doc.nextWorkflowNumber; + + // create the workflow + const workflow = { + ...baseWorkflow, + projectId, + createdAt: new Date().toISOString(), + lastUpdatedAt: new Date().toISOString(), + name: `Version ${nextWorkflowNumber}`, + }; + const { insertedId } = await agentWorkflowsCollection.insertOne(workflow); + const { _id, ...rest } = workflow as WithId>; + return { + ...rest, + _id: insertedId.toString(), + }; +} + +export async function cloneWorkflow(projectId: string, workflowId: string): Promise>> { + await projectAuthCheck(projectId); + const workflow = await agentWorkflowsCollection.findOne({ + _id: new ObjectId(workflowId), + projectId, + }); + if (!workflow) { + throw new Error('Workflow not found'); + } + + // create a new workflow with the same content + const newWorkflow = { + ...workflow, + _id: new ObjectId(), + name: `Copy of ${workflow.name || 'Unnamed workflow'}`, + createdAt: new Date().toISOString(), + lastUpdatedAt: new Date().toISOString(), + }; + const { insertedId } = await agentWorkflowsCollection.insertOne(newWorkflow); + const { _id, ...rest } = newWorkflow as WithId>; + return { + ...rest, + _id: insertedId.toString(), + }; +} + +export async function renameWorkflow(projectId: string, workflowId: string, name: string) { + await projectAuthCheck(projectId); + + await agentWorkflowsCollection.updateOne({ + _id: new ObjectId(workflowId), + projectId, + }, { + $set: { + name, + lastUpdatedAt: new Date().toISOString(), + }, + }); +} + +export async function saveWorkflow(projectId: string, workflowId: string, workflow: z.infer) { + await projectAuthCheck(projectId); + + // check if workflow exists + const existingWorkflow = await agentWorkflowsCollection.findOne({ + _id: new ObjectId(workflowId), + projectId, + }); + if (!existingWorkflow) { + throw new Error('Workflow not found'); + } + + // ensure that this is not the published workflow for this project + const publishedWorkflowId = await fetchPublishedWorkflowId(projectId); + if (publishedWorkflowId && publishedWorkflowId === workflowId) { + throw new Error('Cannot save published workflow'); + } + + // update the workflow, except name and description + const { _id, name, ...rest } = workflow as WithId>; + await agentWorkflowsCollection.updateOne({ + _id: new ObjectId(workflowId), + }, { + $set: { + ...rest, + lastUpdatedAt: new Date().toISOString(), + }, + }); +} + +export async function publishWorkflow(projectId: string, workflowId: string) { + await projectAuthCheck(projectId); + + // check if workflow exists + const existingWorkflow = await agentWorkflowsCollection.findOne({ + _id: new ObjectId(workflowId), + projectId, + }); + if (!existingWorkflow) { + throw new Error('Workflow not found'); + } + + // publish the workflow + await projectsCollection.updateOne({ + "_id": projectId, + }, { + $set: { + publishedWorkflowId: workflowId, + } + }); +} + +export async function fetchPublishedWorkflowId(projectId: string): Promise { + await projectAuthCheck(projectId); + const project = await projectsCollection.findOne({ + _id: projectId, + }); + return project?.publishedWorkflowId || null; +} + +export async function fetchWorkflow(projectId: string, workflowId: string): Promise>> { + await projectAuthCheck(projectId); + + // fetch workflow + const workflow = await agentWorkflowsCollection.findOne({ + _id: new ObjectId(workflowId), + projectId, + }); + if (!workflow) { + throw new Error('Workflow not found'); + } + const { _id, ...rest } = workflow; + return { + ...rest, + _id: _id.toString(), + }; +} + +export async function listWorkflows( + projectId: string, + page: number = 1, + limit: number = 10 +): Promise<{ + workflows: (WithStringId>)[]; + total: number; + publishedWorkflowId: string | null; +}> { + await projectAuthCheck(projectId); + + // fetch total count + const total = await agentWorkflowsCollection.countDocuments({ projectId }); + + // fetch published workflow + let publishedWorkflowId: string | null = null; + let publishedWorkflow: WithId> | null = null; + if (page === 1) { + publishedWorkflowId = await fetchPublishedWorkflowId(projectId); + if (publishedWorkflowId) { + publishedWorkflow = await agentWorkflowsCollection.findOne({ + _id: new ObjectId(publishedWorkflowId), + projectId, + }, { + projection: { + _id: 1, + name: 1, + description: 1, + createdAt: 1, + lastUpdatedAt: 1, + }, + }); + } + } + + // fetch workflows with pagination + let workflows: WithId>[] = await agentWorkflowsCollection.find( + { + projectId, + ...(publishedWorkflowId ? { + _id: { + $ne: new ObjectId(publishedWorkflowId) + } + } : {}), + }, + { + sort: { lastUpdatedAt: -1 }, + projection: { + _id: 1, + name: 1, + description: 1, + createdAt: 1, + lastUpdatedAt: 1, + }, + skip: (page - 1) * limit, + limit: limit, + } + ).toArray(); + workflows = [ + ...(publishedWorkflow ? [publishedWorkflow] : []), + ...workflows, + ]; + + // return workflows + return { + workflows: workflows.map((w) => { + const { _id, ...rest } = w; + return { + ...rest, + _id: _id.toString(), + }; + }), + total, + publishedWorkflowId, + }; +} + +export async function scrapeWebpage(url: string): Promise> { + const page = await webpagesCollection.findOne({ + "_id": url, + lastUpdatedAt: { + '$gte': new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(), // 24 hours + }, + }); + if (page) { + // console.log("found webpage in db", url); + return { + title: page.title, + content: page.contentSimple, + }; + } + + // otherwise use firecrawl + const scrapeResult = await crawler.scrapeUrl( + url, + { + formats: ['markdown'], + onlyMainContent: true + } + ) as ScrapeResponse; + + // save the webpage using upsert + await webpagesCollection.updateOne( + { _id: url }, + { + $set: { + title: scrapeResult.metadata?.title || '', + contentSimple: scrapeResult.markdown || '', + lastUpdatedAt: (new Date()).toISOString(), + } + }, + { upsert: true } + ); + + // console.log("crawled webpage", url); + return { + title: scrapeResult.metadata?.title || '', + content: scrapeResult.markdown || '', + }; +} + +export async function createProject(formData: FormData) { + const user = await authCheck(); + const name = formData.get('name') as string; + const projectId = crypto.randomUUID(); + const chatClientId = crypto.randomBytes(16).toString('base64url'); + const secret = crypto.randomBytes(32).toString('hex'); + await projectsCollection.insertOne({ + _id: projectId, + name: name, + createdAt: (new Date()).toISOString(), + lastUpdatedAt: (new Date()).toISOString(), + createdByUserId: user.sub, + chatClientId, + secret, + }); + // add user to project + await projectMembersCollection.insertOne({ + userId: user.sub, + projectId: projectId, + createdAt: (new Date()).toISOString(), + lastUpdatedAt: (new Date()).toISOString(), + }); + redirect(`/projects/${projectId}/workflow`); +} + +export async function getProjectConfig(projectId: string): Promise> { + await projectAuthCheck(projectId); + const project = await projectsCollection.findOne({ + _id: projectId, + }); + if (!project) { + throw new Error('Project config not found'); + } + return project; +} + +export async function listProjects(): Promise[]> { + const user = await authCheck(); + const memberships = await projectMembersCollection.find({ + userId: user.sub, + }).toArray(); + const projectIds = memberships.map((m) => m.projectId); + const projects = await projectsCollection.find({ + _id: { $in: projectIds }, + }).toArray(); + return projects; +} + +export async function listSources(projectId: string): Promise>[]> { + await projectAuthCheck(projectId); + const sources = await dataSourcesCollection.find({ + projectId: projectId, + }).toArray(); + return sources.map((s) => ({ + ...s, + _id: s._id.toString(), + })); +} + +export async function createCrawlDataSource(projectId: string, formData: FormData) { + await projectAuthCheck(projectId); + const url = formData.get('url') as string; + const name = formData.get('name') as string; + const limit = Number(formData.get('limit')); + + const result = await dataSourcesCollection.insertOne({ + projectId: projectId, + active: true, + name: name, + createdAt: (new Date()).toISOString(), + status: "new", + data: { + type: 'crawl', + startUrl: url, + limit: limit, + } + }); + + redirect(`/projects/${projectId}/sources/${result.insertedId}`); +} + +export async function createUrlsDataSource(projectId: string, formData: FormData) { + await projectAuthCheck(projectId); + const urls = formData.get('urls') as string; + // take first 100 urls + const limitedUrls = urls.split('\n').slice(0, 100).map((url) => url.trim()); + const name = formData.get('name') as string; + + const result = await dataSourcesCollection.insertOne({ + projectId: projectId, + active: true, + name: name, + createdAt: (new Date()).toISOString(), + status: "new", + data: { + type: 'urls', + urls: limitedUrls, + } + }); + + redirect(`/projects/${projectId}/sources/${result.insertedId}`); +} + +export async function recrawlWebDataSource(projectId: string, sourceId: string) { + await projectAuthCheck(projectId); + + const source = await dataSourcesCollection.findOne({ + "_id": new ObjectId(sourceId), + "projectId": projectId, + }); + if (!source) { + throw new Error('Data source not found'); + } + + await dataSourcesCollection.updateOne({ + "_id": new ObjectId(sourceId), + }, { + $set: { + "status": "new", + "attempts": 0, + }, + $unset: { + 'data.firecrawlId': '', + 'data.crawledUrls': '', + 'data.scrapedUrls': '', + } + }); + + revalidatePath(`/projects/${projectId}/sources/${sourceId}`); +} + +export async function deleteDataSource(projectId: string, sourceId: string) { + await projectAuthCheck(projectId); + + await dataSourcesCollection.deleteOne({ + _id: new ObjectId(sourceId), + }); + + await embeddingsCollection.deleteMany({ + sourceId: sourceId, + }); + + redirect(`/projects/${projectId}/sources`); +} + +export async function getAssistantResponse( + projectId: string, + request: z.infer, +): Promise<{ + messages: z.infer[], + state: unknown, + rawAPIResponse: unknown, +}> { + 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, + }; +} + +export async function getCopilotResponse( + projectId: string, + messages: z.infer[], + current_workflow_config: z.infer, + context: z.infer | null, +): Promise> { + await projectAuthCheck(projectId); + + // prepare request + const request: z.infer = { + messages: messages.map(convertToCopilotApiMessage), + workflow_schema: JSON.stringify(zodToJsonSchema(CopilotWorkflow)), + current_workflow_config: JSON.stringify(convertToCopilotWorkflow(current_workflow_config)), + context: context ? convertToCopilotApiChatContext(context) : null, + }; + console.log(`copilot request`, JSON.stringify(request, null, 2)); + + // call copilot api + const response = await fetch(process.env.COPILOT_API_URL + '/chat', { + method: 'POST', + body: JSON.stringify(request), + headers: { + 'Content-Type': 'application/json', + }, + }); + if (!response.ok) { + console.error('Failed to call copilot api', response); + throw new Error(`Failed to call copilot api: ${response.statusText}`); + } + + // parse and return response + const json: z.infer = await response.json(); + console.log(`copilot response`, JSON.stringify(json, null, 2)); + if ('error' in json) { + throw new Error(`Failed to call copilot api: ${json.error}`); + } + // remove leading ```json and trailing ``` + const msg = convertToCopilotMessage({ + role: 'assistant', + content: json.response.replace(/^```json\n/, '').replace(/\n```$/, ''), + }); + return msg as z.infer; +} + +export async function suggestToolResponse(toolId: string, projectId: string, messages: z.infer[]): Promise { + await projectAuthCheck(projectId); + + const prompt = ` +# Your Specific Task: +Here is a chat between a user and a customer support assistant. +The assistant has requested a tool call with ID {{toolID}}. +Your job is to come up with an example of the data that the tool call should return. +The current date is {{date}}. + +CONVERSATION: +{{messages}} +` + .replace('{{toolID}}', toolId) + .replace(`{{date}}`, new Date().toISOString()) + .replace('{{messages}}', JSON.stringify(messages.map((m) => { + let tool_calls; + if ('tool_calls' in m && m.role == 'assistant') { + tool_calls = m.tool_calls; + } + let { role, content } = m; + return { + role, + content, + tool_calls, + } + }))); + // console.log(prompt); + + const { object } = await generateObject({ + model: openai("gpt-4o"), + prompt: prompt, + schema: z.object({ + result: z.any(), + }), + }); + + return JSON.stringify(object); +} + +export async function getDataSource(projectId: string, sourceId: string): Promise> { + await projectAuthCheck(projectId); + + const source = await dataSourcesCollection.findOne({ + "_id": new ObjectId(sourceId), + "projectId": projectId, + }); + if (!source) { + throw new Error('Data source not found'); + } + // send source without _id + const { _id, ...sourceData } = source; + return sourceData +} + +export async function getUpdatedSourceStatus(projectId: string, sourceId: string) { + await projectAuthCheck(projectId); + + const source = await dataSourcesCollection.findOne({ + "_id": new ObjectId(sourceId), + "projectId": projectId, + }, { + projection: { + status: 1, + } + }); + if (!source) { + throw new Error('Data source not found'); + } + return source.status; +} + +export async function getInformationTool( + projectId: string, + query: string, + sourceIds: string[], + returnType: z.infer['ragReturnType'], + k: number, +): Promise> { + await projectAuthCheck(projectId); + + // create embedding for question + const embedResult = await embed({ + model: embeddingModel, + value: query, + }); + + // fetch all data sources for this project + const sources = await dataSourcesCollection.find({ + projectId: projectId, + active: true, + }).toArray(); + const validSourceIds = sources + .filter(s => sourceIds.includes(s._id.toString())) // id should be in sourceIds + .filter(s => s.active) // should be active + .map(s => s._id.toString()); + + // if no sources found, return empty response + if (validSourceIds.length === 0) { + return { + results: [], + }; + } + + // perform vector search on mongodb for similar documents + // from the sources fetched above + const agg = [ + { + '$vectorSearch': { + 'index': 'vector_index', + 'path': 'embeddings', + 'filter': { + 'sourceId': { + '$in': validSourceIds, + } + }, + 'queryVector': embedResult.embedding, + 'numCandidates': 5000, + 'limit': k, + } + }, { + '$project': { + '_id': 0, + 'content': 1, + 'metadata.sourceURL': 1, + 'metadata.title': 1, + 'score': { + '$meta': 'vectorSearchScore' + } + } + } + ]; + + // run pipeline + const embeddingMatches = await embeddingsCollection.aggregate>(agg).toArray(); + + // if return type is chunks, return the chunks + if (returnType === 'chunks') { + return { + results: embeddingMatches.map(m => ({ + title: m.metadata.title, + content: m.content, + url: m.metadata.sourceURL, + score: m.metadata.score, + })), + }; + } + + // else return the content of the webpages + const result: z.infer = { + results: [], + }; + + // coalesce results by url + const seenUrls = new Set(); + for (const match of embeddingMatches) { + if (seenUrls.has(match.metadata.sourceURL)) { + continue; + } + seenUrls.add(match.metadata.sourceURL); + result.results.push({ + title: match.metadata.title, + content: match.content, + url: match.metadata.sourceURL, + score: match.metadata.score, + }); + } + + // now fetch each webpage content and overwrite + for (const res of result.results) { + try { + const page = await webpagesCollection.findOne({ + "_id": res.url, + }); + if (!page) { + continue; + } + res.content = page.contentSimple; + } catch (e) { + // console.error('error fetching page:', e); + } + } + + return result; +} + +export async function toggleDataSource(projectId: string, sourceId: string, active: boolean) { + await projectAuthCheck(projectId); + + await dataSourcesCollection.updateOne({ + "_id": new ObjectId(sourceId), + "projectId": projectId, + }, { + $set: { + "active": active, + } + }); +} + +export async function getScenarios(projectId: string): Promise>[]> { + await projectAuthCheck(projectId); + + const scenarios = await scenariosCollection.find({ projectId }).toArray(); + return scenarios.map(s => ({ ...s, _id: s._id.toString() })); +} + +export async function createScenario(projectId: string, name: string, description: string): Promise { + await projectAuthCheck(projectId); + + const now = new Date().toISOString(); + const result = await scenariosCollection.insertOne({ + projectId, + name, + description, + lastUpdatedAt: now, + createdAt: now, + }); + return result.insertedId.toString(); +} + +export async function updateScenario(projectId: string, scenarioId: string, name: string, description: string) { + await projectAuthCheck(projectId); + + await scenariosCollection.updateOne({ + "_id": new ObjectId(scenarioId), + "projectId": projectId, + }, { + $set: { + name, + description, + lastUpdatedAt: new Date().toISOString(), + } + }); +} + +export async function deleteScenario(projectId: string, scenarioId: string) { + await projectAuthCheck(projectId); + + await scenariosCollection.deleteOne({ + "_id": new ObjectId(scenarioId), + "projectId": projectId, + }); +} + +export async function simulateUserResponse( + projectId: string, + messages: z.infer[], + simulationData: z.infer +): Promise { + await projectAuthCheck(projectId); + const articlePrompt = ` +# Your Specific Task: + +## Context: + +Here is a help article: + +Content: + +Title: {{title}} +{{content}} + + +## Task definition: + +Pretend to be a user reaching out to customer support. Chat with the +customer support assistant, assuming your issue or query is from this article. +Ask follow-up questions and make it real-world like. Don't do dummy +conversations. Your conversation should be a maximum of 5 user turns. + +As output, simply provide your (user) turn of conversation. + +After you are done with the chat, keep replying with a single word EXIT +in all capitals. +`; + + const scenarioPrompt = ` +# Your Specific Task: + +## Context: + +Here is a scenario: + +Scenario: + +{{scenario}} + + +## Task definition: + +Pretend to be a user reaching out to customer support. Chat with the +customer support assistant, assuming your issue is based on this scenario. +Ask follow-up questions and make it real-world like. Don't do dummy +conversations. Your conversation should be a maximum of 5 user turns. + +As output, simply provide your (user) turn of conversation. + +After you are done with the chat, keep replying with a single word EXIT +in all capitals. +`; + + const previousChatPrompt = ` +# Your Specific Task: + +## Context: + +Here is a chat between a user and a customer support assistant: + +Chat: + +{{messages}} + + +## Task definition: + +Pretend to be a user reaching out to customer support. Chat with the +customer support assistant, assuming your issue based on this previous chat. +Ask follow-up questions and make it real-world like. Don't do dummy +conversations. Your conversation should be a maximum of 5 user turns. + +As output, simply provide your (user) turn of conversation. + +After you are done with the chat, keep replying with a single word EXIT +in all capitals. +`; + await projectAuthCheck(projectId); + + // flip message assistant / user message + // roles from chat messages + // use only text response messages + const flippedMessages: { role: 'user' | 'assistant', content: string }[] = messages + .filter(m => m.role == 'assistant' || m.role == 'user') + .map(m => ({ + role: m.role == 'assistant' ? 'user' : 'assistant', + content: m.content || '', + })); + + // simulate user call + let prompt; + if ('articleUrl' in simulationData) { + prompt = articlePrompt + .replace('{{title}}', simulationData.articleTitle || '') + .replace('{{content}}', simulationData.articleContent || ''); + } + if ('scenario' in simulationData) { + prompt = scenarioPrompt + .replace('{{scenario}}', simulationData.scenario); + } + if ('chatMessages' in simulationData) { + prompt = previousChatPrompt + .replace('{{messages}}', simulationData.chatMessages); + } + const { text } = await generateText({ + model: openai("gpt-4o"), + system: prompt || '', + messages: flippedMessages, + }); + + return text.replace(/\. EXIT$/, '.'); +} + +export async function rotateSecret(projectId: string): Promise { + await projectAuthCheck(projectId); + const secret = crypto.randomBytes(32).toString('hex'); + await projectsCollection.updateOne( + { _id: projectId }, + { $set: { secret } } + ); + return secret; +} + +export async function updateWebhookUrl(projectId: string, url: string) { + await projectAuthCheck(projectId); + await projectsCollection.updateOne( + { _id: projectId }, + { $set: { webhookUrl: url } } + ); +} + +export async function executeClientTool( + toolCall: z.infer['tool_calls'][number], + projectId: string, +): 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; +} \ No newline at end of file diff --git a/apps/rowboat/app/android-chrome-192x192.png b/apps/rowboat/app/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..4e31e0801d4635681bb9beec438dfef057f5f511 GIT binary patch literal 2566 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 literal 0 HcmV?d00001 diff --git a/apps/rowboat/app/android-chrome-512x512.png b/apps/rowboat/app/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..bb22e44571755ec68b25b33b210154412fae94fd GIT binary patch literal 5625 zcmbuDRaDeb*T(;}fP@Z;G?F3+2qLKQERPEcIz>mQ+^0Vm@3KUSV2MV^`DkrKt_l))m$@lr}V+K0FDZ zoGhJ0H=&Wa$Y6@#IF7hX$;^_sC2e2Z`V;!c3dd$^XIFbycmC}B+xmBPd3Ak#y)&~D zKok(o6b;vdH@G(R#`Laquk6k5U7TD55C*_lVL7rnRhCu11AiZ`9&S%>$3BcLR4uG` zsUJ)poNt)lo!!M@aobbdA-6-)#nLOyDm#KZ{*L{{o#Vcdd`sd_YKOLOO>W_@@KFp= zg=&Q@J}r10ersY2N&t=LjxW_O?F{XltDif;oJ2E57i$#H)XbdjoURY9htY<$`nS&f zoH^b+Ug=uti|gAM+E{2_2qX>6mdS4RYW}zV?;G*AG|{vs&!+Rk^QrQw^MmuTqOsBJ z(G2m7xw<**F?KLzFoq@Or|r+~h;BR{-|E*I^&qO=xxUJxs`f+e5A7cr&ohE<1tmU8 z47(GC%12?3u#xv8Bkn~sd~R6lTRYu74Zj;6N)vi@akbpJ97G;;@$X`~aypPKut23? zG-ovVNpeqgPnLAn$={RB(akcWG7J)PwtqI9F`USkSYc8zkT?*-9J9Z$k4B&)=_3zU z4#$hf_ZRn5g;VDn=YN0u9nTfN*1wjgkk{zec(i_m@IZ7&c85@fob8>Jzb_95g-6gu z1XBhhz=-r`>AQ2g{qg+?ya@?B37sLGm#3G->cutoH51<_s;#SgVZD8^eMN7I4weq$ zIO7&u7uy5cFV8NUy_-u7N|AZU7T=Z>!IXuTg?y#_jp2=?$4PtWy@Tb0rU_o;5VW4YF; z7K6f+XqMD{tm}#DnJSy=2qY{g=b9KUIImOU83wHRe<{C_^Z;q zGL}6ySs;1h$3(SNb&`r4FqRtPcwn{Jyyu1XgcU6j|LU z*!gb^xvQ#?I{=Wv|GNn+4J3PSAQ@OqLx~JSPIHfkNqFOfGyvSORa1PW>ovQJ#wsM~ zv68xKq&0r|Yp0sb=vAaWWa1#lp;Ra$a&JhTj?U$M_`R8xiifv@jOvV}9Di6h+7$ly z^xRRuF0C$YCR?MC7)@)n91;#hV&C#s76>PvgGd zf3o`Z*x0s5_>7^4Hjp_oHNa(>cLx+>l zlUJ}PE5`U79bsMhQx8E!SWG^{)OP+d{op|B$gLAvzW9fH!JJC|laU|kd{XI@-yLF> ztqbkVOIUo$CRF?XX#M6M34$OcPP~3(F9jU*O4H+CrR4Xjx7~`-pp;;~F?qcjWQiuM z=~`QU=VTg+u&jy(x6Hs%Tu+5OjkybVH$Kj`;e@7v5ltAaF(5B(;HaQ?*0&TMWS)F)Ke zuf)HiNTX0O5SoEyXy9>GwX*?y{w~;ErgD6T$>i|N%xfKU>NTHPMp*46p|sk=9{%_z z#bXpf?~yM!L8kOE#79iZl*n1}tUu$!k?BQNTevl{{3tiRQG6M)`gDsBTrXq4Ff8ad z2b%c^ZfmwpAJyB(QauQBP79@5Co{P&Y6~|;njS}e#bYtT<(Tlj4Ds7)P4}`2#cXwu z<;PI?9BJd_Cv!Rl)UeK?ZkvK{Ltz68`enCK9S`E;7suU|tQBM;m~5mKSYJB`8Lg+Va<|@j zyDHK+(E81=!KXMF`4tlF(V}jy0#?1d?j|M&PH-DQF{pV6A%5?DSp@$}K2NKmLtD`g zM)J$`axRi%1e^3V^dVpg%?47meM)+*`nsMO&ybSmY!c$W&t9|(7w$>bkM-bF7b~6a za~W{GX-PNOmfp*_ea?!JZ)zzL;(v$z?OP^0n5zfuhHpP4%`Jr;nPY<9&T9Q&^sd|`=)pF|cS0%s+JL1a0 z$vts#&T)_s3>ea^dpQKc?oGY|6RSF1RW#O+tdoLC=5 zfSQ+K!X#^TnF24tyS4G7B9{mLPDmrb#@_p*XC)QmS2A@CBiJ4yQs!Gsr=vGfOLK2; zvjCW#I^~Gm;dUmk-bu^-M7%s>QuN1#{b9`B4x1MQd3QjgKWj zEC~Yc6xi8m=-6GbmYdCG!~AIMD@@^9m^2OAP%lbZg`0>C-@~2Xc^5_l0l_1YpTm>= zMLaK3k&Ia>4*b6)ru8!2Q32t#u2P6R_~C>x6t2aG$bmC#75&S_&Tm=If-uOg8HrnH z^?NVEV>A*YhRxbg3Nif1-{kR&ENC?`ZyU*_o+6Age2WhZ=HObxxWn5VAo8KSa`_988mRDj+47pj;-T9?dB11HOUHR=vd|pxyyq+CGmSS>kj6yWW7@OKIcW$4@i2S1|-l#v_FeiD9 zzawFjne~u{`uxHOYamYNbD$hxO0h?w`Oj!^%U}DIL)pKF28G_vjVLF~vteKKR&Yg8 zxq|g>>xt^{*`LK=chu)}v3069moMy^u~W`ZU7EEkAxtdka-zD@94}EF`qwHd=c_?` ze&KV0BO0lIl$JXjXE%yd3l2rtm!SJ4z2>dD^+?Qvfvi8iW?cJJoO|vY&JKwTB#!LxRf9#qC zJ!`5H(6Sv)jT|K{!~P33|8r?7{n{=x|kjt?CRcH zpKJ{uX$}A(uaIy1Z86C0CT27>)w+L(=2aN@OpxU8C#}?3XI+7D-Vc4u669U+!eeRJ z`2fo$SyCH^s;Ot=%9*9dKAF~HprE0UNed&IB+GU=hmbelH@AL!NtU`b$T?tRtQ{+) z4e733nYH3=Z^Rsq^Vs0k6p)=IcAiAM?bDdwIyH7b+F6Lq8u-lo^*FV#o;3Nrnq=j> zKsq3ai5q|T;%R-{Mtaj@*qRYpV}LWE&mZug!>}BZ3Kdv-jNxBDd|(YKu-K>dvmar@ zneKxpdXg=M&9}XaUy6Aob{dD6Dbl1e8ZGZo867X_k!T#AkQ^cz?S@DHK&PUosv%@hPO!&sI2#J=+M8yLrmSzamHvblImbM#5VN zMbisH)$WwLSr8tm-=gP_hpbzqjg{>Cnnk{z0a?)bgwZ9LpAw(5zL1ZA7q`vh=u>i4 zR8iamrtCde78M4MM6aWKk(~vGAL!LG0ohy?mFn_XFIOmhdjGG|!Wgs#lNWaHDIFEjd=F6`Lp=OapRq$&^X``M9se(lDRn-UZsILp3n(;w&k-aF28wXzgffAwRB31QpRop`@-!hsl&7@M2^yFb)!s%7$o}O$(dvs} zeAlFey>IyLn|?^*RTS$jBTDPA3D7;Q-X)I>W+j|C%thnqk3RrKlTp=@1TW=b&>imqGraX(BoVvs*E(^6 zx`J)qrrEiE2u>J`wMi*oB=(jqy!aqgoqf$cj+{*J?={Q;atfSy33!Z0+9(aad3Sj0 z^S0|@2Gqjp?jJ9Hd5&9BHj8`9G-;Hg_Ev+6XYa_Y<)8xS)#EAc}OE z=P{SRTkJA_;OY;#bEo}mKM-bq6HHOO>{rK|dWk2Vr8X+G!vvGNn&!OlMWU$gc&{AB z9H6AYiJM%Xsa4(r#|$p#>XcUIl`{qxaVZ7-#tmi^@N+X1gP%$eDBnbUJ`6(;URdxc zPXSrTp@|ply^FxLk`hK!%3K z_%&Z^HsA@$Bc7{=I{ci`lt(Sz9z)$YKmA{H#&7|6v6}MqyH&*DL>uV4YLp+O0l}EZ zYRQsBx7TC?DSyjsKj&lvWay@vz6!@)6X6*C)M#%qr2=TAUi`Cg(xZKlqJi3&fT%Em zPhKRKw^>ZqwLUnSYqaoZDt0qQiC*mp_vDlea0xyAdG+*ov4@l3g@CW$5P3Vg-R$(! z5N_I4G&iL@LXtpJIYm{ZTZW^`oeH5C|J`Yb2D*ykj#tm=8h{8P;v%-!!g2Ofmo5FZI2yzdc`B;G_w zdARMP2;yaYskib3dAluO=IK9yYZ6eLR*Apj+#+7mr>z|HWq*&CF3#QN?Ej*21)SeC zW$pjs@e8MtSIYVKRIy*ykrg2o|75G;U>LKdihwBhLmtt8^I0r<$J3sKSy4qSA^Wgx zEQ$syV@i!e4LjyHtaX(0Efj>RjU!r!NrmuLl1{bH#CZnGMpZ;JAMA%=&qkG}m8s8# zSBTGP5YOD?P*JK_p;sx&nud4yt{HHo--9zaHK6s#q&QBg1E$<5ALR?+{JtrJIsf`>9btAmw+GM|C~w-WFIsvD}Kk6UUF@r3S_~75O7m%tHRP` zhg3u|k3HTYZlE;1FKO%2fLmO)1P~5p3_RCSB$%Ile0<9<0ZkPHrI{sPkvf)BrR!<>$=>JBsZ>%2P=ypF$GCt zGI&51+z2i=t#Q-k%LWQL-u>qz5MZAeH9!*0+x{4=wD#RUdXd0BpPz&G$;W1Xob(NP zWL|*t=4h~M-H^5@pL{lZZ^YIj9a~|E7dm38vU@Jx*M^`%u(}!g1d_YTBADI&9Fbzp zA1F>MP`@e7fsnopnjRUu04;q0i>>{dy=dxiS%*Pqv57Q5%T@gXalxGjqYpwiPlBFL@Xq=o>@(SpIW*|Ip`k$j3g@FNi7 z54f^4B=Z~-%lv+-;^JInf!SiJQ>(oNg636zZA~D++$R@j)!El3>48b;*U|T6`H`^NlM1)12 zJrS0C@>EP$&9 z0PxCOJ-U7Cv;Lz;18)b1)Yi2DB2G#{Yf2C&=uHA05r|z8`mEE2h_gnQeelmjU)N9< ha~IhlR5katAh1P$2Ye(imUPnsP*c)WtdO_(_8;Ew8EOCk literal 0 HcmV?d00001 diff --git a/apps/rowboat/app/api/auth/[auth0]/route.ts b/apps/rowboat/app/api/auth/[auth0]/route.ts new file mode 100644 index 00000000..aa8b000c --- /dev/null +++ b/apps/rowboat/app/api/auth/[auth0]/route.ts @@ -0,0 +1,3 @@ +import { handleAuth } from '@auth0/nextjs-auth0'; + +export const GET = handleAuth(); \ No newline at end of file diff --git a/apps/rowboat/app/api/v1/chats/[chatId]/close/route.ts b/apps/rowboat/app/api/v1/chats/[chatId]/close/route.ts new file mode 100644 index 00000000..25c7dfba --- /dev/null +++ b/apps/rowboat/app/api/v1/chats/[chatId]/close/route.ts @@ -0,0 +1,40 @@ +import { NextRequest } from "next/server"; +import { apiV1 } from "rowboat-shared"; +import { db } from "@/app/lib/mongodb"; +import { z } from "zod"; +import { ObjectId } from "mongodb"; +import { authCheck } from "../../../utils"; + +const chatsCollection = db.collection>("chats"); + +export async function POST( + request: NextRequest, + { params }: { params: { chatId: string } } +): Promise { + return await authCheck(request, async (session) => { + const { chatId } = params; + + const result = await chatsCollection.findOneAndUpdate( + { + _id: new ObjectId(chatId), + projectId: session.projectId, + userId: session.userId, + closed: { $exists: false }, + }, + { + $set: { + closed: true, + closedAt: new Date().toISOString(), + closeReason: "user-closed-chat", + }, + }, + { returnDocument: 'after' } + ); + + if (!result) { + return Response.json({ error: "Chat not found" }, { status: 404 }); + } + + return Response.json(result); + }); +} diff --git a/apps/rowboat/app/api/v1/chats/[chatId]/messages/route.ts b/apps/rowboat/app/api/v1/chats/[chatId]/messages/route.ts new file mode 100644 index 00000000..696de694 --- /dev/null +++ b/apps/rowboat/app/api/v1/chats/[chatId]/messages/route.ts @@ -0,0 +1,94 @@ +import { NextRequest } from "next/server"; +import { apiV1 } from "rowboat-shared"; +import { db } from "@/app/lib/mongodb"; +import { z } from "zod"; +import { Filter, ObjectId } from "mongodb"; +import { authCheck } from "../../../utils"; + +const chatsCollection = db.collection>("chats"); +const chatMessagesCollection = db.collection>("chatMessages"); + +// list messages +export async function GET( + req: NextRequest, + { params }: { params: { chatId: string } } +): Promise { + return await authCheck(req, async (session) => { + const { chatId } = params; + + // Check if chat exists + const chat = await chatsCollection.findOne({ + _id: new ObjectId(chatId), + projectId: session.projectId, + userId: session.userId + }); + if (!chat) { + return Response.json({ error: "Chat not found" }, { status: 404 }); + } + + // Parse query parameters + const searchParams = req.nextUrl.searchParams; + const limit = 10; // Hardcoded limit + const next = searchParams.get('next'); + const previous = searchParams.get('previous'); + + // Construct the query + const query: Filter> = { + chatId, + $or: [ + { role: 'user' }, + { role: 'assistant', agenticResponseType: { $eq: 'external' } } + ], + }; + + // Add cursor condition to the query + if (previous) { + query._id = { $lt: new ObjectId(previous) }; + } else if (next) { + query._id = { $gt: new ObjectId(next) }; + } + + // Fetch messages from the database + let messages = await chatMessagesCollection + .find(query) + .sort({ _id: previous ? -1 : 1 }) // Sort based on direction + .limit(limit + 1) // Fetch one extra to determine if there are more results + .toArray(); + + // Determine if there are more results + const hasMore = messages.length > limit; + if (hasMore) { + messages.pop(); + } + + // Reverse the array if we're paginating backwards + if (previous) { + messages.reverse(); + } + + let nextCursor: string | undefined; + let previousCursor: string | undefined; + if (messages.length > 0) { + if (hasMore || previous) { + nextCursor = messages[messages.length - 1]._id.toString(); + } + if (next || (previous && hasMore)) { + previousCursor = messages[0]._id.toString(); + } + } + + // Prepare the response + const response: z.infer = { + messages: messages.map(message => ({ + ...message, + id: message._id.toString(), + _id: undefined + })), + next: nextCursor, + previous: previousCursor, + }; + + // Return response + return Response.json(response); + }); +} diff --git a/apps/rowboat/app/api/v1/chats/[chatId]/route.ts b/apps/rowboat/app/api/v1/chats/[chatId]/route.ts new file mode 100644 index 00000000..ae11c95f --- /dev/null +++ b/apps/rowboat/app/api/v1/chats/[chatId]/route.ts @@ -0,0 +1,43 @@ +import { NextRequest } from "next/server"; +import { apiV1 } from "rowboat-shared"; +import { db } from "@/app/lib/mongodb"; +import { z } from "zod"; +import { ObjectId } from "mongodb"; +import { authCheck } from "../../utils"; + +const chatsCollection = db.collection>("chats"); + +// get chat +export async function GET( + req: NextRequest, + { params }: { params: Promise<{ chatId: string }> } +): Promise { + return await authCheck(req, async (session) => { + const { chatId } = await params; + + // fetch the chat from the database + let chatIdObj: ObjectId; + try { + chatIdObj = new ObjectId(chatId); + } catch (e) { + return Response.json({ error: "Invalid chat ID" }, { status: 400 }); + } + + const chat = await chatsCollection.findOne({ + projectId: session.projectId, + userId: session.userId, + _id: chatIdObj + }); + + if (!chat) { + return Response.json({ error: "Chat not found" }, { status: 404 }); + } + + // return the chat + return Response.json({ + ...chat, + id: chat._id.toString(), + _id: undefined, + }); + }); +} diff --git a/apps/rowboat/app/api/v1/chats/[chatId]/turn/route.ts b/apps/rowboat/app/api/v1/chats/[chatId]/turn/route.ts new file mode 100644 index 00000000..80ea9b2d --- /dev/null +++ b/apps/rowboat/app/api/v1/chats/[chatId]/turn/route.ts @@ -0,0 +1,152 @@ +import { NextRequest } from "next/server"; +import { apiV1 } from "rowboat-shared"; +import { agentWorkflowsCollection, db, projectsCollection } from "@/app/lib/mongodb"; +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"; + +const chatsCollection = db.collection>("chats"); +const chatMessagesCollection = db.collection>("chatMessages"); + +// get next turn / agent response +export async function POST( + req: NextRequest, + { params }: { params: Promise<{ chatId: string }> } +): Promise { + return await authCheck(req, async (session) => { + const { chatId } = await params; + + // parse and validate the request body + let body; + try { + body = await req.json(); + } catch (e) { + return Response.json({ error: "Invalid JSON in request body" }, { status: 400 }); + } + const result = apiV1.ApiChatTurnRequest.safeParse(body); + if (!result.success) { + return Response.json({ error: `Invalid request body: ${result.error.message}` }, { status: 400 }); + } + const userMessage: z.infer = { + version: 'v1', + createdAt: new Date().toISOString(), + chatId, + role: 'user', + content: result.data.message, + }; + + // ensure chat exists + const chat = await chatsCollection.findOne({ + projectId: session.projectId, + userId: session.userId, + _id: new ObjectId(chatId) + }); + if (!chat) { + return Response.json({ error: "Chat not found" }, { status: 404 }); + } + + // prepare system message which will contain user data + const systemMessage: z.infer = { + version: 'v1', + createdAt: new Date().toISOString(), + chatId, + role: 'system', + content: `The following user data is available to you: ${JSON.stringify(chat.userData)}`, + }; + + // fetch existing chat messages + const messages = await chatMessagesCollection.find({ chatId: chatId }).toArray(); + + // fetch project settings + const projectSettings = await projectsCollection.findOne({ + "_id": session.projectId, + }); + if (!projectSettings) { + throw new Error("Project settings not found"); + } + + // fetch workflow + const workflow = await agentWorkflowsCollection.findOne({ + projectId: session.projectId, + _id: new ObjectId(projectSettings.publishedWorkflowId), + }); + if (!workflow) { + throw new Error("Workflow not found"); + } + + // get assistant response + const { agents, tools, prompts, startAgent } = convertWorkflowToAgenticAPI(workflow); + const unsavedMessages: z.infer[] = [userMessage]; + let resolvingToolCalls = true; + let state: unknown = chat.agenticState ?? {last_agent_name: startAgent}; + while (resolvingToolCalls) { + const request: z.infer = { + messages: convertToAgenticAPIChatMessages([systemMessage, ...messages, ...unsavedMessages]), + state, + agents, + tools, + prompts, + startAgent, + }; + console.log("turn: sending agentic request", JSON.stringify(request, null, 2)); + const response = await getAssistantResponse(session.projectId, request); + state = response.state; + if (response.messages.length === 0) { + throw new Error("No messages returned from assistant"); + } + unsavedMessages.push(...response.messages.map(m => ({ + ...m, + version: 'v1' as const, + chatId, + createdAt: new Date().toISOString(), + }))); + + // if the last messages is tool call, execute them + const lastMessage = response.messages[response.messages.length - 1]; + if (lastMessage.role === 'assistant' && 'tool_calls' in lastMessage) { + // execute tool calls + console.log("Executing tool calls", lastMessage.tool_calls); + const toolCallResults = await Promise.all(lastMessage.tool_calls.map(async toolCall => { + console.log('executing tool call', toolCall); + try { + return await executeClientTool(toolCall, session.projectId); + } catch (error) { + console.error(`Error executing tool call ${toolCall.id}:`, error); + return { error: "Tool execution failed" }; + } + })); + unsavedMessages.push(...toolCallResults.map((result, index) => ({ + version: 'v1' as const, + chatId, + createdAt: new Date().toISOString(), + role: 'tool' as const, + tool_call_id: lastMessage.tool_calls[index].id, + tool_name: lastMessage.tool_calls[index].function.name, + content: JSON.stringify(result), + }))); + } else { + // ensure that the last message is from an assistant + // and is of an external type + if (lastMessage.role !== 'assistant' || lastMessage.agenticResponseType !== 'external') { + throw new Error("Last message is not from an assistant and is not of an external type"); + } + resolvingToolCalls = false; + break; + } + } + + // save unsaved messages and update chat state + await chatMessagesCollection.insertMany(unsavedMessages); + await chatsCollection.updateOne({ _id: new ObjectId(chatId) }, { $set: { agenticState: state } }); + + // send back the last message + const lastMessage = unsavedMessages[unsavedMessages.length - 1] as WithId>; + return Response.json({ + ...lastMessage, + id: lastMessage._id.toString(), + _id: undefined, + }); + }); +} diff --git a/apps/rowboat/app/api/v1/chats/route.ts b/apps/rowboat/app/api/v1/chats/route.ts new file mode 100644 index 00000000..8990974f --- /dev/null +++ b/apps/rowboat/app/api/v1/chats/route.ts @@ -0,0 +1,116 @@ +import { NextRequest } from "next/server"; +import { db } from "@/app/lib/mongodb"; +import { z } from "zod"; +import { ObjectId } from "mongodb"; +import { apiV1 } from "rowboat-shared"; +import { authCheck } from "../utils"; + +const chatsCollection = db.collection>("chats"); + +// create a chat +export async function POST( + req: NextRequest, +): Promise { + return await authCheck(req, async (session) => { + // parse and validate the request body + let body; + try { + body = await req.json(); + } catch (e) { + return Response.json({ error: "Invalid JSON in request body" }, { status: 400 }); + } + const result = apiV1.ApiCreateChatRequest.safeParse(body); + if (!result.success) { + return new Response(JSON.stringify({ error: `Invalid request body: ${result.error.message}` }), { status: 400 }); + } + + // insert the chat into the database + const id = new ObjectId(); + const chat: z.infer = { + version: "v1", + projectId: session.projectId, + userId: session.userId, + createdAt: new Date().toISOString(), + userData: { + userId: session.userId, + userName: session.userName, + }, + } + await chatsCollection.insertOne({ + ...chat, + _id: id, + }); + + // return response + const response: z.infer = { + ...chat, + id: id.toString(), + }; + return Response.json(response); + }); +} + +// list chats +export async function GET( + req: NextRequest, +): Promise { + return await authCheck(req, async (session) => { + // Parse query parameters + const searchParams = req.nextUrl.searchParams; + const limit = 10; // Hardcoded limit + const next = searchParams.get('next'); + const previous = searchParams.get('previous'); + + // Add userId to query to only show chats for current user + const query: { projectId: string; userId: string; _id?: { $lt?: ObjectId; $gt?: ObjectId } } = { + projectId: session.projectId, + userId: session.userId + }; + + // Add cursor condition to the query + if (next) { + query._id = { $lt: new ObjectId(next) }; + } else if (previous) { + query._id = { $gt: new ObjectId(previous) }; + } + + // Fetch chats from the database + let chats = await chatsCollection + .find(query) + .sort({ _id: -1 }) // Sort in descending order + .limit(limit + 1) // Fetch one extra to determine if there are more results + .toArray(); + + // Determine if there are more results + const hasMore = chats.length > limit; + if (hasMore) { + chats.pop(); + } + let nextCursor: string | undefined; + let previousCursor: string | undefined; + if (chats.length > 0) { + if (hasMore || previous) { + nextCursor = chats[chats.length - 1]._id.toString(); + } + if (next || (previous && hasMore)) { + previousCursor = chats[0]._id.toString(); + } + } + + // Prepare the response + const response: z.infer = { + chats: chats + .slice(0, limit) + .map(chat => ({ + ...chat, + id: chat._id.toString(), + _id: undefined + })), + next: nextCursor, + previous: previousCursor, + }; + + // Return response + return Response.json(response); + }); +} diff --git a/apps/rowboat/app/api/v1/session/guest/route.ts b/apps/rowboat/app/api/v1/session/guest/route.ts new file mode 100644 index 00000000..c660b8ee --- /dev/null +++ b/apps/rowboat/app/api/v1/session/guest/route.ts @@ -0,0 +1,30 @@ +import { NextRequest } from "next/server"; +import { clientIdCheck } from "../../utils"; +import { SignJWT } from "jose"; +import { z } from "zod"; +import { Session } from "../../utils"; +import { apiV1 } from "rowboat-shared"; + +export async function POST(req: NextRequest): Promise { + return await clientIdCheck(req, async (projectId) => { + // create a new guest user + const session: z.infer = { + userId: `guest-${crypto.randomUUID()}`, + userName: 'Guest User', + projectId: projectId + }; + + // Create and sign JWT + const token = await new SignJWT(session) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime('24h') + .sign(new TextEncoder().encode(process.env.CHAT_WIDGET_SESSION_JWT_SECRET)); + + const response: z.infer = { + sessionId: token, + }; + + return Response.json(response); + }); +} diff --git a/apps/rowboat/app/api/v1/session/user/route.ts b/apps/rowboat/app/api/v1/session/user/route.ts new file mode 100644 index 00000000..e747bcf2 --- /dev/null +++ b/apps/rowboat/app/api/v1/session/user/route.ts @@ -0,0 +1,55 @@ +import { NextRequest } from "next/server"; +import { clientIdCheck } from "../../utils"; +import { SignJWT, jwtVerify } from "jose"; +import { z } from "zod"; +import { Session } from "../../utils"; +import { apiV1 } from "rowboat-shared"; +import { projectsCollection } from "@/app/lib/mongodb"; + +export async function POST(req: NextRequest): Promise { + return await clientIdCheck(req, async (projectId) => { + // decode and validate JWT + const json = await req.json(); + const parsedRequest = apiV1.ApiCreateUserSessionRequest.parse(json); + + // fetch client signing key from db + const project = await projectsCollection.findOne({ + _id: projectId + }); + if (!project) { + return Response.json({ error: 'Project not found' }, { status: 404 }); + } + const clientSigningKey = project.secret; + + // verify client signing key + let verified; + try { + verified = await jwtVerify<{ + userId: string; + userName?: string; + }>(parsedRequest.userDataJwt, new TextEncoder().encode(clientSigningKey)); + } catch (e) { + return Response.json({ error: 'Invalid jwt' }, { status: 403 }); + } + + // create new user session + const session: z.infer = { + userId: verified.payload.userId, + userName: verified.payload.userName ?? 'Unknown', + projectId: projectId + }; + + // Create and sign JWT + const token = await new SignJWT(session) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime('24h') + .sign(new TextEncoder().encode(process.env.CHAT_WIDGET_SESSION_JWT_SECRET)); + + const response: z.infer = { + sessionId: token, + }; + + return Response.json(response); + }); +} diff --git a/apps/rowboat/app/api/v1/utils.ts b/apps/rowboat/app/api/v1/utils.ts new file mode 100644 index 00000000..a198135d --- /dev/null +++ b/apps/rowboat/app/api/v1/utils.ts @@ -0,0 +1,62 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; +import { jwtVerify } from "jose"; +import { projectsCollection } from "@/app/lib/mongodb"; + +export const Session = z.object({ + userId: z.string(), + userName: z.string(), + projectId: z.string(), +}); + +/* + This function wraps an API handler with client ID validation. + It checks for a client ID in the request headers and returns a 400 + Bad Request response if missing. It then looks up the client ID in the + database to fetch the corresponding project ID. If no record is found, + it returns a 403 Forbidden response. Otherwise, it sets the project ID + in the request headers and calls the provided handler function. +*/ +export async function clientIdCheck(req: NextRequest, handler: (projectId: string) => Promise): Promise { + const clientId = req.headers.get('x-client-id')?.trim(); + if (!clientId) { + return Response.json({ error: "Missing client ID in request" }, { status: 400 }); + } + const project = await projectsCollection.findOne({ + chatClientId: clientId + }); + if (!project) { + return Response.json({ error: "Invalid client ID" }, { status: 403 }); + } + // set the project id in the request headers + req.headers.set('x-project-id', project._id); + return await handler(project._id); +} + +/* + This function wraps an API handler with session validation. + It checks for a session in the request headers and returns a 400 + Bad Request response if missing. It then verifies the session JWT. + If no record is found, it returns a 403 Forbidden response. Otherwise, + it sets the project ID and user ID in the request headers and calls the + provided handler function. +*/ +export async function authCheck(req: NextRequest, handler: (session: z.infer) => Promise): Promise { + const authHeader = req.headers.get('Authorization'); + if (!authHeader?.startsWith('Bearer ')) { + return Response.json({ error: "Authorization header must be a Bearer token" }, { status: 400 }); + } + const token = authHeader.split(' ')[1]; + if (!token) { + return Response.json({ error: "Missing session token in request" }, { status: 400 }); + } + + let session; + try { + session = await jwtVerify(token, new TextEncoder().encode(process.env.CHAT_WIDGET_SESSION_JWT_SECRET)); + } catch (error) { + return Response.json({ error: "Invalid session token" }, { status: 403 }); + } + + return await handler(session.payload as z.infer); +} diff --git a/apps/rowboat/app/app.tsx b/apps/rowboat/app/app.tsx new file mode 100644 index 00000000..48b68f82 --- /dev/null +++ b/apps/rowboat/app/app.tsx @@ -0,0 +1,64 @@ +'use client'; +import { TypewriterEffect } from "./lib/components/typewriter"; +import Image from 'next/image'; +import logo from "@/public/rowboat-logo.png"; +import { useUser } from "@auth0/nextjs-auth0/client"; +import { useRouter } from "next/navigation"; +import { Spinner } from "@nextui-org/react"; + +export function App() { + const router = useRouter(); + const { user, error, isLoading } = useUser(); + + if (user) { + router.push("/projects"); + } + + return
+
+
+ RowBoat Logo +
+

+ AI agents for human-like customer assistance +

+

+ Set up a personalized agent for your app or website in minutes. +

+
+
+ +
+
+ RowBoat Logo + {isLoading && } + {error &&
{error.message}
} + {!isLoading && !error && !user && ( + + Sign in to get started + + )} + {user &&
+ +
Welcome, {user.name}
+
} +
+
© 2024 RowBoat Labs
+ Terms and Conditions + Privacy Policy +
+
+
; +} diff --git a/apps/rowboat/app/apple-touch-icon.png b/apps/rowboat/app/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..00c5fa3fd1b64e6a2f3217b583c7e2703d95b438 GIT binary patch literal 2456 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 PSr&k<_7kl#4I9+o9);OS literal 0 HcmV?d00001 diff --git a/apps/rowboat/app/browserconfig.xml b/apps/rowboat/app/browserconfig.xml new file mode 100644 index 00000000..b3930d0f --- /dev/null +++ b/apps/rowboat/app/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #da532c + + + diff --git a/apps/rowboat/app/favicon-16x16.png b/apps/rowboat/app/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..9861db05e1dbc26d68e819d0f589408a99d1a2fb GIT binary patch literal 630 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl<6e(pbstU$g(vPY0F z14ES>14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>l{+gt#^^G&C?a%(0xa zuV&xvZMR=vfBogfmnN2`F43+74F}%efB*mg|8H-;bqI7E>pk}S+wVt59yKyIZpqy8 z_V(Kw8*f}+fBoOze+Qcn&b6L9%VgGcz3I=-Kc8bUr=73;^|jaAa<|>vbFY=Vb$j0S zX7=VMC!aL2HZ68rywG_eyFx|`&<%`9-tI2k^4E_X0CG4BJR*yMv?&NP$~<=B0W#Q2 zJbhi+UvTn^s)_l9i?soT;yhg(LnJOICn!i67#VKfuxaDr9Wrxb?l>G^e){Rt$KvKj zh8fx#DmqG9Y+?fJ+#ViWMg|_9%`7Psc&4&;omd#5ta?nhKWigXztXCP4k4}hNLSHN z2VdEN&PEETD7-2pq9iD>T%n*SKP@vSRiUJ^AXOo=pd=X>i*p`-;^8O^)6h8O zfBKB)(;xZee9%@5v&}!U`@8CWlj)l{bedoW618#E~;cWR9?(Ztz&(rN{6} WT(IPmlj&5T6%3xPelF{r5}E)D^y4`I literal 0 HcmV?d00001 diff --git a/apps/rowboat/app/favicon-32x32.png b/apps/rowboat/app/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..9d0624f2bef7db4982d998e8e00edd75d7fefd58 GIT binary patch literal 851 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10;%$IWh-(8w!(_F|Yopd4 z?LK;a{q<*OpS`>L?(_4{-QwMi%#DlP7oVGR?&G76|NsAQ<87N|I_t!Q6TiRzZeVO^ zWNKXIyX?`?N3X8FYGP^Hn7r}L%{SN9TwCO_=;fuCJBxQt*Ps6A$fG$Hb6UAukM$n= z`}^vL>2?-ED&bABe8QUkilN!>Fdh=f|FlVP0TM`tPLo1%+tj&#NzbXi`V&@ z90XV|Z0)xA*2XJ(RIjUR@AqH-=O-&FEI70Id*o=Ln~toDgcyqV(DCY@~pS!mC0eN`ey0 z6$*;-(=u~X6-p`#QWY`_N|G5ED&{=?#KTb-rlE1l|MVHpr$G$N%G`R%+``Jj-jhX` zg%w;HOb(|oD{l@_IDO;Fi6du@$Q)rm-QcmnOON4|xM0aAC)24wD;PXo{an^LB{Ts5 D?KN}E literal 0 HcmV?d00001 diff --git a/apps/rowboat/app/favicon.ico b/apps/rowboat/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..afb1afd407e5598d49ae0bc97262442efc59b5b0 GIT binary patch literal 7406 zcmeI0c~DbV6vjWc0FNXh7$P7LKoA!c7f|#min|82wJy(U#igyww9}dX*LJ3WfD0nH z3|K%YqK*}%IK^$IC@3h77I6U)EzTf=6;Nui$RgNxZx$0EF)7n+nwxptd(QctyvJce z&UXMaSRfz({BDDzVju{BOs2GLfNI`nZ?CX7SOKw8z>jD66om5^$)n3wvC*GqigVtE zJalz-;o*gcD2OaT=JZTNSw*2Tw-U8iYjGs(2+n_X9ufTO$?Ye|UY3oW&O7n!=V!Q) zaRa$)a&hOoJ2!xU}^WV(em2vbzL3Bs)+TRfwEb zIoM{u4fS{GaeVo4#Jj|!e)rBa_C|v*MI%-R5vCC~2p4{SVvNRhNCo1sl0bi4Jz9!+hX#c$(yS;X!xvm+n zU%y7{udUc(y#=X3spxF)MD>r=h%}GHwd8Avca6uR%a2gBtq6zb9mXD?J-E8(D&m~t zP@Y|m)1RD1qAU?rr>ZpmfTp9V0)4LlV_JW<0&X7~nYqr#xP%cw#_>+`=Ngw_x5&z< z1P^YsQT$kL=_X?m;{7eDfzVq@vCPmagGZ#Bw?r;=fVx( zZQtPBtnqJ4a9c-8uU=EM_|@y}H)%N-k+~YB2dQ?L5l)@vD+y6KiYa%| zyF4)<+&h?IJRKHyqj}T28_zK7#f0)-v^lgV&2jyD@XVZvOM4>~zU4i6hFKHlqBCPi zZwZX^Osz6%WRL8XXPA{6_|IT3>mwaJ!z`Q7+wU=5C(kg!7R!6wVX+>bVdOS~G1@6E zZ~IiTlLEA!Yq;JU(4dcH&{~=5HVfa*6GHANaeeOtKRfX)%Om{^bUQg0MV&3 z{+h~PYMpAy8>*$$ske{Z2GOcSmlnqrQ(c-dh1Yxhi|Emsq8c2Ua|kg^4ArJ3i6v-$ z+>F-etw=X(pK4E{IaOLyr89}XObJZE&4V|w*LN?~m=(t> zsLre@u0i{YcB(ZG&Nzt1dyPo&NWk6i@1pG6GO8=nC#U0L%td6)&eGD7nW4Pa=Jh1e zk;OZTsg@)4mUWw?8<;IFd!O-CDgojpK5HxR?HZfQ`E)axT`f3u>yT^H3CKRcWk>0%k0?FtwdnF?6Elzx1 z?iM4)+KHTX#s5bCKmCu3^eOy^o%Sl~K1JD^&}UPg$t*o(Tne3;lOBM~qaWXRjE1`n z^gEi&eMzs97MzBy;;r<1K6zv^or{y%cGj#cocZ(&ookahIhnzeIrjb2_i3*~=GO-% z9iVgihKdH-Pmmcp=@kkh3h3;f%)6i7e~K5syr8pV(mRmZGwC%<%TxhV1^&7UNDW=0 zZlJx3w!?RTOxxkmU)#13OEeuK2nTD}c2+{6E47`?9HmkbZwiHjh{Lg;mxBd}DRfsP l4D@lMrqEVlOQoJ3*2E)L*;1(`W9}sI9`eSa) { + return + + + + {children} + + + + ; +} diff --git a/apps/rowboat/app/lib/components/FormStatusButton.tsx b/apps/rowboat/app/lib/components/FormStatusButton.tsx new file mode 100644 index 00000000..a18441dd --- /dev/null +++ b/apps/rowboat/app/lib/components/FormStatusButton.tsx @@ -0,0 +1,14 @@ +'use client'; + +import { useFormStatus } from "react-dom"; +import { Button, ButtonProps } from "@nextui-org/react"; + +export function FormStatusButton({ + props +}: { + props: ButtonProps; +}) { + const { pending } = useFormStatus(); + + return + + } + + {isEditing ? ( + multiline ? ( + + + } + + ; +} + +function ExpandableContent({ + label, + content, + expanded = false +}: { + label: string, + content: string + expanded?: boolean +}) { + const [isExpanded, setIsExpanded] = useState(expanded); + + function toggleExpanded() { + setIsExpanded(!isExpanded); + } + + return
+
+ {!isExpanded && } + {isExpanded && } +
{label}
+
+ {isExpanded &&
+ {content} +
} +
; +} + +function SystemMessage({ + content, + onChange, + locked +}: { + content: string, + onChange: (content: string) => void, + locked: boolean +}) { + return ( +
+ +
+ ); +} + +export function Messages({ + projectId, + systemMessage, + messages, + toolCallResults, + handleToolCallResults, + loadingAssistantResponse, + loadingUserResponse, + workflow, + onSystemMessageChange, +}: { + projectId: string; + systemMessage: string | undefined; + messages: z.infer[]; + toolCallResults: Record>; + handleToolCallResults: (results: z.infer[]) => void; + loadingAssistantResponse: boolean; + loadingUserResponse: boolean; + workflow: z.infer; + onSystemMessageChange: (message: string) => void; +}) { + const messagesEndRef = useRef(null); + let lastUserMessageTimestamp = 0; + + const systemMessageLocked = messages.length > 0; + + // scroll to bottom on new messages + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }) + }, [messages, loadingAssistantResponse, loadingUserResponse]); + + return
+
+ + {messages.map((message, index) => { + if (message.role === 'assistant') { + if ('tool_calls' in message) { + return ; + } else { + // the assistant message createdAt is an ISO string timestamp + const latency = new Date(message.createdAt).getTime() - lastUserMessageTimestamp; + if (message.agenticResponseType === 'internal') { + return ( + + ); + } else { + return ( + + ); + } + } + } + if (message.role === 'user' && typeof message.content === 'string') { + lastUserMessageTimestamp = new Date(message.createdAt).getTime(); + return ; + } + return <>; + })} + {loadingAssistantResponse && } + {loadingUserResponse && } +
+
+
; +} \ No newline at end of file diff --git a/apps/rowboat/app/projects/[projectId]/playground/scenario-list.tsx b/apps/rowboat/app/projects/[projectId]/playground/scenario-list.tsx new file mode 100644 index 00000000..fe380f10 --- /dev/null +++ b/apps/rowboat/app/projects/[projectId]/playground/scenario-list.tsx @@ -0,0 +1,233 @@ +'use client'; + +import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Input, Spinner, Textarea } from "@nextui-org/react"; +import { useState, useEffect } from "react"; +import { getScenarios, createScenario, updateScenario, deleteScenario } from "@/app/actions"; +import { Scenario, WithStringId } from "@/app/lib/types"; +import { z } from "zod"; +import { EditableField } from "@/app/lib/components/editable-field"; +import { HamburgerIcon } from "@/app/lib/components/icons"; +import { EllipsisVerticalIcon } from "lucide-react"; + +export function AddScenarioForm({ + onAdd, +}: { + onAdd: (name: string, description: string) => void; +}) { + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [error, setError] = useState(null); + const [saving, setSaving] = useState(false); + + const handleAdd = async () => { + try { + setSaving(true); + await onAdd(name, description); + setName(""); + setDescription(""); + setError(null); + } catch (e) { + setError(e instanceof Error ? e.message : "Invalid input"); + } finally { + setSaving(false); + } + }; + + return
+
Add Scenario
+ setName(e.target.value)} + isInvalid={!!error} + required + /> +