diff --git a/examples/anthropic-integration.ts b/examples/anthropic-integration.ts new file mode 100644 index 0000000..f92dc30 --- /dev/null +++ b/examples/anthropic-integration.ts @@ -0,0 +1,63 @@ +/** + * This example shows how to use StackOne tools with Anthropic Claude. + */ + +import assert from 'node:assert'; +import process from 'node:process'; +import Anthropic from '@anthropic-ai/sdk'; +import { StackOneToolSet } from '@stackone/ai'; + +const apiKey = process.env.STACKONE_API_KEY; +if (!apiKey) { + console.error('STACKONE_API_KEY environment variable is required'); + process.exit(1); +} + +// Replace with your actual account ID from StackOne dashboard +const accountId = 'your-hris-account-id'; + +const anthropicIntegration = async (): Promise => { + // Initialise StackOne + const toolset = new StackOneToolSet({ + accountId, + baseUrl: process.env.STACKONE_BASE_URL ?? 'https://api.stackone.com', + }); + + // Filter for any relevant tools + const tools = await toolset.fetchTools({ + actions: ['*_list_*', '*_search_*'], + }); + const anthropicTools = tools.toAnthropic(); + + // Initialise Anthropic client + const anthropic = new Anthropic(); + + // Create a message with tool calls + const response = await anthropic.messages.create({ + model: 'claude-haiku-4-5-20241022', + max_tokens: 1024, + system: 'You are a helpful assistant that can access tools.', + messages: [ + { + role: 'user', + content: 'What is the employee with id: c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA phone number?', + }, + ], + tools: anthropicTools, + }); + + // Verify the response contains tool use + assert(response.content.length > 0, 'Expected at least one content block in the response'); + + const toolUseBlock = response.content.find((block) => block.type === 'tool_use'); + assert(toolUseBlock !== undefined, 'Expected a tool_use block in the response'); + assert(toolUseBlock.type === 'tool_use', 'Expected block to be tool_use type'); + assert(toolUseBlock.name === 'hris_get_employee', 'Expected tool call to be hris_get_employee'); + + // Verify the input contains the expected fields + const input = toolUseBlock.input as Record; + assert(input.id === 'c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA', 'Expected id to match the query'); +}; + +// Run the example +await anthropicIntegration(); diff --git a/examples/package.json b/examples/package.json index 17919d4..5ea8664 100644 --- a/examples/package.json +++ b/examples/package.json @@ -12,6 +12,7 @@ }, "devDependencies": { "@ai-sdk/openai": "catalog:dev", + "@anthropic-ai/sdk": "catalog:peer", "@clack/prompts": "catalog:dev", "@types/node": "catalog:dev", "ai": "catalog:peer", diff --git a/package.json b/package.json index 8079f9a..0b11c50 100644 --- a/package.json +++ b/package.json @@ -64,10 +64,14 @@ "vitest": "catalog:dev" }, "peerDependencies": { + "@anthropic-ai/sdk": "catalog:peer", "ai": "catalog:peer", "openai": "catalog:peer" }, "peerDependenciesMeta": { + "@anthropic-ai/sdk": { + "optional": true + }, "ai": { "optional": true }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 73192fb..935fb0b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -70,6 +70,9 @@ catalogs: specifier: ^4.1.13 version: 4.1.13 peer: + '@anthropic-ai/sdk': + specifier: ^0.52.0 + version: 0.52.0 ai: specifier: ^5.0.108 version: 5.0.108 @@ -93,6 +96,9 @@ catalogs: importers: .: dependencies: + '@anthropic-ai/sdk': + specifier: catalog:peer + version: 0.52.0 '@modelcontextprotocol/sdk': specifier: catalog:prod version: 1.24.3(zod@4.1.13) @@ -182,6 +188,9 @@ importers: '@ai-sdk/openai': specifier: catalog:dev version: 2.0.80(zod@4.1.13) + '@anthropic-ai/sdk': + specifier: catalog:peer + version: 0.52.0 '@clack/prompts': specifier: catalog:dev version: 0.11.0 @@ -236,6 +245,13 @@ packages: } engines: { node: '>=18' } + '@anthropic-ai/sdk@0.52.0': + resolution: + { + integrity: sha512-d4c+fg+xy9e46c8+YnrrgIQR45CZlAi7PwdzIfDXDM6ACxEZli1/fxhURsq30ZpMZy6LvSkr41jGq5aF5TD7rQ==, + } + hasBin: true + '@babel/generator@7.28.5': resolution: { @@ -3733,6 +3749,8 @@ snapshots: dependencies: json-schema: 0.4.0 + '@anthropic-ai/sdk@0.52.0': {} + '@babel/generator@7.28.5': dependencies: '@babel/parser': 7.28.5 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index cbc6349..f2c4844 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -28,6 +28,7 @@ catalogs: vitest: ^4.0.15 zod: ^4.1.13 peer: + '@anthropic-ai/sdk': ^0.52.0 ai: ^5.0.108 openai: ^6.2.0 prod: diff --git a/src/tool.test.ts b/src/tool.test.ts index 1fa00c2..d927257 100644 --- a/src/tool.test.ts +++ b/src/tool.test.ts @@ -72,6 +72,18 @@ describe('StackOneTool', () => { ).toBe('string'); }); + it('should convert to Anthropic tool format', () => { + const tool = createMockTool(); + const anthropicFormat = tool.toAnthropic(); + + expect(anthropicFormat.name).toBe('test_tool'); + expect(anthropicFormat.description).toBe('Test tool'); + expect(anthropicFormat.input_schema.type).toBe('object'); + const properties = anthropicFormat.input_schema.properties as Record; + expect(properties.id).toBeDefined(); + expect(properties.id.type).toBe('string'); + }); + it('should convert to OpenAI Responses API tool format', () => { const tool = createMockTool(); const responsesFormat = tool.toOpenAIResponses(); @@ -352,6 +364,63 @@ describe('Tools', () => { expect(openAITools[1].function.name).toBe('tool2'); }); + it('should convert all tools to Anthropic format', () => { + const tool1 = new BaseTool( + 'tool1', + 'Tool 1', + { + type: 'object', + properties: { id: { type: 'string' } }, + }, + { + kind: 'http', + method: 'GET', + url: 'https://api.example.com/test/{id}', + bodyType: 'json', + params: [ + { + name: 'id', + location: ParameterLocation.PATH, + type: 'string', + }, + ], + }, + ); + + const tool2 = new BaseTool( + 'tool2', + 'Tool 2', + { + type: 'object', + properties: { name: { type: 'string' } }, + }, + { + kind: 'http', + method: 'POST', + url: 'https://api.example.com/test', + bodyType: 'json', + params: [ + { + name: 'name', + location: ParameterLocation.BODY, + type: 'string', + }, + ], + }, + ); + + const tools = new Tools([tool1, tool2]); + const anthropicTools = tools.toAnthropic(); + + expect(anthropicTools).toBeInstanceOf(Array); + expect(anthropicTools.length).toBe(2); + expect(anthropicTools[0].name).toBe('tool1'); + expect(anthropicTools[0].description).toBe('Tool 1'); + expect(anthropicTools[0].input_schema.type).toBe('object'); + expect(anthropicTools[1].name).toBe('tool2'); + expect(anthropicTools[1].description).toBe('Tool 2'); + }); + it('should convert all tools to OpenAI Responses API tools', () => { const tool1 = new StackOneTool( 'tool1', diff --git a/src/tool.ts b/src/tool.ts index f60ada5..841a497 100644 --- a/src/tool.ts +++ b/src/tool.ts @@ -1,3 +1,4 @@ +import type { Tool as AnthropicTool } from '@anthropic-ai/sdk/resources'; import * as orama from '@orama/orama'; import type { ChatCompletionFunctionTool } from 'openai/resources/chat/completions'; import type { FunctionTool as OpenAIResponsesFunctionTool } from 'openai/resources/responses/responses'; @@ -182,6 +183,22 @@ export class BaseTool { }; } + /** + * Convert the tool to Anthropic format + * @see https://docs.anthropic.com/en/docs/build-with-claude/tool-use + */ + toAnthropic(): AnthropicTool { + return { + name: this.name, + description: this.description, + input_schema: { + type: 'object', + properties: this.parameters.properties, + required: this.parameters.required, + }, + }; + } + /** * Convert the tool to OpenAI Responses API format * @see https://platform.openai.com/docs/api-reference/responses @@ -379,6 +396,14 @@ export class Tools implements Iterable { return this.tools.map((tool) => tool.toOpenAI()); } + /** + * Convert all tools to Anthropic format + * @see https://docs.anthropic.com/en/docs/build-with-claude/tool-use + */ + toAnthropic(): AnthropicTool[] { + return this.tools.map((tool) => tool.toAnthropic()); + } + /** * Convert all tools to OpenAI Responses API format * @see https://platform.openai.com/docs/api-reference/responses