mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-08 06:42:39 +02:00
simplify workflow version mgmt
This commit is contained in:
parent
1b19a9bcba
commit
23681d8b4d
49 changed files with 385 additions and 4767 deletions
|
|
@ -58,12 +58,13 @@ export async function scrapeWebpage(url: string): Promise<z.infer<typeof Webpage
|
|||
}
|
||||
|
||||
export async function getAssistantResponseStreamId(
|
||||
projectId: string,
|
||||
workflow: z.infer<typeof Workflow>,
|
||||
projectTools: z.infer<typeof WorkflowTool>[],
|
||||
messages: z.infer<typeof Message>[],
|
||||
): 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;
|
||||
}
|
||||
|
|
@ -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<ty
|
|||
}, { $set: { mcpServers } });
|
||||
}
|
||||
|
||||
export async function listMcpServers(projectId: string): Promise<z.infer<typeof MCPServer>[]> {
|
||||
await projectAuthCheck(projectId);
|
||||
const project = await projectsCollection.findOne({
|
||||
_id: projectId,
|
||||
});
|
||||
return project?.mcpServers ?? [];
|
||||
}
|
||||
|
||||
export async function updateToolInAllWorkflows(
|
||||
projectId: string,
|
||||
mcpServer: z.infer<typeof MCPServer>,
|
||||
toolId: string,
|
||||
shouldAdd: boolean
|
||||
): Promise<void> {
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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<z.infer<typeof User>>): Promise<{ id: string } | { billingError: string }> {
|
||||
async function createBaseProject(
|
||||
name: string,
|
||||
user: WithStringId<z.infer<typeof User>>,
|
||||
workflow?: z.infer<typeof Workflow>
|
||||
): 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<z.infer<typeof
|
|||
createdAt: (new Date()).toISOString(),
|
||||
lastUpdatedAt: (new Date()).toISOString(),
|
||||
createdByUserId: user._id,
|
||||
draftWorkflow: workflow,
|
||||
liveWorkflow: workflow,
|
||||
chatClientId,
|
||||
secret,
|
||||
nextWorkflowNumber: 1,
|
||||
testRunCounter: 0,
|
||||
});
|
||||
|
||||
|
|
@ -85,26 +90,19 @@ export async function createProject(formData: FormData): Promise<{ id: string }
|
|||
const name = formData.get('name') as string;
|
||||
const templateKey = formData.get('template') as string;
|
||||
|
||||
const response = await createBaseProject(name, user);
|
||||
const { agents, prompts, tools, startAgent } = templates[templateKey];
|
||||
const response = await createBaseProject(name, user, {
|
||||
agents,
|
||||
prompts,
|
||||
tools,
|
||||
startAgent,
|
||||
lastUpdatedAt: (new Date()).toISOString(),
|
||||
});
|
||||
if ('billingError' in response) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const projectId = response.id;
|
||||
|
||||
// Add first workflow version with specified template
|
||||
const { agents, prompts, tools, startAgent } = templates[templateKey];
|
||||
await agentWorkflowsCollection.insertOne({
|
||||
projectId,
|
||||
agents,
|
||||
prompts,
|
||||
tools,
|
||||
startAgent,
|
||||
createdAt: (new Date()).toISOString(),
|
||||
lastUpdatedAt: (new Date()).toISOString(),
|
||||
name: `Version 1`,
|
||||
});
|
||||
|
||||
return { id: projectId };
|
||||
}
|
||||
|
||||
|
|
@ -270,13 +268,13 @@ export async function deleteProject(projectId: string) {
|
|||
projectId,
|
||||
});
|
||||
|
||||
// delete workflows
|
||||
await agentWorkflowsCollection.deleteMany({
|
||||
// delete workflow versions
|
||||
await db.collection('agent_workflows').deleteMany({
|
||||
projectId,
|
||||
});
|
||||
|
||||
// delete scenarios
|
||||
await testScenariosCollection.deleteMany({
|
||||
await db.collection('test_scenarios').deleteMany({
|
||||
projectId,
|
||||
});
|
||||
|
||||
|
|
@ -292,26 +290,19 @@ export async function createProjectFromPrompt(formData: FormData): Promise<{ id:
|
|||
const user = await authCheck();
|
||||
const name = formData.get('name') as string;
|
||||
|
||||
const response = await createBaseProject(name, user);
|
||||
const { agents, prompts, tools, startAgent } = templates['default'];
|
||||
const response = await createBaseProject(name, user, {
|
||||
agents,
|
||||
prompts,
|
||||
tools,
|
||||
startAgent,
|
||||
lastUpdatedAt: (new Date()).toISOString(),
|
||||
});
|
||||
if ('billingError' in response) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const projectId = response.id;
|
||||
|
||||
// Add first workflow version with default template
|
||||
const { agents, prompts, tools, startAgent } = templates['default'];
|
||||
await agentWorkflowsCollection.insertOne({
|
||||
projectId,
|
||||
agents,
|
||||
prompts,
|
||||
tools,
|
||||
startAgent,
|
||||
createdAt: (new Date()).toISOString(),
|
||||
lastUpdatedAt: (new Date()).toISOString(),
|
||||
name: `Version 1`,
|
||||
});
|
||||
|
||||
return { id: projectId };
|
||||
}
|
||||
|
||||
|
|
@ -325,29 +316,69 @@ export async function createProjectFromWorkflowJson(formData: FormData): Promise
|
|||
throw new Error('Invalid JSON');
|
||||
}
|
||||
// Validate and parse with zod
|
||||
const parsed = Workflow.omit({ projectId: true }).safeParse(workflowData);
|
||||
const parsed = Workflow.safeParse(workflowData);
|
||||
if (!parsed.success) {
|
||||
throw new Error('Invalid workflow JSON: ' + JSON.stringify(parsed.error.issues));
|
||||
}
|
||||
const workflow = parsed.data;
|
||||
const name = workflow.name || 'Imported Project';
|
||||
const response = await createBaseProject(name, user);
|
||||
const name = 'Imported Project';
|
||||
const response = await createBaseProject(name, user, workflow);
|
||||
if ('billingError' in response) {
|
||||
return response;
|
||||
}
|
||||
const projectId = response.id;
|
||||
const now = new Date().toISOString();
|
||||
await agentWorkflowsCollection.insertOne({
|
||||
...workflow,
|
||||
projectId,
|
||||
createdAt: now,
|
||||
lastUpdatedAt: now,
|
||||
name: workflow.name || 'Version 1',
|
||||
});
|
||||
return { id: projectId };
|
||||
}
|
||||
|
||||
export async function collectProjectTools(projectId: string): Promise<z.infer<typeof WorkflowTool>[]> {
|
||||
await projectAuthCheck(projectId);
|
||||
return libCollectProjectTools(projectId);
|
||||
}
|
||||
|
||||
export async function saveWorkflow(projectId: string, workflow: z.infer<typeof Workflow>) {
|
||||
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<typeof Workflow>) {
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -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<z.infer<typeof TestScenario>>[];
|
||||
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<WithStringId<z.infer<typeof TestScenario>> | 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<void> {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
await testScenariosCollection.deleteOne({
|
||||
_id: new ObjectId(scenarioId),
|
||||
projectId,
|
||||
});
|
||||
}
|
||||
|
||||
export async function createScenario(
|
||||
projectId: string,
|
||||
data: {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
): Promise<WithStringId<z.infer<typeof TestScenario>>> {
|
||||
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<void> {
|
||||
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<z.infer<typeof TestSimulation>>[];
|
||||
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<WithStringId<z.infer<typeof TestSimulation>> | 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<void> {
|
||||
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<WithStringId<z.infer<typeof TestSimulation>>> {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
const doc: z.infer<typeof TestSimulation> = {
|
||||
...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<void> {
|
||||
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<z.infer<typeof TestProfile>>[];
|
||||
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<WithStringId<z.infer<typeof TestProfile>> | 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<void> {
|
||||
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<WithStringId<z.infer<typeof TestProfile>>> {
|
||||
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<void> {
|
||||
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<z.infer<typeof TestRun>>[];
|
||||
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<WithStringId<z.infer<typeof TestRun>> | 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<void> {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
await testRunsCollection.deleteOne({
|
||||
_id: new ObjectId(runId),
|
||||
projectId,
|
||||
});
|
||||
}
|
||||
|
||||
export async function createRun(
|
||||
projectId: string,
|
||||
data: {
|
||||
simulationIds: string[];
|
||||
workflowId: string;
|
||||
}
|
||||
): Promise<WithStringId<z.infer<typeof TestRun>>> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<z.infer<typeof TestResult>>[];
|
||||
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<WithStringId<z.infer<typeof TestResult>> | 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<void> {
|
||||
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<WithStringId<z.infer<typeof TestResult>>> {
|
||||
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<void> {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
await testResultsCollection.updateOne(
|
||||
{
|
||||
_id: new ObjectId(resultId),
|
||||
projectId,
|
||||
},
|
||||
{
|
||||
$set: updates,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function getSimulationResult(
|
||||
projectId: string,
|
||||
runId: string,
|
||||
simulationId: string
|
||||
): Promise<WithStringId<z.infer<typeof TestResult>> | 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<WithStringId<z.infer<typeof TestSimulation>>[]> {
|
||||
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;
|
||||
}
|
||||
|
|
@ -90,13 +90,12 @@ async function saveTwilioConfig(params: z.infer<typeof TwilioConfigParams>): Pro
|
|||
found: existingConfig
|
||||
});
|
||||
|
||||
const configToSave = {
|
||||
const configToSave: z.infer<typeof TwilioConfig> = {
|
||||
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<typeof TwilioConfigParams>): 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<InboundConfigResponse> {
|
||||
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
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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<WithStringId<z.infer<typeof Workflow>>> {
|
||||
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<z.infer<typeof Workflow>>;
|
||||
return {
|
||||
...rest,
|
||||
_id: insertedId.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
export async function cloneWorkflow(projectId: string, workflowId: string): Promise<WithStringId<z.infer<typeof Workflow>>> {
|
||||
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<z.infer<typeof Workflow>>;
|
||||
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<typeof Workflow>) {
|
||||
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<z.infer<typeof Workflow>>;
|
||||
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<string | null> {
|
||||
await projectAuthCheck(projectId);
|
||||
const project = await projectsCollection.findOne({
|
||||
_id: projectId,
|
||||
});
|
||||
return project?.publishedWorkflowId || null;
|
||||
}
|
||||
|
||||
export async function fetchWorkflow(projectId: string, workflowId: string): Promise<WithStringId<z.infer<typeof Workflow>>> {
|
||||
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<z.infer<typeof Workflow>>)[];
|
||||
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<z.infer<typeof Workflow>> | 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<z.infer<typeof Workflow>>[] = 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,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue