Add aggregate result to db

This commit is contained in:
akhisud3195 2025-02-14 17:25:22 +05:30
parent 6f022e3f1b
commit bc8fa90525
6 changed files with 134 additions and 54 deletions

View file

@ -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
};
} }

View file

@ -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");

View file

@ -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(),
});

View file

@ -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);
} }

View file

@ -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';

View file

@ -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) => {