mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-29 02:24:02 +02:00
Refactor Composio integration handling and improve UI components
- Updated composio-handler.ts to invalidate the copilot instructions cache upon connection and disconnection. - Removed unused functions related to tool management in composio-handler.ts. - Enhanced IPC handlers in ipc.ts to streamline Composio connection processes. - Introduced ComposioConnectCard in the renderer to display connection status and handle events. - Refactored tool rendering in App.tsx and chat-sidebar.tsx to utilize new tabbed content for parameters and results. - Improved Composio tools prompt generation in instructions.ts to clarify integration usage and discovery flow. - Cleaned up unused code and improved overall structure for better maintainability.
This commit is contained in:
parent
abf6901cc9
commit
7f8d2e64af
18 changed files with 864 additions and 812 deletions
|
|
@ -370,8 +370,7 @@ function formatLlmStreamError(rawError: unknown): string {
|
|||
|
||||
export async function loadAgent(id: string): Promise<z.infer<typeof Agent>> {
|
||||
if (id === "copilot" || id === "rowboatx") {
|
||||
// Rebuild tools from current BuiltinTools to pick up dynamically
|
||||
// registered Composio tools (added via refreshComposioTools).
|
||||
// Rebuild tools from current BuiltinTools (includes Composio meta-tools).
|
||||
const tools: Record<string, z.infer<typeof ToolAttachment>> = {};
|
||||
for (const name of Object.keys(BuiltinTools)) {
|
||||
tools[name] = { type: "builtin", name };
|
||||
|
|
|
|||
|
|
@ -1,51 +1,55 @@
|
|||
import { skillCatalog } from "./skills/index.js"; // eslint-disable-line @typescript-eslint/no-unused-vars -- used in template literal
|
||||
import { getRuntimeContext, getRuntimeContextPrompt } from "./runtime-context.js";
|
||||
import { composioEnabledToolsRepo } from "../../composio/enabled-tools-repo.js";
|
||||
import { composioAccountsRepo } from "../../composio/repo.js";
|
||||
import { isConfigured as isComposioConfigured } from "../../composio/client.js";
|
||||
import { CURATED_TOOLKITS } from "../../composio/curated-toolkits.js";
|
||||
|
||||
const runtimeContextPrompt = getRuntimeContextPrompt(getRuntimeContext());
|
||||
|
||||
/**
|
||||
* Generate dynamic instructions section for Composio tools.
|
||||
* Returns empty string if no tools are enabled.
|
||||
* Generate dynamic instructions section for Composio integrations.
|
||||
* Lists connected toolkits and explains the meta-tool discovery flow.
|
||||
*/
|
||||
async function getComposioToolsPrompt(): Promise<string> {
|
||||
if (!(await isComposioConfigured())) return '';
|
||||
|
||||
const enabledTools = composioEnabledToolsRepo.getAll();
|
||||
const toolEntries = Object.values(enabledTools);
|
||||
if (toolEntries.length === 0) return '';
|
||||
const connectedToolkits = composioAccountsRepo.getConnectedToolkits();
|
||||
|
||||
// Group tools by toolkit
|
||||
const byToolkit: Record<string, Array<{ slug: string; name: string; description: string }>> = {};
|
||||
for (const tool of toolEntries) {
|
||||
if (!byToolkit[tool.toolkitSlug]) {
|
||||
byToolkit[tool.toolkitSlug] = [];
|
||||
}
|
||||
byToolkit[tool.toolkitSlug].push(tool);
|
||||
let prompt = `\n## Composio Integrations\n\n`;
|
||||
prompt += `You can connect to external services (Gmail, Slack, GitHub, Notion, etc.) via Composio.\n\n`;
|
||||
|
||||
if (connectedToolkits.length > 0) {
|
||||
const connectedNames = connectedToolkits
|
||||
.map(slug => CURATED_TOOLKITS.find(t => t.slug === slug)?.displayName ?? slug)
|
||||
.join(', ');
|
||||
prompt += `**Currently connected:** ${connectedNames}\n\n`;
|
||||
} else {
|
||||
prompt += `**No services connected yet.** Use \`composio-list-toolkits\` to show available integrations, or \`composio-connect-toolkit\` to help the user connect one.\n\n`;
|
||||
}
|
||||
|
||||
// Check which toolkits are connected
|
||||
const connectedToolkits = new Set(composioAccountsRepo.getConnectedToolkits());
|
||||
prompt += `**CRITICAL: NEVER say "I can't access [service]" or "I don't have access to [service]" without FIRST trying Composio.** If a user asks about ANY third-party service (LinkedIn, Gmail, GitHub, Slack, etc.), your FIRST action must be to check \`composio-list-toolkits\` or try \`composio-connect-toolkit\`. Never give up before trying.\n\n`;
|
||||
|
||||
let prompt = `\n## Composio Integration Tools\n\n`;
|
||||
prompt += `You have access to external service integrations via Composio. These tools are prefixed with \`composio-\` and connect to third-party services on the user's behalf. The user has enabled these tools in Settings > Tools Library.\n\n`;
|
||||
prompt += `**How to use Composio tools:**\n`;
|
||||
prompt += `- Each tool is named \`composio-{ACTION_SLUG}\` (e.g., \`composio-GMAIL_SEND_EMAIL\`)\n`;
|
||||
prompt += `- Call them like any other builtin tool — pass the required parameters and they execute via the connected account\n`;
|
||||
prompt += `- If a tool returns an error about the toolkit not being connected, inform the user they need to connect it in Settings > Tools Library\n`;
|
||||
prompt += `- Always confirm with the user before taking actions that send messages, create items, or modify data in external services\n\n`;
|
||||
prompt += `**Discovery & Execution Flow:**\n`;
|
||||
prompt += `1. When the user asks to interact with a service (e.g., "get my LinkedIn profile", "check my email", "list GitHub issues"):\n`;
|
||||
prompt += ` a. Check if the service is connected (via \`composio-list-toolkits\` or the connected list above)\n`;
|
||||
prompt += ` b. If NOT connected, call \`composio-connect-toolkit\` immediately — do NOT ask for confirmation or tell the user you can't do it\n`;
|
||||
prompt += ` c. If connected, proceed to search and execute\n`;
|
||||
prompt += `2. Use \`composio-search-tools\` with SHORT keyword queries (e.g., "list issues", "send email", "get profile") — avoid long sentences.\n`;
|
||||
prompt += `3. Read the \`inputSchema\` from search results carefully — note which fields are in \`required\`.\n`;
|
||||
prompt += `4. Call \`composio-execute-tool\` with the tool slug, toolkit slug, AND all required \`arguments\`. For tools with empty \`properties: {}\`, pass \`arguments: {}\`.\n`;
|
||||
|
||||
for (const [toolkitSlug, tools] of Object.entries(byToolkit)) {
|
||||
const isConnected = connectedToolkits.has(toolkitSlug);
|
||||
const statusBadge = isConnected ? '(Connected)' : '(Not Connected)';
|
||||
prompt += `### ${toolkitSlug.charAt(0).toUpperCase() + toolkitSlug.slice(1)} ${statusBadge}\n`;
|
||||
for (const tool of tools) {
|
||||
prompt += `- \`composio-${tool.slug}\` — ${tool.description}\n`;
|
||||
}
|
||||
prompt += `\n`;
|
||||
}
|
||||
prompt += `**Example — fetching GitHub issues for owner/repo:**\n`;
|
||||
prompt += `1. \`composio-search-tools({ query: "list issues", toolkitSlug: "github" })\` → finds \`GITHUB_ISSUES_LIST_FOR_REPO\`\n`;
|
||||
prompt += `2. Schema shows required: \`["owner", "repo"]\` — extract from user's request (e.g., "rowboatlabs/rowboat" → owner: "rowboatlabs", repo: "rowboat")\n`;
|
||||
prompt += `3. \`composio-execute-tool({ toolSlug: "GITHUB_ISSUES_LIST_FOR_REPO", toolkitSlug: "github", arguments: { owner: "rowboatlabs", repo: "rowboat", state: "open", per_page: 100 } })\`\n\n`;
|
||||
|
||||
prompt += `**Important:**\n`;
|
||||
prompt += `- Use short keyword search queries, NOT full sentences (good: "list issues", bad: "get all open issues for a GitHub repository")\n`;
|
||||
prompt += `- ALWAYS pass required arguments to composio-execute-tool — read the inputSchema from search results\n`;
|
||||
prompt += `- **If a tool call fails (e.g., missing fields), fix the arguments and retry IMMEDIATELY — do NOT stop and narrate the error to the user. Just fix it and continue.**\n`;
|
||||
prompt += `- **Multi-part requests:** When the user asks to "connect X and then do Y", complete BOTH parts in one turn. If part 1 (connect) is already done, proceed directly to part 2 (the actual task).\n`;
|
||||
prompt += `- Confirm with the user before executing tools that send messages, create items, or modify data (NOT for read-only queries or connecting)\n`;
|
||||
prompt += `- Connecting a toolkit is always safe — just do it when needed, don't ask permission\n`;
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
|
@ -71,7 +75,9 @@ You're an insightful, encouraging assistant who combines meticulous clarity with
|
|||
## What Rowboat Is
|
||||
Rowboat is an agentic assistant for everyday work - emails, meetings, projects, and people. Users give you tasks like "draft a follow-up email," "prep me for this meeting," or "summarize where we are with this project." You figure out what context you need, pull from emails and meetings, and get it done.
|
||||
|
||||
**Email Drafting:** When users ask you to draft emails or respond to emails, load the \`draft-emails\` skill first. It provides structured guidance for processing emails, gathering context from calendar and knowledge base, and creating well-informed draft responses.
|
||||
**Email Drafting:** When users ask you to **draft** or **compose** emails (e.g., "draft a follow-up to Monica", "write an email to John about the project"), load the \`draft-emails\` skill first. Do NOT load this skill for reading, fetching, or checking emails — use Composio tools for that instead.
|
||||
|
||||
**Live Email/Calendar/Service Queries:** When users ask to **read**, **fetch**, **check**, or **view** emails, calendar events, or any data from a connected service (e.g., "what's my latest email?", "check my inbox", "what meetings do I have today?"), use \`composio-search-tools\` and \`composio-execute-tool\` to query the connected service directly. Do NOT look in local \`gmail_sync/\` or \`calendar_sync/\` folders — use Composio for live data.
|
||||
|
||||
**Meeting Prep:** When users ask you to prepare for a meeting, prep for a call, or brief them on attendees, load the \`meeting-prep\` skill first. It provides structured guidance for gathering context about attendees from the knowledge base and creating useful meeting briefs.
|
||||
|
||||
|
|
@ -212,13 +218,23 @@ Always consult this catalog first so you load the right skills before taking act
|
|||
- Never start a response with a heading. Lead with a sentence or two of context first.
|
||||
- Avoid deeply nested bullets. If nesting beyond 2 levels, restructure.
|
||||
|
||||
## MCP Tool Discovery (CRITICAL)
|
||||
## Tool Priority: Composio First, Then MCP
|
||||
|
||||
**ALWAYS check for MCP tools BEFORE saying you can't do something.**
|
||||
**When the user wants to interact with a third-party service (GitHub, Gmail, Slack, Notion, Jira, etc.):**
|
||||
1. **FIRST** use the \`composio-*\` builtin tools — they are already authenticated and ready. Do NOT load the mcp-integration skill or draft-emails skill for service queries.
|
||||
2. **ONLY** if the service is NOT available through Composio, fall back to MCP tools.
|
||||
|
||||
When a user asks for ANY task that might require external capabilities (web search, internet access, APIs, data fetching, etc.), check MCP tools first using \`listMcpServers\` and \`listMcpTools\`. Load the "mcp-integration" skill for detailed guidance on discovering and executing MCP tools.
|
||||
**Common Composio tasks (use composio-search-tools + composio-execute-tool):**
|
||||
- "What's my latest email?" → search "fetch emails" in gmail toolkit
|
||||
- "Check my inbox" → search "fetch emails" in gmail toolkit
|
||||
- "What meetings do I have?" → search "list events" in googlecalendar toolkit
|
||||
- "Create a GitHub issue" → search "create issue" in github toolkit
|
||||
- "Send a Slack message" → search "send message" in slack toolkit
|
||||
|
||||
**DO NOT** immediately respond with "I can't access the internet" or "I don't have that capability" without checking MCP tools first!
|
||||
**When the user wants capabilities that Composio does NOT cover** (web search, file scraping, audio generation, etc.):
|
||||
- Check MCP tools using \`listMcpServers\` and \`listMcpTools\`. Load the "mcp-integration" skill for guidance.
|
||||
|
||||
**DO NOT** immediately respond with "I can't access the internet" or "I don't have that capability" without checking both Composio and MCP tools first!
|
||||
|
||||
## Execution Reminders
|
||||
- Explore existing files and structure before creating new assets.
|
||||
|
|
@ -258,7 +274,10 @@ ${runtimeContextPrompt}
|
|||
- \`web-search\` - Search the web. Returns rich results with full text, highlights, and metadata. The \`category\` parameter defaults to \`general\` (full web search) — only use a specific category like \`news\`, \`company\`, \`research paper\` etc. when the query is clearly about that type. For everyday queries (weather, restaurants, prices, how-to), use \`general\`.
|
||||
- \`app-navigation\` - Control the app UI: open notes, switch views, filter/search the knowledge base, manage saved views. **Load the \`app-navigation\` skill before using this tool.**
|
||||
- \`save-to-memory\` - Save observations about the user to the agent memory system. Use this proactively during conversations.
|
||||
- **Composio tools** (\`composio-*\`) — External service integrations enabled by the user in Settings > Tools Library. These connect to third-party apps like Gmail, GitHub, Linear, Notion, etc. See the "Composio Integration Tools" section below for available tools.
|
||||
- \`composio-list-toolkits\` — List available integrations (Gmail, Slack, GitHub, etc.) and their connection status
|
||||
- \`composio-search-tools\` — Search for tools by use case (e.g., "send email", "create issue"); returns tool slugs and input schemas
|
||||
- \`composio-execute-tool\` — Execute a Composio tool by slug with parameters from search results
|
||||
- \`composio-connect-toolkit\` — Connect a service (Gmail, Slack, GitHub, etc.) via OAuth directly from chat
|
||||
|
||||
**Prefer these tools whenever possible** — they work instantly with zero friction. For file operations inside \`~/.rowboat/\`, always use these instead of \`executeCommand\`.
|
||||
|
||||
|
|
@ -306,8 +325,7 @@ let cachedInstructions: string | null = null;
|
|||
|
||||
/**
|
||||
* Invalidate the cached instructions so the next buildCopilotInstructions() call
|
||||
* regenerates the Composio tools section. Call this after enabling/disabling tools
|
||||
* or connecting/disconnecting a toolkit.
|
||||
* regenerates the Composio section. Call this after connecting/disconnecting a toolkit.
|
||||
*/
|
||||
export function invalidateCopilotInstructionsCache(): void {
|
||||
cachedInstructions = null;
|
||||
|
|
|
|||
|
|
@ -3,9 +3,13 @@ export const skill = String.raw`
|
|||
|
||||
**Load this skill proactively** when a user asks for ANY task that might require external capabilities (web search, internet access, APIs, data fetching, time/date, etc.). This skill provides complete guidance on discovering and executing MCP tools.
|
||||
|
||||
## CRITICAL: Always Check MCP Tools First
|
||||
## CRITICAL: Composio Tools Take Priority Over MCP
|
||||
|
||||
**IMPORTANT**: When a user asks for ANY task that might require external capabilities (web search, API calls, data fetching, etc.), ALWAYS:
|
||||
**If a Composio toolkit is connected for the service the user wants (GitHub, Gmail, Slack, etc.), use the \`composio-search-tools\` and \`composio-execute-tool\` builtin tools — NOT MCP tools.** Composio integrations are already authenticated and ready to use. Only fall back to MCP tools if the service is NOT available through Composio.
|
||||
|
||||
## When to Check MCP Tools
|
||||
|
||||
**IMPORTANT**: When a user asks for a task that requires external capabilities AND no Composio toolkit covers it, check MCP tools:
|
||||
|
||||
1. **First check**: Call \`listMcpServers\` to see what's available
|
||||
2. **Then list tools**: Call \`listMcpTools\` on relevant servers
|
||||
|
|
@ -23,9 +27,7 @@ export const skill = String.raw`
|
|||
| "Read/write files" | filesystem | \`read_file\`, \`write_file\` |
|
||||
| "Get current time/date" | time | \`get_current_time\` |
|
||||
| "Make HTTP request" | fetch | \`fetch\`, \`post\` |
|
||||
| "GitHub operations" | github | \`create_issue\`, \`search_repos\` |
|
||||
| "Generate audio/speech" | elevenLabs | \`text_to_speech\` |
|
||||
| "Tweet/social media" | twitter, composio | Various social tools |
|
||||
|
||||
## Key concepts
|
||||
- MCP servers expose tools (web scraping, APIs, databases, etc.) declared in \`config/mcp.json\`.
|
||||
|
|
|
|||
|
|
@ -13,11 +13,11 @@ import * as workspace from "../../workspace/workspace.js";
|
|||
import { IAgentsRepo } from "../../agents/repo.js";
|
||||
import { WorkDir } from "../../config/config.js";
|
||||
import { composioAccountsRepo } from "../../composio/repo.js";
|
||||
import { composioEnabledToolsRepo } from "../../composio/enabled-tools-repo.js";
|
||||
import { executeAction as executeComposioAction, isConfigured as isComposioConfigured } from "../../composio/client.js";
|
||||
import { invalidateCopilotInstructionsCache } from "../assistant/instructions.js";
|
||||
import { executeAction as executeComposioAction, isConfigured as isComposioConfigured, searchTools as searchComposioTools } from "../../composio/client.js";
|
||||
import { CURATED_TOOLKITS, CURATED_TOOLKIT_SLUGS } from "../../composio/curated-toolkits.js";
|
||||
import { getConnectionInitiator } from "../../composio/connection-bridge.js";
|
||||
import type { ToolContext } from "./exec-tool.js";
|
||||
import { generateText, jsonSchema } from "ai";
|
||||
import { generateText } from "ai";
|
||||
import { createProvider } from "../../models/models.js";
|
||||
import { IModelConfigRepo } from "../../models/repo.js";
|
||||
import { isSignedIn } from "../../account/account.js";
|
||||
|
|
@ -1177,93 +1177,174 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
|
|||
};
|
||||
},
|
||||
},
|
||||
|
||||
// ========================================================================
|
||||
// Composio Meta-Tools
|
||||
// ========================================================================
|
||||
|
||||
'composio-list-toolkits': {
|
||||
description: 'List available Composio integrations (Gmail, Slack, GitHub, etc.) and their connection status. Use this to show the user what services they can connect to.',
|
||||
inputSchema: z.object({
|
||||
category: z.enum(['all', 'communication', 'productivity', 'development', 'crm', 'social', 'storage', 'support']).optional()
|
||||
.describe('Filter by category. Defaults to "all".'),
|
||||
}),
|
||||
execute: async ({ category }: { category?: string }) => {
|
||||
const toolkits = CURATED_TOOLKITS
|
||||
.filter(t => !category || category === 'all' || t.category === category)
|
||||
.map(t => ({
|
||||
slug: t.slug,
|
||||
name: t.displayName,
|
||||
category: t.category,
|
||||
isConnected: composioAccountsRepo.isConnected(t.slug),
|
||||
}));
|
||||
|
||||
const connectedCount = toolkits.filter(t => t.isConnected).length;
|
||||
return {
|
||||
toolkits,
|
||||
connectedCount,
|
||||
totalCount: toolkits.length,
|
||||
};
|
||||
},
|
||||
isAvailable: async () => isComposioConfigured(),
|
||||
},
|
||||
|
||||
'composio-search-tools': {
|
||||
description: 'Search for Composio tools by use case across connected services. Returns tool slugs, descriptions, and input schemas so you can call composio-execute-tool with the right parameters. Example: search "send email" to find Gmail tools, "create issue" to find GitHub/Jira tools.',
|
||||
inputSchema: z.object({
|
||||
query: z.string().describe('Natural language description of what you want to do (e.g., "send an email", "create a GitHub issue", "schedule a meeting")'),
|
||||
toolkitSlug: z.string().optional().describe('Optional: limit search to a specific toolkit (e.g., "gmail", "github")'),
|
||||
}),
|
||||
execute: async ({ query, toolkitSlug }: { query: string; toolkitSlug?: string }) => {
|
||||
try {
|
||||
const toolkitFilter = toolkitSlug ? [toolkitSlug] : undefined;
|
||||
const result = await searchComposioTools(query, toolkitFilter);
|
||||
|
||||
// Filter to curated toolkits only (skip if a specific toolkit was requested —
|
||||
// the API already filtered server-side)
|
||||
const filtered = toolkitSlug
|
||||
? result.items
|
||||
: result.items.filter(t => CURATED_TOOLKIT_SLUGS.has(t.toolkitSlug));
|
||||
|
||||
// Annotate with connection status
|
||||
const tools = filtered.map(t => ({
|
||||
slug: t.slug,
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
toolkitSlug: t.toolkitSlug,
|
||||
isConnected: composioAccountsRepo.isConnected(t.toolkitSlug),
|
||||
inputSchema: t.inputParameters,
|
||||
}));
|
||||
|
||||
return {
|
||||
tools,
|
||||
resultCount: tools.length,
|
||||
hint: tools.some(t => !t.isConnected)
|
||||
? 'Some tools require connecting the toolkit first. Use composio-connect-toolkit to help the user authenticate.'
|
||||
: undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return { tools: [], resultCount: 0, error: message };
|
||||
}
|
||||
},
|
||||
isAvailable: async () => isComposioConfigured(),
|
||||
},
|
||||
|
||||
'composio-execute-tool': {
|
||||
description: 'Execute a Composio tool by its slug. You MUST pass the arguments field with all required parameters from the search results inputSchema. Example: composio-execute-tool({ toolSlug: "GITHUB_ISSUES_LIST_FOR_REPO", toolkitSlug: "github", arguments: { owner: "rowboatlabs", repo: "rowboat", state: "open", per_page: 100 } })',
|
||||
inputSchema: z.object({
|
||||
toolSlug: z.string().describe('EXACT tool slug from search results (e.g., "GITHUB_ISSUES_LIST_FOR_REPO"). Copy it exactly — do not modify it.'),
|
||||
toolkitSlug: z.string().describe('The toolkit slug (e.g., "gmail", "github")'),
|
||||
arguments: z.record(z.string(), z.unknown()).describe('REQUIRED: Tool input parameters as key-value pairs. Get the required fields from the inputSchema returned by composio-search-tools. Never omit this.'),
|
||||
}),
|
||||
execute: async ({ toolSlug, toolkitSlug, arguments: args }: { toolSlug: string; toolkitSlug: string; arguments?: Record<string, unknown> }) => {
|
||||
// Default arguments to {} if the LLM omits the field entirely
|
||||
const toolArgs = args ?? {};
|
||||
|
||||
// Check connection
|
||||
const account = composioAccountsRepo.getAccount(toolkitSlug);
|
||||
if (!account || account.status !== 'ACTIVE') {
|
||||
return {
|
||||
successful: false,
|
||||
data: null,
|
||||
error: `Toolkit "${toolkitSlug}" is not connected. Use composio-connect-toolkit to help the user connect it first.`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return await executeComposioAction(toolSlug, {
|
||||
connected_account_id: account.id,
|
||||
user_id: 'rowboat-user',
|
||||
version: 'latest',
|
||||
arguments: toolArgs,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(`[Composio] Tool execution failed for ${toolSlug}:`, message);
|
||||
return {
|
||||
successful: false,
|
||||
data: null,
|
||||
error: `Failed to execute ${toolSlug}: ${message}. If fields are missing, check the inputSchema and retry with the correct arguments.`,
|
||||
};
|
||||
}
|
||||
},
|
||||
isAvailable: async () => isComposioConfigured(),
|
||||
},
|
||||
|
||||
'composio-connect-toolkit': {
|
||||
description: 'Connect a Composio service (Gmail, Slack, GitHub, etc.) via OAuth. Opens the user\'s browser for authentication. After authenticating, the user can use tools from that service.',
|
||||
inputSchema: z.object({
|
||||
toolkitSlug: z.string().describe('The toolkit slug to connect (e.g., "gmail", "github", "slack", "notion")'),
|
||||
}),
|
||||
execute: async ({ toolkitSlug }: { toolkitSlug: string }) => {
|
||||
// Validate against curated list
|
||||
if (!CURATED_TOOLKIT_SLUGS.has(toolkitSlug)) {
|
||||
const available = CURATED_TOOLKITS.map(t => `${t.slug} (${t.displayName})`).join(', ');
|
||||
return {
|
||||
success: false,
|
||||
error: `Unknown toolkit "${toolkitSlug}". Available toolkits: ${available}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Check if already connected
|
||||
if (composioAccountsRepo.isConnected(toolkitSlug)) {
|
||||
return {
|
||||
success: true,
|
||||
message: `${toolkitSlug} is already connected. You can search for and execute its tools.`,
|
||||
alreadyConnected: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Use the connection bridge to trigger OAuth
|
||||
const initiator = getConnectionInitiator();
|
||||
if (!initiator) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Connection system not available. Please try connecting via Settings > Tools Library instead.',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await initiator(toolkitSlug);
|
||||
if (result.success) {
|
||||
const toolkit = CURATED_TOOLKITS.find(t => t.slug === toolkitSlug);
|
||||
return {
|
||||
success: true,
|
||||
message: `Opening browser to authenticate with ${toolkit?.displayName ?? toolkitSlug}. Please complete the authentication in your browser, then let me know when you're done.`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: result.error || 'Failed to initiate connection',
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
success: false,
|
||||
error: `Connection failed: ${message}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
isAvailable: async () => isComposioConfigured(),
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Dynamic Composio Tool Registration
|
||||
// ============================================================================
|
||||
|
||||
const COMPOSIO_TOOL_PREFIX = 'composio-';
|
||||
|
||||
/**
|
||||
* Unregister all dynamically registered Composio tools
|
||||
*/
|
||||
function unregisterComposioTools(): void {
|
||||
for (const key of Object.keys(BuiltinTools)) {
|
||||
if (key.startsWith(COMPOSIO_TOOL_PREFIX)) {
|
||||
delete BuiltinTools[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register enabled Composio tools as builtin tools.
|
||||
* Each enabled tool gets a generic execute function that routes
|
||||
* to the Composio API via the connected account.
|
||||
*/
|
||||
function registerComposioTools(): void {
|
||||
const enabledTools = composioEnabledToolsRepo.getAll();
|
||||
|
||||
for (const [slug, tool] of Object.entries(enabledTools)) {
|
||||
const toolKey = `${COMPOSIO_TOOL_PREFIX}${slug}`;
|
||||
const toolkitSlug = tool.toolkitSlug;
|
||||
|
||||
const inputParams = tool.inputParameters ?? { type: 'object', properties: {} };
|
||||
|
||||
BuiltinTools[toolKey] = {
|
||||
description: `[${tool.toolkitSlug}] ${tool.description}`,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
inputSchema: jsonSchema({
|
||||
type: 'object',
|
||||
properties: (inputParams.properties ?? {}) as any,
|
||||
...(inputParams.required ? { required: inputParams.required } : {}),
|
||||
} as any) as unknown as ZodType,
|
||||
execute: async (input: Record<string, unknown>) => {
|
||||
const account = composioAccountsRepo.getAccount(toolkitSlug);
|
||||
if (!account || account.status !== 'ACTIVE') {
|
||||
return {
|
||||
success: false,
|
||||
error: `Toolkit "${toolkitSlug}" is not connected. Please connect it in Settings > Tools Library.`,
|
||||
};
|
||||
}
|
||||
try {
|
||||
return await executeComposioAction(slug, {
|
||||
connected_account_id: account.id,
|
||||
user_id: 'rowboat-user',
|
||||
version: 'latest',
|
||||
arguments: input,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(`[Composio] Tool execution failed for ${slug}:`, message);
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to execute ${slug}: ${message}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
isAvailable: async () => {
|
||||
return (await isComposioConfigured()) && composioAccountsRepo.isConnected(toolkitSlug);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const count = Object.keys(enabledTools).length;
|
||||
if (count > 0) {
|
||||
console.log(`[Composio] Registered ${count} dynamic tool(s)`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh dynamic Composio tools by unregistering all and re-registering from the repo.
|
||||
* Called after enabling/disabling tools or disconnecting a toolkit.
|
||||
* Also invalidates the cached agent instructions so they reflect the new tool set.
|
||||
*/
|
||||
export function refreshComposioTools(): void {
|
||||
unregisterComposioTools();
|
||||
registerComposioTools();
|
||||
invalidateCopilotInstructionsCache();
|
||||
}
|
||||
|
||||
// Register on module load
|
||||
refreshComposioTools();
|
||||
|
|
|
|||
|
|
@ -291,6 +291,79 @@ export async function deleteConnectedAccount(connectedAccountId: string): Promis
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Schema for search results: includes toolkit info and full input_parameters.
|
||||
*/
|
||||
const ZSearchResultTool = z.object({
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
toolkit: z.object({
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
logo: z.string(),
|
||||
}).optional(),
|
||||
input_parameters: z.object({
|
||||
type: z.literal('object').optional().default('object'),
|
||||
properties: z.record(z.string(), z.unknown()).optional().default({}),
|
||||
required: z.array(z.string()).optional(),
|
||||
}).optional().default({ type: 'object', properties: {} }),
|
||||
}).passthrough();
|
||||
|
||||
/**
|
||||
* Infer toolkit slug from a tool slug.
|
||||
* Tool naming convention: TOOLKIT_ACTION (e.g., GITHUB_CREATE_ISSUE → github).
|
||||
* For multi-word toolkit slugs (GOOGLECALENDAR_CREATE_EVENT), the prefix before
|
||||
* the first underscore matches the toolkit slug.
|
||||
*/
|
||||
function inferToolkitSlug(toolSlug: string): string {
|
||||
// Tool slugs are uppercase: GITHUB_CREATE_ISSUE, GMAIL_SEND_EMAIL, etc.
|
||||
// The toolkit prefix is everything before the first action word.
|
||||
// Strategy: lowercase the slug and try progressively shorter prefixes.
|
||||
const lower = toolSlug.toLowerCase();
|
||||
const parts = lower.split('_');
|
||||
// Try joining from 1 part up to all-but-1 to find a known prefix
|
||||
// Most common: single-word prefix (github, gmail, slack)
|
||||
return parts[0] ?? lower;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for tools across all toolkits (or optionally filtered by specific toolkit slugs).
|
||||
* Returns tools with full input_parameters so the agent knows what params to pass.
|
||||
*/
|
||||
export async function searchTools(
|
||||
searchQuery: string,
|
||||
toolkitSlugs?: string[],
|
||||
): Promise<{ items: Array<{ slug: string; name: string; description: string; toolkitSlug: string; inputParameters: { type: 'object'; properties: Record<string, unknown>; required?: string[] } }> }> {
|
||||
const params: Record<string, string> = {
|
||||
search: searchQuery,
|
||||
limit: '15',
|
||||
};
|
||||
if (toolkitSlugs && toolkitSlugs.length === 1) {
|
||||
params.toolkit_slug = toolkitSlugs[0];
|
||||
}
|
||||
|
||||
const result = await composioApiCall(ZListResponse(ZSearchResultTool), "/tools", params);
|
||||
|
||||
const items = result.items.map((item) => ({
|
||||
slug: item.slug,
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
// Use toolkit.slug from API response, fall back to inferring from tool slug,
|
||||
// and finally fall back to the requested toolkit slug if one was provided.
|
||||
toolkitSlug: item.toolkit?.slug
|
||||
|| inferToolkitSlug(item.slug)
|
||||
|| (toolkitSlugs?.length === 1 ? toolkitSlugs[0] : ''),
|
||||
inputParameters: {
|
||||
type: 'object' as const,
|
||||
properties: item.input_parameters?.properties ?? {},
|
||||
required: item.input_parameters?.required,
|
||||
},
|
||||
}));
|
||||
|
||||
return { items };
|
||||
}
|
||||
|
||||
/**
|
||||
* List available tools for a toolkit
|
||||
*/
|
||||
|
|
@ -308,54 +381,6 @@ export async function listToolkitTools(
|
|||
return composioApiCall(ZListResponse(ZTool), "/tools", params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schema for the detailed tools response (preserves input_parameters).
|
||||
* Uses passthrough so extra API fields don't cause validation failures.
|
||||
*/
|
||||
const ZDetailedTool = z.object({
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
input_parameters: z.object({
|
||||
type: z.literal('object').optional().default('object'),
|
||||
properties: z.record(z.string(), z.unknown()).optional().default({}),
|
||||
required: z.array(z.string()).optional(),
|
||||
}).optional().default({ type: 'object', properties: {} }),
|
||||
}).passthrough();
|
||||
|
||||
/**
|
||||
* List available tools for a toolkit with full details including input_parameters.
|
||||
* Uses composioApiCall for consistent error handling, logging, and validation.
|
||||
*/
|
||||
export async function listToolkitToolsDetailed(
|
||||
toolkitSlug: string,
|
||||
searchQuery: string | null = null,
|
||||
): Promise<{ items: Array<{ slug: string; name: string; description: string; toolkitSlug: string; inputParameters: { type: 'object'; properties: Record<string, unknown>; required?: string[] } }> }> {
|
||||
const params: Record<string, string> = {
|
||||
toolkit_slug: toolkitSlug,
|
||||
limit: '200',
|
||||
};
|
||||
if (searchQuery) {
|
||||
params.search = searchQuery;
|
||||
}
|
||||
|
||||
const result = await composioApiCall(ZListResponse(ZDetailedTool), "/tools", params);
|
||||
|
||||
return {
|
||||
items: result.items.map((item) => ({
|
||||
slug: item.slug,
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
toolkitSlug,
|
||||
inputParameters: {
|
||||
type: 'object' as const,
|
||||
properties: item.input_parameters?.properties ?? {},
|
||||
required: item.input_parameters?.required,
|
||||
},
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a tool action
|
||||
*/
|
||||
|
|
|
|||
33
apps/x/packages/core/src/composio/connection-bridge.ts
Normal file
33
apps/x/packages/core/src/composio/connection-bridge.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* Connection bridge for Composio toolkit OAuth.
|
||||
*
|
||||
* Builtin tools run in the core package which cannot import Electron-specific
|
||||
* code from the main process. This module provides a callback registry so the
|
||||
* main process can register its `initiateConnection` function at startup, and
|
||||
* builtin tools can call it at runtime.
|
||||
*/
|
||||
|
||||
type ConnectionInitiator = (toolkitSlug: string) => Promise<{
|
||||
success: boolean;
|
||||
redirectUrl?: string;
|
||||
connectedAccountId?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
|
||||
let connectionInitiator: ConnectionInitiator | null = null;
|
||||
|
||||
/**
|
||||
* Register the connection initiator callback.
|
||||
* Called once by the main process at startup.
|
||||
*/
|
||||
export function setConnectionInitiator(fn: ConnectionInitiator): void {
|
||||
connectionInitiator = fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the registered connection initiator.
|
||||
* Returns null if not yet registered (app not fully initialized).
|
||||
*/
|
||||
export function getConnectionInitiator(): ConnectionInitiator | null {
|
||||
return connectionInitiator;
|
||||
}
|
||||
74
apps/x/packages/core/src/composio/curated-toolkits.ts
Normal file
74
apps/x/packages/core/src/composio/curated-toolkits.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
/**
|
||||
* Curated list of Composio toolkits available to Rowboat users.
|
||||
* Only these toolkits are shown in the UI and discoverable via chat.
|
||||
* Exact slugs match Composio API naming convention.
|
||||
*
|
||||
* Display names come from @x/shared/composio (single source of truth).
|
||||
*/
|
||||
|
||||
import { COMPOSIO_DISPLAY_NAMES } from "@x/shared/dist/composio.js";
|
||||
|
||||
export { COMPOSIO_DISPLAY_NAMES } from "@x/shared/dist/composio.js";
|
||||
|
||||
export type ToolkitCategory = 'communication' | 'productivity' | 'development' | 'crm' | 'social' | 'storage' | 'support';
|
||||
|
||||
export interface CuratedToolkit {
|
||||
slug: string;
|
||||
displayName: string;
|
||||
category: ToolkitCategory;
|
||||
}
|
||||
|
||||
const toolkit = (slug: string, category: ToolkitCategory): CuratedToolkit => ({
|
||||
slug,
|
||||
displayName: COMPOSIO_DISPLAY_NAMES[slug] ?? slug,
|
||||
category,
|
||||
});
|
||||
|
||||
export const CURATED_TOOLKITS: CuratedToolkit[] = [
|
||||
// Communication
|
||||
toolkit('gmail', 'communication'),
|
||||
toolkit('slack', 'communication'),
|
||||
toolkit('microsoft_outlook', 'communication'),
|
||||
toolkit('microsoft_teams', 'communication'),
|
||||
|
||||
// Productivity
|
||||
toolkit('googlecalendar', 'productivity'),
|
||||
toolkit('googledocs', 'productivity'),
|
||||
toolkit('googlesheets', 'productivity'),
|
||||
toolkit('notion', 'productivity'),
|
||||
toolkit('airtable', 'productivity'),
|
||||
toolkit('calendly', 'productivity'),
|
||||
toolkit('cal', 'productivity'),
|
||||
|
||||
// Storage
|
||||
toolkit('googledrive', 'storage'),
|
||||
toolkit('dropbox', 'storage'),
|
||||
toolkit('onedrive', 'storage'),
|
||||
|
||||
// Development
|
||||
toolkit('github', 'development'),
|
||||
toolkit('linear', 'development'),
|
||||
toolkit('jira', 'development'),
|
||||
|
||||
// Project Management
|
||||
toolkit('asana', 'productivity'),
|
||||
toolkit('trello', 'productivity'),
|
||||
|
||||
// CRM & Sales
|
||||
toolkit('hubspot', 'crm'),
|
||||
toolkit('salesforce', 'crm'),
|
||||
|
||||
// Social
|
||||
toolkit('linkedin', 'social'),
|
||||
toolkit('twitter', 'social'),
|
||||
toolkit('reddit', 'social'),
|
||||
|
||||
// Support
|
||||
toolkit('intercom', 'support'),
|
||||
toolkit('zendesk', 'support'),
|
||||
];
|
||||
|
||||
/**
|
||||
* Set of curated toolkit slugs for fast lookup.
|
||||
*/
|
||||
export const CURATED_TOOLKIT_SLUGS = new Set(CURATED_TOOLKITS.map(t => t.slug));
|
||||
Loading…
Add table
Add a link
Reference in a new issue