diff --git a/.cursor/rules/examples-standards.mdc b/.cursor/rules/examples-standards.mdc index 314c2834..20ede611 100644 --- a/.cursor/rules/examples-standards.mdc +++ b/.cursor/rules/examples-standards.mdc @@ -1,6 +1,7 @@ --- -description: Standards for creating and maintaining examples for all functionality +description: Standards for creating and maintaining examples in the StackOne repository globs: examples/* +alwaysApply: false --- # Examples Standards @@ -23,60 +24,82 @@ actions: ``` examples/ ├── basic_usage/ - │ ├── basic_tool_usage.py # Basic usage examples - │ └── error_handling.py # Error handling examples + │ ├── basic_tool_usage.ts # Basic usage examples + │ └── error_handling.ts # Error handling examples ├── integrations/ # Integration examples - │ ├── openai_integration.py - │ └── other_integration.py + │ ├── openai_integration.ts + │ └── other_integration.ts └── README.md # Examples documentation ``` 2. Example Requirements: - Every public function/class needs at least one example - - Examples should be runnable Python scripts - - Include error handling cases - - Load credentials from .env - - Include type hints + - Examples should be runnable TypeScript scripts + - Use Node's assert module for validation instead of console.logs + - Include proper error handling with try/catch blocks + - Include TypeScript return types for all functions - Follow the same code style as the main codebase + - Exit with non-zero code on error using process.exit(1) 3. Documentation: - Each example file should start with a docstring explaining its purpose - - Include expected output in comments + - Use assertions to validate expected behavior - Document any prerequisites (environment variables, etc) 4. Testing: - Examples should be tested as part of CI - Examples should work with the latest package version - - Include sample responses in comments + - Use assertions to verify expected behavior examples: - input: | - # Good example structure - import os - from dotenv import load_dotenv - from stackone_ai import StackOneToolSet - - def main(): - """Example showing basic usage of StackOneToolSet.""" - load_dotenv() - - api_key = os.getenv("STACKONE_API_KEY") - if not api_key: - raise ValueError("STACKONE_API_KEY not found") - - # Example code... - - if __name__ == "__main__": - main() + /** + * Example showing basic usage of StackOneToolSet. + */ + + import assert from 'node:assert'; + import { StackOneToolSet } from '../src'; + + const exampleFunction = async (): Promise => { + // Initialize the toolset + const toolset = new StackOneToolSet(); + + // Get tools and verify + const tools = toolset.getTools('hris_*'); + assert(tools.length > 0, 'Expected to find HRIS tools'); + + // Use a specific tool + const employeeTool = tools.getTool('hris_list_employees'); + assert(employeeTool !== undefined, 'Expected to find tool'); + + // Execute and verify result + const result = await employeeTool.execute(); + assert(Array.isArray(result), 'Expected result to be an array'); + }; + + // Run the example + exampleFunction().catch((error) => { + console.error('Error:', error); + process.exit(1); + }); output: "Correctly structured example" - input: | - # Bad example - missing error handling, docs, types - from stackone_ai import StackOneToolSet - - toolset = StackOneToolSet("hardcoded_key") - tools = toolset.get_tools("crm") - result = tools["some_tool"].execute() + // Bad example - missing assertions, error handling, types + import { StackOneToolSet } from '../src'; + + const badExample = async () => { + const toolset = new StackOneToolSet(); + const tools = toolset.getTools("hris_*"); + const tool = tools.getTool("hris_list_employees"); + + if (tool) { + const result = await tool.execute(); + console.log(result); + } + }; + + badExample(); output: "Incorrectly structured example" metadata: diff --git a/README.md b/README.md index 158d97ae..cee9c455 100644 --- a/README.md +++ b/README.md @@ -25,79 +25,84 @@ export STACKONE_API_KEY= or load from a .env file using your preferred environment variable library. -## Account IDs - -StackOne uses account IDs to identify different integrations. You can specify the account ID when initializing the SDK or when getting tools. - ## Quickstart ```typescript import { StackOneToolSet } from "@stackone/ai"; -// Initialize with API key from environment variable const toolset = new StackOneToolSet(); +const tools = toolset.getTools("hris_*", "your-account-id"); +const employeeTool = tools.getTool("hris_list_employees"); +const employees = await employeeTool.execute(); +``` -// Or initialize with explicit API key -// const toolset = new StackOneToolSet('your-api-key'); +[View full example](examples/index.ts) -// Get all HRIS-related tools -const accountId = "45072196112816593343"; -const tools = toolset.getTools("hris_*", accountId); +## Account IDs -// Use a specific tool -const employeeTool = tools.getTool("hris_list_employees"); -if (employeeTool) { - const employees = await employeeTool.execute(); - console.log(employees); -} +StackOne uses account IDs to identify different integrations. You can specify the account ID at different levels: + +```typescript +import { StackOneToolSet } from "@stackone/ai"; + +// Method 1: Set at toolset initialization +const toolset = new StackOneToolSet({ accountId: "your-account-id" }); + +// Method 2: Set when getting tools (overrides toolset account ID) +const tools = toolset.getTools("hris_*", "override-account-id"); + +// Method 3: Set directly on a tool instance +tool.setAccountId("direct-account-id"); +const currentAccountId = tool.getAccountId(); // Get the current account ID ``` -## Custom Base URL +[View full example](examples/account-id-usage.ts) -You can specify a custom base URL when initializing the SDK. This is useful for testing against development APIs or working with self-hosted StackOne instances. +## Custom Base URL ```typescript import { StackOneToolSet } from "@stackone/ai"; -// Initialize with a custom base URL -const toolset = new StackOneToolSet( - "your-api-key", - "your-account-id", - "https://api.example-dev.com" -); - -// Get tools with the custom base URL +const toolset = new StackOneToolSet({ baseUrl: "https://api.example-dev.com" }); const tools = toolset.getTools("hris_*"); - -// All API requests will use the custom base URL const employeeTool = tools.getTool("hris_list_employees"); -if (employeeTool) { - const employees = await employeeTool.execute(); - console.log(employees); -} ``` -## File Uploads +[View full example](examples/custom-base-url.ts) -The SDK supports file uploads for tools that accept file parameters. File uploads have been simplified to use a single `file_path` parameter: +## File Uploads ```typescript import { StackOneToolSet } from "@stackone/ai"; -import * as path from "path"; -// Initialize with API key and account ID const toolset = new StackOneToolSet(); -const tools = toolset.getTools("documents_*"); -const uploadTool = tools.getTool("documents_upload_file"); - -// Upload a file using the file_path parameter -const result = await uploadTool.execute({ - file_path: "/path/to/document.pdf", // Path to the file +const uploadTool = toolset + .getTools("hris_*") + .getTool("hris_upload_employee_document"); +await uploadTool.execute({ + file_path: "/path/to/document.pdf", + id: "employee-id", }); +``` + +[View full example](examples/file-uploads.ts) -The name, file format, and content of the file are automatically extracted from the path. +## Error Handling + +```typescript +import { StackOneAPIError, StackOneError } from "@stackone/ai"; + +try { + await tool.execute(); +} catch (error) { + if (error instanceof StackOneAPIError) { + // Handle API errors + } +} ``` +[View full example](examples/error-handling.ts) + ## Integrations ### OpenAI @@ -106,19 +111,13 @@ The name, file format, and content of the file are automatically extracted from import { OpenAI } from "openai"; import { StackOneToolSet } from "@stackone/ai"; -const openai = new OpenAI(); const toolset = new StackOneToolSet(); - -const tools = toolset.getTools("hris_*"); -const tool = tools.getTool("hris_list_employees"); - -const result = await completionTool.execute({ - model: "gpt-4o-mini", - prompt: "What are the names of the employees?", - tools: [tool.toOpenAI()], -}); +const openAITools = toolset.getTools("hris_*").toOpenAI(); +await openai.chat.completions.create({ tools: openAITools }); ``` +[View full example](examples/openai-integration.ts) + ### AI SDK by Vercel ```typescript @@ -126,18 +125,9 @@ import { openai } from "@ai-sdk/openai"; import { generateText } from "ai"; import { StackOneToolSet } from "@stackone/ai"; -// Initialize StackOne const toolset = new StackOneToolSet(); -const tools = toolset.getTools("hris_*", "your-account-id"); - -// Convert to AI SDK tools -const aiSdkTools = tools.toAISDKTools(); - -// Use with AI SDK -const { text } = await generateText({ - model: openai("gpt-4o-mini"), - tools: aiSdkTools, - prompt: "Get employee details for John Doe", - maxSteps: 3, // Automatically calls tools when needed -}); +const aiSdkTools = toolset.getTools("hris_*").toAISDK(); +await generateText({ tools: aiSdkTools, maxSteps: 3 }); ``` + +[View full example](examples/ai-sdk-integration.ts) diff --git a/biome.json b/biome.json index 01afb654..6acd1d28 100644 --- a/biome.json +++ b/biome.json @@ -38,6 +38,6 @@ }, "files": { "ignoreUnknown": true, - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts", "examples/**/*.ts"] } } diff --git a/examples/account-id-usage.ts b/examples/account-id-usage.ts new file mode 100644 index 00000000..ff995398 --- /dev/null +++ b/examples/account-id-usage.ts @@ -0,0 +1,50 @@ +#!/usr/bin/env bun +/** + * Example demonstrating different ways to set the account ID when using StackOne tools. + * + * This example shows: + * 1. Setting account ID when initializing the toolset + * 2. Setting account ID when getting tools + * 3. Using setAccountId method directly on a tool + * + * Usage: + * + * ```bash + * bun run examples/account-id-usage.ts + * ``` + */ + +import assert from 'node:assert'; +import { StackOneToolSet } from '../src'; + +const accountIdUsage = async (): Promise => { + // Set account ID from toolset initialization + const toolset = new StackOneToolSet({ accountId: 'initial-account-id' }); + + const tools = toolset.getTools('hris_*'); + const employeeTool = tools.getTool('hris_list_employees'); + + assert( + employeeTool?.getAccountId() === 'initial-account-id', + 'Account ID should match what was set' + ); + + // Setting account ID when getting tools (overrides toolset account ID) + const toolsWithOverride = toolset.getTools('hris_*', 'override-account-id'); + const employeeToolWithOverride = toolsWithOverride.getTool('hris_list_employees'); + + assert( + employeeToolWithOverride?.getAccountId() === 'override-account-id', + 'Account ID should match what was set' + ); + + // Set the account ID directly on the tool + employeeTool.setAccountId('direct-account-id'); + + assert( + employeeTool.getAccountId() === 'direct-account-id', + 'Account ID should match what was set' + ); +}; + +accountIdUsage(); diff --git a/examples/ai-sdk-integration.ts b/examples/ai-sdk-integration.ts index 71d4ae9c..bb8deaf3 100644 --- a/examples/ai-sdk-integration.ts +++ b/examples/ai-sdk-integration.ts @@ -16,7 +16,7 @@ const aiSdkIntegration = async (): Promise => { const tools = toolset.getTools('hris_*', accountId); // Convert to AI SDK tools - const aiSdkTools = tools.toAISDKTools(); + const aiSdkTools = tools.toAISDK(); // Use max steps to automatically call the tool if it's needed const { text } = await generateText({ @@ -29,4 +29,4 @@ const aiSdkIntegration = async (): Promise => { assert(text.includes('Isac Newton'), 'Expected employee name to be included in the response'); }; -aiSdkIntegration().catch(console.error); +aiSdkIntegration(); diff --git a/examples/custom-base-url.ts b/examples/custom-base-url.ts index 94243d5c..36d7cc63 100644 --- a/examples/custom-base-url.ts +++ b/examples/custom-base-url.ts @@ -13,6 +13,7 @@ * ``` */ +import assert from 'node:assert'; import { StackOneToolSet } from '../src'; const customBaseUrl = async (): Promise => { @@ -22,48 +23,41 @@ const customBaseUrl = async (): Promise => { const defaultToolset = new StackOneToolSet(); const hrisTools = defaultToolset.getTools('hris_*'); - console.log(`Found ${hrisTools.length} HRIS tools with default base URL`); + assert(hrisTools.length > 0, 'Should have at least one HRIS tool'); const defaultTool = hrisTools.getTool('hris_get_employee'); - if (defaultTool) { - console.log(`Default tool URL: ${defaultTool._executeConfig.url}`); - // Should start with https://api.stackone.com + if (!defaultTool) { + throw new Error('Tool not found'); } /** * Custom base URL */ - const devToolset = new StackOneToolSet( - process.env.STACKONE_API_KEY, - process.env.STACKONE_ACCOUNT_ID, - 'https://api.example-dev.com' - ); + const devToolset = new StackOneToolSet({ + baseUrl: 'https://api.example-dev.com', + }); + const devHrisTools = devToolset.getTools('hris_*'); - console.log(`Found ${devHrisTools.length} HRIS tools with custom base URL`); + assert(devHrisTools.length > 0, 'Should have at least one HRIS tool'); const devTool = devHrisTools.getTool('hris_get_employee'); - if (devTool) { - console.log(`Custom tool URL: ${devTool._executeConfig.url}`); - // Should start with https://api.example-dev.com + if (!devTool) { + throw new Error('Tool not found'); } /** * Note this uses the same tools but substitutes the base URL */ if (defaultTool && devTool) { - console.assert(defaultTool.name === devTool.name, 'Tool names should be the same'); - console.assert( + assert(defaultTool.name === devTool.name, 'Tool names should be the same'); + assert( defaultTool._executeConfig.url.includes('https://api.stackone.com'), 'Default tool should use the default base URL' ); - console.assert( + assert( devTool._executeConfig.url.includes('https://api.example-dev.com'), 'Custom tool should use the custom base URL' ); } }; -// Run the example -customBaseUrl().catch((error) => { - console.error('Error:', error); - process.exit(1); -}); +customBaseUrl(); diff --git a/examples/error-handling.ts b/examples/error-handling.ts index 08c9272b..78781af3 100644 --- a/examples/error-handling.ts +++ b/examples/error-handling.ts @@ -4,37 +4,31 @@ * This example shows how to handle errors when using the StackOne SDK. */ -// Load environment variables from .env file -import * as dotenv from 'dotenv'; -dotenv.config(); - +import assert from 'node:assert'; import { StackOneAPIError, StackOneError, StackOneToolSet, ToolsetConfigError } from '../src'; const errorHandling = async (): Promise => { - try { - // Example 1: Handle initialization errors - console.log('Example 1: Handle initialization errors'); - try { - // Temporarily save the API key - const originalKey = process.env.STACKONE_API_KEY; - // Delete the API key to force an error - process.env.STACKONE_API_KEY = undefined; + // Example 1: Handle initialization errors + const testInitializationErrors = async (): Promise => { + // Temporarily save the API key + const originalKey = process.env.STACKONE_API_KEY; + // Delete the API key to force an error + process.env.STACKONE_API_KEY = undefined; + try { // This will throw a ToolsetConfigError const _toolset = new StackOneToolSet(); - + assert(false, 'Expected ToolsetConfigError was not thrown'); + } catch (error) { + assert(error instanceof ToolsetConfigError, 'Expected error to be ToolsetConfigError'); + } finally { // Restore the API key process.env.STACKONE_API_KEY = originalKey; - } catch (error) { - if (error instanceof ToolsetConfigError) { - console.log('✓ Caught ToolsetConfigError:', error.message); - } else { - console.error('Unexpected error:', error); - } } + }; - // Example 2: Handle API errors - console.log('\nExample 2: Handle API errors'); + // Example 2: Handle API errors + const testApiErrors = async (): Promise => { const toolset = new StackOneToolSet(); const accountId = 'invalid-account-id'; // Invalid account ID to force an error @@ -45,60 +39,58 @@ const errorHandling = async (): Promise => { if (employeeTool) { // This will throw a StackOneAPIError due to the invalid account ID await employeeTool.execute(); + assert(false, 'Expected StackOneAPIError was not thrown'); } } catch (error) { + assert( + error instanceof StackOneAPIError || error instanceof StackOneError, + 'Expected error to be StackOneAPIError or StackOneError' + ); + if (error instanceof StackOneAPIError) { - console.log('✓ Caught StackOneAPIError:'); - console.log(` Status code: ${error.statusCode}`); - console.log(` Response body: ${JSON.stringify(error.responseBody)}`); - } else if (error instanceof StackOneError) { - console.log('✓ Caught StackOneError:', error.message); - } else { - console.error('Unexpected error:', error); + assert(error.statusCode !== undefined, 'Expected statusCode to be defined'); + assert(error.responseBody !== undefined, 'Expected responseBody to be defined'); } } + }; - // Example 3: Handle invalid tool name - console.log('\nExample 3: Handle invalid tool name'); - try { - const tools = toolset.getTools('hris_*'); - const nonExistentTool = tools.getTool('non_existent_tool'); + // Example 3: Handle invalid tool name + const testInvalidToolName = async (): Promise => { + const toolset = new StackOneToolSet(); + const tools = toolset.getTools('hris_*'); + const nonExistentTool = tools.getTool('non_existent_tool'); - if (!nonExistentTool) { - console.log('✓ Tool not found, as expected'); - } else { - // This should not happen - console.error('Unexpected: Tool was found'); - } - } catch (error) { - console.error('Unexpected error:', error); - } + assert(nonExistentTool === undefined, 'Expected non-existent tool to be undefined'); + }; - // Example 4: Handle invalid arguments - console.log('\nExample 4: Handle invalid arguments'); - try { - const tools = toolset.getTools('hris_*'); - const employeeTool = tools.getTool('hris_get_employee'); + // Example 4: Handle invalid arguments + const testInvalidArguments = async (): Promise => { + const toolset = new StackOneToolSet(); + const tools = toolset.getTools('hris_*'); + const employeeTool = tools.getTool('hris_get_employee'); - if (employeeTool) { + if (employeeTool) { + try { // This will throw an error due to missing required arguments await employeeTool.execute(); - } - } catch (error) { - if (error instanceof StackOneAPIError) { - console.log('✓ Caught StackOneAPIError:'); - console.log(` Status code: ${error.statusCode}`); - console.log(` Response body: ${JSON.stringify(error.responseBody)}`); - } else if (error instanceof StackOneError) { - console.log('✓ Caught StackOneError:', error.message); - } else { - console.log('✓ Caught error:', error instanceof Error ? error.message : String(error)); + assert(false, 'Expected error was not thrown for missing arguments'); + } catch (error) { + assert( + error instanceof StackOneAPIError || + error instanceof StackOneError || + error instanceof Error, + 'Expected error to be a known error type' + ); } } - } catch (error) { - console.error('Unhandled error:', error); - } + }; + + // Run all tests + await testInitializationErrors(); + await testApiErrors(); + await testInvalidToolName(); + await testInvalidArguments(); }; // Run the example -errorHandling().catch(console.error); +errorHandling(); diff --git a/examples/file-uploads.ts b/examples/file-uploads.ts index df680392..00c780c1 100644 --- a/examples/file-uploads.ts +++ b/examples/file-uploads.ts @@ -1,71 +1,53 @@ /** * Example showing how to upload files using the StackOne SDK. - * + * * This example demonstrates how to upload files using the simplified file_path parameter, * which is the only parameter needed for file uploads. The SDK automatically derives * the necessary file parameters (content, name, file_format) from the file_path. */ -// Load environment variables from .env file -import { config } from 'dotenv'; - -config(); - +import assert from 'node:assert'; import * as fs from 'node:fs'; import * as path from 'node:path'; -import { StackOneAPIError, StackOneToolSet } from '../src'; +import { StackOneToolSet } from '../src'; const accountId = '45072196112816593343'; -const main = async () => { - try { - // Create a sample file for testing - const sampleFilePath = path.join(__dirname, 'sample-file.txt'); - fs.writeFileSync(sampleFilePath, 'This is a sample file for testing file uploads.'); +const fileUploads = async (): Promise => { + // Create a sample file for testing + const sampleFilePath = path.join(__dirname, 'sample-file.txt'); + fs.writeFileSync(sampleFilePath, 'This is a sample file for testing file uploads.'); - // Initialize the StackOne toolset with your API key and account ID + try { + // Initialize the StackOne toolset const toolset = new StackOneToolSet(); - + // Get tools for documents const tools = toolset.getTools('hris_*', accountId); - + // Get the upload file tool const uploadTool = tools.getTool('hris_upload_employee_document'); // Check if upload tool exists - if (!uploadTool) { - console.error('Upload document tool not found'); - return; - } - - try { - // Upload a file using the file_path parameter - // The SDK will automatically derive content, name, and file_format from the file_path - const result = await uploadTool.execute({ - file_path: sampleFilePath, - id: 'c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA', - category: { value: 'shared' }, - }); - - // Only log the final result - console.log('Upload successful:', result); - - } catch (error) { - if (error instanceof StackOneAPIError) { - // Use the toString method to get the properly formatted error message - console.error(error.toString()); - } else { - console.error('Error:', error); - } - } - + assert(uploadTool !== undefined, 'Upload document tool not found'); + + // Upload a file using the file_path parameter + // The SDK will automatically derive content, name, and file_format from the file_path + const result = await uploadTool.execute({ + file_path: sampleFilePath, + id: 'c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA', + category: { value: 'shared' }, + }); + + // Verify the result + assert(result !== undefined, 'Expected result to be defined'); + assert(typeof result === 'object', 'Expected result to be an object'); + } finally { // Clean up the sample file - fs.unlinkSync(sampleFilePath); - console.log('Sample file deleted.'); - - } catch (error) { - console.error('Error:', error); + if (fs.existsSync(sampleFilePath)) { + fs.unlinkSync(sampleFilePath); + } } }; -main(); +fileUploads(); diff --git a/examples/index.ts b/examples/index.ts index 70059991..9a5c8678 100644 --- a/examples/index.ts +++ b/examples/index.ts @@ -42,6 +42,7 @@ const accountId = '45072196112816593343'; * # Quickstart */ +import assert from 'node:assert'; import { StackOneToolSet } from '../src'; const quickstart = async (): Promise => { @@ -50,20 +51,21 @@ const quickstart = async (): Promise => { // Get all HRIS-related tools const tools = toolset.getTools('hris_*', accountId); + // Verify we have tools + assert(tools.length > 0, 'Expected to find HRIS tools'); + // Use a specific tool const employeeTool = tools.getTool('hris_list_employees'); - if (employeeTool) { - try { - const employees = await employeeTool.execute(); - console.log(employees); - } catch (error) { - console.error('Error executing tool:', error); - } - } + assert(employeeTool !== undefined, 'Expected to find hris_list_employees tool'); + + // Execute the tool and verify the response + const employees = await employeeTool.execute(); + assert(Array.isArray(employees), 'Expected employees to be an array'); + assert(employees.length > 0, 'Expected to find at least one employee'); }; // Run the example -quickstart().catch(console.error); +quickstart(); /** * # Next Steps @@ -75,4 +77,5 @@ quickstart().catch(console.error); * - [Error Handling](error-handling.md) * - [File Uploads](file-uploads.md) * - [Custom Base URL](custom-base-url.md) + * - [Account ID Usage](account-id-usage.md) */ diff --git a/examples/openai-integration.ts b/examples/openai-integration.ts index 214769b7..514e2f16 100644 --- a/examples/openai-integration.ts +++ b/examples/openai-integration.ts @@ -2,6 +2,7 @@ * This example shows how to use StackOne tools with OpenAI. */ +import assert from 'node:assert'; import OpenAI from 'openai'; import { StackOneToolSet } from '../src'; @@ -17,47 +18,40 @@ const openaiIntegration = async (): Promise => { // Initialize OpenAI client const openai = new OpenAI(); - try { - await openai.chat.completions.create({ - model: 'gpt-4o-mini', - messages: [ - { - role: 'system', - content: 'You are a helpful assistant that can access HRIS information.', - }, - { - role: 'user', - content: - 'What is the employee with id: c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA phone number?', - }, - ], - tools: openAITools, - }); - - // { - // "index": 0, - // "message": { - // "role": "assistant", - // "content": null, - // "tool_calls": [ - // { - // "id": "call_1ffppzVwBWnTbBR1KKD38GA3", - // "type": "function", - // "function": { - // "name": "hris_get_employee", - // "arguments": "{\"id\":\"c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA\",\"fields\":\"phone_number\"}" - // } - // } - // ], - // "refusal": null - // }, - // "logprobs": null, - // "finish_reason": "tool_calls" - // } - } catch (error) { - console.error('Error:', error); - } + // Create a chat completion with tool calls + const response = await openai.chat.completions.create({ + model: 'gpt-4o-mini', + messages: [ + { + role: 'system', + content: 'You are a helpful assistant that can access HRIS information.', + }, + { + role: 'user', + content: 'What is the employee with id: c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA phone number?', + }, + ], + tools: openAITools, + }); + + // Verify the response contains tool calls + assert(response.choices.length > 0, 'Expected at least one choice in the response'); + + const choice = response.choices[0]; + assert(choice.message.tool_calls !== undefined, 'Expected tool_calls to be defined'); + assert(choice.message.tool_calls.length > 0, 'Expected at least one tool call'); + + const toolCall = choice.message.tool_calls[0]; + assert( + toolCall.function.name === 'hris_get_employee', + 'Expected tool call to be hris_get_employee' + ); + + // Parse the arguments to verify they contain the expected fields + const args = JSON.parse(toolCall.function.arguments); + assert(args.id === 'c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA', 'Expected id to match the query'); + assert(args.fields !== undefined, 'Expected fields to be defined'); }; // Run the example -openaiIntegration().catch(console.error); +openaiIntegration(); diff --git a/src/models.ts b/src/models.ts index a80f1855..abcd3d7c 100644 --- a/src/models.ts +++ b/src/models.ts @@ -194,6 +194,24 @@ export class StackOneTool { this._accountId = accountId; } + /** + * Get the current account ID + * @returns The current account ID or undefined if not set + */ + getAccountId(): string | undefined { + return this._accountId; + } + + /** + * Set the account ID for this tool + * @param accountId The account ID to set + * @returns This tool instance for chaining + */ + setAccountId(accountId: string): StackOneTool { + this._accountId = accountId; + return this; + } + /** * Prepare headers for the API request * @returns Headers to use in the request @@ -315,6 +333,14 @@ export class StackOneTool { userParams = { ...params }; // Create a shallow copy to avoid modifying the original } + // Remove accountId from params if present - we should not be setting it here + if ('accountId' in userParams) { + console.warn( + 'Setting accountId in execute parameters is deprecated. Use setAccountId method instead.' + ); + userParams.accountId = undefined; + } + // Map user parameters to API parameters const apiParams = this._mapParameters(userParams); @@ -544,7 +570,7 @@ export class StackOneTool { * Convert this tool to an AI SDK tool * @returns AI SDK tool */ - toAISDKTool() { + toAISDK() { // Create a wrapper function that will handle the execution const executeWrapper = async ( args: unknown, @@ -613,11 +639,11 @@ export class Tools { * Convert all tools to AI SDK tools * @returns Object with tool names as keys and AI SDK tools as values */ - toAISDKTools(): Record, JsonDict>> { + toAISDK(): Record, JsonDict>> { const result: Record, JsonDict>> = {}; for (const stackOneTool of this.tools) { - result[stackOneTool.name] = stackOneTool.toAISDKTool(); + result[stackOneTool.name] = stackOneTool.toAISDK(); } return result; diff --git a/src/tests/models.spec.ts b/src/tests/models.spec.ts index 6e0afdda..a2c8eec2 100644 --- a/src/tests/models.spec.ts +++ b/src/tests/models.spec.ts @@ -131,7 +131,7 @@ describe('StackOneTool', () => { it('should convert to AI SDK tool format', () => { const tool = createMockTool(); - const aiSdkTool = tool.toAISDKTool(); + const aiSdkTool = tool.toAISDK(); expect(aiSdkTool).toBeDefined(); expect(typeof aiSdkTool.execute).toBe('function'); @@ -186,7 +186,7 @@ describe('StackOneTool', () => { 'test_key' ); - const aiSdkTool = complexTool.toAISDKTool(); + const aiSdkTool = complexTool.toAISDK(); // Check that parameters is a JSON Schema expect(aiSdkTool.parameters).toBeDefined(); @@ -222,7 +222,7 @@ describe('StackOneTool', () => { }; const stackOneTool = createMockTool(); - const aiSdkTool = stackOneTool.toAISDKTool(); + const aiSdkTool = stackOneTool.toAISDK(); // Mock the ToolExecutionOptions const mockOptions = { @@ -300,7 +300,7 @@ describe('Tools', () => { const tools = new Tools([tool1, tool2]); - const aiSdkTools = tools.toAISDKTools(); + const aiSdkTools = tools.toAISDK(); expect(Object.keys(aiSdkTools).length).toBe(2); expect(aiSdkTools.test_tool).toBeDefined(); diff --git a/src/tests/schema-validation.spec.ts b/src/tests/schema-validation.spec.ts index d062a69f..0d9ff96b 100644 --- a/src/tests/schema-validation.spec.ts +++ b/src/tests/schema-validation.spec.ts @@ -227,7 +227,7 @@ describe('Schema Validation', () => { describe('AI SDK Integration', () => { it('should convert to AI SDK tool format', () => { const tool = createArrayTestTool(); - const aiSdkTool = tool.toAISDKTool(); + const aiSdkTool = tool.toAISDK(); expect(aiSdkTool).toBeDefined(); expect(typeof aiSdkTool.execute).toBe('function'); diff --git a/src/tests/toolset.spec.ts b/src/tests/toolset.spec.ts index fa7e2b24..07c077a1 100644 --- a/src/tests/toolset.spec.ts +++ b/src/tests/toolset.spec.ts @@ -9,7 +9,7 @@ env.STACKONE_API_KEY = 'test_key'; describe('StackOneToolSet', () => { it('should initialize with API key from constructor', () => { - const toolset = new StackOneToolSet('custom_key'); + const toolset = new StackOneToolSet({ apiKey: 'custom_key' }); expect(toolset).toBeDefined(); }); @@ -31,7 +31,7 @@ describe('StackOneToolSet', () => { it('should correctly filter tools with a pattern', () => { // Create a test instance of StackOneToolSet - const toolset = new StackOneToolSet('test_key'); + const toolset = new StackOneToolSet({ apiKey: 'test_key' }); // Test the private _matchesFilter method directly // @ts-ignore - Accessing private method for testing @@ -56,7 +56,7 @@ describe('StackOneToolSet', () => { it('should correctly match glob patterns', () => { // Create a test instance of StackOneToolSet - const toolset = new StackOneToolSet('test_key'); + const toolset = new StackOneToolSet({ apiKey: 'test_key' }); // Test the private _matchGlob method directly // @ts-ignore - Accessing private method for testing @@ -98,7 +98,10 @@ describe('StackOneToolSet', () => { it('should pass custom base URL from StackOneToolSet to OpenAPIParser', () => { // Create a StackOneToolSet with a custom base URL const customBaseUrlValue = 'https://api.example-dev.com'; - const toolset = new StackOneToolSet('test-key', undefined, customBaseUrlValue); + const toolset = new StackOneToolSet({ + apiKey: 'test-key', + baseUrl: customBaseUrlValue, + }); // Directly check that the baseUrl property is set correctly // @ts-ignore - Accessing private property for testing @@ -165,7 +168,7 @@ describe('StackOneToolSet', () => { }; try { - const toolset = new StackOneToolSet('test_key'); + const toolset = new StackOneToolSet({ apiKey: 'test_key' }); // Test with no filter (should return all tools) const allTools = toolset.getTools(); @@ -216,7 +219,7 @@ describe('StackOneToolSet', () => { // Replace the single test with multiple focused tests describe('real tool loading', () => { // Create a toolset once for all tests in this group - const toolset = new StackOneToolSet('test_key'); + const toolset = new StackOneToolSet({ apiKey: 'test_key' }); let allTools: Tools; let verticals: string[] = []; diff --git a/src/toolset.ts b/src/toolset.ts index 2c3d80f7..119439ce 100644 --- a/src/toolset.ts +++ b/src/toolset.ts @@ -34,6 +34,12 @@ export class ToolsetLoadError extends ToolsetError { } } +export interface ToolsetConfig { + apiKey?: string; + accountId?: string; + baseUrl?: string; +} + /** * Main class for accessing StackOne tools */ @@ -49,8 +55,8 @@ export class StackOneToolSet { * @param baseUrl Optional base URL for API requests. If not provided, will use the default from the OpenAPI spec * @throws ToolsetConfigError If no API key is provided or found in environment */ - constructor(apiKey?: string, accountId?: string, baseUrl?: string) { - const apiKeyValue = apiKey || process.env.STACKONE_API_KEY; + constructor(config?: ToolsetConfig) { + const apiKeyValue = config?.apiKey || process.env.STACKONE_API_KEY; if (!apiKeyValue) { throw new ToolsetConfigError( 'API key must be provided either through apiKey parameter or ' + @@ -58,8 +64,8 @@ export class StackOneToolSet { ); } this.apiKey = apiKeyValue; - this.accountId = accountId || process.env.STACKONE_ACCOUNT_ID; - this.baseUrl = baseUrl; + this.accountId = config?.accountId || process.env.STACKONE_ACCOUNT_ID; + this.baseUrl = config?.baseUrl; } /**