Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 29 additions & 148 deletions examples/search-tools.ts
Original file line number Diff line number Diff line change
@@ -1,168 +1,49 @@
/**
* This example demonstrates how to use semantic search for dynamic tool discovery.
* Semantic search allows AI agents to find relevant tools based on natural language queries
* using StackOne's search API with local BM25+TF-IDF fallback.
* Search tool patterns: callable wrapper and config overrides.
*
* Search config can be set at the constructor level via `{ search: SearchConfig }` and
* overridden per-call on `searchTools()`. Pass `{ search: null }` to disable search.
* SearchConfig: { method?: 'auto' | 'semantic' | 'local', topK?: number, minSimilarity?: number }
* For full agent execution, see agent-tool-search.ts.
*
* @example
* ```bash
* # Run with required environment variables:
* STACKONE_API_KEY=your-key OPENAI_API_KEY=your-key npx tsx examples/search-tools.ts
* ```
* Run with:
* STACKONE_API_KEY=xxx STACKONE_ACCOUNT_ID=xxx npx tsx examples/search-tools.ts
*/

import process from 'node:process';
import { openai } from '@ai-sdk/openai';
import { StackOneToolSet } from '@stackone/ai';
import { generateText, stepCountIs } from 'ai';

const apiKey = process.env.STACKONE_API_KEY;
const accountId = process.env.STACKONE_ACCOUNT_ID;

if (!apiKey) {
console.error('STACKONE_API_KEY environment variable is required');
console.error('Set STACKONE_API_KEY to run this example.');
process.exit(1);
}
if (!accountId) {
console.error('Set STACKONE_ACCOUNT_ID to run this example.');
process.exit(1);
}

/**
* Example 1: Search for tools with semantic search and use with AI SDK
*/
const searchToolsWithAISDK = async (): Promise<void> => {
console.log('Example 1: Semantic tool search with AI SDK\n');

// Configure search at the constructor level — applies to all searchTools() calls
const toolset = new StackOneToolSet({ search: { method: 'semantic', topK: 5 } });

// searchTools() inherits the constructor's search config
const tools = await toolset.searchTools('manage employee records and time off');

console.log(`Found ${tools.length} relevant tools`);

// Convert to AI SDK format and use with generateText
const aiSdkTools = await tools.toAISDK();

const { text, toolCalls } = await generateText({
model: openai('gpt-5.1'),
tools: aiSdkTools,
prompt: `List the first 5 employees from the HR system.`,
stopWhen: stepCountIs(3),
});

console.log('AI Response:', text);
console.log('\nTool calls made:', toolCalls?.map((call) => call.toolName).join(', '));
};

/**
* Example 2: Using SearchTool for agent loops
*/
const searchToolWithAgentLoop = async (): Promise<void> => {
console.log('\nExample 2: SearchTool for agent loops\n');

// Enable search with default method: 'auto'
const toolset = new StackOneToolSet({ search: {} });

// Per-call options override constructor defaults when needed
const searchTool = toolset.getSearchTool({ search: 'auto' });

// In an agent loop, search for tools as needed
const queries = ['create a new employee', 'list job candidates', 'send a message to a channel'];

for (const query of queries) {
const tools = await searchTool.search(query, { topK: 3 });
const toolNames = tools.toArray().map((t) => t.name);
console.log(`Query: "${query}" -> Found: ${toolNames.join(', ') || '(none)'}`);
}
};

/**
* Example 3: Lightweight action name search
*/
const searchActionNames = async (): Promise<void> => {
console.log('\nExample 3: Lightweight action name search\n');

const toolset = new StackOneToolSet({ search: {} });

// Search for action names without fetching full tool definitions
const results = await toolset.searchActionNames('manage employees', {
topK: 5,
});

console.log('Search results:');
for (const result of results) {
console.log(` - ${result.id}: score=${result.similarityScore.toFixed(2)}`);
}

// Then fetch specific tools based on the results
if (results.length > 0) {
const topActions = results.filter((r) => r.similarityScore > 0.7).map((r) => r.id);
console.log(`\nFetching tools for top actions: ${topActions.join(', ')}`);

const tools = await toolset.fetchTools({ actions: topActions });
console.log(`Fetched ${tools.length} tools`);
}
};

/**
* Example 4: Local-only search (no API call)
*/
const localSearchOnly = async (): Promise<void> => {
console.log('\nExample 4: Local-only BM25+TF-IDF search\n');

// Set search method at constructor level — all searchTools() calls use local search
const toolset = new StackOneToolSet({ search: { method: 'local', topK: 3 } });

// searchTools() inherits local search config from the constructor
const tools = await toolset.searchTools('create time off request');

console.log(`Found ${tools.length} tools using local search:`);
for (const tool of tools) {
console.log(` - ${tool.name}: ${tool.description}`);
}
};
// --- Example 1: getSearchTool() callable ---
console.log('=== getSearchTool() callable ===\n');

/**
* Example 5: Constructor-level topK vs per-call override
*/
const topKConfig = async (): Promise<void> => {
console.log('\nExample 5: topK at constructor vs per-call\n');
const toolset = new StackOneToolSet({ apiKey, accountId, search: {} });
const searchTool = toolset.getSearchTool();

// Constructor-level topK — all calls default to returning 3 results
const toolset = new StackOneToolSet({ search: { topK: 3 } });
const queries = ['cancel an event', 'list employees', 'send a message'];
for (const query of queries) {
const tools = await searchTool.search(query, { topK: 3 });
const names = tools.toArray().map((t) => t.name);
console.log(` "${query}" -> ${names.join(', ') || '(none)'}`);
}

const query = 'manage employee records';
console.log(`Constructor topK=3: searching for "${query}"`);
const toolsDefault = await toolset.searchTools(query);
console.log(` Got ${toolsDefault.length} tools (constructor default)`);
for (const tool of toolsDefault) {
console.log(` - ${tool.name}`);
}
// --- Example 2: Constructor topK vs per-call override ---
console.log('\n=== Constructor topK vs per-call override ===\n');

// Per-call override — this single call returns up to 10 results
console.log('\nPer-call topK=10: overriding constructor default');
const toolsOverride = await toolset.searchTools(query, { topK: 10 });
console.log(` Got ${toolsOverride.length} tools (per-call override)`);
for (const tool of toolsOverride) {
console.log(` - ${tool.name}`);
}
};
const toolset3 = new StackOneToolSet({ apiKey, accountId, search: { topK: 3 } });

// Main execution
const main = async (): Promise<void> => {
try {
if (process.env.OPENAI_API_KEY) {
await searchToolsWithAISDK();
} else {
console.log('OPENAI_API_KEY not found, skipping AI SDK example\n');
}
const query = 'manage employee records';

await searchToolWithAgentLoop();
await searchActionNames();
await localSearchOnly();
await topKConfig();
} catch (error) {
console.error('Error running examples:', error);
}
};
const tools3 = await toolset3.searchTools(query);
console.log(`Constructor topK=3: got ${tools3.length} tools`);

await main();
const toolsOverride = await toolset3.searchTools(query, { topK: 10 });
console.log(`Per-call topK=10 (overrides constructor 3): got ${toolsOverride.length} tools`);
90 changes: 90 additions & 0 deletions examples/workday-integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* Workday integration: timeout and account scoping for slow providers.
*
* Workday can take 10-15s to respond. This example shows how to configure
* timeout for slow providers.
*
* Prerequisites:
* - STACKONE_API_KEY
* - STACKONE_ACCOUNT_ID (a Workday-connected account)
* - OPENAI_API_KEY
*
* Run with:
* STACKONE_API_KEY=xxx OPENAI_API_KEY=xxx STACKONE_ACCOUNT_ID=xxx npx tsx examples/workday-integration.ts
*/

import process from 'node:process';
import { StackOneToolSet } from '@stackone/ai';
import OpenAI from 'openai';

const apiKey = process.env.STACKONE_API_KEY ?? '';
const accountId = process.env.STACKONE_ACCOUNT_ID ?? '';

if (!apiKey) {
console.error('Set STACKONE_API_KEY to run this example.');
process.exit(1);
}
if (!accountId) {
console.error('Set STACKONE_ACCOUNT_ID to run this example.');
process.exit(1);
}
if (!process.env.OPENAI_API_KEY) {
console.error('Set OPENAI_API_KEY to run this example.');
process.exit(1);
}

// Timeout for slow providers (Workday can take 10-15s)
const toolset = new StackOneToolSet({
apiKey,
accountId,
search: { method: 'auto', topK: 5 },
timeout: 120_000,
});

const client = new OpenAI();

async function runAgent(
messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[],
tools: OpenAI.Chat.Completions.ChatCompletionTool[],
maxSteps = 10,
): Promise<void> {
for (let i = 0; i < maxSteps; i++) {
const response = await client.chat.completions.create({ model: 'gpt-5.4', messages, tools });
const choice = response.choices[0];

if (!choice.message.tool_calls?.length) {
console.log(`Answer: ${choice.message.content}`);
return;
}

messages.push(choice.message);
for (const tc of choice.message.tool_calls) {
if (tc.type !== 'function') continue;
console.log(` -> ${tc.function.name}(${tc.function.arguments.slice(0, 80)})`);
const searchTools = toolset.getTools({ accountIds: [accountId] });
const tool = searchTools.getTool(tc.function.name);
const result = tool ? await tool.execute(tc.function.arguments) : { error: 'Unknown tool' };
messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(result) });
}
}
}

// --- Example 1: Search and execute mode ---
console.log('=== Search and execute mode ===\n');
const searchTools = toolset.getTools({ accountIds: [accountId] }).toOpenAI();
await runAgent(
[
{ role: 'system', content: 'Use tool_search to find tools, then tool_execute to run them.' },
{ role: 'user', content: 'List the first 5 employees.' },
],
searchTools,
);

// --- Example 2: Normal mode ---
console.log('\n=== Normal mode ===\n');
const tools = await toolset.fetchTools({ actions: ['workday_*_employee*'] });
if (tools.length === 0) {
console.log('No Workday tools found for this account.');
} else {
await runAgent([{ role: 'user', content: 'List the first 5 employees.' }], tools.toOpenAI());
}
33 changes: 27 additions & 6 deletions src/rpc-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ export type { RpcActionResponse } from './schema';
export class RpcClient {
private readonly baseUrl: string;
private readonly authHeader: string;
private readonly timeout: number;

constructor(config: RpcClientConfig) {
const validatedConfig = rpcClientConfigSchema.parse(config);
this.baseUrl = validatedConfig.serverURL || DEFAULT_BASE_URL;
const username = validatedConfig.security.username;
const password = validatedConfig.security.password || '';
this.authHeader = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
this.timeout = validatedConfig.timeout ?? 60_000;
}

/**
Expand Down Expand Up @@ -73,13 +75,32 @@ export class RpcClient {
...forwardedHeaders,
} satisfies Record<string, string>;

const response = await fetch(url, {
method: 'POST',
headers: httpHeaders,
body: JSON.stringify(requestBody),
});
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);

const responseBody: unknown = await response.json();
let response: Response;
let responseBody: unknown;
try {
response = await fetch(url, {
method: 'POST',
headers: httpHeaders,
body: JSON.stringify(requestBody),
signal: controller.signal,
});
responseBody = await response.json();
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
throw new StackOneAPIError(
`Request timed out after ${this.timeout}ms for ${url}`,
0,
null,
requestBody,
);
}
throw error;
} finally {
clearTimeout(timeoutId);
}

if (!response.ok) {
throw new StackOneAPIError(
Expand Down
1 change: 1 addition & 0 deletions src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const rpcClientConfigSchema = z.object({
username: z.string(),
password: z.optional(z.string()),
}),
timeout: z.optional(z.number()),
});

/**
Expand Down
Loading
Loading