mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-06 20:42:15 +02:00
Add aggregate result to db
This commit is contained in:
parent
6f022e3f1b
commit
bc8fa90525
6 changed files with 134 additions and 54 deletions
|
|
@ -1,11 +1,11 @@
|
||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { ObjectId } from "mongodb";
|
import { ObjectId } from "mongodb";
|
||||||
import { scenariosCollection, simulationRunsCollection, simulationResultsCollection } from "../lib/mongodb";
|
import { scenariosCollection, simulationRunsCollection, simulationResultsCollection, simulationAggregateResultsCollection } from "../lib/mongodb";
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { projectAuthCheck } from "./project_actions";
|
import { projectAuthCheck } from "./project_actions";
|
||||||
import { type WithStringId } from "../lib/types/types";
|
import { type WithStringId } from "../lib/types/types";
|
||||||
import { Scenario, SimulationRun, SimulationResult } from "../lib/types/testing_types";
|
import { Scenario, SimulationRun, SimulationResult, SimulationAggregateResult } from "../lib/types/testing_types";
|
||||||
import { SimulationScenarioData } from "../lib/types/testing_types";
|
import { SimulationScenarioData } from "../lib/types/testing_types";
|
||||||
|
|
||||||
export async function getScenarios(projectId: string): Promise<WithStringId<z.infer<typeof Scenario>>[]> {
|
export async function getScenarios(projectId: string): Promise<WithStringId<z.infer<typeof Scenario>>[]> {
|
||||||
|
|
@ -209,4 +209,45 @@ export async function createRunResult(
|
||||||
|
|
||||||
const insertResult = await simulationResultsCollection.insertOne(resultDoc);
|
const insertResult = await simulationResultsCollection.insertOne(resultDoc);
|
||||||
return insertResult.insertedId.toString();
|
return insertResult.insertedId.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAggregateResult(
|
||||||
|
projectId: string,
|
||||||
|
runId: string,
|
||||||
|
total: number,
|
||||||
|
pass: number,
|
||||||
|
fail: number
|
||||||
|
): Promise<void> {
|
||||||
|
await projectAuthCheck(projectId);
|
||||||
|
|
||||||
|
await simulationAggregateResultsCollection.insertOne({
|
||||||
|
projectId,
|
||||||
|
runId,
|
||||||
|
total,
|
||||||
|
pass,
|
||||||
|
fail,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAggregateResult(
|
||||||
|
projectId: string,
|
||||||
|
runId: string
|
||||||
|
): Promise<z.infer<typeof SimulationAggregateResult> | null> {
|
||||||
|
await projectAuthCheck(projectId);
|
||||||
|
|
||||||
|
const result = await simulationAggregateResultsCollection.findOne({
|
||||||
|
projectId,
|
||||||
|
runId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result) return null;
|
||||||
|
|
||||||
|
// Only include the fields defined in the schema
|
||||||
|
return {
|
||||||
|
projectId: result.projectId,
|
||||||
|
runId: result.runId,
|
||||||
|
total: result.total,
|
||||||
|
pass: result.pass,
|
||||||
|
fail: result.fail
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -7,7 +7,7 @@ import { Project } from "./types/project_types";
|
||||||
import { EmbeddingDoc } from "./types/datasource_types";
|
import { EmbeddingDoc } from "./types/datasource_types";
|
||||||
import { DataSourceDoc } from "./types/datasource_types";
|
import { DataSourceDoc } from "./types/datasource_types";
|
||||||
import { DataSource } from "./types/datasource_types";
|
import { DataSource } from "./types/datasource_types";
|
||||||
import { Scenario, SimulationResult, SimulationRun } from "./types/testing_types";
|
import { Scenario, SimulationResult, SimulationRun, SimulationAggregateResult } from "./types/testing_types";
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const client = new MongoClient(process.env["MONGODB_CONNECTION_STRING"] || "mongodb://localhost:27017");
|
const client = new MongoClient(process.env["MONGODB_CONNECTION_STRING"] || "mongodb://localhost:27017");
|
||||||
|
|
@ -23,4 +23,5 @@ export const agentWorkflowsCollection = db.collection<z.infer<typeof Workflow>>(
|
||||||
export const scenariosCollection = db.collection<z.infer<typeof Scenario>>("scenarios");
|
export const scenariosCollection = db.collection<z.infer<typeof Scenario>>("scenarios");
|
||||||
export const apiKeysCollection = db.collection<z.infer<typeof ApiKey>>("api_keys");
|
export const apiKeysCollection = db.collection<z.infer<typeof ApiKey>>("api_keys");
|
||||||
export const simulationRunsCollection = db.collection<z.infer<typeof SimulationRun>>("simulation_runs");
|
export const simulationRunsCollection = db.collection<z.infer<typeof SimulationRun>>("simulation_runs");
|
||||||
export const simulationResultsCollection = db.collection<z.infer<typeof SimulationResult>>("simulation_results");
|
export const simulationResultsCollection = db.collection<z.infer<typeof SimulationResult>>("simulation_results");
|
||||||
|
export const simulationAggregateResultsCollection = db.collection<z.infer<typeof SimulationAggregateResult>>("simulation_aggregate_results");
|
||||||
|
|
@ -43,3 +43,10 @@ export const SimulationResult = z.object({
|
||||||
details: z.string()
|
details: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const SimulationAggregateResult = z.object({
|
||||||
|
projectId: z.string(),
|
||||||
|
runId: z.string(),
|
||||||
|
total: z.number(),
|
||||||
|
pass: z.number(),
|
||||||
|
fail: z.number(),
|
||||||
|
});
|
||||||
|
|
@ -14,6 +14,8 @@ import {
|
||||||
createRun,
|
createRun,
|
||||||
createRunResult,
|
createRunResult,
|
||||||
updateRunStatus,
|
updateRunStatus,
|
||||||
|
createAggregateResult,
|
||||||
|
getAggregateResult,
|
||||||
} from '../../../actions/simulation_actions';
|
} from '../../../actions/simulation_actions';
|
||||||
import { type WithStringId } from '../../../lib/types/types';
|
import { type WithStringId } from '../../../lib/types/types';
|
||||||
import { Scenario, SimulationRun, SimulationResult } from "../../../lib/types/testing_types";
|
import { Scenario, SimulationRun, SimulationResult } from "../../../lib/types/testing_types";
|
||||||
|
|
@ -37,18 +39,16 @@ const dummySimulator = async (scenario: ScenarioType, runId: string, projectId:
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
const passed = Math.random() > 0.5;
|
const passed = Math.random() > 0.5;
|
||||||
|
|
||||||
// Create the result object with explicitly typed result
|
|
||||||
const result: z.infer<typeof SimulationResult> = {
|
const result: z.infer<typeof SimulationResult> = {
|
||||||
projectId: projectId,
|
projectId: projectId,
|
||||||
runId: runId,
|
runId: runId,
|
||||||
scenarioId: scenario._id,
|
scenarioId: scenario._id,
|
||||||
result: passed ? 'pass' : 'fail' as const, // explicitly type as literal
|
result: passed ? 'pass' : 'fail' as const,
|
||||||
details: passed
|
details: passed
|
||||||
? "The bot successfully completed the conversation"
|
? "The bot successfully completed the conversation"
|
||||||
: "The bot could not handle the conversation",
|
: "The bot could not handle the conversation",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Write to DB using server action
|
|
||||||
await createRunResult(
|
await createRunResult(
|
||||||
projectId,
|
projectId,
|
||||||
runId,
|
runId,
|
||||||
|
|
@ -207,30 +207,40 @@ export default function SimulationApp() {
|
||||||
setSimulationReport(null);
|
setSimulationReport(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create a new run using server action
|
|
||||||
const newRun = await createRun(
|
const newRun = await createRun(
|
||||||
projectId as string,
|
projectId as string,
|
||||||
scenarios.map(s => s._id)
|
scenarios.map(s => s._id)
|
||||||
);
|
);
|
||||||
setActiveRun(newRun);
|
setActiveRun(newRun);
|
||||||
|
|
||||||
// Safely check for mock simulation flag
|
|
||||||
const shouldMock = process.env.NEXT_PUBLIC_MOCK_SIMULATION_RESULTS === 'true';
|
const shouldMock = process.env.NEXT_PUBLIC_MOCK_SIMULATION_RESULTS === 'true';
|
||||||
|
|
||||||
if (shouldMock) {
|
if (shouldMock) {
|
||||||
console.log('Using mock simulation...'); // Debug log
|
console.log('Using mock simulation...');
|
||||||
|
|
||||||
// First update run to 'running' status
|
|
||||||
await updateRunStatus(projectId as string, newRun._id, 'running');
|
await updateRunStatus(projectId as string, newRun._id, 'running');
|
||||||
|
|
||||||
// Generate and save all mock results
|
// Run all scenarios and collect results
|
||||||
await Promise.all(
|
const mockResults = await Promise.all(
|
||||||
scenarios.map(scenario =>
|
scenarios.map(scenario =>
|
||||||
dummySimulator(scenario, newRun._id, projectId as string)
|
dummySimulator(scenario, newRun._id, projectId as string)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update run status to completed
|
// Calculate aggregate results
|
||||||
|
const total = scenarios.length;
|
||||||
|
const pass = mockResults.filter(r => r.result === 'pass').length;
|
||||||
|
const fail = mockResults.filter(r => r.result === 'fail').length;
|
||||||
|
|
||||||
|
// Store aggregate results
|
||||||
|
await createAggregateResult(
|
||||||
|
projectId as string,
|
||||||
|
newRun._id,
|
||||||
|
total,
|
||||||
|
pass,
|
||||||
|
fail
|
||||||
|
);
|
||||||
|
|
||||||
await updateRunStatus(
|
await updateRunStatus(
|
||||||
projectId as string,
|
projectId as string,
|
||||||
newRun._id,
|
newRun._id,
|
||||||
|
|
@ -238,11 +248,9 @@ export default function SimulationApp() {
|
||||||
new Date().toISOString()
|
new Date().toISOString()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fetch the results back from DB to ensure consistency
|
|
||||||
const results = await getRunResults(projectId as string, newRun._id);
|
const results = await getRunResults(projectId as string, newRun._id);
|
||||||
setRunResults(results);
|
setRunResults(results);
|
||||||
|
|
||||||
// Refresh the run to get its updated state
|
|
||||||
const updatedRun = await getRun(projectId as string, newRun._id);
|
const updatedRun = await getRun(projectId as string, newRun._id);
|
||||||
setActiveRun(updatedRun);
|
setActiveRun(updatedRun);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
|
import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
|
||||||
import { WithStringId } from '../../../../lib/types/types';
|
import { WithStringId } from '../../../../lib/types/types';
|
||||||
import { Scenario, SimulationRun, SimulationResult } from "../../../../lib/types/testing_types";
|
import { Scenario, SimulationRun, SimulationResult, SimulationAggregateResult } from "../../../../lib/types/testing_types";
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { getAggregateResult } from '../../../../actions/simulation_actions';
|
||||||
|
|
||||||
type ScenarioType = WithStringId<z.infer<typeof Scenario>>;
|
type ScenarioType = WithStringId<z.infer<typeof Scenario>>;
|
||||||
type SimulationRunType = WithStringId<z.infer<typeof SimulationRun>>;
|
type SimulationRunType = WithStringId<z.infer<typeof SimulationRun>>;
|
||||||
|
|
@ -19,6 +20,15 @@ interface SimulationResultCardProps {
|
||||||
export const SimulationResultCard = ({ run, results, scenarios }: SimulationResultCardProps) => {
|
export const SimulationResultCard = ({ run, results, scenarios }: SimulationResultCardProps) => {
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
const [expandedScenarios, setExpandedScenarios] = useState<Set<string>>(new Set());
|
const [expandedScenarios, setExpandedScenarios] = useState<Set<string>>(new Set());
|
||||||
|
const [aggregateResult, setAggregateResult] = useState<z.infer<typeof SimulationAggregateResult> | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (run.projectId && run._id) {
|
||||||
|
getAggregateResult(run.projectId, run._id)
|
||||||
|
.then(setAggregateResult)
|
||||||
|
.catch(console.error);
|
||||||
|
}
|
||||||
|
}, [run.projectId, run._id]);
|
||||||
|
|
||||||
const statusLabelClass = "px-3 py-1 rounded text-xs min-w-[60px] text-center uppercase font-semibold";
|
const statusLabelClass = "px-3 py-1 rounded text-xs min-w-[60px] text-center uppercase font-semibold";
|
||||||
|
|
||||||
|
|
@ -46,10 +56,10 @@ export const SimulationResultCard = ({ run, results, scenarios }: SimulationResu
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calculate statistics and duration
|
// Replace the manual calculations with aggregate results
|
||||||
const totalScenarios = run.scenarioIds.length;
|
const totalScenarios = aggregateResult?.total ?? run.scenarioIds.length;
|
||||||
const passedScenarios = results.filter(r => r.result === 'pass').length;
|
const passedScenarios = aggregateResult?.pass ?? 0;
|
||||||
const failedScenarios = results.filter(r => r.result === 'fail').length;
|
const failedScenarios = aggregateResult?.fail ?? 0;
|
||||||
|
|
||||||
const getDuration = () => {
|
const getDuration = () => {
|
||||||
if (!run.completedAt) return 'In Progress';
|
if (!run.completedAt) return 'In Progress';
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { PencilIcon, XMarkIcon, DocumentDuplicateIcon } from '@heroicons/react/24/outline';
|
import { XMarkIcon } from '@heroicons/react/24/outline';
|
||||||
import { WithStringId } from '../../../../lib/types/types';
|
import { WithStringId } from '../../../../lib/types/types';
|
||||||
import { Scenario } from "../../../../lib/types/testing_types";
|
import { Scenario } from "../../../../lib/types/testing_types";
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
@ -15,31 +15,44 @@ interface ScenarioViewerProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ScenarioViewer({ scenario, onSave, onClose }: ScenarioViewerProps) {
|
export function ScenarioViewer({ scenario, onSave, onClose }: ScenarioViewerProps) {
|
||||||
const [name, setName] = useState(scenario.name);
|
const [editedScenario, setEditedScenario] = useState<ScenarioType>(scenario);
|
||||||
const [description, setDescription] = useState(scenario.description);
|
const [saveTimeout, setSaveTimeout] = useState<NodeJS.Timeout | null>(null);
|
||||||
const [criteria, setCriteria] = useState(scenario.criteria || '');
|
|
||||||
const [context, setContext] = useState(scenario.context || '');
|
|
||||||
|
|
||||||
// Replace the existing useEffect with this debounced version
|
// Reset state when scenario changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timeoutId = setTimeout(() => {
|
setEditedScenario(scenario);
|
||||||
// Only save if any value has actually changed
|
}, [scenario]);
|
||||||
if (name !== scenario.name ||
|
|
||||||
description !== scenario.description ||
|
|
||||||
criteria !== scenario.criteria ||
|
|
||||||
context !== scenario.context) {
|
|
||||||
onSave({
|
|
||||||
...scenario,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
criteria,
|
|
||||||
context,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 500); // 500ms debounce
|
|
||||||
|
|
||||||
return () => clearTimeout(timeoutId);
|
const handleChange = useCallback((field: keyof ScenarioType, value: string) => {
|
||||||
}, [name, description, criteria, context, onSave, scenario]);
|
setEditedScenario(prev => ({
|
||||||
|
...prev,
|
||||||
|
[field]: value,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Clear existing timeout
|
||||||
|
if (saveTimeout) {
|
||||||
|
clearTimeout(saveTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new timeout
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
onSave({
|
||||||
|
...editedScenario,
|
||||||
|
[field]: value,
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
setSaveTimeout(timeoutId);
|
||||||
|
}, [editedScenario, onSave, saveTimeout]);
|
||||||
|
|
||||||
|
// Cleanup timeout on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (saveTimeout) {
|
||||||
|
clearTimeout(saveTimeout);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [saveTimeout]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -58,8 +71,8 @@ export function ScenarioViewer({ scenario, onSave, onClose }: ScenarioViewerProp
|
||||||
<div className="text-sm font-medium text-gray-500 uppercase tracking-wider mb-4">NAME</div>
|
<div className="text-sm font-medium text-gray-500 uppercase tracking-wider mb-4">NAME</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={name}
|
value={editedScenario.name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => handleChange('name', e.target.value)}
|
||||||
className="text-base border border-gray-200 rounded px-2 py-1 hover:border-gray-300 focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
|
className="text-base border border-gray-200 rounded px-2 py-1 hover:border-gray-300 focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -69,8 +82,8 @@ export function ScenarioViewer({ scenario, onSave, onClose }: ScenarioViewerProp
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="text-sm font-medium text-gray-500 uppercase tracking-wider mb-4">DESCRIPTION</div>
|
<div className="text-sm font-medium text-gray-500 uppercase tracking-wider mb-4">DESCRIPTION</div>
|
||||||
<textarea
|
<textarea
|
||||||
value={description}
|
value={editedScenario.description}
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
onChange={(e) => handleChange('description', e.target.value)}
|
||||||
className="text-base border border-gray-200 rounded px-2 py-1 hover:border-gray-300 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 min-h-[24px] resize-none"
|
className="text-base border border-gray-200 rounded px-2 py-1 hover:border-gray-300 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 min-h-[24px] resize-none"
|
||||||
style={{ height: 'auto', minHeight: '24px' }}
|
style={{ height: 'auto', minHeight: '24px' }}
|
||||||
onInput={(e) => {
|
onInput={(e) => {
|
||||||
|
|
@ -86,8 +99,8 @@ export function ScenarioViewer({ scenario, onSave, onClose }: ScenarioViewerProp
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="text-sm font-medium text-gray-500 uppercase tracking-wider mb-4">CRITERIA</div>
|
<div className="text-sm font-medium text-gray-500 uppercase tracking-wider mb-4">CRITERIA</div>
|
||||||
<textarea
|
<textarea
|
||||||
value={criteria}
|
value={editedScenario.criteria}
|
||||||
onChange={(e) => setCriteria(e.target.value)}
|
onChange={(e) => handleChange('criteria', e.target.value)}
|
||||||
className="text-base border border-gray-200 rounded px-2 py-1 hover:border-gray-300 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 min-h-[24px] resize-none"
|
className="text-base border border-gray-200 rounded px-2 py-1 hover:border-gray-300 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 min-h-[24px] resize-none"
|
||||||
style={{ height: 'auto', minHeight: '24px' }}
|
style={{ height: 'auto', minHeight: '24px' }}
|
||||||
onInput={(e) => {
|
onInput={(e) => {
|
||||||
|
|
@ -103,8 +116,8 @@ export function ScenarioViewer({ scenario, onSave, onClose }: ScenarioViewerProp
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="text-sm font-medium text-gray-500 uppercase tracking-wider mb-4">CONTEXT</div>
|
<div className="text-sm font-medium text-gray-500 uppercase tracking-wider mb-4">CONTEXT</div>
|
||||||
<textarea
|
<textarea
|
||||||
value={context}
|
value={editedScenario.context}
|
||||||
onChange={(e) => setContext(e.target.value)}
|
onChange={(e) => handleChange('context', e.target.value)}
|
||||||
className="text-base border border-gray-200 rounded px-2 py-1 hover:border-gray-300 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 min-h-[24px] resize-none"
|
className="text-base border border-gray-200 rounded px-2 py-1 hover:border-gray-300 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 min-h-[24px] resize-none"
|
||||||
style={{ height: 'auto', minHeight: '24px' }}
|
style={{ height: 'auto', minHeight: '24px' }}
|
||||||
onInput={(e) => {
|
onInput={(e) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue