Refactor agents api integration

This commit is contained in:
ramnique 2025-03-20 17:05:13 +05:30 committed by Ramnique Singh
parent 0e31098d58
commit a02c830fb0
14 changed files with 131 additions and 1121 deletions

View file

@ -1,26 +1,16 @@
'use server';
import { convertFromAgenticAPIChatMessages } from "../lib/types/agents_api_types";
import { AgenticAPIChatRequest } from "../lib/types/agents_api_types";
import { WorkflowAgent } from "../lib/types/workflow_types";
import { EmbeddingRecord } from "../lib/types/datasource_types";
import { WebpageCrawlResponse } from "../lib/types/tool_types";
import { GetInformationToolResult } from "../lib/types/tool_types";
import { EmbeddingDoc } from "../lib/types/datasource_types";
import { generateObject, generateText, embed } from "ai";
import { dataSourceDocsCollection, dataSourcesCollection, embeddingsCollection, webpagesCollection } from "../lib/mongodb";
import { webpagesCollection } from "../lib/mongodb";
import { z } from 'zod';
import { openai } from "@ai-sdk/openai";
import FirecrawlApp, { ScrapeResponse } from '@mendable/firecrawl-js';
import { embeddingModel } from "../lib/embedding";
import { apiV1 } from "rowboat-shared";
import { Claims, getSession } from "@auth0/nextjs-auth0";
import { callClientToolWebhook, getAgenticApiResponse, mockToolResponse, runRAGToolCall } from "../lib/utils";
import { getAgenticApiResponse } from "../lib/utils";
import { check_query_limit } from "../lib/rate_limiting";
import { QueryLimitError } from "../lib/client_utils";
import { projectAuthCheck } from "./project_actions";
import { qdrantClient } from "../lib/qdrant";
import { ObjectId } from "mongodb";
import { TestProfile } from "../lib/types/testing_types";
const crawler = new FirecrawlApp({ apiKey: process.env.FIRECRAWL_API_KEY || '' });
@ -98,96 +88,3 @@ export async function getAssistantResponse(
rawResponse: response.rawAPIResponse,
};
}
export async function suggestToolResponse(toolId: string, projectId: string, messages: z.infer<typeof apiV1.ChatMessage>[], mockInstructions: string): Promise<string> {
await projectAuthCheck(projectId);
if (!await check_query_limit(projectId)) {
throw new QueryLimitError();
}
return await mockToolResponse(toolId, messages, mockInstructions);
}
export async function getInformationTool(
projectId: string,
query: string,
sourceIds: string[],
returnType: z.infer<typeof WorkflowAgent>['ragReturnType'],
k: number,
): Promise<z.infer<typeof GetInformationToolResult>> {
await projectAuthCheck(projectId);
return await runRAGToolCall(projectId, query, sourceIds, returnType, k);
}
export async function simulateUserResponse(
projectId: string,
messages: z.infer<typeof apiV1.ChatMessage>[],
scenario: string,
): Promise<string> {
await projectAuthCheck(projectId);
if (!await check_query_limit(projectId)) {
throw new QueryLimitError();
}
const scenarioPrompt = `
# Your Specific Task:
## Context:
Here is a scenario:
Scenario:
<START_SCENARIO>
{{scenario}}
<END_SCENARIO>
## Task definition:
Pretend to be a user reaching out to customer support. Chat with the
customer support assistant, assuming your issue is based on this scenario.
Ask follow-up questions and make it real-world like. Don't do dummy
conversations. Your conversation should be a maximum of 5 user turns.
As output, simply provide your (user) turn of conversation.
After you are done with the chat, keep replying with a single word EXIT
in all capitals.
`;
await projectAuthCheck(projectId);
// flip message assistant / user message
// roles from chat messages
// use only text response messages
const flippedMessages: { role: 'user' | 'assistant', content: string }[] = messages
.filter(m => m.role == 'assistant' || m.role == 'user')
.map(m => ({
role: m.role == 'assistant' ? 'user' : 'assistant',
content: m.content || '',
}));
// simulate user call
let prompt;
prompt = scenarioPrompt
.replace('{{scenario}}', scenario);
const { text } = await generateText({
model: openai("gpt-4o"),
system: prompt || '',
messages: flippedMessages,
});
return text.replace(/\. EXIT$/, '.');
}
export async function executeClientTool(
toolCall: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'][number],
messages: z.infer<typeof apiV1.ChatMessage>[],
projectId: string,
): Promise<unknown> {
await projectAuthCheck(projectId);
const result = await callClientToolWebhook(toolCall, messages, projectId);
return result;
}

View file

@ -4,7 +4,6 @@ import { WorkflowTool } from "../lib/types/workflow_types";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { projectAuthCheck } from "./project_actions";
import { callMcpTool } from "../lib/utils";
import { projectsCollection } from "../lib/mongodb";
import { Project } from "../lib/types/project_types";
@ -72,11 +71,4 @@ export async function updateMcpServers(projectId: string, mcpServers: z.infer<ty
await projectsCollection.updateOne({
_id: projectId,
}, { $set: { mcpServers } });
}
export async function executeMcpTool(projectId: string, mcpServerName: string, toolName: string, parameters: Record<string, unknown>): Promise<unknown> {
await projectAuthCheck(projectId);
const result = await callMcpTool(projectId, mcpServerName, toolName, parameters);
return result;
}

View file

@ -4,10 +4,9 @@ import { z } from "zod";
import { ObjectId } from "mongodb";
import { authCheck } from "../../utils";
import { ApiRequest, ApiResponse } from "../../../../lib/types/types";
import { AgenticAPIChatRequest, AgenticAPIChatMessage, convertFromAgenticApiToApiMessages, convertFromApiToAgenticApiMessages, convertWorkflowToAgenticAPI } from "../../../../lib/types/agents_api_types";
import { getAgenticApiResponse, callClientToolWebhook, runRAGToolCall, mockToolResponse, callMcpTool } from "../../../../lib/utils";
import { AgenticAPIChatRequest, convertFromAgenticApiToApiMessages, convertFromApiToAgenticApiMessages, convertWorkflowToAgenticAPI } from "../../../../lib/types/agents_api_types";
import { getAgenticApiResponse } from "../../../../lib/utils";
import { check_query_limit } from "../../../../lib/rate_limiting";
import { apiV1 } from "rowboat-shared";
import { PrefixLogger } from "../../../../lib/utils";
import { TestProfile } from "@/app/lib/types/testing_types";
@ -70,7 +69,7 @@ export async function POST(
logger.log(`Workflow ${workflowId} not found for project ${projectId}`);
return Response.json({ error: "Workflow not found" }, { status: 404 });
}
// if test profile is provided in the request, use it
let testProfile: z.infer<typeof TestProfile> | null = null;
if (result.data.testProfileId) {
@ -84,138 +83,29 @@ export async function POST(
}
}
// if profile has a context available, overwrite the system message in the request (if there is one)
let currentMessages = reqMessages;
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 = testProfile.context;
} else {
// if there is no system message, add one
currentMessages.unshift({ role: "system", content: testProfile.context });
}
}
const MAX_TURNS = result.data.maxTurns ?? 3;
let currentState: unknown = reqState ?? { last_agent_name: workflow.agents[0].name };
let turns = 0;
let hasToolCalls = false;
do {
hasToolCalls = false;
// get assistant response
const { agents, tools, prompts, startAgent } = convertWorkflowToAgenticAPI(workflow);
const request: z.infer<typeof AgenticAPIChatRequest> = {
messages: convertFromApiToAgenticApiMessages(currentMessages),
state: currentState,
agents,
tools,
prompts,
startAgent,
};
// get assistant response
const { agents, tools, prompts, startAgent } = convertWorkflowToAgenticAPI(workflow);
const request: z.infer<typeof AgenticAPIChatRequest> = {
messages: convertFromApiToAgenticApiMessages(reqMessages),
state: currentState,
agents,
tools,
prompts,
startAgent,
testProfile: testProfile ?? undefined,
mcpServers: project.mcpServers ?? undefined,
toolWebhookUrl: project.webhookUrl ?? undefined,
};
console.log(`turn ${turns}: sending agentic request from /chat api`, JSON.stringify(request, null, 2));
logger.log(`Processing turn ${turns} for conversation`);
const { messages: agenticMessages, state } = await getAgenticApiResponse(request);
const newMessages = convertFromAgenticApiToApiMessages(agenticMessages);
currentState = state;
// if tool calls are to be skipped, return immediately
if (result.data.skipToolCalls) {
logger.log('Skipping tool calls as requested');
const responseBody: z.infer<typeof ApiResponse> = {
messages: newMessages,
state: currentState,
};
return Response.json(responseBody);
}
// get last message to check for tool calls
const lastMessage = newMessages[newMessages.length - 1];
if (lastMessage?.role === "assistant" &&
'tool_calls' in lastMessage &&
lastMessage.tool_calls?.length > 0) {
hasToolCalls = true;
const toolCallResultMessages: z.infer<typeof apiV1.ToolMessage>[] = [];
// Process tool calls
for (const toolCall of lastMessage.tool_calls) {
let result: unknown;
if (toolCall.function.name === "getArticleInfo") {
logger.log(`Running RAG tool call for agent ${lastMessage.agenticSender}`);
// find the source ids attached to this agent in the workflow
const agent = workflow.agents.find(a => a.name === lastMessage.agenticSender);
if (!agent) {
return Response.json({ error: "Agent not found" }, { status: 404 });
}
const sourceIds = agent.ragDataSources;
if (!sourceIds) {
return Response.json({ error: "Agent has no data sources" }, { status: 404 });
}
try {
result = await runRAGToolCall(projectId, toolCall.function.arguments, sourceIds, agent.ragReturnType, agent.ragK);
logger.log(`RAG tool call completed for agent ${lastMessage.agenticSender}`);
} catch (e) {
logger.log(`Error running RAG tool call: ${e}`);
return Response.json({ error: "Error running RAG tool call" }, { status: 500 });
}
} else {
logger.log(`Processing tool call ${toolCall.function.name}`);
try {
// if tool is supposed to be mocked, mock it
const workflowTool = workflow.tools.find(t => t.name === toolCall.function.name);
if (testProfile?.mockTools || workflowTool?.mockTool) {
logger.log(`Mocking tool call ${toolCall.function.name}`);
result = await mockToolResponse(toolCall.id, currentMessages, testProfile?.mockPrompt || workflowTool?.mockInstructions || '');
} else if (workflowTool?.isMcp) {
// else run the tool call by calling the MCP tool
logger.log(`Calling MCP tool: ${toolCall.function.name}`);
result = await callMcpTool(projectId, workflowTool.mcpServerName ?? 'default', toolCall.function.name, JSON.parse(toolCall.function.arguments));
} else {
// else run the tool call by calling the client tool webhook
logger.log(`Running client tool webhook for tool ${toolCall.function.name}`);
result = await callClientToolWebhook(
toolCall,
currentMessages,
projectId,
);
}
} catch (e) {
logger.log(`Error in tool call ${toolCall.function.name}: ${e}`);
return Response.json({ error: `Error in tool call ${toolCall.function.name}` }, { status: 500 });
}
logger.log(`Tool call ${toolCall.function.name} completed`);
}
toolCallResultMessages.push({
role: "tool",
tool_call_id: toolCall.id,
content: JSON.stringify(result),
tool_name: toolCall.function.name,
});
}
// Add new messages to the conversation
currentMessages = [...currentMessages, ...newMessages, ...toolCallResultMessages];
} else {
// No tool calls, just add the new messages
currentMessages = [...currentMessages, ...newMessages];
}
turns++;
if (turns >= MAX_TURNS && hasToolCalls) {
logger.log(`Max turns (${MAX_TURNS}) reached for conversation`);
return Response.json({ error: "Max turns reached" }, { status: 429 });
}
} while (hasToolCalls);
const { messages: agenticMessages, state } = await getAgenticApiResponse(request);
const newMessages = convertFromAgenticApiToApiMessages(agenticMessages);
const newState = state;
const responseBody: z.infer<typeof ApiResponse> = {
messages: currentMessages.slice(reqMessages.length),
state: currentState,
messages: newMessages,
state: newState,
};
return Response.json(responseBody);
});

View file

@ -8,13 +8,10 @@ import { convertFromAgenticAPIChatMessages } from "../../../../../../lib/types/a
import { convertToAgenticAPIChatMessages } from "../../../../../../lib/types/agents_api_types";
import { convertWorkflowToAgenticAPI } from "../../../../../../lib/types/agents_api_types";
import { AgenticAPIChatRequest } from "../../../../../../lib/types/agents_api_types";
import { callClientToolWebhook, getAgenticApiResponse, runRAGToolCall, mockToolResponse, callMcpTool } from "../../../../../../lib/utils";
import { getAgenticApiResponse } from "../../../../../../lib/utils";
import { check_query_limit } from "../../../../../../lib/rate_limiting";
import { PrefixLogger } from "../../../../../../lib/utils";
// Add max turns constant at the top with other constants
const MAX_TURNS = 3;
// get next turn / agent response
export async function POST(
req: NextRequest,
@ -23,7 +20,7 @@ export async function POST(
return await authCheck(req, async (session) => {
const { chatId } = await params;
const logger = new PrefixLogger(`widget-chat:${chatId}`);
logger.log(`Processing turn request for chat ${chatId}`);
// check query limit
@ -95,109 +92,32 @@ export async function POST(
// get assistant response
const { agents, tools, prompts, startAgent } = convertWorkflowToAgenticAPI(workflow);
const unsavedMessages: z.infer<typeof apiV1.ChatMessage>[] = [userMessage];
let resolvingToolCalls = true;
let state: unknown = chat.agenticState ?? {last_agent_name: startAgent};
let turns = 0; // Add turns counter
let state: unknown = chat.agenticState ?? { last_agent_name: startAgent };
while (resolvingToolCalls) {
if (turns >= MAX_TURNS) {
logger.log(`Max turns (${MAX_TURNS}) reached for chat ${chatId}`);
throw new Error("Max turns reached");
}
turns++;
const request: z.infer<typeof AgenticAPIChatRequest> = {
messages: convertToAgenticAPIChatMessages([systemMessage, ...messages, ...unsavedMessages]),
state,
agents,
tools,
prompts,
startAgent,
};
logger.log(`Turn ${turns}: sending agentic request`);
const response = await getAgenticApiResponse(request);
state = response.state;
if (response.messages.length === 0) {
throw new Error("No messages returned from assistant");
}
const convertedMessages = convertFromAgenticAPIChatMessages(response.messages);
unsavedMessages.push(...convertedMessages.map(m => ({
...m,
version: 'v1' as const,
chatId,
createdAt: new Date().toISOString(),
})));
// if the last messages is tool call, execute them
const lastMessage = convertedMessages[convertedMessages.length - 1];
if (lastMessage.role === 'assistant' && 'tool_calls' in lastMessage) {
logger.log(`Processing ${lastMessage.tool_calls.length} tool calls`);
const toolCallResults = await Promise.all(lastMessage.tool_calls.map(async toolCall => {
logger.log(`Executing tool call: ${toolCall.function.name}`);
try {
if (toolCall.function.name === "getArticleInfo") {
logger.log(`Processing RAG tool call for agent ${lastMessage.agenticSender}`);
const agent = workflow.agents.find(a => a.name === lastMessage.agenticSender);
if (!agent || !agent.ragDataSources) {
throw new Error("Agent not found or has no data sources");
}
return await runRAGToolCall(
session.projectId,
toolCall.function.arguments,
agent.ragDataSources,
agent.ragReturnType,
agent.ragK
);
}
const workflowTool = workflow.tools.find(t => t.name === toolCall.function.name);
if (workflowTool?.mockTool) {
logger.log(`Using mock response for tool: ${toolCall.function.name}`);
return await mockToolResponse(
toolCall.id,
[...messages, ...unsavedMessages],
workflowTool.mockInstructions || ''
);
} else if (workflowTool?.isMcp) {
logger.log(`Calling MCP tool: ${toolCall.function.name}`);
return await callMcpTool(
session.projectId,
workflowTool.mcpServerName ?? 'default',
toolCall.function.name,
JSON.parse(toolCall.function.arguments)
);
} else {
logger.log(`Calling webhook for tool: ${toolCall.function.name}`);
return await callClientToolWebhook(
toolCall,
[...messages, ...unsavedMessages],
session.projectId,
);
}
} catch (error) {
logger.log(`Error executing tool call ${toolCall.id}: ${error}`);
return { error: "Tool execution failed" };
}
}));
unsavedMessages.push(...toolCallResults.map((result, index) => ({
version: 'v1' as const,
chatId,
createdAt: new Date().toISOString(),
role: 'tool' as const,
tool_call_id: lastMessage.tool_calls[index].id,
tool_name: lastMessage.tool_calls[index].function.name,
content: JSON.stringify(result),
})));
} else {
// ensure that the last message is from an assistant
// and is of an external type
if (lastMessage.role !== 'assistant' || lastMessage.agenticResponseType !== 'external') {
throw new Error("Last message is not from an assistant and is not of an external type");
}
resolvingToolCalls = false;
break;
}
const request: z.infer<typeof AgenticAPIChatRequest> = {
messages: convertToAgenticAPIChatMessages([systemMessage, ...messages, ...unsavedMessages]),
state,
agents,
tools,
prompts,
startAgent,
mcpServers: projectSettings.mcpServers ?? undefined,
toolWebhookUrl: projectSettings.webhookUrl ?? undefined,
testProfile: undefined,
};
logger.log(`Sending agentic request`);
const response = await getAgenticApiResponse(request);
state = response.state;
if (response.messages.length === 0) {
throw new Error("No messages returned from assistant");
}
const convertedMessages = convertFromAgenticAPIChatMessages(response.messages);
unsavedMessages.push(...convertedMessages.map(m => ({
...m,
version: 'v1' as const,
chatId,
createdAt: new Date().toISOString(),
})));
logger.log(`Saving ${unsavedMessages.length} new messages and updating chat state`);
await chatMessagesCollection.insertMany(unsavedMessages);

View file

@ -1,7 +1,9 @@
import { z } from "zod";
import { ConnectedEntity, sanitizeTextWithMentions, Workflow, WorkflowAgent, WorkflowPrompt, WorkflowTool } from "./workflow_types";
import { sanitizeTextWithMentions, Workflow, WorkflowAgent, WorkflowPrompt, WorkflowTool } from "./workflow_types";
import { apiV1 } from "rowboat-shared";
import { ApiMessage } from "./types";
import { TestProfile } from "./testing_types";
import { MCPServer } from "./types";
export const AgenticAPIChatMessage = z.object({
role: z.union([z.literal('user'), z.literal('assistant'), z.literal('tool'), z.literal('system')]),
@ -30,12 +32,8 @@ export const AgenticAPIAgent = WorkflowAgent
locked: true,
toggleAble: true,
global: true,
ragDataSources: true,
ragReturnType: true,
ragK: true,
})
.extend({
hasRagSources: z.boolean().default(false).optional(),
tools: z.array(z.string()),
prompts: z.array(z.string()),
connectedAgents: z.array(z.string()),
@ -43,10 +41,10 @@ export const AgenticAPIAgent = WorkflowAgent
export const AgenticAPIPrompt = WorkflowPrompt;
export const AgenticAPITool = WorkflowTool.omit({
mockTool: true,
autoSubmitMockedResponse: true,
});
export const AgenticAPITool = WorkflowTool
.omit({
autoSubmitMockedResponse: true,
})
export const AgenticAPIChatRequest = z.object({
messages: z.array(AgenticAPIChatMessage),
@ -55,6 +53,9 @@ export const AgenticAPIChatRequest = z.object({
tools: z.array(AgenticAPITool),
prompts: z.array(WorkflowPrompt),
startAgent: z.string(),
testProfile: TestProfile.optional(),
mcpServers: z.array(MCPServer).optional(),
toolWebhookUrl: z.string().optional(),
});
export const AgenticAPIChatResponse = z.object({
@ -82,8 +83,10 @@ export function convertWorkflowToAgenticAPI(workflow: z.infer<typeof Workflow>):
description: agent.description,
instructions: sanitized,
model: agent.model,
hasRagSources: agent.ragDataSources ? agent.ragDataSources.length > 0 : false,
controlType: agent.controlType,
ragDataSources: agent.ragDataSources,
ragK: agent.ragK,
ragReturnType: agent.ragReturnType,
tools: entities.filter(e => e.type == 'tool').map(e => e.name),
prompts: entities.filter(e => e.type == 'prompt').map(e => e.name),
connectedAgents: entities.filter(e => e.type === 'agent').map(e => e.name),
@ -91,10 +94,8 @@ export function convertWorkflowToAgenticAPI(workflow: z.infer<typeof Workflow>):
return agenticAgent;
}),
tools: workflow.tools.map(tool => {
const { mockTool, autoSubmitMockedResponse, ...rest } = tool;
return {
...rest,
};
const { autoSubmitMockedResponse, ...rest } = tool;
return rest;
}),
prompts: workflow.prompts
.map(p => {

View file

@ -1,4 +1,5 @@
import { z } from "zod";
import { MCPServer } from "./types";
export const Project = z.object({
_id: z.string().uuid(),
@ -12,10 +13,7 @@ export const Project = z.object({
publishedWorkflowId: z.string().optional(),
nextWorkflowNumber: z.number().optional(),
testRunCounter: z.number().default(0),
mcpServers: z.array(z.object({
name: z.string(),
url: z.string(),
})).optional(),
mcpServers: z.array(MCPServer).optional(),
});
export const ProjectMember = z.object({

View file

@ -2,6 +2,11 @@ import { CoreMessage, ToolCallPart } from "ai";
import { z } from "zod";
import { apiV1 } from "rowboat-shared";
export const MCPServer = z.object({
name: z.string(),
url: z.string(),
});
export const PlaygroundChat = z.object({
createdAt: z.string().datetime(),
projectId: z.string(),

View file

@ -1,136 +1,8 @@
import { convertFromAgenticAPIChatMessages } from "./types/agents_api_types";
import { ClientToolCallRequest } from "./types/tool_types";
import { ClientToolCallJwt, GetInformationToolResult } from "./types/tool_types";
import { ClientToolCallRequestBody } from "./types/tool_types";
import { AgenticAPIChatResponse } from "./types/agents_api_types";
import { AgenticAPIChatRequest } from "./types/agents_api_types";
import { Workflow, WorkflowAgent } from "./types/workflow_types";
import { AgenticAPIChatMessage } from "./types/agents_api_types";
import { AgenticAPIChatResponse, AgenticAPIChatRequest, AgenticAPIChatMessage } from "./types/agents_api_types";
import { z } from "zod";
import { dataSourceDocsCollection, dataSourcesCollection, projectsCollection } from "./mongodb";
import { apiV1 } from "rowboat-shared";
import { SignJWT } from "jose";
import crypto from "crypto";
import { ObjectId } from "mongodb";
import { embeddingModel } from "./embedding";
import { embed, generateObject } from "ai";
import { qdrantClient } from "./qdrant";
import { EmbeddingRecord } from "./types/datasource_types";
import { generateObject } from "ai";
import { ApiMessage } from "./types/types";
import { openai } from "@ai-sdk/openai";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
export async function callMcpTool(
projectId: string,
mcpServerName: string,
toolName: string,
parameters: Record<string, unknown>,
): Promise<unknown> {
const project = await projectsCollection.findOne({
"_id": projectId,
});
if (!project) {
throw new Error('Project not found');
}
const mcpServer = project.mcpServers?.find(s => s.name === mcpServerName);
if (!mcpServer) {
throw new Error('MCP server not found');
}
const transport = new SSEClientTransport(new URL(mcpServer.url));
const client = new Client(
{
name: "rowboat-client",
version: "1.0.0"
},
{
capabilities: {
prompts: {},
resources: {},
tools: {}
}
}
);
await client.connect(transport);
const result = await client.callTool({
name: toolName,
arguments: parameters,
});
await client.close();
return result;
}
export async function callClientToolWebhook(
toolCall: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'][number],
messages: z.infer<typeof ApiMessage>[],
projectId: string,
): Promise<unknown> {
const project = await projectsCollection.findOne({
"_id": projectId,
});
if (!project) {
throw new Error('Project not found');
}
if (!project.webhookUrl) {
throw new Error('Webhook URL not found');
}
// prepare request body
const content = JSON.stringify({
toolCall,
messages,
} as z.infer<typeof ClientToolCallRequestBody>);
const requestId = crypto.randomUUID();
const bodyHash = crypto
.createHash('sha256')
.update(content, 'utf8')
.digest('hex');
// sign request
const jwt = await new SignJWT({
requestId,
projectId,
bodyHash,
} as z.infer<typeof ClientToolCallJwt>)
.setProtectedHeader({
alg: 'HS256',
typ: 'JWT',
})
.setIssuer('rowboat')
.setAudience(project.webhookUrl)
.setSubject(`tool-call-${toolCall.id}`)
.setJti(requestId)
.setIssuedAt()
.setExpirationTime("5 minutes")
.sign(new TextEncoder().encode(project.secret));
// make request
const request: z.infer<typeof ClientToolCallRequest> = {
requestId,
content,
};
const response = await fetch(project.webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-signature-jwt': jwt,
},
body: JSON.stringify(request),
});
if (!response.ok) {
throw new Error(`Failed to call webhook: ${response.status}: ${response.statusText}`);
}
const responseBody = await response.json();
return responseBody;
}
export async function getAgenticApiResponse(
request: z.infer<typeof AgenticAPIChatRequest>,
@ -163,85 +35,6 @@ export async function getAgenticApiResponse(
};
}
export async function runRAGToolCall(
projectId: string,
query: string,
sourceIds: string[],
returnType: z.infer<typeof WorkflowAgent>['ragReturnType'],
k: number,
): Promise<z.infer<typeof GetInformationToolResult>> {
// create embedding for question
const embedResult = await embed({
model: embeddingModel,
value: query,
});
// fetch all data sources for this project
const sources = await dataSourcesCollection.find({
projectId: projectId,
active: true,
}).toArray();
const validSourceIds = sources
.filter(s => sourceIds.includes(s._id.toString())) // id should be in sourceIds
.filter(s => s.active) // should be active
.map(s => s._id.toString());
// if no sources found, return empty response
if (validSourceIds.length === 0) {
return {
results: [],
};
}
// perform qdrant vector search
const qdrantResults = await qdrantClient.query("embeddings", {
query: embedResult.embedding,
filter: {
must: [
{ key: "projectId", match: { value: projectId } },
{ key: "sourceId", match: { any: validSourceIds } },
],
},
limit: k,
with_payload: true,
});
// if return type is chunks, return the chunks
let results = qdrantResults.points.map((point) => {
const { title, name, content, docId, sourceId } = point.payload as z.infer<typeof EmbeddingRecord>['payload'];
return {
title,
name,
content,
docId,
sourceId,
};
});
if (returnType === 'chunks') {
return {
results,
};
}
// otherwise, fetch the doc contents from mongodb
const docs = await dataSourceDocsCollection.find({
_id: { $in: results.map(r => new ObjectId(r.docId)) },
}).toArray();
// map the results to the docs
results = results.map(r => {
const doc = docs.find(d => d._id.toString() === r.docId);
return {
...r,
content: doc?.content || '',
};
});
return {
results,
};
}
// create a PrefixLogger class that wraps console.log with a prefix
// and allows chaining with a parent logger
export class PrefixLogger {

View file

@ -1,7 +1,7 @@
'use client';
import { useState } from "react";
import { z } from "zod";
import { PlaygroundChat } from "../../../lib/types/types";
import { MCPServer, PlaygroundChat } from "../../../lib/types/types";
import { Workflow } from "../../../lib/types/workflow_types";
import { Chat } from "./chat";
import { ActionButton, Pane } from "../workflow/pane";
@ -17,11 +17,15 @@ export function App({
projectId,
workflow,
messageSubscriber,
mcpServerUrls,
toolWebhookUrl,
}: {
hidden?: boolean;
projectId: string;
workflow: z.infer<typeof Workflow>;
messageSubscriber?: (messages: z.infer<typeof apiV1.ChatMessage>[]) => void;
mcpServerUrls: Array<z.infer<typeof MCPServer>>;
toolWebhookUrl: string;
}) {
const [counter, setCounter] = useState<number>(0);
const [testProfile, setTestProfile] = useState<z.infer<typeof TestProfile> | null>(null);
@ -84,6 +88,8 @@ export function App({
onTestProfileChange={handleTestProfileChange}
systemMessage={systemMessage}
onSystemMessageChange={handleSystemMessageChange}
mcpServerUrls={mcpServerUrls}
toolWebhookUrl={toolWebhookUrl}
/>
</div>
</Pane>

View file

@ -1,9 +1,9 @@
'use client';
import { getAssistantResponse, simulateUserResponse } from "../../../actions/actions";
import { getAssistantResponse } from "../../../actions/actions";
import { useEffect, useState } from "react";
import { Messages } from "./messages";
import z from "zod";
import { PlaygroundChat } from "../../../lib/types/types";
import { MCPServer, PlaygroundChat } from "../../../lib/types/types";
import { convertToAgenticAPIChatMessages } from "../../../lib/types/agents_api_types";
import { convertWorkflowToAgenticAPI } from "../../../lib/types/agents_api_types";
import { AgenticAPIChatRequest } from "../../../lib/types/agents_api_types";
@ -15,7 +15,7 @@ import { CopyAsJsonButton } from "./copy-as-json-button";
import { TestProfile } from "@/app/lib/types/testing_types";
import { ProfileSelector } from "@/app/projects/[projectId]/test/[[...slug]]/components/selectors/profile-selector";
import { WithStringId } from "@/app/lib/types/types";
import { XCircleIcon, XIcon } from "lucide-react";
import { XIcon } from "lucide-react";
export function Chat({
chat,
@ -26,6 +26,8 @@ export function Chat({
onTestProfileChange,
systemMessage,
onSystemMessageChange,
mcpServerUrls,
toolWebhookUrl,
}: {
chat: z.infer<typeof PlaygroundChat>;
projectId: string;
@ -35,6 +37,8 @@ export function Chat({
onTestProfileChange: (profile: WithStringId<z.infer<typeof TestProfile>> | null) => void;
systemMessage: string;
onSystemMessageChange: (message: string) => void;
mcpServerUrls: Array<z.infer<typeof MCPServer>>;
toolWebhookUrl: string;
}) {
const [messages, setMessages] = useState<z.infer<typeof apiV1.ChatMessage>[]>(chat.messages);
const [loadingAssistantResponse, setLoadingAssistantResponse] = useState<boolean>(false);
@ -68,15 +72,6 @@ export function Chat({
setFetchResponseError(null);
}
function handleToolCallResults(results: z.infer<typeof apiV1.ToolMessage>[]) {
setMessages([...messages, ...results.map((result) => ({
...result,
version: 'v1' as const,
chatId: '',
createdAt: new Date().toISOString(),
}))]);
}
// reset state when workflow changes
useEffect(() => {
setMessages([]);
@ -113,6 +108,9 @@ export function Chat({
tools,
prompts,
startAgent,
mcpServers: mcpServerUrls,
toolWebhookUrl: toolWebhookUrl,
testProfile: testProfile ?? undefined,
};
setLastAgenticRequest(null);
setLastAgenticResponse(null);
@ -164,105 +162,7 @@ export function Chat({
return () => {
ignore = true;
};
}, [chat.simulated, messages, projectId, agenticState, workflow, fetchResponseError, systemMessage, simulationComplete]);
// simulate user turn
useEffect(() => {
let ignore = false;
async function process() {
if (chat.simulationScenario === undefined) {
return;
}
// fetch next user prompt
setLoadingUserResponse(true);
try {
const response = await simulateUserResponse(projectId, messages, chat.simulationScenario)
if (ignore) {
return;
}
if (simulationComplete) {
return;
}
if (response.trim() === 'EXIT') {
setSimulationComplete(true);
return;
}
setMessages([...messages, {
role: 'user',
content: response,
version: 'v1' as const,
chatId: '',
createdAt: new Date().toISOString(),
}]);
setFetchResponseError(null);
} catch (err) {
setFetchResponseError(`Failed to simulate user response: ${err instanceof Error ? err.message : 'Unknown error'}`);
} finally {
setLoadingUserResponse(false);
}
}
// proceed only if chat is simulated
if (!chat.simulated) {
return;
}
// dont proceed if simulation is complete
if (chat.simulated && simulationComplete) {
return;
}
// check if there are no messages yet OR
// check if the last message is an assistant
// message containing a text response. If so,
// call the simulate user turn api to fetch
// user response
let last = messages[messages.length - 1];
if (last && last.role !== 'assistant') {
return;
}
if (last && 'tool_calls' in last) {
return;
}
process();
return () => {
ignore = true;
};
}, [chat.simulated, messages, projectId, simulationComplete, chat.simulationScenario]);
// save chat on every assistant message
// useEffect(() => {
// let ignore = false;
// function process() {
// savePlaygroundChat(projectId, {
// ...chat,
// messages,
// simulationComplete,
// agenticState,
// }, chatId)
// .then((insertedChatId) => {
// if (!chatId) {
// setChatId(insertedChatId);
// }
// });
// }
// if (messages.length === 0) {
// return;
// }
// const lastMessage = messages[messages.length - 1];
// if (lastMessage && lastMessage.role !== 'assistant') {
// return;
// }
// process();
// }, [chatId, chat, messages, projectId, simulationComplete, agenticState]);
}, [chat.simulated, messages, projectId, agenticState, workflow, fetchResponseError, systemMessage, simulationComplete, mcpServerUrls, toolWebhookUrl, testProfile]);
const handleCopyChat = () => {
const jsonString = JSON.stringify({
@ -303,7 +203,6 @@ export function Chat({
projectId={projectId}
messages={messages}
toolCallResults={toolCallResults}
handleToolCallResults={handleToolCallResults}
loadingAssistantResponse={loadingAssistantResponse}
loadingUserResponse={loadingUserResponse}
workflow={workflow}

View file

@ -1,17 +1,14 @@
'use client';
import { Button, Spinner, Textarea } from "@heroui/react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Spinner } from "@heroui/react";
import { useEffect, useMemo, useRef, useState } from "react";
import z from "zod";
import { Workflow } from "../../../lib/types/workflow_types";
import { WorkflowTool } from "../../../lib/types/workflow_types";
import { GetInformationToolResult } from "../../../lib/types/tool_types";
import { executeClientTool, getInformationTool, suggestToolResponse } from "../../../actions/actions";
import MarkdownContent from "../../../lib/components/markdown-content";
import { apiV1 } from "rowboat-shared";
import { EditableField } from "../../../lib/components/editable-field";
import { MessageSquareIcon, EllipsisIcon, CircleCheckIcon, ChevronRightIcon, ChevronDownIcon, XIcon } from "lucide-react";
import { TestProfile } from "@/app/lib/types/testing_types";
import { executeMcpTool } from "@/app/actions/mcp_actions";
function UserMessage({ content }: { content: string }) {
return <div className="self-end ml-[30%] flex flex-col">
@ -88,7 +85,6 @@ function UserMessageLoading() {
function ToolCalls({
toolCalls,
results,
handleResults,
projectId,
messages,
sender,
@ -98,7 +94,6 @@ function ToolCalls({
}: {
toolCalls: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'];
results: Record<string, z.infer<typeof apiV1.ToolMessage>>;
handleResults: (results: z.infer<typeof apiV1.ToolMessage>[]) => void;
projectId: string;
messages: z.infer<typeof apiV1.ChatMessage>[];
sender: string | null | undefined;
@ -108,21 +103,12 @@ function ToolCalls({
}) {
const resultsMap: Record<string, z.infer<typeof apiV1.ToolMessage>> = {};
function handleToolCallResult(result: z.infer<typeof apiV1.ToolMessage>) {
resultsMap[result.tool_call_id] = result;
if (Object.keys(resultsMap).length === toolCalls.length) {
const results = Object.values(resultsMap);
handleResults(results);
}
}
return <div className="flex flex-col gap-4">
{toolCalls.map(toolCall => {
return <ToolCall
key={toolCall.id}
toolCall={toolCall}
result={results[toolCall.id]}
handleResult={handleToolCallResult}
projectId={projectId}
messages={messages}
sender={sender}
@ -137,7 +123,6 @@ function ToolCalls({
function ToolCall({
toolCall,
result,
handleResult,
projectId,
messages,
sender,
@ -147,7 +132,6 @@ function ToolCall({
}: {
toolCall: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'][number];
result: z.infer<typeof apiV1.ToolMessage> | undefined;
handleResult: (result: z.infer<typeof apiV1.ToolMessage>) => void;
projectId: string;
messages: z.infer<typeof apiV1.ChatMessage>[];
sender: string | null | undefined;
@ -163,63 +147,17 @@ function ToolCall({
}
}
switch (toolCall.function.name) {
case 'getArticleInfo':
return <GetInformationToolCall
toolCall={toolCall}
result={result}
handleResult={handleResult}
projectId={projectId}
messages={messages}
sender={sender}
workflow={workflow}
/>;
default:
if (toolCall.function.name.startsWith('transfer_to_')) {
return <TransferToAgentToolCall
toolCall={toolCall}
result={result}
handleResult={handleResult}
projectId={projectId}
messages={messages}
sender={sender}
/>;
}
if (!matchingWorkflowTool ||
matchingWorkflowTool.mockTool ||
(testProfile && testProfile.mockTools)) {
return <MockToolCall
toolCall={toolCall}
result={result}
handleResult={handleResult}
projectId={projectId}
messages={messages}
sender={sender}
testProfile={testProfile}
workflowTool={matchingWorkflowTool}
systemMessage={systemMessage}
/>;
}
if (matchingWorkflowTool?.isMcp) {
return <McpToolCall
toolCall={toolCall}
workflowTool={matchingWorkflowTool}
result={result}
handleResult={handleResult}
projectId={projectId}
messages={messages}
sender={sender}
/>;
}
return <ClientToolCall
toolCall={toolCall}
result={result}
handleResult={handleResult}
projectId={projectId}
messages={messages}
sender={sender}
/>;
if (toolCall.function.name.startsWith('transfer_to_')) {
return <TransferToAgentToolCall
result={result}
sender={sender}
/>;
}
return <ClientToolCall
toolCall={toolCall}
result={result}
sender={sender}
/>;
}
function ToolCallHeader({
@ -240,105 +178,11 @@ function ToolCallHeader({
</div>;
}
function GetInformationToolCall({
toolCall,
result: availableResult,
handleResult,
projectId,
messages,
sender,
workflow,
}: {
toolCall: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'][number];
result: z.infer<typeof apiV1.ToolMessage> | undefined;
handleResult: (result: z.infer<typeof apiV1.ToolMessage>) => void;
projectId: string;
messages: z.infer<typeof apiV1.ChatMessage>[];
sender: string | null | undefined;
workflow: z.infer<typeof Workflow>;
}) {
const [result, setResult] = useState<z.infer<typeof apiV1.ToolMessage> | undefined>(availableResult);
const args = JSON.parse(toolCall.function.arguments) as { question: string };
let typedResult: z.infer<typeof GetInformationToolResult> | undefined;
if (result) {
typedResult = JSON.parse(result.content) as z.infer<typeof GetInformationToolResult>;
}
useEffect(() => {
if (result) {
return;
}
let ignore = false;
async function process() {
const result: z.infer<typeof apiV1.ToolMessage> = {
role: 'tool',
tool_call_id: toolCall.id,
tool_name: toolCall.function.name,
content: '',
};
// find target agent
const agent = workflow.agents.find(agent => agent.name == sender);
if (!agent || !agent.ragDataSources) {
result.content = JSON.stringify({
results: [],
});
} else {
const matches = await getInformationTool(projectId, args.question, agent.ragDataSources, agent.ragReturnType, agent.ragK);
if (ignore) {
return;
}
result.content = JSON.stringify(matches);
}
setResult(result);
handleResult(result);
}
process();
return () => {
ignore = true;
};
}, [result, toolCall.id, toolCall.function.name, projectId, args.question, workflow.agents, sender, handleResult]);
return <div className="flex flex-col gap-1">
{sender && <div className='text-gray-500 text-sm ml-3'>{sender}</div>}
<div className='border border-gray-300 p-2 rounded-lg rounded-bl-none flex flex-col gap-2 mr-[30%]'>
<ToolCallHeader toolCall={toolCall} result={result} />
<div className='mt-1'>
{result ? 'Fetched' : 'Fetch'} information for question: <span className='font-mono font-semibold'>{args['question']}</span>
{result && <div className='flex flex-col gap-2 mt-2 pt-2 border-t border-t-gray-200'>
{typedResult && typedResult.results.length === 0 && <div>No matches found.</div>}
{typedResult && typedResult.results.length > 0 && <ul className="list-disc ml-6">
{typedResult.results.map((result, index) => {
return <li key={'' + index} className="mb-2">
<ExpandableContent
label={result.title || result.name}
content={result.content}
expanded={false}
/>
</li>
})}
</ul>}
</div>}
</div>
</div>
</div>;
}
function TransferToAgentToolCall({
toolCall,
result: availableResult,
handleResult,
projectId,
messages,
sender,
}: {
toolCall: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'][number];
result: z.infer<typeof apiV1.ToolMessage> | undefined;
handleResult: (result: z.infer<typeof apiV1.ToolMessage>) => void;
projectId: string;
messages: z.infer<typeof apiV1.ChatMessage>[];
sender: string | null | undefined;
}) {
const typedResult = availableResult ? JSON.parse(availableResult.content) as { assistant: string } : undefined;
@ -355,282 +199,28 @@ function TransferToAgentToolCall({
</div>;
}
function McpToolCall({
toolCall,
result: availableResult,
handleResult,
projectId,
messages,
sender,
workflowTool,
}: {
toolCall: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'][number];
result: z.infer<typeof apiV1.ToolMessage> | undefined;
handleResult: (result: z.infer<typeof apiV1.ToolMessage>) => void;
projectId: string;
messages: z.infer<typeof apiV1.ChatMessage>[];
sender: string | null | undefined;
workflowTool: z.infer<typeof WorkflowTool>;
}) {
const [result, setResult] = useState<z.infer<typeof apiV1.ToolMessage> | undefined>(availableResult);
useEffect(() => {
if (result) {
return;
}
let ignore = false;
async function process() {
let response;
try {
response = await executeMcpTool(
projectId,
workflowTool.mcpServerName || '',
workflowTool.name,
JSON.parse(toolCall.function.arguments),
);
} catch (e) {
response = {
error: (e as Error).message,
};
}
if (ignore) {
return;
}
const result: z.infer<typeof apiV1.ToolMessage> = {
role: 'tool',
tool_call_id: toolCall.id,
tool_name: toolCall.function.name,
content: JSON.stringify(response),
};
setResult(result);
handleResult(result);
}
process();
return () => {
ignore = true;
};
}, [result, toolCall, projectId, messages, handleResult, workflowTool.mcpServerName, workflowTool.name]);
return <div className="flex flex-col gap-1">
{sender && <div className='text-gray-500 text-sm ml-3'>{sender}</div>}
<div className='border border-gray-300 p-2 pt-2 rounded-lg rounded-bl-none flex flex-col gap-2 mr-[30%]'>
<ToolCallHeader toolCall={toolCall} result={result} />
<div className='flex flex-col gap-2'>
<ExpandableContent label='Params' content={toolCall.function.arguments} expanded={false} />
{result && <ExpandableContent label='Result' content={result.content} expanded={false} />}
</div>
</div>
</div>;
}
function ClientToolCall({
toolCall,
result: availableResult,
handleResult,
projectId,
messages,
sender,
}: {
toolCall: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'][number];
result: z.infer<typeof apiV1.ToolMessage> | undefined;
handleResult: (result: z.infer<typeof apiV1.ToolMessage>) => void;
projectId: string;
messages: z.infer<typeof apiV1.ChatMessage>[];
sender: string | null | undefined;
}) {
const [result, setResult] = useState<z.infer<typeof apiV1.ToolMessage> | undefined>(availableResult);
useEffect(() => {
if (result) {
return;
}
let ignore = false;
async function process() {
let response;
try {
response = await executeClientTool(
toolCall,
messages,
projectId,
);
} catch (e) {
response = {
error: (e as Error).message,
};
}
if (ignore) {
return;
}
const result: z.infer<typeof apiV1.ToolMessage> = {
role: 'tool',
tool_call_id: toolCall.id,
tool_name: toolCall.function.name,
content: JSON.stringify(response),
};
setResult(result);
handleResult(result);
}
process();
return () => {
ignore = true;
};
}, [result, toolCall, projectId, messages, handleResult]);
return <div className="flex flex-col gap-1">
{sender && <div className='text-gray-500 text-sm ml-3'>{sender}</div>}
<div className='border border-gray-300 p-2 pt-2 rounded-lg rounded-bl-none flex flex-col gap-2 mr-[30%]'>
<ToolCallHeader toolCall={toolCall} result={result} />
<ToolCallHeader toolCall={toolCall} result={availableResult} />
<div className='flex flex-col gap-2'>
<ExpandableContent label='Params' content={toolCall.function.arguments} expanded={false} />
{result && <ExpandableContent label='Result' content={result.content} expanded={false} />}
{availableResult && <ExpandableContent label='Result' content={availableResult.content} expanded={false} />}
</div>
</div>
</div>;
}
function MockToolCall({
toolCall,
result: availableResult,
handleResult,
projectId,
messages,
sender,
testProfile = null,
workflowTool,
systemMessage,
}: {
toolCall: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'][number];
result: z.infer<typeof apiV1.ToolMessage> | undefined;
handleResult: (result: z.infer<typeof apiV1.ToolMessage>) => void;
projectId: string;
messages: z.infer<typeof apiV1.ChatMessage>[];
sender: string | null | undefined;
testProfile: z.infer<typeof TestProfile> | null;
workflowTool: z.infer<typeof WorkflowTool> | undefined;
systemMessage: string | undefined;
}) {
const [result, setResult] = useState<z.infer<typeof apiV1.ToolMessage> | undefined>(availableResult);
const [response, setResponse] = useState('');
const [generatingResponse, setGeneratingResponse] = useState(false);
const handleSubmit = useCallback(() => {
let parsed;
try {
parsed = JSON.parse(response);
} catch (e) {
alert('Invalid JSON');
return;
}
const result: z.infer<typeof apiV1.ToolMessage> = {
role: 'tool',
tool_call_id: toolCall.id,
tool_name: toolCall.function.name,
content: JSON.stringify(parsed),
};
setResult(result);
handleResult(result);
}, [toolCall.id, toolCall.function.name, handleResult, response]);
useEffect(() => {
if (result) {
return;
}
if (response) {
return;
}
let ignore = false;
async function process() {
setGeneratingResponse(true);
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;
}
setResponse(response);
setGeneratingResponse(false);
}
process();
return () => {
ignore = true;
};
}, [result, response, toolCall.id, projectId, messages, testProfile, systemMessage, workflowTool?.mockInstructions]);
// auto submit if autoSubmitMockedResponse is true
useEffect(() => {
if (!workflowTool?.autoSubmitMockedResponse) {
return;
}
if (result) {
return;
}
if (response) {
handleSubmit();
}
}, [workflowTool?.autoSubmitMockedResponse, response, handleSubmit, result]);
return <div className="flex flex-col gap-1">
{sender && <div className='text-gray-500 dark:text-gray-400 text-xs ml-3'>{sender}</div>}
<div className='border border-gray-300 dark:border-gray-700 p-2 pt-2 rounded-lg rounded-bl-none flex flex-col gap-2 mr-[30%] bg-white dark:bg-gray-900'>
<div className="flex items-center gap-2">
{!result && <Spinner size="sm" />}
{result && <CircleCheckIcon size={16} className="text-gray-500 dark:text-gray-400" />}
<span className="text-sm text-gray-700 dark:text-gray-300">
Function Call: <code className='bg-gray-100 dark:bg-neutral-800 px-2 py-0.5 rounded font-mono'>{toolCall.function.name}</code>
</span>
</div>
<div className='flex flex-col gap-2'>
<ExpandableContent label='Params' content={toolCall.function.arguments} expanded={false} />
{result && <ExpandableContent label='Result' content={result.content} expanded={false} />}
</div>
{!result && !workflowTool?.autoSubmitMockedResponse && <div className='flex flex-col gap-2 mt-2'>
<div>Response:</div>
<Textarea
maxRows={10}
placeholder='{}'
variant="bordered"
value={response}
disabled={generatingResponse}
onValueChange={(value) => setResponse(value)}
className='font-mono'
size="sm"
>
</Textarea>
<Button
onPress={handleSubmit}
disabled={generatingResponse}
isLoading={generatingResponse}
size="sm"
>
Submit result
</Button>
</div>}
</div>
</div>;
}
function ExpandableContent({
label,
content,
@ -701,7 +291,6 @@ export function Messages({
projectId,
messages,
toolCallResults,
handleToolCallResults,
loadingAssistantResponse,
loadingUserResponse,
workflow,
@ -712,7 +301,6 @@ export function Messages({
projectId: string;
messages: z.infer<typeof apiV1.ChatMessage>[];
toolCallResults: Record<string, z.infer<typeof apiV1.ToolMessage>>;
handleToolCallResults: (results: z.infer<typeof apiV1.ToolMessage>[]) => void;
loadingAssistantResponse: boolean;
loadingUserResponse: boolean;
workflow: z.infer<typeof Workflow>;
@ -742,7 +330,6 @@ export function Messages({
key={index}
toolCalls={message.tool_calls}
results={toolCallResults}
handleResults={handleToolCallResults}
projectId={projectId}
messages={messages}
sender={message.agenticSender}

View file

@ -1,5 +1,5 @@
"use client";
import { WithStringId } from "../../../lib/types/types";
import { MCPServer, WithStringId } from "../../../lib/types/types";
import { Workflow } from "../../../lib/types/workflow_types";
import { DataSource } from "../../../lib/types/datasource_types";
import { z } from "zod";
@ -13,9 +13,13 @@ import { listDataSources } from "../../../actions/datasource_actions";
export function App({
projectId,
useRag,
mcpServerUrls,
toolWebhookUrl,
}: {
projectId: string;
useRag: boolean;
mcpServerUrls: Array<z.infer<typeof MCPServer>>;
toolWebhookUrl: string;
}) {
const [selectorKey, setSelectorKey] = useState(0);
const [workflow, setWorkflow] = useState<WithStringId<z.infer<typeof Workflow>> | null>(null);
@ -108,6 +112,8 @@ export function App({
handleShowSelector={handleShowSelector}
handleCloneVersion={handleCloneVersion}
useRag={useRag}
mcpServerUrls={mcpServerUrls}
toolWebhookUrl={toolWebhookUrl}
/>}
</>
}

View file

@ -1,8 +1,8 @@
import { Metadata } from "next";
import { App } from "./app";
import { USE_RAG } from "@/app/lib/feature_flags";
export const dynamic = 'force-dynamic';
import { projectsCollection } from "@/app/lib/mongodb";
import { notFound } from "next/navigation";
export const metadata: Metadata = {
title: "Workflow"
@ -13,8 +13,18 @@ export default async function Page({
}: {
params: { projectId: string };
}) {
const project = await projectsCollection.findOne({
_id: params.projectId,
});
if (!project) {
notFound();
}
const toolWebhookUrl = project.webhookUrl ?? '';
return <App
projectId={params.projectId}
useRag={USE_RAG}
mcpServerUrls={project.mcpServers ?? []}
toolWebhookUrl={toolWebhookUrl}
/>;
}

View file

@ -1,5 +1,5 @@
"use client";
import { WithStringId } from "../../../lib/types/types";
import { MCPServer, WithStringId } from "../../../lib/types/types";
import { Workflow } from "../../../lib/types/workflow_types";
import { WorkflowTool } from "../../../lib/types/workflow_types";
import { WorkflowPrompt } from "../../../lib/types/workflow_types";
@ -559,6 +559,8 @@ export function WorkflowEditor({
handleShowSelector,
handleCloneVersion,
useRag,
mcpServerUrls,
toolWebhookUrl,
}: {
dataSources: WithStringId<z.infer<typeof DataSource>>[];
workflow: WithStringId<z.infer<typeof Workflow>>;
@ -566,6 +568,8 @@ export function WorkflowEditor({
handleShowSelector: () => void;
handleCloneVersion: (workflowId: string) => void;
useRag: boolean;
mcpServerUrls: Array<z.infer<typeof MCPServer>>;
toolWebhookUrl: string;
}) {
const [state, dispatch] = useReducer<Reducer<State, Action>>(reducer, {
patches: [],
@ -911,6 +915,8 @@ export function WorkflowEditor({
projectId={state.present.workflow.projectId}
workflow={state.present.workflow}
messageSubscriber={updateChatMessages}
mcpServerUrls={mcpServerUrls}
toolWebhookUrl={toolWebhookUrl}
/>
{state.present.selection?.type === "agent" && <AgentConfig
key={state.present.selection.name}