mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-25 00:16:29 +02:00
add composio tools
This commit is contained in:
parent
8038d52495
commit
078f785a9e
27 changed files with 2514 additions and 140 deletions
226
apps/rowboat/app/actions/composio_actions.ts
Normal file
226
apps/rowboat/app/actions/composio_actions.ts
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
"use server";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
listToolkits as libListToolkits,
|
||||
listTools as libListTools,
|
||||
getConnectedAccount as libGetConnectedAccount,
|
||||
deleteConnectedAccount as libDeleteConnectedAccount,
|
||||
listAuthConfigs as libListAuthConfigs,
|
||||
createAuthConfig as libCreateAuthConfig,
|
||||
getToolkit as libGetToolkit,
|
||||
createConnectedAccount as libCreateConnectedAccount,
|
||||
getAuthConfig as libGetAuthConfig,
|
||||
deleteAuthConfig as libDeleteAuthConfig,
|
||||
ZToolkit,
|
||||
ZGetToolkitResponse,
|
||||
ZTool,
|
||||
ZListResponse,
|
||||
ZCreateConnectedAccountResponse,
|
||||
ZAuthScheme,
|
||||
ZCredentials,
|
||||
} from "@/app/lib/composio/composio";
|
||||
import { ComposioConnectedAccount } from "@/app/lib/types/project_types";
|
||||
import { getProjectConfig, projectAuthCheck } from "./project_actions";
|
||||
import { projectsCollection } from "../lib/mongodb";
|
||||
|
||||
const ZCreateCustomConnectedAccountRequest = z.object({
|
||||
toolkitSlug: z.string(),
|
||||
authConfig: z.object({
|
||||
authScheme: ZAuthScheme,
|
||||
credentials: ZCredentials,
|
||||
}),
|
||||
callbackUrl: z.string(),
|
||||
});
|
||||
|
||||
export async function listToolkits(projectId: string, cursor: string | null = null): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZToolkit>>>> {
|
||||
await projectAuthCheck(projectId);
|
||||
return await libListToolkits(cursor);
|
||||
}
|
||||
|
||||
export async function getToolkit(projectId: string, toolkitSlug: string): Promise<z.infer<typeof ZGetToolkitResponse>> {
|
||||
await projectAuthCheck(projectId);
|
||||
return await libGetToolkit(toolkitSlug);
|
||||
}
|
||||
|
||||
export async function listTools(projectId: string, toolkitSlug: string, cursor: string | null = null): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>> {
|
||||
await projectAuthCheck(projectId);
|
||||
return await libListTools(toolkitSlug, cursor);
|
||||
}
|
||||
|
||||
export async function createComposioManagedOauth2ConnectedAccount(projectId: string, toolkitSlug: string, callbackUrl: string): Promise<z.infer<typeof ZCreateConnectedAccountResponse>> {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
// fetch managed auth configs
|
||||
const configs = await libListAuthConfigs(toolkitSlug, null, true);
|
||||
|
||||
// check if managed oauth2 config exists
|
||||
let authConfigId: string | undefined = undefined;
|
||||
const authConfig = configs.items.find(config => config.auth_scheme === 'OAUTH2' && config.is_composio_managed);
|
||||
authConfigId = authConfig?.id;
|
||||
if (!authConfig) {
|
||||
// create a new managed oauth2 auth config
|
||||
const newAuthConfig = await libCreateAuthConfig({
|
||||
toolkit: {
|
||||
slug: toolkitSlug,
|
||||
},
|
||||
auth_config: {
|
||||
type: 'use_composio_managed_auth',
|
||||
name: 'composio-managed-oauth2',
|
||||
},
|
||||
});
|
||||
authConfigId = newAuthConfig.auth_config.id;
|
||||
}
|
||||
if (!authConfigId) {
|
||||
throw new Error(`No managed oauth2 auth config found for toolkit ${toolkitSlug}`);
|
||||
}
|
||||
|
||||
// create new connected account
|
||||
const response = await libCreateConnectedAccount({
|
||||
auth_config: {
|
||||
id: authConfigId,
|
||||
},
|
||||
connection: {
|
||||
user_id: projectId,
|
||||
callback_url: callbackUrl,
|
||||
},
|
||||
});
|
||||
|
||||
// update project with new connected account
|
||||
const key = `composioConnectedAccounts.${toolkitSlug}`;
|
||||
const data: z.infer<typeof ComposioConnectedAccount> = {
|
||||
id: response.id,
|
||||
authConfigId: authConfigId,
|
||||
status: 'INITIATED',
|
||||
createdAt: new Date().toISOString(),
|
||||
lastUpdatedAt: new Date().toISOString(),
|
||||
}
|
||||
await projectsCollection.updateOne({ _id: projectId }, { $set: { [key]: data } });
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function createCustomConnectedAccount(projectId: string, request: z.infer<typeof ZCreateCustomConnectedAccountRequest>): Promise<z.infer<typeof ZCreateConnectedAccountResponse>> {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
// first, create the auth config
|
||||
const authConfig = await libCreateAuthConfig({
|
||||
toolkit: {
|
||||
slug: request.toolkitSlug,
|
||||
},
|
||||
auth_config: {
|
||||
type: 'use_custom_auth',
|
||||
authScheme: request.authConfig.authScheme,
|
||||
credentials: request.authConfig.credentials,
|
||||
name: `pid-${projectId}-${Date.now()}`,
|
||||
},
|
||||
});
|
||||
|
||||
// then, create the connected account
|
||||
let state = undefined;
|
||||
if (request.authConfig.authScheme !== 'OAUTH2') {
|
||||
state = {
|
||||
authScheme: request.authConfig.authScheme,
|
||||
val: {
|
||||
status: 'ACTIVE' as const,
|
||||
...request.authConfig.credentials,
|
||||
},
|
||||
};
|
||||
}
|
||||
const response = await libCreateConnectedAccount({
|
||||
auth_config: {
|
||||
id: authConfig.auth_config.id,
|
||||
},
|
||||
connection: {
|
||||
state,
|
||||
user_id: projectId,
|
||||
callback_url: request.callbackUrl,
|
||||
},
|
||||
});
|
||||
|
||||
// update project with new connected account
|
||||
const key = `composioConnectedAccounts.${request.toolkitSlug}`;
|
||||
const data: z.infer<typeof ComposioConnectedAccount> = {
|
||||
id: response.id,
|
||||
authConfigId: authConfig.auth_config.id,
|
||||
status: 'INITIATED',
|
||||
createdAt: new Date().toISOString(),
|
||||
lastUpdatedAt: new Date().toISOString(),
|
||||
}
|
||||
await projectsCollection.updateOne({ _id: projectId }, { $set: { [key]: data } });
|
||||
|
||||
// return the connected account
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function syncConnectedAccount(projectId: string, toolkitSlug: string, connectedAccountId: string): Promise<z.infer<typeof ComposioConnectedAccount>> {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
// ensure that the connected account belongs to this project
|
||||
const project = await getProjectConfig(projectId);
|
||||
const account = project.composioConnectedAccounts?.[toolkitSlug];
|
||||
if (!account || account.id !== connectedAccountId) {
|
||||
throw new Error(`Connected account ${connectedAccountId} not found in project ${projectId}`);
|
||||
}
|
||||
|
||||
// if account is already active, nothing to sync
|
||||
if (account.status === 'ACTIVE') {
|
||||
return account;
|
||||
}
|
||||
|
||||
// get the connected account
|
||||
const response = await libGetConnectedAccount(connectedAccountId);
|
||||
|
||||
// update project with new connected account
|
||||
const key = `composioConnectedAccounts.${response.toolkit.slug}`;
|
||||
switch (response.status) {
|
||||
case 'INITIALIZING':
|
||||
case 'INITIATED':
|
||||
account.status = 'INITIATED';
|
||||
break;
|
||||
case 'ACTIVE':
|
||||
account.status = 'ACTIVE';
|
||||
break;
|
||||
default:
|
||||
account.status = 'FAILED';
|
||||
break;
|
||||
}
|
||||
account.lastUpdatedAt = new Date().toISOString();
|
||||
await projectsCollection.updateOne({ _id: projectId }, { $set: { [key]: account } });
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
export async function deleteConnectedAccount(projectId: string, toolkitSlug: string, connectedAccountId: string): Promise<boolean> {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
// ensure that the connected account belongs to this project
|
||||
const project = await getProjectConfig(projectId);
|
||||
const account = project.composioConnectedAccounts?.[toolkitSlug];
|
||||
if (!account || account.id !== connectedAccountId) {
|
||||
throw new Error(`Connected account ${connectedAccountId} not found in project ${projectId} for toolkit ${toolkitSlug}`);
|
||||
}
|
||||
|
||||
// delete the connected account
|
||||
await libDeleteConnectedAccount(connectedAccountId);
|
||||
|
||||
// get auth config data
|
||||
const authConfig = await libGetAuthConfig(account.authConfigId);
|
||||
|
||||
// delete the auth config if it is NOT managed by composio
|
||||
if (!authConfig.is_composio_managed) {
|
||||
await libDeleteAuthConfig(account.authConfigId);
|
||||
}
|
||||
|
||||
// update project with deleted connected account
|
||||
const key = `composioConnectedAccounts.${toolkitSlug}`;
|
||||
await projectsCollection.updateOne({ _id: projectId }, { $unset: { [key]: "" } });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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 } });
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ import { check_query_limit } from "../lib/rate_limiting";
|
|||
import { QueryLimitError } from "../lib/client_utils";
|
||||
import { projectAuthCheck } from "./project_actions";
|
||||
import { redisClient } from "../lib/redis";
|
||||
import { fetchProjectMcpTools } from "../lib/project_tools";
|
||||
import { collectProjectTools } from "../lib/project_tools";
|
||||
import { mergeProjectTools } from "../lib/types/project_types";
|
||||
import { authorizeUserAction, logUsage } from "./billing_actions";
|
||||
import { USE_BILLING } from "../lib/feature_flags";
|
||||
|
|
@ -46,12 +46,12 @@ export async function getCopilotResponseStream(
|
|||
}
|
||||
|
||||
// Get MCP tools from project and merge with workflow tools
|
||||
const mcpTools = await fetchProjectMcpTools(projectId);
|
||||
const projectTools = await collectProjectTools(projectId);
|
||||
|
||||
// Convert workflow to copilot format with both workflow and project tools
|
||||
const wflow = {
|
||||
...current_workflow_config,
|
||||
tools: mergeProjectTools(current_workflow_config.tools, mcpTools)
|
||||
tools: mergeProjectTools(current_workflow_config.tools, projectTools)
|
||||
};
|
||||
|
||||
// prepare request
|
||||
|
|
@ -98,12 +98,12 @@ export async function getCopilotAgentInstructions(
|
|||
}
|
||||
|
||||
// Get MCP tools from project and merge with workflow tools
|
||||
const mcpTools = await fetchProjectMcpTools(projectId);
|
||||
const projectTools = await collectProjectTools(projectId);
|
||||
|
||||
// Convert workflow to copilot format with both workflow and project tools
|
||||
const wflow = {
|
||||
...current_workflow_config,
|
||||
tools: mergeProjectTools(current_workflow_config.tools, mcpTools)
|
||||
tools: mergeProjectTools(current_workflow_config.tools, projectTools)
|
||||
};
|
||||
|
||||
// prepare request
|
||||
|
|
|
|||
|
|
@ -296,37 +296,6 @@ export async function getSelectedMcpTools(projectId: string, serverName: string)
|
|||
return server.tools.map(t => t.id);
|
||||
}
|
||||
|
||||
export async function listProjectMcpTools(projectId: string): Promise<z.infer<typeof WorkflowTool>[]> {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
try {
|
||||
// Get project's MCP servers and their tools
|
||||
const project = await projectsCollection.findOne({ _id: projectId });
|
||||
if (!project?.mcpServers) return [];
|
||||
|
||||
// Convert MCP tools to workflow tools format, but only from ready servers
|
||||
return project.mcpServers
|
||||
.filter(server => server.isReady) // Only include tools from ready servers
|
||||
.flatMap(server => {
|
||||
return server.tools.map(tool => ({
|
||||
name: tool.name,
|
||||
description: tool.description || "",
|
||||
parameters: {
|
||||
type: 'object' as const,
|
||||
properties: tool.parameters?.properties || {},
|
||||
required: tool.parameters?.required || []
|
||||
},
|
||||
isMcp: true,
|
||||
mcpServerName: server.name,
|
||||
mcpServerURL: server.serverUrl,
|
||||
}));
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching project tools:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function testMcpTool(
|
||||
projectId: string,
|
||||
serverName: string,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import { USE_AUTH } from "../lib/feature_flags";
|
|||
import { deleteMcpServerInstance, listActiveServerInstances } from "./klavis_actions";
|
||||
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";
|
||||
|
||||
const KLAVIS_API_KEY = process.env.KLAVIS_API_KEY || '';
|
||||
|
||||
|
|
@ -344,3 +346,8 @@ export async function createProjectFromWorkflowJson(formData: FormData): Promise
|
|||
});
|
||||
return { id: projectId };
|
||||
}
|
||||
|
||||
export async function collectProjectTools(projectId: string): Promise<z.infer<typeof WorkflowTool>[]> {
|
||||
await projectAuthCheck(projectId);
|
||||
return libCollectProjectTools(projectId);
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ import { ApiRequest, ApiResponse } from "../../../../lib/types/types";
|
|||
import { check_query_limit } from "../../../../lib/rate_limiting";
|
||||
import { PrefixLogger } from "../../../../lib/utils";
|
||||
import { TestProfile } from "@/app/lib/types/testing_types";
|
||||
import { fetchProjectMcpTools } from "@/app/lib/project_tools";
|
||||
import { collectProjectTools } from "@/app/lib/project_tools";
|
||||
import { authorize, getCustomerIdForProject, logUsage } from "@/app/lib/billing";
|
||||
import { USE_BILLING } from "@/app/lib/feature_flags";
|
||||
import { getResponse } from "@/app/lib/agents";
|
||||
|
|
@ -62,7 +62,7 @@ export async function POST(
|
|||
}
|
||||
|
||||
// fetch project tools
|
||||
const projectTools = await fetchProjectMcpTools(projectId);
|
||||
const projectTools = await collectProjectTools(projectId);
|
||||
|
||||
// if workflow id is provided in the request, use it, else use the published workflow id
|
||||
let workflowId = result.data.workflowId ?? project.publishedWorkflowId;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { ObjectId, WithId } from "mongodb";
|
|||
import { authCheck } from "../../../utils";
|
||||
import { check_query_limit } from "../../../../../../lib/rate_limiting";
|
||||
import { PrefixLogger } from "../../../../../../lib/utils";
|
||||
import { fetchProjectMcpTools } from "@/app/lib/project_tools";
|
||||
import { collectProjectTools } from "@/app/lib/project_tools";
|
||||
import { authorize, getCustomerIdForProject, logUsage } from "@/app/lib/billing";
|
||||
import { USE_BILLING } from "@/app/lib/feature_flags";
|
||||
import { getResponse } from "@/app/lib/agents";
|
||||
|
|
@ -182,7 +182,7 @@ export async function POST(
|
|||
}
|
||||
|
||||
// fetch project tools
|
||||
const projectTools = await fetchProjectMcpTools(session.projectId);
|
||||
const projectTools = await collectProjectTools(session.projectId);
|
||||
|
||||
// fetch workflow
|
||||
const workflow = await agentWorkflowsCollection.findOne({
|
||||
|
|
|
|||
72
apps/rowboat/app/composio/oauth2/callback/page.tsx
Normal file
72
apps/rowboat/app/composio/oauth2/callback/page.tsx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CheckCircle, XCircle } from 'lucide-react';
|
||||
|
||||
export default function OAuth2CallbackPage() {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [isError, setIsError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Small delay for smooth animation
|
||||
const timer = setTimeout(() => setIsVisible(true), 100);
|
||||
|
||||
// Check for error parameters in URL
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const error = urlParams.get('error');
|
||||
const errorDescription = urlParams.get('error_description');
|
||||
|
||||
if (error) {
|
||||
setIsError(true);
|
||||
}
|
||||
|
||||
// Send message to parent window that OAuth is complete
|
||||
if (window.opener) {
|
||||
window.opener.postMessage({
|
||||
type: 'OAUTH_COMPLETE',
|
||||
success: !error,
|
||||
error: error || null,
|
||||
errorDescription: errorDescription || null,
|
||||
timestamp: Date.now()
|
||||
}, window.location.origin);
|
||||
|
||||
// Close this window after a short delay
|
||||
setTimeout(() => {
|
||||
window.close();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4">
|
||||
<div className={`max-w-md w-full bg-white rounded-lg shadow-lg p-8 text-center transition-all duration-500 ${
|
||||
isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'
|
||||
}`}>
|
||||
<div className="mb-6">
|
||||
{isError ? (
|
||||
<XCircle className="w-16 h-16 text-red-500 mx-auto" />
|
||||
) : (
|
||||
<CheckCircle className="w-16 h-16 text-green-500 mx-auto" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h1 className="text-2xl font-semibold text-gray-900 mb-4">
|
||||
{isError ? 'OAuth2 Flow Failed' : 'OAuth2 Flow Completed'}
|
||||
</h1>
|
||||
|
||||
<p className="text-gray-600 mb-6">
|
||||
{isError
|
||||
? 'There was an issue with the authentication. Please try again.'
|
||||
: 'Your authentication was successful. You can safely close this page now.'
|
||||
}
|
||||
</p>
|
||||
|
||||
<div className="text-sm text-gray-500">
|
||||
This window will automatically close in a few seconds...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -6,11 +6,12 @@ import { createOpenAI } from "@ai-sdk/openai";
|
|||
import { CoreMessage, embed, generateText } from "ai";
|
||||
import { ObjectId } from "mongodb";
|
||||
import { z } from "zod";
|
||||
import { Composio } from '@composio/core';
|
||||
|
||||
// Internal dependencies
|
||||
import { embeddingModel } from '../lib/embedding';
|
||||
import { getMcpClient } from "./mcp";
|
||||
import { dataSourceDocsCollection, dataSourcesCollection } from "./mongodb";
|
||||
import { dataSourceDocsCollection, dataSourcesCollection, projectsCollection } from "./mongodb";
|
||||
import { qdrantClient } from '../lib/qdrant';
|
||||
import { EmbeddingRecord } from "./types/datasource_types";
|
||||
import { ConnectedEntity, sanitizeTextWithMentions, Workflow, WorkflowAgent, WorkflowPrompt, WorkflowTool } from "./types/workflow_types";
|
||||
|
|
@ -192,6 +193,44 @@ async function invokeMcpTool(
|
|||
return result;
|
||||
}
|
||||
|
||||
// Helper to handle composio tool calls
|
||||
async function invokeComposioTool(
|
||||
logger: PrefixLogger,
|
||||
projectId: string,
|
||||
name: string,
|
||||
composioData: z.infer<typeof WorkflowTool>['composioData'] & {},
|
||||
input: any,
|
||||
) {
|
||||
logger = logger.child(`invokeComposioTool`);
|
||||
logger.log(`projectId: ${projectId}`);
|
||||
logger.log(`name: ${name}`);
|
||||
logger.log(`input: ${JSON.stringify(input)}`);
|
||||
|
||||
const { slug, toolkitSlug, noAuth } = composioData;
|
||||
|
||||
let connectedAccountId: string | undefined = undefined;
|
||||
if (!noAuth) {
|
||||
const project = await projectsCollection.findOne({ _id: projectId });
|
||||
if (!project) {
|
||||
throw new Error(`project ${projectId} not found`);
|
||||
}
|
||||
connectedAccountId = project.composioConnectedAccounts?.[toolkitSlug]?.id;
|
||||
if (!connectedAccountId) {
|
||||
throw new Error(`connected account id not found for project ${projectId} and toolkit ${toolkitSlug}`);
|
||||
}
|
||||
}
|
||||
|
||||
const composio = new Composio();
|
||||
|
||||
const result = await composio.tools.execute(slug, {
|
||||
userId: projectId,
|
||||
arguments: input,
|
||||
connectedAccountId: connectedAccountId,
|
||||
});
|
||||
logger.log(`composio tool result: ${JSON.stringify(result)}`);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
// Helper to create RAG tool
|
||||
function createRagTool(
|
||||
logger: PrefixLogger,
|
||||
|
|
@ -291,6 +330,44 @@ function createMcpTool(
|
|||
});
|
||||
}
|
||||
|
||||
// Helper to create a composio tool
|
||||
function createComposioTool(
|
||||
logger: PrefixLogger,
|
||||
config: z.infer<typeof WorkflowTool>,
|
||||
projectId: string
|
||||
): Tool {
|
||||
const { name, description, parameters, composioData } = config;
|
||||
|
||||
if (!composioData) {
|
||||
throw new Error(`composio data not found for tool ${name}`);
|
||||
}
|
||||
|
||||
return tool({
|
||||
name,
|
||||
description,
|
||||
strict: false,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: parameters.properties,
|
||||
required: parameters.required || [],
|
||||
additionalProperties: true,
|
||||
},
|
||||
async execute(input: any) {
|
||||
try {
|
||||
const result = await invokeComposioTool(logger, projectId, name, composioData, input);
|
||||
return JSON.stringify({
|
||||
result,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.log(`Error executing composio tool ${name}:`, error);
|
||||
return JSON.stringify({
|
||||
error: `Tool execution failed: ${error}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Helper to create an agent
|
||||
function createAgent(
|
||||
logger: PrefixLogger,
|
||||
|
|
@ -594,6 +671,9 @@ function createTools(logger: PrefixLogger, workflow: z.infer<typeof Workflow>, t
|
|||
if (config.isMcp) {
|
||||
tools[toolName] = createMcpTool(logger, config, workflow.projectId);
|
||||
logger.log(`created mcp tool: ${toolName}`);
|
||||
} else if (config.isComposio) {
|
||||
tools[toolName] = createComposioTool(logger, config, workflow.projectId);
|
||||
logger.log(`created composio tool: ${toolName}`);
|
||||
} else if (config.mockTool) {
|
||||
tools[toolName] = createMockTool(logger, config);
|
||||
logger.log(`created mock tool: ${toolName}`);
|
||||
|
|
|
|||
410
apps/rowboat/app/lib/composio/composio.ts
Normal file
410
apps/rowboat/app/lib/composio/composio.ts
Normal file
|
|
@ -0,0 +1,410 @@
|
|||
import { z } from "zod";
|
||||
import { PrefixLogger } from "../utils";
|
||||
|
||||
const BASE_URL = 'https://backend.composio.dev/api/v3';
|
||||
const COMPOSIO_API_KEY = process.env.COMPOSIO_API_KEY || "";
|
||||
|
||||
export const ZAuthScheme = z.enum([
|
||||
'API_KEY',
|
||||
'BASIC',
|
||||
'BASIC_WITH_JWT',
|
||||
'BEARER_TOKEN',
|
||||
'BILLCOM_AUTH',
|
||||
'CALCOM_AUTH',
|
||||
'COMPOSIO_LINK',
|
||||
'GOOGLE_SERVICE_ACCOUNT',
|
||||
'NO_AUTH',
|
||||
'OAUTH1',
|
||||
'OAUTH2',
|
||||
]);
|
||||
|
||||
export const ZConnectedAccountStatus = z.enum([
|
||||
'INITIALIZING',
|
||||
'INITIATED',
|
||||
'ACTIVE',
|
||||
'FAILED',
|
||||
'EXPIRED',
|
||||
'INACTIVE',
|
||||
]);
|
||||
|
||||
export const ZToolkit = z.object({
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
meta: z.object({
|
||||
description: z.string(),
|
||||
logo: z.string(),
|
||||
tools_count: z.number(),
|
||||
}),
|
||||
no_auth: z.boolean(),
|
||||
auth_schemes: z.array(ZAuthScheme),
|
||||
composio_managed_auth_schemes: z.array(ZAuthScheme),
|
||||
});
|
||||
|
||||
export const ZComposioField = z.object({
|
||||
name: z.string(),
|
||||
displayName: z.string(),
|
||||
type: z.string(),
|
||||
description: z.string(),
|
||||
required: z.boolean(),
|
||||
default: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export const ZGetToolkitResponse = z.object({
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
composio_managed_auth_schemes: z.array(ZAuthScheme),
|
||||
auth_config_details: z.array(z.object({
|
||||
name: z.string(),
|
||||
mode: ZAuthScheme,
|
||||
fields: z.object({
|
||||
auth_config_creation: z.object({
|
||||
required: z.array(ZComposioField),
|
||||
optional: z.array(ZComposioField),
|
||||
}),
|
||||
connected_account_initiation: z.object({
|
||||
required: z.array(ZComposioField),
|
||||
optional: z.array(ZComposioField),
|
||||
}),
|
||||
})
|
||||
})).nullable(),
|
||||
});
|
||||
|
||||
export const ZTool = z.object({
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
toolkit: z.object({
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
logo: z.string(),
|
||||
}),
|
||||
input_parameters: z.object({
|
||||
type: z.literal('object'),
|
||||
properties: z.record(z.string(), z.any()),
|
||||
required: z.array(z.string()).optional(),
|
||||
additionalProperties: z.boolean().optional(),
|
||||
}),
|
||||
no_auth: z.boolean(),
|
||||
});
|
||||
|
||||
export const ZAuthConfig = z.object({
|
||||
id: z.string(),
|
||||
is_composio_managed: z.boolean(),
|
||||
auth_scheme: ZAuthScheme,
|
||||
});
|
||||
|
||||
export const ZCredentials = z.record(z.string(), z.union([z.string(), z.number(), z.boolean()]));
|
||||
|
||||
export const ZCreateAuthConfigRequest = z.object({
|
||||
toolkit: z.object({
|
||||
slug: z.string(),
|
||||
}),
|
||||
auth_config: z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
type: z.literal('use_composio_managed_auth'),
|
||||
name: z.string().optional(),
|
||||
credentials: ZCredentials.optional(),
|
||||
restrict_to_following_tools: z.array(z.string()).optional(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('use_custom_auth'),
|
||||
authScheme: ZAuthScheme,
|
||||
credentials: ZCredentials,
|
||||
name: z.string().optional(),
|
||||
proxy_config: z.object({
|
||||
proxy_url: z.string(),
|
||||
proxy_auth_key: z.string().optional(),
|
||||
}).optional(),
|
||||
restrict_to_following_tools: z.array(z.string()).optional(),
|
||||
}),
|
||||
]).optional(),
|
||||
});
|
||||
|
||||
/*
|
||||
{
|
||||
"toolkit": {
|
||||
"slug": "github"
|
||||
},
|
||||
"auth_config": {
|
||||
"id": "ac_ZiLwFAWuGA7G",
|
||||
"auth_scheme": "OAUTH2",
|
||||
"is_composio_managed": false,
|
||||
"restrict_to_following_tools": []
|
||||
}
|
||||
}
|
||||
*/
|
||||
export const ZCreateAuthConfigResponse = z.object({
|
||||
toolkit: z.object({
|
||||
slug: z.string(),
|
||||
}),
|
||||
auth_config: ZAuthConfig,
|
||||
});
|
||||
|
||||
const ZConnectionData = z.object({
|
||||
authScheme: ZAuthScheme,
|
||||
val: z.record(z.string(), z.unknown())
|
||||
.and(z.object({
|
||||
status: ZConnectedAccountStatus,
|
||||
})),
|
||||
});
|
||||
|
||||
export const ZCreateConnectedAccountRequest = z.object({
|
||||
auth_config: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
connection: z.object({
|
||||
state: ZConnectionData.optional(),
|
||||
user_id: z.string().optional(),
|
||||
callback_url: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
/*
|
||||
{
|
||||
"id": "ca_vTkCeLZSGab-",
|
||||
"connectionData": {
|
||||
"authScheme": "OAUTH2",
|
||||
"val": {
|
||||
"status": "INITIATED",
|
||||
"code_verifier": "cd0103c5d8836a387adab1635b65ff0d2f51f77a1a79b7ff",
|
||||
"redirectUrl": "https://backend.composio.dev/api/v3/s/DbTOWAyR",
|
||||
"callback_url": "https://backend.composio.dev/api/v1/auth-apps/add"
|
||||
}
|
||||
},
|
||||
"status": "INITIATED",
|
||||
"redirect_url": "https://backend.composio.dev/api/v3/s/DbTOWAyR",
|
||||
"redirect_uri": "https://backend.composio.dev/api/v3/s/DbTOWAyR",
|
||||
"deprecated": {
|
||||
"uuid": "fe66d24b-59d8-4abf-adb2-d8f74353da9e",
|
||||
"authConfigUuid": "8c4d4c84-56e2-4a80-aa59-9e84503381d8"
|
||||
}
|
||||
}
|
||||
*/
|
||||
export const ZCreateConnectedAccountResponse = z.object({
|
||||
id: z.string(),
|
||||
connectionData: ZConnectionData,
|
||||
});
|
||||
|
||||
export const ZConnectedAccount = z.object({
|
||||
id: z.string(),
|
||||
toolkit: z.object({
|
||||
slug: z.string(),
|
||||
}),
|
||||
auth_config: z.object({
|
||||
id: z.string(),
|
||||
is_composio_managed: z.boolean(),
|
||||
is_disabled: z.boolean(),
|
||||
}),
|
||||
status: ZConnectedAccountStatus,
|
||||
});
|
||||
|
||||
const ZErrorResponse = z.object({
|
||||
error: z.object({
|
||||
message: z.string(),
|
||||
error_code: z.number(),
|
||||
suggested_fix: z.string().nullable(),
|
||||
errors: z.array(z.string()).nullable(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const ZError = z.object({
|
||||
error: z.enum([
|
||||
'CUSTOM_OAUTH2_CONFIG_REQUIRED',
|
||||
]),
|
||||
});
|
||||
|
||||
export const ZDeleteOperationResponse = z.object({
|
||||
success: z.boolean(),
|
||||
});
|
||||
|
||||
export const ZListResponse = <T extends z.ZodTypeAny>(schema: T) => z.object({
|
||||
items: z.array(schema),
|
||||
next_cursor: z.string().nullable(),
|
||||
total_pages: z.number(),
|
||||
current_page: z.number(),
|
||||
total_items: z.number(),
|
||||
});
|
||||
|
||||
export async function composioApiCall<T extends z.ZodTypeAny>(
|
||||
schema: T,
|
||||
url: string,
|
||||
options: RequestInit = {},
|
||||
): Promise<z.infer<T>> {
|
||||
const logger = new PrefixLogger('composioApiCall');
|
||||
logger.log(`[${options.method || 'GET'}] ${url}`, options);
|
||||
|
||||
const then = Date.now();
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...options.headers,
|
||||
"x-api-key": COMPOSIO_API_KEY,
|
||||
...(options.method === 'POST' ? {
|
||||
"Content-Type": "application/json",
|
||||
} : {}),
|
||||
},
|
||||
});
|
||||
const duration = Date.now() - then;
|
||||
logger.log(`Took: ${duration}ms`);
|
||||
const data = await response.json();
|
||||
if ('error' in data) {
|
||||
const response = ZErrorResponse.parse(data);
|
||||
throw new Error(`(code: ${response.error.error_code}): ${response.error.message}: ${response.error.suggested_fix}: ${response.error.errors?.join(', ')}`);
|
||||
}
|
||||
return schema.parse(data);
|
||||
} catch (error) {
|
||||
logger.log(`Error:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function listToolkits(cursor: string | null = null): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZToolkit>>>> {
|
||||
const url = new URL(`${BASE_URL}/toolkits`);
|
||||
|
||||
// set params
|
||||
url.searchParams.set("sort_by", "usage");
|
||||
if (cursor) {
|
||||
url.searchParams.set("cursor", cursor);
|
||||
}
|
||||
|
||||
// fetch
|
||||
return composioApiCall(ZListResponse(ZToolkit), url.toString());
|
||||
}
|
||||
|
||||
export async function getToolkit(toolkitSlug: string): Promise<z.infer<typeof ZGetToolkitResponse>> {
|
||||
const url = new URL(`${BASE_URL}/toolkits/${toolkitSlug}`);
|
||||
return composioApiCall(ZGetToolkitResponse, url.toString());
|
||||
}
|
||||
|
||||
export async function listTools(toolkitSlug: string, cursor: string | null = null): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>> {
|
||||
const url = new URL(`${BASE_URL}/tools`);
|
||||
|
||||
// set params
|
||||
url.searchParams.set("toolkit_slug", toolkitSlug);
|
||||
if (cursor) {
|
||||
url.searchParams.set("cursor", cursor);
|
||||
}
|
||||
|
||||
// fetch
|
||||
return composioApiCall(ZListResponse(ZTool), url.toString());
|
||||
}
|
||||
|
||||
export async function listAuthConfigs(toolkitSlug: string, cursor: string | null = null, managedOnly: boolean = false): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZAuthConfig>>>> {
|
||||
const url = new URL(`${BASE_URL}/auth_configs`);
|
||||
url.searchParams.set("toolkit_slug", toolkitSlug);
|
||||
if (cursor) {
|
||||
url.searchParams.set("cursor", cursor);
|
||||
}
|
||||
if (managedOnly) {
|
||||
url.searchParams.set("is_composio_managed", "true");
|
||||
}
|
||||
|
||||
// fetch
|
||||
return composioApiCall(ZListResponse(ZAuthConfig), url.toString());
|
||||
}
|
||||
|
||||
export async function createAuthConfig(request: z.infer<typeof ZCreateAuthConfigRequest>): Promise<z.infer<typeof ZCreateAuthConfigResponse>> {
|
||||
const url = new URL(`${BASE_URL}/auth_configs`);
|
||||
return composioApiCall(ZCreateAuthConfigResponse, url.toString(), {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAuthConfig(authConfigId: string): Promise<z.infer<typeof ZAuthConfig>> {
|
||||
const url = new URL(`${BASE_URL}/auth_configs/${authConfigId}`);
|
||||
return composioApiCall(ZAuthConfig, url.toString());
|
||||
}
|
||||
|
||||
export async function deleteAuthConfig(authConfigId: string): Promise<z.infer<typeof ZDeleteOperationResponse>> {
|
||||
const url = new URL(`${BASE_URL}/auth_configs/${authConfigId}`);
|
||||
return composioApiCall(ZDeleteOperationResponse, url.toString(), {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
// export async function createComposioManagedOauth2AuthConfig(toolkitSlug: string): Promise<z.infer<typeof ZAuthConfig>> {
|
||||
// const response = await createAuthConfig({
|
||||
// toolkit: {
|
||||
// slug: toolkitSlug,
|
||||
// },
|
||||
// auth_config: {
|
||||
// type: 'use_composio_managed_auth',
|
||||
// },
|
||||
// });
|
||||
// return response.auth_config;
|
||||
// }
|
||||
|
||||
// export async function autocreateOauth2Integration(toolkitSlug: string): Promise<z.infer<typeof ZAuthConfig | typeof ZError>> {
|
||||
// // fetch toolkit
|
||||
// const toolkit = await getToolkit(toolkitSlug);
|
||||
|
||||
// // ensure oauth2 is supported
|
||||
// if (!toolkit.auth_config_details?.some(config => config.mode === 'OAUTH2')) {
|
||||
// throw new Error(`OAuth2 is not supported for toolkit ${toolkitSlug}`);
|
||||
// }
|
||||
|
||||
// // fetch existing auth configs
|
||||
// const authConfigs = await fetchAuthConfigs(toolkitSlug);
|
||||
|
||||
// // find a valid oauth2 config
|
||||
// const oauth2AuthConfig = authConfigs.items.find(config => config.auth_scheme === 'OAUTH2');
|
||||
|
||||
// // if valid auth config, return it
|
||||
// if (oauth2AuthConfig) {
|
||||
// return oauth2AuthConfig;
|
||||
// }
|
||||
|
||||
// // check if composio managed oauth2 is supported
|
||||
// if (toolkit.composio_managed_auth_schemes.includes('OAUTH2')) {
|
||||
// return await createComposioManagedOauth2AuthConfig(toolkitSlug);
|
||||
// }
|
||||
|
||||
// // else return error
|
||||
// return {
|
||||
// error: 'CUSTOM_OAUTH2_CONFIG_REQUIRED',
|
||||
// };
|
||||
// }
|
||||
|
||||
export async function createConnectedAccount(request: z.infer<typeof ZCreateConnectedAccountRequest>): Promise<z.infer<typeof ZCreateConnectedAccountResponse>> {
|
||||
const url = new URL(`${BASE_URL}/connected_accounts`);
|
||||
return composioApiCall(ZCreateConnectedAccountResponse, url.toString(), {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
}
|
||||
|
||||
// export async function createOauth2ConnectedAccount(toolkitSlug: string, userId: string, callbackUrl: string): Promise<z.infer<typeof ZCreateConnectedAccountResponse | typeof ZError>> {
|
||||
// // fetch auth config
|
||||
// const authConfig = await autocreateOauth2Integration(toolkitSlug);
|
||||
|
||||
// // if error, return error
|
||||
// if ('error' in authConfig) {
|
||||
// return authConfig;
|
||||
// }
|
||||
|
||||
// // create connected account
|
||||
// return await createConnectedAccount({
|
||||
// auth_config: {
|
||||
// id: authConfig.id,
|
||||
// },
|
||||
// connection: {
|
||||
// user_id: userId,
|
||||
// callback_url: callbackUrl,
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
|
||||
export async function getConnectedAccount(connectedAccountId: string): Promise<z.infer<typeof ZConnectedAccount>> {
|
||||
const url = new URL(`${BASE_URL}/connected_accounts/${connectedAccountId}`);
|
||||
return await composioApiCall(ZConnectedAccount, url.toString());
|
||||
}
|
||||
|
||||
export async function deleteConnectedAccount(connectedAccountId: string): Promise<z.infer<typeof ZDeleteOperationResponse>> {
|
||||
const url = new URL(`${BASE_URL}/connected_accounts/${connectedAccountId}`);
|
||||
return await composioApiCall(ZDeleteOperationResponse, url.toString(), {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ export const USE_AUTH = process.env.USE_AUTH === 'true';
|
|||
export const USE_RAG_S3_UPLOADS = process.env.USE_RAG_S3_UPLOADS === 'true';
|
||||
export const USE_GEMINI_FILE_PARSING = process.env.USE_GEMINI_FILE_PARSING === 'true';
|
||||
export const USE_BILLING = process.env.USE_BILLING === 'true';
|
||||
export const USE_COMPOSIO_TOOLS = process.env.USE_COMPOSIO_TOOLS === 'true';
|
||||
|
||||
// Hardcoded flags
|
||||
export const USE_MULTIPLE_PROJECTS = true;
|
||||
|
|
|
|||
|
|
@ -2,50 +2,59 @@ import { z } from "zod";
|
|||
import { projectsCollection } from "./mongodb";
|
||||
import { WorkflowTool } from "./types/workflow_types";
|
||||
|
||||
export async function fetchProjectMcpTools(projectId: string): Promise<z.infer<typeof WorkflowTool>[]> {
|
||||
// Get project's MCP servers and their tools
|
||||
const project = await projectsCollection.findOne({ _id: projectId });
|
||||
if (!project?.mcpServers) return [];
|
||||
export async function collectProjectTools(projectId: string): Promise<z.infer<typeof WorkflowTool>[]> {
|
||||
const tools: z.infer<typeof WorkflowTool>[] = [];
|
||||
|
||||
console.log('[MCP] Getting tools from project:', {
|
||||
serverCount: project.mcpServers.length,
|
||||
servers: project.mcpServers.map(s => ({
|
||||
name: s.name,
|
||||
isReady: s.isReady,
|
||||
toolCount: s.tools.length,
|
||||
tools: s.tools.map(t => ({
|
||||
name: t.name,
|
||||
hasParams: !!t.parameters,
|
||||
paramCount: t.parameters ? Object.keys(t.parameters.properties).length : 0,
|
||||
required: t.parameters?.required || []
|
||||
}))
|
||||
}))
|
||||
});
|
||||
// Get project data
|
||||
const project = await projectsCollection.findOne({ _id: projectId });
|
||||
if (!project) {
|
||||
throw new Error(`Project ${projectId} not found`);
|
||||
}
|
||||
|
||||
// Convert MCP tools to workflow tools format, but only from ready servers
|
||||
const mcpTools = project.mcpServers
|
||||
.filter(server => server.isReady) // Only include tools from ready servers
|
||||
.flatMap(server => {
|
||||
return server.tools.map(tool => ({
|
||||
name: tool.name,
|
||||
if (project.mcpServers) {
|
||||
for (const server of project.mcpServers) {
|
||||
if (server.isReady) {
|
||||
for (const tool of server.tools) {
|
||||
tools.push({
|
||||
name: tool.name,
|
||||
description: tool.description || "",
|
||||
parameters: {
|
||||
type: 'object' as const,
|
||||
properties: tool.parameters?.properties || {},
|
||||
required: tool.parameters?.required || []
|
||||
},
|
||||
isMcp: true,
|
||||
mcpServerName: server.name,
|
||||
mcpServerURL: server.serverUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add Composio tools
|
||||
if (project.composioSelectedTools) {
|
||||
for (const tool of project.composioSelectedTools) {
|
||||
tools.push({
|
||||
name: tool.slug,
|
||||
description: tool.description || "",
|
||||
parameters: {
|
||||
type: 'object' as const,
|
||||
properties: tool.parameters?.properties || {},
|
||||
required: tool.parameters?.required || []
|
||||
properties: tool.input_parameters?.properties || {},
|
||||
required: tool.input_parameters?.required || []
|
||||
},
|
||||
isMcp: true,
|
||||
mcpServerName: server.name,
|
||||
mcpServerURL: server.serverUrl,
|
||||
}));
|
||||
});
|
||||
isComposio: true,
|
||||
composioData: {
|
||||
slug: tool.slug,
|
||||
noAuth: tool.no_auth,
|
||||
toolkitName: tool.toolkit.name,
|
||||
toolkitSlug: tool.toolkit.slug,
|
||||
logo: tool.toolkit.logo,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[MCP] Converted tools from ready servers:', mcpTools.map(t => ({
|
||||
name: t.name,
|
||||
hasParams: !!t.parameters,
|
||||
paramCount: t.parameters ? Object.keys(t.parameters.properties).length : 0,
|
||||
required: t.parameters?.required || []
|
||||
})));
|
||||
|
||||
return mcpTools;
|
||||
return tools;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,19 @@
|
|||
import { z } from "zod";
|
||||
import { MCPServer } from "./types";
|
||||
import { WorkflowTool } from "./workflow_types";
|
||||
import { ZTool } from "../composio/composio";
|
||||
|
||||
export const ComposioConnectedAccount = z.object({
|
||||
id: z.string(),
|
||||
authConfigId: z.string(),
|
||||
status: z.enum([
|
||||
'INITIATED',
|
||||
'ACTIVE',
|
||||
'FAILED',
|
||||
]),
|
||||
createdAt: z.string().datetime(),
|
||||
lastUpdatedAt: z.string().datetime(),
|
||||
});
|
||||
|
||||
export const Project = z.object({
|
||||
_id: z.string().uuid(),
|
||||
|
|
@ -15,6 +28,8 @@ export const Project = z.object({
|
|||
nextWorkflowNumber: z.number().optional(),
|
||||
testRunCounter: z.number().default(0),
|
||||
mcpServers: z.array(MCPServer).optional(),
|
||||
composioConnectedAccounts: z.record(z.string(), ComposioConnectedAccount).optional(),
|
||||
composioSelectedTools: z.array(ZTool).optional(),
|
||||
});
|
||||
|
||||
export const ProjectMember = z.object({
|
||||
|
|
@ -38,32 +53,11 @@ export function mergeProjectTools(
|
|||
// Filter out any existing MCP tools from workflow tools
|
||||
const nonMcpTools = workflowTools.filter(t => !t.isMcp);
|
||||
|
||||
// Merge with MCP tools
|
||||
// Merge with project tools
|
||||
const merged = [
|
||||
...nonMcpTools,
|
||||
...projectTools.map(tool => ({
|
||||
...tool,
|
||||
isMcp: true as const, // Ensure isMcp is set
|
||||
parameters: {
|
||||
type: 'object' as const,
|
||||
properties: tool.parameters?.properties || {},
|
||||
required: tool.parameters?.required || []
|
||||
}
|
||||
}))
|
||||
...projectTools
|
||||
];
|
||||
|
||||
console.log('[mergeMcpTools] Merged tools:', {
|
||||
totalCount: merged.length,
|
||||
nonMcpCount: nonMcpTools.length,
|
||||
mcpCount: projectTools.length,
|
||||
tools: merged.map(t => ({
|
||||
name: t.name,
|
||||
isMcp: t.isMcp,
|
||||
hasParams: !!t.parameters,
|
||||
paramCount: t.parameters ? Object.keys(t.parameters.properties).length : 0,
|
||||
parameters: t.parameters
|
||||
}))
|
||||
});
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,14 @@ export const WorkflowTool = z.object({
|
|||
isLibrary: z.boolean().default(false).optional(),
|
||||
mcpServerName: z.string().optional(),
|
||||
mcpServerURL: z.string().optional(),
|
||||
isComposio: z.boolean().optional(), // whether this is a Composio tool
|
||||
composioData: z.object({
|
||||
slug: z.string(), // the slug for the Composio tool e.g. "GITHUB_CREATE_AN_ISSUE"
|
||||
noAuth: z.boolean(), // whether the tool requires no authentication
|
||||
toolkitName: z.string(), // the name for the Composio toolkit e.g. "GITHUB"
|
||||
toolkitSlug: z.string(), // the slug for the Composio toolkit e.g. "GITHUB"
|
||||
logo: z.string(), // the logo for the Composio tool
|
||||
}).optional(), // the data for the Composio tool, if it is a Composio tool
|
||||
});
|
||||
export const Workflow = z.object({
|
||||
name: z.string().optional(),
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ export function ToolConfig({
|
|||
});
|
||||
|
||||
const [selectedParams, setSelectedParams] = useState(new Set([]));
|
||||
const isReadOnly = tool.isMcp || tool.isLibrary;
|
||||
const isReadOnly = tool.isMcp || tool.isLibrary || tool.isComposio;
|
||||
const [nameError, setNameError] = useState<string | null>(null);
|
||||
|
||||
// Log when parameters are being rendered
|
||||
|
|
|
|||
|
|
@ -0,0 +1,283 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Info, RefreshCw, Search } from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
import { listToolkits, listTools, updateComposioSelectedTools } from '@/app/actions/composio_actions';
|
||||
import { getProjectConfig } from '@/app/actions/project_actions';
|
||||
import { z } from 'zod';
|
||||
import { ZToolkit, ZListResponse, ZTool } from '@/app/lib/composio/composio';
|
||||
import { Project } from '@/app/lib/types/project_types';
|
||||
import { ComposioToolsPanel } from './ComposioToolsPanel';
|
||||
import { ToolkitCard } from './ToolkitCard';
|
||||
|
||||
type ToolkitType = z.infer<typeof ZToolkit>;
|
||||
type ToolkitListResponse = z.infer<ReturnType<typeof ZListResponse<typeof ZToolkit>>>;
|
||||
type ProjectType = z.infer<typeof Project>;
|
||||
|
||||
export function Composio() {
|
||||
const params = useParams();
|
||||
const projectId = typeof params.projectId === 'string' ? params.projectId : params.projectId?.[0];
|
||||
if (!projectId) throw new Error('Project ID is required');
|
||||
|
||||
const [toolkits, setToolkits] = useState<ToolkitType[]>([]);
|
||||
const [projectConfig, setProjectConfig] = useState<ProjectType | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedToolkit, setSelectedToolkit] = useState<ToolkitType | null>(null);
|
||||
const [isToolsPanelOpen, setIsToolsPanelOpen] = useState(false);
|
||||
const [savingTools, setSavingTools] = useState(false);
|
||||
|
||||
const loadProjectConfig = useCallback(async () => {
|
||||
try {
|
||||
const config = await getProjectConfig(projectId);
|
||||
setProjectConfig(config);
|
||||
} catch (err: any) {
|
||||
console.error('Error fetching project config:', err);
|
||||
setError('Unable to load project configuration.');
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
const loadAllToolkits = useCallback(async () => {
|
||||
let cursor: string | null = null;
|
||||
let allToolkits: ToolkitType[] = [];
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
do {
|
||||
const response: ToolkitListResponse = await listToolkits(projectId, cursor);
|
||||
allToolkits = [...allToolkits, ...response.items];
|
||||
cursor = response.next_cursor;
|
||||
} while (cursor !== null);
|
||||
|
||||
// // Only show those toolkits that
|
||||
// // - either do not require authentication, OR
|
||||
// // - have oauth2 managed by Composio
|
||||
// const filteredToolkits = allToolkits.filter(toolkit => {
|
||||
// const noAuth = toolkit.no_auth;
|
||||
// const hasOAuth2 = toolkit.auth_schemes.includes('OAUTH2');
|
||||
// const hasComposioManagedOAuth2 = toolkit.composio_managed_auth_schemes.includes('OAUTH2');
|
||||
// return noAuth || hasOAuth2;
|
||||
// });
|
||||
|
||||
setToolkits(allToolkits);
|
||||
setError(null);
|
||||
} catch (err: any) {
|
||||
setError('Unable to load all Composio toolkits. Please check your connection and try again.');
|
||||
console.error('Error fetching all toolkits:', err);
|
||||
setToolkits([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
const handleManageTools = useCallback((toolkit: ToolkitType) => {
|
||||
setSelectedToolkit(toolkit);
|
||||
setIsToolsPanelOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleCloseToolsPanel = useCallback(() => {
|
||||
setSelectedToolkit(null);
|
||||
setIsToolsPanelOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleProjectConfigUpdate = useCallback(() => {
|
||||
loadProjectConfig();
|
||||
}, [loadProjectConfig]);
|
||||
|
||||
const handleUpdateToolsSelection = useCallback(async (selectedToolObjects: z.infer<typeof ZTool>[]) => {
|
||||
if (!projectId) return;
|
||||
|
||||
setSavingTools(true);
|
||||
try {
|
||||
// Get existing selected tools from project config
|
||||
const existingSelectedTools = projectConfig?.composioSelectedTools || [];
|
||||
|
||||
// Create a map of existing tools by slug for easy lookup
|
||||
const existingToolsMap = new Map(existingSelectedTools.map(tool => [tool.slug, tool]));
|
||||
|
||||
// Add or update the new selections
|
||||
for (const tool of selectedToolObjects) {
|
||||
existingToolsMap.set(tool.slug, tool);
|
||||
}
|
||||
|
||||
// Convert back to array
|
||||
const mergedSelectedTools = Array.from(existingToolsMap.values());
|
||||
|
||||
await updateComposioSelectedTools(projectId, mergedSelectedTools);
|
||||
|
||||
// Refresh project config to get updated data
|
||||
await loadProjectConfig();
|
||||
} catch (error) {
|
||||
console.error('Error saving tool selection:', error);
|
||||
} finally {
|
||||
setSavingTools(false);
|
||||
}
|
||||
}, [projectId, projectConfig, loadProjectConfig]);
|
||||
|
||||
const handleRemoveToolkitTools = useCallback(async (toolkitSlug: string) => {
|
||||
if (!projectId) return;
|
||||
|
||||
setSavingTools(true);
|
||||
try {
|
||||
// Get existing selected tools from project config
|
||||
const existingSelectedTools = projectConfig?.composioSelectedTools || [];
|
||||
|
||||
// Filter out all tools from the specified toolkit
|
||||
const filteredSelectedTools = existingSelectedTools.filter(tool =>
|
||||
tool.toolkit.slug !== toolkitSlug
|
||||
);
|
||||
|
||||
await updateComposioSelectedTools(projectId, filteredSelectedTools);
|
||||
|
||||
// Refresh project config to get updated data
|
||||
await loadProjectConfig();
|
||||
} catch (error) {
|
||||
console.error('Error removing toolkit tools:', error);
|
||||
} finally {
|
||||
setSavingTools(false);
|
||||
}
|
||||
}, [projectId, projectConfig, loadProjectConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
loadProjectConfig();
|
||||
}, [loadProjectConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
loadAllToolkits();
|
||||
}, [loadAllToolkits]);
|
||||
|
||||
const filteredToolkits = toolkits.filter(toolkit => {
|
||||
const searchLower = searchQuery.toLowerCase();
|
||||
return (
|
||||
toolkit.name.toLowerCase().includes(searchLower) ||
|
||||
toolkit.meta.description.toLowerCase().includes(searchLower) ||
|
||||
toolkit.slug.toLowerCase().includes(searchLower)
|
||||
);
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="text-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-800 dark:border-gray-200 mx-auto"></div>
|
||||
<p className="mt-4 text-sm text-gray-600 dark:text-gray-400">Loading Composio toolkits...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-[50vh] space-y-6 px-4">
|
||||
<p className="text-center text-red-500 dark:text-red-400 max-w-[600px]">
|
||||
{error}
|
||||
</p>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
loadProjectConfig();
|
||||
loadAllToolkits();
|
||||
}}
|
||||
>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-100 dark:border-blue-800 rounded-lg p-4">
|
||||
<div className="flex gap-3">
|
||||
<div className="shrink-0">
|
||||
<Info className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex-1 flex items-center gap-4">
|
||||
<div className="relative flex-1">
|
||||
<div className="absolute inset-y-0 left-2 flex items-center pointer-events-none">
|
||||
<Search className="h-4 w-4 text-gray-400 dark:text-gray-500" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search toolkits..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-8 pr-4 py-2 text-sm border border-gray-200 dark:border-gray-700 rounded-md
|
||||
bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100
|
||||
placeholder-gray-400 dark:placeholder-gray-500
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400
|
||||
hover:border-gray-300 dark:hover:border-gray-600 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
||||
{filteredToolkits.length} {filteredToolkits.length === 1 ? 'toolkit' : 'toolkits'}
|
||||
</div>
|
||||
<div className="h-4 w-px bg-gray-200 dark:bg-gray-700" />
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
loadProjectConfig();
|
||||
loadAllToolkits();
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
<div className="inline-flex items-center">
|
||||
<RefreshCw className={clsx("h-4 w-4", loading && "animate-spin")} />
|
||||
<span className="ml-2">Refresh</span>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredToolkits.map((toolkit) => {
|
||||
const isConnected = toolkit.no_auth || projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.status === 'ACTIVE';
|
||||
const connectedAccountId = projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.id;
|
||||
|
||||
return (
|
||||
<ToolkitCard
|
||||
key={toolkit.slug}
|
||||
toolkit={toolkit}
|
||||
projectId={projectId}
|
||||
isConnected={isConnected}
|
||||
connectedAccountId={connectedAccountId}
|
||||
projectConfig={projectConfig}
|
||||
onManageTools={() => handleManageTools(toolkit)}
|
||||
onProjectConfigUpdate={handleProjectConfigUpdate}
|
||||
onRemoveToolkitTools={handleRemoveToolkitTools}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{filteredToolkits.length === 0 && !loading && (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
{searchQuery ? 'No toolkits found matching your search.' : 'No toolkits available.'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tools Panel */}
|
||||
<ComposioToolsPanel
|
||||
toolkit={selectedToolkit}
|
||||
isOpen={isToolsPanelOpen}
|
||||
onClose={handleCloseToolsPanel}
|
||||
projectConfig={projectConfig}
|
||||
onUpdateToolsSelection={handleUpdateToolsSelection}
|
||||
isSaving={savingTools}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,269 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { PictureImg } from '@/components/ui/picture-img';
|
||||
import { Checkbox } from '@heroui/react';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { listTools } from '@/app/actions/composio_actions';
|
||||
import { z } from 'zod';
|
||||
import { ZTool, ZListResponse } from '@/app/lib/composio/composio';
|
||||
import { SlidePanel } from '@/components/ui/slide-panel';
|
||||
import { Project } from '@/app/lib/types/project_types';
|
||||
|
||||
type ToolType = z.infer<typeof ZTool>;
|
||||
type ToolListResponse = z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>;
|
||||
type ProjectType = z.infer<typeof Project>;
|
||||
|
||||
interface ComposioToolsPanelProps {
|
||||
toolkit: {
|
||||
slug: string;
|
||||
name: string;
|
||||
meta: {
|
||||
logo: string;
|
||||
};
|
||||
no_auth?: boolean;
|
||||
} | null;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
projectConfig: ProjectType | null;
|
||||
onUpdateToolsSelection: (selectedToolObjects: ToolType[]) => void;
|
||||
isSaving: boolean;
|
||||
}
|
||||
|
||||
export function ComposioToolsPanel({
|
||||
toolkit,
|
||||
isOpen,
|
||||
onClose,
|
||||
projectConfig,
|
||||
onUpdateToolsSelection,
|
||||
isSaving
|
||||
}: ComposioToolsPanelProps) {
|
||||
const params = useParams();
|
||||
const projectId = typeof params.projectId === 'string' ? params.projectId : params.projectId?.[0];
|
||||
if (!projectId) throw new Error('Project ID is required');
|
||||
|
||||
const [tools, setTools] = useState<ToolType[]>([]);
|
||||
const [toolsLoading, setToolsLoading] = useState(false);
|
||||
const [currentCursor, setCurrentCursor] = useState<string | null>(null);
|
||||
const [nextCursor, setNextCursor] = useState<string | null>(null);
|
||||
const [cursorHistory, setCursorHistory] = useState<string[]>([]);
|
||||
const [selectedTools, setSelectedTools] = useState<Set<string>>(new Set());
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
|
||||
const loadToolsForToolkit = useCallback(async (toolkitSlug: string, cursor: string | null = null) => {
|
||||
try {
|
||||
setToolsLoading(true);
|
||||
|
||||
const response: ToolListResponse = await listTools(projectId, toolkitSlug, cursor);
|
||||
|
||||
setTools(response.items);
|
||||
setNextCursor(response.next_cursor);
|
||||
|
||||
if (cursor === null) {
|
||||
// First page - reset pagination state
|
||||
setCurrentCursor(null);
|
||||
setCursorHistory([]);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Error fetching tools:', err);
|
||||
setTools([]);
|
||||
} finally {
|
||||
setToolsLoading(false);
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
const handleNextPage = useCallback(async () => {
|
||||
if (!nextCursor || !toolkit) return;
|
||||
|
||||
// Add current cursor to history
|
||||
setCursorHistory(prev => [...prev, currentCursor || '']);
|
||||
setCurrentCursor(nextCursor);
|
||||
|
||||
await loadToolsForToolkit(toolkit.slug, nextCursor);
|
||||
}, [nextCursor, toolkit, currentCursor, loadToolsForToolkit]);
|
||||
|
||||
const handlePreviousPage = useCallback(async () => {
|
||||
if (cursorHistory.length === 0 || !toolkit) return;
|
||||
|
||||
// Get the previous cursor from history
|
||||
const previousCursor = cursorHistory[cursorHistory.length - 1];
|
||||
const newHistory = cursorHistory.slice(0, -1);
|
||||
|
||||
setCursorHistory(newHistory);
|
||||
setCurrentCursor(previousCursor);
|
||||
|
||||
await loadToolsForToolkit(toolkit.slug, previousCursor);
|
||||
}, [cursorHistory, toolkit, loadToolsForToolkit]);
|
||||
|
||||
const handleToolSelectionChange = useCallback((toolSlug: string, selected: boolean) => {
|
||||
setSelectedTools(prev => {
|
||||
const next = new Set(prev);
|
||||
if (selected) {
|
||||
next.add(toolSlug);
|
||||
} else {
|
||||
next.delete(toolSlug);
|
||||
}
|
||||
setHasChanges(true);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleSaveTools = useCallback(async () => {
|
||||
// Convert selected tool slugs to actual tool objects
|
||||
const selectedToolObjects = tools.filter(tool => selectedTools.has(tool.slug));
|
||||
await onUpdateToolsSelection(selectedToolObjects);
|
||||
setHasChanges(false);
|
||||
}, [onUpdateToolsSelection, selectedTools, tools]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setTools([]);
|
||||
setSelectedTools(new Set());
|
||||
setHasChanges(false);
|
||||
if (hasChanges) {
|
||||
if (window.confirm('You have unsaved changes. Are you sure you want to close?')) {
|
||||
onClose();
|
||||
}
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
}, [onClose, hasChanges]);
|
||||
|
||||
// Initialize selected tools from project config when opening the panel
|
||||
useEffect(() => {
|
||||
if (toolkit && isOpen && projectConfig?.composioSelectedTools) {
|
||||
const toolSlugs = new Set(projectConfig.composioSelectedTools.map(tool => tool.slug));
|
||||
setSelectedTools(toolSlugs);
|
||||
setHasChanges(false);
|
||||
}
|
||||
}, [toolkit, isOpen, projectConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
if (toolkit && isOpen) {
|
||||
loadToolsForToolkit(toolkit.slug, null);
|
||||
}
|
||||
}, [toolkit, isOpen, loadToolsForToolkit]);
|
||||
|
||||
if (!toolkit) return null;
|
||||
|
||||
// Check if the toolkit is connected (has an active connected account) or doesn't require auth
|
||||
const isToolkitConnected = toolkit.no_auth || projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.status === 'ACTIVE';
|
||||
|
||||
return (
|
||||
<SlidePanel
|
||||
isOpen={isOpen}
|
||||
onClose={handleClose}
|
||||
title={
|
||||
<div className="flex items-center gap-3">
|
||||
{toolkit.meta.logo && (
|
||||
<PictureImg
|
||||
src={toolkit.meta.logo}
|
||||
alt={`${toolkit.name} logo`}
|
||||
width={24}
|
||||
height={24}
|
||||
className="rounded-md object-cover"
|
||||
/>
|
||||
)}
|
||||
<span>{toolkit.name}</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Available Tools</h4>
|
||||
<div className="flex items-center gap-2">
|
||||
{!isToolkitConnected && !toolkit.no_auth && (
|
||||
<div className="text-sm text-orange-600 dark:text-orange-400 px-3 py-1 rounded-full bg-orange-50 dark:bg-orange-900/20">
|
||||
Toolkit not connected
|
||||
</div>
|
||||
)}
|
||||
{hasChanges && (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={handleSaveTools}
|
||||
disabled={isSaving || !isToolkitConnected}
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-2 border-b-transparent border-white mr-2" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
'Save Changes'
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scrollable Tools List */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{toolsLoading ? (
|
||||
<div className="text-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-800 dark:border-gray-200 mx-auto"></div>
|
||||
<p className="mt-4 text-sm text-gray-600 dark:text-gray-400">Loading tools...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{tools.map((tool) => (
|
||||
<div key={tool.slug} className={`group p-4 rounded-lg transition-all duration-200 border border-transparent ${
|
||||
isToolkitConnected
|
||||
? 'bg-gray-50/50 dark:bg-gray-800/50 hover:bg-gray-100/50 dark:hover:bg-gray-700/50 hover:border-gray-200 dark:hover:border-gray-600'
|
||||
: 'bg-gray-100/50 dark:bg-gray-900/50 opacity-60'
|
||||
}`}>
|
||||
<div className="flex items-start gap-3">
|
||||
<Checkbox
|
||||
isSelected={selectedTools.has(tool.slug)}
|
||||
onValueChange={(selected) => handleToolSelectionChange(tool.slug, selected)}
|
||||
size="sm"
|
||||
isDisabled={!isToolkitConnected}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-1">
|
||||
{tool.name}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{tool.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Fixed Pagination Controls */}
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 pt-4 mt-4">
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={handlePreviousPage}
|
||||
disabled={cursorHistory.length === 0 || toolsLoading}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4 mr-1" />
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={handleNextPage}
|
||||
disabled={!nextCursor || toolsLoading}
|
||||
>
|
||||
Next
|
||||
<ChevronRight className="h-4 w-4 ml-1" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SlidePanel>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,518 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Spinner, Button as HeroButton, Input } from "@heroui/react";
|
||||
import { PictureImg } from '@/components/ui/picture-img';
|
||||
import { Wrench, Shield, Key, Globe, ArrowLeft } from "lucide-react";
|
||||
import { getToolkit, createComposioManagedOauth2ConnectedAccount, syncConnectedAccount, listToolkits, createCustomConnectedAccount } from '@/app/actions/composio_actions';
|
||||
import { z } from 'zod';
|
||||
import { ZGetToolkitResponse, ZToolkit, ZComposioField, ZAuthScheme } from '@/app/lib/composio/composio';
|
||||
|
||||
interface ToolkitAuthModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
toolkitSlug: string;
|
||||
projectId: string;
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
export function ToolkitAuthModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
toolkitSlug,
|
||||
projectId,
|
||||
onComplete
|
||||
}: ToolkitAuthModalProps) {
|
||||
const [toolkit, setToolkit] = useState<z.infer<typeof ZGetToolkitResponse> | null>(null);
|
||||
const [toolkitDetails, setToolkitDetails] = useState<z.infer<typeof ZToolkit> | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Form state
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [selectedAuthScheme, setSelectedAuthScheme] = useState<z.infer<typeof ZAuthScheme> | null>(null);
|
||||
const [formData, setFormData] = useState<Record<string, string>>({});
|
||||
|
||||
// Fetch toolkit details when modal opens
|
||||
useEffect(() => {
|
||||
if (isOpen && toolkitSlug) {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Fetch both toolkit auth details and full toolkit info
|
||||
Promise.all([
|
||||
getToolkit(projectId, toolkitSlug),
|
||||
listToolkits(projectId).then(response =>
|
||||
response.items.find(t => t.slug === toolkitSlug) || null
|
||||
)
|
||||
])
|
||||
.then(([authDetails, fullDetails]) => {
|
||||
setToolkit(authDetails);
|
||||
setToolkitDetails(fullDetails);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to fetch toolkit:', err);
|
||||
setError('Failed to load toolkit details');
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
}, [isOpen, toolkitSlug, projectId]);
|
||||
|
||||
// Reset form state when modal closes
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setShowForm(false);
|
||||
setSelectedAuthScheme(null);
|
||||
setFormData({});
|
||||
setError(null);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const handleOAuthCompletion = useCallback(async (connectedAccountId: string) => {
|
||||
try {
|
||||
// Sync the connected account to get the latest status
|
||||
await syncConnectedAccount(projectId, toolkitSlug, connectedAccountId);
|
||||
|
||||
// Call completion callback
|
||||
onComplete();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Error syncing connected account after OAuth:', error);
|
||||
setError('Authentication completed but failed to sync status. Please refresh and try again.');
|
||||
}
|
||||
}, [projectId, toolkitSlug, onComplete, onClose]);
|
||||
|
||||
const handleComposioOAuth2 = useCallback(async () => {
|
||||
setError(null);
|
||||
setProcessing(true);
|
||||
|
||||
try {
|
||||
// Start OAuth flow
|
||||
const returnUrl = `${window.location.origin}/composio/oauth2/callback`;
|
||||
const response = await createComposioManagedOauth2ConnectedAccount(projectId, toolkitSlug, returnUrl);
|
||||
console.log('OAuth response:', JSON.stringify(response, null, 2));
|
||||
|
||||
// if error, set error
|
||||
if ('error' in response) {
|
||||
if (response.error === 'CUSTOM_OAUTH2_CONFIG_REQUIRED') {
|
||||
setError('Please set up a custom OAuth2 configuration for this toolkit in the Composio dashboard');
|
||||
} else {
|
||||
setError('Failed to connect to toolkit');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Open OAuth window
|
||||
const authWindow = window.open(
|
||||
response.connectionData.val.redirectUrl as string,
|
||||
'_blank',
|
||||
'width=600,height=700'
|
||||
);
|
||||
|
||||
if (authWindow) {
|
||||
// Use postMessage since we control the callback URL
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
// Only accept messages from our own origin
|
||||
if (event.origin !== window.location.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is an OAuth completion message
|
||||
if (event.data && event.data.type === 'OAUTH_COMPLETE') {
|
||||
window.removeEventListener('message', handleMessage);
|
||||
clearInterval(checkInterval);
|
||||
|
||||
if (event.data.success) {
|
||||
// Handle successful OAuth completion
|
||||
handleOAuthCompletion(response.id);
|
||||
} else {
|
||||
// Handle OAuth error
|
||||
const errorMessage = event.data.errorDescription || event.data.error || 'OAuth authentication failed';
|
||||
setError(errorMessage);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for postMessage from our callback page
|
||||
window.addEventListener('message', handleMessage);
|
||||
|
||||
// Minimal fallback: check if window closes without message
|
||||
const checkInterval = setInterval(() => {
|
||||
if (authWindow.closed) {
|
||||
clearInterval(checkInterval);
|
||||
window.removeEventListener('message', handleMessage);
|
||||
|
||||
// If we didn't get a postMessage, still try to sync
|
||||
// (in case the message was missed for some reason)
|
||||
handleOAuthCompletion(response.id);
|
||||
}
|
||||
}, 1000); // Check less frequently since we expect postMessage
|
||||
} else {
|
||||
window.alert('Failed to open authentication window. Please check your popup blocker settings.');
|
||||
setError('Failed to open authentication window');
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('OAuth flow failed:', err);
|
||||
const errorMessage = err.message || 'Failed to connect to toolkit';
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
}, [projectId, toolkitSlug, handleOAuthCompletion]);
|
||||
|
||||
const handleCustomAuth = useCallback((authScheme: z.infer<typeof ZAuthScheme>) => {
|
||||
setSelectedAuthScheme(authScheme);
|
||||
|
||||
// Initialize form data with default values
|
||||
const authConfig = toolkit?.auth_config_details?.find(config => config.mode === authScheme);
|
||||
|
||||
if (authConfig) {
|
||||
const initialData: Record<string, string> = {};
|
||||
|
||||
// Try connected_account_initiation first, fallback to auth_config_creation
|
||||
const requiredFields = authConfig.fields.connected_account_initiation.required.length > 0
|
||||
? authConfig.fields.connected_account_initiation.required
|
||||
: authConfig.fields.auth_config_creation.required;
|
||||
|
||||
const optionalFields = authConfig.fields.connected_account_initiation.optional.length > 0
|
||||
? authConfig.fields.connected_account_initiation.optional
|
||||
: authConfig.fields.auth_config_creation.optional;
|
||||
|
||||
// Add defaults for required fields
|
||||
requiredFields.forEach(field => {
|
||||
if (field.default) {
|
||||
initialData[field.name] = field.default;
|
||||
}
|
||||
});
|
||||
|
||||
// Add defaults for optional fields
|
||||
optionalFields.forEach(field => {
|
||||
if (field.default) {
|
||||
initialData[field.name] = field.default;
|
||||
}
|
||||
});
|
||||
|
||||
setFormData(initialData);
|
||||
}
|
||||
|
||||
setShowForm(true);
|
||||
}, [toolkit]);
|
||||
|
||||
const handleFormSubmit = useCallback(async () => {
|
||||
if (!selectedAuthScheme || !toolkit) return;
|
||||
|
||||
setError(null);
|
||||
setProcessing(true);
|
||||
|
||||
try {
|
||||
const callbackUrl = `${window.location.origin}/composio/oauth2/callback`;
|
||||
const response = await createCustomConnectedAccount(projectId, {
|
||||
toolkitSlug: toolkit.slug,
|
||||
authConfig: {
|
||||
authScheme: selectedAuthScheme,
|
||||
credentials: formData,
|
||||
},
|
||||
callbackUrl,
|
||||
});
|
||||
|
||||
console.log('Custom auth response:', JSON.stringify(response, null, 2));
|
||||
|
||||
// Check if we need to open a popup window (OAuth2 flow)
|
||||
if ('connectionData' in response &&
|
||||
response.connectionData.val &&
|
||||
'redirectUrl' in response.connectionData.val &&
|
||||
response.connectionData.val.redirectUrl) {
|
||||
|
||||
// Open OAuth window for custom OAuth2
|
||||
const authWindow = window.open(
|
||||
response.connectionData.val.redirectUrl as string,
|
||||
'_blank',
|
||||
'width=600,height=700'
|
||||
);
|
||||
|
||||
if (authWindow) {
|
||||
// Use the same postMessage logic as Composio OAuth2
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.origin !== window.location.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data && event.data.type === 'OAUTH_COMPLETE') {
|
||||
window.removeEventListener('message', handleMessage);
|
||||
clearInterval(checkInterval);
|
||||
|
||||
if (event.data.success) {
|
||||
handleOAuthCompletion(response.id);
|
||||
} else {
|
||||
const errorMessage = event.data.errorDescription || event.data.error || 'OAuth authentication failed';
|
||||
setError(errorMessage);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', handleMessage);
|
||||
|
||||
const checkInterval = setInterval(() => {
|
||||
if (authWindow.closed) {
|
||||
clearInterval(checkInterval);
|
||||
window.removeEventListener('message', handleMessage);
|
||||
handleOAuthCompletion(response.id);
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
window.alert('Failed to open authentication window. Please check your popup blocker settings.');
|
||||
setError('Failed to open authentication window');
|
||||
}
|
||||
} else {
|
||||
// No redirect needed, just sync and complete
|
||||
await syncConnectedAccount(projectId, toolkitSlug, response.id);
|
||||
onComplete();
|
||||
onClose();
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Custom auth failed:', err);
|
||||
const errorMessage = err.message || 'Failed to authenticate with toolkit';
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
}, [selectedAuthScheme, toolkit, projectId, formData, handleOAuthCompletion, onComplete, onClose, toolkitSlug]);
|
||||
|
||||
const handleBackToOptions = useCallback(() => {
|
||||
setShowForm(false);
|
||||
setSelectedAuthScheme(null);
|
||||
setFormData({});
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
const getAuthMethodIcon = (authScheme: string) => {
|
||||
switch (authScheme) {
|
||||
case 'OAUTH2':
|
||||
return <Shield className="h-5 w-5" />;
|
||||
case 'API_KEY':
|
||||
return <Key className="h-5 w-5" />;
|
||||
case 'BEARER_TOKEN':
|
||||
return <Key className="h-5 w-5" />;
|
||||
default:
|
||||
return <Globe className="h-5 w-5" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getAuthMethodName = (authScheme: string) => {
|
||||
switch (authScheme) {
|
||||
case 'OAUTH2':
|
||||
return 'OAuth2';
|
||||
case 'API_KEY':
|
||||
return 'API Key';
|
||||
case 'BEARER_TOKEN':
|
||||
return 'Bearer Token';
|
||||
case 'BASIC':
|
||||
return 'Basic Auth';
|
||||
default:
|
||||
return authScheme.toLowerCase().replace('_', ' ');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onClose}
|
||||
size="md"
|
||||
classNames={{
|
||||
base: "bg-white dark:bg-gray-900",
|
||||
header: "border-b border-gray-200 dark:border-gray-800",
|
||||
footer: "border-t border-gray-200 dark:border-gray-800",
|
||||
}}
|
||||
>
|
||||
<ModalContent>
|
||||
<ModalHeader className="flex gap-3 items-center">
|
||||
{showForm && (
|
||||
<HeroButton
|
||||
variant="light"
|
||||
size="sm"
|
||||
isIconOnly
|
||||
onPress={handleBackToOptions}
|
||||
className="mr-1"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</HeroButton>
|
||||
)}
|
||||
{toolkitDetails?.meta?.logo ? (
|
||||
<PictureImg
|
||||
src={toolkitDetails.meta.logo}
|
||||
alt={`${toolkitSlug} logo`}
|
||||
className="w-8 h-8 rounded-md object-cover"
|
||||
/>
|
||||
) : (
|
||||
<Wrench className="w-5 h-5 text-blue-500" />
|
||||
)}
|
||||
<span>
|
||||
{showForm
|
||||
? `Configure ${getAuthMethodName(selectedAuthScheme || '')}`
|
||||
: `Connect to ${toolkitSlug}`
|
||||
}
|
||||
</span>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<Spinner size="lg" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="text-sm text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20 p-3 rounded-md">
|
||||
{error}
|
||||
</div>
|
||||
) : toolkit ? (
|
||||
showForm ? (
|
||||
// Form view
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Enter your credentials for {getAuthMethodName(selectedAuthScheme || '')} authentication:
|
||||
</div>
|
||||
|
||||
{(() => {
|
||||
const authConfig = toolkit.auth_config_details?.find(config => config.mode === selectedAuthScheme);
|
||||
|
||||
if (!authConfig) {
|
||||
return <div>No configuration found for {selectedAuthScheme}</div>;
|
||||
}
|
||||
|
||||
// Try connected_account_initiation first, fallback to auth_config_creation
|
||||
const allFields = [
|
||||
...authConfig.fields.connected_account_initiation.required.map(field => ({ ...field, required: true })),
|
||||
...authConfig.fields.connected_account_initiation.optional.map(field => ({ ...field, required: false }))
|
||||
];
|
||||
|
||||
// If no fields in connected_account_initiation, try auth_config_creation
|
||||
if (allFields.length === 0) {
|
||||
allFields.push(
|
||||
...authConfig.fields.auth_config_creation.required.map(field => ({ ...field, required: true })),
|
||||
...authConfig.fields.auth_config_creation.optional.map(field => ({ ...field, required: false }))
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{allFields.map(field => (
|
||||
<Input
|
||||
key={field.name}
|
||||
label={field.displayName}
|
||||
placeholder={field.description}
|
||||
value={formData[field.name] || ''}
|
||||
onValueChange={(value) => setFormData(prev => ({ ...prev, [field.name]: value }))}
|
||||
isRequired={field.required}
|
||||
type={field.type === 'password' ? 'password' : 'text'}
|
||||
variant="bordered"
|
||||
description={field.description}
|
||||
required={field.required}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
) : (
|
||||
// Auth options view
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Choose how you'd like to authenticate with this toolkit:
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{/* OAuth2 Composio Managed */}
|
||||
{toolkit.composio_managed_auth_schemes.includes('OAUTH2') && (
|
||||
<HeroButton
|
||||
className="w-full justify-start gap-3 h-auto py-4 px-4"
|
||||
variant="bordered"
|
||||
onPress={handleComposioOAuth2}
|
||||
isDisabled={processing}
|
||||
size="lg"
|
||||
>
|
||||
<div className="bg-green-100 dark:bg-green-900/20 p-2 rounded-lg">
|
||||
<Shield className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="font-medium">Connect using OAuth2</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Secure authentication managed by Composio
|
||||
</div>
|
||||
</div>
|
||||
{processing && <Spinner size="sm" className="ml-auto" />}
|
||||
</HeroButton>
|
||||
)}
|
||||
|
||||
{/* Custom OAuth2 - always show if OAuth2 is supported */}
|
||||
{(toolkit.composio_managed_auth_schemes.includes('OAUTH2') ||
|
||||
toolkit.auth_config_details?.some(config => config.mode === 'OAUTH2')) && (
|
||||
<HeroButton
|
||||
className="w-full justify-start gap-3 h-auto py-4 px-4"
|
||||
variant="bordered"
|
||||
onPress={() => handleCustomAuth('OAUTH2')}
|
||||
isDisabled={processing}
|
||||
size="lg"
|
||||
>
|
||||
<div className="bg-orange-100 dark:bg-orange-900/20 p-2 rounded-lg">
|
||||
<Shield className="h-5 w-5 text-orange-600 dark:text-orange-400" />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="font-medium">Connect using custom OAuth2 app</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Use your own OAuth2 configuration
|
||||
</div>
|
||||
</div>
|
||||
</HeroButton>
|
||||
)}
|
||||
|
||||
{/* Other auth schemes (excluding OAuth2 since it's shown above) */}
|
||||
{toolkit.auth_config_details?.filter(config => config.mode !== 'OAUTH2').map(config => (
|
||||
<HeroButton
|
||||
key={config.mode}
|
||||
className="w-full justify-start gap-3 h-auto py-4 px-4"
|
||||
variant="bordered"
|
||||
onPress={() => handleCustomAuth(config.mode)}
|
||||
isDisabled={processing}
|
||||
size="lg"
|
||||
>
|
||||
<div className="bg-blue-100 dark:bg-blue-900/20 p-2 rounded-lg">
|
||||
{getAuthMethodIcon(config.mode)}
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="font-medium">Connect using {getAuthMethodName(config.mode)}</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Enter your credentials
|
||||
</div>
|
||||
</div>
|
||||
</HeroButton>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
) : null}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
{showForm ? (
|
||||
<>
|
||||
<HeroButton variant="bordered" onPress={handleBackToOptions} isDisabled={processing}>
|
||||
Back
|
||||
</HeroButton>
|
||||
<HeroButton
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onPress={handleFormSubmit}
|
||||
isDisabled={processing}
|
||||
isLoading={processing}
|
||||
>
|
||||
{processing ? 'Connecting...' : 'Connect'}
|
||||
</HeroButton>
|
||||
</>
|
||||
) : (
|
||||
<HeroButton variant="bordered" onPress={onClose}>
|
||||
Cancel
|
||||
</HeroButton>
|
||||
)}
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { PictureImg } from '@/components/ui/picture-img';
|
||||
import { Wrench } from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
import { Spinner } from '@heroui/react';
|
||||
import { deleteConnectedAccount } from '@/app/actions/composio_actions';
|
||||
import { z } from 'zod';
|
||||
import { ZToolkit } from '@/app/lib/composio/composio';
|
||||
import { Project } from '@/app/lib/types/project_types';
|
||||
import { ToolkitAuthModal } from './ToolkitAuthModal';
|
||||
|
||||
type ToolkitType = z.infer<typeof ZToolkit>;
|
||||
type ProjectType = z.infer<typeof Project>;
|
||||
|
||||
const toolkitCardStyles = {
|
||||
base: clsx(
|
||||
"group p-6 rounded-xl transition-all duration-200",
|
||||
"bg-white dark:bg-gray-900 shadow-sm dark:shadow-none",
|
||||
"border-2 border-gray-200/80 dark:border-gray-700/80",
|
||||
"hover:shadow-md dark:hover:shadow-none",
|
||||
"hover:border-blue-200 dark:hover:border-blue-900",
|
||||
"min-h-[280px] flex flex-col"
|
||||
),
|
||||
};
|
||||
|
||||
interface ToolkitCardProps {
|
||||
toolkit: ToolkitType;
|
||||
projectId: string;
|
||||
isConnected: boolean;
|
||||
connectedAccountId?: string;
|
||||
projectConfig: ProjectType | null;
|
||||
onManageTools: () => void;
|
||||
onProjectConfigUpdate: () => void;
|
||||
onRemoveToolkitTools: (toolkitSlug: string) => void;
|
||||
}
|
||||
|
||||
export function ToolkitCard({
|
||||
toolkit,
|
||||
projectId,
|
||||
isConnected,
|
||||
connectedAccountId,
|
||||
projectConfig,
|
||||
onManageTools,
|
||||
onProjectConfigUpdate,
|
||||
onRemoveToolkitTools
|
||||
}: ToolkitCardProps) {
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showAuthModal, setShowAuthModal] = useState(false);
|
||||
|
||||
const handleToggleConnection = useCallback(async () => {
|
||||
const newState = !isConnected;
|
||||
|
||||
// Clear any previous error when starting a new operation
|
||||
setError(null);
|
||||
|
||||
if (newState) {
|
||||
// Show authentication modal
|
||||
setShowAuthModal(true);
|
||||
} else {
|
||||
// Disconnect - remove the connected account
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
if (connectedAccountId) {
|
||||
await deleteConnectedAccount(projectId, toolkit.slug, connectedAccountId);
|
||||
onProjectConfigUpdate();
|
||||
onRemoveToolkitTools(toolkit.slug);
|
||||
} else {
|
||||
// Fallback: just refresh the project config
|
||||
onProjectConfigUpdate();
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Disconnect failed:', err);
|
||||
const errorMessage = err.message || 'Failed to disconnect toolkit';
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
}
|
||||
}, [projectId, toolkit.slug, isConnected, connectedAccountId, onProjectConfigUpdate, onRemoveToolkitTools]);
|
||||
|
||||
const handleAuthComplete = useCallback(() => {
|
||||
// Update project config when authentication completes
|
||||
onProjectConfigUpdate();
|
||||
}, [onProjectConfigUpdate]);
|
||||
|
||||
// Calculate selected tools count for this toolkit
|
||||
const selectedToolsCount = projectConfig?.composioSelectedTools?.filter(tool =>
|
||||
tool.toolkit.slug === toolkit.slug
|
||||
).length || 0;
|
||||
|
||||
return (
|
||||
<div className={toolkitCardStyles.base}>
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{toolkit.meta.logo && (
|
||||
<PictureImg
|
||||
src={toolkit.meta.logo}
|
||||
alt={`${toolkit.name} logo`}
|
||||
className="w-8 h-8 rounded-md object-cover"
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg text-gray-900 dark:text-gray-100">
|
||||
{toolkit.name}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className="px-1.5 py-0.5 rounded-full text-xs font-medium
|
||||
bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300">
|
||||
{toolkit.meta.tools_count} tools
|
||||
</span>
|
||||
{selectedToolsCount > 0 && (
|
||||
<span className="px-1.5 py-0.5 rounded-full text-xs font-medium
|
||||
bg-purple-50 dark:bg-purple-900/20 text-purple-700 dark:text-purple-300">
|
||||
{selectedToolsCount} selected
|
||||
</span>
|
||||
)}
|
||||
{toolkit.no_auth && (
|
||||
<span className="px-1.5 py-0.5 rounded-full text-xs font-medium
|
||||
bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-300">
|
||||
No Auth
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{toolkit.no_auth ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={true}
|
||||
onCheckedChange={() => {}} // No-op for no-auth toolkits
|
||||
disabled={true}
|
||||
className={clsx(
|
||||
"data-[state=checked]:bg-emerald-500 dark:data-[state=checked]:bg-emerald-600",
|
||||
"data-[state=unchecked]:bg-emerald-500 dark:data-[state=unchecked]:bg-emerald-600",
|
||||
"opacity-50 cursor-not-allowed",
|
||||
"scale-75"
|
||||
)}
|
||||
/>
|
||||
<span className="text-xs text-emerald-600 dark:text-emerald-400 font-medium">
|
||||
Always Available
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<Switch
|
||||
checked={isConnected}
|
||||
onCheckedChange={handleToggleConnection}
|
||||
disabled={isProcessing}
|
||||
className={clsx(
|
||||
"data-[state=checked]:bg-blue-500 dark:data-[state=checked]:bg-blue-600",
|
||||
"data-[state=unchecked]:bg-gray-200 dark:data-[state=unchecked]:bg-gray-700",
|
||||
isProcessing && "opacity-50 cursor-not-allowed",
|
||||
"scale-75"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4 line-clamp-3">
|
||||
{toolkit.meta.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs text-gray-400 dark:text-gray-500">
|
||||
ID: {toolkit.slug}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{isProcessing && (
|
||||
<div className="flex items-center gap-1 text-xs py-1 px-2 rounded-full text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20">
|
||||
<Spinner size="sm" />
|
||||
<span>Processing...</span>
|
||||
</div>
|
||||
)}
|
||||
{(isConnected || toolkit.no_auth) && !isProcessing && (
|
||||
<div className="text-xs py-1 px-2 rounded-full text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-900/20">
|
||||
{toolkit.no_auth ? 'Available' : 'Connected'}
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="text-xs py-1 px-2 rounded-full text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20">
|
||||
Error: {error}
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={onManageTools}
|
||||
className="text-xs"
|
||||
>
|
||||
<div className="inline-flex items-center">
|
||||
<Wrench className="h-3.5 w-3.5" />
|
||||
<span className="ml-1.5">Tools</span>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ToolkitAuthModal
|
||||
key={toolkit.slug}
|
||||
isOpen={showAuthModal}
|
||||
onClose={() => setShowAuthModal(false)}
|
||||
toolkitSlug={toolkit.slug}
|
||||
projectId={projectId}
|
||||
onComplete={handleAuthComplete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -4,10 +4,11 @@ import { useState } from 'react';
|
|||
import { Tabs, Tab } from '@/components/ui/tabs';
|
||||
import { HostedServers } from './HostedServers';
|
||||
import { CustomServers } from './CustomServers';
|
||||
import { Composio } from './Composio';
|
||||
import type { Key } from 'react';
|
||||
|
||||
export function ToolsConfig() {
|
||||
const [activeTab, setActiveTab] = useState('hosted');
|
||||
export function ToolsConfig({ useComposioTools }: { useComposioTools: boolean }) {
|
||||
const [activeTab, setActiveTab] = useState(useComposioTools ? 'composio' : 'hosted');
|
||||
|
||||
const handleTabChange = (key: Key) => {
|
||||
setActiveTab(key.toString());
|
||||
|
|
@ -22,6 +23,13 @@ export function ToolsConfig() {
|
|||
className="w-full"
|
||||
fullWidth
|
||||
>
|
||||
{useComposioTools && (
|
||||
<Tab key="composio" title="Composio">
|
||||
<div className="mt-4 p-6">
|
||||
<Composio />
|
||||
</div>
|
||||
</Tab>
|
||||
)}
|
||||
<Tab key="hosted" title={
|
||||
<div className="flex items-center gap-2">
|
||||
<span>Tools Library</span>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { Suspense } from 'react';
|
|||
import { ToolsConfig } from './components/ToolsConfig';
|
||||
import { PageHeader } from '@/components/ui/page-header';
|
||||
import { requireActiveBillingSubscription } from '@/app/lib/billing';
|
||||
import { USE_COMPOSIO_TOOLS } from '@/app/lib/feature_flags';
|
||||
|
||||
export default async function ToolsPage() {
|
||||
await requireActiveBillingSubscription();
|
||||
|
|
@ -14,7 +15,7 @@ export default async function ToolsPage() {
|
|||
/>
|
||||
<div className="flex-1 p-6">
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<ToolsConfig />
|
||||
<ToolsConfig useComposioTools={USE_COMPOSIO_TOOLS} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ import { WorkflowSelector } from "./workflow_selector";
|
|||
import { Spinner } from "@heroui/react";
|
||||
import { cloneWorkflow, createWorkflow, fetchPublishedWorkflowId, fetchWorkflow } from "../../../actions/workflow_actions";
|
||||
import { listDataSources } from "../../../actions/datasource_actions";
|
||||
import { listMcpServers, listProjectMcpTools } from "@/app/actions/mcp_actions";
|
||||
import { listMcpServers } from "@/app/actions/mcp_actions";
|
||||
import { collectProjectTools } from "@/app/actions/project_actions";
|
||||
import { getProjectConfig } from "@/app/actions/project_actions";
|
||||
import { WorkflowTool } from "@/app/lib/types/workflow_types";
|
||||
import { getEligibleModels } from "@/app/actions/billing_actions";
|
||||
|
|
@ -51,7 +52,7 @@ export function App({
|
|||
listDataSources(projectId),
|
||||
listMcpServers(projectId),
|
||||
getProjectConfig(projectId),
|
||||
listProjectMcpTools(projectId),
|
||||
collectProjectTools(projectId),
|
||||
getEligibleModels(),
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { SortableContext, sortableKeyboardCoordinates, useSortable, verticalList
|
|||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { Panel } from "@/components/common/panel-common";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { PictureImg } from "@/components/ui/picture-img";
|
||||
import { clsx } from "clsx";
|
||||
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "@/components/ui/resizable";
|
||||
import { ServerLogo } from '../tools/components/MCPServersCommon';
|
||||
|
|
@ -204,6 +205,13 @@ const ServerCard = ({
|
|||
);
|
||||
};
|
||||
|
||||
type ComposioToolkit = {
|
||||
slug: string;
|
||||
name: string;
|
||||
logo: string;
|
||||
tools: z.infer<typeof WorkflowTool>[];
|
||||
}
|
||||
|
||||
export function EntityList({
|
||||
agents,
|
||||
tools,
|
||||
|
|
@ -234,6 +242,22 @@ export function EntityList({
|
|||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [containerHeight, setContainerHeight] = useState<number>(0);
|
||||
|
||||
// collect composio tools
|
||||
const composioTools: Record<string, ComposioToolkit> = {};
|
||||
for (const tool of mergedTools) {
|
||||
if (tool.isComposio) {
|
||||
if (!composioTools[tool.composioData?.toolkitSlug || '']) {
|
||||
composioTools[tool.composioData?.toolkitSlug || ''] = {
|
||||
name: tool.composioData?.toolkitName || '',
|
||||
slug: tool.composioData?.toolkitSlug || '',
|
||||
logo: tool.composioData?.logo || '',
|
||||
tools: []
|
||||
};
|
||||
}
|
||||
composioTools[tool.composioData?.toolkitSlug || ''].tools.push(tool);
|
||||
}
|
||||
}
|
||||
|
||||
// Panel expansion states
|
||||
const [expandedPanels, setExpandedPanels] = useState({
|
||||
agents: true,
|
||||
|
|
@ -469,7 +493,7 @@ export function EntityList({
|
|||
{/* Group tools by server */}
|
||||
{(() => {
|
||||
// Get custom tools (non-MCP tools)
|
||||
const customTools = mergedTools.filter(tool => !tool.isMcp);
|
||||
const customTools = mergedTools.filter(tool => !tool.isMcp && !tool.isComposio);
|
||||
|
||||
// Group MCP tools by server
|
||||
const serverTools = mergedTools.reduce((acc, tool) => {
|
||||
|
|
@ -484,7 +508,19 @@ export function EntityList({
|
|||
|
||||
return (
|
||||
<>
|
||||
{/* Show MCP server cards first */}
|
||||
{/* Show composio cards */}
|
||||
{Object.values(composioTools).map((card) => (
|
||||
<ComposioCard
|
||||
key={card.slug}
|
||||
card={card}
|
||||
selectedEntity={selectedEntity}
|
||||
onSelectTool={handleToolSelection}
|
||||
onDeleteTool={onDeleteTool}
|
||||
selectedRef={selectedRef}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Show MCP server cards */}
|
||||
{Object.entries(serverTools).map(([serverName, tools]) => (
|
||||
<ServerCard
|
||||
key={serverName}
|
||||
|
|
@ -690,6 +726,90 @@ function EntityDropdown({
|
|||
);
|
||||
}
|
||||
|
||||
interface ComposioCardProps {
|
||||
card: ComposioToolkit;
|
||||
selectedEntity: {
|
||||
type: "agent" | "tool" | "prompt";
|
||||
name: string;
|
||||
} | null;
|
||||
onSelectTool: (name: string) => void;
|
||||
onDeleteTool: (name: string) => void;
|
||||
selectedRef: React.RefObject<HTMLButtonElement | null>;
|
||||
}
|
||||
|
||||
const ComposioCard = ({
|
||||
card,
|
||||
selectedEntity,
|
||||
onSelectTool,
|
||||
onDeleteTool,
|
||||
selectedRef,
|
||||
}: ComposioCardProps) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="w-full flex items-center gap-2 px-2 py-1.5 hover:bg-zinc-50 dark:hover:bg-zinc-800 rounded-md text-sm text-left"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="w-4 h-4 text-gray-500" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 text-gray-500" />
|
||||
)}
|
||||
<div className="flex items-center gap-1">
|
||||
{card.logo ? (
|
||||
<div className="relative w-4 h-4">
|
||||
<PictureImg
|
||||
src={card.logo}
|
||||
alt={`${card.name} logo`}
|
||||
className="w-full h-full object-contain rounded"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<ImportIcon className="w-4 h-4 text-blue-600 dark:text-blue-500" />
|
||||
)}
|
||||
<span>{card.name}</span>
|
||||
</div>
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="ml-6 mt-1 space-y-1">
|
||||
{card.tools.map((tool, index) => (
|
||||
<ListItemWithMenu
|
||||
key={`composio-tool-${index}`}
|
||||
name={tool.name}
|
||||
isSelected={selectedEntity?.type === "tool" && selectedEntity.name === tool.name}
|
||||
onClick={() => onSelectTool(tool.name)}
|
||||
disabled={tool.isLibrary}
|
||||
selectedRef={selectedEntity?.type === "tool" && selectedEntity.name === tool.name ? selectedRef : undefined}
|
||||
icon={
|
||||
card.logo ? (
|
||||
<div className="relative w-4 h-4">
|
||||
<PictureImg
|
||||
src={card.logo}
|
||||
alt={`${card.name} logo`}
|
||||
className="w-full h-full object-contain rounded"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<ImportIcon className="w-4 h-4 text-blue-600 dark:text-blue-500" />
|
||||
)
|
||||
}
|
||||
menuContent={
|
||||
<EntityDropdown
|
||||
name={tool.name}
|
||||
onDelete={onDeleteTool}
|
||||
isLocked={tool.isComposio}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Add SortableItem component for agents
|
||||
const SortableAgentItem = ({ agent, isSelected, onClick, selectedRef, statusLabel, onToggle, onSetMainAgent, onDelete, isStartAgent }: {
|
||||
agent: z.infer<typeof WorkflowAgent>;
|
||||
|
|
|
|||
61
apps/rowboat/components/ui/picture-img.tsx
Normal file
61
apps/rowboat/components/ui/picture-img.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import React from 'react';
|
||||
|
||||
interface SourceProps {
|
||||
srcSet: string;
|
||||
media?: string;
|
||||
type?: string;
|
||||
sizes?: string;
|
||||
}
|
||||
|
||||
interface PictureImgProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
||||
src: string;
|
||||
alt: string;
|
||||
sources?: SourceProps[];
|
||||
fallbackSrc?: string;
|
||||
onError?: React.ReactEventHandler<HTMLImageElement>;
|
||||
}
|
||||
|
||||
export function PictureImg({
|
||||
src,
|
||||
alt,
|
||||
sources = [],
|
||||
fallbackSrc,
|
||||
onError,
|
||||
className,
|
||||
...imgProps
|
||||
}: PictureImgProps) {
|
||||
const handleError: React.ReactEventHandler<HTMLImageElement> = (e) => {
|
||||
if (fallbackSrc && e.currentTarget.src !== fallbackSrc) {
|
||||
e.currentTarget.src = fallbackSrc;
|
||||
return;
|
||||
}
|
||||
|
||||
if (onError) {
|
||||
onError(e);
|
||||
} else {
|
||||
// Default error handling - hide the image
|
||||
e.currentTarget.style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<picture>
|
||||
{sources.map((source, index) => (
|
||||
<source
|
||||
key={index}
|
||||
srcSet={source.srcSet}
|
||||
media={source.media}
|
||||
type={source.type}
|
||||
sizes={source.sizes}
|
||||
/>
|
||||
))}
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
className={className}
|
||||
onError={handleError}
|
||||
{...imgProps}
|
||||
/>
|
||||
</picture>
|
||||
);
|
||||
}
|
||||
169
apps/rowboat/package-lock.json
generated
169
apps/rowboat/package-lock.json
generated
|
|
@ -12,6 +12,7 @@
|
|||
"@auth0/nextjs-auth0": "^4.7.0",
|
||||
"@aws-sdk/client-s3": "^3.743.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.743.0",
|
||||
"@composio/core": "^0.1.36-next.7",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
|
|
@ -1356,6 +1357,59 @@
|
|||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cfworker/json-schema": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz",
|
||||
"integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@composio/client": {
|
||||
"version": "0.1.0-alpha.26",
|
||||
"resolved": "https://registry.npmjs.org/@composio/client/-/client-0.1.0-alpha.26.tgz",
|
||||
"integrity": "sha512-6cLf8sABDe0tjG4U33GDd3ZS4S3YdVCEpFyi9bt22vanqtNKJk8wNEOFOui9TgI131B45Tt/XtlCeCR+PpxQLQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@composio/core": {
|
||||
"version": "0.1.36-next.7",
|
||||
"resolved": "https://registry.npmjs.org/@composio/core/-/core-0.1.36-next.7.tgz",
|
||||
"integrity": "sha512-MqwYKBIoCMn9VegS67UO/RkO/C5L7aA0r28K7VbIS4ZwMY/9/hl7UPqat7EZapyhpLFw4OaTHeAjAMcu6dRm1g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@composio/client": "0.1.0-alpha.26",
|
||||
"@composio/json-schema-to-zod": "0.1.10-next.3",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"chalk": "^4.1.2",
|
||||
"openai": "^4.94.0",
|
||||
"pusher-js": "^8.4.0",
|
||||
"semver": "^7.7.2",
|
||||
"uuid": "^11.1.0",
|
||||
"zod": "^3.24.2",
|
||||
"zod-to-json-schema": "^3.24.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@composio/core/node_modules/uuid": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@composio/json-schema-to-zod": {
|
||||
"version": "0.1.10-next.3",
|
||||
"resolved": "https://registry.npmjs.org/@composio/json-schema-to-zod/-/json-schema-to-zod-0.1.10-next.3.tgz",
|
||||
"integrity": "sha512-kglZdteq+18Tcikb1US7D47VX3A3x8haeE+qFCwpQbBAgp3vG+eMQnZ6WgMkLNroZNrTm5oiAJrSdjerbiAqIA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"zod": "^3.24.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/accessibility": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
||||
|
|
@ -4331,20 +4385,22 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@langchain/core": {
|
||||
"version": "0.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.7.tgz",
|
||||
"integrity": "sha512-6wsnEtw5GlhmBhoLfw/g8Hrp09BNwQwDLXyuv3GyK+ay4/3H3YuhAphqQLO4HNphuZIZKlW9ihSrqdCMvvbvZQ==",
|
||||
"version": "0.3.61",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.61.tgz",
|
||||
"integrity": "sha512-4O7fw5SXNSE+uBnathLQrhm3t+7dZGagt/5kt37A+pXw0AkudxEBvveg73sSnpBd9SIz3/Vc7F4k8rCKXGbEDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cfworker/json-schema": "^4.0.2",
|
||||
"ansi-styles": "^5.0.0",
|
||||
"camelcase": "6",
|
||||
"decamelize": "1.2.0",
|
||||
"js-tiktoken": "^1.0.12",
|
||||
"langsmith": "^0.1.56",
|
||||
"langsmith": "^0.3.33",
|
||||
"mustache": "^4.2.0",
|
||||
"p-queue": "^6.6.2",
|
||||
"p-retry": "4",
|
||||
"uuid": "^10.0.0",
|
||||
"zod": "^3.22.4",
|
||||
"zod": "^3.25.32",
|
||||
"zod-to-json-schema": "^3.22.3"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -7576,6 +7632,12 @@
|
|||
"@types/unist": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/json5": {
|
||||
"version": "0.0.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
||||
|
|
@ -7661,7 +7723,8 @@
|
|||
"node_modules/@types/retry": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
|
||||
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="
|
||||
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/styled-system": {
|
||||
"version": "5.1.23",
|
||||
|
|
@ -7696,7 +7759,8 @@
|
|||
"node_modules/@types/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ=="
|
||||
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/webidl-conversions": {
|
||||
"version": "7.0.3",
|
||||
|
|
@ -8133,7 +8197,6 @@
|
|||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
|
|
@ -8687,7 +8750,6 @@
|
|||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
|
|
@ -8904,6 +8966,15 @@
|
|||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/console-table-printer": {
|
||||
"version": "2.14.6",
|
||||
"resolved": "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.14.6.tgz",
|
||||
"integrity": "sha512-MCBl5HNVaFuuHW6FGbL/4fB7N/ormCy+tQ+sxTrF6QtSbSNETvPuOVbkJBhzDgYhvjWGrTma4eYJa37ZuoQsPw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"simple-wcswidth": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
|
||||
|
|
@ -10151,7 +10222,8 @@
|
|||
"node_modules/eventemitter3": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
|
||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/eventsource": {
|
||||
"version": "3.0.5",
|
||||
|
|
@ -10821,7 +10893,6 @@
|
|||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
|
|
@ -11853,34 +11924,40 @@
|
|||
}
|
||||
},
|
||||
"node_modules/langsmith": {
|
||||
"version": "0.1.61",
|
||||
"resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.1.61.tgz",
|
||||
"integrity": "sha512-XQE4KPScwPmdaT0mWDzhNxj9gvqXUR+C7urLA0QFi27XeoQdm17eYpudenn4wxC0gIyUJutQCyuYJpfwlT5JnQ==",
|
||||
"version": "0.3.37",
|
||||
"resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.37.tgz",
|
||||
"integrity": "sha512-aDFM+LbT01gP8hsJNs4QJjmbRNfoifqhpCSpk8j4k/V8wejEgvgATbgj9W9DQsfQFdtfwx+8G48sK5/0PqQisg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/uuid": "^10.0.0",
|
||||
"commander": "^10.0.1",
|
||||
"chalk": "^4.1.2",
|
||||
"console-table-printer": "^2.12.1",
|
||||
"p-queue": "^6.6.2",
|
||||
"p-retry": "4",
|
||||
"semver": "^7.6.3",
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "*",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "*",
|
||||
"@opentelemetry/sdk-trace-base": "*",
|
||||
"openai": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentelemetry/exporter-trace-otlp-proto": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentelemetry/sdk-trace-base": {
|
||||
"optional": true
|
||||
},
|
||||
"openai": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/langsmith/node_modules/commander": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
|
||||
"integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/langsmith/node_modules/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||
|
|
@ -11889,6 +11966,7 @@
|
|||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
|
|
@ -14129,9 +14207,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/openai": {
|
||||
"version": "4.67.2",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-4.67.2.tgz",
|
||||
"integrity": "sha512-u4FJFGXgqEHrCYcD5jAD4nHj6JCiicH+/dskQY7qka9R6hOw29R0kOz7GwcA9k2JKcLf86lzAWPtPagPbO8KnQ==",
|
||||
"version": "4.104.0",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz",
|
||||
"integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node-fetch": "^2.6.4",
|
||||
|
|
@ -14145,9 +14224,13 @@
|
|||
"openai": "bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ws": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
|
|
@ -14200,6 +14283,7 @@
|
|||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
||||
"integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
|
|
@ -14238,6 +14322,7 @@
|
|||
"version": "6.6.2",
|
||||
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
|
||||
"integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eventemitter3": "^4.0.4",
|
||||
"p-timeout": "^3.2.0"
|
||||
|
|
@ -14253,6 +14338,7 @@
|
|||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
|
||||
"integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/retry": "0.12.0",
|
||||
"retry": "^0.13.1"
|
||||
|
|
@ -14265,6 +14351,7 @@
|
|||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
|
||||
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-finally": "^1.0.0"
|
||||
},
|
||||
|
|
@ -14542,6 +14629,15 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/pusher-js": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.4.0.tgz",
|
||||
"integrity": "sha512-wp3HqIIUc1GRyu1XrP6m2dgyE9MoCsXVsWNlohj0rjSkLf+a0jLvEyVubdg58oMk7bhjBWnFClgp8jfAa6Ak4Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tweetnacl": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||
|
|
@ -14953,6 +15049,7 @@
|
|||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
||||
"integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
|
|
@ -15404,6 +15501,12 @@
|
|||
"is-arrayish": "^0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-wcswidth": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz",
|
||||
"integrity": "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||
|
|
@ -15759,7 +15862,6 @@
|
|||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
|
|
@ -16006,6 +16108,12 @@
|
|||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/tweetnacl": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
|
||||
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/twilio": {
|
||||
"version": "5.4.5",
|
||||
"resolved": "https://registry.npmjs.org/twilio/-/twilio-5.4.5.tgz",
|
||||
|
|
@ -16672,9 +16780,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/zod-to-json-schema": {
|
||||
"version": "3.24.3",
|
||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.3.tgz",
|
||||
"integrity": "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A==",
|
||||
"version": "3.24.6",
|
||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz",
|
||||
"integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.24.1"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
"@auth0/nextjs-auth0": "^4.7.0",
|
||||
"@aws-sdk/client-s3": "^3.743.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.743.0",
|
||||
"@composio/core": "^0.1.36-next.7",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ services:
|
|||
- USE_BILLING=${USE_BILLING}
|
||||
- BILLING_API_URL=${BILLING_API_URL}
|
||||
- BILLING_API_KEY=${BILLING_API_KEY}
|
||||
- USE_COMPOSIO_TOOLS=${USE_COMPOSIO_TOOLS}
|
||||
- COMPOSIO_API_KEY=${COMPOSIO_API_KEY}
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- uploads:/app/uploads
|
||||
|
|
|
|||
5
start.sh
5
start.sh
|
|
@ -9,6 +9,11 @@ mkdir -p data/mongo
|
|||
export USE_RAG=true
|
||||
export USE_RAG_UPLOADS=true
|
||||
|
||||
# enable composio tools if API key is set
|
||||
if [ -n "$COMPOSIO_API_KEY" ]; then
|
||||
export USE_COMPOSIO_TOOLS=true
|
||||
fi
|
||||
|
||||
# Start with the base command and profile flags
|
||||
CMD="docker-compose"
|
||||
CMD="$CMD --profile setup_qdrant"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue