Skip to content

Commit e06602b

Browse files
feat(search-tools): LLM-driven search and execute and new API (#325)
* add LLM-driven tool_search and tool_execute * chore: retrigger CI * fix lint and tests * lint and tests * Lint formatter CI vs local * PR Suggestion from bots * Fix linter error * Port the search execute changes to the node * Fix CI * Adopt the latest API changes * update the files * Fix test * Fix lint issues * update the doc strings
1 parent f8ec8c4 commit e06602b

9 files changed

Lines changed: 881 additions & 99 deletions

examples/agent-tool-search.ts

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/**
2+
* This example demonstrates the search and execute tools pattern (tool_search + tool_execute)
3+
* for LLM-driven tool discovery and execution.
4+
*
5+
* Instead of loading all tools upfront, the LLM autonomously searches for
6+
* relevant tools and executes them — keeping token usage minimal.
7+
*
8+
* @example
9+
* ```bash
10+
* # Run with required environment variables:
11+
* STACKONE_API_KEY=your-key OPENAI_API_KEY=your-key STACKONE_ACCOUNT_ID=your-account npx tsx examples/agent-tool-search.ts
12+
* ```
13+
*/
14+
15+
import process from 'node:process';
16+
import { openai } from '@ai-sdk/openai';
17+
import { StackOneToolSet } from '@stackone/ai';
18+
import { generateText, stepCountIs } from 'ai';
19+
import OpenAI from 'openai';
20+
21+
const apiKey = process.env.STACKONE_API_KEY;
22+
if (!apiKey) {
23+
console.error('STACKONE_API_KEY environment variable is required');
24+
process.exit(1);
25+
}
26+
27+
if (!process.env.OPENAI_API_KEY) {
28+
console.error('OPENAI_API_KEY environment variable is required');
29+
process.exit(1);
30+
}
31+
32+
const accountId = process.env.STACKONE_ACCOUNT_ID;
33+
34+
/**
35+
* Example 1: Search and execute with Vercel AI SDK
36+
*
37+
* The LLM receives only tool_search and tool_execute — two small tool definitions
38+
* regardless of how many tools exist. It searches for what it needs and executes.
39+
*/
40+
const toolsWithAISDK = async (): Promise<void> => {
41+
console.log('Example 1: Search and execute with Vercel AI SDK\n');
42+
43+
const toolset = new StackOneToolSet({
44+
search: { method: 'semantic', topK: 3 },
45+
...(accountId ? { accountId } : {}),
46+
});
47+
48+
// Get search and execute tools — returns a Tools collection with tool_search + tool_execute
49+
const accountIds = accountId ? [accountId] : [];
50+
const tools = toolset.getTools({ accountIds });
51+
52+
console.log(
53+
`Search and execute: ${tools
54+
.toArray()
55+
.map((t) => t.name)
56+
.join(', ')}`,
57+
);
58+
console.log();
59+
60+
// Pass to the LLM — it will search for calendly tools, then execute
61+
const { text, steps } = await generateText({
62+
model: openai('gpt-5.4'),
63+
tools: await tools.toAISDK(),
64+
prompt: 'List my upcoming Calendly events for the next week.',
65+
stopWhen: stepCountIs(10),
66+
});
67+
68+
console.log('AI Response:', text);
69+
console.log('\nSteps taken:');
70+
for (const step of steps) {
71+
for (const call of step.toolCalls ?? []) {
72+
const args = (call as unknown as Record<string, unknown>).args;
73+
const argsStr = args ? JSON.stringify(args).slice(0, 100) : '{}';
74+
console.log(` - ${call.toolName}(${argsStr})`);
75+
}
76+
}
77+
};
78+
79+
/**
80+
* Example 2: Search and execute with OpenAI Chat Completions
81+
*
82+
* Same pattern, different framework. The search and execute tools convert to any format.
83+
*/
84+
const toolsWithOpenAI = async (): Promise<void> => {
85+
console.log('\nExample 2: Search and execute with OpenAI Chat Completions\n');
86+
87+
const toolset = new StackOneToolSet({
88+
search: { method: 'semantic', topK: 3 },
89+
...(accountId ? { accountId } : {}),
90+
});
91+
92+
const accountIds = accountId ? [accountId] : [];
93+
const tools = toolset.getTools({ accountIds });
94+
const openaiTools = tools.toOpenAI();
95+
96+
const client = new OpenAI();
97+
const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
98+
{
99+
role: 'system',
100+
content:
101+
'You are a helpful scheduling assistant. Use tool_search to find relevant tools, then tool_execute to run them. Always read the parameter schemas from tool_search results carefully. If a tool needs a user URI, first search for and call a "get current user" tool to obtain it. If a tool execution fails, try different parameters or a different tool.',
102+
},
103+
{
104+
role: 'user',
105+
content: 'Check my upcoming Calendly events and list them.',
106+
},
107+
];
108+
109+
// Agent loop — let the LLM drive search and execution
110+
const maxIterations = 10;
111+
for (let i = 0; i < maxIterations; i++) {
112+
const response = await client.chat.completions.create({
113+
model: 'gpt-5.4',
114+
messages,
115+
tools: openaiTools,
116+
tool_choice: 'auto',
117+
});
118+
119+
const choice = response.choices[0];
120+
121+
if (!choice.message.tool_calls?.length) {
122+
console.log('Final response:', choice.message.content);
123+
break;
124+
}
125+
126+
// Add assistant message with tool calls
127+
messages.push(choice.message);
128+
129+
// Execute each tool call
130+
for (const toolCall of choice.message.tool_calls) {
131+
if (toolCall.type !== 'function') {
132+
continue;
133+
}
134+
135+
console.log(`LLM called: ${toolCall.function.name}(${toolCall.function.arguments})`);
136+
137+
const tool = tools.getTool(toolCall.function.name);
138+
if (!tool) {
139+
messages.push({
140+
role: 'tool',
141+
tool_call_id: toolCall.id,
142+
content: JSON.stringify({ error: `Unknown tool: ${toolCall.function.name}` }),
143+
});
144+
continue;
145+
}
146+
147+
const result = await tool.execute(toolCall.function.arguments);
148+
messages.push({
149+
role: 'tool',
150+
tool_call_id: toolCall.id,
151+
content: JSON.stringify(result),
152+
});
153+
}
154+
}
155+
};
156+
157+
// Main execution
158+
const main = async (): Promise<void> => {
159+
try {
160+
await toolsWithAISDK();
161+
await toolsWithOpenAI();
162+
} catch (error) {
163+
console.error('Error running examples:', error);
164+
}
165+
};
166+
167+
await main();

examples/search-tools.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ const searchToolsWithAISDK = async (): Promise<void> => {
5959
const searchToolWithAgentLoop = async (): Promise<void> => {
6060
console.log('\nExample 2: SearchTool for agent loops\n');
6161

62-
// Default constructor — search enabled with method: 'auto'
63-
const toolset = new StackOneToolSet();
62+
// Enable search with default method: 'auto'
63+
const toolset = new StackOneToolSet({ search: {} });
6464

6565
// Per-call options override constructor defaults when needed
6666
const searchTool = toolset.getSearchTool({ search: 'auto' });
@@ -81,7 +81,7 @@ const searchToolWithAgentLoop = async (): Promise<void> => {
8181
const searchActionNames = async (): Promise<void> => {
8282
console.log('\nExample 3: Lightweight action name search\n');
8383

84-
const toolset = new StackOneToolSet();
84+
const toolset = new StackOneToolSet({ search: {} });
8585

8686
// Search for action names without fetching full tool definitions
8787
const results = await toolset.searchActionNames('manage employees', {
@@ -90,14 +90,12 @@ const searchActionNames = async (): Promise<void> => {
9090

9191
console.log('Search results:');
9292
for (const result of results) {
93-
console.log(
94-
` - ${result.actionName} (${result.connectorKey}): score=${result.similarityScore.toFixed(2)}`,
95-
);
93+
console.log(` - ${result.id}: score=${result.similarityScore.toFixed(2)}`);
9694
}
9795

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

103101
const tools = await toolset.fetchTools({ actions: topActions });

0 commit comments

Comments
 (0)