diff --git a/apps/rowboat/app/actions/actions.ts b/apps/rowboat/app/actions/actions.ts index 6ebd5fd4..eb694019 100644 --- a/apps/rowboat/app/actions/actions.ts +++ b/apps/rowboat/app/actions/actions.ts @@ -99,13 +99,13 @@ export async function getAssistantResponse( }; } -export async function suggestToolResponse(toolId: string, projectId: string, messages: z.infer[], testProfile: z.infer): Promise { +export async function suggestToolResponse(toolId: string, projectId: string, messages: z.infer[], mockInstructions: string): Promise { await projectAuthCheck(projectId); if (!await check_query_limit(projectId)) { throw new QueryLimitError(); } - return await mockToolResponse(toolId, messages, testProfile); + return await mockToolResponse(toolId, messages, mockInstructions); } export async function getInformationTool( diff --git a/apps/rowboat/app/actions/project_actions.ts b/apps/rowboat/app/actions/project_actions.ts index c5016b70..286d79c2 100644 --- a/apps/rowboat/app/actions/project_actions.ts +++ b/apps/rowboat/app/actions/project_actions.ts @@ -41,7 +41,6 @@ export async function createProject(formData: FormData) { const projectId = crypto.randomUUID(); const chatClientId = crypto.randomBytes(16).toString('base64url'); const secret = crypto.randomBytes(32).toString('hex'); - const defaultTestProfileId = new ObjectId(); // create project await projectsCollection.insertOne({ @@ -54,7 +53,6 @@ export async function createProject(formData: FormData) { secret, nextWorkflowNumber: 1, testRunCounter: 0, - defaultTestProfileId: defaultTestProfileId.toString(), }); // add first workflow version @@ -70,17 +68,6 @@ export async function createProject(formData: FormData) { name: `Version 1`, }); - // add default test profile - await testProfilesCollection.insertOne({ - _id: defaultTestProfileId, - projectId, - name: "Default", - context: "", - mockTools: false, - createdAt: (new Date()).toISOString(), - lastUpdatedAt: (new Date()).toISOString(), - }); - // add user to project await projectMembersCollection.insertOne({ userId: user.sub, diff --git a/apps/rowboat/app/actions/testing_actions.ts b/apps/rowboat/app/actions/testing_actions.ts index 88f01142..c697b483 100644 --- a/apps/rowboat/app/actions/testing_actions.ts +++ b/apps/rowboat/app/actions/testing_actions.ts @@ -171,13 +171,13 @@ export async function createSimulation( data: { name: string; scenarioId: string; - profileId: string; + profileId: string | null; passCriteria: string; } ): Promise>> { await projectAuthCheck(projectId); - const doc = { + const doc: z.infer = { ...data, projectId, createdAt: new Date().toISOString(), @@ -196,7 +196,7 @@ export async function updateSimulation( updates: { name?: string; scenarioId?: string; - profileId?: string; + profileId?: string | null; passCriteria?: string; } ): Promise { @@ -245,34 +245,6 @@ export async function listProfiles( }; } -export async function getDefaultProfile(projectId: string): Promise> | null> { - await projectAuthCheck(projectId); - const project = await projectsCollection.findOne({ _id: projectId }); - if (!project) { - return null; - } - if (!project.defaultTestProfileId) { - // create a default profile - const profile = await createProfile(projectId, { - name: 'Default', - context: '', - mockTools: false, - mockPrompt: '', - }); - await setDefaultProfile(projectId, profile._id); - return profile; - } - return getProfile(projectId, project.defaultTestProfileId); -} - -export async function setDefaultProfile(projectId: string, profileId: string): Promise { - await projectAuthCheck(projectId); - await projectsCollection.updateOne( - { _id: projectId }, - { $set: { defaultTestProfileId: profileId } } - ); -} - export async function getProfile(projectId: string, profileId: string): Promise> | null> { await projectAuthCheck(projectId); diff --git a/apps/rowboat/app/api/v1/[projectId]/chat/route.ts b/apps/rowboat/app/api/v1/[projectId]/chat/route.ts index 1d7e8257..8047f40d 100644 --- a/apps/rowboat/app/api/v1/[projectId]/chat/route.ts +++ b/apps/rowboat/app/api/v1/[projectId]/chat/route.ts @@ -72,9 +72,9 @@ export async function POST( } // if test profile is provided in the request, use it - let profile: z.infer | null = null; + let testProfile: z.infer | null = null; if (result.data.testProfileId) { - const testProfile = await testProfilesCollection.findOne({ + testProfile = await testProfilesCollection.findOne({ projectId: projectId, _id: new ObjectId(result.data.testProfileId), }); @@ -82,34 +82,18 @@ export async function POST( logger.log(`Test profile ${result.data.testProfileId} not found for project ${projectId}`); return Response.json({ error: "Test profile not found" }, { status: 404 }); } - profile = testProfile; - } else { - // if no test profile is provided, use the default profile - const defaultProfile = await testProfilesCollection.findOne({ - projectId: projectId, - _id: new ObjectId(project.defaultTestProfileId), - }); - if (!defaultProfile) { - logger.log(`Default test profile not found for project ${projectId}`); - return Response.json({ error: "Default test profile not found" }, { status: 404 }); - } - profile = defaultProfile; - } - if (!profile) { - logger.log(`No test profile found for project ${projectId}`); - return Response.json({ error: "No test profile found" }, { status: 404 }); } // if profile has a context available, overwrite the system message in the request (if there is one) let currentMessages = reqMessages; - if (profile.context) { + if (testProfile?.context) { // if there is a system message, overwrite it const systemMessageIndex = reqMessages.findIndex(m => m.role === "system"); if (systemMessageIndex !== -1) { - currentMessages[systemMessageIndex].content = profile.context; + currentMessages[systemMessageIndex].content = testProfile.context; } else { // if there is no system message, add one - currentMessages.unshift({ role: "system", content: profile.context }); + currentMessages.unshift({ role: "system", content: testProfile.context }); } } @@ -183,9 +167,9 @@ export async function POST( try { // if tool is supposed to be mocked, mock it const workflowTool = workflow.tools.find(t => t.name === toolCall.function.name); - if (profile.mockTools) { + if (testProfile?.mockTools || workflowTool?.mockTool) { logger.log(`Mocking tool call ${toolCall.function.name}`); - result = await mockToolResponse(toolCall.id, currentMessages, profile); + result = await mockToolResponse(toolCall.id, currentMessages, testProfile?.mockPrompt || workflowTool?.mockInstructions || ''); } else { // else run the tool call by calling the client tool webhook logger.log(`Running client tool webhook for tool ${toolCall.function.name}`); diff --git a/apps/rowboat/app/lib/project_templates.ts b/apps/rowboat/app/lib/project_templates.ts index 505bffab..1a9427b1 100644 --- a/apps/rowboat/app/lib/project_templates.ts +++ b/apps/rowboat/app/lib/project_templates.ts @@ -147,7 +147,7 @@ You are an helpful customer support assistant "user_id" ] }, - "mockInPlayground": true, + "mockTool": true, "autoSubmitMockedResponse": true }, { @@ -165,7 +165,7 @@ You are an helpful customer support assistant "param1" ] }, - "mockInPlayground": true, + "mockTool": true, "autoSubmitMockedResponse": true } ], @@ -289,13 +289,13 @@ You are an helpful customer support assistant "orderId" ] }, - "mockInPlayground": true, + "mockTool": true, "autoSubmitMockedResponse": true }, { "name": "retrieve_snippet", "description": "This is a mock RAG service. Always return 2 paragraphs about a fictional scooter rental product, based on the query. Be verbose.", - "mockInPlayground": true, + "mockTool": true, "autoSubmitMockedResponse": true, "parameters": { "type": "object", @@ -325,7 +325,7 @@ You are an helpful customer support assistant "orderId" ] }, - "mockInPlayground": true, + "mockTool": true, "autoSubmitMockedResponse": true } ], diff --git a/apps/rowboat/app/lib/types/agents_api_types.ts b/apps/rowboat/app/lib/types/agents_api_types.ts index 42e9e950..5fd45758 100644 --- a/apps/rowboat/app/lib/types/agents_api_types.ts +++ b/apps/rowboat/app/lib/types/agents_api_types.ts @@ -44,7 +44,7 @@ export const AgenticAPIAgent = WorkflowAgent export const AgenticAPIPrompt = WorkflowPrompt; export const AgenticAPITool = WorkflowTool.omit({ - mockInPlayground: true, + mockTool: true, autoSubmitMockedResponse: true, }); @@ -91,7 +91,7 @@ export function convertWorkflowToAgenticAPI(workflow: z.infer): return agenticAgent; }), tools: workflow.tools.map(tool => { - const { mockInPlayground, autoSubmitMockedResponse, ...rest } = tool; + const { mockTool, autoSubmitMockedResponse, ...rest } = tool; return { ...rest, }; diff --git a/apps/rowboat/app/lib/types/project_types.ts b/apps/rowboat/app/lib/types/project_types.ts index b426ed6c..50df41b4 100644 --- a/apps/rowboat/app/lib/types/project_types.ts +++ b/apps/rowboat/app/lib/types/project_types.ts @@ -11,7 +11,6 @@ export const Project = z.object({ publishedWorkflowId: z.string().optional(), nextWorkflowNumber: z.number().optional(), testRunCounter: z.number().default(0), - defaultTestProfileId: z.string().optional(), });export const ProjectMember = z.object({ userId: z.string(), projectId: z.string(), diff --git a/apps/rowboat/app/lib/types/testing_types.ts b/apps/rowboat/app/lib/types/testing_types.ts index 4a1440bb..ff544517 100644 --- a/apps/rowboat/app/lib/types/testing_types.ts +++ b/apps/rowboat/app/lib/types/testing_types.ts @@ -22,7 +22,7 @@ export const TestSimulation = z.object({ projectId: z.string(), name: z.string().min(1, "Name cannot be empty"), scenarioId: z.string(), - profileId: z.string(), + profileId: z.string().nullable(), passCriteria: z.string(), createdAt: z.string().datetime(), lastUpdatedAt: z.string().datetime(), diff --git a/apps/rowboat/app/lib/types/workflow_types.ts b/apps/rowboat/app/lib/types/workflow_types.ts index 46554c36..160fc645 100644 --- a/apps/rowboat/app/lib/types/workflow_types.ts +++ b/apps/rowboat/app/lib/types/workflow_types.ts @@ -33,8 +33,9 @@ export const WorkflowPrompt = z.object({ export const WorkflowTool = z.object({ name: z.string(), description: z.string(), - mockInPlayground: z.boolean().default(false).optional(), + mockTool: z.boolean().default(false).optional(), autoSubmitMockedResponse: z.boolean().default(false).optional(), + mockInstructions: z.string().optional(), parameters: z.object({ type: z.literal('object'), properties: z.record(z.object({ diff --git a/apps/rowboat/app/lib/utils.ts b/apps/rowboat/app/lib/utils.ts index f371eb74..d72088c2 100644 --- a/apps/rowboat/app/lib/utils.ts +++ b/apps/rowboat/app/lib/utils.ts @@ -221,7 +221,7 @@ export class PrefixLogger { } } -export async function mockToolResponse(toolId: string, messages: z.infer[], testProfile: z.infer): Promise { +export async function mockToolResponse(toolId: string, messages: z.infer[], mockInstructions: string): Promise { const prompt = `Given below is a chat between a user and a customer support assistant. The assistant has requested a tool call with ID {{toolID}}. @@ -234,10 +234,6 @@ and also some instructions on how to mock the tool call. {{messages}} <<>>CONTEXT -{{context}} -<<>>MOCK_INSTRUCTIONS {{mockInstructions}} << { let tool_calls; if ('tool_calls' in m && m.role == 'assistant') { diff --git a/apps/rowboat/app/projects/[projectId]/playground/app.tsx b/apps/rowboat/app/projects/[projectId]/playground/app.tsx index 0c27bd39..e46bf6d6 100644 --- a/apps/rowboat/app/projects/[projectId]/playground/app.tsx +++ b/apps/rowboat/app/projects/[projectId]/playground/app.tsx @@ -24,16 +24,14 @@ export function App({ projectId, workflow, messageSubscriber, - initialTestProfile, }: { hidden?: boolean; projectId: string; workflow: z.infer; messageSubscriber?: (messages: z.infer[]) => void; - initialTestProfile: z.infer; }) { const [counter, setCounter] = useState(0); - const [testProfile, setTestProfile] = useState>(initialTestProfile); + const [testProfile, setTestProfile] = useState | null>(null); const [chat, setChat] = useState>({ projectId, createdAt: new Date().toISOString(), @@ -42,7 +40,7 @@ export function App({ systemMessage: defaultSystemMessage, }); - function handleTestProfileChange(profile: WithStringId>) { + function handleTestProfileChange(profile: WithStringId> | null) { setTestProfile(profile); setCounter(counter + 1); } diff --git a/apps/rowboat/app/projects/[projectId]/playground/chat.tsx b/apps/rowboat/app/projects/[projectId]/playground/chat.tsx index ea9bf9fa..3e348668 100644 --- a/apps/rowboat/app/projects/[projectId]/playground/chat.tsx +++ b/apps/rowboat/app/projects/[projectId]/playground/chat.tsx @@ -15,21 +15,22 @@ import { CopyAsJsonButton } from "./copy-as-json-button"; import { TestProfile } from "@/app/lib/types/testing_types"; import { ProfileSelector } from "@/app/lib/components/selectors/profile-selector"; import { WithStringId } from "@/app/lib/types/types"; +import { XCircleIcon, XIcon } from "lucide-react"; export function Chat({ chat, projectId, workflow, messageSubscriber, - testProfile, + testProfile=null, onTestProfileChange, }: { chat: z.infer; projectId: string; workflow: z.infer; messageSubscriber?: (messages: z.infer[]) => void; - testProfile: z.infer; - onTestProfileChange: (profile: WithStringId>) => void; + testProfile?: z.infer | null; + onTestProfileChange: (profile: WithStringId> | null) => void; }) { const [messages, setMessages] = useState[]>(chat.messages); const [loadingAssistantResponse, setLoadingAssistantResponse] = useState(false); @@ -41,7 +42,7 @@ export function Chat({ const [fetchResponseError, setFetchResponseError] = useState(null); const [lastAgenticRequest, setLastAgenticRequest] = useState(null); const [lastAgenticResponse, setLastAgenticResponse] = useState(null); - const [systemMessage, setSystemMessage] = useState(testProfile.context); + const [systemMessage, setSystemMessage] = useState(testProfile?.context); const [isProfileSelectorOpen, setIsProfileSelectorOpen] = useState(false); // collect published tool call results @@ -279,15 +280,20 @@ export function Chat({ return
-
+
+ {testProfile && + + }
['tool_calls']; results: Record>; @@ -103,7 +104,8 @@ function ToolCalls({ messages: z.infer[]; sender: string | null | undefined; workflow: z.infer; - testProfile: z.infer; + testProfile: z.infer | null; + systemMessage: string | undefined; }) { const resultsMap: Record> = {}; @@ -127,6 +129,7 @@ function ToolCalls({ sender={sender} workflow={workflow} testProfile={testProfile} + systemMessage={systemMessage} /> })}
; @@ -140,7 +143,8 @@ function ToolCall({ messages, sender, workflow, - testProfile, + testProfile=null, + systemMessage, }: { toolCall: z.infer['tool_calls'][number]; result: z.infer | undefined; @@ -149,7 +153,8 @@ function ToolCall({ messages: z.infer[]; sender: string | null | undefined; workflow: z.infer; - testProfile: z.infer; + testProfile: z.infer | null; + systemMessage: string | undefined; }) { let matchingWorkflowTool: z.infer | undefined; for (const tool of workflow.tools) { @@ -181,7 +186,7 @@ function ToolCall({ sender={sender} />; } - if (matchingWorkflowTool && !testProfile.mockTools) { + if (matchingWorkflowTool && testProfile && !testProfile.mockTools) { return ; } } @@ -413,8 +419,9 @@ function MockToolCall({ projectId, messages, sender, - autoSubmit = false, - testProfile, + testProfile=null, + workflowTool, + systemMessage, }: { toolCall: z.infer['tool_calls'][number]; result: z.infer | undefined; @@ -422,8 +429,9 @@ function MockToolCall({ projectId: string; messages: z.infer[]; sender: string | null | undefined; - autoSubmit?: boolean; - testProfile: z.infer; + testProfile: z.infer | null; + workflowTool: z.infer | undefined; + systemMessage: string | undefined; }) { const [result, setResult] = useState | undefined>(availableResult); const [response, setResponse] = useState(''); @@ -459,7 +467,18 @@ function MockToolCall({ async function process() { setGeneratingResponse(true); - const response = await suggestToolResponse(toolCall.id, projectId, messages, testProfile); + const response = await suggestToolResponse( + toolCall.id, + projectId, + [{ + role: 'system', + content: systemMessage || '', + createdAt: new Date().toISOString(), + version: 'v1', + chatId: '', + }, ...messages], + testProfile?.mockPrompt || workflowTool?.mockInstructions || '', + ); if (ignore) { return; } @@ -471,11 +490,11 @@ function MockToolCall({ return () => { ignore = true; }; - }, [result, response, toolCall.id, projectId, messages, testProfile]); + }, [result, response, toolCall.id, projectId, messages, testProfile, systemMessage, workflowTool?.mockInstructions]); // auto submit if autoSubmitMockedResponse is true useEffect(() => { - if (!autoSubmit) { + if (!workflowTool?.autoSubmitMockedResponse) { return; } if (result) { @@ -484,13 +503,14 @@ function MockToolCall({ if (response) { handleSubmit(); } - }, [autoSubmit, response, handleSubmit, result]); + }, [workflowTool?.autoSubmitMockedResponse, response, handleSubmit, result]); return
{sender &&
{sender}
}
- + {!result && } + {result && } Function Call: {toolCall.function.name} @@ -501,7 +521,7 @@ function MockToolCall({ {result && }
- {!result && !autoSubmit &&
+ {!result && !workflowTool?.autoSubmitMockedResponse &&
Response: