@with-logic/intent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,402 @@
1
+ import { CONFIG } from "./config";
2
+ import type { IntentOptions } from "./types";
3
+ /**
4
+ * LLM-based reranker for arbitrary items.
5
+ *
6
+ * Uses a listwise LLM approach to score candidates within a configurable range based on relevance to a query,
7
+ * then filters by threshold and returns results sorted by score with stable ordering.
8
+ *
9
+ * @template T - The type of items to rerank (defaults to any)
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * // Simplest: uses defaults and GROQ_API_KEY from environment
14
+ * const intent = new Intent();
15
+ * const ranked = await intent.rank("find expense reports", items);
16
+ *
17
+ * // With custom extractors
18
+ * type Document = { id: string; title: string; content: string };
19
+ * const intent = new Intent<Document>({
20
+ * key: doc => doc.title,
21
+ * summary: doc => doc.content.slice(0, 200),
22
+ * relevancyThreshold: 5,
23
+ * batchSize: 20
24
+ * });
25
+ *
26
+ * // With custom LLM client
27
+ * const intent = new Intent<Document>({
28
+ * llm: myClient,
29
+ * userId: "user-123",
30
+ * key: doc => doc.title,
31
+ * summary: doc => doc.content.slice(0, 200)
32
+ * });
33
+ * ```
34
+ */
35
+ export declare class Intent<T = any> {
36
+ private readonly cfg;
37
+ private readonly llm;
38
+ private readonly ctx;
39
+ private readonly extractors;
40
+ private readonly env;
41
+ /**
42
+ * Resolve the model name to use for this Intent instance.
43
+ *
44
+ * Intent is provider-driven. Today only GROQ is supported; when using GROQ
45
+ * we always take the model from GROQ's config defaults.
46
+ *
47
+ * @returns Provider-specific model name
48
+ * @private
49
+ */
50
+ private resolveModel;
51
+ /**
52
+ * Builds the context object from options.
53
+ *
54
+ * Constructs an IntentContext with only defined properties to satisfy
55
+ * TypeScript's exactOptionalPropertyTypes requirement.
56
+ *
57
+ * @param options - The options object containing llm, logger, and userId
58
+ * @returns IntentContext with only defined properties
59
+ * @private
60
+ */
61
+ private buildContext;
62
+ /**
63
+ * Builds the extractors object from options.
64
+ *
65
+ * Uses provided extractors or falls back to generic defaults that work
66
+ * for any type T via JSON stringification and hashing.
67
+ *
68
+ * @param options - The options object containing key and summary extractors
69
+ * @returns Required extractors with defaults applied
70
+ * @private
71
+ */
72
+ private buildExtractors;
73
+ /**
74
+ * Builds the configuration object from options.
75
+ *
76
+ * Merges user-provided options with environment-based CONFIG defaults.
77
+ *
78
+ * @param options - The options object containing config overrides
79
+ * @returns Required config with all values populated
80
+ * @private
81
+ */
82
+ private buildConfig;
83
+ /**
84
+ * Validates the configuration values.
85
+ *
86
+ * Ensures the configured score range is valid and the relevancyThreshold is in range.
87
+ *
88
+ * @throws {Error} If maxScore is below minScore
89
+ * @throws {Error} If relevancyThreshold is not within [minScore, maxScore]
90
+ * @private
91
+ */
92
+ private validateConfig;
93
+ /**
94
+ * Selects and validates the LLM client.
95
+ *
96
+ * Uses the provided client from context or attempts to create a default
97
+ * Groq client if GROQ_API_KEY is available.
98
+ *
99
+ * @returns The selected LLM client
100
+ * @throws {Error} If no LLM client is provided and GROQ_API_KEY is not set
101
+ * @private
102
+ */
103
+ private selectAndValidateLlmClient;
104
+ /**
105
+ * Creates a new Intent instance.
106
+ *
107
+ * All options are optional with sensible defaults:
108
+ * - llm: Auto-detected from GROQ_API_KEY environment variable if available
109
+ * - key: Hash-based string from JSON representation of items
110
+ * - summary: Pretty-printed JSON of items (2-space indentation for LLM readability)
111
+ * - Config values: From INTENT_* environment variables or built-in defaults
112
+ *
113
+ * @param options - Optional configuration object
114
+ * @param options.llm - Optional LLM client. If omitted, uses Groq client when GROQ_API_KEY is set
115
+ * @param options.logger - Optional logger for warnings and errors
116
+ * @param options.userId - Optional user identifier for LLM provider abuse monitoring
117
+ * @param options.key - Optional function extracting a short human-readable key from items
118
+ * @param options.summary - Optional function extracting a short description for LLM reasoning
119
+ * @param options.provider - Optional provider override (default: INTENT_PROVIDER or "GROQ")
120
+ * @param options.timeoutMs - Optional timeout in milliseconds (default: INTENT_TIMEOUT_MS or 3000)
121
+ * @param options.relevancyThreshold - Optional minimum score to include results (default: INTENT_RELEVANCY_THRESHOLD)
122
+ * @param options.minScore - Optional minimum score value (default: INTENT_MIN_SCORE or 0)
123
+ * @param options.maxScore - Optional maximum score value (default: INTENT_MAX_SCORE or 10)
124
+ * @param options.batchSize - Optional number of candidates per LLM call (default: INTENT_BATCH_SIZE or 20)
125
+ * @param options.tinyBatchFraction - Optional threshold for merging small batches (default: INTENT_TINY_BATCH_FRACTION or 0.2)
126
+ * @throws {Error} If no LLM client is provided and GROQ_API_KEY is not set
127
+ * @throws {Error} If maxScore is below minScore
128
+ * @throws {Error} If relevancyThreshold is not within [minScore, maxScore]
129
+ *
130
+ * @example
131
+ * ```typescript
132
+ * // Minimal - uses all defaults
133
+ * const intent = new Intent();
134
+ *
135
+ * // With extractors
136
+ * const intent = new Intent<Doc>({
137
+ * key: doc => doc.title,
138
+ * summary: doc => doc.content
139
+ * });
140
+ *
141
+ * // Full configuration
142
+ * const intent = new Intent<Doc>({
143
+ * llm: myClient,
144
+ * userId: "org-123",
145
+ * key: doc => doc.title,
146
+ * summary: doc => doc.content,
147
+ * relevancyThreshold: 5,
148
+ * batchSize: 20
149
+ * });
150
+ * ```
151
+ */
152
+ constructor(options?: IntentOptions<T> & {
153
+ config?: typeof CONFIG;
154
+ });
155
+ /**
156
+ * Rerank candidates based on relevance to a query.
157
+ *
158
+ * Calls the LLM to evaluate each candidate and returns only those above the
159
+ * configured threshold, sorted by score (desc) with stable ordering on ties.
160
+ *
161
+ * The LLM is instructed to always generate an explanation before the score.
162
+ * Explanations are only returned when `options.explain` is true.
163
+ *
164
+ * Fast-path optimizations:
165
+ * - Returns empty array for 0 candidates without LLM call
166
+ * - Returns single candidate unchanged without LLM call
167
+ *
168
+ * Error handling:
169
+ * - On any batch error, returns that batch's items in original order
170
+ * - On top-level error, returns all items in original order
171
+ * - All errors are logged via the configured logger
172
+ *
173
+ * @param query - The search query or user intent to rank against
174
+ * @param candidates - Array of items to rerank
175
+ * @param options - Optional per-call configuration
176
+ * @param options.explain - When true, return `{ item, explanation }[]` instead of `T[]`
177
+ * @param options.userId - Optional user ID for this specific call, overrides ctx.userId
178
+ * @returns Filtered and sorted array of items, or original order on any error
179
+ *
180
+ * @example
181
+ * ```typescript
182
+ * const itemsOnly = await intent.rank("find expense reports", docs);
183
+ *
184
+ * const withExplanations = await intent.rank("find expense reports", docs, { explain: true });
185
+ * // => [{ item: Doc, explanation: string }, ...]
186
+ * ```
187
+ */
188
+ rank(query: string, candidates: T[], options?: {
189
+ explain?: false;
190
+ userId?: string;
191
+ }): Promise<T[]>;
192
+ rank(query: string, candidates: T[], options: {
193
+ explain: true;
194
+ userId?: string;
195
+ }): Promise<Array<{
196
+ item: T;
197
+ explanation: string;
198
+ }>>;
199
+ /**
200
+ * Filter candidates based on relevance to a query.
201
+ *
202
+ * Calls the LLM to decide whether each candidate is relevant, then returns
203
+ * only the relevant items in the same order they were provided.
204
+ *
205
+ * Explanations are only returned when `options.explain` is true.
206
+ *
207
+ * Fast paths:
208
+ * - Returns [] for 0 candidates without LLM call
209
+ * - Returns the single candidate unchanged without LLM call
210
+ *
211
+ * Error handling:
212
+ * - On any batch error, preserves that batch's original order
213
+ * - On top-level error, preserves original order
214
+ *
215
+ * @param query - The search query or user intent to filter against
216
+ * @param candidates - Array of items to filter
217
+ * @param options - Optional per-call configuration
218
+ * @param options.explain - When true, return `{ item, explanation }[]` instead of `T[]`
219
+ * @param options.userId - Optional user ID for this specific call, overrides ctx.userId
220
+ * @returns Filtered array of items, preserving input order
221
+ */
222
+ filter(query: string, candidates: T[], options?: {
223
+ explain?: false;
224
+ userId?: string;
225
+ }): Promise<T[]>;
226
+ filter(query: string, candidates: T[], options: {
227
+ explain: true;
228
+ userId?: string;
229
+ }): Promise<Array<{
230
+ item: T;
231
+ explanation: string;
232
+ }>>;
233
+ /**
234
+ * Choose exactly one candidate as the best match for a query.
235
+ *
236
+ * Uses a tournament strategy when inputs exceed the batch size:
237
+ * - Choose one from each batch
238
+ * - Then choose one from the batch winners
239
+ *
240
+ * This method always returns a single item.
241
+ *
242
+ * @param query - The search query or user intent
243
+ * @param candidates - Array of items to choose from
244
+ * @param options - Optional per-call configuration
245
+ * @param options.explain - When true, return `{ item, explanation }` instead of `T`
246
+ * @param options.userId - Optional user ID for this specific call, overrides ctx.userId
247
+ * @returns The single chosen item (or item + explanation)
248
+ */
249
+ choice(query: string, candidates: T[], options?: {
250
+ explain?: false;
251
+ userId?: string;
252
+ }): Promise<T>;
253
+ choice(query: string, candidates: T[], options: {
254
+ explain: true;
255
+ userId?: string;
256
+ }): Promise<{
257
+ item: T;
258
+ explanation: string;
259
+ }>;
260
+ /**
261
+ * Normalize incoming items into a consistent shape for downstream processing.
262
+ *
263
+ * Extracts the key and summary from each item using the configured extractors,
264
+ * and attaches the original input index for stable sorting later.
265
+ *
266
+ * @param candidates - Raw items to prepare
267
+ * @returns Array of prepared candidates with extracted metadata and original index
268
+ * @private
269
+ */
270
+ private prepareCandidates;
271
+ /**
272
+ * Ensure keys are unique by suffixing duplicates with their input index.
273
+ *
274
+ * When multiple items share the same key, subsequent occurrences are renamed
275
+ * to "Key (idx)" where idx is the original input index. This prevents JSON
276
+ * schema validation errors and ensures the LLM can score each item independently.
277
+ *
278
+ * @param itemsBase - Prepared candidates with potentially duplicate keys
279
+ * @returns Candidates with guaranteed unique keys
280
+ * @private
281
+ */
282
+ private ensureUniqueKeys;
283
+ /**
284
+ * Build the JSON schema and chat messages payload for the LLM.
285
+ *
286
+ * Creates a strict JSON schema requiring one integer property (minScore-maxScore) per candidate key,
287
+ * and constructs system + user messages instructing the LLM to score relevance.
288
+ *
289
+ * @param query - The search query to evaluate candidates against
290
+ * @param items - Candidates with unique keys and summaries
291
+ * @returns Object containing JSON schema and chat messages array
292
+ * @private
293
+ */
294
+ private buildRequest;
295
+ /**
296
+ * Build the JSON schema and chat messages payload for the LLM filter call.
297
+ *
298
+ * @param query - The search query to evaluate candidates against
299
+ * @param items - Candidates with unique keys and summaries
300
+ * @returns Object containing JSON schema and chat messages array
301
+ * @private
302
+ */
303
+ private buildFilterRequest;
304
+ /**
305
+ * Build the JSON schema and chat messages payload for the LLM choice call.
306
+ *
307
+ * @param query - The search query to choose against
308
+ * @param items - Candidates with unique keys and summaries
309
+ * @returns Object containing JSON schema and chat messages array
310
+ * @private
311
+ */
312
+ private buildChoiceRequest;
313
+ /**
314
+ * Invoke the LLM and return the parsed map of candidate scores.
315
+ *
316
+ * Calls the configured LLM client with the messages, JSON schema, model config,
317
+ * and user ID. Returns null if the response is invalid or missing.
318
+ *
319
+ * @param messages - Chat messages (system + user) to send to LLM
320
+ * @param schema - Strict JSON schema defining expected response structure
321
+ * @param userId - Optional user identifier for provider abuse monitoring
322
+ * @returns Map of candidate keys to numeric scores, or null if response invalid
323
+ * @private
324
+ */
325
+ private fetchEvaluations;
326
+ /**
327
+ * Invoke the LLM and return boolean relevancy decisions.
328
+ *
329
+ * @param messages - Chat messages to send
330
+ * @param schema - Strict JSON schema defining expected response structure
331
+ * @param userId - Optional user id
332
+ * @returns Map of candidate keys to filter decisions, or null if invalid
333
+ * @private
334
+ */
335
+ private fetchFilterDecisions;
336
+ /**
337
+ * Invoke the LLM and return a single selected key.
338
+ *
339
+ * @param messages - Chat messages to send
340
+ * @param schema - Strict JSON schema defining expected response structure
341
+ * @param userId - Optional user id
342
+ * @returns Choice result, or null if invalid
343
+ * @private
344
+ */
345
+ private fetchChoice;
346
+ /**
347
+ * Apply boolean filter decisions while preserving input order.
348
+ *
349
+ * @param items - Candidates with unique keys
350
+ * @param decisions - Map of candidate keys to decisions
351
+ * @returns Filtered items with explanations
352
+ * @private
353
+ */
354
+ private applyFilter;
355
+ /**
356
+ * Process a single batch of candidates through the LLM filter.
357
+ *
358
+ * @param query - Query to filter against
359
+ * @param batch - Prepared candidates
360
+ * @param options - Optional userId override
361
+ * @returns Filtered items (stable order), with explanations
362
+ * @private
363
+ */
364
+ private processFilterBatch;
365
+ /**
366
+ * Process a batch of candidates and choose a single winner.
367
+ *
368
+ * @param query - Query to choose against
369
+ * @param batch - Candidates with unique keys
370
+ * @param userId - Optional userId override
371
+ * @returns Winner item with explanation
372
+ * @private
373
+ */
374
+ private processChoiceBatch;
375
+ /**
376
+ * Apply relevancy threshold filtering and stable sorting.
377
+ *
378
+ * Scores are clamped to the configured score range, then filtered to keep only items with
379
+ * score > threshold. Results are sorted by score descending, with ties
380
+ * preserving original input order for deterministic results.
381
+ *
382
+ * @param items - Candidates with unique keys
383
+ * @param scores - Map of candidate keys to LLM-assigned scores
384
+ * @returns Filtered and sorted array of original items
385
+ * @private
386
+ */
387
+ private rankAndFilter;
388
+ /**
389
+ * Process a single batch of candidates through the LLM.
390
+ *
391
+ * Ensures unique keys, builds the request payload, fetches scores from the LLM,
392
+ * and returns filtered and sorted results. On any error or null response from
393
+ * the LLM, returns items in their original order as a fallback.
394
+ *
395
+ * @param query - The search query to evaluate candidates against
396
+ * @param batch - Batch of prepared candidates to process
397
+ * @param userId - Optional user identifier for provider abuse monitoring
398
+ * @returns Ranked and filtered items, or original order on error
399
+ * @private
400
+ */
401
+ private processBatch;
402
+ }