diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index fdf06c5..993c512 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -25,8 +25,7 @@ jobs: - name: Check format and lint run: | - bun run format - bun run lint + bun run check # TODO: tests diff --git a/README.md b/README.md index d750afb..8c00c2a 100644 --- a/README.md +++ b/README.md @@ -51,20 +51,12 @@ bun run start bun run dev ``` -### Lint your code +### Format and lint your code ```bash -bun run lint +bun run check # To fix automatically: -bun run lint:fix -``` - -### Format your code - -```bash -bun run format -# To fix automatically: -bun run format:fix +bun run check:fix ``` ### Run individual services diff --git a/apps/bitte-ai/index.ts b/apps/bitte-ai/index.ts index 26fbdca..f638174 100644 --- a/apps/bitte-ai/index.ts +++ b/apps/bitte-ai/index.ts @@ -1,7 +1,7 @@ import { FastMCP } from 'fastmcp'; import { z } from 'zod'; +import { type PluginToolSpec, createToolFromPluginSpec } from './lib/bitte-plugins'; import { searchAgents, searchAgentsSchema, searchTools, searchToolsSchema } from './lib/search'; -import { services } from './tools'; import { callBitteAPI } from './utils/bitte'; // Export configuration export { config } from './config'; @@ -24,6 +24,22 @@ export interface ExecuteAgentParams { input: string; } +// Create a log wrapper that also logs to console +function wrapLogger(log: any) { + return new Proxy(log, { + get(target, prop) { + const originalMethod = target[prop]; + if (typeof originalMethod === 'function') { + return (...args: any[]) => { + console.log(`[${String(prop)}]`, ...args); + return originalMethod.apply(target, args); + }; + } + return originalMethod; + }, + }); +} + // Create and export the server export const server = new FastMCP({ name: 'bitte-ai-mcp-proxy', @@ -49,9 +65,10 @@ server.addTool({ agentId: z.string().describe('ID of the agent to retrieve'), }), execute: async (args, { log }) => { - log.info(`Getting agent with ID: ${args.agentId}`); + const wrappedLog = wrapLogger(log); + wrappedLog.info(`Getting agent with ID: ${args.agentId}`); const endpoint = `/api/agents/${args.agentId}`; - const data = await callBitteAPI(endpoint, 'GET', undefined, log); + const data = await callBitteAPI(endpoint, 'GET', undefined, wrappedLog); return JSON.stringify(data); }, }); @@ -64,7 +81,8 @@ server.addTool({ input: z.string().describe('Input to the agent'), }), execute: async (args, { log, session }) => { - log.info(`Executing agent with ID: ${args.agentId}`); + const wrappedLog = wrapLogger(log); + wrappedLog.info(`Executing agent with ID: ${args.agentId}`); try { // First, search for the agent to make sure it exists @@ -73,7 +91,7 @@ server.addTool({ query: args.agentId, threshold: 0.1, // Lower threshold for more exact matching }, - log + wrappedLog ); // Check if we found a matching agent @@ -90,7 +108,7 @@ server.addTool({ }; // Call the Bitte API to execute the agent - const data = await callBitteAPI('/chat', 'POST', body, log); + const data = await callBitteAPI('/chat', 'POST', body, wrappedLog); if (typeof data === 'string') { return { @@ -104,7 +122,7 @@ server.addTool({ }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - log.error(`Error executing agent: ${errorMessage}`); + wrappedLog.error(`Error executing agent: ${errorMessage}`); return { content: [{ type: 'text', text: `Error executing agent: ${errorMessage}` }], isError: true, @@ -120,11 +138,16 @@ server.addTool({ parameters: z.object({ tool: z.string().describe('The tool to execute'), params: z.string().describe('The parameters to pass to the tool as a JSON string'), + metadata: z + .object({}) + .describe( + 'Optional metadata to pass to the tool i.e. {accountId: "123", evmAddress: "0x123"}' + ) + .optional(), }), execute: async (args, { log }) => { - log.info(`Executing execute-tool tool with params: ${JSON.stringify(args)}`); - console.log('execute-tool with args', JSON.stringify(args)); - console.log('args', args); + const wrappedLog = wrapLogger(log); + wrappedLog.info(`Executing execute-tool tool with params: ${JSON.stringify(args)}`); try { // Use searchTools to find the specified tool @@ -133,25 +156,36 @@ server.addTool({ query: args.tool, threshold: 0.1, // Lower threshold for more exact matching }, - log + wrappedLog ); // Get the first (best) match const toolMatch = searchResult.combinedResults[0]; + if (!toolMatch) { throw new Error(`Tool '${args.tool}' not found`); } const tool = toolMatch.item as { execute?: (params: Record) => Promise; + execution?: { baseUrl: string; path: string; httpMethod: string }; + function?: { name: string; description: string; parameters?: any }; }; - if (!tool || typeof tool.execute !== 'function') { + let result: unknown; + + // Check if the tool has an execution field + if (tool.execution && tool.function) { + // Create and execute a core tool with HTTP-based execution + const coreTool = createToolFromPluginSpec(tool as PluginToolSpec, args.metadata); + result = await coreTool.execute(JSON.parse(args.params)); + } else if (tool.execute && typeof tool.execute === 'function') { + // Use the tool's execute method directly + result = await tool.execute(JSON.parse(args.params)); + } else { throw new Error(`Tool '${args.tool}' found but cannot be executed`); } - const result = await tool.execute(JSON.parse(args.params)); - // Ensure we return a properly typed result if (typeof result === 'string') { return { @@ -164,7 +198,7 @@ server.addTool({ }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - log.error(`Error executing tool: ${errorMessage}`); + wrappedLog.error(`Error executing tool: ${errorMessage}`); return { content: [{ type: 'text', text: `Error executing tool: ${errorMessage}` }], isError: true, @@ -179,13 +213,14 @@ server.addTool({ description: 'Search for AI agents across Bitte API and other services', parameters: searchAgentsSchema, execute: async (args, { log }) => { - log.info(`Searching agents with params: ${JSON.stringify(args)}`); + const wrappedLog = wrapLogger(log); + wrappedLog.info(`Searching agents with params: ${JSON.stringify(args)}`); try { - const result = await searchAgents(args, log); + const result = await searchAgents(args, wrappedLog); return JSON.stringify(result); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - log.error(`Error searching agents: ${errorMessage}`); + wrappedLog.error(`Error searching agents: ${errorMessage}`); return { content: [{ type: 'text', text: `Error searching agents: ${errorMessage}` }], isError: true, @@ -200,13 +235,13 @@ server.addTool({ description: 'Search for tools across Bitte API and other services', parameters: searchToolsSchema, execute: async (args, { log }) => { - log.info(`Searching tools with params: ${JSON.stringify(args)}`); + const wrappedLog = wrapLogger(log); try { - const result = await searchTools(args, log); + const result = await searchTools(args, wrappedLog); return JSON.stringify(result); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - log.error(`Error searching tools: ${errorMessage}`); + wrappedLog.error(`Error searching tools: ${errorMessage}`); return { content: [{ type: 'text', text: `Error searching tools: ${errorMessage}` }], isError: true, diff --git a/apps/bitte-ai/lib/bitte-plugins.ts b/apps/bitte-ai/lib/bitte-plugins.ts new file mode 100644 index 0000000..cf15a82 --- /dev/null +++ b/apps/bitte-ai/lib/bitte-plugins.ts @@ -0,0 +1,117 @@ +import type { z } from 'zod'; + +export interface ExecutionSpec { + baseUrl: string; + path: string; + httpMethod: string; +} + +export interface FunctionSpec { + name: string; + description: string; + parameters?: z.ZodObject; +} + +export interface PluginToolSpec { + function: FunctionSpec; + execution: ExecutionSpec; +} + +export interface BitteMetadata { + [key: string]: any; +} + +// Type for tool execution function +export type BitteToolExecutor = ( + args: Record +) => Promise<{ data?: unknown; error?: string }>; + +// Helper function to extract error messages +export function getErrorMsg(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + return String(error); +} + +export const createExecutor = ( + tool: PluginToolSpec, + metadata?: BitteMetadata +): BitteToolExecutor => { + return async (args) => { + try { + const { baseUrl, path, httpMethod } = tool.execution; + const fullBaseUrl = baseUrl.startsWith('http') ? baseUrl : `https://${baseUrl}`; + + // Build URL with path parameters + let url = `${fullBaseUrl}${path}`; + const remainingArgs = { ...args }; + + url = url.replace(/\{(\w+)\}/g, (_, key) => { + if (remainingArgs[key] === undefined) { + throw new Error(`Missing required path parameter: ${key}`); + } + const value = remainingArgs[key]; + delete remainingArgs[key]; + return encodeURIComponent(String(value)); + }); + + // Setup request + const headers: Record = {}; + if (metadata) { + headers['mb-metadata'] = JSON.stringify(metadata); + } + + const method = httpMethod.toUpperCase(); + const fetchOptions: RequestInit = { method, headers }; + + // Handle query parameters + const queryParams = new URLSearchParams(); + for (const [key, value] of Object.entries(remainingArgs)) { + if (value != null) { + queryParams.append(key, String(value)); + } + } + + const queryString = queryParams.toString(); + if (queryString) { + url += (url.includes('?') ? '&' : '?') + queryString; + } + + // Handle request body + if (['POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'].includes(method)) { + headers['Content-Type'] = 'application/json'; + fetchOptions.body = JSON.stringify(remainingArgs); + } + + // Execute request + const response = await fetch(url, fetchOptions); + + if (!response.ok) { + throw new Error( + `HTTP error during plugin tool execution: ${response.status} ${response.statusText}` + ); + } + // Parse response based on content type + const contentType = response.headers.get('Content-Type') || ''; + const data = await (contentType.includes('application/json') + ? response.json() + : contentType.includes('text') + ? response.text() + : response.blob()); + + return { data }; + } catch (error) { + return { + error: `Error executing pluginTool ${tool.function.name}. ${getErrorMsg(error)}`, + }; + } + }; +}; + +export const createToolFromPluginSpec = (func: PluginToolSpec, metadata?: BitteMetadata) => { + return { + ...func.function, + execute: async (args: Record) => createExecutor(func, metadata)(args), + }; +}; diff --git a/apps/bitte-ai/lib/search.ts b/apps/bitte-ai/lib/search.ts index 2ead176..92070d9 100644 --- a/apps/bitte-ai/lib/search.ts +++ b/apps/bitte-ai/lib/search.ts @@ -117,6 +117,7 @@ export async function searchAgents( } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); + log?.error?.(`Error searching Bitte API agents: ${errorMessage}`); } // We skip searching services as requested @@ -150,8 +151,8 @@ export interface SearchToolsParams { * Default search options for tools */ const DEFAULT_TOOLS_SEARCH_PARAMS: Omit = { - limit: 10, - threshold: 0.3, + limit: 5, + threshold: 1, includeServices: [ 'goat', 'agentkit', @@ -179,13 +180,14 @@ export async function searchTools( params: SearchToolsParams, log?: { info?: (message: string) => void; error?: (message: string) => void } ): Promise { + log?.info?.(`Searching tools with params: ${JSON.stringify(params)}`); // Merge with default parameters const mergedParams = { ...DEFAULT_TOOLS_SEARCH_PARAMS, ...params }; const { query, limit, threshold, includeServices } = mergedParams; // Search options for Fuse.js const searchOptions: SearchOptions = { - keys: ['name', 'description', 'function.name', 'function.description'], + keys: ['id', 'name', 'description', 'function.name', 'function.description'], limit, threshold, }; @@ -206,14 +208,17 @@ export async function searchTools( // If response is an array, search within it if (Array.isArray(response)) { + log?.info?.('Response is an array, searching within it'); // If query is "*", return all results without searching if (query === '*') { + log?.info?.('Query is "*", returning all results'); result.bitteResults = response.map((tool) => ({ item: tool, score: 1, refIndex: 0 })); result.totalResults += result.bitteResults.length; // Add Bitte results to combined results result.combinedResults.push(...result.bitteResults); } else { + log?.info?.('Query is not "*", searching within the results'); // Search within the results using Fuse.js result.bitteResults = searchArray(response, query, searchOptions); result.totalResults += result.bitteResults.length; @@ -222,9 +227,11 @@ export async function searchTools( result.combinedResults.push(...result.bitteResults); } } else { + log?.info?.('Response is not an array, UNIMPLEMENTED'); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); + log?.error?.(`Error searching Bitte API tools: ${errorMessage}`); } // Then, search each included service @@ -273,6 +280,7 @@ export async function searchTools( } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); result.serviceResults[serviceName] = []; + log?.error?.(`Error searching service ${serviceName}: ${errorMessage}`); } }) ); diff --git a/apps/bitte-ai/package.json b/apps/bitte-ai/package.json index f604977..ebfe153 100644 --- a/apps/bitte-ai/package.json +++ b/apps/bitte-ai/package.json @@ -13,9 +13,7 @@ "start": "bun run dist/index.js", "dev": "bun --watch run index.ts", "build": "tsup", - "watch": "tsup --watch", - "lint": "biome check .", - "format": "biome format ." + "watch": "tsup --watch" }, "peerDependencies": { "typescript": "^5.0.0" diff --git a/apps/bitte-ai/utils/search.ts b/apps/bitte-ai/utils/search.ts index f780110..a10a56c 100644 --- a/apps/bitte-ai/utils/search.ts +++ b/apps/bitte-ai/utils/search.ts @@ -93,16 +93,12 @@ export function searchArray( * Internal helper to perform the actual search operation */ function performSearch(data: T[], query: string, options: SearchOptions): SearchResult[] { - // Configure Fuse.js - const fuseOptions = { + const fuse = new Fuse(data, { includeScore: true, - includeRefIndex: true, threshold: options.threshold ?? DEFAULT_SEARCH_OPTIONS.threshold, keys: options.keys ?? [], - }; - - // Create Fuse instance - const fuse = new Fuse(data, fuseOptions); + isCaseSensitive: false, + }); // Perform the search const limit = options.limit ?? DEFAULT_SEARCH_OPTIONS.limit ?? 10; diff --git a/package.json b/package.json index b4f4703..d44967b 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,8 @@ "dev": "turbo dev", "build": "turbo build", "start": "turbo start", - "lint": "turbo lint && biome check .", - "lint:fix": "biome check --apply .", - "format": "turbo format && biome format .", - "format:fix": "biome format --write .", + "check": "biome check", + "check:fix": "biome check --write", "start:all": "bun run ./scripts/start-dev.ts", "docker:up": "bun run ./scripts/start-all.ts", "docker:down": "bun run ./scripts/docker-down.ts" diff --git a/turbo.json b/turbo.json index d66333c..0c1d6b5 100644 --- a/turbo.json +++ b/turbo.json @@ -6,18 +6,18 @@ "dependsOn": ["^build"], "outputs": ["dist/**"] }, - "lint": { - "outputs": [] - }, - "format": { - "outputs": [] - }, "dev": { "cache": false, "persistent": true }, "start": { "dependsOn": ["build"] + }, + "check": { + "cache": false + }, + "check:fix": { + "cache": false } } }