mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-27 17:36:25 +02:00
Feature/composio tools library (#461)
* first version of composio * Enhance error handling in Composio tool execution. Added try-catch block to log errors and return a structured error response when executing tools fails. * Add tool search functionality to settings dialog Implemented a debounced search feature for tools within the toolkit, allowing users to filter tools based on a search query. Added state management for search results and loading status. Updated the UI to accommodate the search input and results display. Additionally, modified the API call to use 'query' instead of 'search' for consistency. * Enhance Composio OAuth flow management and improve tool handling - Updated the activeFlows management to prevent concurrent OAuth flows for the same toolkit by using toolkitSlug as the key. - Implemented cleanup logic for existing flows, ensuring proper resource management by aborting and closing servers as needed. - Introduced a timeout mechanism for abandoned flows, enhancing reliability. - Refactored the Composio tools repository to use an in-memory cache for improved performance and added methods for persisting changes to disk. - Updated the detailed tools listing to use a consistent API call structure and improved input parameter handling. - Made connectionData in the response optional for better flexibility in handling connected accounts. * Improve error handling in Composio API calls - Enhanced error reporting by extracting human-readable messages from the JSON response body when the API call fails. - Added logic to parse the response and include specific error details in the thrown error message, improving debugging and user feedback. * 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. * Refactor listToolkits function to remove cursor parameter and implement pagination - Updated listToolkits in composio-handler.ts to paginate through API results, collecting all curated toolkits. - Adjusted IPC handler in ipc.ts to call the modified listToolkits without cursor argument. - Made properties in ToolkitInfo optional in settings-dialog.tsx for improved flexibility. - Removed the unused enabled-tools-repo.ts file to clean up the codebase. * Refactor Composio toolkit management for improved structure and maintainability - Consolidated toolkit definitions and display names into a single source of truth in shared/composio.ts. - Updated core composio/curated-toolkits.ts to re-export types and constants for backward compatibility. - Enhanced the organization of toolkit data, ensuring clarity and ease of access for future development. * Refactor Composio integration and improve component structure - Updated imports in composio-handler.ts and various components to utilize shared/composio.js for consistency. - Simplified ComposioConnectCard by removing unnecessary state management and improving event handling. - Enhanced chat-conversation.ts to directly reference COMPOSIO_DISPLAY_NAMES from shared/composio.js. - Cleaned up unused functions and types in client.ts and types.ts for better maintainability. - Removed deprecated curated-toolkits.ts file to streamline the codebase. * Refactor Composio connection handling and improve tool display logic - Removed the connection bridge for Composio toolkit OAuth, simplifying the connection process. - Updated ComposioConnectCard to display a more user-friendly connection message. - Introduced a new utility function, getToolDisplayName, to provide human-friendly names for builtin tools. - Refactored App and ChatSidebar components to utilize the new getToolDisplayName function for improved clarity in tool titles. - Cleaned up imports and removed unused code for better maintainability. * remove from diff * Address PR review: consolidate types, refactor CopilotAgent, sort toolkits - Move ZSearchResultTool and ZNormalizedToolResult into composio/types.ts - Convert CopilotAgent from static const to async buildCopilotAgent() - Simplify loadAgent to delegate to buildCopilotAgent() - Sort CURATED_TOOLKITS alphabetically by slug - Remove inline type annotations in composio-handler, use inferred types - Bump search limit from 15→50 for unscoped queries - Add docstrings explaining inferToolkitSlug fallback behavior - Add IPC schema reference comment for composio channels Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Enhance Composio connection handling and improve rendering logic - Added a 'hidden' property to ComposioConnectCardData to prevent rendering of duplicate connection cards. - Updated App and ChatSidebar components to skip rendering if the card is marked as hidden. - Refactored ComposioConnectCard to utilize a ref for callback firing, ensuring onConnected is only called once. - Improved instructions for Composio integration to clarify usage and loading of the composio-integration skill. This update streamlines the user experience by avoiding duplicate connection prompts and enhances the overall clarity of integration instructions. * Address PR round 2: use query param, remove inferToolkitSlug, consolidate types - Rename deprecated `search` param to `query` per Composio docs - Remove inferToolkitSlug fallback; make toolkit required in ZSearchResultTool - Replace inline Awaited<ReturnType<...>> with concrete Toolkit type in handler - Move ZToolkitMeta/ZToolkitItem/ZListToolkitsResponse to shared/composio.ts - Reference shared schemas in ipc.ts and core/types.ts (single source of truth) - Remove unused ZTool import from client.ts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add Toolkit type inference to composio/types.ts - Introduced a new type `Toolkit` inferred from `ZToolkit` to enhance type safety and clarity in type definitions. - This addition supports better integration and usage of the toolkit within the Composio framework. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bc929b6c1b
commit
e0aaa9a27e
19 changed files with 1324 additions and 214 deletions
|
|
@ -10,7 +10,7 @@ import { LlmStepStreamEvent } from "@x/shared/dist/llm-step-events.js";
|
|||
import { execTool } from "../application/lib/exec-tool.js";
|
||||
import { AskHumanRequestEvent, RunEvent, ToolPermissionRequestEvent } from "@x/shared/dist/runs.js";
|
||||
import { BuiltinTools } from "../application/lib/builtin-tools.js";
|
||||
import { CopilotAgent } from "../application/assistant/agent.js";
|
||||
import { buildCopilotAgent } from "../application/assistant/agent.js";
|
||||
import { isBlocked, extractCommandNames } from "../application/lib/command-executor.js";
|
||||
import container from "../di/container.js";
|
||||
import { IModelConfigRepo } from "../models/repo.js";
|
||||
|
|
@ -369,7 +369,7 @@ function formatLlmStreamError(rawError: unknown): string {
|
|||
|
||||
export async function loadAgent(id: string): Promise<z.infer<typeof Agent>> {
|
||||
if (id === "copilot" || id === "rowboatx") {
|
||||
return CopilotAgent;
|
||||
return buildCopilotAgent();
|
||||
}
|
||||
|
||||
if (id === 'note_creation') {
|
||||
|
|
|
|||
|
|
@ -1,19 +1,23 @@
|
|||
import { Agent, ToolAttachment } from "@x/shared/dist/agent.js";
|
||||
import z from "zod";
|
||||
import { CopilotInstructions } from "./instructions.js";
|
||||
import { buildCopilotInstructions } from "./instructions.js";
|
||||
import { BuiltinTools } from "../lib/builtin-tools.js";
|
||||
|
||||
const tools: Record<string, z.infer<typeof ToolAttachment>> = {};
|
||||
for (const name of Object.keys(BuiltinTools)) {
|
||||
tools[name] = {
|
||||
type: "builtin",
|
||||
name,
|
||||
/**
|
||||
* Build the CopilotAgent dynamically.
|
||||
* Tools are derived from the current BuiltinTools (which include Composio meta-tools),
|
||||
* and instructions include the live Composio connection status.
|
||||
*/
|
||||
export async function buildCopilotAgent(): Promise<z.infer<typeof Agent>> {
|
||||
const tools: Record<string, z.infer<typeof ToolAttachment>> = {};
|
||||
for (const name of Object.keys(BuiltinTools)) {
|
||||
tools[name] = { type: "builtin", name };
|
||||
}
|
||||
const instructions = await buildCopilotInstructions();
|
||||
return {
|
||||
name: "rowboatx",
|
||||
description: "Rowboatx copilot",
|
||||
instructions,
|
||||
tools,
|
||||
};
|
||||
}
|
||||
|
||||
export const CopilotAgent: z.infer<typeof Agent> = {
|
||||
name: "rowboatx",
|
||||
description: "Rowboatx copilot",
|
||||
instructions: CopilotInstructions,
|
||||
tools,
|
||||
}
|
||||
|
|
@ -1,9 +1,32 @@
|
|||
import { skillCatalog } from "./skills/index.js";
|
||||
import { WorkDir as BASE_DIR } from "../../config/config.js";
|
||||
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 { composioAccountsRepo } from "../../composio/repo.js";
|
||||
import { isConfigured as isComposioConfigured } from "../../composio/client.js";
|
||||
import { CURATED_TOOLKITS } from "@x/shared/dist/composio.js";
|
||||
|
||||
const runtimeContextPrompt = getRuntimeContextPrompt(getRuntimeContext());
|
||||
|
||||
/**
|
||||
* 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 connectedToolkits = composioAccountsRepo.getConnectedToolkits();
|
||||
const connectedSection = connectedToolkits.length > 0
|
||||
? `**Currently connected:** ${connectedToolkits.map(slug => CURATED_TOOLKITS.find(t => t.slug === slug)?.displayName ?? slug).join(', ')}`
|
||||
: `**No services connected yet.** Load the \`composio-integration\` skill to help the user connect one.`;
|
||||
|
||||
return `
|
||||
## Composio Integrations
|
||||
|
||||
${connectedSection}
|
||||
|
||||
Load the \`composio-integration\` skill when the user asks to interact with any third-party service. NEVER say "I can't access [service]" without loading the skill and trying Composio first.
|
||||
`;
|
||||
}
|
||||
|
||||
export const CopilotInstructions = `You are Rowboat Copilot - an AI assistant for everyday work. You help users with anything they want. For instance, drafting emails, prepping for meetings, tracking projects, or answering questions - with memory that compounds from their emails, calendar, and notes. Everything runs locally on the user's machine. The nerdy coworker who remembers everything.
|
||||
|
||||
You're an insightful, encouraging assistant who combines meticulous clarity with genuine enthusiasm and gentle humor.
|
||||
|
|
@ -25,7 +48,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 the \`composio-integration\` skill for that instead.
|
||||
|
||||
**Third-Party Services:** When users ask to interact with any external service (Gmail, GitHub, Slack, LinkedIn, Notion, Google Sheets, Jira, etc.) — reading emails, listing issues, sending messages, fetching profiles — load the \`composio-integration\` skill first. Do NOT look in local \`gmail_sync/\` or \`calendar_sync/\` folders 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.
|
||||
|
||||
|
|
@ -166,13 +191,9 @@ 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
|
||||
|
||||
**ALWAYS check for MCP tools BEFORE saying you can't do something.**
|
||||
|
||||
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.
|
||||
|
||||
**DO NOT** immediately respond with "I can't access the internet" or "I don't have that capability" without checking MCP tools first!
|
||||
For third-party services (GitHub, Gmail, Slack, etc.), load the \`composio-integration\` skill. For capabilities Composio doesn't cover (web search, file scraping, audio), use MCP tools via the \`mcp-integration\` skill.
|
||||
|
||||
## Execution Reminders
|
||||
- Explore existing files and structure before creating new assets.
|
||||
|
|
@ -212,6 +233,7 @@ ${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-list-toolkits\`, \`composio-search-tools\`, \`composio-execute-tool\`, \`composio-connect-toolkit\` — Composio integration tools. Load the \`composio-integration\` skill for usage guidance.
|
||||
|
||||
**Prefer these tools whenever possible** — they work instantly with zero friction. For file operations inside \`~/.rowboat/\`, always use these instead of \`executeCommand\`.
|
||||
|
||||
|
|
@ -251,3 +273,29 @@ This renders as an interactive card in the UI that the user can click to open th
|
|||
**IMPORTANT:** Only use filepath blocks for files that already exist. The card is clickable and opens the file, so it must point to a real file. If you are proposing a path for a file that hasn't been created yet (e.g., "Shall I save it at ~/Documents/report.pdf?"), use inline code (\`~/Documents/report.pdf\`) instead of a filepath block. Use the filepath block only after the file has been written/created successfully.
|
||||
|
||||
Never output raw file paths in plain text when they could be wrapped in a filepath block — unless the file does not exist yet.`;
|
||||
|
||||
/**
|
||||
* Cached Composio instructions. Invalidated by calling invalidateCopilotInstructionsCache().
|
||||
*/
|
||||
let cachedInstructions: string | null = null;
|
||||
|
||||
/**
|
||||
* Invalidate the cached instructions so the next buildCopilotInstructions() call
|
||||
* regenerates the Composio section. Call this after connecting/disconnecting a toolkit.
|
||||
*/
|
||||
export function invalidateCopilotInstructionsCache(): void {
|
||||
cachedInstructions = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build full copilot instructions with dynamic Composio tools section.
|
||||
* Results are cached and reused until invalidated via invalidateCopilotInstructionsCache().
|
||||
*/
|
||||
export async function buildCopilotInstructions(): Promise<string> {
|
||||
if (cachedInstructions !== null) return cachedInstructions;
|
||||
const composioPrompt = await getComposioToolsPrompt();
|
||||
cachedInstructions = composioPrompt
|
||||
? CopilotInstructions + '\n' + composioPrompt
|
||||
: CopilotInstructions;
|
||||
return cachedInstructions;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
export const skill = String.raw`
|
||||
# Composio Integration
|
||||
|
||||
**Load this skill** when the user asks to interact with ANY third-party service — email, GitHub, Slack, LinkedIn, Notion, Jira, Google Sheets, calendar, etc. This skill provides the complete workflow for discovering, connecting, and executing Composio tools.
|
||||
|
||||
## Available Tools
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| **composio-list-toolkits** | List all available integrations and their connection status |
|
||||
| **composio-search-tools** | Search for tools by use case; returns slugs and input schemas |
|
||||
| **composio-execute-tool** | Execute a tool by slug with parameters |
|
||||
| **composio-connect-toolkit** | Connect a service via OAuth (opens browser) |
|
||||
|
||||
## Toolkit Slugs (exact values for toolkitSlug parameter)
|
||||
|
||||
| Service | Slug |
|
||||
|---------|------|
|
||||
| Gmail | \`gmail\` |
|
||||
| Google Calendar | \`googlecalendar\` |
|
||||
| Google Sheets | \`googlesheets\` |
|
||||
| Google Docs | \`googledocs\` |
|
||||
| Google Drive | \`googledrive\` |
|
||||
| Slack | \`slack\` |
|
||||
| GitHub | \`github\` |
|
||||
| Notion | \`notion\` |
|
||||
| Linear | \`linear\` |
|
||||
| Jira | \`jira\` |
|
||||
| Asana | \`asana\` |
|
||||
| Trello | \`trello\` |
|
||||
| HubSpot | \`hubspot\` |
|
||||
| Salesforce | \`salesforce\` |
|
||||
| LinkedIn | \`linkedin\` |
|
||||
| X (Twitter) | \`twitter\` |
|
||||
| Reddit | \`reddit\` |
|
||||
| Dropbox | \`dropbox\` |
|
||||
| OneDrive | \`onedrive\` |
|
||||
| Microsoft Outlook | \`microsoft_outlook\` |
|
||||
| Microsoft Teams | \`microsoft_teams\` |
|
||||
| Calendly | \`calendly\` |
|
||||
| Cal.com | \`cal\` |
|
||||
| Intercom | \`intercom\` |
|
||||
| Zendesk | \`zendesk\` |
|
||||
| Airtable | \`airtable\` |
|
||||
|
||||
**IMPORTANT:** Always use these exact slugs. Do NOT guess — e.g., Google Sheets is \`googlesheets\` (no underscore), not \`google_sheets\`.
|
||||
|
||||
## Critical: Check First, Connect Second
|
||||
|
||||
**BEFORE calling composio-connect-toolkit, ALWAYS check if the service is already connected.** The system prompt includes a "Currently connected" list. If the service is there, skip connecting and go straight to search + execute.
|
||||
|
||||
**Flow:**
|
||||
1. Check if the service is in the "Currently connected" list (in the system prompt above)
|
||||
2. If **connected** → go directly to step 4
|
||||
3. If **NOT connected** → call \`composio-connect-toolkit\` once, wait for user to authenticate, then continue
|
||||
4. Call \`composio-search-tools\` with SHORT keyword queries
|
||||
5. Read the \`inputSchema\` from results — note \`required\` fields
|
||||
6. Call \`composio-execute-tool\` with slug, toolkit, and all required arguments
|
||||
|
||||
**NEVER call composio-connect-toolkit for a service that's already connected.** This creates duplicate connect cards in the UI.
|
||||
|
||||
## Search Query Tips
|
||||
|
||||
Use **short keyword queries**, not full sentences:
|
||||
|
||||
| ✅ Good | ❌ Bad |
|
||||
|---------|--------|
|
||||
| "list issues" | "get all open issues for a GitHub repository" |
|
||||
| "send email" | "send an email to someone using Gmail" |
|
||||
| "get profile" | "fetch the authenticated user's profile details" |
|
||||
| "create spreadsheet" | "create a new Google Sheets spreadsheet with data" |
|
||||
|
||||
If the first search returns 0 results, try a different short query (e.g., "issues" instead of "list issues").
|
||||
|
||||
## Passing Arguments
|
||||
|
||||
**ALWAYS include the \`arguments\` field** when calling \`composio-execute-tool\`, even if the tool has no required parameters.
|
||||
|
||||
- Read the \`inputSchema\` from search results carefully
|
||||
- Extract user-provided values into the correct fields (e.g., "rowboatlabs/rowboat" → \`owner: "rowboatlabs", repo: "rowboat"\`)
|
||||
- For tools with empty \`properties: {}\`, pass \`arguments: {}\`
|
||||
- For tools with required fields, pass all of them
|
||||
|
||||
### Example: GitHub Issues
|
||||
|
||||
User says: "Get me the open issues on rowboatlabs/rowboat"
|
||||
|
||||
1. \`composio-search-tools({ query: "list issues", toolkitSlug: "github" })\`
|
||||
→ finds \`GITHUB_ISSUES_LIST_FOR_REPO\` with required: ["owner", "repo"]
|
||||
2. \`composio-execute-tool({ toolSlug: "GITHUB_ISSUES_LIST_FOR_REPO", toolkitSlug: "github", arguments: { owner: "rowboatlabs", repo: "rowboat", state: "open", per_page: 100 } })\`
|
||||
|
||||
### Example: Gmail Fetch
|
||||
|
||||
User says: "What's my latest email?"
|
||||
|
||||
1. \`composio-search-tools({ query: "fetch emails", toolkitSlug: "gmail" })\`
|
||||
→ finds \`GMAIL_FETCH_EMAILS\`
|
||||
2. \`composio-execute-tool({ toolSlug: "GMAIL_FETCH_EMAILS", toolkitSlug: "gmail", arguments: { user_id: "me", max_results: 5 } })\`
|
||||
|
||||
### Example: LinkedIn Profile (no-arg tool)
|
||||
|
||||
User says: "Get my LinkedIn profile"
|
||||
|
||||
1. \`composio-search-tools({ query: "get profile", toolkitSlug: "linkedin" })\`
|
||||
→ finds \`LINKEDIN_GET_MY_INFO\` with properties: {}
|
||||
2. \`composio-execute-tool({ toolSlug: "LINKEDIN_GET_MY_INFO", toolkitSlug: "linkedin", arguments: {} })\`
|
||||
|
||||
## Error Recovery
|
||||
|
||||
- **If a tool call fails** (missing fields, 500 error): Fix the arguments and retry IMMEDIATELY. Do NOT stop and narrate the error to the user.
|
||||
- **If search returns 0 results**: Try a different short query. If still 0, the tool may not exist for that service.
|
||||
- **If a tool requires connection**: Call \`composio-connect-toolkit\` once, then retry after connection.
|
||||
|
||||
## Multi-Part Requests
|
||||
|
||||
When the user says "connect X and then do Y" — complete BOTH parts in one turn:
|
||||
1. If X is already connected (check the connected list), skip to Y immediately
|
||||
2. If X needs connecting, connect it, then proceed to Y after authentication
|
||||
|
||||
## Confirmation Rules
|
||||
|
||||
- **Read-only actions** (fetch, list, get, search): Execute without asking
|
||||
- **Mutating actions** (send email, create issue, post, delete): Show the user what you're about to do and confirm before executing
|
||||
- **Connecting a toolkit**: Always safe — just do it when needed
|
||||
`;
|
||||
|
||||
export default skill;
|
||||
|
|
@ -12,6 +12,7 @@ import backgroundAgentsSkill from "./background-agents/skill.js";
|
|||
import createPresentationsSkill from "./create-presentations/skill.js";
|
||||
|
||||
import appNavigationSkill from "./app-navigation/skill.js";
|
||||
import composioIntegrationSkill from "./composio-integration/skill.js";
|
||||
|
||||
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
||||
const CATALOG_PREFIX = "src/application/assistant/skills";
|
||||
|
|
@ -84,6 +85,12 @@ const definitions: SkillDefinition[] = [
|
|||
summary: "Discovering, executing, and integrating MCP tools. Use this to check what external capabilities are available and execute MCP tools on behalf of users.",
|
||||
content: mcpIntegrationSkill,
|
||||
},
|
||||
{
|
||||
id: "composio-integration",
|
||||
title: "Composio Integration",
|
||||
summary: "Interact with third-party services (Gmail, GitHub, Slack, LinkedIn, Notion, Jira, Google Sheets, etc.) via Composio. Search, connect, and execute tools.",
|
||||
content: composioIntegrationSkill,
|
||||
},
|
||||
{
|
||||
id: "deletion-guardrails",
|
||||
title: "Deletion Guardrails",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -18,14 +22,12 @@ export const skill = String.raw`
|
|||
|
||||
| User Request | Check For | Likely Tool |
|
||||
|--------------|-----------|-------------|
|
||||
| "Search the web/internet" | firecrawl, composio, fetch | \`firecrawl_search\`, \`COMPOSIO_SEARCH_WEB\` |
|
||||
| "Search the web/internet" | firecrawl, fetch | \`firecrawl_search\` |
|
||||
| "Scrape this website" | firecrawl | \`firecrawl_scrape\` |
|
||||
| "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\`.
|
||||
|
|
@ -242,7 +244,7 @@ The schema tells you:
|
|||
**Example schema from listMcpTools:**
|
||||
\`\`\`json
|
||||
{
|
||||
"name": "COMPOSIO_SEARCH_WEB",
|
||||
"name": "firecrawl_search",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -263,10 +265,10 @@ The schema tells you:
|
|||
**Correct executeMcpTool call:**
|
||||
\`\`\`json
|
||||
{
|
||||
"serverName": "composio",
|
||||
"toolName": "COMPOSIO_SEARCH_WEB",
|
||||
"serverName": "firecrawl",
|
||||
"toolName": "firecrawl_search",
|
||||
"arguments": {
|
||||
"query": "elon musk latest news"
|
||||
"query": "latest AI news"
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
|
@ -274,18 +276,18 @@ The schema tells you:
|
|||
**WRONG - Missing arguments:**
|
||||
\`\`\`json
|
||||
{
|
||||
"serverName": "composio",
|
||||
"toolName": "COMPOSIO_SEARCH_WEB"
|
||||
"serverName": "firecrawl",
|
||||
"toolName": "firecrawl_search"
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
**WRONG - Wrong parameter name:**
|
||||
\`\`\`json
|
||||
{
|
||||
"serverName": "composio",
|
||||
"toolName": "COMPOSIO_SEARCH_WEB",
|
||||
"serverName": "firecrawl",
|
||||
"toolName": "firecrawl_search",
|
||||
"arguments": {
|
||||
"search": "elon musk" // Wrong! Should be "query"
|
||||
"search": "latest AI news" // Wrong! Should be "query"
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ import { McpServerDefinition } from "@x/shared/dist/mcp.js";
|
|||
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 { executeAction as executeComposioAction, isConfigured as isComposioConfigured, searchTools as searchComposioTools } from "../../composio/client.js";
|
||||
import { CURATED_TOOLKITS, CURATED_TOOLKIT_SLUGS } from "@x/shared/dist/composio.js";
|
||||
import type { ToolContext } from "./exec-tool.js";
|
||||
import { generateText } from "ai";
|
||||
import { createProvider } from "../../models/models.js";
|
||||
|
|
@ -1173,4 +1176,152 @@ 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. Shows a connect card for the user to authenticate.',
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
// Return signal — the UI renders a ComposioConnectCard with a Connect button.
|
||||
// OAuth only starts when the user clicks that button.
|
||||
const toolkit = CURATED_TOOLKITS.find(t => t.slug === toolkitSlug);
|
||||
return {
|
||||
success: true,
|
||||
message: `Please connect ${toolkit?.displayName ?? toolkitSlug} to continue.`,
|
||||
};
|
||||
},
|
||||
isAvailable: async () => isComposioConfigured(),
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,8 +14,9 @@ import {
|
|||
ZExecuteActionRequest,
|
||||
ZExecuteActionResponse,
|
||||
ZListResponse,
|
||||
ZTool,
|
||||
ZSearchResultTool,
|
||||
ZToolkit,
|
||||
type NormalizedToolResult,
|
||||
} from "./types.js";
|
||||
import { isSignedIn } from "../account/account.js";
|
||||
import { getAccessToken } from "../auth/tokens.js";
|
||||
|
|
@ -72,7 +73,7 @@ function loadConfig(): ComposioConfig {
|
|||
/**
|
||||
* Save Composio configuration
|
||||
*/
|
||||
export function saveConfig(config: ComposioConfig): void {
|
||||
function saveConfig(config: ComposioConfig): void {
|
||||
const dir = path.dirname(CONFIG_FILE);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
|
|
@ -167,7 +168,15 @@ export async function composioApiCall<T extends z.ZodTypeAny>(
|
|||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Composio API error: ${response.status} ${response.statusText}`);
|
||||
// Try to extract a human-readable message from the JSON body
|
||||
let detail = '';
|
||||
try {
|
||||
const body = JSON.parse(rawText);
|
||||
if (typeof body?.error === 'string') detail = body.error;
|
||||
else if (typeof body?.message === 'string') detail = body.message;
|
||||
} catch { /* body isn't JSON or has no message field */ }
|
||||
const suffix = detail ? `: ${detail}` : '';
|
||||
throw new Error(`Composio API error: ${response.status} ${response.statusText}${suffix}`);
|
||||
}
|
||||
|
||||
if (!contentType.includes('application/json')) {
|
||||
|
|
@ -246,15 +255,6 @@ export async function createAuthConfig(
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an auth config
|
||||
*/
|
||||
export async function deleteAuthConfig(authConfigId: string): Promise<z.infer<typeof ZDeleteOperationResponse>> {
|
||||
return composioApiCall(ZDeleteOperationResponse, `/auth_configs/${authConfigId}`, {}, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a connected account
|
||||
*/
|
||||
|
|
@ -284,20 +284,39 @@ export async function deleteConnectedAccount(connectedAccountId: string): Promis
|
|||
}
|
||||
|
||||
/**
|
||||
* List available tools for a toolkit
|
||||
* 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.
|
||||
*
|
||||
* Uses a limit of 50 (not 15) to avoid the curated-filter-after-limit problem where
|
||||
* in-scope results at position 16+ would be discarded if earlier results are out-of-scope.
|
||||
*/
|
||||
export async function listToolkitTools(
|
||||
toolkitSlug: string,
|
||||
searchQuery: string | null = null,
|
||||
): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>> {
|
||||
export async function searchTools(
|
||||
searchQuery: string,
|
||||
toolkitSlugs?: string[],
|
||||
): Promise<{ items: NormalizedToolResult[] }> {
|
||||
const params: Record<string, string> = {
|
||||
toolkit_slug: toolkitSlug,
|
||||
limit: '200',
|
||||
query: searchQuery,
|
||||
limit: '50',
|
||||
};
|
||||
if (searchQuery) {
|
||||
params.search = searchQuery;
|
||||
if (toolkitSlugs && toolkitSlugs.length === 1) {
|
||||
params.toolkit_slug = toolkitSlugs[0];
|
||||
}
|
||||
return composioApiCall(ZListResponse(ZTool), "/tools", params);
|
||||
|
||||
const result = await composioApiCall(ZListResponse(ZSearchResultTool), "/tools", params);
|
||||
|
||||
const items: NormalizedToolResult[] = result.items.map((item) => ({
|
||||
slug: item.slug,
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
toolkitSlug: item.toolkit.slug,
|
||||
inputParameters: {
|
||||
type: 'object' as const,
|
||||
properties: item.input_parameters?.properties ?? {},
|
||||
required: item.input_parameters?.required,
|
||||
},
|
||||
}));
|
||||
|
||||
return { items };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
import { z } from "zod";
|
||||
import { ZToolkitMeta as ZSharedToolkitMeta, ZToolkitItem } from "@x/shared/dist/composio.js";
|
||||
|
||||
// Re-export the shared toolkit schemas so existing imports continue to work
|
||||
export const ZToolkitMeta = ZSharedToolkitMeta;
|
||||
|
||||
/**
|
||||
* Composio authentication schemes
|
||||
|
|
@ -29,26 +33,9 @@ export const ZConnectedAccountStatus = z.enum([
|
|||
]);
|
||||
|
||||
/**
|
||||
* Toolkit metadata
|
||||
* Toolkit schema — same shape as ZToolkitItem from shared, re-exported for convenience.
|
||||
*/
|
||||
export const ZToolkitMeta = z.object({
|
||||
description: z.string(),
|
||||
logo: z.string(),
|
||||
tools_count: z.number(),
|
||||
triggers_count: z.number(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Toolkit schema
|
||||
*/
|
||||
export const ZToolkit = z.object({
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
meta: ZToolkitMeta,
|
||||
no_auth: z.boolean().optional(),
|
||||
auth_schemes: z.array(ZAuthScheme).optional(),
|
||||
composio_managed_auth_schemes: z.array(ZAuthScheme).optional(),
|
||||
});
|
||||
export const ZToolkit = ZToolkitItem;
|
||||
|
||||
/**
|
||||
* Tool schema
|
||||
|
|
@ -147,7 +134,7 @@ export const ZCreateConnectedAccountRequest = z.object({
|
|||
*/
|
||||
export const ZCreateConnectedAccountResponse = z.object({
|
||||
id: z.string(),
|
||||
connectionData: ZConnectionData,
|
||||
connectionData: ZConnectionData.optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
@ -227,12 +214,44 @@ export const ZLocalConnectedAccount = z.object({
|
|||
lastUpdatedAt: z.string(),
|
||||
});
|
||||
|
||||
export type AuthScheme = z.infer<typeof ZAuthScheme>;
|
||||
export type ConnectedAccountStatus = z.infer<typeof ZConnectedAccountStatus>;
|
||||
export type Toolkit = z.infer<typeof ZToolkit>;
|
||||
export type Tool = z.infer<typeof ZTool>;
|
||||
export type AuthConfig = z.infer<typeof ZAuthConfig>;
|
||||
export type ConnectedAccount = z.infer<typeof ZConnectedAccount>;
|
||||
export type LocalConnectedAccount = z.infer<typeof ZLocalConnectedAccount>;
|
||||
export type ExecuteActionRequest = z.infer<typeof ZExecuteActionRequest>;
|
||||
export type ExecuteActionResponse = z.infer<typeof ZExecuteActionResponse>;
|
||||
export type ConnectedAccountStatus = z.infer<typeof ZConnectedAccountStatus>;
|
||||
|
||||
/**
|
||||
* Tool schema for search results.
|
||||
* Unlike ZTool, `toolkit` is optional because the Composio /tools search endpoint
|
||||
* sometimes omits the toolkit object from results. `input_parameters` uses
|
||||
* lenient defaults so tools with no params (e.g. LINKEDIN_GET_MY_INFO) parse cleanly.
|
||||
*/
|
||||
export 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(),
|
||||
}),
|
||||
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();
|
||||
|
||||
/**
|
||||
* Normalized tool result returned from searchTools().
|
||||
*/
|
||||
export const ZNormalizedToolResult = z.object({
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
toolkitSlug: z.string(),
|
||||
inputParameters: z.object({
|
||||
type: z.literal('object'),
|
||||
properties: z.record(z.string(), z.unknown()),
|
||||
required: z.array(z.string()).optional(),
|
||||
}),
|
||||
});
|
||||
export type NormalizedToolResult = z.infer<typeof ZNormalizedToolResult>;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue