Skip to content

Commit feefdc5

Browse files
feat: add tool search with semantic, local, and auto search modes (#322)
* Tool search with semantic and auto mode * Fix CI Lint issues * Cubic fixes * refactor earch to make aligned with defender * Fix CI Lint issues * Add topk example
1 parent 0b6b7df commit feefdc5

38 files changed

Lines changed: 2081 additions & 1341 deletions

README.md

Lines changed: 35 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ bun add @stackone/ai zod
3434
import { StackOneToolSet } from '@stackone/ai';
3535

3636
const toolset = new StackOneToolSet({
37-
baseUrl: 'https://api.stackone.com',
3837
accountId: 'your-account-id',
3938
});
4039

@@ -97,7 +96,6 @@ import { OpenAI } from 'openai';
9796
import { StackOneToolSet } from '@stackone/ai';
9897

9998
const toolset = new StackOneToolSet({
100-
baseUrl: 'https://api.stackone.com',
10199
accountId: 'your-account-id',
102100
});
103101

@@ -135,7 +133,6 @@ import OpenAI from 'openai';
135133
import { StackOneToolSet } from '@stackone/ai';
136134

137135
const toolset = new StackOneToolSet({
138-
baseUrl: 'https://api.stackone.com',
139136
accountId: 'your-account-id',
140137
});
141138

@@ -167,7 +164,6 @@ import Anthropic from '@anthropic-ai/sdk';
167164
import { StackOneToolSet } from '@stackone/ai';
168165

169166
const toolset = new StackOneToolSet({
170-
baseUrl: 'https://api.stackone.com',
171167
accountId: 'your-account-id',
172168
});
173169

@@ -206,7 +202,6 @@ import { generateText } from 'ai';
206202
import { StackOneToolSet } from '@stackone/ai';
207203

208204
const toolset = new StackOneToolSet({
209-
baseUrl: 'https://api.stackone.com',
210205
accountId: 'your-account-id',
211206
});
212207

@@ -237,7 +232,6 @@ import { z } from 'zod';
237232
import { StackOneToolSet } from '@stackone/ai';
238233

239234
const toolset = new StackOneToolSet({
240-
baseUrl: 'https://api.stackone.com',
241235
accountId: 'your-account-id',
242236
});
243237

@@ -285,7 +279,6 @@ import { query } from '@anthropic-ai/claude-agent-sdk';
285279
import { StackOneToolSet } from '@stackone/ai';
286280

287281
const toolset = new StackOneToolSet({
288-
baseUrl: 'https://api.stackone.com',
289282
accountId: 'your-account-id',
290283
});
291284

@@ -355,77 +348,60 @@ This is especially useful when you want to:
355348

356349
[View full example](examples/fetch-tools.ts)
357350

358-
### Utility Tools (Beta)
351+
### Search Tool
359352

360-
Utility tools enable dynamic tool discovery and execution, allowing AI agents to search for relevant tools based on natural language queries without hardcoding tool names.
361-
362-
> **Beta Feature**: Utility tools are currently in beta and the API may change in future versions.
363-
364-
#### How Utility Tools Work
365-
366-
Utility tools provide two core capabilities:
367-
368-
1. **Tool Discovery** (`tool_search`): Search for tools using natural language queries
369-
2. **Tool Execution** (`tool_execute`): Execute discovered tools dynamically
353+
Search for tools using natural language queries. Works with both semantic (cloud) and local BM25+TF-IDF search.
370354

371355
#### Basic Usage
372356

373357
```typescript
374358
import { StackOneToolSet } from '@stackone/ai';
375359

376-
const toolset = new StackOneToolSet({
377-
baseUrl: 'https://api.stackone.com',
378-
});
379-
const tools = await toolset.fetchTools();
360+
// Get a callable search tool
361+
const toolset = new StackOneToolSet({ accountId: 'your-account-id' });
362+
const searchTool = toolset.getSearchTool();
380363

381-
// Get utility tools for dynamic discovery
382-
const utilityTools = await tools.utilityTools();
364+
// Search for relevant tools — returns a Tools collection
365+
const tools = await searchTool.search('manage employees', { topK: 5 });
383366

384-
// Use with OpenAI
385-
const openAITools = utilityTools.toOpenAI();
386-
387-
// Use with AI SDK
388-
const aiSdkTools = await utilityTools.toAISDK();
367+
// Execute a discovered tool directly
368+
const listTool = tools.getTool('bamboohr_list_employees');
369+
const result = await listTool.execute({ query: { limit: 10 } });
389370
```
390371

391-
#### Example: Dynamic Tool Discovery with AI SDK
372+
### Semantic Search
373+
374+
Discover tools using natural language instead of exact names. Queries like "onboard new hire" resolve to the right actions even when the tool is called `bamboohr_create_employee`.
392375

393376
```typescript
394-
import { generateText } from 'ai';
395-
import { openai } from '@ai-sdk/openai';
377+
import { StackOneToolSet } from '@stackone/ai';
396378

397-
const { text } = await generateText({
398-
model: openai('gpt-5.1'),
399-
tools: aiSdkTools,
400-
prompt: 'Find tools for managing employees and create a time off request',
401-
maxSteps: 3, // Allow multiple tool calls
402-
});
379+
const toolset = new StackOneToolSet({ accountId: 'your-account-id' });
380+
381+
// Search by intent — returns Tools collection ready for any framework
382+
const tools = await toolset.searchTools('manage employee records', { topK: 5 });
383+
const openAITools = tools.toOpenAI();
384+
385+
// Lightweight: inspect results without fetching full tool definitions
386+
const results = await toolset.searchActionNames('time off requests', { topK: 5 });
403387
```
404388

405-
#### Direct Usage Without AI
389+
#### Search Modes
390+
391+
Control which search backend `searchTools()` uses via the `search` option:
406392

407393
```typescript
408-
// Step 1: Discover relevant tools
409-
const filterTool = utilityTools.getTool('tool_search');
410-
const searchResult = await filterTool.execute({
411-
query: 'employee time off vacation',
412-
limit: 5,
413-
minScore: 0.3, // Minimum relevance score (0-1)
414-
});
394+
// 'auto' (default) — tries semantic search first, falls back to local
395+
const tools = await toolset.searchTools('manage employees', { search: 'auto' });
415396

416-
// Step 2: Execute a discovered tool
417-
const executeTool = utilityTools.getTool('tool_execute');
418-
const result = await executeTool.execute({
419-
toolName: 'bamboohr_create_time_off',
420-
params: {
421-
employeeId: 'emp_123',
422-
startDate: '2024-01-15',
423-
endDate: '2024-01-19',
424-
},
425-
});
397+
// 'semantic' — semantic API only, throws if unavailable
398+
const tools = await toolset.searchTools('manage employees', { search: 'semantic' });
399+
400+
// 'local' — local BM25+TF-IDF only, no semantic API call
401+
const tools = await toolset.searchTools('manage employees', { search: 'local' });
426402
```
427403

428-
[View full example](examples/utility-tools.ts)
404+
Results are automatically scoped to connectors in your linked accounts. See [Search Tools Example](examples/search-tools.ts) for `SearchTool` (`getSearchTool`) integration, AI SDK, and agent loop patterns.
429405

430406
### Custom Base URL
431407

@@ -443,9 +419,7 @@ You can use the `dryRun` option to return the api arguments from a tool call wit
443419
import { StackOneToolSet } from '@stackone/ai';
444420

445421
// Initialize the toolset
446-
const toolset = new StackOneToolSet({
447-
baseUrl: 'https://api.stackone.com',
448-
});
422+
const toolset = new StackOneToolSet();
449423

450424
const tools = await toolset.fetchTools();
451425
const employeeTool = tools.getTool('bamboohr_list_employees');
@@ -492,9 +466,7 @@ The feedback tool is automatically available when using `StackOneToolSet`:
492466
```typescript
493467
import { StackOneToolSet } from '@stackone/ai';
494468

495-
const toolset = new StackOneToolSet({
496-
baseUrl: 'https://api.stackone.com',
497-
});
469+
const toolset = new StackOneToolSet();
498470
const tools = await toolset.fetchTools();
499471

500472
// The feedback tool is automatically included

examples/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,25 @@ Shows how to implement human-in-the-loop workflows for validation.
154154
- **API Calls**: Conditional
155155
- **Key Features**: Manual approval workflows, UI integration patterns
156156

157+
### Semantic Search
158+
159+
#### [`search-tools.ts`](./search-tools.ts) - Semantic Tool Search
160+
161+
Demonstrates dynamic tool discovery using semantic search. Includes four examples:
162+
163+
1. **Semantic search + AI SDK** — search for tools by natural language query, then use them with `generateText`
164+
2. **SearchTool for agent loops** — reusable search tool for multi-step agent workflows
165+
3. **Lightweight action name search** — search action names without fetching full tool definitions
166+
4. **Local-only search** — BM25+TF-IDF search with no API call to the semantic search endpoint
167+
168+
```bash
169+
# Run without OpenAI (examples 2-4)
170+
npx tsx examples/search-tools.ts
171+
172+
# Run all 4 examples
173+
OPENAI_API_KEY=your-key npx tsx examples/search-tools.ts
174+
```
175+
157176
### OpenAPI Toolset Examples
158177

159178
#### [`openapi-toolset.ts`](./openapi-toolset.ts) - OpenAPI Integration

examples/ai-sdk-integration.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import { openai } from '@ai-sdk/openai';
88
import { generateText, stepCountIs } from 'ai';
9+
import { TEST_BASE_URL } from '../mocks/constants';
910
import { StackOneToolSet } from '../src';
1011

1112
describe('ai-sdk-integration example e2e', () => {
@@ -21,7 +22,7 @@ describe('ai-sdk-integration example e2e', () => {
2122
it('should fetch tools, convert to AI SDK format, and generate text with tool calls', async () => {
2223
const toolset = new StackOneToolSet({
2324
accountId: 'your-bamboohr-account-id',
24-
baseUrl: 'https://api.stackone.com',
25+
baseUrl: TEST_BASE_URL,
2526
});
2627

2728
// Fetch all tools for this account via MCP

examples/ai-sdk-integration.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,9 @@ if (!apiKey) {
2222
process.exit(1);
2323
}
2424

25-
// Replace with your actual account ID from StackOne dashboard
26-
const accountId = 'your-bamboohr-account-id';
27-
2825
const aiSdkIntegration = async (): Promise<void> => {
29-
// Initialize StackOne
30-
const toolset = new StackOneToolSet({
31-
accountId,
32-
baseUrl: process.env.STACKONE_BASE_URL ?? 'https://api.stackone.com',
33-
});
26+
// Initialize StackOne — reads STACKONE_API_KEY and STACKONE_ACCOUNT_ID from env
27+
const toolset = new StackOneToolSet();
3428

3529
// Fetch all tools for this account via MCP
3630
const tools = await toolset.fetchTools();

examples/anthropic-integration.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,9 @@ if (!apiKey) {
1313
process.exit(1);
1414
}
1515

16-
// Replace with your actual account ID from StackOne dashboard
17-
const accountId = 'your-hris-account-id';
18-
1916
const anthropicIntegration = async (): Promise<void> => {
20-
// Initialize StackOne
21-
const toolset = new StackOneToolSet({
22-
accountId,
23-
baseUrl: process.env.STACKONE_BASE_URL ?? 'https://api.stackone.com',
24-
});
17+
// Initialize StackOne — reads STACKONE_API_KEY and STACKONE_ACCOUNT_ID from env
18+
const toolset = new StackOneToolSet();
2519

2620
// Filter for any relevant tools
2721
const tools = await toolset.fetchTools({

examples/claude-agent-sdk-integration.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import { tool, createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk';
1313
import { z } from 'zod';
14+
import { TEST_BASE_URL } from '../mocks/constants';
1415
import { StackOneToolSet } from '../src';
1516

1617
describe('claude-agent-sdk-integration example e2e', () => {
@@ -25,7 +26,7 @@ describe('claude-agent-sdk-integration example e2e', () => {
2526
it('should fetch tools and create Claude Agent SDK tool wrapper', async () => {
2627
const toolset = new StackOneToolSet({
2728
accountId: 'your-bamboohr-account-id',
28-
baseUrl: 'https://api.stackone.com',
29+
baseUrl: TEST_BASE_URL,
2930
});
3031

3132
// Fetch all tools for this account via MCP
@@ -61,7 +62,7 @@ describe('claude-agent-sdk-integration example e2e', () => {
6162
it('should create MCP server with StackOne tools', async () => {
6263
const toolset = new StackOneToolSet({
6364
accountId: 'your-bamboohr-account-id',
64-
baseUrl: 'https://api.stackone.com',
65+
baseUrl: TEST_BASE_URL,
6566
});
6667

6768
const tools = await toolset.fetchTools();
@@ -99,7 +100,7 @@ describe('claude-agent-sdk-integration example e2e', () => {
99100
it('should execute tool handler directly', async () => {
100101
const toolset = new StackOneToolSet({
101102
accountId: 'your-bamboohr-account-id',
102-
baseUrl: 'https://api.stackone.com',
103+
baseUrl: TEST_BASE_URL,
103104
});
104105

105106
const tools = await toolset.fetchTools();

examples/claude-agent-sdk-integration.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,9 @@ if (!apiKey) {
1919
process.exit(1);
2020
}
2121

22-
// Replace with your actual account ID from StackOne dashboard
23-
const accountId = 'your-hris-account-id';
24-
2522
const claudeAgentSdkIntegration = async (): Promise<void> => {
26-
// Initialize StackOne
27-
const toolset = new StackOneToolSet({
28-
accountId,
29-
baseUrl: process.env.STACKONE_BASE_URL ?? 'https://api.stackone.com',
30-
});
23+
// Initialize StackOne — reads STACKONE_API_KEY and STACKONE_ACCOUNT_ID from env
24+
const toolset = new StackOneToolSet();
3125

3226
// Fetch tools from StackOne and convert to Claude Agent SDK format
3327
const tools = await toolset.fetchTools();

examples/fetch-tools-debug.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Interactive CLI Demo
2+
* Fetch Tools Debug CLI
33
*
44
* This example demonstrates how to build an interactive CLI tool using
55
* @clack/prompts to dynamically discover and execute StackOne tools.
@@ -11,7 +11,7 @@
1111
*
1212
* Run with:
1313
* ```bash
14-
* node --env-files=.env examples/interactive-cli.ts
14+
* npx tsx examples/fetch-tools-debug.ts
1515
* ```
1616
*/
1717

examples/fetch-tools.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import { http, HttpResponse } from 'msw';
88
import { server } from '../mocks/node';
9+
import { TEST_BASE_URL } from '../mocks/constants';
910
import { StackOneToolSet } from '../src';
1011

1112
describe('fetch-tools example e2e', () => {
@@ -20,7 +21,7 @@ describe('fetch-tools example e2e', () => {
2021
it('should fetch tools, filter by various criteria, and execute a tool', async () => {
2122
// Setup RPC handler for tool execution
2223
server.use(
23-
http.post('https://api.stackone.com/actions/rpc', async ({ request }) => {
24+
http.post(`${TEST_BASE_URL}/actions/rpc`, async ({ request }) => {
2425
const body: unknown = await request.json();
2526
assert(typeof body === 'object' && body !== null);
2627
const { action } = body as Record<string, unknown>;
@@ -40,7 +41,7 @@ describe('fetch-tools example e2e', () => {
4041
);
4142

4243
const toolset = new StackOneToolSet({
43-
baseUrl: 'https://api.stackone.com',
44+
baseUrl: TEST_BASE_URL,
4445
});
4546

4647
// Example 1: Fetch all tools (without account filter)

examples/fetch-tools.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* Example: fetch the latest StackOne tool catalog with filtering options.
33
*
4-
* Set `STACKONE_API_KEY` (and optionally `STACKONE_BASE_URL`) before running.
4+
* Set `STACKONE_API_KEY` before running.
55
* By default the script exits early in test environments where a real key is
66
* not available.
77
*/
@@ -15,9 +15,7 @@ if (!apiKey) {
1515
process.exit(1);
1616
}
1717

18-
const toolset = new StackOneToolSet({
19-
baseUrl: process.env.STACKONE_BASE_URL ?? 'https://api.stackone.com',
20-
});
18+
const toolset = new StackOneToolSet({});
2119

2220
// Example 1: Fetch all tools
2321
console.log('\n=== Example 1: Fetch all tools ===');
@@ -33,9 +31,9 @@ console.log(`Loaded ${toolsByAccounts.length} tools for specified accounts`);
3331
// Example 3: Filter by account IDs using options
3432
console.log('\n=== Example 3: Filter by account IDs (using options) ===');
3533
const toolsByAccountsOption = await toolset.fetchTools({
36-
accountIds: ['account-789'],
34+
accountIds: ['your-account-id'],
3735
});
38-
console.log(`Loaded ${toolsByAccountsOption.length} tools for account-789`);
36+
console.log(`Loaded ${toolsByAccountsOption.length} tools for your-account-id`);
3937

4038
// Example 4: Filter by providers
4139
console.log('\n=== Example 4: Filter by providers ===');

0 commit comments

Comments
 (0)