@librechat/agents 2.4.22 → 2.4.31

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 (90) 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/llm/anthropic/index.cjs +1 -1
  4. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  5. package/dist/cjs/llm/anthropic/types.cjs +50 -0
  6. package/dist/cjs/llm/anthropic/types.cjs.map +1 -0
  7. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +227 -21
  8. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  9. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +1 -0
  10. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
  11. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  12. package/dist/cjs/main.cjs +2 -0
  13. package/dist/cjs/main.cjs.map +1 -1
  14. package/dist/cjs/run.cjs.map +1 -1
  15. package/dist/cjs/tools/search/firecrawl.cjs +149 -0
  16. package/dist/cjs/tools/search/firecrawl.cjs.map +1 -0
  17. package/dist/cjs/tools/search/format.cjs +116 -0
  18. package/dist/cjs/tools/search/format.cjs.map +1 -0
  19. package/dist/cjs/tools/search/highlights.cjs +193 -0
  20. package/dist/cjs/tools/search/highlights.cjs.map +1 -0
  21. package/dist/cjs/tools/search/rerankers.cjs +187 -0
  22. package/dist/cjs/tools/search/rerankers.cjs.map +1 -0
  23. package/dist/cjs/tools/search/search.cjs +410 -0
  24. package/dist/cjs/tools/search/search.cjs.map +1 -0
  25. package/dist/cjs/tools/search/tool.cjs +103 -0
  26. package/dist/cjs/tools/search/tool.cjs.map +1 -0
  27. package/dist/esm/common/enum.mjs +1 -0
  28. package/dist/esm/common/enum.mjs.map +1 -1
  29. package/dist/esm/llm/anthropic/index.mjs +1 -1
  30. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  31. package/dist/esm/llm/anthropic/types.mjs +48 -0
  32. package/dist/esm/llm/anthropic/types.mjs.map +1 -0
  33. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +228 -22
  34. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  35. package/dist/esm/llm/anthropic/utils/message_outputs.mjs +1 -0
  36. package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
  37. package/dist/esm/llm/openai/index.mjs.map +1 -1
  38. package/dist/esm/main.mjs +1 -0
  39. package/dist/esm/main.mjs.map +1 -1
  40. package/dist/esm/run.mjs.map +1 -1
  41. package/dist/esm/tools/search/firecrawl.mjs +145 -0
  42. package/dist/esm/tools/search/firecrawl.mjs.map +1 -0
  43. package/dist/esm/tools/search/format.mjs +114 -0
  44. package/dist/esm/tools/search/format.mjs.map +1 -0
  45. package/dist/esm/tools/search/highlights.mjs +191 -0
  46. package/dist/esm/tools/search/highlights.mjs.map +1 -0
  47. package/dist/esm/tools/search/rerankers.mjs +181 -0
  48. package/dist/esm/tools/search/rerankers.mjs.map +1 -0
  49. package/dist/esm/tools/search/search.mjs +407 -0
  50. package/dist/esm/tools/search/search.mjs.map +1 -0
  51. package/dist/esm/tools/search/tool.mjs +101 -0
  52. package/dist/esm/tools/search/tool.mjs.map +1 -0
  53. package/dist/types/common/enum.d.ts +1 -0
  54. package/dist/types/index.d.ts +1 -0
  55. package/dist/types/llm/anthropic/index.d.ts +3 -4
  56. package/dist/types/llm/anthropic/types.d.ts +4 -35
  57. package/dist/types/llm/anthropic/utils/message_inputs.d.ts +2 -2
  58. package/dist/types/llm/anthropic/utils/message_outputs.d.ts +1 -3
  59. package/dist/types/llm/anthropic/utils/output_parsers.d.ts +22 -0
  60. package/dist/types/llm/openai/index.d.ts +3 -2
  61. package/dist/types/scripts/search.d.ts +1 -0
  62. package/dist/types/tools/example.d.ts +21 -3
  63. package/dist/types/tools/search/firecrawl.d.ts +117 -0
  64. package/dist/types/tools/search/format.d.ts +2 -0
  65. package/dist/types/tools/search/highlights.d.ts +13 -0
  66. package/dist/types/tools/search/index.d.ts +2 -0
  67. package/dist/types/tools/search/rerankers.d.ts +32 -0
  68. package/dist/types/tools/search/search.d.ts +9 -0
  69. package/dist/types/tools/search/tool.d.ts +12 -0
  70. package/dist/types/tools/search/types.d.ts +150 -0
  71. package/package.json +10 -9
  72. package/src/common/enum.ts +1 -0
  73. package/src/index.ts +1 -0
  74. package/src/llm/anthropic/index.ts +6 -5
  75. package/src/llm/anthropic/llm.spec.ts +176 -179
  76. package/src/llm/anthropic/types.ts +64 -39
  77. package/src/llm/anthropic/utils/message_inputs.ts +275 -37
  78. package/src/llm/anthropic/utils/message_outputs.ts +4 -21
  79. package/src/llm/anthropic/utils/output_parsers.ts +114 -0
  80. package/src/llm/openai/index.ts +7 -6
  81. package/src/run.ts +1 -1
  82. package/src/scripts/search.ts +141 -0
  83. package/src/tools/search/firecrawl.ts +270 -0
  84. package/src/tools/search/format.ts +121 -0
  85. package/src/tools/search/highlights.ts +237 -0
  86. package/src/tools/search/index.ts +2 -0
  87. package/src/tools/search/rerankers.ts +248 -0
  88. package/src/tools/search/search.ts +567 -0
  89. package/src/tools/search/tool.ts +151 -0
  90. package/src/tools/search/types.ts +179 -0
@@ -0,0 +1,237 @@
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
+ resultCopy.highlights = highlights;
232
+ return resultCopy;
233
+ });
234
+ }
235
+
236
+ return resultCopy;
237
+ }
@@ -0,0 +1,2 @@
1
+ export * from './tool';
2
+ export type * from './types';
@@ -0,0 +1,248 @@
1
+ /* eslint-disable no-console */
2
+ import axios from 'axios';
3
+ import type * as t from './types';
4
+
5
+ export abstract class BaseReranker {
6
+ protected apiKey: string | undefined;
7
+
8
+ constructor() {
9
+ // Each specific reranker will set its API key
10
+ }
11
+
12
+ abstract rerank(
13
+ query: string,
14
+ documents: string[],
15
+ topK?: number
16
+ ): Promise<t.Highlight[]>;
17
+
18
+ protected getDefaultRanking(
19
+ documents: string[],
20
+ topK: number
21
+ ): t.Highlight[] {
22
+ return documents
23
+ .slice(0, Math.min(topK, documents.length))
24
+ .map((doc) => ({ text: doc, score: 0 }));
25
+ }
26
+
27
+ protected logDocumentSamples(documents: string[]): void {
28
+ console.log('Sample documents being sent to API:');
29
+ for (let i = 0; i < Math.min(3, documents.length); i++) {
30
+ console.log(`Document ${i}: ${documents[i].substring(0, 100)}...`);
31
+ }
32
+ }
33
+ }
34
+
35
+ export class JinaReranker extends BaseReranker {
36
+ constructor({ apiKey = process.env.JINA_API_KEY }: { apiKey?: string }) {
37
+ super();
38
+ this.apiKey = apiKey;
39
+ }
40
+
41
+ async rerank(
42
+ query: string,
43
+ documents: string[],
44
+ topK: number = 5
45
+ ): Promise<t.Highlight[]> {
46
+ console.log(`Reranking ${documents.length} documents with Jina`);
47
+
48
+ try {
49
+ if (this.apiKey == null || this.apiKey === '') {
50
+ console.warn('JINA_API_KEY is not set. Using default ranking.');
51
+ return this.getDefaultRanking(documents, topK);
52
+ }
53
+
54
+ this.logDocumentSamples(documents);
55
+
56
+ const requestData = {
57
+ model: 'jina-reranker-v2-base-multilingual',
58
+ query: query,
59
+ top_n: topK,
60
+ documents: documents,
61
+ return_documents: true,
62
+ };
63
+
64
+ const response = await axios.post<t.JinaRerankerResponse | undefined>(
65
+ 'https://api.jina.ai/v1/rerank',
66
+ requestData,
67
+ {
68
+ headers: {
69
+ 'Content-Type': 'application/json',
70
+ Authorization: `Bearer ${this.apiKey}`,
71
+ },
72
+ }
73
+ );
74
+
75
+ // Log the response data structure
76
+ console.log('Jina API response structure:');
77
+ console.log('Model:', response.data?.model);
78
+ console.log('Usage:', response.data?.usage);
79
+ console.log('Results count:', response.data?.results.length);
80
+
81
+ // Log a sample of the results
82
+ if ((response.data?.results.length ?? 0) > 0) {
83
+ console.log(
84
+ 'Sample result:',
85
+ JSON.stringify(response.data?.results[0], null, 2)
86
+ );
87
+ }
88
+
89
+ if (response.data && response.data.results.length) {
90
+ return response.data.results.map((result) => {
91
+ const docIndex = result.index;
92
+ const score = result.relevance_score;
93
+ let text = '';
94
+
95
+ // If return_documents is true, the document field will be present
96
+ if (result.document != null) {
97
+ const doc = result.document;
98
+ if (typeof doc === 'object' && 'text' in doc) {
99
+ text = doc.text;
100
+ } else if (typeof doc === 'string') {
101
+ text = doc;
102
+ }
103
+ } else {
104
+ // Otherwise, use the index to get the document
105
+ text = documents[docIndex];
106
+ }
107
+
108
+ return { text, score };
109
+ });
110
+ } else {
111
+ console.warn(
112
+ 'Unexpected response format from Jina API. Using default ranking.'
113
+ );
114
+ return this.getDefaultRanking(documents, topK);
115
+ }
116
+ } catch (error) {
117
+ console.error('Error using Jina reranker:', error);
118
+ // Fallback to default ranking on error
119
+ return this.getDefaultRanking(documents, topK);
120
+ }
121
+ }
122
+ }
123
+
124
+ export class CohereReranker extends BaseReranker {
125
+ constructor({ apiKey = process.env.COHERE_API_KEY }: { apiKey?: string }) {
126
+ super();
127
+ this.apiKey = apiKey;
128
+ }
129
+
130
+ async rerank(
131
+ query: string,
132
+ documents: string[],
133
+ topK: number = 5
134
+ ): Promise<t.Highlight[]> {
135
+ console.log(`Reranking ${documents.length} documents with Cohere`);
136
+
137
+ try {
138
+ if (this.apiKey == null || this.apiKey === '') {
139
+ console.warn('COHERE_API_KEY is not set. Using default ranking.');
140
+ return this.getDefaultRanking(documents, topK);
141
+ }
142
+
143
+ this.logDocumentSamples(documents);
144
+
145
+ const requestData = {
146
+ model: 'rerank-v3.5',
147
+ query: query,
148
+ top_n: topK,
149
+ documents: documents,
150
+ };
151
+
152
+ const response = await axios.post<t.CohereRerankerResponse | undefined>(
153
+ 'https://api.cohere.com/v2/rerank',
154
+ requestData,
155
+ {
156
+ headers: {
157
+ 'Content-Type': 'application/json',
158
+ Authorization: `Bearer ${this.apiKey}`,
159
+ },
160
+ }
161
+ );
162
+
163
+ // Log the response data structure
164
+ console.log('Cohere API response structure:');
165
+ console.log('ID:', response.data?.id);
166
+ console.log('Meta:', response.data?.meta);
167
+ console.log('Results count:', response.data?.results.length);
168
+
169
+ // Log a sample of the results
170
+ if ((response.data?.results.length ?? 0) > 0) {
171
+ console.log(
172
+ 'Sample result:',
173
+ JSON.stringify(response.data?.results[0], null, 2)
174
+ );
175
+ }
176
+
177
+ if (response.data && response.data.results.length) {
178
+ return response.data.results.map((result) => {
179
+ const docIndex = result.index;
180
+ const score = result.relevance_score;
181
+ const text = documents[docIndex];
182
+ return { text, score };
183
+ });
184
+ } else {
185
+ console.warn(
186
+ 'Unexpected response format from Cohere API. Using default ranking.'
187
+ );
188
+ return this.getDefaultRanking(documents, topK);
189
+ }
190
+ } catch (error) {
191
+ console.error('Error using Cohere reranker:', error);
192
+ // Fallback to default ranking on error
193
+ return this.getDefaultRanking(documents, topK);
194
+ }
195
+ }
196
+ }
197
+
198
+ export class InfinityReranker extends BaseReranker {
199
+ constructor() {
200
+ super();
201
+ // No API key needed for the placeholder implementation
202
+ }
203
+
204
+ async rerank(
205
+ query: string,
206
+ documents: string[],
207
+ topK: number = 5
208
+ ): Promise<t.Highlight[]> {
209
+ console.log(
210
+ `Reranking ${documents.length} documents with Infinity (placeholder)`
211
+ );
212
+ // This would be replaced with actual Infinity reranker implementation
213
+ return this.getDefaultRanking(documents, topK);
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Creates the appropriate reranker based on type and configuration
219
+ */
220
+ export const createReranker = (config: {
221
+ rerankerType: t.RerankerType;
222
+ jinaApiKey?: string;
223
+ cohereApiKey?: string;
224
+ }): BaseReranker | undefined => {
225
+ const { rerankerType, jinaApiKey, cohereApiKey } = config;
226
+
227
+ switch (rerankerType.toLowerCase()) {
228
+ case 'jina':
229
+ return new JinaReranker({ apiKey: jinaApiKey });
230
+ case 'cohere':
231
+ return new CohereReranker({ apiKey: cohereApiKey });
232
+ case 'infinity':
233
+ return new InfinityReranker();
234
+ case 'none':
235
+ console.log('Skipping reranking as reranker is set to "none"');
236
+ return undefined;
237
+ default:
238
+ console.warn(
239
+ `Unknown reranker type: ${rerankerType}. Defaulting to InfinityReranker.`
240
+ );
241
+ return new JinaReranker({ apiKey: jinaApiKey });
242
+ }
243
+ };
244
+
245
+ // Example usage:
246
+ // const jinaReranker = new JinaReranker();
247
+ // const cohereReranker = new CohereReranker();
248
+ // const infinityReranker = new InfinityReranker();