Skip to content

Commit 16a9870

Browse files
authored
add bitte tool execution, debug logger, inc threshold (#2)
* add bitte tool execution, debug logger, inc threshold * update format / lint commands
1 parent 1898e62 commit 16a9870

File tree

9 files changed

+200
-57
lines changed

9 files changed

+200
-57
lines changed

.github/workflows/cicd.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ jobs:
2525

2626
- name: Check format and lint
2727
run: |
28-
bun run format
29-
bun run lint
28+
bun run check
3029
3130
# TODO: tests
3231

README.md

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,20 +51,12 @@ bun run start
5151
bun run dev
5252
```
5353

54-
### Lint your code
54+
### Format and lint your code
5555

5656
```bash
57-
bun run lint
57+
bun run check
5858
# To fix automatically:
59-
bun run lint:fix
60-
```
61-
62-
### Format your code
63-
64-
```bash
65-
bun run format
66-
# To fix automatically:
67-
bun run format:fix
59+
bun run check:fix
6860
```
6961

7062
### Run individual services

apps/bitte-ai/index.ts

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { FastMCP } from 'fastmcp';
22
import { z } from 'zod';
3+
import { type PluginToolSpec, createToolFromPluginSpec } from './lib/bitte-plugins';
34
import { searchAgents, searchAgentsSchema, searchTools, searchToolsSchema } from './lib/search';
4-
import { services } from './tools';
55
import { callBitteAPI } from './utils/bitte';
66
// Export configuration
77
export { config } from './config';
@@ -24,6 +24,22 @@ export interface ExecuteAgentParams {
2424
input: string;
2525
}
2626

27+
// Create a log wrapper that also logs to console
28+
function wrapLogger(log: any) {
29+
return new Proxy(log, {
30+
get(target, prop) {
31+
const originalMethod = target[prop];
32+
if (typeof originalMethod === 'function') {
33+
return (...args: any[]) => {
34+
console.log(`[${String(prop)}]`, ...args);
35+
return originalMethod.apply(target, args);
36+
};
37+
}
38+
return originalMethod;
39+
},
40+
});
41+
}
42+
2743
// Create and export the server
2844
export const server = new FastMCP({
2945
name: 'bitte-ai-mcp-proxy',
@@ -49,9 +65,10 @@ server.addTool({
4965
agentId: z.string().describe('ID of the agent to retrieve'),
5066
}),
5167
execute: async (args, { log }) => {
52-
log.info(`Getting agent with ID: ${args.agentId}`);
68+
const wrappedLog = wrapLogger(log);
69+
wrappedLog.info(`Getting agent with ID: ${args.agentId}`);
5370
const endpoint = `/api/agents/${args.agentId}`;
54-
const data = await callBitteAPI(endpoint, 'GET', undefined, log);
71+
const data = await callBitteAPI(endpoint, 'GET', undefined, wrappedLog);
5572
return JSON.stringify(data);
5673
},
5774
});
@@ -64,7 +81,8 @@ server.addTool({
6481
input: z.string().describe('Input to the agent'),
6582
}),
6683
execute: async (args, { log, session }) => {
67-
log.info(`Executing agent with ID: ${args.agentId}`);
84+
const wrappedLog = wrapLogger(log);
85+
wrappedLog.info(`Executing agent with ID: ${args.agentId}`);
6886

6987
try {
7088
// First, search for the agent to make sure it exists
@@ -73,7 +91,7 @@ server.addTool({
7391
query: args.agentId,
7492
threshold: 0.1, // Lower threshold for more exact matching
7593
},
76-
log
94+
wrappedLog
7795
);
7896

7997
// Check if we found a matching agent
@@ -90,7 +108,7 @@ server.addTool({
90108
};
91109

92110
// Call the Bitte API to execute the agent
93-
const data = await callBitteAPI('/chat', 'POST', body, log);
111+
const data = await callBitteAPI('/chat', 'POST', body, wrappedLog);
94112

95113
if (typeof data === 'string') {
96114
return {
@@ -104,7 +122,7 @@ server.addTool({
104122
};
105123
} catch (error) {
106124
const errorMessage = error instanceof Error ? error.message : String(error);
107-
log.error(`Error executing agent: ${errorMessage}`);
125+
wrappedLog.error(`Error executing agent: ${errorMessage}`);
108126
return {
109127
content: [{ type: 'text', text: `Error executing agent: ${errorMessage}` }],
110128
isError: true,
@@ -120,11 +138,16 @@ server.addTool({
120138
parameters: z.object({
121139
tool: z.string().describe('The tool to execute'),
122140
params: z.string().describe('The parameters to pass to the tool as a JSON string'),
141+
metadata: z
142+
.object({})
143+
.describe(
144+
'Optional metadata to pass to the tool i.e. {accountId: "123", evmAddress: "0x123"}'
145+
)
146+
.optional(),
123147
}),
124148
execute: async (args, { log }) => {
125-
log.info(`Executing execute-tool tool with params: ${JSON.stringify(args)}`);
126-
console.log('execute-tool with args', JSON.stringify(args));
127-
console.log('args', args);
149+
const wrappedLog = wrapLogger(log);
150+
wrappedLog.info(`Executing execute-tool tool with params: ${JSON.stringify(args)}`);
128151

129152
try {
130153
// Use searchTools to find the specified tool
@@ -133,25 +156,36 @@ server.addTool({
133156
query: args.tool,
134157
threshold: 0.1, // Lower threshold for more exact matching
135158
},
136-
log
159+
wrappedLog
137160
);
138161

139162
// Get the first (best) match
140163
const toolMatch = searchResult.combinedResults[0];
164+
141165
if (!toolMatch) {
142166
throw new Error(`Tool '${args.tool}' not found`);
143167
}
144168

145169
const tool = toolMatch.item as {
146170
execute?: (params: Record<string, unknown>) => Promise<unknown>;
171+
execution?: { baseUrl: string; path: string; httpMethod: string };
172+
function?: { name: string; description: string; parameters?: any };
147173
};
148174

149-
if (!tool || typeof tool.execute !== 'function') {
175+
let result: unknown;
176+
177+
// Check if the tool has an execution field
178+
if (tool.execution && tool.function) {
179+
// Create and execute a core tool with HTTP-based execution
180+
const coreTool = createToolFromPluginSpec(tool as PluginToolSpec, args.metadata);
181+
result = await coreTool.execute(JSON.parse(args.params));
182+
} else if (tool.execute && typeof tool.execute === 'function') {
183+
// Use the tool's execute method directly
184+
result = await tool.execute(JSON.parse(args.params));
185+
} else {
150186
throw new Error(`Tool '${args.tool}' found but cannot be executed`);
151187
}
152188

153-
const result = await tool.execute(JSON.parse(args.params));
154-
155189
// Ensure we return a properly typed result
156190
if (typeof result === 'string') {
157191
return {
@@ -164,7 +198,7 @@ server.addTool({
164198
};
165199
} catch (error) {
166200
const errorMessage = error instanceof Error ? error.message : String(error);
167-
log.error(`Error executing tool: ${errorMessage}`);
201+
wrappedLog.error(`Error executing tool: ${errorMessage}`);
168202
return {
169203
content: [{ type: 'text', text: `Error executing tool: ${errorMessage}` }],
170204
isError: true,
@@ -179,13 +213,14 @@ server.addTool({
179213
description: 'Search for AI agents across Bitte API and other services',
180214
parameters: searchAgentsSchema,
181215
execute: async (args, { log }) => {
182-
log.info(`Searching agents with params: ${JSON.stringify(args)}`);
216+
const wrappedLog = wrapLogger(log);
217+
wrappedLog.info(`Searching agents with params: ${JSON.stringify(args)}`);
183218
try {
184-
const result = await searchAgents(args, log);
219+
const result = await searchAgents(args, wrappedLog);
185220
return JSON.stringify(result);
186221
} catch (error) {
187222
const errorMessage = error instanceof Error ? error.message : String(error);
188-
log.error(`Error searching agents: ${errorMessage}`);
223+
wrappedLog.error(`Error searching agents: ${errorMessage}`);
189224
return {
190225
content: [{ type: 'text', text: `Error searching agents: ${errorMessage}` }],
191226
isError: true,
@@ -200,13 +235,13 @@ server.addTool({
200235
description: 'Search for tools across Bitte API and other services',
201236
parameters: searchToolsSchema,
202237
execute: async (args, { log }) => {
203-
log.info(`Searching tools with params: ${JSON.stringify(args)}`);
238+
const wrappedLog = wrapLogger(log);
204239
try {
205-
const result = await searchTools(args, log);
240+
const result = await searchTools(args, wrappedLog);
206241
return JSON.stringify(result);
207242
} catch (error) {
208243
const errorMessage = error instanceof Error ? error.message : String(error);
209-
log.error(`Error searching tools: ${errorMessage}`);
244+
wrappedLog.error(`Error searching tools: ${errorMessage}`);
210245
return {
211246
content: [{ type: 'text', text: `Error searching tools: ${errorMessage}` }],
212247
isError: true,

apps/bitte-ai/lib/bitte-plugins.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import type { z } from 'zod';
2+
3+
export interface ExecutionSpec {
4+
baseUrl: string;
5+
path: string;
6+
httpMethod: string;
7+
}
8+
9+
export interface FunctionSpec {
10+
name: string;
11+
description: string;
12+
parameters?: z.ZodObject<any>;
13+
}
14+
15+
export interface PluginToolSpec {
16+
function: FunctionSpec;
17+
execution: ExecutionSpec;
18+
}
19+
20+
export interface BitteMetadata {
21+
[key: string]: any;
22+
}
23+
24+
// Type for tool execution function
25+
export type BitteToolExecutor = (
26+
args: Record<string, unknown>
27+
) => Promise<{ data?: unknown; error?: string }>;
28+
29+
// Helper function to extract error messages
30+
export function getErrorMsg(error: unknown): string {
31+
if (error instanceof Error) {
32+
return error.message;
33+
}
34+
return String(error);
35+
}
36+
37+
export const createExecutor = (
38+
tool: PluginToolSpec,
39+
metadata?: BitteMetadata
40+
): BitteToolExecutor => {
41+
return async (args) => {
42+
try {
43+
const { baseUrl, path, httpMethod } = tool.execution;
44+
const fullBaseUrl = baseUrl.startsWith('http') ? baseUrl : `https://${baseUrl}`;
45+
46+
// Build URL with path parameters
47+
let url = `${fullBaseUrl}${path}`;
48+
const remainingArgs = { ...args };
49+
50+
url = url.replace(/\{(\w+)\}/g, (_, key) => {
51+
if (remainingArgs[key] === undefined) {
52+
throw new Error(`Missing required path parameter: ${key}`);
53+
}
54+
const value = remainingArgs[key];
55+
delete remainingArgs[key];
56+
return encodeURIComponent(String(value));
57+
});
58+
59+
// Setup request
60+
const headers: Record<string, string> = {};
61+
if (metadata) {
62+
headers['mb-metadata'] = JSON.stringify(metadata);
63+
}
64+
65+
const method = httpMethod.toUpperCase();
66+
const fetchOptions: RequestInit = { method, headers };
67+
68+
// Handle query parameters
69+
const queryParams = new URLSearchParams();
70+
for (const [key, value] of Object.entries(remainingArgs)) {
71+
if (value != null) {
72+
queryParams.append(key, String(value));
73+
}
74+
}
75+
76+
const queryString = queryParams.toString();
77+
if (queryString) {
78+
url += (url.includes('?') ? '&' : '?') + queryString;
79+
}
80+
81+
// Handle request body
82+
if (['POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'].includes(method)) {
83+
headers['Content-Type'] = 'application/json';
84+
fetchOptions.body = JSON.stringify(remainingArgs);
85+
}
86+
87+
// Execute request
88+
const response = await fetch(url, fetchOptions);
89+
90+
if (!response.ok) {
91+
throw new Error(
92+
`HTTP error during plugin tool execution: ${response.status} ${response.statusText}`
93+
);
94+
}
95+
// Parse response based on content type
96+
const contentType = response.headers.get('Content-Type') || '';
97+
const data = await (contentType.includes('application/json')
98+
? response.json()
99+
: contentType.includes('text')
100+
? response.text()
101+
: response.blob());
102+
103+
return { data };
104+
} catch (error) {
105+
return {
106+
error: `Error executing pluginTool ${tool.function.name}. ${getErrorMsg(error)}`,
107+
};
108+
}
109+
};
110+
};
111+
112+
export const createToolFromPluginSpec = (func: PluginToolSpec, metadata?: BitteMetadata) => {
113+
return {
114+
...func.function,
115+
execute: async (args: Record<string, unknown>) => createExecutor(func, metadata)(args),
116+
};
117+
};

0 commit comments

Comments
 (0)