Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 | 12x 12x 15x 15x 15x 29x 29x 29x 29x 29x 29x 9x 29x 19x 29x 29x 1x 29x 29x 29x 29x 28x 11x 11x 17x 17x 17x 12x 11x 1x 1x 29x 1x 2x | /**
* Semantic search client for StackOne action search API.
*
* How Semantic Search Works
* =========================
*
* The SDK provides three ways to discover tools using semantic search.
* Each path trades off between speed, filtering, and completeness.
*
* 1. `searchTools(query)` — Full tool discovery (recommended for agent frameworks)
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*
* This is the primary method used when integrating with OpenAI, Anthropic, or AI SDK.
* The internal flow is:
*
* 1. Fetch ALL tools from linked accounts via MCP (uses accountIds to scope the request)
* 2. Extract available connectors from the fetched tools (e.g. {bamboohr, hibob})
* 3. Search EACH connector in parallel via the semantic search API (/actions/search)
* 4. Collect results, sort by relevance score, apply topK if specified
* 5. Match semantic results back to the fetched tool definitions
* 6. Return Tools sorted by relevance score
*
* Key point: only the user's own connectors are searched — no wasted results
* from connectors the user doesn't have. Tools are fetched first, semantic
* search runs second, and only tools that exist in the user's linked
* accounts AND match the semantic query are returned. This prevents
* suggesting tools the user cannot execute.
*
* If the semantic API is unavailable, the SDK falls back to a local
* BM25 + TF-IDF hybrid search over the fetched tools (unless
* `search: "semantic"` is specified).
*
*
* 2. `searchActionNames(query)` — Lightweight discovery
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*
* Queries the semantic API directly and returns action name metadata
* (name, connector, score, description) **without** fetching full tool
* definitions. This is useful for previewing results before committing
* to a full fetch.
*
* When `accountIds` are provided, each connector is searched in
* parallel (same as `searchTools`). Without `accountIds`, results
* come from the full StackOne catalog.
*
*
* 3. `toolset.getSearchTool()` — Agent-loop callable
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*
* Returns a `SearchTool` instance that wraps `searchTools()`.
* Call it with a natural language query to get a `Tools` collection
* back. Designed for agent loops where the LLM decides what to search for.
*/
import { DEFAULT_BASE_URL } from './consts';
import { StackOneError } from './utils/error-stackone';
/**
* Raised when semantic search fails.
*/
export class SemanticSearchError extends StackOneError {
constructor(message: string, options?: ErrorOptions) {
super(message, options);
this.name = 'SemanticSearchError';
}
}
/**
* Single result from semantic search API.
*/
export interface SemanticSearchResult {
actionName: string;
connectorKey: string;
similarityScore: number;
label: string;
description: string;
projectId: string;
}
/**
* Response from /actions/search endpoint.
*/
export interface SemanticSearchResponse {
results: SemanticSearchResult[];
totalCount: number;
query: string;
connectorFilter?: string;
projectFilter?: string;
}
/**
* Options for semantic search
*/
export interface SemanticSearchOptions {
connector?: string;
topK?: number;
projectId?: string;
minSimilarity?: number;
}
/**
* Client for StackOne semantic search API.
*
* This client provides access to the semantic search endpoint which uses
* enhanced embeddings for higher accuracy than local BM25+TF-IDF search.
*
* @example
* ```typescript
* const client = new SemanticSearchClient({ apiKey: 'sk-xxx' });
* const response = await client.search('create employee', { connector: 'bamboohr', topK: 5 });
* for (const result of response.results) {
* console.log(`${result.actionName}: ${result.similarityScore.toFixed(2)}`);
* }
* ```
*/
export class SemanticSearchClient {
private readonly apiKey: string;
private readonly baseUrl: string;
private readonly timeout: number;
constructor({
apiKey,
baseUrl = DEFAULT_BASE_URL,
timeout = 30_000,
}: {
apiKey: string;
baseUrl?: string;
timeout?: number;
}) {
this.apiKey = apiKey;
this.baseUrl = baseUrl.replace(/\/+$/, '');
this.timeout = timeout;
}
/**
* Build the Basic auth header.
*/
private buildAuthHeader(): string {
const token = Buffer.from(`${this.apiKey}:`).toString('base64');
return `Basic ${token}`;
}
/**
* Search for relevant actions using semantic search.
*
* @param query - Natural language query describing what tools/actions you need
* @param options - Search options (connector, topK, projectId, minSimilarity)
* @returns SemanticSearchResponse containing matching actions with similarity scores
* @throws SemanticSearchError if the API call fails
*
* @example
* ```typescript
* const response = await client.search('onboard a new team member', { topK: 5 });
* for (const result of response.results) {
* console.log(`${result.actionName}: ${result.similarityScore.toFixed(2)}`);
* }
* ```
*/
async search(query: string, options?: SemanticSearchOptions): Promise<SemanticSearchResponse> {
const url = `${this.baseUrl}/actions/search`;
const headers: Record<string, string> = {
Authorization: this.buildAuthHeader(),
'Content-Type': 'application/json',
};
const payload: Record<string, unknown> = { query };
if (options?.topK != null) {
payload.top_k = options.topK;
}
if (options?.connector) {
payload.connector = options.connector;
}
Iif (options?.projectId) {
payload.project_id = options.projectId;
}
if (options?.minSimilarity != null) {
payload.min_similarity = options.minSimilarity;
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
try {
const response = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify(payload),
signal: controller.signal,
});
if (!response.ok) {
const text = await response.text();
throw new SemanticSearchError(`API error: ${response.status} - ${text}`);
}
const data = (await response.json()) as {
results: Array<{
action_name: string;
connector_key: string;
similarity_score: number;
label: string;
description: string;
project_id?: string;
}>;
total_count: number;
query: string;
connector_filter?: string;
project_filter?: string;
};
return {
results: data.results.map((r) => ({
actionName: r.action_name,
connectorKey: r.connector_key,
similarityScore: r.similarity_score,
label: r.label,
description: r.description,
projectId: r.project_id ?? 'global',
})),
totalCount: data.total_count,
query: data.query,
connectorFilter: data.connector_filter,
projectFilter: data.project_filter,
};
} catch (error) {
if (error instanceof SemanticSearchError) {
throw error;
}
Iif (error instanceof Error && error.name === 'AbortError') {
throw new SemanticSearchError(`Request timed out after ${this.timeout}ms`);
}
throw new SemanticSearchError(
`Search failed: ${error instanceof Error ? error.message : String(error)}`,
{ cause: error },
);
} finally {
clearTimeout(timeoutId);
}
}
/**
* Convenience method returning just action names.
*
* @param query - Natural language query
* @param options - Search options (connector, topK, minSimilarity, projectId)
* @returns List of action names sorted by relevance
*
* @example
* ```typescript
* const actionNames = await client.searchActionNames('create employee', {
* connector: 'bamboohr',
* minSimilarity: 0.5,
* });
* ```
*/
async searchActionNames(query: string, options?: SemanticSearchOptions): Promise<string[]> {
const response = await this.search(query, options);
return response.results.map((r) => r.actionName);
}
}
|