Skip to content

Commit c30c24e

Browse files
refactor earch to make aligned with defender
1 parent 8d0e6ff commit c30c24e

4 files changed

Lines changed: 103 additions & 35 deletions

File tree

examples/search-tools.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
* Semantic search allows AI agents to find relevant tools based on natural language queries
44
* using StackOne's search API with local BM25+TF-IDF fallback.
55
*
6+
* Search config can be set at the constructor level via `{ search: SearchConfig }` and
7+
* overridden per-call on `searchTools()`. Pass `{ search: null }` to disable search.
8+
* SearchConfig: { method?: 'auto' | 'semantic' | 'local', topK?: number, minSimilarity?: number }
9+
*
610
* @example
711
* ```bash
812
* # Run with required environment variables:
@@ -27,14 +31,11 @@ if (!apiKey) {
2731
const searchToolsWithAISDK = async (): Promise<void> => {
2832
console.log('Example 1: Semantic tool search with AI SDK\n');
2933

30-
// Initialize StackOne — reads STACKONE_API_KEY and STACKONE_ACCOUNT_ID from env
31-
const toolset = new StackOneToolSet();
34+
// Configure search at the constructor level — applies to all searchTools() calls
35+
const toolset = new StackOneToolSet({ search: { method: 'semantic', topK: 5 } });
3236

33-
// Search for relevant tools using semantic search
34-
const tools = await toolset.searchTools('manage employee records and time off', {
35-
topK: 5,
36-
search: 'auto', // tries semantic API first, falls back to local search
37-
});
37+
// searchTools() inherits the constructor's search config
38+
const tools = await toolset.searchTools('manage employee records and time off');
3839

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

@@ -58,9 +59,10 @@ const searchToolsWithAISDK = async (): Promise<void> => {
5859
const searchToolWithAgentLoop = async (): Promise<void> => {
5960
console.log('\nExample 2: SearchTool for agent loops\n');
6061

62+
// Default constructor — search enabled with method: 'auto'
6163
const toolset = new StackOneToolSet();
6264

63-
// Get a reusable search tool
65+
// Per-call options override constructor defaults when needed
6466
const searchTool = toolset.getSearchTool({ search: 'auto' });
6567

6668
// In an agent loop, search for tools as needed
@@ -109,13 +111,11 @@ const searchActionNames = async (): Promise<void> => {
109111
const localSearchOnly = async (): Promise<void> => {
110112
console.log('\nExample 4: Local-only BM25+TF-IDF search\n');
111113

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

114-
// Use local search mode (BM25 + TF-IDF, no API call to semantic search endpoint)
115-
const tools = await toolset.searchTools('create time off request', {
116-
search: 'local',
117-
topK: 3,
118-
});
117+
// searchTools() inherits local search config from the constructor
118+
const tools = await toolset.searchTools('create time off request');
119119

120120
console.log(`Found ${tools.length} tools using local search:`);
121121
for (const tool of tools) {

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,6 @@ export type {
3737
JsonObject,
3838
JsonValue,
3939
ParameterLocation,
40+
SearchConfig,
4041
ToolDefinition,
4142
} from './types';

src/toolsets.ts

Lines changed: 69 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {
1717
JsonObject,
1818
JsonSchemaProperties,
1919
RpcExecuteConfig,
20+
SearchConfig,
2021
ToolParameters,
2122
} from './types';
2223
import { StackOneError } from './utils/error-stackone';
@@ -129,6 +130,17 @@ type AccountConfig = SimplifyDeep<MergeExclusive<SingleAccountConfig, MultipleAc
129130
interface StackOneToolSetBaseConfig extends BaseToolSetConfig {
130131
apiKey?: string;
131132
strict?: boolean;
133+
/**
134+
* Search configuration. Controls default search behavior for `searchTools()`,
135+
* `getSearchTool()`, and `searchActionNames()`.
136+
*
137+
* - Omit or pass `undefined` → search enabled with defaults (`method: 'auto'`)
138+
* - Pass `null` → search disabled
139+
* - Pass `{ method, topK, minSimilarity }` → search enabled with custom defaults
140+
*
141+
* Per-call options always override these defaults.
142+
*/
143+
search?: SearchConfig | null;
132144
}
133145

134146
/**
@@ -215,24 +227,27 @@ export interface SearchActionNamesOptions {
215227
*/
216228
export class SearchTool {
217229
private readonly toolset: StackOneToolSet;
218-
private readonly defaultSearch: SearchMode;
230+
private readonly defaultConfig: SearchConfig;
219231

220-
constructor(toolset: StackOneToolSet, search: SearchMode = 'auto') {
232+
constructor(toolset: StackOneToolSet, config: SearchConfig = {}) {
221233
this.toolset = toolset;
222-
this.defaultSearch = search;
234+
this.defaultConfig = config;
223235
}
224236

225237
/**
226238
* Search for tools using natural language.
227239
*
228240
* @param query - Natural language description of needed functionality
229-
* @param options - Search options (connector, topK, minSimilarity, accountIds, search)
241+
* @param options - Search options (connector, topK, minSimilarity, accountIds, search).
242+
* Per-call options override the defaults from the constructor config.
230243
* @returns Tools collection with matched tools
231244
*/
232245
async search(query: string, options?: SearchToolsOptions): Promise<Tools> {
233246
return this.toolset.searchTools(query, {
234247
...options,
235-
search: options?.search ?? this.defaultSearch,
248+
search: options?.search ?? this.defaultConfig.method,
249+
topK: options?.topK ?? this.defaultConfig.topK,
250+
minSimilarity: options?.minSimilarity ?? this.defaultConfig.minSimilarity,
236251
});
237252
}
238253
}
@@ -245,6 +260,7 @@ export class StackOneToolSet {
245260
private authentication?: AuthenticationConfig;
246261
private headers: Record<string, string>;
247262
private rpcClient?: RpcClient;
263+
private readonly searchConfig: SearchConfig | null;
248264

249265
/**
250266
* Account ID for StackOne API
@@ -301,6 +317,9 @@ export class StackOneToolSet {
301317
this.accountId = accountId;
302318
this.accountIds = config?.accountIds ?? [];
303319

320+
// Resolve search config: undefined → defaults, null → disabled, object → custom
321+
this.searchConfig = config?.search === null ? null : { method: 'auto', ...config?.search };
322+
304323
// Set Authentication headers if provided
305324
if (this.authentication) {
306325
// Only set auth headers if they don't already exist in custom headers
@@ -400,7 +419,17 @@ export class StackOneToolSet {
400419
* ```
401420
*/
402421
getSearchTool(options?: { search?: SearchMode }): SearchTool {
403-
return new SearchTool(this, options?.search ?? 'auto');
422+
if (this.searchConfig === null) {
423+
throw new ToolSetConfigError(
424+
'Search is disabled. Initialize StackOneToolSet with a search config to enable.',
425+
);
426+
}
427+
428+
const config: SearchConfig = options?.search
429+
? { ...this.searchConfig, method: options.search }
430+
: this.searchConfig;
431+
432+
return new SearchTool(this, config);
404433
}
405434

406435
/**
@@ -433,8 +462,18 @@ export class StackOneToolSet {
433462
* ```
434463
*/
435464
async searchTools(query: string, options?: SearchToolsOptions): Promise<Tools> {
436-
const search = options?.search ?? 'auto';
437-
const allTools = await this.fetchTools({ accountIds: options?.accountIds });
465+
if (this.searchConfig === null) {
466+
throw new ToolSetConfigError(
467+
'Search is disabled. Initialize StackOneToolSet with a search config to enable.',
468+
);
469+
}
470+
471+
const search = options?.search ?? this.searchConfig.method ?? 'auto';
472+
const topK = options?.topK ?? this.searchConfig.topK;
473+
const minSimilarity = options?.minSimilarity ?? this.searchConfig.minSimilarity;
474+
const mergedOptions = { ...options, search, topK, minSimilarity };
475+
476+
const allTools = await this.fetchTools({ accountIds: mergedOptions.accountIds });
438477
const availableConnectors = allTools.getConnectors();
439478

440479
if (availableConnectors.size === 0) {
@@ -443,14 +482,14 @@ export class StackOneToolSet {
443482

444483
// Local-only search — skip semantic API entirely
445484
if (search === 'local') {
446-
return this.localSearch(query, allTools, options);
485+
return this.localSearch(query, allTools, mergedOptions);
447486
}
448487

449488
try {
450489
// Determine which connectors to search
451490
let connectorsToSearch: Set<string>;
452-
if (options?.connector) {
453-
const connectorLower = options.connector.toLowerCase();
491+
if (mergedOptions.connector) {
492+
const connectorLower = mergedOptions.connector.toLowerCase();
454493
connectorsToSearch = availableConnectors.has(connectorLower)
455494
? new Set([connectorLower])
456495
: new Set();
@@ -468,7 +507,7 @@ export class StackOneToolSet {
468507
client = this.getSemanticClient();
469508
} catch (error) {
470509
if (search === 'auto' && error instanceof ToolSetConfigError) {
471-
return this.localSearch(query, allTools, options);
510+
return this.localSearch(query, allTools, mergedOptions);
472511
}
473512
throw error;
474513
}
@@ -479,8 +518,8 @@ export class StackOneToolSet {
479518
try {
480519
const response = await client.search(query, {
481520
connector,
482-
topK: options?.topK,
483-
minSimilarity: options?.minSimilarity,
521+
topK: mergedOptions.topK,
522+
minSimilarity: mergedOptions.minSimilarity,
484523
});
485524
return response.results;
486525
} catch (error) {
@@ -504,7 +543,7 @@ export class StackOneToolSet {
504543

505544
// Sort by score, apply topK
506545
allResults.sort((a, b) => b.similarityScore - a.similarityScore);
507-
const topResults = options?.topK != null ? allResults.slice(0, options.topK) : allResults;
546+
const topResults = mergedOptions.topK != null ? allResults.slice(0, mergedOptions.topK) : allResults;
508547

509548
if (topResults.length === 0) {
510549
return new Tools([]);
@@ -530,7 +569,7 @@ export class StackOneToolSet {
530569
}
531570

532571
// Auto mode: silently fall back to local search
533-
return this.localSearch(query, allTools, options);
572+
return this.localSearch(query, allTools, mergedOptions);
534573
}
535574
throw error;
536575
}
@@ -565,6 +604,15 @@ export class StackOneToolSet {
565604
query: string,
566605
options?: SearchActionNamesOptions,
567606
): Promise<SemanticSearchResult[]> {
607+
if (this.searchConfig === null) {
608+
throw new ToolSetConfigError(
609+
'Search is disabled. Initialize StackOneToolSet with a search config to enable.',
610+
);
611+
}
612+
613+
const effectiveTopK = options?.topK ?? this.searchConfig.topK;
614+
const effectiveMinSimilarity = options?.minSimilarity ?? this.searchConfig.minSimilarity;
615+
568616
// Resolve available connectors from account IDs
569617
let availableConnectors: Set<string> | undefined;
570618
const effectiveAccountIds = options?.accountIds || this.accountIds;
@@ -596,8 +644,8 @@ export class StackOneToolSet {
596644
try {
597645
const response = await client.search(query, {
598646
connector,
599-
topK: options?.topK,
600-
minSimilarity: options?.minSimilarity,
647+
topK: effectiveTopK,
648+
minSimilarity: effectiveMinSimilarity,
601649
});
602650
return response.results;
603651
} catch {
@@ -613,8 +661,8 @@ export class StackOneToolSet {
613661
// No account filtering — single global search
614662
const response = await client.search(query, {
615663
connector: options?.connector,
616-
topK: options?.topK,
617-
minSimilarity: options?.minSimilarity,
664+
topK: effectiveTopK,
665+
minSimilarity: effectiveMinSimilarity,
618666
});
619667
allResults = response.results;
620668
}
@@ -626,7 +674,7 @@ export class StackOneToolSet {
626674
actionName: normalizeActionName(r.actionName),
627675
}));
628676

629-
return options?.topK != null ? normalized.slice(0, options.topK) : normalized;
677+
return effectiveTopK != null ? normalized.slice(0, effectiveTopK) : normalized;
630678
} catch (error) {
631679
if (error instanceof SemanticSearchError) {
632680
return [];

src/types.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,25 @@ export type AISDKToolResult<T extends string = string> = ToolSet & {
205205
/**
206206
* Options for toClaudeAgentSdk() method
207207
*/
208+
/**
209+
* Search configuration for the StackOneToolSet constructor.
210+
*
211+
* When provided as an object, sets default search options that flow through
212+
* to `searchTools()`, `getSearchTool()`, and `searchActionNames()`.
213+
* Per-call options override these defaults.
214+
*
215+
* When set to `null`, search is disabled entirely.
216+
* When omitted (`undefined`), defaults to `{ method: 'auto' }`.
217+
*/
218+
export interface SearchConfig {
219+
/** Search backend to use. Defaults to `'auto'`. */
220+
method?: 'auto' | 'semantic' | 'local';
221+
/** Maximum number of tools to return. */
222+
topK?: number;
223+
/** Minimum similarity score threshold 0-1. */
224+
minSimilarity?: number;
225+
}
226+
208227
export interface ClaudeAgentSdkOptions {
209228
/**
210229
* Name of the MCP server. Defaults to 'stackone-tools'.

0 commit comments

Comments
 (0)