@librechat/agents 2.4.62 → 2.4.64

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 (40) hide show
  1. package/dist/cjs/common/enum.cjs +6 -0
  2. package/dist/cjs/common/enum.cjs.map +1 -1
  3. package/dist/cjs/main.cjs +4 -0
  4. package/dist/cjs/main.cjs.map +1 -1
  5. package/dist/cjs/run.cjs +5 -3
  6. package/dist/cjs/run.cjs.map +1 -1
  7. package/dist/cjs/tools/search/firecrawl.cjs +54 -8
  8. package/dist/cjs/tools/search/firecrawl.cjs.map +1 -1
  9. package/dist/cjs/tools/search/tool.cjs +4 -3
  10. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  11. package/dist/cjs/utils/title.cjs +32 -1
  12. package/dist/cjs/utils/title.cjs.map +1 -1
  13. package/dist/esm/common/enum.mjs +7 -1
  14. package/dist/esm/common/enum.mjs.map +1 -1
  15. package/dist/esm/main.mjs +1 -1
  16. package/dist/esm/run.mjs +7 -5
  17. package/dist/esm/run.mjs.map +1 -1
  18. package/dist/esm/tools/search/firecrawl.mjs +54 -8
  19. package/dist/esm/tools/search/firecrawl.mjs.map +1 -1
  20. package/dist/esm/tools/search/tool.mjs +4 -3
  21. package/dist/esm/tools/search/tool.mjs.map +1 -1
  22. package/dist/esm/utils/title.mjs +32 -2
  23. package/dist/esm/utils/title.mjs.map +1 -1
  24. package/dist/types/common/enum.d.ts +5 -0
  25. package/dist/types/run.d.ts +3 -3
  26. package/dist/types/tools/search/firecrawl.d.ts +15 -0
  27. package/dist/types/tools/search/types.d.ts +20 -9
  28. package/dist/types/types/run.d.ts +2 -0
  29. package/dist/types/utils/title.d.ts +2 -1
  30. package/package.json +3 -3
  31. package/src/common/enum.ts +6 -0
  32. package/src/run.ts +14 -6
  33. package/src/scripts/simple.ts +1 -1
  34. package/src/specs/anthropic.simple.test.ts +58 -1
  35. package/src/specs/openai.simple.test.ts +57 -1
  36. package/src/tools/search/firecrawl.ts +68 -19
  37. package/src/tools/search/tool.ts +4 -3
  38. package/src/tools/search/types.ts +20 -10
  39. package/src/types/run.ts +2 -0
  40. package/src/utils/title.ts +47 -2
package/src/run.ts CHANGED
@@ -10,10 +10,13 @@ import type {
10
10
  import type { ClientCallbacks, SystemCallbacks } from '@/graphs/Graph';
11
11
  import type { RunnableConfig } from '@langchain/core/runnables';
12
12
  import type * as t from '@/types';
13
- import { GraphEvents, Providers, Callback } from '@/common';
13
+ import { GraphEvents, Providers, Callback, TitleMethod } from '@/common';
14
14
  import { manualToolStreamProviders } from '@/llm/providers';
15
15
  import { shiftIndexTokenCountMap } from '@/messages/format';
16
- import { createTitleRunnable } from '@/utils/title';
16
+ import {
17
+ createTitleRunnable,
18
+ createCompletionTitleRunnable,
19
+ } from '@/utils/title';
17
20
  import { createTokenCounter } from '@/utils/tokens';
18
21
  import { StandardGraph } from '@/graphs/Graph';
19
22
  import { HandlerRegistry } from '@/events';
@@ -259,9 +262,11 @@ export class Run<T extends t.BaseGraphState> {
259
262
  chainOptions,
260
263
  skipLanguage,
261
264
  omitOptions = defaultOmitOptions,
262
- }: t.RunTitleOptions): Promise<{ language: string; title: string }> {
265
+ titleMethod = TitleMethod.COMPLETION,
266
+ convoPromptTemplate,
267
+ }: t.RunTitleOptions): Promise<{ language?: string; title?: string }> {
263
268
  const convoTemplate = PromptTemplate.fromTemplate(
264
- 'User: {input}\nAI: {output}'
269
+ convoPromptTemplate ?? 'User: {input}\nAI: {output}'
265
270
  );
266
271
  const response = contentParts
267
272
  .map((part) => {
@@ -297,10 +302,13 @@ export class Run<T extends t.BaseGraphState> {
297
302
  model.n = (clientOptions as t.OpenAIClientOptions | undefined)
298
303
  ?.n as number;
299
304
  }
300
- const chain = await createTitleRunnable(model, titlePrompt);
305
+ const chain =
306
+ titleMethod === TitleMethod.COMPLETION
307
+ ? await createCompletionTitleRunnable(model, titlePrompt)
308
+ : await createTitleRunnable(model, titlePrompt);
301
309
  return (await chain.invoke(
302
310
  { convo, inputText, skipLanguage },
303
311
  chainOptions
304
- )) as { language: string; title: string };
312
+ )) as { language?: string; title?: string };
305
313
  }
306
314
  }
@@ -14,9 +14,9 @@ import {
14
14
  ModelEndHandler,
15
15
  createMetadataAggregator,
16
16
  } from '@/events';
17
+ import { GraphEvents, Providers, TitleMethod } from '@/common';
17
18
  import { getLLMConfig } from '@/utils/llmConfig';
18
19
  import { getArgs } from '@/scripts/args';
19
- import { GraphEvents, Providers } from '@/common';
20
20
  import { Run } from '@/run';
21
21
 
22
22
  const conversationHistory: BaseMessage[] = [];
@@ -16,8 +16,8 @@ import {
16
16
  ModelEndHandler,
17
17
  createMetadataAggregator,
18
18
  } from '@/events';
19
+ import { ContentTypes, GraphEvents, Providers, TitleMethod } from '@/common';
19
20
  import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
20
- import { ContentTypes, GraphEvents, Providers } from '@/common';
21
21
  import { capitalizeFirstLetter } from './spec.utils';
22
22
  import { getLLMConfig } from '@/utils/llmConfig';
23
23
  import { getArgs } from '@/scripts/args';
@@ -175,6 +175,7 @@ describe(`${capitalizeFirstLetter(provider)} Streaming Tests`, () => {
175
175
  const titleResult = await run.generateTitle({
176
176
  provider,
177
177
  inputText: userMessage,
178
+ titleMethod: TitleMethod.STRUCTURED,
178
179
  contentParts,
179
180
  chainOptions: {
180
181
  callbacks: [
@@ -191,6 +192,62 @@ describe(`${capitalizeFirstLetter(provider)} Streaming Tests`, () => {
191
192
  expect(collected).toBeDefined();
192
193
  });
193
194
 
195
+ test(`${capitalizeFirstLetter(provider)}: should generate title using completion method`, async () => {
196
+ const { userName, location } = await getArgs();
197
+ const llmConfig = getLLMConfig(provider);
198
+ const customHandlers = setupCustomHandlers();
199
+
200
+ run = await Run.create<t.IState>({
201
+ runId: 'test-run-id-completion',
202
+ graphConfig: {
203
+ type: 'standard',
204
+ llmConfig,
205
+ tools: [new Calculator()],
206
+ instructions:
207
+ 'You are a friendly AI assistant. Always address the user by their name.',
208
+ additional_instructions: `The user's name is ${userName} and they are located in ${location}.`,
209
+ },
210
+ returnContent: true,
211
+ customHandlers,
212
+ });
213
+
214
+ const userMessage =
215
+ 'Can you help me calculate the area of a circle with radius 5?';
216
+ conversationHistory = [];
217
+ conversationHistory.push(new HumanMessage(userMessage));
218
+
219
+ const inputs = {
220
+ messages: conversationHistory,
221
+ };
222
+
223
+ const finalContentParts = await run.processStream(inputs, config);
224
+ expect(finalContentParts).toBeDefined();
225
+
226
+ const { handleLLMEnd, collected } = createMetadataAggregator();
227
+ const titleResult = await run.generateTitle({
228
+ provider,
229
+ inputText: userMessage,
230
+ titleMethod: TitleMethod.COMPLETION, // Using completion method
231
+ contentParts,
232
+ chainOptions: {
233
+ callbacks: [
234
+ {
235
+ handleLLMEnd,
236
+ },
237
+ ],
238
+ },
239
+ });
240
+
241
+ expect(titleResult).toBeDefined();
242
+ expect(titleResult.title).toBeDefined();
243
+ expect(titleResult.title).not.toBe('');
244
+ expect(titleResult.title).not.toBe('Untitled Conversation');
245
+ // Completion method doesn't return language
246
+ expect(titleResult.language).toBeUndefined();
247
+ expect(collected).toBeDefined();
248
+ console.log(`Completion method generated title: "${titleResult.title}"`);
249
+ });
250
+
194
251
  test(`${capitalizeFirstLetter(provider)}: should follow-up`, async () => {
195
252
  console.log('Previous conversation length:', runningHistory.length);
196
253
  console.log(
@@ -16,8 +16,8 @@ import {
16
16
  ModelEndHandler,
17
17
  createMetadataAggregator,
18
18
  } from '@/events';
19
+ import { ContentTypes, GraphEvents, Providers, TitleMethod } from '@/common';
19
20
  import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
20
- import { ContentTypes, GraphEvents, Providers } from '@/common';
21
21
  import { capitalizeFirstLetter } from './spec.utils';
22
22
  import { getLLMConfig } from '@/utils/llmConfig';
23
23
  import { getArgs } from '@/scripts/args';
@@ -175,6 +175,7 @@ describe(`${capitalizeFirstLetter(provider)} Streaming Tests`, () => {
175
175
  const titleResult = await run.generateTitle({
176
176
  provider,
177
177
  inputText: userMessage,
178
+ titleMethod: TitleMethod.STRUCTURED,
178
179
  contentParts,
179
180
  chainOptions: {
180
181
  callbacks: [
@@ -191,6 +192,61 @@ describe(`${capitalizeFirstLetter(provider)} Streaming Tests`, () => {
191
192
  expect(collected).toBeDefined();
192
193
  });
193
194
 
195
+ test(`${capitalizeFirstLetter(provider)}: should generate title using completion method`, async () => {
196
+ const { userName, location } = await getArgs();
197
+ const llmConfig = getLLMConfig(provider);
198
+ const customHandlers = setupCustomHandlers();
199
+
200
+ run = await Run.create<t.IState>({
201
+ runId: 'test-run-id-completion',
202
+ graphConfig: {
203
+ type: 'standard',
204
+ llmConfig,
205
+ tools: [new Calculator()],
206
+ instructions:
207
+ 'You are a friendly AI assistant. Always address the user by their name.',
208
+ additional_instructions: `The user's name is ${userName} and they are located in ${location}.`,
209
+ },
210
+ returnContent: true,
211
+ customHandlers,
212
+ });
213
+
214
+ const userMessage = 'What is the weather like today?';
215
+ conversationHistory = [];
216
+ conversationHistory.push(new HumanMessage(userMessage));
217
+
218
+ const inputs = {
219
+ messages: conversationHistory,
220
+ };
221
+
222
+ const finalContentParts = await run.processStream(inputs, config);
223
+ expect(finalContentParts).toBeDefined();
224
+
225
+ const { handleLLMEnd, collected } = createMetadataAggregator();
226
+ const titleResult = await run.generateTitle({
227
+ provider,
228
+ inputText: userMessage,
229
+ titleMethod: TitleMethod.COMPLETION, // Using completion method
230
+ contentParts,
231
+ chainOptions: {
232
+ callbacks: [
233
+ {
234
+ handleLLMEnd,
235
+ },
236
+ ],
237
+ },
238
+ });
239
+
240
+ expect(titleResult).toBeDefined();
241
+ expect(titleResult.title).toBeDefined();
242
+ expect(titleResult.title).not.toBe('');
243
+ expect(titleResult.title).not.toBe('Untitled Conversation');
244
+ // Completion method doesn't return language
245
+ expect(titleResult.language).toBeUndefined();
246
+ expect(collected).toBeDefined();
247
+ console.log(`Completion method generated title: "${titleResult.title}"`);
248
+ });
249
+
194
250
  test(`${capitalizeFirstLetter(provider)}: should follow-up`, async () => {
195
251
  console.log('Previous conversation length:', runningHistory.length);
196
252
  console.log(
@@ -13,6 +13,21 @@ export class FirecrawlScraper {
13
13
  private defaultFormats: string[];
14
14
  private timeout: number;
15
15
  private logger: t.Logger;
16
+ private includeTags?: string[];
17
+ private excludeTags?: string[];
18
+ private waitFor?: number;
19
+ private maxAge?: number;
20
+ private mobile?: boolean;
21
+ private skipTlsVerification?: boolean;
22
+ private blockAds?: boolean;
23
+ private removeBase64Images?: boolean;
24
+ private parsePDF?: boolean;
25
+ private storeInCache?: boolean;
26
+ private zeroDataRetention?: boolean;
27
+ private headers?: Record<string, string>;
28
+ private location?: { country?: string; languages?: string[] };
29
+ private onlyMainContent?: boolean;
30
+ private changeTrackingOptions?: object;
16
31
 
17
32
  constructor(config: t.FirecrawlScraperConfig = {}) {
18
33
  this.apiKey = config.apiKey ?? process.env.FIRECRAWL_API_KEY ?? '';
@@ -23,11 +38,27 @@ export class FirecrawlScraper {
23
38
  'https://api.firecrawl.dev';
24
39
  this.apiUrl = `${baseUrl.replace(/\/+$/, '')}/v1/scrape`;
25
40
 
26
- this.defaultFormats = config.formats ?? ['markdown', 'html'];
41
+ this.defaultFormats = config.formats ?? ['markdown', 'rawHtml'];
27
42
  this.timeout = config.timeout ?? 7500;
28
43
 
29
44
  this.logger = config.logger || createDefaultLogger();
30
45
 
46
+ this.includeTags = config.includeTags;
47
+ this.excludeTags = config.excludeTags;
48
+ this.waitFor = config.waitFor;
49
+ this.maxAge = config.maxAge;
50
+ this.mobile = config.mobile;
51
+ this.skipTlsVerification = config.skipTlsVerification;
52
+ this.blockAds = config.blockAds;
53
+ this.removeBase64Images = config.removeBase64Images;
54
+ this.parsePDF = config.parsePDF;
55
+ this.storeInCache = config.storeInCache;
56
+ this.zeroDataRetention = config.zeroDataRetention;
57
+ this.headers = config.headers;
58
+ this.location = config.location;
59
+ this.onlyMainContent = config.onlyMainContent;
60
+ this.changeTrackingOptions = config.changeTrackingOptions;
61
+
31
62
  if (!this.apiKey) {
32
63
  this.logger.warn('FIRECRAWL_API_KEY is not set. Scraping will not work.');
33
64
  }
@@ -58,25 +89,36 @@ export class FirecrawlScraper {
58
89
  }
59
90
 
60
91
  try {
61
- const response = await axios.post(
62
- this.apiUrl,
63
- {
64
- url,
65
- formats: options.formats || this.defaultFormats,
66
- includeTags: options.includeTags,
67
- excludeTags: options.excludeTags,
68
- headers: options.headers,
69
- waitFor: options.waitFor,
70
- timeout: options.timeout ?? this.timeout,
92
+ const payload = omitUndefined({
93
+ url,
94
+ formats: options.formats ?? this.defaultFormats,
95
+ includeTags: options.includeTags ?? this.includeTags,
96
+ excludeTags: options.excludeTags ?? this.excludeTags,
97
+ headers: options.headers ?? this.headers,
98
+ waitFor: options.waitFor ?? this.waitFor,
99
+ timeout: options.timeout ?? this.timeout,
100
+ onlyMainContent: options.onlyMainContent ?? this.onlyMainContent,
101
+ maxAge: options.maxAge ?? this.maxAge,
102
+ mobile: options.mobile ?? this.mobile,
103
+ skipTlsVerification:
104
+ options.skipTlsVerification ?? this.skipTlsVerification,
105
+ parsePDF: options.parsePDF ?? this.parsePDF,
106
+ location: options.location ?? this.location,
107
+ removeBase64Images:
108
+ options.removeBase64Images ?? this.removeBase64Images,
109
+ blockAds: options.blockAds ?? this.blockAds,
110
+ storeInCache: options.storeInCache ?? this.storeInCache,
111
+ zeroDataRetention: options.zeroDataRetention ?? this.zeroDataRetention,
112
+ changeTrackingOptions:
113
+ options.changeTrackingOptions ?? this.changeTrackingOptions,
114
+ });
115
+ const response = await axios.post(this.apiUrl, payload, {
116
+ headers: {
117
+ 'Content-Type': 'application/json',
118
+ Authorization: `Bearer ${this.apiKey}`,
71
119
  },
72
- {
73
- headers: {
74
- 'Content-Type': 'application/json',
75
- Authorization: `Bearer ${this.apiKey}`,
76
- },
77
- timeout: this.timeout,
78
- }
79
- );
120
+ timeout: this.timeout,
121
+ });
80
122
 
81
123
  return [url, response.data];
82
124
  } catch (error) {
@@ -156,3 +198,10 @@ export const createFirecrawlScraper = (
156
198
  ): FirecrawlScraper => {
157
199
  return new FirecrawlScraper(config);
158
200
  };
201
+
202
+ // Helper function to clean up payload for firecrawl
203
+ function omitUndefined<T extends object>(obj: T): Partial<T> {
204
+ return Object.fromEntries(
205
+ Object.entries(obj).filter(([, v]) => v !== undefined)
206
+ ) as Partial<T>;
207
+ }
@@ -346,7 +346,7 @@ export const createSearchTool = (
346
346
  safeSearch = 1,
347
347
  firecrawlApiKey,
348
348
  firecrawlApiUrl,
349
- firecrawlFormats = ['markdown', 'html'],
349
+ firecrawlOptions,
350
350
  scraperTimeout,
351
351
  jinaApiKey,
352
352
  cohereApiKey,
@@ -385,10 +385,11 @@ export const createSearchTool = (
385
385
  });
386
386
 
387
387
  const firecrawlScraper = createFirecrawlScraper({
388
+ ...firecrawlOptions,
388
389
  apiKey: firecrawlApiKey ?? process.env.FIRECRAWL_API_KEY,
389
390
  apiUrl: firecrawlApiUrl,
390
- timeout: scraperTimeout,
391
- formats: firecrawlFormats,
391
+ timeout: scraperTimeout ?? firecrawlOptions?.timeout,
392
+ formats: firecrawlOptions?.formats ?? ['markdown', 'rawHtml'],
392
393
  });
393
394
 
394
395
  const selectedReranker = createReranker({
@@ -94,7 +94,7 @@ export interface ProcessSourcesConfig {
94
94
  export interface FirecrawlConfig {
95
95
  firecrawlApiKey?: string;
96
96
  firecrawlApiUrl?: string;
97
- firecrawlFormats?: string[];
97
+ firecrawlOptions?: FirecrawlScraperConfig;
98
98
  }
99
99
 
100
100
  export interface ScraperContentResult {
@@ -170,15 +170,10 @@ export type UsedReferences = {
170
170
  }[];
171
171
 
172
172
  /** Firecrawl */
173
-
174
- export interface FirecrawlScrapeOptions {
175
- formats?: string[];
176
- includeTags?: string[];
177
- excludeTags?: string[];
178
- headers?: Record<string, string>;
179
- waitFor?: number;
180
- timeout?: number;
181
- }
173
+ export type FirecrawlScrapeOptions = Omit<
174
+ FirecrawlScraperConfig,
175
+ 'apiKey' | 'apiUrl' | 'logger'
176
+ >;
182
177
 
183
178
  export interface ScrapeMetadata {
184
179
  // Core source information
@@ -261,6 +256,21 @@ export interface FirecrawlScraperConfig {
261
256
  formats?: string[];
262
257
  timeout?: number;
263
258
  logger?: Logger;
259
+ includeTags?: string[];
260
+ excludeTags?: string[];
261
+ waitFor?: number;
262
+ maxAge?: number;
263
+ mobile?: boolean;
264
+ skipTlsVerification?: boolean;
265
+ blockAds?: boolean;
266
+ removeBase64Images?: boolean;
267
+ parsePDF?: boolean;
268
+ storeInCache?: boolean;
269
+ zeroDataRetention?: boolean;
270
+ headers?: Record<string, string>;
271
+ location?: { country?: string; languages?: string[] };
272
+ onlyMainContent?: boolean;
273
+ changeTrackingOptions?: object;
264
274
  }
265
275
 
266
276
  export type GetSourcesParams = {
package/src/types/run.ts CHANGED
@@ -33,6 +33,8 @@ export type RunTitleOptions = {
33
33
  clientOptions?: l.ClientOptions;
34
34
  chainOptions?: Partial<RunnableConfig> | undefined;
35
35
  omitOptions?: Set<string>;
36
+ titleMethod?: e.TitleMethod;
37
+ convoPromptTemplate?: string;
36
38
  };
37
39
 
38
40
  export interface AgentStateChannels {
@@ -1,8 +1,9 @@
1
1
  import { z } from 'zod';
2
- import { ChatPromptTemplate } from '@langchain/core/prompts';
3
2
  import { RunnableLambda } from '@langchain/core/runnables';
3
+ import { ChatPromptTemplate } from '@langchain/core/prompts';
4
4
  import type { Runnable } from '@langchain/core/runnables';
5
- import * as t from '@/types';
5
+ import type * as t from '@/types';
6
+ import { ContentTypes } from '@/common';
6
7
 
7
8
  const defaultTitlePrompt = `Analyze this conversation and provide:
8
9
  1. The detected language of the conversation
@@ -66,3 +67,47 @@ export const createTitleRunnable = async (
66
67
  },
67
68
  });
68
69
  };
70
+
71
+ const defaultCompletionPrompt = `Provide a concise, 5-word-or-less title for the conversation, using its same language, with no punctuation. Apply title case conventions appropriate for the language. Never directly mention the language name or the word "title" and only return the title itself.
72
+
73
+ Conversation:
74
+ {convo}`;
75
+
76
+ export const createCompletionTitleRunnable = async (
77
+ model: t.ChatModelInstance,
78
+ titlePrompt?: string
79
+ ): Promise<Runnable> => {
80
+ const completionPrompt = ChatPromptTemplate.fromTemplate(
81
+ titlePrompt ?? defaultCompletionPrompt
82
+ );
83
+
84
+ return new RunnableLambda({
85
+ func: async (input: {
86
+ convo: string;
87
+ inputText: string;
88
+ skipLanguage: boolean;
89
+ }): Promise<{ title: string }> => {
90
+ const promptOutput = await completionPrompt.invoke({
91
+ convo: input.convo,
92
+ });
93
+
94
+ const response = await model.invoke(promptOutput);
95
+ let content = '';
96
+ if (typeof response.content === 'string') {
97
+ content = response.content;
98
+ } else if (Array.isArray(response.content)) {
99
+ content = response.content
100
+ .filter(
101
+ (part): part is { type: ContentTypes.TEXT; text: string } =>
102
+ part.type === ContentTypes.TEXT
103
+ )
104
+ .map((part) => part.text)
105
+ .join('');
106
+ }
107
+ const title = content.trim();
108
+ return {
109
+ title: title || 'Untitled Conversation',
110
+ };
111
+ },
112
+ });
113
+ };