mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-24 21:41:08 +02:00
Refactor agents api integration
This commit is contained in:
parent
0e31098d58
commit
a02c830fb0
14 changed files with 131 additions and 1121 deletions
|
|
@ -1,26 +1,16 @@
|
||||||
'use server';
|
'use server';
|
||||||
import { convertFromAgenticAPIChatMessages } from "../lib/types/agents_api_types";
|
import { convertFromAgenticAPIChatMessages } from "../lib/types/agents_api_types";
|
||||||
import { AgenticAPIChatRequest } 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 { WebpageCrawlResponse } from "../lib/types/tool_types";
|
||||||
import { GetInformationToolResult } from "../lib/types/tool_types";
|
import { webpagesCollection } from "../lib/mongodb";
|
||||||
import { EmbeddingDoc } from "../lib/types/datasource_types";
|
|
||||||
import { generateObject, generateText, embed } from "ai";
|
|
||||||
import { dataSourceDocsCollection, dataSourcesCollection, embeddingsCollection, webpagesCollection } from "../lib/mongodb";
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { openai } from "@ai-sdk/openai";
|
|
||||||
import FirecrawlApp, { ScrapeResponse } from '@mendable/firecrawl-js';
|
import FirecrawlApp, { ScrapeResponse } from '@mendable/firecrawl-js';
|
||||||
import { embeddingModel } from "../lib/embedding";
|
|
||||||
import { apiV1 } from "rowboat-shared";
|
import { apiV1 } from "rowboat-shared";
|
||||||
import { Claims, getSession } from "@auth0/nextjs-auth0";
|
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 { check_query_limit } from "../lib/rate_limiting";
|
||||||
import { QueryLimitError } from "../lib/client_utils";
|
import { QueryLimitError } from "../lib/client_utils";
|
||||||
import { projectAuthCheck } from "./project_actions";
|
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 || '' });
|
const crawler = new FirecrawlApp({ apiKey: process.env.FIRECRAWL_API_KEY || '' });
|
||||||
|
|
||||||
|
|
@ -98,96 +88,3 @@ export async function getAssistantResponse(
|
||||||
rawResponse: response.rawAPIResponse,
|
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;
|
|
||||||
}
|
|
||||||
|
|
@ -4,7 +4,6 @@ import { WorkflowTool } from "../lib/types/workflow_types";
|
||||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||||
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
||||||
import { projectAuthCheck } from "./project_actions";
|
import { projectAuthCheck } from "./project_actions";
|
||||||
import { callMcpTool } from "../lib/utils";
|
|
||||||
import { projectsCollection } from "../lib/mongodb";
|
import { projectsCollection } from "../lib/mongodb";
|
||||||
import { Project } from "../lib/types/project_types";
|
import { Project } from "../lib/types/project_types";
|
||||||
|
|
||||||
|
|
@ -73,10 +72,3 @@ export async function updateMcpServers(projectId: string, mcpServers: z.infer<ty
|
||||||
_id: projectId,
|
_id: projectId,
|
||||||
}, { $set: { mcpServers } });
|
}, { $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;
|
|
||||||
}
|
|
||||||
|
|
@ -4,10 +4,9 @@ import { z } from "zod";
|
||||||
import { ObjectId } from "mongodb";
|
import { ObjectId } from "mongodb";
|
||||||
import { authCheck } from "../../utils";
|
import { authCheck } from "../../utils";
|
||||||
import { ApiRequest, ApiResponse } from "../../../../lib/types/types";
|
import { ApiRequest, ApiResponse } from "../../../../lib/types/types";
|
||||||
import { AgenticAPIChatRequest, AgenticAPIChatMessage, convertFromAgenticApiToApiMessages, convertFromApiToAgenticApiMessages, convertWorkflowToAgenticAPI } from "../../../../lib/types/agents_api_types";
|
import { AgenticAPIChatRequest, convertFromAgenticApiToApiMessages, convertFromApiToAgenticApiMessages, convertWorkflowToAgenticAPI } from "../../../../lib/types/agents_api_types";
|
||||||
import { getAgenticApiResponse, callClientToolWebhook, runRAGToolCall, mockToolResponse, callMcpTool } from "../../../../lib/utils";
|
import { getAgenticApiResponse } from "../../../../lib/utils";
|
||||||
import { check_query_limit } from "../../../../lib/rate_limiting";
|
import { check_query_limit } from "../../../../lib/rate_limiting";
|
||||||
import { apiV1 } from "rowboat-shared";
|
|
||||||
import { PrefixLogger } from "../../../../lib/utils";
|
import { PrefixLogger } from "../../../../lib/utils";
|
||||||
import { TestProfile } from "@/app/lib/types/testing_types";
|
import { TestProfile } from "@/app/lib/types/testing_types";
|
||||||
|
|
||||||
|
|
@ -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 currentState: unknown = reqState ?? { last_agent_name: workflow.agents[0].name };
|
||||||
let turns = 0;
|
|
||||||
let hasToolCalls = false;
|
|
||||||
|
|
||||||
do {
|
// get assistant response
|
||||||
hasToolCalls = false;
|
const { agents, tools, prompts, startAgent } = convertWorkflowToAgenticAPI(workflow);
|
||||||
// get assistant response
|
const request: z.infer<typeof AgenticAPIChatRequest> = {
|
||||||
const { agents, tools, prompts, startAgent } = convertWorkflowToAgenticAPI(workflow);
|
messages: convertFromApiToAgenticApiMessages(reqMessages),
|
||||||
const request: z.infer<typeof AgenticAPIChatRequest> = {
|
state: currentState,
|
||||||
messages: convertFromApiToAgenticApiMessages(currentMessages),
|
agents,
|
||||||
state: currentState,
|
tools,
|
||||||
agents,
|
prompts,
|
||||||
tools,
|
startAgent,
|
||||||
prompts,
|
testProfile: testProfile ?? undefined,
|
||||||
startAgent,
|
mcpServers: project.mcpServers ?? undefined,
|
||||||
};
|
toolWebhookUrl: project.webhookUrl ?? undefined,
|
||||||
|
};
|
||||||
|
|
||||||
console.log(`turn ${turns}: sending agentic request from /chat api`, JSON.stringify(request, null, 2));
|
const { messages: agenticMessages, state } = await getAgenticApiResponse(request);
|
||||||
logger.log(`Processing turn ${turns} for conversation`);
|
const newMessages = convertFromAgenticApiToApiMessages(agenticMessages);
|
||||||
const { messages: agenticMessages, state } = await getAgenticApiResponse(request);
|
const newState = state;
|
||||||
|
|
||||||
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 responseBody: z.infer<typeof ApiResponse> = {
|
const responseBody: z.infer<typeof ApiResponse> = {
|
||||||
messages: currentMessages.slice(reqMessages.length),
|
messages: newMessages,
|
||||||
state: currentState,
|
state: newState,
|
||||||
};
|
};
|
||||||
return Response.json(responseBody);
|
return Response.json(responseBody);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,10 @@ import { convertFromAgenticAPIChatMessages } from "../../../../../../lib/types/a
|
||||||
import { convertToAgenticAPIChatMessages } from "../../../../../../lib/types/agents_api_types";
|
import { convertToAgenticAPIChatMessages } from "../../../../../../lib/types/agents_api_types";
|
||||||
import { convertWorkflowToAgenticAPI } from "../../../../../../lib/types/agents_api_types";
|
import { convertWorkflowToAgenticAPI } from "../../../../../../lib/types/agents_api_types";
|
||||||
import { AgenticAPIChatRequest } 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 { check_query_limit } from "../../../../../../lib/rate_limiting";
|
||||||
import { PrefixLogger } from "../../../../../../lib/utils";
|
import { PrefixLogger } from "../../../../../../lib/utils";
|
||||||
|
|
||||||
// Add max turns constant at the top with other constants
|
|
||||||
const MAX_TURNS = 3;
|
|
||||||
|
|
||||||
// get next turn / agent response
|
// get next turn / agent response
|
||||||
export async function POST(
|
export async function POST(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
|
|
@ -95,109 +92,32 @@ export async function POST(
|
||||||
// get assistant response
|
// get assistant response
|
||||||
const { agents, tools, prompts, startAgent } = convertWorkflowToAgenticAPI(workflow);
|
const { agents, tools, prompts, startAgent } = convertWorkflowToAgenticAPI(workflow);
|
||||||
const unsavedMessages: z.infer<typeof apiV1.ChatMessage>[] = [userMessage];
|
const unsavedMessages: z.infer<typeof apiV1.ChatMessage>[] = [userMessage];
|
||||||
let resolvingToolCalls = true;
|
let state: unknown = chat.agenticState ?? { last_agent_name: startAgent };
|
||||||
let state: unknown = chat.agenticState ?? {last_agent_name: startAgent};
|
|
||||||
let turns = 0; // Add turns counter
|
|
||||||
|
|
||||||
while (resolvingToolCalls) {
|
const request: z.infer<typeof AgenticAPIChatRequest> = {
|
||||||
if (turns >= MAX_TURNS) {
|
messages: convertToAgenticAPIChatMessages([systemMessage, ...messages, ...unsavedMessages]),
|
||||||
logger.log(`Max turns (${MAX_TURNS}) reached for chat ${chatId}`);
|
state,
|
||||||
throw new Error("Max turns reached");
|
agents,
|
||||||
}
|
tools,
|
||||||
turns++;
|
prompts,
|
||||||
|
startAgent,
|
||||||
const request: z.infer<typeof AgenticAPIChatRequest> = {
|
mcpServers: projectSettings.mcpServers ?? undefined,
|
||||||
messages: convertToAgenticAPIChatMessages([systemMessage, ...messages, ...unsavedMessages]),
|
toolWebhookUrl: projectSettings.webhookUrl ?? undefined,
|
||||||
state,
|
testProfile: undefined,
|
||||||
agents,
|
};
|
||||||
tools,
|
logger.log(`Sending agentic request`);
|
||||||
prompts,
|
const response = await getAgenticApiResponse(request);
|
||||||
startAgent,
|
state = response.state;
|
||||||
};
|
if (response.messages.length === 0) {
|
||||||
logger.log(`Turn ${turns}: sending agentic request`);
|
throw new Error("No messages returned from assistant");
|
||||||
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 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`);
|
logger.log(`Saving ${unsavedMessages.length} new messages and updating chat state`);
|
||||||
await chatMessagesCollection.insertMany(unsavedMessages);
|
await chatMessagesCollection.insertMany(unsavedMessages);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import { z } from "zod";
|
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 { apiV1 } from "rowboat-shared";
|
||||||
import { ApiMessage } from "./types";
|
import { ApiMessage } from "./types";
|
||||||
|
import { TestProfile } from "./testing_types";
|
||||||
|
import { MCPServer } from "./types";
|
||||||
|
|
||||||
export const AgenticAPIChatMessage = z.object({
|
export const AgenticAPIChatMessage = z.object({
|
||||||
role: z.union([z.literal('user'), z.literal('assistant'), z.literal('tool'), z.literal('system')]),
|
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,
|
locked: true,
|
||||||
toggleAble: true,
|
toggleAble: true,
|
||||||
global: true,
|
global: true,
|
||||||
ragDataSources: true,
|
|
||||||
ragReturnType: true,
|
|
||||||
ragK: true,
|
|
||||||
})
|
})
|
||||||
.extend({
|
.extend({
|
||||||
hasRagSources: z.boolean().default(false).optional(),
|
|
||||||
tools: z.array(z.string()),
|
tools: z.array(z.string()),
|
||||||
prompts: z.array(z.string()),
|
prompts: z.array(z.string()),
|
||||||
connectedAgents: z.array(z.string()),
|
connectedAgents: z.array(z.string()),
|
||||||
|
|
@ -43,10 +41,10 @@ export const AgenticAPIAgent = WorkflowAgent
|
||||||
|
|
||||||
export const AgenticAPIPrompt = WorkflowPrompt;
|
export const AgenticAPIPrompt = WorkflowPrompt;
|
||||||
|
|
||||||
export const AgenticAPITool = WorkflowTool.omit({
|
export const AgenticAPITool = WorkflowTool
|
||||||
mockTool: true,
|
.omit({
|
||||||
autoSubmitMockedResponse: true,
|
autoSubmitMockedResponse: true,
|
||||||
});
|
})
|
||||||
|
|
||||||
export const AgenticAPIChatRequest = z.object({
|
export const AgenticAPIChatRequest = z.object({
|
||||||
messages: z.array(AgenticAPIChatMessage),
|
messages: z.array(AgenticAPIChatMessage),
|
||||||
|
|
@ -55,6 +53,9 @@ export const AgenticAPIChatRequest = z.object({
|
||||||
tools: z.array(AgenticAPITool),
|
tools: z.array(AgenticAPITool),
|
||||||
prompts: z.array(WorkflowPrompt),
|
prompts: z.array(WorkflowPrompt),
|
||||||
startAgent: z.string(),
|
startAgent: z.string(),
|
||||||
|
testProfile: TestProfile.optional(),
|
||||||
|
mcpServers: z.array(MCPServer).optional(),
|
||||||
|
toolWebhookUrl: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AgenticAPIChatResponse = z.object({
|
export const AgenticAPIChatResponse = z.object({
|
||||||
|
|
@ -82,8 +83,10 @@ export function convertWorkflowToAgenticAPI(workflow: z.infer<typeof Workflow>):
|
||||||
description: agent.description,
|
description: agent.description,
|
||||||
instructions: sanitized,
|
instructions: sanitized,
|
||||||
model: agent.model,
|
model: agent.model,
|
||||||
hasRagSources: agent.ragDataSources ? agent.ragDataSources.length > 0 : false,
|
|
||||||
controlType: agent.controlType,
|
controlType: agent.controlType,
|
||||||
|
ragDataSources: agent.ragDataSources,
|
||||||
|
ragK: agent.ragK,
|
||||||
|
ragReturnType: agent.ragReturnType,
|
||||||
tools: entities.filter(e => e.type == 'tool').map(e => e.name),
|
tools: entities.filter(e => e.type == 'tool').map(e => e.name),
|
||||||
prompts: entities.filter(e => e.type == 'prompt').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),
|
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;
|
return agenticAgent;
|
||||||
}),
|
}),
|
||||||
tools: workflow.tools.map(tool => {
|
tools: workflow.tools.map(tool => {
|
||||||
const { mockTool, autoSubmitMockedResponse, ...rest } = tool;
|
const { autoSubmitMockedResponse, ...rest } = tool;
|
||||||
return {
|
return rest;
|
||||||
...rest,
|
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
prompts: workflow.prompts
|
prompts: workflow.prompts
|
||||||
.map(p => {
|
.map(p => {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { MCPServer } from "./types";
|
||||||
|
|
||||||
export const Project = z.object({
|
export const Project = z.object({
|
||||||
_id: z.string().uuid(),
|
_id: z.string().uuid(),
|
||||||
|
|
@ -12,10 +13,7 @@ export const Project = z.object({
|
||||||
publishedWorkflowId: z.string().optional(),
|
publishedWorkflowId: z.string().optional(),
|
||||||
nextWorkflowNumber: z.number().optional(),
|
nextWorkflowNumber: z.number().optional(),
|
||||||
testRunCounter: z.number().default(0),
|
testRunCounter: z.number().default(0),
|
||||||
mcpServers: z.array(z.object({
|
mcpServers: z.array(MCPServer).optional(),
|
||||||
name: z.string(),
|
|
||||||
url: z.string(),
|
|
||||||
})).optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ProjectMember = z.object({
|
export const ProjectMember = z.object({
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,11 @@ import { CoreMessage, ToolCallPart } from "ai";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { apiV1 } from "rowboat-shared";
|
import { apiV1 } from "rowboat-shared";
|
||||||
|
|
||||||
|
export const MCPServer = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
url: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
export const PlaygroundChat = z.object({
|
export const PlaygroundChat = z.object({
|
||||||
createdAt: z.string().datetime(),
|
createdAt: z.string().datetime(),
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
|
|
|
||||||
|
|
@ -1,136 +1,8 @@
|
||||||
import { convertFromAgenticAPIChatMessages } from "./types/agents_api_types";
|
import { AgenticAPIChatResponse, AgenticAPIChatRequest, AgenticAPIChatMessage } 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 { z } from "zod";
|
import { z } from "zod";
|
||||||
import { dataSourceDocsCollection, dataSourcesCollection, projectsCollection } from "./mongodb";
|
import { generateObject } from "ai";
|
||||||
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 { ApiMessage } from "./types/types";
|
import { ApiMessage } from "./types/types";
|
||||||
import { openai } from "@ai-sdk/openai";
|
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(
|
export async function getAgenticApiResponse(
|
||||||
request: z.infer<typeof AgenticAPIChatRequest>,
|
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
|
// create a PrefixLogger class that wraps console.log with a prefix
|
||||||
// and allows chaining with a parent logger
|
// and allows chaining with a parent logger
|
||||||
export class PrefixLogger {
|
export class PrefixLogger {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { z } from "zod";
|
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 { Workflow } from "../../../lib/types/workflow_types";
|
||||||
import { Chat } from "./chat";
|
import { Chat } from "./chat";
|
||||||
import { ActionButton, Pane } from "../workflow/pane";
|
import { ActionButton, Pane } from "../workflow/pane";
|
||||||
|
|
@ -17,11 +17,15 @@ export function App({
|
||||||
projectId,
|
projectId,
|
||||||
workflow,
|
workflow,
|
||||||
messageSubscriber,
|
messageSubscriber,
|
||||||
|
mcpServerUrls,
|
||||||
|
toolWebhookUrl,
|
||||||
}: {
|
}: {
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
workflow: z.infer<typeof Workflow>;
|
workflow: z.infer<typeof Workflow>;
|
||||||
messageSubscriber?: (messages: z.infer<typeof apiV1.ChatMessage>[]) => void;
|
messageSubscriber?: (messages: z.infer<typeof apiV1.ChatMessage>[]) => void;
|
||||||
|
mcpServerUrls: Array<z.infer<typeof MCPServer>>;
|
||||||
|
toolWebhookUrl: string;
|
||||||
}) {
|
}) {
|
||||||
const [counter, setCounter] = useState<number>(0);
|
const [counter, setCounter] = useState<number>(0);
|
||||||
const [testProfile, setTestProfile] = useState<z.infer<typeof TestProfile> | null>(null);
|
const [testProfile, setTestProfile] = useState<z.infer<typeof TestProfile> | null>(null);
|
||||||
|
|
@ -84,6 +88,8 @@ export function App({
|
||||||
onTestProfileChange={handleTestProfileChange}
|
onTestProfileChange={handleTestProfileChange}
|
||||||
systemMessage={systemMessage}
|
systemMessage={systemMessage}
|
||||||
onSystemMessageChange={handleSystemMessageChange}
|
onSystemMessageChange={handleSystemMessageChange}
|
||||||
|
mcpServerUrls={mcpServerUrls}
|
||||||
|
toolWebhookUrl={toolWebhookUrl}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Pane>
|
</Pane>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { getAssistantResponse, simulateUserResponse } from "../../../actions/actions";
|
import { getAssistantResponse } from "../../../actions/actions";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Messages } from "./messages";
|
import { Messages } from "./messages";
|
||||||
import z from "zod";
|
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 { convertToAgenticAPIChatMessages } from "../../../lib/types/agents_api_types";
|
||||||
import { convertWorkflowToAgenticAPI } from "../../../lib/types/agents_api_types";
|
import { convertWorkflowToAgenticAPI } from "../../../lib/types/agents_api_types";
|
||||||
import { AgenticAPIChatRequest } 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 { TestProfile } from "@/app/lib/types/testing_types";
|
||||||
import { ProfileSelector } from "@/app/projects/[projectId]/test/[[...slug]]/components/selectors/profile-selector";
|
import { ProfileSelector } from "@/app/projects/[projectId]/test/[[...slug]]/components/selectors/profile-selector";
|
||||||
import { WithStringId } from "@/app/lib/types/types";
|
import { WithStringId } from "@/app/lib/types/types";
|
||||||
import { XCircleIcon, XIcon } from "lucide-react";
|
import { XIcon } from "lucide-react";
|
||||||
|
|
||||||
export function Chat({
|
export function Chat({
|
||||||
chat,
|
chat,
|
||||||
|
|
@ -26,6 +26,8 @@ export function Chat({
|
||||||
onTestProfileChange,
|
onTestProfileChange,
|
||||||
systemMessage,
|
systemMessage,
|
||||||
onSystemMessageChange,
|
onSystemMessageChange,
|
||||||
|
mcpServerUrls,
|
||||||
|
toolWebhookUrl,
|
||||||
}: {
|
}: {
|
||||||
chat: z.infer<typeof PlaygroundChat>;
|
chat: z.infer<typeof PlaygroundChat>;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
|
@ -35,6 +37,8 @@ export function Chat({
|
||||||
onTestProfileChange: (profile: WithStringId<z.infer<typeof TestProfile>> | null) => void;
|
onTestProfileChange: (profile: WithStringId<z.infer<typeof TestProfile>> | null) => void;
|
||||||
systemMessage: string;
|
systemMessage: string;
|
||||||
onSystemMessageChange: (message: string) => void;
|
onSystemMessageChange: (message: string) => void;
|
||||||
|
mcpServerUrls: Array<z.infer<typeof MCPServer>>;
|
||||||
|
toolWebhookUrl: string;
|
||||||
}) {
|
}) {
|
||||||
const [messages, setMessages] = useState<z.infer<typeof apiV1.ChatMessage>[]>(chat.messages);
|
const [messages, setMessages] = useState<z.infer<typeof apiV1.ChatMessage>[]>(chat.messages);
|
||||||
const [loadingAssistantResponse, setLoadingAssistantResponse] = useState<boolean>(false);
|
const [loadingAssistantResponse, setLoadingAssistantResponse] = useState<boolean>(false);
|
||||||
|
|
@ -68,15 +72,6 @@ export function Chat({
|
||||||
setFetchResponseError(null);
|
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
|
// reset state when workflow changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMessages([]);
|
setMessages([]);
|
||||||
|
|
@ -113,6 +108,9 @@ export function Chat({
|
||||||
tools,
|
tools,
|
||||||
prompts,
|
prompts,
|
||||||
startAgent,
|
startAgent,
|
||||||
|
mcpServers: mcpServerUrls,
|
||||||
|
toolWebhookUrl: toolWebhookUrl,
|
||||||
|
testProfile: testProfile ?? undefined,
|
||||||
};
|
};
|
||||||
setLastAgenticRequest(null);
|
setLastAgenticRequest(null);
|
||||||
setLastAgenticResponse(null);
|
setLastAgenticResponse(null);
|
||||||
|
|
@ -164,105 +162,7 @@ export function Chat({
|
||||||
return () => {
|
return () => {
|
||||||
ignore = true;
|
ignore = true;
|
||||||
};
|
};
|
||||||
}, [chat.simulated, messages, projectId, agenticState, workflow, fetchResponseError, systemMessage, simulationComplete]);
|
}, [chat.simulated, messages, projectId, agenticState, workflow, fetchResponseError, systemMessage, simulationComplete, mcpServerUrls, toolWebhookUrl, testProfile]);
|
||||||
|
|
||||||
// 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]);
|
|
||||||
|
|
||||||
const handleCopyChat = () => {
|
const handleCopyChat = () => {
|
||||||
const jsonString = JSON.stringify({
|
const jsonString = JSON.stringify({
|
||||||
|
|
@ -303,7 +203,6 @@ export function Chat({
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
messages={messages}
|
messages={messages}
|
||||||
toolCallResults={toolCallResults}
|
toolCallResults={toolCallResults}
|
||||||
handleToolCallResults={handleToolCallResults}
|
|
||||||
loadingAssistantResponse={loadingAssistantResponse}
|
loadingAssistantResponse={loadingAssistantResponse}
|
||||||
loadingUserResponse={loadingUserResponse}
|
loadingUserResponse={loadingUserResponse}
|
||||||
workflow={workflow}
|
workflow={workflow}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,14 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { Button, Spinner, Textarea } from "@heroui/react";
|
import { Spinner } from "@heroui/react";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { Workflow } from "../../../lib/types/workflow_types";
|
import { Workflow } from "../../../lib/types/workflow_types";
|
||||||
import { WorkflowTool } 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 MarkdownContent from "../../../lib/components/markdown-content";
|
||||||
import { apiV1 } from "rowboat-shared";
|
import { apiV1 } from "rowboat-shared";
|
||||||
import { EditableField } from "../../../lib/components/editable-field";
|
import { EditableField } from "../../../lib/components/editable-field";
|
||||||
import { MessageSquareIcon, EllipsisIcon, CircleCheckIcon, ChevronRightIcon, ChevronDownIcon, XIcon } from "lucide-react";
|
import { MessageSquareIcon, EllipsisIcon, CircleCheckIcon, ChevronRightIcon, ChevronDownIcon, XIcon } from "lucide-react";
|
||||||
import { TestProfile } from "@/app/lib/types/testing_types";
|
import { TestProfile } from "@/app/lib/types/testing_types";
|
||||||
import { executeMcpTool } from "@/app/actions/mcp_actions";
|
|
||||||
|
|
||||||
function UserMessage({ content }: { content: string }) {
|
function UserMessage({ content }: { content: string }) {
|
||||||
return <div className="self-end ml-[30%] flex flex-col">
|
return <div className="self-end ml-[30%] flex flex-col">
|
||||||
|
|
@ -88,7 +85,6 @@ function UserMessageLoading() {
|
||||||
function ToolCalls({
|
function ToolCalls({
|
||||||
toolCalls,
|
toolCalls,
|
||||||
results,
|
results,
|
||||||
handleResults,
|
|
||||||
projectId,
|
projectId,
|
||||||
messages,
|
messages,
|
||||||
sender,
|
sender,
|
||||||
|
|
@ -98,7 +94,6 @@ function ToolCalls({
|
||||||
}: {
|
}: {
|
||||||
toolCalls: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'];
|
toolCalls: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'];
|
||||||
results: Record<string, z.infer<typeof apiV1.ToolMessage>>;
|
results: Record<string, z.infer<typeof apiV1.ToolMessage>>;
|
||||||
handleResults: (results: z.infer<typeof apiV1.ToolMessage>[]) => void;
|
|
||||||
projectId: string;
|
projectId: string;
|
||||||
messages: z.infer<typeof apiV1.ChatMessage>[];
|
messages: z.infer<typeof apiV1.ChatMessage>[];
|
||||||
sender: string | null | undefined;
|
sender: string | null | undefined;
|
||||||
|
|
@ -108,21 +103,12 @@ function ToolCalls({
|
||||||
}) {
|
}) {
|
||||||
const resultsMap: Record<string, z.infer<typeof apiV1.ToolMessage>> = {};
|
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">
|
return <div className="flex flex-col gap-4">
|
||||||
{toolCalls.map(toolCall => {
|
{toolCalls.map(toolCall => {
|
||||||
return <ToolCall
|
return <ToolCall
|
||||||
key={toolCall.id}
|
key={toolCall.id}
|
||||||
toolCall={toolCall}
|
toolCall={toolCall}
|
||||||
result={results[toolCall.id]}
|
result={results[toolCall.id]}
|
||||||
handleResult={handleToolCallResult}
|
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
messages={messages}
|
messages={messages}
|
||||||
sender={sender}
|
sender={sender}
|
||||||
|
|
@ -137,7 +123,6 @@ function ToolCalls({
|
||||||
function ToolCall({
|
function ToolCall({
|
||||||
toolCall,
|
toolCall,
|
||||||
result,
|
result,
|
||||||
handleResult,
|
|
||||||
projectId,
|
projectId,
|
||||||
messages,
|
messages,
|
||||||
sender,
|
sender,
|
||||||
|
|
@ -147,7 +132,6 @@ function ToolCall({
|
||||||
}: {
|
}: {
|
||||||
toolCall: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'][number];
|
toolCall: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'][number];
|
||||||
result: z.infer<typeof apiV1.ToolMessage> | undefined;
|
result: z.infer<typeof apiV1.ToolMessage> | undefined;
|
||||||
handleResult: (result: z.infer<typeof apiV1.ToolMessage>) => void;
|
|
||||||
projectId: string;
|
projectId: string;
|
||||||
messages: z.infer<typeof apiV1.ChatMessage>[];
|
messages: z.infer<typeof apiV1.ChatMessage>[];
|
||||||
sender: string | null | undefined;
|
sender: string | null | undefined;
|
||||||
|
|
@ -163,63 +147,17 @@ function ToolCall({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (toolCall.function.name) {
|
if (toolCall.function.name.startsWith('transfer_to_')) {
|
||||||
case 'getArticleInfo':
|
return <TransferToAgentToolCall
|
||||||
return <GetInformationToolCall
|
result={result}
|
||||||
toolCall={toolCall}
|
sender={sender}
|
||||||
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}
|
|
||||||
/>;
|
|
||||||
}
|
}
|
||||||
|
return <ClientToolCall
|
||||||
|
toolCall={toolCall}
|
||||||
|
result={result}
|
||||||
|
sender={sender}
|
||||||
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToolCallHeader({
|
function ToolCallHeader({
|
||||||
|
|
@ -240,105 +178,11 @@ function ToolCallHeader({
|
||||||
</div>;
|
</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({
|
function TransferToAgentToolCall({
|
||||||
toolCall,
|
|
||||||
result: availableResult,
|
result: availableResult,
|
||||||
handleResult,
|
|
||||||
projectId,
|
|
||||||
messages,
|
|
||||||
sender,
|
sender,
|
||||||
}: {
|
}: {
|
||||||
toolCall: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'][number];
|
|
||||||
result: z.infer<typeof apiV1.ToolMessage> | undefined;
|
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;
|
sender: string | null | undefined;
|
||||||
}) {
|
}) {
|
||||||
const typedResult = availableResult ? JSON.parse(availableResult.content) as { assistant: string } : undefined;
|
const typedResult = availableResult ? JSON.parse(availableResult.content) as { assistant: string } : undefined;
|
||||||
|
|
@ -355,282 +199,28 @@ function TransferToAgentToolCall({
|
||||||
</div>;
|
</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({
|
function ClientToolCall({
|
||||||
toolCall,
|
toolCall,
|
||||||
result: availableResult,
|
result: availableResult,
|
||||||
handleResult,
|
|
||||||
projectId,
|
|
||||||
messages,
|
|
||||||
sender,
|
sender,
|
||||||
}: {
|
}: {
|
||||||
toolCall: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'][number];
|
toolCall: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'][number];
|
||||||
result: z.infer<typeof apiV1.ToolMessage> | undefined;
|
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;
|
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">
|
return <div className="flex flex-col gap-1">
|
||||||
{sender && <div className='text-gray-500 text-sm ml-3'>{sender}</div>}
|
{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%]'>
|
<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'>
|
<div className='flex flex-col gap-2'>
|
||||||
<ExpandableContent label='Params' content={toolCall.function.arguments} expanded={false} />
|
<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>
|
</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({
|
function ExpandableContent({
|
||||||
label,
|
label,
|
||||||
content,
|
content,
|
||||||
|
|
@ -701,7 +291,6 @@ export function Messages({
|
||||||
projectId,
|
projectId,
|
||||||
messages,
|
messages,
|
||||||
toolCallResults,
|
toolCallResults,
|
||||||
handleToolCallResults,
|
|
||||||
loadingAssistantResponse,
|
loadingAssistantResponse,
|
||||||
loadingUserResponse,
|
loadingUserResponse,
|
||||||
workflow,
|
workflow,
|
||||||
|
|
@ -712,7 +301,6 @@ export function Messages({
|
||||||
projectId: string;
|
projectId: string;
|
||||||
messages: z.infer<typeof apiV1.ChatMessage>[];
|
messages: z.infer<typeof apiV1.ChatMessage>[];
|
||||||
toolCallResults: Record<string, z.infer<typeof apiV1.ToolMessage>>;
|
toolCallResults: Record<string, z.infer<typeof apiV1.ToolMessage>>;
|
||||||
handleToolCallResults: (results: z.infer<typeof apiV1.ToolMessage>[]) => void;
|
|
||||||
loadingAssistantResponse: boolean;
|
loadingAssistantResponse: boolean;
|
||||||
loadingUserResponse: boolean;
|
loadingUserResponse: boolean;
|
||||||
workflow: z.infer<typeof Workflow>;
|
workflow: z.infer<typeof Workflow>;
|
||||||
|
|
@ -742,7 +330,6 @@ export function Messages({
|
||||||
key={index}
|
key={index}
|
||||||
toolCalls={message.tool_calls}
|
toolCalls={message.tool_calls}
|
||||||
results={toolCallResults}
|
results={toolCallResults}
|
||||||
handleResults={handleToolCallResults}
|
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
messages={messages}
|
messages={messages}
|
||||||
sender={message.agenticSender}
|
sender={message.agenticSender}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { WithStringId } from "../../../lib/types/types";
|
import { MCPServer, WithStringId } from "../../../lib/types/types";
|
||||||
import { Workflow } from "../../../lib/types/workflow_types";
|
import { Workflow } from "../../../lib/types/workflow_types";
|
||||||
import { DataSource } from "../../../lib/types/datasource_types";
|
import { DataSource } from "../../../lib/types/datasource_types";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
@ -13,9 +13,13 @@ import { listDataSources } from "../../../actions/datasource_actions";
|
||||||
export function App({
|
export function App({
|
||||||
projectId,
|
projectId,
|
||||||
useRag,
|
useRag,
|
||||||
|
mcpServerUrls,
|
||||||
|
toolWebhookUrl,
|
||||||
}: {
|
}: {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
useRag: boolean;
|
useRag: boolean;
|
||||||
|
mcpServerUrls: Array<z.infer<typeof MCPServer>>;
|
||||||
|
toolWebhookUrl: string;
|
||||||
}) {
|
}) {
|
||||||
const [selectorKey, setSelectorKey] = useState(0);
|
const [selectorKey, setSelectorKey] = useState(0);
|
||||||
const [workflow, setWorkflow] = useState<WithStringId<z.infer<typeof Workflow>> | null>(null);
|
const [workflow, setWorkflow] = useState<WithStringId<z.infer<typeof Workflow>> | null>(null);
|
||||||
|
|
@ -108,6 +112,8 @@ export function App({
|
||||||
handleShowSelector={handleShowSelector}
|
handleShowSelector={handleShowSelector}
|
||||||
handleCloneVersion={handleCloneVersion}
|
handleCloneVersion={handleCloneVersion}
|
||||||
useRag={useRag}
|
useRag={useRag}
|
||||||
|
mcpServerUrls={mcpServerUrls}
|
||||||
|
toolWebhookUrl={toolWebhookUrl}
|
||||||
/>}
|
/>}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { App } from "./app";
|
import { App } from "./app";
|
||||||
import { USE_RAG } from "@/app/lib/feature_flags";
|
import { USE_RAG } from "@/app/lib/feature_flags";
|
||||||
|
import { projectsCollection } from "@/app/lib/mongodb";
|
||||||
export const dynamic = 'force-dynamic';
|
import { notFound } from "next/navigation";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Workflow"
|
title: "Workflow"
|
||||||
|
|
@ -13,8 +13,18 @@ export default async function Page({
|
||||||
}: {
|
}: {
|
||||||
params: { projectId: string };
|
params: { projectId: string };
|
||||||
}) {
|
}) {
|
||||||
|
const project = await projectsCollection.findOne({
|
||||||
|
_id: params.projectId,
|
||||||
|
});
|
||||||
|
if (!project) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
const toolWebhookUrl = project.webhookUrl ?? '';
|
||||||
|
|
||||||
return <App
|
return <App
|
||||||
projectId={params.projectId}
|
projectId={params.projectId}
|
||||||
useRag={USE_RAG}
|
useRag={USE_RAG}
|
||||||
|
mcpServerUrls={project.mcpServers ?? []}
|
||||||
|
toolWebhookUrl={toolWebhookUrl}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { WithStringId } from "../../../lib/types/types";
|
import { MCPServer, WithStringId } from "../../../lib/types/types";
|
||||||
import { Workflow } from "../../../lib/types/workflow_types";
|
import { Workflow } from "../../../lib/types/workflow_types";
|
||||||
import { WorkflowTool } from "../../../lib/types/workflow_types";
|
import { WorkflowTool } from "../../../lib/types/workflow_types";
|
||||||
import { WorkflowPrompt } from "../../../lib/types/workflow_types";
|
import { WorkflowPrompt } from "../../../lib/types/workflow_types";
|
||||||
|
|
@ -559,6 +559,8 @@ export function WorkflowEditor({
|
||||||
handleShowSelector,
|
handleShowSelector,
|
||||||
handleCloneVersion,
|
handleCloneVersion,
|
||||||
useRag,
|
useRag,
|
||||||
|
mcpServerUrls,
|
||||||
|
toolWebhookUrl,
|
||||||
}: {
|
}: {
|
||||||
dataSources: WithStringId<z.infer<typeof DataSource>>[];
|
dataSources: WithStringId<z.infer<typeof DataSource>>[];
|
||||||
workflow: WithStringId<z.infer<typeof Workflow>>;
|
workflow: WithStringId<z.infer<typeof Workflow>>;
|
||||||
|
|
@ -566,6 +568,8 @@ export function WorkflowEditor({
|
||||||
handleShowSelector: () => void;
|
handleShowSelector: () => void;
|
||||||
handleCloneVersion: (workflowId: string) => void;
|
handleCloneVersion: (workflowId: string) => void;
|
||||||
useRag: boolean;
|
useRag: boolean;
|
||||||
|
mcpServerUrls: Array<z.infer<typeof MCPServer>>;
|
||||||
|
toolWebhookUrl: string;
|
||||||
}) {
|
}) {
|
||||||
const [state, dispatch] = useReducer<Reducer<State, Action>>(reducer, {
|
const [state, dispatch] = useReducer<Reducer<State, Action>>(reducer, {
|
||||||
patches: [],
|
patches: [],
|
||||||
|
|
@ -911,6 +915,8 @@ export function WorkflowEditor({
|
||||||
projectId={state.present.workflow.projectId}
|
projectId={state.present.workflow.projectId}
|
||||||
workflow={state.present.workflow}
|
workflow={state.present.workflow}
|
||||||
messageSubscriber={updateChatMessages}
|
messageSubscriber={updateChatMessages}
|
||||||
|
mcpServerUrls={mcpServerUrls}
|
||||||
|
toolWebhookUrl={toolWebhookUrl}
|
||||||
/>
|
/>
|
||||||
{state.present.selection?.type === "agent" && <AgentConfig
|
{state.present.selection?.type === "agent" && <AgentConfig
|
||||||
key={state.present.selection.name}
|
key={state.present.selection.name}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue