@librechat/agents 2.4.63 → 2.4.65
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.
- package/dist/cjs/common/enum.cjs +6 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/main.cjs +4 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/run.cjs +6 -4
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/utils/title.cjs +35 -4
- package/dist/cjs/utils/title.cjs.map +1 -1
- package/dist/esm/common/enum.mjs +7 -1
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/main.mjs +1 -1
- package/dist/esm/run.mjs +8 -6
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/utils/title.mjs +35 -5
- package/dist/esm/utils/title.mjs.map +1 -1
- package/dist/types/common/enum.d.ts +5 -0
- package/dist/types/run.d.ts +3 -3
- package/dist/types/types/run.d.ts +2 -0
- package/dist/types/utils/title.d.ts +2 -1
- package/package.json +3 -1
- package/src/common/enum.ts +6 -0
- package/src/run.ts +14 -9
- package/src/scripts/simple.ts +1 -1
- package/src/specs/anthropic.simple.test.ts +57 -1
- package/src/specs/openai.simple.test.ts +56 -1
- package/src/specs/title.memory-leak.test.ts +453 -0
- package/src/types/run.ts +2 -0
- package/src/utils/title.ts +71 -14
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
|
|
3
|
+
import { config } from 'dotenv';
|
|
4
|
+
config();
|
|
5
|
+
import type { LLMResult } from '@langchain/core/outputs';
|
|
6
|
+
import { getLLMConfig } from '@/utils/llmConfig';
|
|
7
|
+
import { Providers, TitleMethod } from '@/common';
|
|
8
|
+
import { Run } from '@/run';
|
|
9
|
+
import type * as t from '@/types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Helper to force garbage collection if available
|
|
13
|
+
* Note: This requires Node.js to be run with --expose-gc flag
|
|
14
|
+
*/
|
|
15
|
+
function forceGC(): void {
|
|
16
|
+
if (global.gc) {
|
|
17
|
+
global.gc();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Helper to wait for potential async cleanup
|
|
23
|
+
*/
|
|
24
|
+
async function waitForCleanup(ms: number = 100): Promise<void> {
|
|
25
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Factory function to create a callback handler that captures LLM results
|
|
30
|
+
* without creating memory leaks through closures
|
|
31
|
+
*/
|
|
32
|
+
function createLLMResultCapture(): {
|
|
33
|
+
callback: {
|
|
34
|
+
handleLLMEnd(data: LLMResult): void;
|
|
35
|
+
};
|
|
36
|
+
getResult(): LLMResult | undefined;
|
|
37
|
+
} {
|
|
38
|
+
let capturedResult: LLMResult | undefined;
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
callback: {
|
|
42
|
+
handleLLMEnd(data: LLMResult): void {
|
|
43
|
+
capturedResult = data;
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
getResult(): LLMResult | undefined {
|
|
47
|
+
return capturedResult;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Test to ensure title generation doesn't create memory leaks
|
|
54
|
+
* Note: These tests verify that the title generation functions
|
|
55
|
+
* properly clean up after themselves and don't retain references
|
|
56
|
+
*/
|
|
57
|
+
describe('Title Generation Memory Leak Tests', () => {
|
|
58
|
+
jest.setTimeout(120000); // 2 minutes timeout for memory tests
|
|
59
|
+
|
|
60
|
+
const providers = [Providers.OPENAI];
|
|
61
|
+
|
|
62
|
+
providers.forEach((provider) => {
|
|
63
|
+
describe(`${provider} Memory Leak Tests`, () => {
|
|
64
|
+
let run: Run<t.IState>;
|
|
65
|
+
|
|
66
|
+
beforeEach(async () => {
|
|
67
|
+
const llmConfig = getLLMConfig(provider);
|
|
68
|
+
run = await Run.create<t.IState>({
|
|
69
|
+
runId: `memory-test-${Date.now()}`,
|
|
70
|
+
graphConfig: {
|
|
71
|
+
type: 'standard',
|
|
72
|
+
llmConfig,
|
|
73
|
+
tools: [],
|
|
74
|
+
instructions: 'You are a helpful assistant.',
|
|
75
|
+
},
|
|
76
|
+
returnContent: true,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('should not leak memory when using callback factory', async () => {
|
|
81
|
+
const weakRefs: WeakRef<LLMResult>[] = [];
|
|
82
|
+
const iterations = 5;
|
|
83
|
+
|
|
84
|
+
// Run multiple title generations with callbacks
|
|
85
|
+
for (let i = 0; i < iterations; i++) {
|
|
86
|
+
const resultCapture = createLLMResultCapture();
|
|
87
|
+
|
|
88
|
+
const result = await run.generateTitle({
|
|
89
|
+
provider,
|
|
90
|
+
inputText: `Test message ${i}`,
|
|
91
|
+
titleMethod: TitleMethod.STRUCTURED,
|
|
92
|
+
contentParts: [{ type: 'text' as const, text: `Response ${i}` }],
|
|
93
|
+
chainOptions: {
|
|
94
|
+
callbacks: [resultCapture.callback],
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(result).toBeDefined();
|
|
99
|
+
expect(result.title).toBeDefined();
|
|
100
|
+
expect(result.language).toBeDefined();
|
|
101
|
+
|
|
102
|
+
const capturedResult = resultCapture.getResult();
|
|
103
|
+
if (capturedResult) {
|
|
104
|
+
weakRefs.push(new WeakRef(capturedResult));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Clear references and wait for async operations
|
|
109
|
+
await waitForCleanup(3000);
|
|
110
|
+
forceGC();
|
|
111
|
+
await waitForCleanup(3000);
|
|
112
|
+
forceGC(); // Run GC twice to be thorough
|
|
113
|
+
await waitForCleanup(3000);
|
|
114
|
+
|
|
115
|
+
// Check that most LLMResult objects have been garbage collected
|
|
116
|
+
let aliveCount = 0;
|
|
117
|
+
weakRefs.forEach((ref) => {
|
|
118
|
+
if (ref.deref() !== undefined) {
|
|
119
|
+
aliveCount++;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// We expect most references to be collected
|
|
124
|
+
// LangChain may cache 0-2 results temporarily for optimization
|
|
125
|
+
// The exact number can vary based on timing and internal optimizations
|
|
126
|
+
expect(aliveCount).toBeLessThanOrEqual(2);
|
|
127
|
+
console.log(
|
|
128
|
+
`Memory leak test: ${aliveCount} out of ${iterations} references still alive`
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('should not accumulate callbacks across multiple invocations', async () => {
|
|
133
|
+
const callbackCounts: number[] = [];
|
|
134
|
+
|
|
135
|
+
// Run multiple title generations with the same callback pattern
|
|
136
|
+
for (let i = 0; i < 3; i++) {
|
|
137
|
+
let callbackInvocations = 0;
|
|
138
|
+
|
|
139
|
+
const trackingCallback = {
|
|
140
|
+
handleLLMEnd: (_data: LLMResult): void => {
|
|
141
|
+
callbackInvocations++;
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
await run.generateTitle({
|
|
146
|
+
provider,
|
|
147
|
+
inputText: `Test message ${i}`,
|
|
148
|
+
titleMethod: TitleMethod.STRUCTURED,
|
|
149
|
+
contentParts: [{ type: 'text' as const, text: `Response ${i}` }],
|
|
150
|
+
chainOptions: {
|
|
151
|
+
callbacks: [trackingCallback],
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Each generation should trigger the callback exactly once
|
|
156
|
+
callbackCounts.push(callbackInvocations);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Verify each invocation triggered the callback exactly once
|
|
160
|
+
expect(callbackCounts).toEqual([1, 1, 1]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('should isolate callback state between concurrent invocations', async () => {
|
|
164
|
+
const captures: ReturnType<typeof createLLMResultCapture>[] = [];
|
|
165
|
+
|
|
166
|
+
// Run multiple concurrent title generations
|
|
167
|
+
const promises = Array.from({ length: 5 }, (_, i) => {
|
|
168
|
+
const capture = createLLMResultCapture();
|
|
169
|
+
captures.push(capture);
|
|
170
|
+
|
|
171
|
+
return run.generateTitle({
|
|
172
|
+
provider,
|
|
173
|
+
inputText: `Concurrent test ${i}`,
|
|
174
|
+
titleMethod: TitleMethod.STRUCTURED,
|
|
175
|
+
contentParts: [
|
|
176
|
+
{ type: 'text' as const, text: `Concurrent response ${i}` },
|
|
177
|
+
],
|
|
178
|
+
chainOptions: {
|
|
179
|
+
callbacks: [capture.callback],
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const results = await Promise.all(promises);
|
|
185
|
+
|
|
186
|
+
// All results should be defined and have titles
|
|
187
|
+
results.forEach((result, i) => {
|
|
188
|
+
expect(result).toBeDefined();
|
|
189
|
+
expect(result.title).toBeDefined();
|
|
190
|
+
expect(result.language).toBeDefined();
|
|
191
|
+
|
|
192
|
+
// Each capture should have its own result
|
|
193
|
+
const capturedResult = captures[i].getResult();
|
|
194
|
+
expect(capturedResult).toBeDefined();
|
|
195
|
+
expect(capturedResult?.generations).toBeDefined();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Verify all captured results are unique instances
|
|
199
|
+
const capturedResults = captures
|
|
200
|
+
.map((c) => c.getResult())
|
|
201
|
+
.filter((r) => r !== undefined);
|
|
202
|
+
const uniqueResults = new Set(capturedResults);
|
|
203
|
+
expect(uniqueResults.size).toBe(capturedResults.length);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('should handle completion method with callbacks properly', async () => {
|
|
207
|
+
const resultCapture = createLLMResultCapture();
|
|
208
|
+
|
|
209
|
+
const result = await run.generateTitle({
|
|
210
|
+
provider,
|
|
211
|
+
inputText: 'Completion test',
|
|
212
|
+
titleMethod: TitleMethod.COMPLETION,
|
|
213
|
+
contentParts: [
|
|
214
|
+
{ type: 'text' as const, text: 'Response for completion' },
|
|
215
|
+
],
|
|
216
|
+
chainOptions: {
|
|
217
|
+
callbacks: [resultCapture.callback],
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
expect(result).toBeDefined();
|
|
222
|
+
expect(result.title).toBeDefined();
|
|
223
|
+
// Completion method doesn't return language
|
|
224
|
+
expect(result.language).toBeUndefined();
|
|
225
|
+
|
|
226
|
+
const capturedResult = resultCapture.getResult();
|
|
227
|
+
expect(capturedResult).toBeDefined();
|
|
228
|
+
expect(capturedResult?.generations).toBeDefined();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('factory function should create isolated instances', async () => {
|
|
232
|
+
// Create multiple captures
|
|
233
|
+
const captures = Array.from({ length: 3 }, () =>
|
|
234
|
+
createLLMResultCapture()
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// Simulate different LLM results
|
|
238
|
+
const mockResults: LLMResult[] = [
|
|
239
|
+
{ generations: [[{ text: 'Result 1' }]], llmOutput: {} },
|
|
240
|
+
{ generations: [[{ text: 'Result 2' }]], llmOutput: {} },
|
|
241
|
+
{ generations: [[{ text: 'Result 3' }]], llmOutput: {} },
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
// Each capture should store its own result
|
|
245
|
+
captures.forEach((capture, i) => {
|
|
246
|
+
capture.callback.handleLLMEnd(mockResults[i]);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Verify each capture has its own isolated result
|
|
250
|
+
captures.forEach((capture, i) => {
|
|
251
|
+
const result = capture.getResult();
|
|
252
|
+
expect(result).toBe(mockResults[i]);
|
|
253
|
+
expect(result?.generations[0][0].text).toBe(`Result ${i + 1}`);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('diagnostic: check if creating new Run instances helps', async () => {
|
|
258
|
+
const weakRefs: WeakRef<LLMResult>[] = [];
|
|
259
|
+
const iterations = 5;
|
|
260
|
+
|
|
261
|
+
// Create a new Run instance for each iteration
|
|
262
|
+
for (let i = 0; i < iterations; i++) {
|
|
263
|
+
const llmConfig = getLLMConfig(provider);
|
|
264
|
+
const newRun = await Run.create<t.IState>({
|
|
265
|
+
runId: `memory-test-${Date.now()}-${i}`,
|
|
266
|
+
graphConfig: {
|
|
267
|
+
type: 'standard',
|
|
268
|
+
llmConfig,
|
|
269
|
+
tools: [],
|
|
270
|
+
instructions: 'You are a helpful assistant.',
|
|
271
|
+
},
|
|
272
|
+
returnContent: true,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const resultCapture = createLLMResultCapture();
|
|
276
|
+
|
|
277
|
+
await newRun.generateTitle({
|
|
278
|
+
provider,
|
|
279
|
+
inputText: `Test message ${i}`,
|
|
280
|
+
titleMethod: TitleMethod.STRUCTURED,
|
|
281
|
+
contentParts: [{ type: 'text' as const, text: `Response ${i}` }],
|
|
282
|
+
chainOptions: {
|
|
283
|
+
callbacks: [resultCapture.callback],
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const capturedResult = resultCapture.getResult();
|
|
288
|
+
if (capturedResult) {
|
|
289
|
+
weakRefs.push(new WeakRef(capturedResult));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Clear references and wait for async operations
|
|
294
|
+
await waitForCleanup(200);
|
|
295
|
+
forceGC();
|
|
296
|
+
await waitForCleanup(200);
|
|
297
|
+
forceGC();
|
|
298
|
+
await waitForCleanup(100);
|
|
299
|
+
|
|
300
|
+
// Check how many references are still alive
|
|
301
|
+
let aliveCount = 0;
|
|
302
|
+
weakRefs.forEach((ref) => {
|
|
303
|
+
if (ref.deref() !== undefined) {
|
|
304
|
+
aliveCount++;
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
console.log(
|
|
309
|
+
`Diagnostic (new Run instances): ${aliveCount} out of ${iterations} references still alive`
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
// Hypothesis: If it's still 2, it's LangChain global cache
|
|
313
|
+
// If it's 5 or more, it's per-instance caching
|
|
314
|
+
expect(aliveCount).toBeLessThanOrEqual(2);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test('memory retention patterns with different scenarios', async () => {
|
|
318
|
+
const scenarios = [
|
|
319
|
+
{
|
|
320
|
+
iterations: 3,
|
|
321
|
+
waitTime: 100,
|
|
322
|
+
description: '3 iterations, short wait',
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
iterations: 5,
|
|
326
|
+
waitTime: 200,
|
|
327
|
+
description: '5 iterations, medium wait',
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
iterations: 10,
|
|
331
|
+
waitTime: 300,
|
|
332
|
+
description: '10 iterations, long wait',
|
|
333
|
+
},
|
|
334
|
+
];
|
|
335
|
+
|
|
336
|
+
for (const scenario of scenarios) {
|
|
337
|
+
const weakRefs: WeakRef<LLMResult>[] = [];
|
|
338
|
+
|
|
339
|
+
// Run title generations
|
|
340
|
+
for (let i = 0; i < scenario.iterations; i++) {
|
|
341
|
+
const resultCapture = createLLMResultCapture();
|
|
342
|
+
|
|
343
|
+
await run.generateTitle({
|
|
344
|
+
provider,
|
|
345
|
+
inputText: `Test message ${i}`,
|
|
346
|
+
titleMethod: TitleMethod.STRUCTURED,
|
|
347
|
+
contentParts: [{ type: 'text' as const, text: `Response ${i}` }],
|
|
348
|
+
chainOptions: {
|
|
349
|
+
callbacks: [resultCapture.callback],
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const capturedResult = resultCapture.getResult();
|
|
354
|
+
if (capturedResult) {
|
|
355
|
+
weakRefs.push(new WeakRef(capturedResult));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Multiple cleanup cycles with increasing wait times
|
|
360
|
+
await waitForCleanup(scenario.waitTime);
|
|
361
|
+
forceGC();
|
|
362
|
+
await waitForCleanup(scenario.waitTime);
|
|
363
|
+
forceGC();
|
|
364
|
+
await waitForCleanup(scenario.waitTime / 2);
|
|
365
|
+
|
|
366
|
+
// Count alive references
|
|
367
|
+
let aliveCount = 0;
|
|
368
|
+
weakRefs.forEach((ref) => {
|
|
369
|
+
if (ref.deref() !== undefined) {
|
|
370
|
+
aliveCount++;
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
console.log(
|
|
375
|
+
`${scenario.description}: ${aliveCount} out of ${scenario.iterations} references still alive`
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
// Expect 0-2 references regardless of iteration count
|
|
379
|
+
expect(aliveCount).toBeLessThanOrEqual(2);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test('should properly handle skipLanguage option with callbacks', async () => {
|
|
384
|
+
const resultCapture = createLLMResultCapture();
|
|
385
|
+
|
|
386
|
+
const result = await run.generateTitle({
|
|
387
|
+
provider,
|
|
388
|
+
inputText: 'Skip language test',
|
|
389
|
+
titleMethod: TitleMethod.STRUCTURED,
|
|
390
|
+
contentParts: [{ type: 'text' as const, text: 'Response' }],
|
|
391
|
+
skipLanguage: true,
|
|
392
|
+
chainOptions: {
|
|
393
|
+
callbacks: [resultCapture.callback],
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
expect(result).toBeDefined();
|
|
398
|
+
expect(result.title).toBeDefined();
|
|
399
|
+
// When skipLanguage is true, language should not be returned
|
|
400
|
+
expect(result.language).toBeUndefined();
|
|
401
|
+
|
|
402
|
+
const capturedResult = resultCapture.getResult();
|
|
403
|
+
expect(capturedResult).toBeDefined();
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test('should handle errors gracefully', async () => {
|
|
409
|
+
const llmConfig = getLLMConfig(Providers.OPENAI);
|
|
410
|
+
|
|
411
|
+
// Create a run with invalid configuration to trigger errors
|
|
412
|
+
const run = await Run.create<t.IState>({
|
|
413
|
+
runId: 'error-test',
|
|
414
|
+
graphConfig: {
|
|
415
|
+
type: 'standard',
|
|
416
|
+
llmConfig: {
|
|
417
|
+
...llmConfig,
|
|
418
|
+
apiKey: 'invalid-key', // This will cause API errors
|
|
419
|
+
},
|
|
420
|
+
tools: [],
|
|
421
|
+
instructions: 'Test',
|
|
422
|
+
},
|
|
423
|
+
returnContent: true,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// Attempt multiple failing title generations
|
|
427
|
+
for (let i = 0; i < 3; i++) {
|
|
428
|
+
try {
|
|
429
|
+
const resultCapture = createLLMResultCapture();
|
|
430
|
+
|
|
431
|
+
await run.generateTitle({
|
|
432
|
+
provider: Providers.OPENAI,
|
|
433
|
+
inputText: `Error test ${i}`,
|
|
434
|
+
titleMethod: TitleMethod.STRUCTURED,
|
|
435
|
+
contentParts: [{ type: 'text' as const, text: `Response ${i}` }],
|
|
436
|
+
chainOptions: {
|
|
437
|
+
callbacks: [resultCapture.callback],
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// Should not reach here
|
|
442
|
+
fail('Expected error to be thrown');
|
|
443
|
+
} catch (error) {
|
|
444
|
+
// Expected to fail due to invalid API key
|
|
445
|
+
console.log(
|
|
446
|
+
`Expected error ${i}:`,
|
|
447
|
+
error instanceof Error ? error.message : String(error)
|
|
448
|
+
);
|
|
449
|
+
expect(error).toBeDefined();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
});
|
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 {
|
package/src/utils/title.ts
CHANGED
|
@@ -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';
|
|
4
|
-
import
|
|
5
|
-
import
|
|
3
|
+
import { ChatPromptTemplate } from '@langchain/core/prompts';
|
|
4
|
+
import type { Runnable, RunnableConfig } from '@langchain/core/runnables';
|
|
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
|
|
@@ -44,20 +45,29 @@ export const createTitleRunnable = async (
|
|
|
44
45
|
);
|
|
45
46
|
|
|
46
47
|
return new RunnableLambda({
|
|
47
|
-
func: async (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
func: async (
|
|
49
|
+
input: {
|
|
50
|
+
convo: string;
|
|
51
|
+
inputText: string;
|
|
52
|
+
skipLanguage: boolean;
|
|
53
|
+
},
|
|
54
|
+
config?: Partial<RunnableConfig>
|
|
55
|
+
): Promise<{ language: string; title: string } | { title: string }> => {
|
|
52
56
|
if (input.skipLanguage) {
|
|
53
|
-
return (await titlePrompt.pipe(titleLLM).invoke(
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
return (await titlePrompt.pipe(titleLLM).invoke(
|
|
58
|
+
{
|
|
59
|
+
convo: input.convo,
|
|
60
|
+
},
|
|
61
|
+
config
|
|
62
|
+
)) as { title: string };
|
|
56
63
|
}
|
|
57
64
|
|
|
58
|
-
const result = (await titlePrompt.pipe(combinedLLM).invoke(
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
const result = (await titlePrompt.pipe(combinedLLM).invoke(
|
|
66
|
+
{
|
|
67
|
+
convo: input.convo,
|
|
68
|
+
},
|
|
69
|
+
config
|
|
70
|
+
)) as { language: string; title: string } | undefined;
|
|
61
71
|
|
|
62
72
|
return {
|
|
63
73
|
language: result?.language ?? 'English',
|
|
@@ -66,3 +76,50 @@ export const createTitleRunnable = async (
|
|
|
66
76
|
},
|
|
67
77
|
});
|
|
68
78
|
};
|
|
79
|
+
|
|
80
|
+
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.
|
|
81
|
+
|
|
82
|
+
Conversation:
|
|
83
|
+
{convo}`;
|
|
84
|
+
|
|
85
|
+
export const createCompletionTitleRunnable = async (
|
|
86
|
+
model: t.ChatModelInstance,
|
|
87
|
+
titlePrompt?: string
|
|
88
|
+
): Promise<Runnable> => {
|
|
89
|
+
const completionPrompt = ChatPromptTemplate.fromTemplate(
|
|
90
|
+
titlePrompt ?? defaultCompletionPrompt
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return new RunnableLambda({
|
|
94
|
+
func: async (
|
|
95
|
+
input: {
|
|
96
|
+
convo: string;
|
|
97
|
+
inputText: string;
|
|
98
|
+
skipLanguage: boolean;
|
|
99
|
+
},
|
|
100
|
+
config?: Partial<RunnableConfig>
|
|
101
|
+
): Promise<{ title: string }> => {
|
|
102
|
+
const promptOutput = await completionPrompt.invoke({
|
|
103
|
+
convo: input.convo,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const response = await model.invoke(promptOutput, config);
|
|
107
|
+
let content = '';
|
|
108
|
+
if (typeof response.content === 'string') {
|
|
109
|
+
content = response.content;
|
|
110
|
+
} else if (Array.isArray(response.content)) {
|
|
111
|
+
content = response.content
|
|
112
|
+
.filter(
|
|
113
|
+
(part): part is { type: ContentTypes.TEXT; text: string } =>
|
|
114
|
+
part.type === ContentTypes.TEXT
|
|
115
|
+
)
|
|
116
|
+
.map((part) => part.text)
|
|
117
|
+
.join('');
|
|
118
|
+
}
|
|
119
|
+
const title = content.trim();
|
|
120
|
+
return {
|
|
121
|
+
title,
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
};
|