diff --git a/apps/rowboat/app/actions/actions.ts b/apps/rowboat/app/actions/actions.ts index 2b5114e5..4b765e85 100644 --- a/apps/rowboat/app/actions/actions.ts +++ b/apps/rowboat/app/actions/actions.ts @@ -58,12 +58,13 @@ export async function scrapeWebpage(url: string): Promise, projectTools: z.infer[], messages: z.infer[], ): Promise<{ streamId: string } | { billingError: string }> { - await projectAuthCheck(workflow.projectId); - if (!await check_query_limit(workflow.projectId)) { + await projectAuthCheck(projectId); + if (!await check_query_limit(projectId)) { throw new QueryLimitError(); } @@ -82,6 +83,6 @@ export async function getAssistantResponseStreamId( return { billingError: error || 'Billing error' }; } - const response = await getAgenticResponseStreamId(workflow, projectTools, messages); + const response = await getAgenticResponseStreamId(projectId, workflow, projectTools, messages); return response; } \ No newline at end of file diff --git a/apps/rowboat/app/actions/mcp_actions.ts b/apps/rowboat/app/actions/mcp_actions.ts index 2ead9471..b495f457 100644 --- a/apps/rowboat/app/actions/mcp_actions.ts +++ b/apps/rowboat/app/actions/mcp_actions.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { WorkflowTool } from "../lib/types/workflow_types"; import { projectAuthCheck } from "./project_actions"; -import { projectsCollection, agentWorkflowsCollection } from "../lib/mongodb"; +import { projectsCollection } from "../lib/mongodb"; import { Project } from "../lib/types/project_types"; import { MCPServer, McpServerTool, convertMcpServerToolToWorkflowTool } from "../lib/types/types"; import { getMcpClient } from "../lib/mcp"; @@ -169,72 +169,6 @@ export async function updateMcpServers(projectId: string, mcpServers: z.infer[]> { - await projectAuthCheck(projectId); - const project = await projectsCollection.findOne({ - _id: projectId, - }); - return project?.mcpServers ?? []; -} - -export async function updateToolInAllWorkflows( - projectId: string, - mcpServer: z.infer, - toolId: string, - shouldAdd: boolean -): Promise { - await projectAuthCheck(projectId); - - // 1. Get all workflows in the project - const workflows = await agentWorkflowsCollection.find({ projectId }).toArray(); - - // 2. For each workflow - for (const workflow of workflows) { - // 3. Find if the tool already exists in this workflow - const existingTool = workflow.tools.find(t => - t.isMcp && - t.mcpServerName === mcpServer.name && - t.name === toolId - ); - - if (shouldAdd && !existingTool) { - // 4a. If adding and tool doesn't exist, add it - const tool = mcpServer.tools.find(t => t.id === toolId); - if (tool) { - const workflowTool = convertMcpServerToolToWorkflowTool( - { - name: tool.name, - description: tool.description, - inputSchema: { - type: 'object', - properties: tool.parameters?.properties ?? {}, - required: tool.parameters?.required ?? [], - }, - }, - mcpServer - ); - workflow.tools.push(workflowTool); - } - } else if (!shouldAdd && existingTool) { - // 4b. If removing and tool exists, remove it - workflow.tools = workflow.tools.filter(t => - !(t.isMcp && t.mcpServerName === mcpServer.name && t.name === toolId) - ); - } - - // 5. Update the workflow - await agentWorkflowsCollection.updateOne( - { _id: workflow._id }, - { - $set: { - tools: workflow.tools, - lastUpdatedAt: new Date().toISOString() - } - } - ); - } -} - export async function toggleMcpTool( projectId: string, serverName: string, diff --git a/apps/rowboat/app/actions/project_actions.ts b/apps/rowboat/app/actions/project_actions.ts index b7f34d91..ba5b7569 100644 --- a/apps/rowboat/app/actions/project_actions.ts +++ b/apps/rowboat/app/actions/project_actions.ts @@ -1,7 +1,7 @@ 'use server'; import { redirect } from "next/navigation"; import { ObjectId } from "mongodb"; -import { dataSourcesCollection, embeddingsCollection, projectsCollection, agentWorkflowsCollection, testScenariosCollection, projectMembersCollection, apiKeysCollection, dataSourceDocsCollection, testProfilesCollection } from "../lib/mongodb"; +import { db, dataSourcesCollection, embeddingsCollection, projectsCollection, projectMembersCollection, apiKeysCollection, dataSourceDocsCollection } from "../lib/mongodb"; import { z } from 'zod'; import crypto from 'crypto'; import { revalidatePath } from "next/cache"; @@ -33,7 +33,11 @@ export async function projectAuthCheck(projectId: string) { } } -async function createBaseProject(name: string, user: WithStringId>): Promise<{ id: string } | { billingError: string }> { +async function createBaseProject( + name: string, + user: WithStringId>, + workflow?: z.infer +): Promise<{ id: string } | { billingError: string }> { // fetch project count for this user const projectCount = await projectsCollection.countDocuments({ createdByUserId: user._id, @@ -60,9 +64,10 @@ async function createBaseProject(name: string, user: WithStringId[]> { await projectAuthCheck(projectId); return libCollectProjectTools(projectId); +} + +export async function saveWorkflow(projectId: string, workflow: z.infer) { + await projectAuthCheck(projectId); + + // update the project's draft workflow + workflow.lastUpdatedAt = new Date().toISOString(); + await projectsCollection.updateOne({ + _id: projectId, + }, { + $set: { + draftWorkflow: workflow, + }, + }); +} + +export async function publishWorkflow(projectId: string, workflow: z.infer) { + await projectAuthCheck(projectId); + + // update the project's draft workflow + workflow.lastUpdatedAt = new Date().toISOString(); + await projectsCollection.updateOne({ + _id: projectId, + }, { + $set: { + liveWorkflow: workflow, + }, + }); +} + +export async function revertToLiveWorkflow(projectId: string) { + await projectAuthCheck(projectId); + + const project = await getProjectConfig(projectId); + const workflow = project.liveWorkflow; + + if (!workflow) { + throw new Error('No live workflow found'); + } + + workflow.lastUpdatedAt = new Date().toISOString(); + await projectsCollection.updateOne({ + _id: projectId, + }, { + $set: { + draftWorkflow: workflow, + }, + }); } \ No newline at end of file diff --git a/apps/rowboat/app/actions/testing_actions.ts b/apps/rowboat/app/actions/testing_actions.ts deleted file mode 100644 index ed6ee745..00000000 --- a/apps/rowboat/app/actions/testing_actions.ts +++ /dev/null @@ -1,610 +0,0 @@ -'use server'; -import { ObjectId } from "mongodb"; -import { testScenariosCollection, testSimulationsCollection, testProfilesCollection, testRunsCollection, testResultsCollection, projectsCollection } from "../lib/mongodb"; -import { z } from 'zod'; -import { projectAuthCheck } from "./project_actions"; -import { type WithStringId } from "../lib/types/types"; -import { TestScenario, TestSimulation, TestProfile, TestRun, TestResult } from "../lib/types/testing_types"; - -export async function listScenarios( - projectId: string, - page: number = 1, - pageSize: number = 10 -): Promise<{ - scenarios: WithStringId>[]; - total: number; -}> { - await projectAuthCheck(projectId); - - // Calculate skip value for pagination - const skip = (page - 1) * pageSize; - - // Get total count for pagination - const total = await testScenariosCollection.countDocuments({ projectId }); - - // Get paginated scenarios - const scenarios = await testScenariosCollection - .find({ projectId }) - .skip(skip) - .limit(pageSize) - .toArray(); - - return { - scenarios: scenarios.map(scenario => ({ - ...scenario, - _id: scenario._id.toString(), - })), - total, - }; -} - -export async function getScenario(projectId: string, scenarioId: string): Promise> | null> { - await projectAuthCheck(projectId); - - // fetch scenario - const scenario = await testScenariosCollection.findOne({ - _id: new ObjectId(scenarioId), - projectId, - }); - if (!scenario) { - return null; - } - const { _id, ...rest } = scenario; - return { - ...rest, - _id: _id.toString(), - }; -} - -export async function deleteScenario(projectId: string, scenarioId: string): Promise { - await projectAuthCheck(projectId); - - await testScenariosCollection.deleteOne({ - _id: new ObjectId(scenarioId), - projectId, - }); -} - -export async function createScenario( - projectId: string, - data: { - name: string; - description: string; - } -): Promise>> { - await projectAuthCheck(projectId); - - const doc = { - ...data, - projectId, - createdAt: new Date().toISOString(), - lastUpdatedAt: new Date().toISOString(), - }; - const result = await testScenariosCollection.insertOne(doc); - return { - ...doc, - _id: result.insertedId.toString(), - }; -} - -export async function updateScenario( - projectId: string, - scenarioId: string, - updates: { - name?: string; - description?: string; - } -): Promise { - await projectAuthCheck(projectId); - - const updateData: any = { - ...updates, - lastUpdatedAt: new Date().toISOString(), - }; - - await testScenariosCollection.updateOne( - { - _id: new ObjectId(scenarioId), - projectId, - }, - { - $set: updateData, - } - ); -} - -export async function listSimulations( - projectId: string, - page: number = 1, - pageSize: number = 10 -): Promise<{ - simulations: WithStringId>[]; - total: number; -}> { - await projectAuthCheck(projectId); - const skip = (page - 1) * pageSize; - const total = await testSimulationsCollection.countDocuments({ projectId }); - - const simulations = await testSimulationsCollection - .find({ projectId }) - .skip(skip) - .limit(pageSize) - .toArray(); - - return { - simulations: simulations.map(simulation => ({ - ...simulation, - _id: simulation._id.toString(), - })), - total, - }; -} - -export async function getSimulation(projectId: string, simulationId: string): Promise> | null> { - await projectAuthCheck(projectId); - - const simulation = await testSimulationsCollection.findOne({ - _id: new ObjectId(simulationId), - projectId, - }); - if (!simulation) { - return null; - } - const { _id, ...rest } = simulation; - return { - ...rest, - _id: _id.toString(), - }; -} - -export async function deleteSimulation(projectId: string, simulationId: string): Promise { - await projectAuthCheck(projectId); - - await testSimulationsCollection.deleteOne({ - _id: new ObjectId(simulationId), - projectId, - }); -} - -export async function createSimulation( - projectId: string, - data: { - name: string; - description?: string; - scenarioId: string; - profileId: string | null; - passCriteria: string; - } -): Promise>> { - await projectAuthCheck(projectId); - - const doc: z.infer = { - ...data, - projectId, - createdAt: new Date().toISOString(), - lastUpdatedAt: new Date().toISOString(), - }; - const result = await testSimulationsCollection.insertOne(doc); - return { - ...doc, - _id: result.insertedId.toString(), - }; -} - -export async function updateSimulation( - projectId: string, - simulationId: string, - updates: { - name?: string; - description?: string; - scenarioId?: string; - profileId?: string | null; - passCriteria?: string; - } -): Promise { - await projectAuthCheck(projectId); - - const updateData: any = { - ...updates, - lastUpdatedAt: new Date().toISOString(), - }; - - await testSimulationsCollection.updateOne( - { - _id: new ObjectId(simulationId), - projectId, - }, - { - $set: updateData, - } - ); -} - -export async function listProfiles( - projectId: string, - page: number = 1, - pageSize: number = 10 -): Promise<{ - profiles: WithStringId>[]; - total: number; -}> { - await projectAuthCheck(projectId); - const skip = (page - 1) * pageSize; - const total = await testProfilesCollection.countDocuments({ projectId }); - - const profiles = await testProfilesCollection - .find({ projectId }) - .skip(skip) - .limit(pageSize) - .toArray(); - - return { - profiles: profiles.map(profile => ({ - ...profile, - _id: profile._id.toString(), - })), - total, - }; -} - -export async function getProfile(projectId: string, profileId: string): Promise> | null> { - await projectAuthCheck(projectId); - - const profile = await testProfilesCollection.findOne({ - _id: new ObjectId(profileId), - projectId, - }); - if (!profile) { - return null; - } - const { _id, ...rest } = profile; - return { - ...rest, - _id: _id.toString(), - }; -} - -export async function deleteProfile(projectId: string, profileId: string): Promise { - await projectAuthCheck(projectId); - - await testProfilesCollection.deleteOne({ - _id: new ObjectId(profileId), - projectId, - }); -} - -export async function createProfile( - projectId: string, - data: { - name: string; - context: string; - mockTools: boolean; - mockPrompt?: string; - } -): Promise>> { - await projectAuthCheck(projectId); - - const doc = { - ...data, - projectId, - createdAt: new Date().toISOString(), - lastUpdatedAt: new Date().toISOString(), - }; - const result = await testProfilesCollection.insertOne(doc); - return { - ...doc, - _id: result.insertedId.toString(), - }; -} - -export async function updateProfile( - projectId: string, - profileId: string, - updates: { - name?: string; - context?: string; - mockTools?: boolean; - mockPrompt?: string; - } -): Promise { - await projectAuthCheck(projectId); - - const updateData: any = { - ...updates, - lastUpdatedAt: new Date().toISOString(), - }; - - await testProfilesCollection.updateOne( - { - _id: new ObjectId(profileId), - projectId, - }, - { - $set: updateData, - } - ); -} - -export async function listRuns( - projectId: string, - page: number = 1, - pageSize: number = 10 -): Promise<{ - runs: WithStringId>[]; - total: number; -}> { - await projectAuthCheck(projectId); - const skip = (page - 1) * pageSize; - const total = await testRunsCollection.countDocuments({ projectId }); - - const runs = await testRunsCollection - .find({ projectId }) - .sort({ startedAt: -1 }) // Sort by most recent first - .skip(skip) - .limit(pageSize) - .toArray(); - - return { - runs: runs.map(run => ({ - ...run, - _id: run._id.toString(), - })), - total, - }; -} - -export async function getRun(projectId: string, runId: string): Promise> | null> { - await projectAuthCheck(projectId); - - const run = await testRunsCollection.findOne({ - _id: new ObjectId(runId), - projectId, - }); - if (!run) { - return null; - } - const { _id, ...rest } = run; - return { - ...rest, - _id: _id.toString(), - }; -} - -export async function deleteRun(projectId: string, runId: string): Promise { - await projectAuthCheck(projectId); - - await testRunsCollection.deleteOne({ - _id: new ObjectId(runId), - projectId, - }); -} - -export async function createRun( - projectId: string, - data: { - simulationIds: string[]; - workflowId: string; - } -): Promise>> { - await projectAuthCheck(projectId); - - // Increment the testRunCounter and get the new value - const result = await projectsCollection.findOneAndUpdate( - { _id: projectId }, - { $inc: { testRunCounter: 1 } }, - { returnDocument: 'after' } - ); - - if (!result) { - throw new Error("Project not found"); - } - - const runNumber = result.testRunCounter || 1; - - const doc = { - ...data, - projectId, - name: `Run #${runNumber}`, - status: 'pending' as const, - startedAt: new Date().toISOString(), - aggregateResults: { - total: 0, - passCount: 0, - failCount: 0, - }, - }; - const insertResult = await testRunsCollection.insertOne(doc); - return { - ...doc, - _id: insertResult.insertedId.toString(), - }; -} - -export async function updateRun( - projectId: string, - runId: string, - updates: { - status?: 'pending' | 'running' | 'completed' | 'cancelled' | 'failed' | 'error'; - completedAt?: string; - aggregateResults?: { - total: number; - passCount: number; - failCount: number; - }; - } -): Promise { - await projectAuthCheck(projectId); - - const updateData: any = { - ...updates, - }; - - await testRunsCollection.updateOne( - { - _id: new ObjectId(runId), - projectId, - }, - { - $set: updateData, - } - ); -} - -export async function cancelRun(projectId: string, runId: string): Promise { - await projectAuthCheck(projectId); - - await testRunsCollection.updateOne( - { _id: new ObjectId(runId), projectId }, - { $set: { status: 'cancelled' } } - ); -} - -export async function listResults( - projectId: string, - runId: string, - page: number = 1, - pageSize: number = 10 -): Promise<{ - results: WithStringId>[]; - total: number; -}> { - await projectAuthCheck(projectId); - const skip = (page - 1) * pageSize; - const total = await testResultsCollection.countDocuments({ projectId, runId }); - - const results = await testResultsCollection - .find({ projectId, runId }) - .skip(skip) - .limit(pageSize) - .toArray(); - - return { - results: results.map(result => ({ - ...result, - _id: result._id.toString(), - })), - total, - }; -} - -export async function getResult(projectId: string, resultId: string): Promise> | null> { - await projectAuthCheck(projectId); - - const result = await testResultsCollection.findOne({ - _id: new ObjectId(resultId), - projectId, - }); - if (!result) { - return null; - } - const { _id, ...rest } = result; - return { - ...rest, - _id: _id.toString(), - }; -} - -export async function deleteResult(projectId: string, resultId: string): Promise { - await projectAuthCheck(projectId); - - await testResultsCollection.deleteOne({ - _id: new ObjectId(resultId), - projectId, - }); -} - -export async function createResult( - projectId: string, - data: { - runId: string; - simulationId: string; - result: 'pass' | 'fail'; - details: string; - transcript: string; - } -): Promise>> { - await projectAuthCheck(projectId); - - const doc = { - ...data, - projectId, - }; - const result = await testResultsCollection.insertOne(doc); - return { - ...doc, - _id: result.insertedId.toString(), - }; -} - -export async function updateResult( - projectId: string, - resultId: string, - updates: { - result?: 'pass' | 'fail'; - details?: string; - } -): Promise { - await projectAuthCheck(projectId); - - await testResultsCollection.updateOne( - { - _id: new ObjectId(resultId), - projectId, - }, - { - $set: updates, - } - ); -} - -export async function getSimulationResult( - projectId: string, - runId: string, - simulationId: string -): Promise> | null> { - await projectAuthCheck(projectId); - - const result = await testResultsCollection.findOne({ - projectId, - runId, - simulationId - }); - - if (!result) { - return null; - } - - const { _id, ...rest } = result; - return { - ...rest, - _id: _id.toString(), - }; -} - -export async function listRunSimulations( - projectId: string, - simulationIds: string[] -): Promise>[]> { - await projectAuthCheck(projectId); - - const simulations = await testSimulationsCollection - .find({ - _id: { $in: simulationIds.map(id => new ObjectId(id)) }, - projectId - }) - .toArray(); - - // Fetch associated scenario and profile names - const enrichedSimulations = await Promise.all(simulations.map(async (simulation) => { - const scenario = simulation.scenarioId ? await testScenariosCollection.findOne({ _id: new ObjectId(simulation.scenarioId) }) : null; - const profile = simulation.profileId ? await testProfilesCollection.findOne({ _id: new ObjectId(simulation.profileId) }) : null; - return { - ...simulation, - _id: simulation._id.toString(), - scenarioName: scenario?.name || 'Unknown', - profileName: profile?.name || 'None', - }; - })); - - return enrichedSimulations; -} \ No newline at end of file diff --git a/apps/rowboat/app/actions/voice_actions.ts b/apps/rowboat/app/actions/voice_actions.ts index 4db40088..4793111a 100644 --- a/apps/rowboat/app/actions/voice_actions.ts +++ b/apps/rowboat/app/actions/voice_actions.ts @@ -90,13 +90,12 @@ async function saveTwilioConfig(params: z.infer): Pro found: existingConfig }); - const configToSave = { + const configToSave: z.infer = { phone_number: params.phone_number, account_sid: params.account_sid, auth_token: params.auth_token, label: params.label || '', // Use empty string instead of undefined project_id: params.project_id, - workflow_id: params.workflow_id, createdAt: existingConfig?.createdAt || new Date(), status: 'active' as const }; @@ -108,7 +107,6 @@ async function saveTwilioConfig(params: z.infer): Pro params.phone_number, params.account_sid, params.auth_token, - params.workflow_id ); // Then save/update the config in database @@ -190,7 +188,6 @@ async function configureInboundCall( phone_number: string, account_sid: string, auth_token: string, - workflow_id: string ): Promise { try { // Normalize phone number format @@ -200,7 +197,6 @@ async function configureInboundCall( console.log('Configuring inbound call for:', { phone_number, - workflow_id }); // Initialize Twilio client @@ -262,7 +258,6 @@ async function configureInboundCall( return { status: wasPreviouslyConfigured ? 'reconfigured' : 'configured', phone_number: phone_number, - workflow_id: workflow_id, previous_webhook: wasPreviouslyConfigured ? currentVoiceUrl : undefined }; diff --git a/apps/rowboat/app/actions/workflow_actions.ts b/apps/rowboat/app/actions/workflow_actions.ts deleted file mode 100644 index ba8fdeae..00000000 --- a/apps/rowboat/app/actions/workflow_actions.ts +++ /dev/null @@ -1,241 +0,0 @@ -'use server'; -import { ObjectId, WithId } from "mongodb"; -import { projectsCollection, agentWorkflowsCollection } from "../lib/mongodb"; -import { z } from 'zod'; -import { templates } from "../lib/project_templates"; -import { projectAuthCheck } from "./project_actions"; -import { WithStringId } from "../lib/types/types"; -import { Workflow } from "../lib/types/workflow_types"; - -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 { agents, prompts, tools, startAgent } = templates['default']; - const workflow = { - agents, - prompts, - tools, - startAgent, - 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, - }; -} \ No newline at end of file diff --git a/apps/rowboat/app/api/stream-response/[streamId]/route.ts b/apps/rowboat/app/api/stream-response/[streamId]/route.ts index 3e765e66..14d09a3d 100644 --- a/apps/rowboat/app/api/stream-response/[streamId]/route.ts +++ b/apps/rowboat/app/api/stream-response/[streamId]/route.ts @@ -1,16 +1,8 @@ import { getCustomerIdForProject, logUsage } from "@/app/lib/billing"; import { USE_BILLING } from "@/app/lib/feature_flags"; import { redisClient } from "@/app/lib/redis"; -import { Workflow, WorkflowTool } from "@/app/lib/types/workflow_types"; import { streamResponse } from "@/app/lib/agents"; -import { Message } from "@/app/lib/types/types"; -import { z } from "zod"; - -const PayloadSchema = z.object({ - workflow: Workflow, - projectTools: z.array(WorkflowTool), - messages: z.array(Message), -}); +import { ZStreamAgentResponsePayload } from "@/app/lib/types/types"; export async function GET(request: Request, props: { params: Promise<{ streamId: string }> }) { const params = await props.params; @@ -21,13 +13,13 @@ export async function GET(request: Request, props: { params: Promise<{ streamId: } // parse the payload - const { workflow, projectTools, messages } = PayloadSchema.parse(JSON.parse(payload)); + const { projectId, workflow, projectTools, messages } = ZStreamAgentResponsePayload.parse(JSON.parse(payload)); console.log('payload', payload); // fetch billing customer id let billingCustomerId: string | null = null; if (USE_BILLING) { - billingCustomerId = await getCustomerIdForProject(workflow.projectId); + billingCustomerId = await getCustomerIdForProject(projectId); } const encoder = new TextEncoder(); @@ -37,7 +29,7 @@ export async function GET(request: Request, props: { params: Promise<{ streamId: async start(controller) { try { // Iterate over the generator - for await (const event of streamResponse(workflow, projectTools, messages)) { + for await (const event of streamResponse(projectId, workflow, projectTools, messages)) { // Check if this is a message event (has role property) if ('role' in event) { if (event.role === 'assistant') { diff --git a/apps/rowboat/app/api/twilio/inbound_call/route.ts b/apps/rowboat/app/api/twilio/inbound_call/route.ts index bbae718d..1869a954 100644 --- a/apps/rowboat/app/api/twilio/inbound_call/route.ts +++ b/apps/rowboat/app/api/twilio/inbound_call/route.ts @@ -1,9 +1,8 @@ import { getResponse } from "@/app/lib/agents"; -import { agentWorkflowsCollection, twilioConfigsCollection, twilioInboundCallsCollection } from "@/app/lib/mongodb"; +import { projectsCollection, twilioConfigsCollection, twilioInboundCallsCollection } from "@/app/lib/mongodb"; import { collectProjectTools } from "@/app/lib/project_tools"; import { PrefixLogger } from "@/app/lib/utils"; import VoiceResponse from "twilio/lib/twiml/VoiceResponse"; -import { ObjectId } from "mongodb"; import { z } from "zod"; import { TwilioInboundCall } from "@/app/lib/types/voice_types"; import { hangup, reject, XmlResponse, ZStandardRequestParams } from "../utils"; @@ -63,16 +62,19 @@ export async function POST(request: Request) { return reject('rejected'); } - // extract workflow and project id and fetch workflow from db + // fetch project and extract live workflow // if workflow not found, reject the call const projectId = twilioConfig.project_id; - const workflowId = twilioConfig.workflow_id; - const workflow = await agentWorkflowsCollection.findOne({ - projectId: projectId, - _id: new ObjectId(workflowId), + const project = await projectsCollection.findOne({ + _id: projectId, }); + if (!project) { + logger.log(`Project ${projectId} not found`); + return reject('rejected'); + } + const workflow = project.liveWorkflow; if (!workflow) { - logger.log(`Workflow ${workflowId} not found for project ${projectId}`); + logger.log(`Workflow not found for project ${projectId}`); return reject('rejected'); } @@ -81,7 +83,7 @@ export async function POST(request: Request) { // this is the first turn, get the initial assistant response // and validate it - const { messages } = await getResponse(workflow, projectTools, []); + const { messages } = await getResponse(projectId, workflow, projectTools, []); if (messages.length === 0) { logger.log('Agent response is empty'); return hangup(); @@ -98,7 +100,6 @@ export async function POST(request: Request) { to: data.To, from: data.From, projectId, - workflowId, messages, createdAt: recvdAt.toISOString(), lastUpdatedAt: new Date().toISOString(), diff --git a/apps/rowboat/app/api/twilio/turn/[callSid]/route.ts b/apps/rowboat/app/api/twilio/turn/[callSid]/route.ts index a0832b4b..29fa71dd 100644 --- a/apps/rowboat/app/api/twilio/turn/[callSid]/route.ts +++ b/apps/rowboat/app/api/twilio/turn/[callSid]/route.ts @@ -1,9 +1,8 @@ import { getResponse } from "@/app/lib/agents"; -import { agentWorkflowsCollection, twilioConfigsCollection, twilioInboundCallsCollection } from "@/app/lib/mongodb"; +import { projectsCollection, twilioInboundCallsCollection } from "@/app/lib/mongodb"; import { collectProjectTools } from "@/app/lib/project_tools"; import { PrefixLogger } from "@/app/lib/utils"; import VoiceResponse from "twilio/lib/twiml/VoiceResponse"; -import { ObjectId } from "mongodb"; import { z } from "zod"; import { hangup, XmlResponse, ZStandardRequestParams } from "../../utils"; import { Message } from "@/app/lib/types/types"; @@ -35,15 +34,19 @@ export async function POST( logger.log('Call not found'); return hangup(); } - const { workflowId, projectId } = call; + const { projectId } = call; - // fetch workflow - const workflow = await agentWorkflowsCollection.findOne({ - projectId: projectId, - _id: new ObjectId(workflowId), + // fetch project and extract live workflow + const project = await projectsCollection.findOne({ + _id: projectId, }); + if (!project) { + logger.log(`Project ${projectId} not found`); + return hangup(); + } + const workflow = project.liveWorkflow; if (!workflow) { - logger.log(`Workflow ${workflowId} not found for project ${projectId}`); + logger.log(`Workflow not found for project ${projectId}`); return hangup(); } @@ -58,7 +61,7 @@ export async function POST( content: data.SpeechResult, } ]; - const { messages } = await getResponse(workflow, projectTools, reqMessages); + const { messages } = await getResponse(projectId, workflow, projectTools, reqMessages); if (messages.length === 0) { logger.log('Agent response is empty'); return hangup(); diff --git a/apps/rowboat/app/api/v1/[projectId]/chat/route.ts b/apps/rowboat/app/api/v1/[projectId]/chat/route.ts index 09659cf6..1001cf9c 100644 --- a/apps/rowboat/app/api/v1/[projectId]/chat/route.ts +++ b/apps/rowboat/app/api/v1/[projectId]/chat/route.ts @@ -1,12 +1,11 @@ import { NextRequest } from "next/server"; -import { agentWorkflowsCollection, db, projectsCollection, testProfilesCollection } from "../../../../lib/mongodb"; +import { projectsCollection } from "../../../../lib/mongodb"; import { z } from "zod"; import { ObjectId } from "mongodb"; import { authCheck } from "../../utils"; import { ApiRequest, ApiResponse } from "../../../../lib/types/types"; import { check_query_limit } from "../../../../lib/rate_limiting"; import { PrefixLogger } from "../../../../lib/utils"; -import { TestProfile } from "@/app/lib/types/testing_types"; import { collectProjectTools } from "@/app/lib/project_tools"; import { authorize, getCustomerIdForProject, logUsage } from "@/app/lib/billing"; import { USE_BILLING } from "@/app/lib/feature_flags"; @@ -65,19 +64,10 @@ export async function POST( // fetch project tools const projectTools = await collectProjectTools(projectId); - // if workflow id is provided in the request, use it, else use the published workflow id - let workflowId = result.data.workflowId ?? project.publishedWorkflowId; - if (!workflowId) { - logger.log(`No workflow id provided in request or project has no published workflow`); - return Response.json({ error: "No workflow id provided in request or project has no published workflow" }, { status: 404 }); - } // fetch workflow - const workflow = await agentWorkflowsCollection.findOne({ - projectId: projectId, - _id: new ObjectId(workflowId), - }); + const workflow = project.liveWorkflow; if (!workflow) { - logger.log(`Workflow ${workflowId} not found for project ${projectId}`); + logger.log(`Workflow not found for project ${projectId}`); return Response.json({ error: "Workflow not found" }, { status: 404 }); } @@ -103,21 +93,8 @@ export async function POST( } } - // if test profile is provided in the request, use it - let testProfile: z.infer | null = null; - if (result.data.testProfileId) { - testProfile = await testProfilesCollection.findOne({ - projectId: projectId, - _id: new ObjectId(result.data.testProfileId), - }); - if (!testProfile) { - logger.log(`Test profile ${result.data.testProfileId} not found for project ${projectId}`); - return Response.json({ error: "Test profile not found" }, { status: 404 }); - } - } - // get assistant response - const { messages } = await getResponse(workflow, projectTools, reqMessages); + const { messages } = await getResponse(projectId, workflow, projectTools, reqMessages); // log billing usage if (USE_BILLING && billingCustomerId) { diff --git a/apps/rowboat/app/api/widget/v1/chats/[chatId]/turn/route.ts b/apps/rowboat/app/api/widget/v1/chats/[chatId]/turn/route.ts index 8b4e98ed..7f901e70 100644 --- a/apps/rowboat/app/api/widget/v1/chats/[chatId]/turn/route.ts +++ b/apps/rowboat/app/api/widget/v1/chats/[chatId]/turn/route.ts @@ -1,6 +1,6 @@ import { NextRequest } from "next/server"; import { apiV1 } from "rowboat-shared"; -import { agentWorkflowsCollection, projectsCollection, chatsCollection, chatMessagesCollection } from "../../../../../../lib/mongodb"; +import { projectsCollection, chatsCollection, chatMessagesCollection } from "../../../../../../lib/mongodb"; import { z } from "zod"; import { ObjectId, WithId } from "mongodb"; import { authCheck } from "../../../utils"; @@ -185,10 +185,7 @@ export async function POST( const projectTools = await collectProjectTools(session.projectId); // fetch workflow - const workflow = await agentWorkflowsCollection.findOne({ - projectId: session.projectId, - _id: new ObjectId(projectSettings.publishedWorkflowId), - }); + const workflow = projectSettings.liveWorkflow; if (!workflow) { throw new Error("Workflow not found"); } @@ -214,7 +211,7 @@ export async function POST( const inMessages: z.infer[] = convert(messages); inMessages.push(userMessage); - const { messages: responseMessages } = await getResponse(workflow, projectTools, [systemMessage, ...inMessages]); + const { messages: responseMessages } = await getResponse(session.projectId, workflow, projectTools, [systemMessage, ...inMessages]); const convertedResponseMessages = convertBack(responseMessages); const unsavedMessages = [ userMessage, diff --git a/apps/rowboat/app/lib/agents.ts b/apps/rowboat/app/lib/agents.ts index aa8a482a..ef06efc0 100644 --- a/apps/rowboat/app/lib/agents.ts +++ b/apps/rowboat/app/lib/agents.ts @@ -489,6 +489,7 @@ function createComposioTool( // Helper to create an agent function createAgent( logger: PrefixLogger, + projectId: string, config: z.infer, tools: Record, projectTools: z.infer[], @@ -539,7 +540,7 @@ ${CHILD_TRANSFER_RELATED_INSTRUCTIONS} // Add RAG tool if needed if (config.ragDataSources?.length) { - const ragTool = createRagTool(logger, config, workflow.projectId); + const ragTool = createRagTool(logger, config, projectId); agentTools.push(ragTool); // update instructions to include RAG instructions @@ -794,7 +795,12 @@ async function* emitGreetingTurn(logger: PrefixLogger, workflow: z.infer, toolConfig: Record>): Record { +function createTools( + logger: PrefixLogger, + projectId: string, + workflow: z.infer, + toolConfig: Record>, +): Record { const tools: Record = {}; for (const [toolName, config] of Object.entries(toolConfig)) { if (workflow.mockTools?.[toolName]) { @@ -804,16 +810,16 @@ function createTools(logger: PrefixLogger, workflow: z.infer, t }); logger.log(`created mock tool: ${toolName}`); } else if (config.isMcp) { - tools[toolName] = createMcpTool(logger, config, workflow.projectId); + tools[toolName] = createMcpTool(logger, config, projectId); logger.log(`created mcp tool: ${toolName}`); } else if (config.isComposio) { - tools[toolName] = createComposioTool(logger, config, workflow.projectId); + tools[toolName] = createComposioTool(logger, config, projectId); logger.log(`created composio tool: ${toolName}`); } else if (config.mockTool) { tools[toolName] = createMockTool(logger, config); logger.log(`created mock tool: ${toolName}`); } else { - tools[toolName] = createWebhookTool(logger, config, workflow.projectId); + tools[toolName] = createWebhookTool(logger, config, projectId); logger.log(`created webhook tool: ${toolName}`); } } @@ -822,6 +828,7 @@ function createTools(logger: PrefixLogger, workflow: z.infer, t function createAgents( logger: PrefixLogger, + projectId: string, workflow: z.infer, agentConfig: Record>, tools: Record, @@ -837,6 +844,7 @@ function createAgents( for (const [agentName, config] of Object.entries(agentConfig)) { const { agent, entities } = createAgent( logger, + projectId, config, tools, projectTools, @@ -918,6 +926,7 @@ function maybeInjectGiveUpControlInstructions( // Main function to stream an agentic response // using OpenAI Agents SDK export async function* streamResponse( + projectId: string, workflow: z.infer, projectTools: z.infer[], messages: z.infer[], @@ -926,8 +935,7 @@ export async function* streamResponse( console.log('-------------------- AGENT LOOP START --------------------'); // set up logging let logger = new PrefixLogger(`agent-loop`) - logger.log('projectId', workflow.projectId); - logger.log('workflow', workflow.name); + logger.log('projectId', projectId); // ensure valid system message ensureSystemMessage(logger, messages); @@ -946,10 +954,10 @@ export async function* streamResponse( logger.log(`initialized stack: ${JSON.stringify(stack)}`); // create tools - const tools = createTools(logger, workflow, toolConfig); + const tools = createTools(logger, projectId, workflow, toolConfig); // create agents - const { agents, originalInstructions, originalHandoffs } = createAgents(logger, workflow, agentConfig, tools, projectTools, promptConfig); + const { agents, originalInstructions, originalHandoffs } = createAgents(logger, projectId, workflow, agentConfig, tools, projectTools, promptConfig); // track agent to agent calls const transferCounter = new AgentTransferCounter(); @@ -1203,6 +1211,7 @@ export async function* streamResponse( // this is a sync version of streamResponse export async function getResponse( + projectId: string, workflow: z.infer, projectTools: z.infer[], messages: z.infer[], @@ -1218,7 +1227,7 @@ export async function getResponse( completion: 0, }, }; - for await (const event of streamResponse(workflow, projectTools, messages)) { + for await (const event of streamResponse(projectId, workflow, projectTools, messages)) { if ('role' in event) { out.push(event); } diff --git a/apps/rowboat/app/lib/feature_flags.ts b/apps/rowboat/app/lib/feature_flags.ts index 4d3f6c9b..99a3ae60 100644 --- a/apps/rowboat/app/lib/feature_flags.ts +++ b/apps/rowboat/app/lib/feature_flags.ts @@ -11,7 +11,6 @@ export const USE_KLAVIS_TOOLS = process.env.USE_KLAVIS_TOOLS === 'true'; // Hardcoded flags export const USE_MULTIPLE_PROJECTS = true; -export const USE_TESTING_FEATURE = false; export const USE_VOICE_FEATURE = false; export const USE_TRANSFER_CONTROL_OPTIONS = true; export const USE_PRODUCT_TOUR = true; diff --git a/apps/rowboat/app/lib/migrate_versioned_workflows.ts b/apps/rowboat/app/lib/migrate_versioned_workflows.ts new file mode 100644 index 00000000..18771c8b --- /dev/null +++ b/apps/rowboat/app/lib/migrate_versioned_workflows.ts @@ -0,0 +1,84 @@ +import { agentWorkflowsCollection, projectsCollection } from "./mongodb"; +import { ObjectId } from "mongodb"; +import { Workflow } from "./types/workflow_types"; +import { z } from "zod"; + +export async function migrate_versioned_workflows(projectId: string) { + // fetch project data + const project = await projectsCollection.findOne({ _id: projectId }); + if (!project) { + throw new Error(`Project ${projectId} not found`); + } + + // Skip if project already has workflows migrated + if (project.draftWorkflow && project.liveWorkflow) { + console.log(`Project ${projectId} already has migrated workflows, skipping...`); + return; + } + + const updateFields: { draftWorkflow?: z.infer; liveWorkflow?: z.infer } = {}; + + // 1. Migrate published workflow to liveWorkflow + if (project.publishedWorkflowId) { + const publishedWorkflow = await agentWorkflowsCollection.findOne({ + _id: new ObjectId(project.publishedWorkflowId) + }); + + if (publishedWorkflow) { + // @ts-ignore - Workflow type mismatch + const { _id, name, createdAt, projectId, ...rest } = publishedWorkflow; + updateFields.liveWorkflow = rest; + console.log(`Found published workflow ${project.publishedWorkflowId} for project ${projectId}`); + } else { + console.warn(`Published workflow ${project.publishedWorkflowId} not found for project ${projectId}`); + } + } + + // 2. Find the latest workflow for draft (that isn't the published one) + const workflows = await agentWorkflowsCollection.find({ + projectId, + }).sort({ lastUpdatedAt: -1 }).toArray(); + + let latestWorkflow; + for (const workflow of workflows) { + // Skip if this is the published workflow + if (project.publishedWorkflowId && workflow._id.toString() === project.publishedWorkflowId) { + continue; + } + + latestWorkflow = workflow; + break; + } + + // Handle cases where no published workflow exists + if (!updateFields.liveWorkflow && latestWorkflow) { + // No published workflow found, use latest as live workflow + // @ts-ignore - Workflow type mismatch + const { _id, name, createdAt, projectId, ...rest } = latestWorkflow; + updateFields.liveWorkflow = rest; + console.log(`No published workflow found, using latest workflow as live for project ${projectId}`); + } + + // Set draft workflow + if (latestWorkflow) { + // @ts-ignore - Workflow type mismatch + const { _id, name, createdAt, projectId, ...rest } = latestWorkflow; + updateFields.draftWorkflow = rest; + console.log(`Found draft workflow for project ${projectId}`); + } else if (updateFields.liveWorkflow) { + // No separate draft found, use the published workflow as draft too + updateFields.draftWorkflow = updateFields.liveWorkflow; + console.log(`No separate draft found, using live workflow as draft for project ${projectId}`); + } + + // 3. Update the project with the migrated workflows + if (Object.keys(updateFields).length > 0) { + await projectsCollection.updateOne( + { _id: projectId }, + { $set: updateFields } + ); + console.log(`Successfully migrated ${Object.keys(updateFields).length} workflow(s) for project ${projectId}`); + } else { + console.log(`No workflows found to migrate for project ${projectId}`); + } +} \ No newline at end of file diff --git a/apps/rowboat/app/lib/mongodb.ts b/apps/rowboat/app/lib/mongodb.ts index f1ba5ddd..e47555f5 100644 --- a/apps/rowboat/app/lib/mongodb.ts +++ b/apps/rowboat/app/lib/mongodb.ts @@ -7,7 +7,6 @@ import { Project } from "./types/project_types"; import { EmbeddingDoc } from "./types/datasource_types"; import { DataSourceDoc } from "./types/datasource_types"; import { DataSource } from "./types/datasource_types"; -import { TestScenario, TestResult, TestRun, TestProfile, TestSimulation } from "./types/testing_types"; import { TwilioConfig, TwilioInboundCall } from "./types/voice_types"; import { z } from 'zod'; import { apiV1 } from "rowboat-shared"; @@ -23,11 +22,6 @@ export const projectMembersCollection = db.collection>('webpages'); export const agentWorkflowsCollection = db.collection>("agent_workflows"); export const apiKeysCollection = db.collection>("api_keys"); -export const testScenariosCollection = db.collection>("test_scenarios"); -export const testProfilesCollection = db.collection>("test_profiles"); -export const testSimulationsCollection = db.collection>("test_simulations"); -export const testRunsCollection = db.collection>("test_runs"); -export const testResultsCollection = db.collection>("test_results"); export const chatsCollection = db.collection>("chats"); export const chatMessagesCollection = db.collection>("chat_messages"); export const twilioConfigsCollection = db.collection>("twilio_configs"); diff --git a/apps/rowboat/app/lib/types/project_types.ts b/apps/rowboat/app/lib/types/project_types.ts index 54c46984..1f5ec836 100644 --- a/apps/rowboat/app/lib/types/project_types.ts +++ b/apps/rowboat/app/lib/types/project_types.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { MCPServer } from "./types"; -import { WorkflowTool } from "./workflow_types"; +import { Workflow, WorkflowTool } from "./workflow_types"; import { ZTool } from "../composio/composio"; export const ComposioConnectedAccount = z.object({ @@ -23,9 +23,10 @@ export const Project = z.object({ createdByUserId: z.string(), secret: z.string(), chatClientId: z.string(), + draftWorkflow: Workflow.optional(), + liveWorkflow: Workflow.optional(), webhookUrl: z.string().optional(), publishedWorkflowId: z.string().optional(), - nextWorkflowNumber: z.number().optional(), testRunCounter: z.number().default(0), mcpServers: z.array(MCPServer).optional(), composioConnectedAccounts: z.record(z.string(), ComposioConnectedAccount).optional(), diff --git a/apps/rowboat/app/lib/types/testing_types.ts b/apps/rowboat/app/lib/types/testing_types.ts deleted file mode 100644 index bbace333..00000000 --- a/apps/rowboat/app/lib/types/testing_types.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { z } from "zod"; - -export const TestScenario = z.object({ - projectId: z.string(), - name: z.string().min(1, "Name cannot be empty"), - description: z.string().min(1, "Description cannot be empty"), - createdAt: z.string().datetime(), - lastUpdatedAt: z.string().datetime(), -}); - -export const TestProfile = z.object({ - projectId: z.string(), - name: z.string().min(1, "Name cannot be empty"), - context: z.string(), - createdAt: z.string().datetime(), - lastUpdatedAt: z.string().datetime(), - mockTools: z.boolean(), - mockPrompt: z.string().optional(), -}); - -export const TestSimulation = z.object({ - projectId: z.string(), - name: z.string(), - description: z.string().optional().nullable(), - createdAt: z.string().datetime(), - lastUpdatedAt: z.string().datetime(), - scenarioId: z.string(), - profileId: z.string().nullable(), - passCriteria: z.string(), -}); - -export const TestRun = z.object({ - projectId: z.string(), - name: z.string(), - simulationIds: z.array(z.string()), - workflowId: z.string(), - status: z.enum(['pending', 'running', 'completed', 'cancelled', 'failed', 'error']), - startedAt: z.string(), - completedAt: z.string().optional(), - aggregateResults: z.object({ - total: z.number(), - passCount: z.number(), - failCount: z.number(), - }).optional(), -}); - -export const TestResult = z.object({ - projectId: z.string(), - runId: z.string(), - simulationId: z.string(), - result: z.union([z.literal('pass'), z.literal('fail')]), - details: z.string(), - transcript: z.string() -}); \ No newline at end of file diff --git a/apps/rowboat/app/lib/types/types.ts b/apps/rowboat/app/lib/types/types.ts index deed883f..0900d748 100644 --- a/apps/rowboat/app/lib/types/types.ts +++ b/apps/rowboat/app/lib/types/types.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { WorkflowTool } from "./workflow_types"; +import { Workflow, WorkflowTool } from "./workflow_types"; export const SystemMessage = z.object({ role: z.literal("system"), @@ -158,7 +158,6 @@ export type WithStringId = T & { _id: string }; export const ApiRequest = z.object({ messages: z.array(Message), state: z.unknown(), - workflowId: z.string().nullable().optional(), testProfileId: z.string().nullable().optional(), mockTools: z.record(z.string(), z.string()).nullable().optional(), }); @@ -208,3 +207,9 @@ export function convertMcpServerToolToWorkflowTool( return converted; } +export const ZStreamAgentResponsePayload = z.object({ + projectId: z.string(), + workflow: Workflow, + projectTools: z.array(WorkflowTool), + messages: z.array(Message), +}); diff --git a/apps/rowboat/app/lib/types/voice_types.ts b/apps/rowboat/app/lib/types/voice_types.ts index e7facebf..03e682c4 100644 --- a/apps/rowboat/app/lib/types/voice_types.ts +++ b/apps/rowboat/app/lib/types/voice_types.ts @@ -8,7 +8,6 @@ export const TwilioConfigParams = z.object({ auth_token: z.string(), label: z.string(), project_id: z.string(), - workflow_id: z.string(), }); export const TwilioConfig = TwilioConfigParams.extend({ @@ -24,7 +23,6 @@ export interface TwilioConfigResponse { export interface InboundConfigResponse { status: 'configured' | 'reconfigured'; phone_number: string; - workflow_id: string; previous_webhook?: string; error?: string; } @@ -34,7 +32,6 @@ export const TwilioInboundCall = z.object({ to: z.string(), from: z.string(), projectId: z.string(), - workflowId: z.string(), messages: z.array(Message), createdAt: z.string().datetime(), lastUpdatedAt: z.string().datetime().optional(), diff --git a/apps/rowboat/app/lib/types/workflow_types.ts b/apps/rowboat/app/lib/types/workflow_types.ts index eaf479e0..c127529c 100644 --- a/apps/rowboat/app/lib/types/workflow_types.ts +++ b/apps/rowboat/app/lib/types/workflow_types.ts @@ -61,21 +61,16 @@ export const WorkflowTool = z.object({ }).optional(), // the data for the Composio tool, if it is a Composio tool }); export const Workflow = z.object({ - name: z.string().optional(), agents: z.array(WorkflowAgent), prompts: z.array(WorkflowPrompt), tools: z.array(WorkflowTool), startAgent: z.string(), - createdAt: z.string().datetime(), lastUpdatedAt: z.string().datetime(), - projectId: z.string(), mockTools: z.record(z.string(), z.string()).optional(), // a dict of toolName => mockInstructions }); export const WorkflowTemplate = Workflow .omit({ - projectId: true, lastUpdatedAt: true, - createdAt: true, }) .extend({ name: z.string(), diff --git a/apps/rowboat/app/lib/utils.ts b/apps/rowboat/app/lib/utils.ts index 13293a10..c054d355 100644 --- a/apps/rowboat/app/lib/utils.ts +++ b/apps/rowboat/app/lib/utils.ts @@ -3,27 +3,30 @@ import { generateObject } from "ai"; import { openai } from "@ai-sdk/openai"; import { redisClient } from "./redis"; import { Workflow, WorkflowTool } from "./types/workflow_types"; -import { Message } from "./types/types"; +import { Message, ZStreamAgentResponsePayload } from "./types/types"; export async function getAgenticResponseStreamId( + projectId: string, workflow: z.infer, projectTools: z.infer[], messages: z.infer[], ): Promise<{ streamId: string, }> { - // serialize the request - const payload = JSON.stringify({ + const payload: z.infer = { + projectId, workflow, projectTools, messages, - }); + } + // serialize the request + const serialized = JSON.stringify(payload); // create a uuid for the stream const streamId = crypto.randomUUID(); // store payload in redis - await redisClient.set(`chat-stream-${streamId}`, payload, 'EX', 60 * 10); // expire in 10 minutes + await redisClient.set(`chat-stream-${streamId}`, serialized, 'EX', 60 * 10); // expire in 10 minutes return { streamId, diff --git a/apps/rowboat/app/projects/[projectId]/config/components/voice.tsx b/apps/rowboat/app/projects/[projectId]/config/components/voice.tsx index 2c1c6ce2..b9afb60a 100644 --- a/apps/rowboat/app/projects/[projectId]/config/components/voice.tsx +++ b/apps/rowboat/app/projects/[projectId]/config/components/voice.tsx @@ -5,7 +5,7 @@ import { Spinner } from "@heroui/react"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { configureTwilioNumber, getTwilioConfigs, deleteTwilioConfig } from "../../../../actions/voice_actions"; -import { TwilioConfig } from "../../../../lib/types/voice_types"; +import { TwilioConfig, TwilioConfigParams } from "../../../../lib/types/voice_types"; import { CheckCircleIcon, XCircleIcon, InfoIcon, EyeOffIcon, EyeIcon } from "lucide-react"; import { Section } from './project'; import { clsx } from 'clsx'; @@ -198,23 +198,15 @@ export function VoiceSection({ projectId }: { projectId: string }) { return; } - const workflowId = localStorage.getItem(`lastWorkflowId_${projectId}`); - if (!workflowId) { - setError('No workflow selected. Please select a workflow first.'); - setConfigurationValid(false); - return; - } - setLoading(true); setError(null); - const configParams = { + const configParams: z.infer = { phone_number: formState.phone.replaceAll(/[^0-9\+]/g, ''), account_sid: formState.accountSid, auth_token: formState.authToken, label: formState.label, project_id: projectId, - workflow_id: workflowId, }; const result = await configureTwilioNumber(configParams); diff --git a/apps/rowboat/app/projects/[projectId]/playground/app.tsx b/apps/rowboat/app/projects/[projectId]/playground/app.tsx index 9c49125e..5ab76f37 100644 --- a/apps/rowboat/app/projects/[projectId]/playground/app.tsx +++ b/apps/rowboat/app/projects/[projectId]/playground/app.tsx @@ -7,11 +7,8 @@ import { Chat } from "./components/chat"; import { Panel } from "@/components/common/panel-common"; import { Button } from "@/components/ui/button"; import { Tooltip } from "@heroui/react"; -import { TestProfile } from "@/app/lib/types/testing_types"; import { WithStringId } from "@/app/lib/types/types"; -import { ProfileSelector } from "@/app/projects/[projectId]/test/[[...slug]]/components/selectors/profile-selector"; import { CheckIcon, CopyIcon, PlusIcon, UserIcon, InfoIcon, BugIcon, BugOffIcon, CodeIcon } from "lucide-react"; -import { USE_TESTING_FEATURE } from "@/app/lib/feature_flags"; import { clsx } from "clsx"; const defaultSystemMessage = ''; @@ -40,7 +37,6 @@ export function App({ triggerCopilotChat?: (message: string) => void; }) { const [counter, setCounter] = useState(0); - const [testProfile, setTestProfile] = useState> | null>(null); const [systemMessage, setSystemMessage] = useState(defaultSystemMessage); const [showDebugMessages, setShowDebugMessages] = useState(true); const [chat, setChat] = useState>({ @@ -59,11 +55,6 @@ export function App({ setCounter(counter + 1); } - function handleTestProfileChange(profile: WithStringId> | null) { - setTestProfile(profile); - setCounter(counter + 1); - } - function handleNewChatButtonClick() { setCounter(counter + 1); setChat({ @@ -138,17 +129,6 @@ export function App({ } rightActions={
- {USE_TESTING_FEATURE && ( - - )}