Skip to content

Commit 2449b18

Browse files
authored
feat: Add load toolset method (#17)
* feat: Add basic client and tool * cleanup * clean * add unit tests * increase test cov * test file cleanup * clean * small fix * Added unit tests for tool * cleanup * test cleanup * new tests * del e2e file * test * chore: lint (#11) * lint * lint * lint * move files to different PR * move files to different pr * move files to correct pr * lint * lint * lint * lint * lint * fix test file * lint * rebase * Update protocol.ts * package file cleanup * try * fix config * lint fix * cleanup * clean * fix tests * lint * lint * fix * fix config * fix * fix tests * lint * fix tests * fix docstrings * fix e2e tests * Update test.e2e.ts * feat: add load toolset method * Update client.ts * lint * add e2e tests * lint * Update integration.cloudbuild.yaml * fix test * move files to correct pr * lint * lint * lint * lint * fix test file * lint * fix docstrings * Update client.ts * lint * fix any type errors * lint * clean * remove any * "any" type lint * fix method * change any to unknown * lint * remove unused deps * move deps to correct pr * rename methods * lint * fix method name * rename methods * resolve comments * lint * use parse instead of safeparse * use parse instead of safeparse * any issue fix * any lint * fix any type issue * resolve comments * correct type annotations * rename methods correctly * move from safeparse to parse in tests * update unit tests * fix tests * modularise code * lint * better error handling for manifest parsing * fix type annotations * lint
1 parent 74bf295 commit 2449b18

File tree

3 files changed

+418
-59
lines changed

3 files changed

+418
-59
lines changed

packages/toolbox-core/src/toolbox_core/client.ts

Lines changed: 99 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ import axios from 'axios';
1717
import {type AxiosInstance, type AxiosResponse} from 'axios';
1818
import {ZodManifestSchema, createZodSchemaFromParams} from './protocol';
1919
import {logApiError} from './errorUtils';
20+
import {ZodError} from 'zod';
21+
22+
type Manifest = import('zod').infer<typeof ZodManifestSchema>;
23+
type ToolSchemaFromManifest = Manifest['tools'][string];
2024

2125
/**
2226
* An asynchronous client for interacting with a Toolbox service.
@@ -34,62 +38,119 @@ class ToolboxClient {
3438
*/
3539
constructor(url: string, session?: AxiosInstance) {
3640
this._baseUrl = url;
37-
this._session = session || axios.create({baseURL: url});
41+
this._session = session || axios.create({baseURL: this._baseUrl});
3842
}
3943

4044
/**
41-
* Asynchronously loads a tool from the server.
42-
* Retrieves the schema for the specified tool from the Toolbox server and
43-
* returns a callable (`ToolboxTool`) that can be used to invoke the
44-
* tool remotely.
45-
*
46-
* @param {string} name - The unique name or identifier of the tool to load.
47-
* @returns {Promise<ReturnType<typeof ToolboxTool>>} A promise that resolves
48-
* to a ToolboxTool function, ready for execution.
49-
* @throws {Error} If the tool is not found in the manifest, the manifest structure is invalid,
50-
* or if there's an error fetching data from the API.
45+
* Fetches and parses the manifest from a given API path.
46+
* @param {string} apiPath - The API path to fetch the manifest from (e.g., "/api/tool/mytool").
47+
* @returns {Promise<Manifest>} A promise that resolves to the parsed manifest.
48+
* @throws {Error} If there's an error fetching data or if the manifest structure is invalid.
49+
* @private
5150
*/
52-
async loadTool(name: string): Promise<ReturnType<typeof ToolboxTool>> {
53-
const url = `${this._baseUrl}/api/tool/${name}`;
51+
private async _fetchAndParseManifest(apiPath: string): Promise<Manifest> {
52+
const url = `${this._baseUrl}${apiPath}`;
5453
try {
5554
const response: AxiosResponse = await this._session.get(url);
5655
const responseData = response.data;
5756

5857
try {
5958
const manifest = ZodManifestSchema.parse(responseData);
60-
if (
61-
manifest.tools &&
62-
Object.prototype.hasOwnProperty.call(manifest.tools, name)
63-
) {
64-
const specificToolSchema = manifest.tools[name];
65-
const paramZodSchema = createZodSchemaFromParams(
66-
specificToolSchema.parameters
67-
);
68-
return ToolboxTool(
69-
this._session,
70-
this._baseUrl,
71-
name,
72-
specificToolSchema.description,
73-
paramZodSchema
74-
);
75-
} else {
76-
throw new Error(`Tool "${name}" not found in manifest.`);
77-
}
59+
return manifest;
7860
} catch (validationError) {
79-
if (validationError instanceof Error) {
80-
throw new Error(
81-
`Invalid manifest structure received: ${validationError.message}`
82-
);
61+
let detailedMessage = `Invalid manifest structure received from ${url}: `;
62+
if (validationError instanceof ZodError) {
63+
const issueDetails = validationError.issues;
64+
detailedMessage += JSON.stringify(issueDetails, null, 2);
65+
} else if (validationError instanceof Error) {
66+
detailedMessage += validationError.message;
67+
} else {
68+
detailedMessage += 'Unknown validation error.';
8369
}
84-
throw new Error(
85-
'Invalid manifest structure received: Unknown validation error.'
86-
);
70+
throw new Error(detailedMessage);
8771
}
8872
} catch (error) {
73+
if (
74+
error instanceof Error &&
75+
error.message.startsWith('Invalid manifest structure received from')
76+
) {
77+
throw error;
78+
}
8979
logApiError(`Error fetching data from ${url}:`, error);
9080
throw error;
9181
}
9282
}
83+
84+
/**
85+
* Creates a ToolboxTool instance from its schema.
86+
* @param {string} toolName - The name of the tool.
87+
* @param {ToolSchemaFromManifest} toolSchema - The schema definition of the tool from the manifest.
88+
* @returns {ReturnType<typeof ToolboxTool>} A ToolboxTool function.
89+
* @private
90+
*/
91+
private _createToolInstance(
92+
toolName: string,
93+
toolSchema: ToolSchemaFromManifest
94+
): ReturnType<typeof ToolboxTool> {
95+
const paramZodSchema = createZodSchemaFromParams(toolSchema.parameters);
96+
return ToolboxTool(
97+
this._session,
98+
this._baseUrl,
99+
toolName,
100+
toolSchema.description,
101+
paramZodSchema
102+
);
103+
}
104+
105+
/**
106+
* Asynchronously loads a tool from the server.
107+
* Retrieves the schema for the specified tool from the Toolbox server and
108+
* returns a callable (`ToolboxTool`) that can be used to invoke the
109+
* tool remotely.
110+
*
111+
* @param {string} name - The unique name or identifier of the tool to load.
112+
* @returns {Promise<ReturnType<typeof ToolboxTool>>} A promise that resolves
113+
* to a ToolboxTool function, ready for execution.
114+
* @throws {Error} If the tool is not found in the manifest, the manifest structure is invalid,
115+
* or if there's an error fetching data from the API.
116+
*/
117+
async loadTool(name: string): Promise<ReturnType<typeof ToolboxTool>> {
118+
const apiPath = `/api/tool/${name}`;
119+
const manifest = await this._fetchAndParseManifest(apiPath);
120+
121+
if (
122+
manifest.tools && // Zod ensures manifest.tools exists if schema requires it
123+
Object.prototype.hasOwnProperty.call(manifest.tools, name)
124+
) {
125+
const specificToolSchema = manifest.tools[name];
126+
return this._createToolInstance(name, specificToolSchema);
127+
} else {
128+
throw new Error(`Tool "${name}" not found in manifest from ${apiPath}.`);
129+
}
130+
}
131+
132+
/**
133+
* Asynchronously fetches a toolset and loads all tools defined within it.
134+
*
135+
* @param {string | null} [name] - Name of the toolset to load. If null or undefined, loads the default toolset.
136+
* @returns {Promise<Array<ReturnType<typeof ToolboxTool>>>} A promise that resolves
137+
* to a list of ToolboxTool functions, ready for execution.
138+
* @throws {Error} If the manifest structure is invalid or if there's an error fetching data from the API.
139+
*/
140+
async loadToolset(
141+
name?: string
142+
): Promise<Array<ReturnType<typeof ToolboxTool>>> {
143+
const toolsetName = name || '';
144+
const apiPath = `/api/toolset/${toolsetName}`;
145+
const manifest = await this._fetchAndParseManifest(apiPath);
146+
const tools: Array<ReturnType<typeof ToolboxTool>> = [];
147+
148+
for (const [toolName, toolSchema] of Object.entries(manifest.tools)) {
149+
const toolInstance = this._createToolInstance(toolName, toolSchema);
150+
tools.push(toolInstance);
151+
}
152+
return tools;
153+
}
93154
}
94155

95156
export {ToolboxClient};

packages/toolbox-core/test/e2e/test.e2e.ts

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ import {ToolboxTool} from '../../src/toolbox_core/tool';
1818
describe('ToolboxClient E2E Tests', () => {
1919
let commonToolboxClient: ToolboxClient;
2020
let getNRowsTool: ReturnType<typeof ToolboxTool>;
21+
const testBaseUrl = 'http://localhost:5000';
2122

2223
beforeAll(async () => {
23-
commonToolboxClient = new ToolboxClient('http://localhost:5000');
24+
commonToolboxClient = new ToolboxClient(testBaseUrl);
2425
});
2526

2627
beforeEach(async () => {
@@ -50,4 +51,80 @@ describe('ToolboxClient E2E Tests', () => {
5051
);
5152
});
5253
});
54+
55+
describe('loadToolset', () => {
56+
const specificToolsetTestCases = [
57+
{
58+
name: 'my-toolset',
59+
expectedLength: 1,
60+
expectedTools: ['get-row-by-id'],
61+
},
62+
{
63+
name: 'my-toolset-2',
64+
expectedLength: 2,
65+
expectedTools: ['get-n-rows', 'get-row-by-id'],
66+
},
67+
];
68+
69+
specificToolsetTestCases.forEach(testCase => {
70+
it(`should successfully load the specific toolset "${testCase.name}"`, async () => {
71+
const loadedTools = await commonToolboxClient.loadToolset(
72+
testCase.name
73+
);
74+
75+
expect(Array.isArray(loadedTools)).toBe(true);
76+
expect(loadedTools.length).toBe(testCase.expectedLength);
77+
78+
const loadedToolNames = new Set(
79+
loadedTools.map(tool => tool.getName())
80+
);
81+
expect(loadedToolNames).toEqual(new Set(testCase.expectedTools));
82+
83+
for (const tool of loadedTools) {
84+
expect(typeof tool).toBe('function');
85+
expect(tool.getName).toBeInstanceOf(Function);
86+
expect(tool.getDescription).toBeInstanceOf(Function);
87+
expect(tool.getParamSchema).toBeInstanceOf(Function);
88+
}
89+
});
90+
});
91+
92+
it('should successfully load the default toolset (all tools)', async () => {
93+
const loadedTools = await commonToolboxClient.loadToolset(); // Load the default toolset (no name provided)
94+
expect(Array.isArray(loadedTools)).toBe(true);
95+
expect(loadedTools.length).toBeGreaterThan(0);
96+
const getNRowsToolFromSet = loadedTools.find(
97+
tool => tool.getName() === 'get-n-rows'
98+
);
99+
100+
expect(getNRowsToolFromSet).toBeDefined();
101+
expect(typeof getNRowsToolFromSet).toBe('function');
102+
expect(getNRowsToolFromSet?.getName()).toBe('get-n-rows');
103+
expect(getNRowsToolFromSet?.getDescription()).toBeDefined();
104+
expect(getNRowsToolFromSet?.getParamSchema()).toBeDefined();
105+
106+
const loadedToolNames = new Set(loadedTools.map(tool => tool.getName()));
107+
const expectedDefaultTools = new Set([
108+
'get-row-by-content-auth',
109+
'get-row-by-email-auth',
110+
'get-row-by-id-auth',
111+
'get-row-by-id',
112+
'get-n-rows',
113+
]);
114+
expect(loadedToolNames).toEqual(expectedDefaultTools);
115+
116+
for (const tool of loadedTools) {
117+
expect(typeof tool).toBe('function');
118+
expect(tool.getName).toBeInstanceOf(Function);
119+
expect(tool.getDescription).toBeInstanceOf(Function);
120+
expect(tool.getParamSchema).toBeInstanceOf(Function);
121+
}
122+
});
123+
124+
it('should throw an error when trying to load a non-existent toolset', async () => {
125+
await expect(
126+
commonToolboxClient.loadToolset('non-existent-toolset')
127+
).rejects.toThrow('Request failed with status code 404');
128+
});
129+
});
53130
});

0 commit comments

Comments
 (0)