diff --git a/README.md b/README.md index 2fe153b..d720e7e 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,21 @@ yarn add @stackone/ai bun add @stackone/ai ``` +### Optional: AI SDK Integration + +If you plan to use the AI SDK integration (Vercel AI SDK), install it separately: + +```bash +# Using npm +npm install ai + +# Using yarn +yarn add ai + +# Using bun +bun add ai +``` + ## Integrations The OpenAPIToolSet and StackOneToolSet make it super easy to use these APIs as tools in your AI applications. @@ -70,7 +85,7 @@ import { StackOneToolSet } from "@stackone/ai"; const toolset = new StackOneToolSet(); -const aiSdkTools = toolset.getTools("hris_*").toAISDK(); +const aiSdkTools = await toolset.getTools("hris_*").toAISDK(); await generateText({ model: openai("gpt-5"), tools: aiSdkTools, @@ -274,7 +289,7 @@ const metaTools = await tools.metaTools(); const openAITools = metaTools.toOpenAI(); // Use with AI SDK -const aiSdkTools = metaTools.toAISDK(); +const aiSdkTools = await metaTools.toAISDK(); ``` #### Example: Dynamic Tool Discovery with AI SDK diff --git a/bun.lock b/bun.lock index d47d1d9..937dc06 100644 --- a/bun.lock +++ b/bun.lock @@ -35,6 +35,10 @@ "ai": "4.x|5.x", "openai": "5.x|6.x", }, + "optionalPeers": [ + "ai", + "openai", + ], }, }, "packages": { diff --git a/examples/ai-sdk-integration.ts b/examples/ai-sdk-integration.ts index 790bb3e..990b2ce 100644 --- a/examples/ai-sdk-integration.ts +++ b/examples/ai-sdk-integration.ts @@ -17,7 +17,7 @@ const aiSdkIntegration = async (): Promise => { const tools = toolset.getStackOneTools('hris_get_*', accountId); // Convert to AI SDK tools - const aiSdkTools = tools.toAISDK(); + const aiSdkTools = await tools.toAISDK(); // Use max steps to automatically call the tool if it's needed const { text } = await generateText({ diff --git a/examples/human-in-the-loop.ts b/examples/human-in-the-loop.ts index 30b584b..6d4e100 100644 --- a/examples/human-in-the-loop.ts +++ b/examples/human-in-the-loop.ts @@ -33,7 +33,7 @@ const humanInTheLoopExample = async (): Promise => { } // Get the AI SDK version of the tool without the execute function - const tool = createEmployeeTool.toAISDK({ + const tool = await createEmployeeTool.toAISDK({ executable: false, }); diff --git a/examples/meta-tools.ts b/examples/meta-tools.ts index adb2659..c77b0b0 100644 --- a/examples/meta-tools.ts +++ b/examples/meta-tools.ts @@ -27,7 +27,7 @@ const metaToolsWithAISDK = async (): Promise => { // Get meta tools for dynamic discovery and execution const metaTools = await allTools.metaTools(); - const aiSdkMetaTools = metaTools.toAISDK(); + const aiSdkMetaTools = await metaTools.toAISDK(); // Use meta tools to dynamically find and execute relevant tools const { text, toolCalls } = await generateText({ diff --git a/examples/planning.ts b/examples/planning.ts index e7f5a07..39b21f6 100644 --- a/examples/planning.ts +++ b/examples/planning.ts @@ -31,7 +31,7 @@ export const planningModule = async (): Promise => { await generateText({ model: openai('gpt-5'), prompt: 'You are a workplace agent, onboard the latest hires to our systems', - tools: onboardWorkflow.toAISDK(), + tools: await onboardWorkflow.toAISDK(), maxSteps: 3, }); }; diff --git a/package.json b/package.json index 5fbe369..bf111e2 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,14 @@ "ai": "4.x|5.x", "openai": "5.x|6.x" }, + "peerDependenciesMeta": { + "ai": { + "optional": true + }, + "openai": { + "optional": true + } + }, "repository": { "type": "git", "url": "git+https://github.com/StackOneHQ/stackone-ai-node.git" diff --git a/src/tests/json-schema.spec.ts b/src/tests/json-schema.spec.ts index a17237e..d3d67f6 100644 --- a/src/tests/json-schema.spec.ts +++ b/src/tests/json-schema.spec.ts @@ -248,9 +248,9 @@ describe('Schema Validation', () => { }); describe('AI SDK Integration', () => { - it('should convert to AI SDK tool format', () => { + it('should convert to AI SDK tool format', async () => { const tool = createArrayTestTool(); - const aiSdkTool = tool.toAISDK(); + const aiSdkTool = await tool.toAISDK(); expect(aiSdkTool).toBeDefined(); // The AI SDK tool is an object with the tool name as the key @@ -278,7 +278,7 @@ describe('Schema Validation', () => { expect(arrayWithItems.items.type).toBe('string'); }); - it('should handle the problematic nested array case', () => { + it('should handle the problematic nested array case', async () => { const tool = createNestedArrayTestTool(); const openAIFormat = tool.toOpenAI(); const parameters = openAIFormat.function.parameters; @@ -305,7 +305,7 @@ describe('Schema Validation', () => { expect(aiSchema).toBeDefined(); // Generate the SDK tool and verify its structure - const aiSdkTool = tool.toAISDK(); + const aiSdkTool = await tool.toAISDK(); expect(aiSdkTool).toBeDefined(); const toolObj = aiSdkTool[tool.name]; diff --git a/src/tests/meta-tools.spec.ts b/src/tests/meta-tools.spec.ts index d3e13aa..efdb5db 100644 --- a/src/tests/meta-tools.spec.ts +++ b/src/tests/meta-tools.spec.ts @@ -414,8 +414,8 @@ describe('Meta Search Tools', () => { }); describe('AI SDK format', () => { - it('should convert meta tools to AI SDK format', () => { - const aiSdkTools = metaTools.toAISDK(); + it('should convert meta tools to AI SDK format', async () => { + const aiSdkTools = await metaTools.toAISDK(); expect(aiSdkTools).toHaveProperty('meta_search_tools'); expect(aiSdkTools).toHaveProperty('meta_execute_tool'); @@ -425,7 +425,7 @@ describe('Meta Search Tools', () => { }); it('should execute through AI SDK format', async () => { - const aiSdkTools = metaTools.toAISDK(); + const aiSdkTools = await metaTools.toAISDK(); const result = await aiSdkTools.meta_search_tools.execute?.( { query: 'ATS candidates', limit: 2 }, diff --git a/src/tests/tool.spec.ts b/src/tests/tool.spec.ts index e80e441..4923fbe 100644 --- a/src/tests/tool.spec.ts +++ b/src/tests/tool.spec.ts @@ -94,10 +94,10 @@ describe('StackOneTool', () => { ).toBe('string'); }); - it('should convert to AI SDK tool format', () => { + it('should convert to AI SDK tool format', async () => { const tool = createMockTool(); - const aiSdkTool = tool.toAISDK(); + const aiSdkTool = await tool.toAISDK(); // Test the basic structure expect(aiSdkTool).toBeDefined(); @@ -114,10 +114,10 @@ describe('StackOneTool', () => { expect(schema.properties.id.type).toBe('string'); }); - it('should include execution metadata by default in AI SDK conversion', () => { + it('should include execution metadata by default in AI SDK conversion', async () => { const tool = createMockTool(); - const aiSdkTool = tool.toAISDK(); + const aiSdkTool = await tool.toAISDK(); const execution = aiSdkTool.test_tool.execution; expect(execution).toBeDefined(); @@ -126,15 +126,15 @@ describe('StackOneTool', () => { expect(execution?.headers).toEqual({}); }); - it('should allow disabling execution metadata exposure for AI SDK conversion', () => { + it('should allow disabling execution metadata exposure for AI SDK conversion', async () => { const tool = createMockTool().setExposeExecutionMetadata(false); - const aiSdkTool = tool.toAISDK(); + const aiSdkTool = await tool.toAISDK(); expect(aiSdkTool.test_tool.execution).toBeUndefined(); }); - it('should convert complex parameter types to zod schema', () => { + it('should convert complex parameter types to zod schema', async () => { const complexTool = new BaseTool( 'complex_tool', 'Complex tool', @@ -165,7 +165,7 @@ describe('StackOneTool', () => { } ); - const aiSdkTool = complexTool.toAISDK(); + const aiSdkTool = await complexTool.toAISDK(); // Check that the tool is defined expect(aiSdkTool).toBeDefined(); @@ -190,7 +190,7 @@ describe('StackOneTool', () => { it('should execute AI SDK tool with parameters', async () => { const tool = createMockTool(); - const aiSdkTool = tool.toAISDK(); + const aiSdkTool = await tool.toAISDK(); if (!aiSdkTool.test_tool.execute) { throw new Error('test_tool.execute is undefined'); @@ -212,7 +212,7 @@ describe('StackOneTool', () => { throw mockError; }); - const aiSdkTool = tool.toAISDK(); + const aiSdkTool = await tool.toAISDK(); if (!aiSdkTool.test_tool.execute) { throw new Error('test_tool.execute is undefined'); @@ -300,7 +300,7 @@ describe('Tools', () => { expect(openAITools[1].function.name).toBe('tool2'); }); - it('should convert all tools to AI SDK tools', () => { + it('should convert all tools to AI SDK tools', async () => { const tool1 = createMockTool(); const tool2 = new StackOneTool( 'another_tool', @@ -329,7 +329,7 @@ describe('Tools', () => { const tools = new Tools([tool1, tool2]); - const aiSdkTools = tools.toAISDK(); + const aiSdkTools = await tools.toAISDK(); expect(Object.keys(aiSdkTools).length).toBe(2); expect(aiSdkTools.test_tool).toBeDefined(); diff --git a/src/tool.ts b/src/tool.ts index ec42403..c0425ab 100644 --- a/src/tool.ts +++ b/src/tool.ts @@ -1,5 +1,4 @@ import * as orama from '@orama/orama'; -import { jsonSchema } from 'ai'; import type { ChatCompletionTool } from 'openai/resources/chat/completions'; import { RequestBuilder } from './modules/requestBuilder'; import type { @@ -181,7 +180,7 @@ export class BaseTool { /** * Convert the tool to AI SDK format */ - toAISDK( + async toAISDK( options: { executable?: boolean; execution?: ToolExecution | false } = { executable: true, } @@ -193,6 +192,17 @@ export class BaseTool { additionalProperties: false, }; + /** AI SDK is optional dependency, import only when needed */ + let jsonSchema: typeof import('ai').jsonSchema; + try { + const ai = await import('ai'); + jsonSchema = ai.jsonSchema; + } catch { + throw new StackOneError( + 'AI SDK is not installed. Please install it with: npm install ai@4.x|5.x or bun add ai@4.x|5.x' + ); + } + const schemaObject = jsonSchema(schema); const toolDefinition: Record = { inputSchema: schemaObject, // v5 @@ -346,14 +356,14 @@ export class Tools implements Iterable { /** * Convert all tools to AI SDK format */ - toAISDK( + async toAISDK( options: { executable?: boolean; execution?: ToolExecution | false } = { executable: true, } ) { const result: Record = {}; for (const tool of this.tools) { - Object.assign(result, tool.toAISDK(options)); + Object.assign(result, await tool.toAISDK(options)); } return result; } diff --git a/src/toolsets/tests/stackone.mcp-fetch.spec.ts b/src/toolsets/tests/stackone.mcp-fetch.spec.ts index 33f4949..54bfe80 100644 --- a/src/toolsets/tests/stackone.mcp-fetch.spec.ts +++ b/src/toolsets/tests/stackone.mcp-fetch.spec.ts @@ -107,14 +107,14 @@ describe('ToolSet.fetchTools (MCP + RPC integration)', () => { const tool = tools.toArray()[0]; expect(tool.name).toBe('dummy_action'); - const aiTools = tool.toAISDK({ executable: false }); + const aiTools = await tool.toAISDK({ executable: false }); const aiToolDefinition = aiTools.dummy_action; expect(aiToolDefinition).toBeDefined(); expect(aiToolDefinition.description).toBe('Dummy tool'); expect(aiToolDefinition.inputSchema.jsonSchema.properties.foo.type).toBe('string'); expect(aiToolDefinition.execution).toBeUndefined(); - const executableTool = tool.toAISDK().dummy_action; + const executableTool = (await tool.toAISDK()).dummy_action; const result = await executableTool.execute({ foo: 'bar' }); expect(stackOneClient.actions.rpcAction).toHaveBeenCalledWith({