@librechat/agents 2.0.5 → 2.1.1

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 (87) 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/events.cjs +10 -0
  4. package/dist/cjs/events.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +34 -5
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/llm/anthropic/llm.cjs +1 -3
  8. package/dist/cjs/llm/anthropic/llm.cjs.map +1 -1
  9. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  10. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
  11. package/dist/cjs/llm/fake.cjs +55 -0
  12. package/dist/cjs/llm/fake.cjs.map +1 -0
  13. package/dist/cjs/llm/providers.cjs +7 -5
  14. package/dist/cjs/llm/providers.cjs.map +1 -1
  15. package/dist/cjs/llm/text.cjs.map +1 -1
  16. package/dist/cjs/messages.cjs.map +1 -1
  17. package/dist/cjs/run.cjs +3 -7
  18. package/dist/cjs/run.cjs.map +1 -1
  19. package/dist/cjs/splitStream.cjs.map +1 -1
  20. package/dist/cjs/stream.cjs +93 -55
  21. package/dist/cjs/stream.cjs.map +1 -1
  22. package/dist/cjs/tools/CodeExecutor.cjs +8 -2
  23. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  24. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  25. package/dist/cjs/utils/graph.cjs.map +1 -1
  26. package/dist/cjs/utils/llm.cjs.map +1 -1
  27. package/dist/cjs/utils/misc.cjs.map +1 -1
  28. package/dist/cjs/utils/run.cjs.map +1 -1
  29. package/dist/cjs/utils/title.cjs.map +1 -1
  30. package/dist/esm/common/enum.mjs +1 -0
  31. package/dist/esm/common/enum.mjs.map +1 -1
  32. package/dist/esm/events.mjs +10 -0
  33. package/dist/esm/events.mjs.map +1 -1
  34. package/dist/esm/graphs/Graph.mjs +35 -6
  35. package/dist/esm/graphs/Graph.mjs.map +1 -1
  36. package/dist/esm/llm/anthropic/llm.mjs +1 -3
  37. package/dist/esm/llm/anthropic/llm.mjs.map +1 -1
  38. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  39. package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
  40. package/dist/esm/llm/fake.mjs +52 -0
  41. package/dist/esm/llm/fake.mjs.map +1 -0
  42. package/dist/esm/llm/providers.mjs +8 -6
  43. package/dist/esm/llm/providers.mjs.map +1 -1
  44. package/dist/esm/llm/text.mjs.map +1 -1
  45. package/dist/esm/messages.mjs.map +1 -1
  46. package/dist/esm/run.mjs +3 -7
  47. package/dist/esm/run.mjs.map +1 -1
  48. package/dist/esm/splitStream.mjs.map +1 -1
  49. package/dist/esm/stream.mjs +94 -56
  50. package/dist/esm/stream.mjs.map +1 -1
  51. package/dist/esm/tools/CodeExecutor.mjs +9 -3
  52. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  53. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  54. package/dist/esm/utils/graph.mjs.map +1 -1
  55. package/dist/esm/utils/llm.mjs.map +1 -1
  56. package/dist/esm/utils/misc.mjs.map +1 -1
  57. package/dist/esm/utils/run.mjs.map +1 -1
  58. package/dist/esm/utils/title.mjs.map +1 -1
  59. package/dist/types/common/enum.d.ts +2 -1
  60. package/dist/types/events.d.ts +4 -1
  61. package/dist/types/graphs/Graph.d.ts +9 -13
  62. package/dist/types/llm/fake.d.ts +21 -0
  63. package/dist/types/specs/spec.utils.d.ts +1 -0
  64. package/dist/types/stream.d.ts +9 -13
  65. package/dist/types/types/graph.d.ts +16 -0
  66. package/dist/types/types/llm.d.ts +10 -5
  67. package/dist/types/types/run.d.ts +4 -12
  68. package/dist/types/types/stream.d.ts +12 -0
  69. package/package.json +15 -26
  70. package/src/common/enum.ts +1 -0
  71. package/src/events.ts +13 -1
  72. package/src/graphs/Graph.ts +43 -21
  73. package/src/llm/fake.ts +83 -0
  74. package/src/llm/providers.ts +7 -5
  75. package/src/run.ts +3 -7
  76. package/src/scripts/simple.ts +28 -14
  77. package/src/specs/anthropic.simple.test.ts +204 -0
  78. package/src/specs/openai.simple.test.ts +204 -0
  79. package/src/specs/reasoning.test.ts +165 -0
  80. package/src/specs/spec.utils.ts +3 -0
  81. package/src/stream.ts +100 -72
  82. package/src/tools/CodeExecutor.ts +8 -2
  83. package/src/types/graph.ts +18 -1
  84. package/src/types/llm.ts +10 -5
  85. package/src/types/run.ts +5 -13
  86. package/src/types/stream.ts +14 -1
  87. package/src/utils/llmConfig.ts +7 -1
@@ -0,0 +1,204 @@
1
+ /* eslint-disable no-console */
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+ // src/scripts/cli.test.ts
4
+ import { config } from 'dotenv';
5
+ config();
6
+ import { Calculator } from '@langchain/community/tools/calculator';
7
+ import { HumanMessage, BaseMessage, UsageMetadata } from '@langchain/core/messages';
8
+ import type { StandardGraph } from '@/graphs';
9
+ import type * as t from '@/types';
10
+ import { ToolEndHandler, ModelEndHandler, createMetadataAggregator } from '@/events';
11
+ import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
12
+ import { ContentTypes, GraphEvents, Providers } from '@/common';
13
+ import { capitalizeFirstLetter } from './spec.utils';
14
+ import { getLLMConfig } from '@/utils/llmConfig';
15
+ import { getArgs } from '@/scripts/args';
16
+ import { Run } from '@/run';
17
+
18
+ const provider = Providers.ANTHROPIC;
19
+ describe(`${capitalizeFirstLetter(provider)} Streaming Tests`, () => {
20
+ jest.setTimeout(30000);
21
+ let run: Run<t.IState>;
22
+ let runningHistory: BaseMessage[];
23
+ let collectedUsage: UsageMetadata[];
24
+ let conversationHistory: BaseMessage[];
25
+ let aggregateContent: t.ContentAggregator;
26
+ let contentParts: t.MessageContentComplex[];
27
+
28
+ const config = {
29
+ configurable: {
30
+ thread_id: 'conversation-num-1',
31
+ },
32
+ streamMode: 'values',
33
+ version: 'v2' as const,
34
+ };
35
+
36
+ beforeEach(async () => {
37
+ conversationHistory = [];
38
+ collectedUsage = [];
39
+ const { contentParts: cp, aggregateContent: ac } = createContentAggregator();
40
+ contentParts = cp as t.MessageContentComplex[];
41
+ aggregateContent = ac;
42
+ });
43
+
44
+ const onMessageDeltaSpy = jest.fn();
45
+ const onRunStepSpy = jest.fn();
46
+
47
+ afterAll(() => {
48
+ onMessageDeltaSpy.mockReset();
49
+ onRunStepSpy.mockReset();
50
+ });
51
+
52
+ const setupCustomHandlers = (): Record<string | GraphEvents, t.EventHandler> => ({
53
+ [GraphEvents.TOOL_END]: new ToolEndHandler(),
54
+ [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(collectedUsage),
55
+ [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
56
+ [GraphEvents.ON_RUN_STEP_COMPLETED]: {
57
+ handle: (event: GraphEvents.ON_RUN_STEP_COMPLETED, data: t.StreamEventData): void => {
58
+ aggregateContent({ event, data: data as unknown as { result: t.ToolEndEvent; } });
59
+ }
60
+ },
61
+ [GraphEvents.ON_RUN_STEP]: {
62
+ handle: (event: GraphEvents.ON_RUN_STEP, data: t.StreamEventData, metadata, graph): void => {
63
+ onRunStepSpy(event, data, metadata, graph);
64
+ aggregateContent({ event, data: data as t.RunStep });
65
+ }
66
+ },
67
+ [GraphEvents.ON_RUN_STEP_DELTA]: {
68
+ handle: (event: GraphEvents.ON_RUN_STEP_DELTA, data: t.StreamEventData): void => {
69
+ aggregateContent({ event, data: data as t.RunStepDeltaEvent });
70
+ }
71
+ },
72
+ [GraphEvents.ON_MESSAGE_DELTA]: {
73
+ handle: (event: GraphEvents.ON_MESSAGE_DELTA, data: t.StreamEventData, metadata, graph): void => {
74
+ onMessageDeltaSpy(event, data, metadata, graph);
75
+ aggregateContent({ event, data: data as t.MessageDeltaEvent });
76
+ }
77
+ },
78
+ [GraphEvents.TOOL_START]: {
79
+ handle: (_event: string, _data: t.StreamEventData, _metadata?: Record<string, unknown>): void => {
80
+ // Handle tool start
81
+ }
82
+ },
83
+ });
84
+
85
+ test(`${capitalizeFirstLetter(provider)}: should process a simple message, generate title`, async () => {
86
+ const { userName, location } = await getArgs();
87
+ const llmConfig = getLLMConfig(provider);
88
+ const customHandlers = setupCustomHandlers();
89
+
90
+ run = await Run.create<t.IState>({
91
+ runId: 'test-run-id',
92
+ graphConfig: {
93
+ type: 'standard',
94
+ llmConfig,
95
+ tools: [new Calculator()],
96
+ instructions: 'You are a friendly AI assistant. Always address the user by their name.',
97
+ additional_instructions: `The user's name is ${userName} and they are located in ${location}.`,
98
+ },
99
+ returnContent: true,
100
+ customHandlers,
101
+ });
102
+
103
+ const userMessage = 'hi';
104
+ conversationHistory.push(new HumanMessage(userMessage));
105
+
106
+ const inputs = {
107
+ messages: conversationHistory,
108
+ };
109
+
110
+ const finalContentParts = await run.processStream(inputs, config);
111
+ expect(finalContentParts).toBeDefined();
112
+ const allTextParts = finalContentParts?.every((part) => part.type === ContentTypes.TEXT);
113
+ expect(allTextParts).toBe(true);
114
+ expect(collectedUsage.length).toBeGreaterThan(0);
115
+ expect(collectedUsage[0].input_tokens).toBeGreaterThan(0);
116
+ expect(collectedUsage[0].output_tokens).toBeGreaterThan(0);
117
+
118
+ const finalMessages = run.getRunMessages();
119
+ expect(finalMessages).toBeDefined();
120
+ conversationHistory.push(...finalMessages ?? []);
121
+ expect(conversationHistory.length).toBeGreaterThan(1);
122
+ runningHistory = conversationHistory.slice();
123
+
124
+ expect(onMessageDeltaSpy).toHaveBeenCalled();
125
+ expect(onMessageDeltaSpy.mock.calls.length).toBeGreaterThan(1);
126
+ expect((onMessageDeltaSpy.mock.calls[0][3] as StandardGraph).provider).toBeDefined();
127
+
128
+ expect(onRunStepSpy).toHaveBeenCalled();
129
+ expect(onRunStepSpy.mock.calls.length).toBeGreaterThan(0);
130
+ expect((onRunStepSpy.mock.calls[0][3] as StandardGraph).provider).toBeDefined();
131
+
132
+ const { handleLLMEnd, collected } = createMetadataAggregator();
133
+ const titleResult = await run.generateTitle({
134
+ inputText: userMessage,
135
+ contentParts,
136
+ chainOptions: {
137
+ callbacks: [{
138
+ handleLLMEnd,
139
+ }],
140
+ },
141
+ });
142
+
143
+ expect(titleResult).toBeDefined();
144
+ expect(titleResult.title).toBeDefined();
145
+ expect(titleResult.language).toBeDefined();
146
+ expect(collected).toBeDefined();
147
+ });
148
+
149
+ test(`${capitalizeFirstLetter(provider)}: should follow-up`, async () => {
150
+ console.log('Previous conversation length:', runningHistory.length);
151
+ console.log('Last message:', runningHistory[runningHistory.length - 1].content);
152
+ const { userName, location } = await getArgs();
153
+ const llmConfig = getLLMConfig(provider);
154
+ const customHandlers = setupCustomHandlers();
155
+
156
+ run = await Run.create<t.IState>({
157
+ runId: 'test-run-id',
158
+ graphConfig: {
159
+ type: 'standard',
160
+ llmConfig,
161
+ tools: [new Calculator()],
162
+ instructions: 'You are a friendly AI assistant. Always address the user by their name.',
163
+ additional_instructions: `The user's name is ${userName} and they are located in ${location}.`,
164
+ },
165
+ returnContent: true,
166
+ customHandlers,
167
+ });
168
+
169
+ conversationHistory = runningHistory.slice();
170
+ conversationHistory.push(new HumanMessage('how are you?'));
171
+
172
+ const inputs = {
173
+ messages: conversationHistory,
174
+ };
175
+
176
+ const finalContentParts = await run.processStream(inputs, config);
177
+ expect(finalContentParts).toBeDefined();
178
+ const allTextParts = finalContentParts?.every((part) => part.type === ContentTypes.TEXT);
179
+ expect(allTextParts).toBe(true);
180
+ expect(collectedUsage.length).toBeGreaterThan(0);
181
+ expect(collectedUsage[0].input_tokens).toBeGreaterThan(0);
182
+ expect(collectedUsage[0].output_tokens).toBeGreaterThan(0);
183
+
184
+ const finalMessages = run.getRunMessages();
185
+ expect(finalMessages).toBeDefined();
186
+ expect(finalMessages?.length).toBeGreaterThan(0);
187
+ console.log(`${capitalizeFirstLetter(provider)} follow-up message:`, finalMessages?.[finalMessages.length - 1]?.content);
188
+
189
+ expect(onMessageDeltaSpy).toHaveBeenCalled();
190
+ expect(onMessageDeltaSpy.mock.calls.length).toBeGreaterThan(1);
191
+
192
+ expect(onRunStepSpy).toHaveBeenCalled();
193
+ expect(onRunStepSpy.mock.calls.length).toBeGreaterThan(0);
194
+ });
195
+
196
+ test('should handle errors appropriately', async () => {
197
+ // Test error scenarios
198
+ await expect(async () => {
199
+ await run.processStream({
200
+ messages: [],
201
+ }, {} as any);
202
+ }).rejects.toThrow();
203
+ });
204
+ });
@@ -0,0 +1,204 @@
1
+ /* eslint-disable no-console */
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+ // src/scripts/cli.test.ts
4
+ import { config } from 'dotenv';
5
+ config();
6
+ import { Calculator } from '@langchain/community/tools/calculator';
7
+ import { HumanMessage, BaseMessage, UsageMetadata } from '@langchain/core/messages';
8
+ import type { StandardGraph } from '@/graphs';
9
+ import type * as t from '@/types';
10
+ import { ToolEndHandler, ModelEndHandler, createMetadataAggregator } from '@/events';
11
+ import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
12
+ import { ContentTypes, GraphEvents, Providers } from '@/common';
13
+ import { capitalizeFirstLetter } from './spec.utils';
14
+ import { getLLMConfig } from '@/utils/llmConfig';
15
+ import { getArgs } from '@/scripts/args';
16
+ import { Run } from '@/run';
17
+
18
+ const provider = Providers.OPENAI;
19
+ describe(`${capitalizeFirstLetter(provider)} Streaming Tests`, () => {
20
+ jest.setTimeout(30000);
21
+ let run: Run<t.IState>;
22
+ let runningHistory: BaseMessage[];
23
+ let collectedUsage: UsageMetadata[];
24
+ let conversationHistory: BaseMessage[];
25
+ let aggregateContent: t.ContentAggregator;
26
+ let contentParts: t.MessageContentComplex[];
27
+
28
+ const config = {
29
+ configurable: {
30
+ thread_id: 'conversation-num-1',
31
+ },
32
+ streamMode: 'values',
33
+ version: 'v2' as const,
34
+ };
35
+
36
+ beforeEach(async () => {
37
+ conversationHistory = [];
38
+ collectedUsage = [];
39
+ const { contentParts: cp, aggregateContent: ac } = createContentAggregator();
40
+ contentParts = cp as t.MessageContentComplex[];
41
+ aggregateContent = ac;
42
+ });
43
+
44
+ const onMessageDeltaSpy = jest.fn();
45
+ const onRunStepSpy = jest.fn();
46
+
47
+ afterAll(() => {
48
+ onMessageDeltaSpy.mockReset();
49
+ onRunStepSpy.mockReset();
50
+ });
51
+
52
+ const setupCustomHandlers = (): Record<string | GraphEvents, t.EventHandler> => ({
53
+ [GraphEvents.TOOL_END]: new ToolEndHandler(),
54
+ [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(collectedUsage),
55
+ [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
56
+ [GraphEvents.ON_RUN_STEP_COMPLETED]: {
57
+ handle: (event: GraphEvents.ON_RUN_STEP_COMPLETED, data: t.StreamEventData): void => {
58
+ aggregateContent({ event, data: data as unknown as { result: t.ToolEndEvent; } });
59
+ }
60
+ },
61
+ [GraphEvents.ON_RUN_STEP]: {
62
+ handle: (event: GraphEvents.ON_RUN_STEP, data: t.StreamEventData, metadata, graph): void => {
63
+ onRunStepSpy(event, data, metadata, graph);
64
+ aggregateContent({ event, data: data as t.RunStep });
65
+ }
66
+ },
67
+ [GraphEvents.ON_RUN_STEP_DELTA]: {
68
+ handle: (event: GraphEvents.ON_RUN_STEP_DELTA, data: t.StreamEventData): void => {
69
+ aggregateContent({ event, data: data as t.RunStepDeltaEvent });
70
+ }
71
+ },
72
+ [GraphEvents.ON_MESSAGE_DELTA]: {
73
+ handle: (event: GraphEvents.ON_MESSAGE_DELTA, data: t.StreamEventData, metadata, graph): void => {
74
+ onMessageDeltaSpy(event, data, metadata, graph);
75
+ aggregateContent({ event, data: data as t.MessageDeltaEvent });
76
+ }
77
+ },
78
+ [GraphEvents.TOOL_START]: {
79
+ handle: (_event: string, _data: t.StreamEventData, _metadata?: Record<string, unknown>): void => {
80
+ // Handle tool start
81
+ }
82
+ },
83
+ });
84
+
85
+ test(`${capitalizeFirstLetter(provider)}: should process a simple message, generate title`, async () => {
86
+ const { userName, location } = await getArgs();
87
+ const llmConfig = getLLMConfig(provider);
88
+ const customHandlers = setupCustomHandlers();
89
+
90
+ run = await Run.create<t.IState>({
91
+ runId: 'test-run-id',
92
+ graphConfig: {
93
+ type: 'standard',
94
+ llmConfig,
95
+ tools: [new Calculator()],
96
+ instructions: 'You are a friendly AI assistant. Always address the user by their name.',
97
+ additional_instructions: `The user's name is ${userName} and they are located in ${location}.`,
98
+ },
99
+ returnContent: true,
100
+ customHandlers,
101
+ });
102
+
103
+ const userMessage = 'hi';
104
+ conversationHistory.push(new HumanMessage(userMessage));
105
+
106
+ const inputs = {
107
+ messages: conversationHistory,
108
+ };
109
+
110
+ const finalContentParts = await run.processStream(inputs, config);
111
+ expect(finalContentParts).toBeDefined();
112
+ const allTextParts = finalContentParts?.every((part) => part.type === ContentTypes.TEXT);
113
+ expect(allTextParts).toBe(true);
114
+ expect(collectedUsage.length).toBeGreaterThan(0);
115
+ expect(collectedUsage[0].input_tokens).toBeGreaterThan(0);
116
+ expect(collectedUsage[0].output_tokens).toBeGreaterThan(0);
117
+
118
+ const finalMessages = run.getRunMessages();
119
+ expect(finalMessages).toBeDefined();
120
+ conversationHistory.push(...finalMessages ?? []);
121
+ expect(conversationHistory.length).toBeGreaterThan(1);
122
+ runningHistory = conversationHistory.slice();
123
+
124
+ expect(onMessageDeltaSpy).toHaveBeenCalled();
125
+ expect(onMessageDeltaSpy.mock.calls.length).toBeGreaterThan(1);
126
+ expect((onMessageDeltaSpy.mock.calls[0][3] as StandardGraph).provider).toBeDefined();
127
+
128
+ expect(onRunStepSpy).toHaveBeenCalled();
129
+ expect(onRunStepSpy.mock.calls.length).toBeGreaterThan(0);
130
+ expect((onRunStepSpy.mock.calls[0][3] as StandardGraph).provider).toBeDefined();
131
+
132
+ const { handleLLMEnd, collected } = createMetadataAggregator();
133
+ const titleResult = await run.generateTitle({
134
+ inputText: userMessage,
135
+ contentParts,
136
+ chainOptions: {
137
+ callbacks: [{
138
+ handleLLMEnd,
139
+ }],
140
+ },
141
+ });
142
+
143
+ expect(titleResult).toBeDefined();
144
+ expect(titleResult.title).toBeDefined();
145
+ expect(titleResult.language).toBeDefined();
146
+ expect(collected).toBeDefined();
147
+ });
148
+
149
+ test(`${capitalizeFirstLetter(provider)}: should follow-up`, async () => {
150
+ console.log('Previous conversation length:', runningHistory.length);
151
+ console.log('Last message:', runningHistory[runningHistory.length - 1].content);
152
+ const { userName, location } = await getArgs();
153
+ const llmConfig = getLLMConfig(provider);
154
+ const customHandlers = setupCustomHandlers();
155
+
156
+ run = await Run.create<t.IState>({
157
+ runId: 'test-run-id',
158
+ graphConfig: {
159
+ type: 'standard',
160
+ llmConfig,
161
+ tools: [new Calculator()],
162
+ instructions: 'You are a friendly AI assistant. Always address the user by their name.',
163
+ additional_instructions: `The user's name is ${userName} and they are located in ${location}.`,
164
+ },
165
+ returnContent: true,
166
+ customHandlers,
167
+ });
168
+
169
+ conversationHistory = runningHistory.slice();
170
+ conversationHistory.push(new HumanMessage('how are you?'));
171
+
172
+ const inputs = {
173
+ messages: conversationHistory,
174
+ };
175
+
176
+ const finalContentParts = await run.processStream(inputs, config);
177
+ expect(finalContentParts).toBeDefined();
178
+ const allTextParts = finalContentParts?.every((part) => part.type === ContentTypes.TEXT);
179
+ expect(allTextParts).toBe(true);
180
+ expect(collectedUsage.length).toBeGreaterThan(0);
181
+ expect(collectedUsage[0].input_tokens).toBeGreaterThan(0);
182
+ expect(collectedUsage[0].output_tokens).toBeGreaterThan(0);
183
+
184
+ const finalMessages = run.getRunMessages();
185
+ expect(finalMessages).toBeDefined();
186
+ expect(finalMessages?.length).toBeGreaterThan(0);
187
+ console.log(`${capitalizeFirstLetter(provider)} follow-up message:`, finalMessages?.[finalMessages.length - 1]?.content);
188
+
189
+ expect(onMessageDeltaSpy).toHaveBeenCalled();
190
+ expect(onMessageDeltaSpy.mock.calls.length).toBeGreaterThan(1);
191
+
192
+ expect(onRunStepSpy).toHaveBeenCalled();
193
+ expect(onRunStepSpy.mock.calls.length).toBeGreaterThan(0);
194
+ });
195
+
196
+ test('should handle errors appropriately', async () => {
197
+ // Test error scenarios
198
+ await expect(async () => {
199
+ await run.processStream({
200
+ messages: [],
201
+ }, {} as any);
202
+ }).rejects.toThrow();
203
+ });
204
+ });
@@ -0,0 +1,165 @@
1
+ /* eslint-disable no-console */
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+ // src/scripts/cli.test.ts
4
+ import { config } from 'dotenv';
5
+ config();
6
+ import { HumanMessage, BaseMessage, MessageContentText } from '@langchain/core/messages';
7
+ import type { RunnableConfig } from '@langchain/core/runnables';
8
+ import type { StandardGraph } from '@/graphs';
9
+ import type * as t from '@/types';
10
+ import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
11
+ import { capitalizeFirstLetter } from './spec.utils';
12
+ import { GraphEvents, Providers } from '@/common';
13
+ import { getLLMConfig } from '@/utils/llmConfig';
14
+ import { getArgs } from '@/scripts/args';
15
+ import { Run } from '@/run';
16
+
17
+ const reasoningText = `<think>
18
+ Okay, the user is Jo from New York. I should start by greeting them by name. Let's keep it friendly and open-ended. Maybe mention the weather in New York to make it personal. Then offer help with something specific like plans or questions. Need to keep it concise and welcoming. Check for any typos. Alright, that should work.
19
+ </think>
20
+ Hi Jo! 🌆 How's everything in New York today? Whether you need recommendations for the city, help with a task, or just want to chat, I'm here for it. What's on your mind? 😊`;
21
+
22
+ const provider = 'Reasoning LLM';
23
+ describe(`${capitalizeFirstLetter(provider)} Streaming Tests`, () => {
24
+ jest.setTimeout(30000);
25
+ let run: Run<t.IState>;
26
+ let contentParts: t.MessageContentComplex[];
27
+ let conversationHistory: BaseMessage[];
28
+ let aggregateContent: t.ContentAggregator;
29
+ let runSteps: Set<string>;
30
+
31
+ const config: Partial<RunnableConfig> & { version: 'v1' | 'v2'; run_id?: string; streamMode: string } = {
32
+ configurable: {
33
+ thread_id: 'conversation-num-1',
34
+ },
35
+ streamMode: 'values',
36
+ version: 'v2' as const,
37
+ callbacks: [{
38
+ async handleCustomEvent(event, data, metadata): Promise<void> {
39
+ if (event !== GraphEvents.ON_MESSAGE_DELTA) {
40
+ return;
41
+ }
42
+ const messageDeltaData = data as t.MessageDeltaEvent;
43
+
44
+ // Wait until we see the run step (with timeout for safety)
45
+ const maxAttempts = 50; // 5 seconds total
46
+ let attempts = 0;
47
+ while (!runSteps.has(messageDeltaData.id) && attempts < maxAttempts) {
48
+ await new Promise(resolve => setTimeout(resolve, 100));
49
+ attempts++;
50
+ }
51
+
52
+ if (!runSteps.has(messageDeltaData.id)) {
53
+ console.warn(`Timeout waiting for run step: ${messageDeltaData.id}`);
54
+ }
55
+
56
+ onMessageDeltaSpy(event, data, metadata, run.Graph);
57
+ aggregateContent({ event, data: messageDeltaData });
58
+ },
59
+ }],
60
+ };
61
+
62
+ beforeEach(async () => {
63
+ conversationHistory = [];
64
+ const { contentParts: parts, aggregateContent: ac } = createContentAggregator();
65
+ aggregateContent = ac;
66
+ runSteps = new Set();
67
+ contentParts = parts as t.MessageContentComplex[];
68
+ });
69
+
70
+ afterEach(() => {
71
+ runSteps.clear();
72
+ });
73
+
74
+ const onReasoningDeltaSpy = jest.fn();
75
+ const onMessageDeltaSpy = jest.fn();
76
+ const onRunStepSpy = jest.fn();
77
+
78
+ afterAll(() => {
79
+ onReasoningDeltaSpy.mockReset();
80
+ onMessageDeltaSpy.mockReset();
81
+ onRunStepSpy.mockReset();
82
+ });
83
+
84
+ const setupCustomHandlers = (): Record<string | GraphEvents, t.EventHandler> => ({
85
+ [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
86
+ [GraphEvents.ON_RUN_STEP_COMPLETED]: {
87
+ handle: (event: GraphEvents.ON_RUN_STEP_COMPLETED, data: t.StreamEventData): void => {
88
+ aggregateContent({ event, data: data as unknown as { result: t.ToolEndEvent; } });
89
+ }
90
+ },
91
+ [GraphEvents.ON_RUN_STEP]: {
92
+ handle: (event: GraphEvents.ON_RUN_STEP, data: t.StreamEventData, metadata, graph): void => {
93
+ const runStepData = data as t.RunStep;
94
+ runSteps.add(runStepData.id);
95
+
96
+ onRunStepSpy(event, runStepData, metadata, graph);
97
+ aggregateContent({ event, data: runStepData });
98
+ }
99
+ },
100
+ [GraphEvents.ON_RUN_STEP_DELTA]: {
101
+ handle: (event: GraphEvents.ON_RUN_STEP_DELTA, data: t.StreamEventData): void => {
102
+ aggregateContent({ event, data: data as t.RunStepDeltaEvent });
103
+ }
104
+ },
105
+ [GraphEvents.ON_REASONING_DELTA]: {
106
+ handle: (event: GraphEvents.ON_REASONING_DELTA, data: t.StreamEventData, metadata, graph): void => {
107
+ onReasoningDeltaSpy(event, data, metadata, graph);
108
+ aggregateContent({ event, data: data as t.ReasoningDeltaEvent });
109
+ }
110
+ },
111
+ });
112
+
113
+ test(`${capitalizeFirstLetter(provider)}: should process a simple reasoning message`, async () => {
114
+ const { userName, location } = await getArgs();
115
+ const llmConfig = getLLMConfig(Providers.OPENAI);
116
+ const customHandlers = setupCustomHandlers();
117
+
118
+ run = await Run.create<t.IState>({
119
+ runId: 'test-run-id',
120
+ graphConfig: {
121
+ type: 'standard',
122
+ llmConfig,
123
+ instructions: 'You are a friendly AI assistant. Always address the user by their name.',
124
+ additional_instructions: `The user's name is ${userName} and they are located in ${location}.`,
125
+ },
126
+ returnContent: true,
127
+ customHandlers,
128
+ });
129
+
130
+ run.Graph?.overrideTestModel([reasoningText], 2);
131
+
132
+ const userMessage = 'hi';
133
+ conversationHistory.push(new HumanMessage(userMessage));
134
+
135
+ const inputs = {
136
+ messages: conversationHistory,
137
+ };
138
+
139
+ await run.processStream(inputs, config);
140
+ expect(contentParts).toBeDefined();
141
+ expect(contentParts.length).toBe(2);
142
+ const reasoningContent = reasoningText.match(/<think>(.*)<\/think>/s)?.[0];
143
+ const content = reasoningText.split(/<\/think>/)[1];
144
+ expect((contentParts[0] as t.ReasoningContentText).think).toBe(reasoningContent);
145
+ expect((contentParts[1] as MessageContentText).text).toBe(content);
146
+
147
+ const finalMessages = run.getRunMessages();
148
+ expect(finalMessages).toBeDefined();
149
+ conversationHistory.push(...finalMessages ?? []);
150
+ expect(conversationHistory.length).toBeGreaterThan(1);
151
+
152
+ expect(onMessageDeltaSpy).toHaveBeenCalled();
153
+ expect(onMessageDeltaSpy.mock.calls.length).toBeGreaterThan(1);
154
+ expect((onMessageDeltaSpy.mock.calls[0][3] as StandardGraph).provider).toBeDefined();
155
+
156
+ expect(onReasoningDeltaSpy).toHaveBeenCalled();
157
+ expect(onReasoningDeltaSpy.mock.calls.length).toBeGreaterThan(1);
158
+ expect((onReasoningDeltaSpy.mock.calls[0][3] as StandardGraph).provider).toBeDefined();
159
+
160
+ expect(onRunStepSpy).toHaveBeenCalled();
161
+ expect(onRunStepSpy.mock.calls.length).toBeGreaterThan(0);
162
+ expect((onRunStepSpy.mock.calls[0][3] as StandardGraph).provider).toBeDefined();
163
+
164
+ });
165
+ });
@@ -0,0 +1,3 @@
1
+ export function capitalizeFirstLetter(string: string): string {
2
+ return string.charAt(0).toUpperCase() + string.slice(1);
3
+ }