@lobehub/chat 1.128.0 → 1.128.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.
- package/.github/workflows/test.yml +8 -1
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/next.config.ts +8 -1
- package/package.json +71 -69
- package/packages/context-engine/ARCHITECTURE.md +425 -0
- package/packages/context-engine/package.json +40 -0
- package/packages/context-engine/src/base/BaseProcessor.ts +87 -0
- package/packages/context-engine/src/base/BaseProvider.ts +22 -0
- package/packages/context-engine/src/index.ts +32 -0
- package/packages/context-engine/src/pipeline.ts +219 -0
- package/packages/context-engine/src/processors/HistoryTruncate.ts +76 -0
- package/packages/context-engine/src/processors/InputTemplate.ts +83 -0
- package/packages/context-engine/src/processors/MessageCleanup.ts +87 -0
- package/packages/context-engine/src/processors/MessageContent.ts +298 -0
- package/packages/context-engine/src/processors/PlaceholderVariables.ts +196 -0
- package/packages/context-engine/src/processors/ToolCall.ts +186 -0
- package/packages/context-engine/src/processors/ToolMessageReorder.ts +113 -0
- package/packages/context-engine/src/processors/__tests__/HistoryTruncate.test.ts +175 -0
- package/packages/context-engine/src/processors/__tests__/InputTemplate.test.ts +243 -0
- package/packages/context-engine/src/processors/__tests__/MessageContent.test.ts +394 -0
- package/packages/context-engine/src/processors/__tests__/PlaceholderVariables.test.ts +334 -0
- package/packages/context-engine/src/processors/__tests__/ToolMessageReorder.test.ts +186 -0
- package/packages/context-engine/src/processors/index.ts +15 -0
- package/packages/context-engine/src/providers/HistorySummary.ts +102 -0
- package/packages/context-engine/src/providers/InboxGuide.ts +102 -0
- package/packages/context-engine/src/providers/SystemRoleInjector.ts +64 -0
- package/packages/context-engine/src/providers/ToolSystemRole.ts +118 -0
- package/packages/context-engine/src/providers/__tests__/HistorySummaryProvider.test.ts +112 -0
- package/packages/context-engine/src/providers/__tests__/InboxGuideProvider.test.ts +121 -0
- package/packages/context-engine/src/providers/__tests__/SystemRoleInjector.test.ts +200 -0
- package/packages/context-engine/src/providers/__tests__/ToolSystemRoleProvider.test.ts +140 -0
- package/packages/context-engine/src/providers/index.ts +11 -0
- package/packages/context-engine/src/types.ts +201 -0
- package/packages/context-engine/vitest.config.mts +10 -0
- package/packages/database/package.json +1 -1
- package/packages/prompts/src/prompts/systemRole/index.ts +1 -1
- package/packages/utils/src/index.ts +2 -0
- package/packages/utils/src/uriParser.test.ts +29 -0
- package/packages/utils/src/uriParser.ts +24 -0
- package/src/services/{__tests__ → chat}/chat.test.ts +22 -1032
- package/src/services/chat/clientModelRuntime.test.ts +385 -0
- package/src/services/chat/clientModelRuntime.ts +34 -0
- package/src/services/chat/contextEngineering.test.ts +848 -0
- package/src/services/chat/contextEngineering.ts +123 -0
- package/src/services/chat/helper.ts +61 -0
- package/src/services/{chat.ts → chat/index.ts} +24 -366
- package/src/services/chat/types.ts +9 -0
- package/src/services/models.ts +1 -1
- package/src/store/aiInfra/slices/aiModel/selectors.ts +2 -2
- package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +1 -40
- /package/src/services/{__tests__ → chat}/__snapshots__/chat.test.ts.snap +0 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import debug from 'debug';
|
|
2
|
+
|
|
3
|
+
import { BaseProcessor } from '../base/BaseProcessor';
|
|
4
|
+
import type { PipelineContext, ProcessorOptions } from '../types';
|
|
5
|
+
|
|
6
|
+
const log = debug('context-engine:processor:ToolMessageReorder');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Reorder tool messages to ensure that tool messages are displayed in the correct order.
|
|
10
|
+
* see https://github.com/lobehub/lobe-chat/pull/3155
|
|
11
|
+
*/
|
|
12
|
+
export class ToolMessageReorder extends BaseProcessor {
|
|
13
|
+
readonly name = 'ToolMessageReorder';
|
|
14
|
+
|
|
15
|
+
constructor(options: ProcessorOptions = {}) {
|
|
16
|
+
super(options);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
|
|
20
|
+
const clonedContext = this.cloneContext(context);
|
|
21
|
+
|
|
22
|
+
// 重新排序消息
|
|
23
|
+
const reorderedMessages = this.reorderToolMessages(clonedContext.messages);
|
|
24
|
+
|
|
25
|
+
const originalCount = clonedContext.messages.length;
|
|
26
|
+
const reorderedCount = reorderedMessages.length;
|
|
27
|
+
|
|
28
|
+
clonedContext.messages = reorderedMessages;
|
|
29
|
+
|
|
30
|
+
// 更新元数据
|
|
31
|
+
clonedContext.metadata.toolMessageReorder = {
|
|
32
|
+
originalCount,
|
|
33
|
+
removedInvalidTools: originalCount - reorderedCount,
|
|
34
|
+
reorderedCount,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
if (originalCount !== reorderedCount) {
|
|
38
|
+
log(
|
|
39
|
+
'Tool message reordering completed, removed',
|
|
40
|
+
originalCount - reorderedCount,
|
|
41
|
+
'invalid tool messages',
|
|
42
|
+
);
|
|
43
|
+
} else {
|
|
44
|
+
log('Tool message reordering completed, message order optimized');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return this.markAsExecuted(clonedContext);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 重新排序工具消息
|
|
52
|
+
*/
|
|
53
|
+
private reorderToolMessages(messages: any[]): any[] {
|
|
54
|
+
// 1. 先收集所有 assistant 消息中的有效 tool_call_id
|
|
55
|
+
const validToolCallIds = new Set<string>();
|
|
56
|
+
messages.forEach((message) => {
|
|
57
|
+
if (message.role === 'assistant' && message.tool_calls) {
|
|
58
|
+
message.tool_calls.forEach((toolCall: any) => {
|
|
59
|
+
validToolCallIds.add(toolCall.id);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// 2. 收集所有有效的 tool 消息
|
|
65
|
+
const toolMessages: Record<string, any> = {};
|
|
66
|
+
messages.forEach((message) => {
|
|
67
|
+
if (
|
|
68
|
+
message.role === 'tool' &&
|
|
69
|
+
message.tool_call_id &&
|
|
70
|
+
validToolCallIds.has(message.tool_call_id)
|
|
71
|
+
) {
|
|
72
|
+
toolMessages[message.tool_call_id] = message;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// 3. 重新排序消息
|
|
77
|
+
const reorderedMessages: any[] = [];
|
|
78
|
+
messages.forEach((message) => {
|
|
79
|
+
// 跳过无效的 tool 消息
|
|
80
|
+
if (
|
|
81
|
+
message.role === 'tool' &&
|
|
82
|
+
(!message.tool_call_id || !validToolCallIds.has(message.tool_call_id))
|
|
83
|
+
) {
|
|
84
|
+
log('Skipping invalid tool message:', message.id);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 检查是否已经添加过该 tool 消息
|
|
89
|
+
const hasPushed = reorderedMessages.some(
|
|
90
|
+
(m) => !!message.tool_call_id && m.tool_call_id === message.tool_call_id,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
if (hasPushed) return;
|
|
94
|
+
|
|
95
|
+
reorderedMessages.push(message);
|
|
96
|
+
|
|
97
|
+
// 如果是 assistant 消息且有 tool_calls,添加对应的 tool 消息
|
|
98
|
+
if (message.role === 'assistant' && message.tool_calls) {
|
|
99
|
+
message.tool_calls.forEach((toolCall: any) => {
|
|
100
|
+
const correspondingToolMessage = toolMessages[toolCall.id];
|
|
101
|
+
if (correspondingToolMessage) {
|
|
102
|
+
reorderedMessages.push(correspondingToolMessage);
|
|
103
|
+
delete toolMessages[toolCall.id];
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return reorderedMessages;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 简化:移除验证/统计等辅助方法
|
|
113
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { HistoryTruncateProcessor, getSlicedMessages } from '../HistoryTruncate';
|
|
4
|
+
|
|
5
|
+
describe('HistoryTruncateProcessor', () => {
|
|
6
|
+
describe('getSlicedMessages', () => {
|
|
7
|
+
const messages = [
|
|
8
|
+
{ id: '1', content: 'First', role: 'user' },
|
|
9
|
+
{ id: '2', content: 'Second', role: 'assistant' },
|
|
10
|
+
{ id: '3', content: 'Third', role: 'user' },
|
|
11
|
+
{ id: '4', content: 'Fourth', role: 'assistant' },
|
|
12
|
+
{ id: '5', content: 'Fifth', role: 'user' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
it('should return all messages when history count is disabled', () => {
|
|
16
|
+
const result = getSlicedMessages(messages, { enableHistoryCount: false });
|
|
17
|
+
expect(result).toEqual(messages);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should return all messages when historyCount is undefined', () => {
|
|
21
|
+
const result = getSlicedMessages(messages, {
|
|
22
|
+
enableHistoryCount: true,
|
|
23
|
+
historyCount: undefined,
|
|
24
|
+
});
|
|
25
|
+
expect(result).toEqual(messages);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should return last N messages based on historyCount', () => {
|
|
29
|
+
const result = getSlicedMessages(messages, {
|
|
30
|
+
enableHistoryCount: true,
|
|
31
|
+
historyCount: 2,
|
|
32
|
+
});
|
|
33
|
+
expect(result).toEqual([
|
|
34
|
+
{ id: '4', content: 'Fourth', role: 'assistant' },
|
|
35
|
+
{ id: '5', content: 'Fifth', role: 'user' },
|
|
36
|
+
]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should include new user message in count when includeNewUserMessage is true', () => {
|
|
40
|
+
const result = getSlicedMessages(messages, {
|
|
41
|
+
enableHistoryCount: true,
|
|
42
|
+
historyCount: 3,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(result).toEqual([
|
|
46
|
+
{ id: '3', content: 'Third', role: 'user' },
|
|
47
|
+
{ id: '4', content: 'Fourth', role: 'assistant' },
|
|
48
|
+
{ id: '5', content: 'Fifth', role: 'user' },
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should return empty array when historyCount is 0', () => {
|
|
53
|
+
const result = getSlicedMessages(messages, {
|
|
54
|
+
enableHistoryCount: true,
|
|
55
|
+
historyCount: 0,
|
|
56
|
+
});
|
|
57
|
+
expect(result).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should return empty array when historyCount is negative', () => {
|
|
61
|
+
const result = getSlicedMessages(messages, {
|
|
62
|
+
enableHistoryCount: true,
|
|
63
|
+
historyCount: -1,
|
|
64
|
+
});
|
|
65
|
+
expect(result).toEqual([]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should return all messages when historyCount exceeds array length', () => {
|
|
69
|
+
const result = getSlicedMessages(messages, {
|
|
70
|
+
enableHistoryCount: true,
|
|
71
|
+
historyCount: 10,
|
|
72
|
+
});
|
|
73
|
+
expect(result).toEqual(messages);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should handle empty message array', () => {
|
|
77
|
+
const result = getSlicedMessages([], {
|
|
78
|
+
enableHistoryCount: true,
|
|
79
|
+
historyCount: 2,
|
|
80
|
+
});
|
|
81
|
+
expect(result).toEqual([]);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('HistoryTruncateProcessor', () => {
|
|
86
|
+
it('should truncate messages based on configuration', async () => {
|
|
87
|
+
const processor = new HistoryTruncateProcessor({
|
|
88
|
+
enableHistoryCount: true,
|
|
89
|
+
historyCount: 3,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const context = {
|
|
93
|
+
initialState: {
|
|
94
|
+
messages: [],
|
|
95
|
+
model: 'gpt-4',
|
|
96
|
+
provider: 'openai',
|
|
97
|
+
systemRole: '',
|
|
98
|
+
tools: [],
|
|
99
|
+
},
|
|
100
|
+
messages: [
|
|
101
|
+
{ id: '1', content: 'First', role: 'user', createdAt: Date.now(), updatedAt: Date.now() },
|
|
102
|
+
{
|
|
103
|
+
id: '2',
|
|
104
|
+
content: 'Second',
|
|
105
|
+
role: 'assistant',
|
|
106
|
+
createdAt: Date.now(),
|
|
107
|
+
updatedAt: Date.now(),
|
|
108
|
+
},
|
|
109
|
+
{ id: '3', content: 'Third', role: 'user', createdAt: Date.now(), updatedAt: Date.now() },
|
|
110
|
+
{
|
|
111
|
+
id: '4',
|
|
112
|
+
content: 'Fourth',
|
|
113
|
+
role: 'assistant',
|
|
114
|
+
createdAt: Date.now(),
|
|
115
|
+
updatedAt: Date.now(),
|
|
116
|
+
},
|
|
117
|
+
{ id: '5', content: 'Fifth', role: 'user', createdAt: Date.now(), updatedAt: Date.now() },
|
|
118
|
+
],
|
|
119
|
+
metadata: {
|
|
120
|
+
model: 'gpt-4',
|
|
121
|
+
maxTokens: 4096,
|
|
122
|
+
},
|
|
123
|
+
isAborted: false,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const result = await processor.process(context);
|
|
127
|
+
|
|
128
|
+
expect(result.messages).toHaveLength(3); // 2 + 1 for new user message
|
|
129
|
+
expect(result.messages).toEqual([
|
|
130
|
+
expect.objectContaining({ content: 'Third' }),
|
|
131
|
+
expect.objectContaining({ content: 'Fourth' }),
|
|
132
|
+
expect.objectContaining({ content: 'Fifth' }),
|
|
133
|
+
]);
|
|
134
|
+
expect(result.metadata.historyTruncated).toBe(2);
|
|
135
|
+
expect(result.metadata.finalMessageCount).toBe(3);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should not truncate when history count is disabled', async () => {
|
|
139
|
+
const processor = new HistoryTruncateProcessor({
|
|
140
|
+
enableHistoryCount: false,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const context = {
|
|
144
|
+
initialState: {
|
|
145
|
+
messages: [],
|
|
146
|
+
model: 'gpt-4',
|
|
147
|
+
provider: 'openai',
|
|
148
|
+
systemRole: '',
|
|
149
|
+
tools: [],
|
|
150
|
+
},
|
|
151
|
+
messages: [
|
|
152
|
+
{ id: '1', content: 'First', role: 'user', createdAt: Date.now(), updatedAt: Date.now() },
|
|
153
|
+
{
|
|
154
|
+
id: '2',
|
|
155
|
+
content: 'Second',
|
|
156
|
+
role: 'assistant',
|
|
157
|
+
createdAt: Date.now(),
|
|
158
|
+
updatedAt: Date.now(),
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
metadata: {
|
|
162
|
+
model: 'gpt-4',
|
|
163
|
+
maxTokens: 4096,
|
|
164
|
+
},
|
|
165
|
+
isAborted: false,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const result = await processor.process(context);
|
|
169
|
+
|
|
170
|
+
expect(result.messages).toHaveLength(2);
|
|
171
|
+
expect(result.metadata.historyTruncated).toBe(0);
|
|
172
|
+
expect(result.metadata.finalMessageCount).toBe(2);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { InputTemplateProcessor } from '../InputTemplate';
|
|
4
|
+
|
|
5
|
+
describe('InputTemplateProcessor', () => {
|
|
6
|
+
it('should apply template to user messages', async () => {
|
|
7
|
+
const processor = new InputTemplateProcessor({
|
|
8
|
+
inputTemplate: 'Template: {{text}} - End',
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const context = {
|
|
12
|
+
initialState: {
|
|
13
|
+
messages: [],
|
|
14
|
+
model: 'gpt-4',
|
|
15
|
+
provider: 'openai',
|
|
16
|
+
systemRole: '',
|
|
17
|
+
tools: [],
|
|
18
|
+
},
|
|
19
|
+
messages: [
|
|
20
|
+
{
|
|
21
|
+
id: '1',
|
|
22
|
+
role: 'user',
|
|
23
|
+
content: 'Original user message',
|
|
24
|
+
createdAt: Date.now(),
|
|
25
|
+
updatedAt: Date.now(),
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: '2',
|
|
29
|
+
role: 'assistant',
|
|
30
|
+
content: 'Assistant response',
|
|
31
|
+
createdAt: Date.now(),
|
|
32
|
+
updatedAt: Date.now(),
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
metadata: {
|
|
36
|
+
model: 'gpt-4',
|
|
37
|
+
maxTokens: 4096,
|
|
38
|
+
},
|
|
39
|
+
isAborted: false,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const result = await processor.process(context);
|
|
43
|
+
|
|
44
|
+
expect(result.messages[0].content).toBe('Template: Original user message - End');
|
|
45
|
+
expect(result.messages[1].content).toBe('Assistant response'); // Assistant message unchanged
|
|
46
|
+
expect(result.metadata.inputTemplateProcessed).toBe(1);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should skip processing when no template is configured', async () => {
|
|
50
|
+
const processor = new InputTemplateProcessor({});
|
|
51
|
+
|
|
52
|
+
const context = {
|
|
53
|
+
initialState: {
|
|
54
|
+
messages: [],
|
|
55
|
+
model: 'gpt-4',
|
|
56
|
+
provider: 'openai',
|
|
57
|
+
systemRole: '',
|
|
58
|
+
tools: [],
|
|
59
|
+
},
|
|
60
|
+
messages: [
|
|
61
|
+
{
|
|
62
|
+
id: '1',
|
|
63
|
+
role: 'user',
|
|
64
|
+
content: 'User message',
|
|
65
|
+
createdAt: Date.now(),
|
|
66
|
+
updatedAt: Date.now(),
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
metadata: {
|
|
70
|
+
model: 'gpt-4',
|
|
71
|
+
maxTokens: 4096,
|
|
72
|
+
},
|
|
73
|
+
isAborted: false,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const result = await processor.process(context);
|
|
77
|
+
|
|
78
|
+
expect(result.messages[0].content).toBe('User message'); // Unchanged
|
|
79
|
+
expect(result.metadata.inputTemplateProcessed).toBeUndefined();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should handle template without {{text}} placeholder', async () => {
|
|
83
|
+
const processor = new InputTemplateProcessor({
|
|
84
|
+
inputTemplate: 'Static template content',
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const context = {
|
|
88
|
+
initialState: {
|
|
89
|
+
messages: [],
|
|
90
|
+
model: 'gpt-4',
|
|
91
|
+
provider: 'openai',
|
|
92
|
+
systemRole: '',
|
|
93
|
+
tools: [],
|
|
94
|
+
},
|
|
95
|
+
messages: [
|
|
96
|
+
{
|
|
97
|
+
id: '1',
|
|
98
|
+
role: 'user',
|
|
99
|
+
content: 'Original message',
|
|
100
|
+
createdAt: Date.now(),
|
|
101
|
+
updatedAt: Date.now(),
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
metadata: {
|
|
105
|
+
model: 'gpt-4',
|
|
106
|
+
maxTokens: 4096,
|
|
107
|
+
},
|
|
108
|
+
isAborted: false,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const result = await processor.process(context);
|
|
112
|
+
|
|
113
|
+
expect(result.messages[0].content).toBe('Static template content');
|
|
114
|
+
expect(result.metadata.inputTemplateProcessed).toBe(1);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should handle template compilation errors gracefully', async () => {
|
|
118
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
119
|
+
|
|
120
|
+
const processor = new InputTemplateProcessor({
|
|
121
|
+
inputTemplate: '<%- invalid javascript code %>',
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const context = {
|
|
125
|
+
initialState: {
|
|
126
|
+
messages: [],
|
|
127
|
+
model: 'gpt-4',
|
|
128
|
+
provider: 'openai',
|
|
129
|
+
systemRole: '',
|
|
130
|
+
tools: [],
|
|
131
|
+
},
|
|
132
|
+
messages: [
|
|
133
|
+
{
|
|
134
|
+
id: '1',
|
|
135
|
+
role: 'user',
|
|
136
|
+
content: 'User message',
|
|
137
|
+
createdAt: Date.now(),
|
|
138
|
+
updatedAt: Date.now(),
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
metadata: {
|
|
142
|
+
model: 'gpt-4',
|
|
143
|
+
maxTokens: 4096,
|
|
144
|
+
},
|
|
145
|
+
isAborted: false,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const result = await processor.process(context);
|
|
149
|
+
|
|
150
|
+
// Should skip processing due to compilation error
|
|
151
|
+
expect(result.messages[0].content).toBe('User message'); // Original content preserved
|
|
152
|
+
expect(result.metadata.inputTemplateProcessed).toBe(0);
|
|
153
|
+
|
|
154
|
+
consoleSpy.mockRestore();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should handle template application errors gracefully', async () => {
|
|
158
|
+
const processor = new InputTemplateProcessor({
|
|
159
|
+
inputTemplate: '{{text}} <%- throw new Error("Application error") %>',
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const context = {
|
|
163
|
+
initialState: {
|
|
164
|
+
messages: [],
|
|
165
|
+
model: 'gpt-4',
|
|
166
|
+
provider: 'openai',
|
|
167
|
+
systemRole: '',
|
|
168
|
+
tools: [],
|
|
169
|
+
},
|
|
170
|
+
messages: [
|
|
171
|
+
{
|
|
172
|
+
id: '1',
|
|
173
|
+
role: 'user',
|
|
174
|
+
content: 'User message',
|
|
175
|
+
createdAt: Date.now(),
|
|
176
|
+
updatedAt: Date.now(),
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
metadata: {
|
|
180
|
+
model: 'gpt-4',
|
|
181
|
+
maxTokens: 4096,
|
|
182
|
+
},
|
|
183
|
+
isAborted: false,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const result = await processor.process(context);
|
|
187
|
+
|
|
188
|
+
// Should keep original message when template application fails
|
|
189
|
+
expect(result.messages[0].content).toBe('User message');
|
|
190
|
+
expect(result.metadata.inputTemplateProcessed).toBe(0);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should only process user messages, not assistant messages', async () => {
|
|
194
|
+
const processor = new InputTemplateProcessor({
|
|
195
|
+
inputTemplate: 'Processed: {{text}}',
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const context = {
|
|
199
|
+
initialState: {
|
|
200
|
+
messages: [],
|
|
201
|
+
model: 'gpt-4',
|
|
202
|
+
provider: 'openai',
|
|
203
|
+
systemRole: '',
|
|
204
|
+
tools: [],
|
|
205
|
+
},
|
|
206
|
+
messages: [
|
|
207
|
+
{
|
|
208
|
+
id: '1',
|
|
209
|
+
role: 'user',
|
|
210
|
+
content: 'User message',
|
|
211
|
+
createdAt: Date.now(),
|
|
212
|
+
updatedAt: Date.now(),
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
id: '2',
|
|
216
|
+
role: 'assistant',
|
|
217
|
+
content: 'Assistant message',
|
|
218
|
+
createdAt: Date.now(),
|
|
219
|
+
updatedAt: Date.now(),
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: '3',
|
|
223
|
+
role: 'system',
|
|
224
|
+
content: 'System message',
|
|
225
|
+
createdAt: Date.now(),
|
|
226
|
+
updatedAt: Date.now(),
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
metadata: {
|
|
230
|
+
model: 'gpt-4',
|
|
231
|
+
maxTokens: 4096,
|
|
232
|
+
},
|
|
233
|
+
isAborted: false,
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const result = await processor.process(context);
|
|
237
|
+
|
|
238
|
+
expect(result.messages[0].content).toBe('Processed: User message');
|
|
239
|
+
expect(result.messages[1].content).toBe('Assistant message'); // Unchanged
|
|
240
|
+
expect(result.messages[2].content).toBe('System message'); // Unchanged
|
|
241
|
+
expect(result.metadata.inputTemplateProcessed).toBe(1);
|
|
242
|
+
});
|
|
243
|
+
});
|