@librechat/agents 2.4.30 → 2.4.311

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.
Files changed (55) hide show
  1. package/dist/cjs/common/enum.cjs +1 -0
  2. package/dist/cjs/common/enum.cjs.map +1 -1
  3. package/dist/cjs/main.cjs +2 -0
  4. package/dist/cjs/main.cjs.map +1 -1
  5. package/dist/cjs/tools/search/firecrawl.cjs +149 -0
  6. package/dist/cjs/tools/search/firecrawl.cjs.map +1 -0
  7. package/dist/cjs/tools/search/format.cjs +116 -0
  8. package/dist/cjs/tools/search/format.cjs.map +1 -0
  9. package/dist/cjs/tools/search/highlights.cjs +194 -0
  10. package/dist/cjs/tools/search/highlights.cjs.map +1 -0
  11. package/dist/cjs/tools/search/rerankers.cjs +187 -0
  12. package/dist/cjs/tools/search/rerankers.cjs.map +1 -0
  13. package/dist/cjs/tools/search/search.cjs +410 -0
  14. package/dist/cjs/tools/search/search.cjs.map +1 -0
  15. package/dist/cjs/tools/search/tool.cjs +103 -0
  16. package/dist/cjs/tools/search/tool.cjs.map +1 -0
  17. package/dist/esm/common/enum.mjs +1 -0
  18. package/dist/esm/common/enum.mjs.map +1 -1
  19. package/dist/esm/main.mjs +1 -0
  20. package/dist/esm/main.mjs.map +1 -1
  21. package/dist/esm/tools/search/firecrawl.mjs +145 -0
  22. package/dist/esm/tools/search/firecrawl.mjs.map +1 -0
  23. package/dist/esm/tools/search/format.mjs +114 -0
  24. package/dist/esm/tools/search/format.mjs.map +1 -0
  25. package/dist/esm/tools/search/highlights.mjs +192 -0
  26. package/dist/esm/tools/search/highlights.mjs.map +1 -0
  27. package/dist/esm/tools/search/rerankers.mjs +181 -0
  28. package/dist/esm/tools/search/rerankers.mjs.map +1 -0
  29. package/dist/esm/tools/search/search.mjs +407 -0
  30. package/dist/esm/tools/search/search.mjs.map +1 -0
  31. package/dist/esm/tools/search/tool.mjs +101 -0
  32. package/dist/esm/tools/search/tool.mjs.map +1 -0
  33. package/dist/types/common/enum.d.ts +1 -0
  34. package/dist/types/index.d.ts +1 -0
  35. package/dist/types/scripts/search.d.ts +1 -0
  36. package/dist/types/tools/search/firecrawl.d.ts +117 -0
  37. package/dist/types/tools/search/format.d.ts +2 -0
  38. package/dist/types/tools/search/highlights.d.ts +13 -0
  39. package/dist/types/tools/search/index.d.ts +2 -0
  40. package/dist/types/tools/search/rerankers.d.ts +32 -0
  41. package/dist/types/tools/search/search.d.ts +9 -0
  42. package/dist/types/tools/search/tool.d.ts +12 -0
  43. package/dist/types/tools/search/types.d.ts +150 -0
  44. package/package.json +2 -1
  45. package/src/common/enum.ts +1 -0
  46. package/src/index.ts +1 -0
  47. package/src/scripts/search.ts +141 -0
  48. package/src/tools/search/firecrawl.ts +270 -0
  49. package/src/tools/search/format.ts +121 -0
  50. package/src/tools/search/highlights.ts +238 -0
  51. package/src/tools/search/index.ts +2 -0
  52. package/src/tools/search/rerankers.ts +248 -0
  53. package/src/tools/search/search.ts +567 -0
  54. package/src/tools/search/tool.ts +151 -0
  55. package/src/tools/search/types.ts +179 -0
@@ -0,0 +1,270 @@
1
+ /* eslint-disable no-console */
2
+ import axios from 'axios';
3
+
4
+ export interface FirecrawlScrapeOptions {
5
+ formats?: string[];
6
+ includeTags?: string[];
7
+ excludeTags?: string[];
8
+ headers?: Record<string, string>;
9
+ waitFor?: number;
10
+ timeout?: number;
11
+ }
12
+
13
+ interface ScrapeMetadata {
14
+ // Core source information
15
+ sourceURL?: string;
16
+ url?: string;
17
+ scrapeId?: string;
18
+ statusCode?: number;
19
+ // Basic metadata
20
+ title?: string;
21
+ description?: string;
22
+ language?: string;
23
+ favicon?: string;
24
+ viewport?: string;
25
+ robots?: string;
26
+ 'theme-color'?: string;
27
+ // Open Graph metadata
28
+ 'og:url'?: string;
29
+ 'og:title'?: string;
30
+ 'og:description'?: string;
31
+ 'og:type'?: string;
32
+ 'og:image'?: string;
33
+ 'og:image:width'?: string;
34
+ 'og:image:height'?: string;
35
+ 'og:site_name'?: string;
36
+ ogUrl?: string;
37
+ ogTitle?: string;
38
+ ogDescription?: string;
39
+ ogImage?: string;
40
+ ogSiteName?: string;
41
+ // Article metadata
42
+ 'article:author'?: string;
43
+ 'article:published_time'?: string;
44
+ 'article:modified_time'?: string;
45
+ 'article:section'?: string;
46
+ 'article:tag'?: string;
47
+ 'article:publisher'?: string;
48
+ publishedTime?: string;
49
+ modifiedTime?: string;
50
+ // Twitter metadata
51
+ 'twitter:site'?: string;
52
+ 'twitter:creator'?: string;
53
+ 'twitter:card'?: string;
54
+ 'twitter:image'?: string;
55
+ 'twitter:dnt'?: string;
56
+ 'twitter:app:name:iphone'?: string;
57
+ 'twitter:app:id:iphone'?: string;
58
+ 'twitter:app:url:iphone'?: string;
59
+ 'twitter:app:name:ipad'?: string;
60
+ 'twitter:app:id:ipad'?: string;
61
+ 'twitter:app:url:ipad'?: string;
62
+ 'twitter:app:name:googleplay'?: string;
63
+ 'twitter:app:id:googleplay'?: string;
64
+ 'twitter:app:url:googleplay'?: string;
65
+ // Facebook metadata
66
+ 'fb:app_id'?: string;
67
+ // App links
68
+ 'al:ios:url'?: string;
69
+ 'al:ios:app_name'?: string;
70
+ 'al:ios:app_store_id'?: string;
71
+ // Allow for additional properties that might be present
72
+ [key: string]: string | number | boolean | null | undefined;
73
+ }
74
+
75
+ export interface FirecrawlScrapeResponse {
76
+ success: boolean;
77
+ data?: {
78
+ markdown?: string;
79
+ html?: string;
80
+ rawHtml?: string;
81
+ screenshot?: string;
82
+ links?: string[];
83
+ metadata?: ScrapeMetadata;
84
+ };
85
+ error?: string;
86
+ }
87
+
88
+ export interface FirecrawlScraperConfig {
89
+ apiKey?: string;
90
+ apiUrl?: string;
91
+ formats?: string[];
92
+ timeout?: number;
93
+ }
94
+ const getDomainName = (
95
+ link: string,
96
+ metadata?: ScrapeMetadata
97
+ ): string | undefined => {
98
+ try {
99
+ const url = metadata?.sourceURL ?? metadata?.url ?? (link || '');
100
+ const domain = new URL(url).hostname.replace(/^www\./, '');
101
+ if (domain) {
102
+ return domain;
103
+ }
104
+ } catch (e) {
105
+ // URL parsing failed
106
+ console.error('Error parsing URL:', e);
107
+ }
108
+
109
+ return;
110
+ };
111
+
112
+ export function getAttribution(
113
+ link: string,
114
+ metadata?: ScrapeMetadata
115
+ ): string | undefined {
116
+ if (!metadata) return getDomainName(link, metadata);
117
+
118
+ const possibleAttributions = [
119
+ metadata.ogSiteName,
120
+ metadata['og:site_name'],
121
+ metadata.title?.split('|').pop()?.trim(),
122
+ metadata['twitter:site']?.replace(/^@/, ''),
123
+ ];
124
+
125
+ const attribution = possibleAttributions.find(
126
+ (attr) => attr != null && typeof attr === 'string' && attr.trim() !== ''
127
+ );
128
+ if (attribution != null) {
129
+ return attribution;
130
+ }
131
+
132
+ return getDomainName(link, metadata);
133
+ }
134
+
135
+ /**
136
+ * Firecrawl scraper implementation
137
+ * Uses the Firecrawl API to scrape web pages
138
+ */
139
+ export class FirecrawlScraper {
140
+ private apiKey: string;
141
+ private apiUrl: string;
142
+ private defaultFormats: string[];
143
+ private timeout: number;
144
+
145
+ constructor(config: FirecrawlScraperConfig = {}) {
146
+ this.apiKey = config.apiKey ?? process.env.FIRECRAWL_API_KEY ?? '';
147
+
148
+ const baseUrl =
149
+ config.apiUrl ??
150
+ process.env.FIRECRAWL_BASE_URL ??
151
+ 'https://api.firecrawl.dev';
152
+ this.apiUrl = `${baseUrl.replace(/\/+$/, '')}/v1/scrape`;
153
+
154
+ this.defaultFormats = config.formats ?? ['markdown', 'html'];
155
+ this.timeout = config.timeout ?? 15000;
156
+
157
+ if (!this.apiKey) {
158
+ console.warn('FIRECRAWL_API_KEY is not set. Scraping will not work.');
159
+ }
160
+
161
+ console.log(`Firecrawl scraper initialized with API URL: ${this.apiUrl}`);
162
+ }
163
+
164
+ /**
165
+ * Scrape a single URL
166
+ * @param url URL to scrape
167
+ * @param options Scrape options
168
+ * @returns Scrape response
169
+ */
170
+ async scrapeUrl(
171
+ url: string,
172
+ options: FirecrawlScrapeOptions = {}
173
+ ): Promise<[string, FirecrawlScrapeResponse]> {
174
+ if (!this.apiKey) {
175
+ return [
176
+ url,
177
+ {
178
+ success: false,
179
+ error: 'FIRECRAWL_API_KEY is not set',
180
+ },
181
+ ];
182
+ }
183
+
184
+ try {
185
+ const response = await axios.post(
186
+ this.apiUrl,
187
+ {
188
+ url,
189
+ formats: options.formats || this.defaultFormats,
190
+ includeTags: options.includeTags,
191
+ excludeTags: options.excludeTags,
192
+ headers: options.headers,
193
+ waitFor: options.waitFor,
194
+ timeout: options.timeout ?? this.timeout,
195
+ },
196
+ {
197
+ headers: {
198
+ 'Content-Type': 'application/json',
199
+ Authorization: `Bearer ${this.apiKey}`,
200
+ },
201
+ timeout: this.timeout,
202
+ }
203
+ );
204
+
205
+ return [url, response.data];
206
+ } catch (error) {
207
+ const errorMessage =
208
+ error instanceof Error ? error.message : String(error);
209
+ return [
210
+ url,
211
+ {
212
+ success: false,
213
+ error: `Firecrawl API request failed: ${errorMessage}`,
214
+ },
215
+ ];
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Extract content from scrape response
221
+ * @param response Scrape response
222
+ * @returns Extracted content or empty string if not available
223
+ */
224
+ extractContent(response: FirecrawlScrapeResponse): string {
225
+ if (!response.success || !response.data) {
226
+ return '';
227
+ }
228
+
229
+ // Prefer markdown content if available
230
+ if (response.data.markdown != null) {
231
+ return response.data.markdown;
232
+ }
233
+
234
+ // Fall back to HTML content
235
+ if (response.data.html != null) {
236
+ return response.data.html;
237
+ }
238
+
239
+ // Fall back to raw HTML content
240
+ if (response.data.rawHtml != null) {
241
+ return response.data.rawHtml;
242
+ }
243
+
244
+ return '';
245
+ }
246
+
247
+ /**
248
+ * Extract metadata from scrape response
249
+ * @param response Scrape response
250
+ * @returns Metadata object
251
+ */
252
+ extractMetadata(response: FirecrawlScrapeResponse): ScrapeMetadata {
253
+ if (!response.success || !response.data || !response.data.metadata) {
254
+ return {};
255
+ }
256
+
257
+ return response.data.metadata;
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Create a Firecrawl scraper instance
263
+ * @param config Scraper configuration
264
+ * @returns Firecrawl scraper instance
265
+ */
266
+ export const createFirecrawlScraper = (
267
+ config: FirecrawlScraperConfig = {}
268
+ ): FirecrawlScraper => {
269
+ return new FirecrawlScraper(config);
270
+ };
@@ -0,0 +1,121 @@
1
+ import type * as t from './types';
2
+
3
+ export function formatResultsForLLM(results: t.SearchResultData): string {
4
+ let output = '';
5
+
6
+ const addSection = (title: string): void => {
7
+ output += `\n=== ${title} ===\n`;
8
+ };
9
+
10
+ // Organic (web) results
11
+ const organic = results.organic ?? [];
12
+ if (organic.length) {
13
+ addSection('Web Results');
14
+ organic.forEach((r, i) => {
15
+ output += [
16
+ `Source ${i}: ${r.title ?? '(no title)'}`,
17
+ `Citation Anchor: \\ue202turn0search${i}`,
18
+ `URL: ${r.link}`,
19
+ r.snippet != null ? `Summary: ${r.snippet}` : '',
20
+ r.date != null ? `Date: ${r.date}` : '',
21
+ r.attribution != null ? `Source: ${r.attribution}` : '',
22
+ '',
23
+ '--- Content Highlights ---',
24
+ ...(r.highlights ?? [])
25
+ .filter((h) => h.text.trim().length > 0)
26
+ .map((h) => `[Relevance: ${h.score.toFixed(2)}]\n${h.text.trim()}`),
27
+ '',
28
+ ]
29
+ .filter(Boolean)
30
+ .join('\n');
31
+ });
32
+ }
33
+
34
+ // Ignoring these sections for now
35
+ // // Top stories (news)
36
+ // const topStores = results.topStories ?? [];
37
+ // if (topStores.length) {
38
+ // addSection('News Results');
39
+ // topStores.forEach((r, i) => {
40
+ // output += [
41
+ // `Anchor: \ue202turn0news${i}`,
42
+ // `Title: ${r.title ?? '(no title)'}`,
43
+ // `URL: ${r.link}`,
44
+ // r.snippet != null ? `Snippet: ${r.snippet}` : '',
45
+ // r.date != null ? `Date: ${r.date}` : '',
46
+ // r.attribution != null ? `Source: ${r.attribution}` : '',
47
+ // ''
48
+ // ].filter(Boolean).join('\n');
49
+ // });
50
+ // }
51
+
52
+ // // Images
53
+ // const images = results.images ?? [];
54
+ // if (images.length) {
55
+ // addSection('Image Results');
56
+ // images.forEach((img, i) => {
57
+ // output += [
58
+ // `Anchor: \ue202turn0image${i}`,
59
+ // `Title: ${img.title ?? '(no title)'}`,
60
+ // `Image URL: ${img.imageUrl}`,
61
+ // ''
62
+ // ].join('\n');
63
+ // });
64
+ // }
65
+
66
+ // Knowledge Graph
67
+ if (results.knowledgeGraph != null) {
68
+ addSection('Knowledge Graph');
69
+ output += [
70
+ `Title: ${results.knowledgeGraph.title ?? '(no title)'}`,
71
+ results.knowledgeGraph.description != null
72
+ ? `Description: ${results.knowledgeGraph.description}`
73
+ : '',
74
+ results.knowledgeGraph.type != null
75
+ ? `Type: ${results.knowledgeGraph.type}`
76
+ : '',
77
+ results.knowledgeGraph.imageUrl != null
78
+ ? `Image URL: ${results.knowledgeGraph.imageUrl}`
79
+ : '',
80
+ results.knowledgeGraph.attributes != null
81
+ ? `Attributes: ${JSON.stringify(results.knowledgeGraph.attributes, null, 2)}`
82
+ : '',
83
+ '',
84
+ ]
85
+ .filter(Boolean)
86
+ .join('\n');
87
+ }
88
+
89
+ // Answer Box
90
+ if (results.answerBox != null) {
91
+ addSection('Answer Box');
92
+ output += [
93
+ results.answerBox.title != null
94
+ ? `Title: ${results.answerBox.title}`
95
+ : '',
96
+ results.answerBox.answer != null
97
+ ? `Answer: ${results.answerBox.answer}`
98
+ : '',
99
+ results.answerBox.snippet != null
100
+ ? `Snippet: ${results.answerBox.snippet}`
101
+ : '',
102
+ results.answerBox.date != null ? `Date: ${results.answerBox.date}` : '',
103
+ '',
104
+ ]
105
+ .filter(Boolean)
106
+ .join('\n');
107
+ }
108
+
109
+ // People also ask
110
+ const peopleAlsoAsk = results.peopleAlsoAsk ?? [];
111
+ if (peopleAlsoAsk.length) {
112
+ addSection('People Also Ask');
113
+ peopleAlsoAsk.forEach((p, _i) => {
114
+ output += [`Q: ${p.question}`, `A: ${p.answer}`, '']
115
+ .filter(Boolean)
116
+ .join('\n');
117
+ });
118
+ }
119
+
120
+ return output.trim();
121
+ }
@@ -0,0 +1,238 @@
1
+ import type * as t from './types';
2
+
3
+ // 2. Pre-compile all regular expressions (only do this once)
4
+ // Group patterns by priority for early returns
5
+ const priorityPatterns = [
6
+ // High priority patterns (structural)
7
+ [
8
+ { regex: /\n\n/g }, // Double newline (paragraph break)
9
+ { regex: /\n/g }, // Single newline
10
+ { regex: /={3,}\s*\n|-{3,}\s*\n/g }, // Section separators
11
+ ],
12
+ // Medium priority (semantic)
13
+ [
14
+ { regex: /[.!?][")\]]?\s/g }, // End of sentence
15
+ { regex: /;\s/g }, // Semicolon
16
+ { regex: /:\s/g }, // Colon
17
+ ],
18
+ // Low priority (any breaks)
19
+ [
20
+ { regex: /,\s/g }, // Comma
21
+ { regex: /\s-\s/g }, // Dash surrounded by spaces
22
+ { regex: /\s/g }, // Any space
23
+ ],
24
+ ];
25
+
26
+ function findFirstMatch(text: string, regex: RegExp): number {
27
+ // Reset regex
28
+ regex.lastIndex = 0;
29
+
30
+ // For very long texts, try chunking
31
+ if (text.length > 10000) {
32
+ const chunkSize = 2000;
33
+ let position = 0;
34
+
35
+ while (position < text.length) {
36
+ const chunk = text.substring(position, position + chunkSize);
37
+ regex.lastIndex = 0;
38
+
39
+ const match = regex.exec(chunk);
40
+ if (match) {
41
+ return position + match.index;
42
+ }
43
+
44
+ // Move to next chunk with some overlap
45
+ position += chunkSize - 100;
46
+ if (position >= text.length) break;
47
+ }
48
+ return -1;
49
+ }
50
+
51
+ // For shorter texts, normal regex search
52
+ const match = regex.exec(text);
53
+ return match ? match.index : -1;
54
+ }
55
+
56
+ // 3. Optimized boundary finding functions
57
+ function findLastMatch(text: string, regex: RegExp): number {
58
+ // Reset regex state
59
+ regex.lastIndex = 0;
60
+
61
+ let lastIndex = -1;
62
+ let lastLength = 0;
63
+ let match;
64
+
65
+ // For very long texts, use a different approach to avoid regex engine slowdowns
66
+ if (text.length > 10000) {
67
+ // Try dividing the text into chunks for faster processing
68
+ const chunkSize = 2000;
69
+ let startPosition = Math.max(0, text.length - chunkSize);
70
+
71
+ while (startPosition >= 0) {
72
+ const chunk = text.substring(startPosition, startPosition + chunkSize);
73
+ regex.lastIndex = 0;
74
+
75
+ let chunkLastIndex = -1;
76
+ let chunkLastLength = 0;
77
+
78
+ while ((match = regex.exec(chunk)) !== null) {
79
+ chunkLastIndex = match.index;
80
+ chunkLastLength = match[0].length;
81
+ }
82
+
83
+ if (chunkLastIndex !== -1) {
84
+ return startPosition + chunkLastIndex + chunkLastLength;
85
+ }
86
+
87
+ // Move to previous chunk with some overlap
88
+ startPosition = Math.max(0, startPosition - chunkSize + 100) - 1;
89
+ if (startPosition <= 0) break;
90
+ }
91
+ return -1;
92
+ }
93
+
94
+ // For shorter texts, normal regex search
95
+ while ((match = regex.exec(text)) !== null) {
96
+ lastIndex = match.index;
97
+ lastLength = match[0].length;
98
+ }
99
+
100
+ return lastIndex === -1 ? -1 : lastIndex + lastLength;
101
+ }
102
+
103
+ // 4. Find the best boundary with priority groups
104
+ function findBestBoundary(text: string, direction = 'backward'): number {
105
+ if (!text || text.length === 0) return 0;
106
+
107
+ // Try each priority group
108
+ for (const patternGroup of priorityPatterns) {
109
+ for (const pattern of patternGroup) {
110
+ const position =
111
+ direction === 'backward'
112
+ ? findLastMatch(text, pattern.regex)
113
+ : findFirstMatch(text, pattern.regex);
114
+
115
+ if (position !== -1) {
116
+ return position;
117
+ }
118
+ }
119
+ }
120
+
121
+ // No match found, use character boundary
122
+ return direction === 'backward' ? text.length : 0;
123
+ }
124
+
125
+ /**
126
+ * Expand highlights in search results using smart boundary detection.
127
+ *
128
+ * This implementation finds natural text boundaries like paragraphs, sentences,
129
+ * and phrases to provide context while maintaining readability.
130
+ *
131
+ * @param searchResults - Search results object
132
+ * @param mainExpandBy - Primary expansion size on each side (default: 300)
133
+ * @param separatorExpandBy - Additional range to look for separators (default: 150)
134
+ * @returns Copy of search results with expanded highlights
135
+ */
136
+ export function expandHighlights(
137
+ searchResults: t.SearchResultData,
138
+ mainExpandBy = 300,
139
+ separatorExpandBy = 150
140
+ ): t.SearchResultData {
141
+ // 1. Avoid full deep copy - only copy what we modify
142
+ const resultCopy = { ...searchResults };
143
+
144
+ // Only deep copy the relevant arrays
145
+ if (resultCopy.organic) {
146
+ resultCopy.organic = [...resultCopy.organic];
147
+ }
148
+ if (resultCopy.topStories) {
149
+ resultCopy.topStories = [...resultCopy.topStories];
150
+ }
151
+
152
+ // 5. Process the results efficiently
153
+ const processResultTypes = ['organic', 'topStories'] as const;
154
+
155
+ for (const resultType of processResultTypes) {
156
+ if (!resultCopy[resultType as 'organic' | 'topStories']) continue;
157
+
158
+ // Map results to new array with modified highlights
159
+ resultCopy[resultType] = resultCopy[resultType]?.map((result) => {
160
+ if (
161
+ result.content == null ||
162
+ result.content === '' ||
163
+ !result.highlights ||
164
+ result.highlights.length === 0
165
+ ) {
166
+ return result; // No modification needed
167
+ }
168
+
169
+ // Create a shallow copy with expanded highlights
170
+ const resultCopy = { ...result };
171
+ const content = result.content;
172
+ const highlights = [];
173
+
174
+ // Process each highlight
175
+ for (const highlight of result.highlights) {
176
+ const highlightText = highlight.text;
177
+
178
+ let startPos = content.indexOf(highlightText);
179
+ let highlightLen = highlightText.length;
180
+
181
+ if (startPos === -1) {
182
+ // Try with stripped whitespace
183
+ const strippedHighlight = highlightText.trim();
184
+ startPos = content.indexOf(strippedHighlight);
185
+
186
+ if (startPos === -1) {
187
+ highlights.push({
188
+ text: highlight.text,
189
+ score: highlight.score,
190
+ });
191
+ continue;
192
+ }
193
+ highlightLen = strippedHighlight.length;
194
+ }
195
+
196
+ // Calculate boundaries
197
+ const mainStart = Math.max(0, startPos - mainExpandBy);
198
+ const mainEnd = Math.min(
199
+ content.length,
200
+ startPos + highlightLen + mainExpandBy
201
+ );
202
+
203
+ const separatorStart = Math.max(0, mainStart - separatorExpandBy);
204
+ const separatorEnd = Math.min(
205
+ content.length,
206
+ mainEnd + separatorExpandBy
207
+ );
208
+
209
+ // Extract text segments
210
+ const headText = content.substring(separatorStart, mainStart);
211
+ const tailText = content.substring(mainEnd, separatorEnd);
212
+
213
+ // Find natural boundaries
214
+ const bestHeadBoundary = findBestBoundary(headText, 'backward');
215
+ const bestTailBoundary = findBestBoundary(tailText, 'forward');
216
+
217
+ // Calculate final positions
218
+ const finalStart = separatorStart + bestHeadBoundary;
219
+ const finalEnd = mainEnd + bestTailBoundary;
220
+
221
+ // Extract the expanded highlight
222
+ const expandedHighlightText = content
223
+ .substring(finalStart, finalEnd)
224
+ .trim();
225
+ highlights.push({
226
+ text: expandedHighlightText,
227
+ score: highlight.score,
228
+ });
229
+ }
230
+
231
+ delete resultCopy.content;
232
+ resultCopy.highlights = highlights;
233
+ return resultCopy;
234
+ });
235
+ }
236
+
237
+ return resultCopy;
238
+ }
@@ -0,0 +1,2 @@
1
+ export * from './tool';
2
+ export type * from './types';