mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-08 06:42:39 +02:00
refactor tools UX: part 1
This commit is contained in:
parent
cccd383b92
commit
751a86c34d
21 changed files with 1462 additions and 258 deletions
|
|
@ -3,6 +3,9 @@ import { z } from "zod";
|
|||
import {
|
||||
listToolkits as libListToolkits,
|
||||
listTools as libListTools,
|
||||
searchTools as libSearchTools,
|
||||
getToolsByIds as libGetToolsByIds,
|
||||
getTool as libGetTool,
|
||||
getConnectedAccount as libGetConnectedAccount,
|
||||
deleteConnectedAccount as libDeleteConnectedAccount,
|
||||
listAuthConfigs as libListAuthConfigs,
|
||||
|
|
@ -20,6 +23,7 @@ import {
|
|||
ZCredentials,
|
||||
} from "@/app/lib/composio/composio";
|
||||
import { ComposioConnectedAccount } from "@/app/lib/types/project_types";
|
||||
import { WorkflowTool } from "@/app/lib/types/workflow_types";
|
||||
import { getProjectConfig, projectAuthCheck } from "./project_actions";
|
||||
import { projectsCollection } from "../lib/mongodb";
|
||||
|
||||
|
|
@ -47,6 +51,24 @@ export async function listTools(projectId: string, toolkitSlug: string, cursor:
|
|||
return await libListTools(toolkitSlug, cursor);
|
||||
}
|
||||
|
||||
// New efficient search functions
|
||||
|
||||
export async function searchTools(projectId: string, searchQuery: string, cursor: string | null = null, limit?: number): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>> {
|
||||
await projectAuthCheck(projectId);
|
||||
return await libSearchTools(searchQuery, cursor, limit);
|
||||
}
|
||||
|
||||
export async function getToolsByIds(projectId: string, toolSlugs: string[], cursor: string | null = null): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>> {
|
||||
await projectAuthCheck(projectId);
|
||||
return await libGetToolsByIds(toolSlugs, cursor);
|
||||
}
|
||||
|
||||
export async function getTool(projectId: string, toolSlug: string): Promise<z.infer<typeof ZTool>> {
|
||||
await projectAuthCheck(projectId);
|
||||
return await libGetTool(toolSlug);
|
||||
}
|
||||
|
||||
|
||||
export async function createComposioManagedOauth2ConnectedAccount(projectId: string, toolkitSlug: string, callbackUrl: string): Promise<z.infer<typeof ZCreateConnectedAccountResponse>> {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
|
|
@ -215,12 +237,196 @@ export async function deleteConnectedAccount(projectId: string, toolkitSlug: str
|
|||
const key = `composioConnectedAccounts.${toolkitSlug}`;
|
||||
await projectsCollection.updateOne({ _id: projectId }, { $unset: { [key]: "" } });
|
||||
|
||||
// Notify other tabs about the tools update (lightweight refresh)
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(`tools-light-refresh-${projectId}`, Date.now().toString());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Note: composio tools are now stored in workflow.tools array with isComposio: true
|
||||
// This function provides backward compatibility by updating workflow tools
|
||||
export async function getComposioToolsFromWorkflow(projectId: string): Promise<z.infer<typeof ZTool>[]> {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
// Get the project to access draft workflow
|
||||
const project = await projectsCollection.findOne({ _id: projectId });
|
||||
if (!project || !project.draftWorkflow) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Extract composio tools from workflow and convert back to ZTool format
|
||||
const composioTools = project.draftWorkflow.tools
|
||||
.filter(tool => tool.isComposio && tool.composioData)
|
||||
.map(tool => ({
|
||||
slug: tool.composioData!.slug,
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
no_auth: tool.composioData!.noAuth,
|
||||
input_parameters: {
|
||||
type: 'object' as const,
|
||||
properties: tool.parameters.properties,
|
||||
required: tool.parameters.required || []
|
||||
},
|
||||
toolkit: {
|
||||
name: tool.composioData!.toolkitName,
|
||||
slug: tool.composioData!.toolkitSlug,
|
||||
logo: tool.composioData!.logo,
|
||||
}
|
||||
}));
|
||||
|
||||
return composioTools;
|
||||
}
|
||||
|
||||
export async function updateComposioSelectedTools(projectId: string, tools: z.infer<typeof ZTool>[]): Promise<void> {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
// update project with new selected tools
|
||||
await projectsCollection.updateOne({ _id: projectId }, { $set: { composioSelectedTools: tools } });
|
||||
// Get the project to access draft workflow
|
||||
const project = await projectsCollection.findOne({ _id: projectId });
|
||||
if (!project || !project.draftWorkflow) {
|
||||
throw new Error(`Project ${projectId} not found or has no draft workflow`);
|
||||
}
|
||||
|
||||
// Convert Composio tools to workflow tool format
|
||||
const composioWorkflowTools: z.infer<typeof WorkflowTool>[] = tools.map(tool => ({
|
||||
name: tool.slug,
|
||||
description: tool.description || "",
|
||||
parameters: {
|
||||
type: 'object' as const,
|
||||
properties: tool.input_parameters?.properties || {},
|
||||
required: tool.input_parameters?.required || []
|
||||
},
|
||||
isComposio: true,
|
||||
composioData: {
|
||||
slug: tool.slug,
|
||||
noAuth: tool.no_auth,
|
||||
toolkitName: tool.toolkit.name,
|
||||
toolkitSlug: tool.toolkit.slug,
|
||||
logo: tool.toolkit.logo,
|
||||
},
|
||||
}));
|
||||
|
||||
// Remove existing composio tools and add new ones
|
||||
const nonComposioTools = project.draftWorkflow.tools.filter(tool => !tool.isComposio);
|
||||
const updatedWorkflow = {
|
||||
...project.draftWorkflow,
|
||||
tools: [...nonComposioTools, ...composioWorkflowTools],
|
||||
lastUpdatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Update the project's draft workflow
|
||||
const result = await projectsCollection.updateOne(
|
||||
{ _id: projectId },
|
||||
{ $set: { draftWorkflow: updatedWorkflow } }
|
||||
);
|
||||
|
||||
if (result.modifiedCount === 0) {
|
||||
throw new Error(`Failed to update workflow for project ${projectId}`);
|
||||
}
|
||||
|
||||
// Notify other tabs about the tools update (lightweight refresh)
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(`tools-light-refresh-${projectId}`, Date.now().toString());
|
||||
}
|
||||
}
|
||||
|
||||
// Note: composio mock states are now stored in workflow.composioMockToolkitStates
|
||||
// This function provides backward compatibility by updating workflow mock states
|
||||
export async function toggleMockToolkitState(projectId: string, toolkitSlug: string, isMocked: boolean, mockInstructions?: string): Promise<void> {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
// Get the project to access draft workflow
|
||||
const project = await projectsCollection.findOne({ _id: projectId });
|
||||
if (!project || !project.draftWorkflow) {
|
||||
throw new Error(`Project ${projectId} not found or has no draft workflow`);
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
let updatedMockToolkitStates = { ...(project.draftWorkflow.composioMockToolkitStates || {}) };
|
||||
|
||||
if (isMocked) {
|
||||
// Enable mock mode
|
||||
updatedMockToolkitStates[toolkitSlug] = {
|
||||
toolkitSlug,
|
||||
isMocked: true,
|
||||
mockInstructions: mockInstructions || 'Mock responses using GPT-4.1 based on tool descriptions.',
|
||||
autoSubmitMockedResponse: false,
|
||||
createdAt: now,
|
||||
lastUpdatedAt: now,
|
||||
};
|
||||
} else {
|
||||
// Disable mock mode - remove the toolkit from the object
|
||||
delete updatedMockToolkitStates[toolkitSlug];
|
||||
}
|
||||
|
||||
// Update the workflow with new mock states
|
||||
const updatedWorkflow = {
|
||||
...project.draftWorkflow,
|
||||
composioMockToolkitStates: updatedMockToolkitStates,
|
||||
lastUpdatedAt: now
|
||||
};
|
||||
|
||||
// Update the project's draft workflow
|
||||
const result = await projectsCollection.updateOne(
|
||||
{ _id: projectId },
|
||||
{ $set: { draftWorkflow: updatedWorkflow } }
|
||||
);
|
||||
|
||||
if (result.modifiedCount === 0) {
|
||||
throw new Error(`Failed to update workflow mock states for project ${projectId}`);
|
||||
}
|
||||
|
||||
// Notify other tabs about the tools update (lightweight refresh)
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(`tools-light-refresh-${projectId}`, Date.now().toString());
|
||||
}
|
||||
}
|
||||
|
||||
// Note: composio mock states are now stored in workflow.composioMockToolkitStates
|
||||
// This function provides backward compatibility by updating workflow mock states
|
||||
export async function updateMockToolkitInstructions(projectId: string, toolkitSlug: string, mockInstructions: string): Promise<void> {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
// Get the project to access draft workflow
|
||||
const project = await projectsCollection.findOne({ _id: projectId });
|
||||
if (!project || !project.draftWorkflow) {
|
||||
throw new Error(`Project ${projectId} not found or has no draft workflow`);
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
let updatedMockToolkitStates = { ...(project.draftWorkflow.composioMockToolkitStates || {}) };
|
||||
|
||||
// Update the mock instructions for the specified toolkit
|
||||
if (updatedMockToolkitStates[toolkitSlug]) {
|
||||
updatedMockToolkitStates[toolkitSlug] = {
|
||||
...updatedMockToolkitStates[toolkitSlug],
|
||||
mockInstructions,
|
||||
lastUpdatedAt: now
|
||||
};
|
||||
|
||||
// Update the workflow with new mock states
|
||||
const updatedWorkflow = {
|
||||
...project.draftWorkflow,
|
||||
composioMockToolkitStates: updatedMockToolkitStates,
|
||||
lastUpdatedAt: now
|
||||
};
|
||||
|
||||
// Update the project's draft workflow
|
||||
const result = await projectsCollection.updateOne(
|
||||
{ _id: projectId },
|
||||
{ $set: { draftWorkflow: updatedWorkflow } }
|
||||
);
|
||||
|
||||
if (result.modifiedCount === 0) {
|
||||
throw new Error(`Failed to update workflow mock instructions for project ${projectId}`);
|
||||
}
|
||||
|
||||
// Notify other tabs about the tools update
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(`tools-updated-${projectId}`, Date.now().toString());
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Mock toolkit state for ${toolkitSlug} not found in project ${projectId}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,13 @@ import { authorizeUserAction } from "./billing_actions";
|
|||
import { Workflow } from "../lib/types/workflow_types";
|
||||
import { WorkflowTool } from "../lib/types/workflow_types";
|
||||
import { collectProjectTools as libCollectProjectTools } from "../lib/project_tools";
|
||||
import {
|
||||
searchTools as libSearchTools,
|
||||
getToolsByIds as libGetToolsByIds,
|
||||
getTool as libGetTool,
|
||||
ZTool,
|
||||
ZToolkit
|
||||
} from "../lib/composio/composio";
|
||||
|
||||
const KLAVIS_API_KEY = process.env.KLAVIS_API_KEY || '';
|
||||
|
||||
|
|
@ -306,6 +313,113 @@ export async function createProjectFromPrompt(formData: FormData): Promise<{ id:
|
|||
return { id: projectId };
|
||||
}
|
||||
|
||||
async function detectAndAddComposioTools(projectId: string, workflow: z.infer<typeof Workflow>) {
|
||||
// Extract tool mentions from agent instructions
|
||||
const toolMentionPattern = /\[@tool:([^\]]+)\]\(#mention[^\)]*\)/g;
|
||||
const mentionedToolNames = new Set<string>();
|
||||
|
||||
// Scan all agent instructions for tool mentions
|
||||
for (const agent of workflow.agents || []) {
|
||||
const instructions = agent.instructions || "";
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = toolMentionPattern.exec(instructions))) {
|
||||
mentionedToolNames.add(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (mentionedToolNames.size === 0) {
|
||||
return; // No tool mentions found
|
||||
}
|
||||
|
||||
console.log(`Found ${mentionedToolNames.size} tool mentions in workflow:`, Array.from(mentionedToolNames));
|
||||
|
||||
// Search for these tools in Composio using the new efficient search methods
|
||||
const foundTools: z.infer<typeof ZTool>[] = [];
|
||||
|
||||
try {
|
||||
// Method 1: Try to get tools directly by their exact slugs/names
|
||||
const mentionedToolNamesArray = Array.from(mentionedToolNames);
|
||||
|
||||
try {
|
||||
const directToolsResponse = await libGetToolsByIds(mentionedToolNamesArray);
|
||||
foundTools.push(...directToolsResponse.items);
|
||||
console.log(`Found ${directToolsResponse.items.length} tools by direct lookup`);
|
||||
} catch (error) {
|
||||
console.log('Direct tool lookup failed, trying search approach');
|
||||
}
|
||||
|
||||
// Method 2: For any remaining tools, use search functionality
|
||||
const foundToolSlugs = new Set(foundTools.map(tool => tool.slug));
|
||||
const foundToolNames = new Set(foundTools.map(tool => tool.name));
|
||||
const remainingToolNames = mentionedToolNamesArray.filter(name =>
|
||||
!foundToolSlugs.has(name) && !foundToolNames.has(name)
|
||||
);
|
||||
|
||||
for (const toolName of remainingToolNames) {
|
||||
try {
|
||||
// Search for tools by name/description
|
||||
const searchResponse = await libSearchTools(toolName, null, 10);
|
||||
|
||||
// Find exact matches by name or slug
|
||||
const exactMatches = searchResponse.items.filter(tool =>
|
||||
tool.name === toolName ||
|
||||
tool.slug === toolName ||
|
||||
tool.name.toLowerCase() === toolName.toLowerCase() ||
|
||||
tool.slug.toLowerCase() === toolName.toLowerCase()
|
||||
);
|
||||
|
||||
if (exactMatches.length > 0) {
|
||||
foundTools.push(...exactMatches);
|
||||
console.log(`Found ${exactMatches.length} tools for search term "${toolName}"`);
|
||||
} else {
|
||||
console.log(`No exact matches found for tool "${toolName}"`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error searching for tool "${toolName}":`, error);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error searching for Composio tools:', error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (foundTools.length > 0) {
|
||||
console.log(`Adding ${foundTools.length} Composio tools to workflow`);
|
||||
|
||||
// Remove duplicates based on slug
|
||||
const uniqueTools = foundTools.filter((tool, index, self) =>
|
||||
index === self.findIndex(t => t.slug === tool.slug)
|
||||
);
|
||||
|
||||
// Convert Composio tools to workflow tool format
|
||||
const composioWorkflowTools: z.infer<typeof WorkflowTool>[] = uniqueTools.map(tool => ({
|
||||
name: tool.slug,
|
||||
description: tool.description || "",
|
||||
parameters: {
|
||||
type: 'object' as const,
|
||||
properties: tool.input_parameters?.properties || {},
|
||||
required: tool.input_parameters?.required || []
|
||||
},
|
||||
isComposio: true,
|
||||
composioData: {
|
||||
slug: tool.slug,
|
||||
noAuth: tool.no_auth,
|
||||
toolkitName: tool.toolkit.name,
|
||||
toolkitSlug: tool.toolkit.slug,
|
||||
logo: tool.toolkit.logo,
|
||||
},
|
||||
}));
|
||||
|
||||
// Add these tools to the workflow.tools array
|
||||
workflow.tools = [...workflow.tools, ...composioWorkflowTools];
|
||||
|
||||
console.log(`Added ${composioWorkflowTools.length} Composio tools to workflow`);
|
||||
} else {
|
||||
console.log('No matching Composio tools found for the mentioned tool names');
|
||||
}
|
||||
}
|
||||
|
||||
export async function createProjectFromWorkflowJson(formData: FormData): Promise<{ id: string } | { billingError: string }> {
|
||||
const user = await authCheck();
|
||||
const workflowJson = formData.get('workflowJson') as string;
|
||||
|
|
@ -327,6 +441,15 @@ export async function createProjectFromWorkflowJson(formData: FormData): Promise
|
|||
return response;
|
||||
}
|
||||
const projectId = response.id;
|
||||
|
||||
// Automatically detect and add Composio tools mentioned in agent instructions
|
||||
try {
|
||||
await detectAndAddComposioTools(projectId, workflow);
|
||||
} catch (error) {
|
||||
// Log error but don't fail the import if tool detection fails
|
||||
console.error('Failed to auto-detect Composio tools:', error);
|
||||
}
|
||||
|
||||
return { id: projectId };
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue