mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-15 20:05:16 +02:00
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>
This commit is contained in:
parent
ad2c9f232c
commit
a2c92c7491
7 changed files with 97 additions and 104 deletions
|
|
@ -307,20 +307,10 @@ export async function useComposioForGoogleCalendar(): Promise<{ enabled: boolean
|
|||
}
|
||||
|
||||
/**
|
||||
* List available Composio toolkits — filtered to curated list only
|
||||
* List available Composio toolkits — filtered to curated list only.
|
||||
* Return type matches the ZToolkit schema from core/composio/types.ts.
|
||||
*/
|
||||
export async function listToolkits(): Promise<{
|
||||
items: Array<{
|
||||
slug: string;
|
||||
name: string;
|
||||
meta: { description: string; logo: string; tools_count: number; triggers_count: number };
|
||||
no_auth?: boolean;
|
||||
auth_schemes?: string[];
|
||||
composio_managed_auth_schemes?: string[];
|
||||
}>;
|
||||
nextCursor: string | null;
|
||||
totalItems: number;
|
||||
}> {
|
||||
export async function listToolkits() {
|
||||
// Paginate through all API pages to collect every curated toolkit
|
||||
type ToolkitItem = Awaited<ReturnType<typeof composioClient.listToolkits>>['items'][number];
|
||||
const allItems: ToolkitItem[] = [];
|
||||
|
|
@ -335,7 +325,7 @@ export async function listToolkits(): Promise<{
|
|||
const filtered = allItems.filter(item => CURATED_TOOLKIT_SLUGS.has(item.slug));
|
||||
return {
|
||||
items: filtered,
|
||||
nextCursor: null,
|
||||
nextCursor: null as string | null,
|
||||
totalItems: filtered.length,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +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 { buildCopilotInstructions } from "../application/assistant/instructions.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";
|
||||
|
|
@ -370,14 +369,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 (includes Composio meta-tools).
|
||||
const tools: Record<string, z.infer<typeof ToolAttachment>> = {};
|
||||
for (const name of Object.keys(BuiltinTools)) {
|
||||
tools[name] = { type: "builtin", name };
|
||||
}
|
||||
// Rebuild instructions to include current Composio tools section
|
||||
const instructions = await buildCopilotInstructions();
|
||||
return { ...CopilotAgent, tools, instructions };
|
||||
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,
|
||||
}
|
||||
|
|
@ -14,8 +14,10 @@ import {
|
|||
ZExecuteActionRequest,
|
||||
ZExecuteActionResponse,
|
||||
ZListResponse,
|
||||
ZSearchResultTool,
|
||||
ZTool,
|
||||
ZToolkit,
|
||||
type NormalizedToolResult,
|
||||
} from "./types.js";
|
||||
import { isSignedIn } from "../account/account.js";
|
||||
import { getAccessToken } from "../auth/tokens.js";
|
||||
|
|
@ -283,52 +285,34 @@ 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.
|
||||
* Infer toolkit slug from a tool slug when the API omits the toolkit object.
|
||||
* Convention: TOOLKIT_ACTION (e.g., GITHUB_CREATE_ISSUE → github).
|
||||
*
|
||||
* This fallback exists because the Composio /tools search endpoint occasionally
|
||||
* returns results without the `toolkit` field — observed with certain search
|
||||
* queries and when the tool is from a less-common integration. The inference is
|
||||
* a best-effort heuristic: lowercase the first segment before the first underscore.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* 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 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[] } }> }> {
|
||||
): Promise<{ items: NormalizedToolResult[] }> {
|
||||
const params: Record<string, string> = {
|
||||
search: searchQuery,
|
||||
limit: '15',
|
||||
limit: '50',
|
||||
};
|
||||
if (toolkitSlugs && toolkitSlugs.length === 1) {
|
||||
params.toolkit_slug = toolkitSlugs[0];
|
||||
|
|
@ -336,12 +320,10 @@ export async function searchTools(
|
|||
|
||||
const result = await composioApiCall(ZListResponse(ZSearchResultTool), "/tools", params);
|
||||
|
||||
const items = result.items.map((item) => ({
|
||||
const items: NormalizedToolResult[] = 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] : ''),
|
||||
|
|
|
|||
|
|
@ -231,3 +231,41 @@ export const ZLocalConnectedAccount = z.object({
|
|||
|
||||
export type LocalConnectedAccount = z.infer<typeof ZLocalConnectedAccount>;
|
||||
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(),
|
||||
}).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();
|
||||
|
||||
/**
|
||||
* 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>;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
/**
|
||||
* Curated Composio toolkits available to Rowboat users.
|
||||
* Single source of truth for slugs, display names, and categories.
|
||||
* Sorted by slug (ASC) for maintainability.
|
||||
*/
|
||||
|
||||
export type ToolkitCategory = 'communication' | 'productivity' | 'development' | 'crm' | 'social' | 'storage' | 'support';
|
||||
|
|
@ -12,46 +13,31 @@ export interface CuratedToolkit {
|
|||
}
|
||||
|
||||
export const CURATED_TOOLKITS: CuratedToolkit[] = [
|
||||
// Communication
|
||||
{ slug: 'airtable', displayName: 'Airtable', category: 'productivity' },
|
||||
{ slug: 'asana', displayName: 'Asana', category: 'productivity' },
|
||||
{ slug: 'cal', displayName: 'Cal.com', category: 'productivity' },
|
||||
{ slug: 'calendly', displayName: 'Calendly', category: 'productivity' },
|
||||
{ slug: 'dropbox', displayName: 'Dropbox', category: 'storage' },
|
||||
{ slug: 'github', displayName: 'GitHub', category: 'development' },
|
||||
{ slug: 'gmail', displayName: 'Gmail', category: 'communication' },
|
||||
{ slug: 'slack', displayName: 'Slack', category: 'communication' },
|
||||
{ slug: 'microsoft_outlook', displayName: 'Microsoft Outlook', category: 'communication' },
|
||||
{ slug: 'microsoft_teams', displayName: 'Microsoft Teams', category: 'communication' },
|
||||
|
||||
// Productivity
|
||||
{ slug: 'googlecalendar', displayName: 'Google Calendar', category: 'productivity' },
|
||||
{ slug: 'googledocs', displayName: 'Google Docs', category: 'productivity' },
|
||||
{ slug: 'googlesheets', displayName: 'Google Sheets', category: 'productivity' },
|
||||
{ slug: 'notion', displayName: 'Notion', category: 'productivity' },
|
||||
{ slug: 'airtable', displayName: 'Airtable', category: 'productivity' },
|
||||
{ slug: 'calendly', displayName: 'Calendly', category: 'productivity' },
|
||||
{ slug: 'cal', displayName: 'Cal.com', category: 'productivity' },
|
||||
|
||||
// Storage
|
||||
{ slug: 'googledrive', displayName: 'Google Drive', category: 'storage' },
|
||||
{ slug: 'dropbox', displayName: 'Dropbox', category: 'storage' },
|
||||
{ slug: 'onedrive', displayName: 'OneDrive', category: 'storage' },
|
||||
|
||||
// Development
|
||||
{ slug: 'github', displayName: 'GitHub', category: 'development' },
|
||||
{ slug: 'linear', displayName: 'Linear', category: 'development' },
|
||||
{ slug: 'jira', displayName: 'Jira', category: 'development' },
|
||||
|
||||
// Project Management
|
||||
{ slug: 'asana', displayName: 'Asana', category: 'productivity' },
|
||||
{ slug: 'trello', displayName: 'Trello', category: 'productivity' },
|
||||
|
||||
// CRM & Sales
|
||||
{ slug: 'googlesheets', displayName: 'Google Sheets', category: 'productivity' },
|
||||
{ slug: 'hubspot', displayName: 'HubSpot', category: 'crm' },
|
||||
{ slug: 'salesforce', displayName: 'Salesforce', category: 'crm' },
|
||||
|
||||
// Social
|
||||
{ slug: 'linkedin', displayName: 'LinkedIn', category: 'social' },
|
||||
{ slug: 'twitter', displayName: 'X', category: 'social' },
|
||||
{ slug: 'reddit', displayName: 'Reddit', category: 'social' },
|
||||
|
||||
// Support
|
||||
{ slug: 'intercom', displayName: 'Intercom', category: 'support' },
|
||||
{ slug: 'jira', displayName: 'Jira', category: 'development' },
|
||||
{ slug: 'linear', displayName: 'Linear', category: 'development' },
|
||||
{ slug: 'linkedin', displayName: 'LinkedIn', category: 'social' },
|
||||
{ slug: 'microsoft_outlook', displayName: 'Microsoft Outlook', category: 'communication' },
|
||||
{ slug: 'microsoft_teams', displayName: 'Microsoft Teams', category: 'communication' },
|
||||
{ slug: 'notion', displayName: 'Notion', category: 'productivity' },
|
||||
{ slug: 'onedrive', displayName: 'OneDrive', category: 'storage' },
|
||||
{ slug: 'reddit', displayName: 'Reddit', category: 'social' },
|
||||
{ slug: 'salesforce', displayName: 'Salesforce', category: 'crm' },
|
||||
{ slug: 'slack', displayName: 'Slack', category: 'communication' },
|
||||
{ slug: 'trello', displayName: 'Trello', category: 'productivity' },
|
||||
{ slug: 'twitter', displayName: 'X', category: 'social' },
|
||||
{ slug: 'zendesk', displayName: 'Zendesk', category: 'support' },
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -398,6 +398,7 @@ const ipcSchemas = {
|
|||
res: z.null(),
|
||||
},
|
||||
// Composio Tools Library channels
|
||||
// Response schema mirrors core/composio/types.ts ZToolkit (kept inline to avoid cross-package import)
|
||||
'composio:list-toolkits': {
|
||||
req: z.object({}),
|
||||
res: z.object({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue