@lobehub/lobehub 2.0.0-next.361 → 2.0.0-next.362
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/CHANGELOG.md +25 -0
- package/Dockerfile +2 -1
- package/changelog/v1.json +9 -0
- package/locales/en-US/chat.json +3 -1
- package/locales/zh-CN/chat.json +2 -0
- package/package.json +1 -1
- package/packages/context-engine/src/base/BaseEveryUserContentProvider.ts +204 -0
- package/packages/context-engine/src/base/BaseLastUserContentProvider.ts +1 -8
- package/packages/context-engine/src/base/__tests__/BaseEveryUserContentProvider.test.ts +354 -0
- package/packages/context-engine/src/base/constants.ts +20 -0
- package/packages/context-engine/src/engine/messages/MessagesEngine.ts +27 -23
- package/packages/context-engine/src/engine/messages/__tests__/MessagesEngine.test.ts +364 -0
- package/packages/context-engine/src/providers/PageEditorContextInjector.ts +17 -13
- package/packages/context-engine/src/providers/PageSelectionsInjector.ts +65 -0
- package/packages/context-engine/src/providers/__tests__/PageSelectionsInjector.test.ts +333 -0
- package/packages/context-engine/src/providers/index.ts +3 -1
- package/packages/prompts/src/agents/index.ts +1 -0
- package/packages/prompts/src/agents/pageSelectionContext.ts +28 -0
- package/packages/types/src/aiChat.ts +4 -0
- package/packages/types/src/message/common/index.ts +1 -0
- package/packages/types/src/message/common/metadata.ts +8 -0
- package/packages/types/src/message/common/pageSelection.ts +36 -0
- package/packages/types/src/message/ui/params.ts +16 -0
- package/scripts/prebuild.mts +1 -0
- package/src/features/ChatInput/Desktop/ContextContainer/ContextList.tsx +1 -1
- package/src/features/Conversation/ChatInput/index.tsx +9 -1
- package/src/features/Conversation/Messages/User/components/MessageContent.tsx +7 -1
- package/src/features/Conversation/Messages/User/components/PageSelections.tsx +62 -0
- package/src/features/PageEditor/EditorCanvas/useAskCopilotItem.tsx +5 -1
- package/src/locales/default/chat.ts +3 -2
- package/src/server/routers/lambda/aiChat.ts +7 -0
- package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +5 -19
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## [Version 2.0.0-next.362](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.361...v2.0.0-next.362)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2026-01-24**</sup>
|
|
8
|
+
|
|
9
|
+
#### 🐛 Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **misc**: Fix page selection not display correctly.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's fixed
|
|
19
|
+
|
|
20
|
+
- **misc**: Fix page selection not display correctly, closes [#11765](https://github.com/lobehub/lobe-chat/issues/11765) ([7ae5f68](https://github.com/lobehub/lobe-chat/commit/7ae5f68))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
5
30
|
## [Version 2.0.0-next.361](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.360...v2.0.0-next.361)
|
|
6
31
|
|
|
7
32
|
<sup>Released on **2026-01-24**</sup>
|
package/Dockerfile
CHANGED
|
@@ -47,7 +47,8 @@ ENV NEXT_PUBLIC_BASE_PATH="${NEXT_PUBLIC_BASE_PATH}" \
|
|
|
47
47
|
ENV APP_URL="http://app.com" \
|
|
48
48
|
DATABASE_DRIVER="node" \
|
|
49
49
|
DATABASE_URL="postgres://postgres:password@localhost:5432/postgres" \
|
|
50
|
-
KEY_VAULTS_SECRET="use-for-build"
|
|
50
|
+
KEY_VAULTS_SECRET="use-for-build" \
|
|
51
|
+
AUTH_SECRET="use-for-build"
|
|
51
52
|
|
|
52
53
|
# Sentry
|
|
53
54
|
ENV NEXT_PUBLIC_SENTRY_DSN="${NEXT_PUBLIC_SENTRY_DSN}" \
|
package/changelog/v1.json
CHANGED
package/locales/en-US/chat.json
CHANGED
|
@@ -208,7 +208,9 @@
|
|
|
208
208
|
"operation.sendMessage": "Sending message",
|
|
209
209
|
"owner": "Group owner",
|
|
210
210
|
"pageCopilot.title": "Page Agent",
|
|
211
|
-
"pageCopilot.welcome": "**Clearer, sharper writing**\n\nDraft, rewrite, or polish—tell me your intent and I
|
|
211
|
+
"pageCopilot.welcome": "**Clearer, sharper writing**\n\nDraft, rewrite, or polish—tell me your intent and I'll refine the rest.",
|
|
212
|
+
"pageSelection.lines": "Lines {{start}}-{{end}}",
|
|
213
|
+
"pageSelection.reference": "Selected Text",
|
|
212
214
|
"pin": "Pin",
|
|
213
215
|
"pinOff": "Unpin",
|
|
214
216
|
"prompts.summaryExpert": "As a summary expert, please summarize the following content based on the system prompts above:",
|
package/locales/zh-CN/chat.json
CHANGED
|
@@ -209,6 +209,8 @@
|
|
|
209
209
|
"owner": "群主",
|
|
210
210
|
"pageCopilot.title": "文稿助理",
|
|
211
211
|
"pageCopilot.welcome": "**让文字更清晰、更到位**\n\n起草、改写、润色都可以。你把意图说清楚,其余交给我打磨",
|
|
212
|
+
"pageSelection.lines": "第 {{start}}-{{end}} 行",
|
|
213
|
+
"pageSelection.reference": "选中文本",
|
|
212
214
|
"pin": "置顶",
|
|
213
215
|
"pinOff": "取消置顶",
|
|
214
216
|
"prompts.summaryExpert": "作为一名总结专家,请结合以上系统提示词,将以下内容进行总结:",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/lobehub",
|
|
3
|
-
"version": "2.0.0-next.
|
|
3
|
+
"version": "2.0.0-next.362",
|
|
4
4
|
"description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import type { Message, PipelineContext, ProcessorOptions } from '../types';
|
|
2
|
+
import { BaseProcessor } from './BaseProcessor';
|
|
3
|
+
import { CONTEXT_INSTRUCTION, SYSTEM_CONTEXT_END, SYSTEM_CONTEXT_START } from './constants';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Base Provider for appending content to every user message
|
|
7
|
+
* Used for injecting context that should be attached to each user message individually
|
|
8
|
+
* (e.g., page selections that are specific to each message)
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Iterates through all user messages
|
|
12
|
+
* - For each message, calls buildContentForMessage to get content to inject
|
|
13
|
+
* - Wraps content with SYSTEM CONTEXT markers (or reuses existing wrapper)
|
|
14
|
+
* - Runs BEFORE BaseLastUserContentProvider so that the last user message
|
|
15
|
+
* can reuse the SYSTEM CONTEXT wrapper created here
|
|
16
|
+
*/
|
|
17
|
+
export abstract class BaseEveryUserContentProvider extends BaseProcessor {
|
|
18
|
+
constructor(options: ProcessorOptions = {}) {
|
|
19
|
+
super(options);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build the content to inject for a specific user message
|
|
24
|
+
* Subclasses must implement this method
|
|
25
|
+
* @param message - The user message to build content for
|
|
26
|
+
* @param index - The index of the message in the messages array
|
|
27
|
+
* @param isLastUser - Whether this is the last user message
|
|
28
|
+
* @returns Object with content and contextType, or null to skip injection for this message
|
|
29
|
+
*/
|
|
30
|
+
protected abstract buildContentForMessage(
|
|
31
|
+
message: Message,
|
|
32
|
+
index: number,
|
|
33
|
+
isLastUser: boolean,
|
|
34
|
+
): { content: string; contextType: string } | null;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get the text content from a message (handles both string and array content)
|
|
38
|
+
*/
|
|
39
|
+
private getTextContent(content: string | any[]): string {
|
|
40
|
+
if (typeof content === 'string') {
|
|
41
|
+
return content;
|
|
42
|
+
}
|
|
43
|
+
if (Array.isArray(content)) {
|
|
44
|
+
const lastTextPart = content.findLast((part: any) => part.type === 'text');
|
|
45
|
+
return lastTextPart?.text || '';
|
|
46
|
+
}
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if the content already has a system context wrapper
|
|
52
|
+
*/
|
|
53
|
+
protected hasSystemContextWrapper(content: string | any[]): boolean {
|
|
54
|
+
const textContent = this.getTextContent(content);
|
|
55
|
+
return textContent.includes(SYSTEM_CONTEXT_START) && textContent.includes(SYSTEM_CONTEXT_END);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Wrap content with system context markers
|
|
60
|
+
*/
|
|
61
|
+
protected wrapWithSystemContext(content: string, contextType: string): string {
|
|
62
|
+
return `${SYSTEM_CONTEXT_START}
|
|
63
|
+
${CONTEXT_INSTRUCTION}
|
|
64
|
+
<${contextType}>
|
|
65
|
+
${content}
|
|
66
|
+
</${contextType}>
|
|
67
|
+
${SYSTEM_CONTEXT_END}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Insert content into existing system context wrapper (before the END marker)
|
|
72
|
+
*/
|
|
73
|
+
private insertIntoExistingWrapper(existingContent: string, newContextBlock: string): string {
|
|
74
|
+
const endMarkerIndex = existingContent.lastIndexOf(SYSTEM_CONTEXT_END);
|
|
75
|
+
if (endMarkerIndex === -1) {
|
|
76
|
+
return existingContent + '\n\n' + newContextBlock;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const beforeEnd = existingContent.slice(0, endMarkerIndex);
|
|
80
|
+
const afterEnd = existingContent.slice(endMarkerIndex);
|
|
81
|
+
|
|
82
|
+
return beforeEnd + newContextBlock + '\n' + afterEnd;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create a context block without the full wrapper (for inserting into existing wrapper)
|
|
87
|
+
*/
|
|
88
|
+
protected createContextBlock(content: string, contextType: string): string {
|
|
89
|
+
return `<${contextType}>
|
|
90
|
+
${content}
|
|
91
|
+
</${contextType}>`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Append content to a message with SYSTEM CONTEXT wrapper
|
|
96
|
+
*/
|
|
97
|
+
protected appendToMessage(message: Message, content: string, contextType: string): Message {
|
|
98
|
+
const currentContent = message.content;
|
|
99
|
+
|
|
100
|
+
// Handle string content
|
|
101
|
+
if (typeof currentContent === 'string') {
|
|
102
|
+
let newContent: string;
|
|
103
|
+
|
|
104
|
+
if (this.hasSystemContextWrapper(currentContent)) {
|
|
105
|
+
// Insert into existing wrapper
|
|
106
|
+
const contextBlock = this.createContextBlock(content, contextType);
|
|
107
|
+
newContent = this.insertIntoExistingWrapper(currentContent, contextBlock);
|
|
108
|
+
} else {
|
|
109
|
+
// Create new wrapper
|
|
110
|
+
newContent = currentContent + '\n\n' + this.wrapWithSystemContext(content, contextType);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
...message,
|
|
115
|
+
content: newContent,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Handle array content (multimodal messages)
|
|
120
|
+
if (Array.isArray(currentContent)) {
|
|
121
|
+
const lastTextIndex = currentContent.findLastIndex((part: any) => part.type === 'text');
|
|
122
|
+
|
|
123
|
+
if (lastTextIndex !== -1) {
|
|
124
|
+
const newContent = [...currentContent];
|
|
125
|
+
const existingText = newContent[lastTextIndex].text;
|
|
126
|
+
let updatedText: string;
|
|
127
|
+
|
|
128
|
+
if (this.hasSystemContextWrapper(existingText)) {
|
|
129
|
+
// Insert into existing wrapper
|
|
130
|
+
const contextBlock = this.createContextBlock(content, contextType);
|
|
131
|
+
updatedText = this.insertIntoExistingWrapper(existingText, contextBlock);
|
|
132
|
+
} else {
|
|
133
|
+
// Create new wrapper
|
|
134
|
+
updatedText = existingText + '\n\n' + this.wrapWithSystemContext(content, contextType);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
newContent[lastTextIndex] = {
|
|
138
|
+
...newContent[lastTextIndex],
|
|
139
|
+
text: updatedText,
|
|
140
|
+
};
|
|
141
|
+
return {
|
|
142
|
+
...message,
|
|
143
|
+
content: newContent,
|
|
144
|
+
};
|
|
145
|
+
} else {
|
|
146
|
+
// No text part found, add a new one with wrapper
|
|
147
|
+
return {
|
|
148
|
+
...message,
|
|
149
|
+
content: [
|
|
150
|
+
...currentContent,
|
|
151
|
+
{ text: this.wrapWithSystemContext(content, contextType), type: 'text' },
|
|
152
|
+
],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return message;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Find the index of the last user message
|
|
162
|
+
*/
|
|
163
|
+
protected findLastUserMessageIndex(messages: Message[]): number {
|
|
164
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
165
|
+
if (messages[i].role === 'user') {
|
|
166
|
+
return i;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return -1;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Process the context by injecting content to every user message
|
|
174
|
+
*/
|
|
175
|
+
protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
|
|
176
|
+
const clonedContext = this.cloneContext(context);
|
|
177
|
+
const lastUserIndex = this.findLastUserMessageIndex(clonedContext.messages);
|
|
178
|
+
let injectCount = 0;
|
|
179
|
+
|
|
180
|
+
// Iterate through all messages
|
|
181
|
+
for (let i = 0; i < clonedContext.messages.length; i++) {
|
|
182
|
+
const message = clonedContext.messages[i];
|
|
183
|
+
|
|
184
|
+
// Only process user messages
|
|
185
|
+
if (message.role !== 'user') continue;
|
|
186
|
+
|
|
187
|
+
const isLastUser = i === lastUserIndex;
|
|
188
|
+
const result = this.buildContentForMessage(message, i, isLastUser);
|
|
189
|
+
|
|
190
|
+
if (!result) continue;
|
|
191
|
+
|
|
192
|
+
// Append to this user message with SYSTEM CONTEXT wrapper
|
|
193
|
+
clonedContext.messages[i] = this.appendToMessage(message, result.content, result.contextType);
|
|
194
|
+
injectCount++;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Update metadata with injection count
|
|
198
|
+
if (injectCount > 0) {
|
|
199
|
+
clonedContext.metadata[`${this.name}InjectedCount`] = injectCount;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return this.markAsExecuted(clonedContext);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import type { Message, PipelineContext, ProcessorOptions } from '../types';
|
|
2
2
|
import { BaseProcessor } from './BaseProcessor';
|
|
3
|
-
|
|
4
|
-
const SYSTEM_CONTEXT_START = '<!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->';
|
|
5
|
-
const SYSTEM_CONTEXT_END = '<!-- END SYSTEM CONTEXT -->';
|
|
6
|
-
const CONTEXT_INSTRUCTION = `<context.instruction>following part contains context information injected by the system. Please follow these instructions:
|
|
7
|
-
|
|
8
|
-
1. Always prioritize handling user-visible content.
|
|
9
|
-
2. the context is only required when user's queries rely on it.
|
|
10
|
-
</context.instruction>`;
|
|
3
|
+
import { CONTEXT_INSTRUCTION, SYSTEM_CONTEXT_END, SYSTEM_CONTEXT_START } from './constants';
|
|
11
4
|
|
|
12
5
|
/**
|
|
13
6
|
* Base Provider for appending content to the last user message
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type { Message, PipelineContext } from '../../types';
|
|
4
|
+
import { BaseEveryUserContentProvider } from '../BaseEveryUserContentProvider';
|
|
5
|
+
|
|
6
|
+
class TestEveryUserContentProvider extends BaseEveryUserContentProvider {
|
|
7
|
+
readonly name = 'TestEveryUserContentProvider';
|
|
8
|
+
|
|
9
|
+
constructor(
|
|
10
|
+
private contentBuilder?: (
|
|
11
|
+
message: Message,
|
|
12
|
+
index: number,
|
|
13
|
+
isLastUser: boolean,
|
|
14
|
+
) => { content: string; contextType: string } | null,
|
|
15
|
+
) {
|
|
16
|
+
super();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
protected buildContentForMessage(
|
|
20
|
+
message: Message,
|
|
21
|
+
index: number,
|
|
22
|
+
isLastUser: boolean,
|
|
23
|
+
): { content: string; contextType: string } | null {
|
|
24
|
+
if (this.contentBuilder) {
|
|
25
|
+
return this.contentBuilder(message, index, isLastUser);
|
|
26
|
+
}
|
|
27
|
+
// Default: inject content for every user message
|
|
28
|
+
return {
|
|
29
|
+
content: `Content for message ${index}`,
|
|
30
|
+
contextType: 'test_context',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Expose protected methods for testing
|
|
35
|
+
testHasSystemContextWrapper(content: string | any[]) {
|
|
36
|
+
return this.hasSystemContextWrapper(content);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
testWrapWithSystemContext(content: string, contextType: string) {
|
|
40
|
+
return this.wrapWithSystemContext(content, contextType);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
testCreateContextBlock(content: string, contextType: string) {
|
|
44
|
+
return this.createContextBlock(content, contextType);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
testAppendToMessage(message: Message, content: string, contextType: string) {
|
|
48
|
+
return this.appendToMessage(message, content, contextType);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
testFindLastUserMessageIndex(messages: Message[]) {
|
|
52
|
+
return this.findLastUserMessageIndex(messages);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe('BaseEveryUserContentProvider', () => {
|
|
57
|
+
const createContext = (messages: any[] = []): PipelineContext => ({
|
|
58
|
+
initialState: {
|
|
59
|
+
messages: [],
|
|
60
|
+
model: 'test-model',
|
|
61
|
+
provider: 'test-provider',
|
|
62
|
+
},
|
|
63
|
+
isAborted: false,
|
|
64
|
+
messages,
|
|
65
|
+
metadata: {
|
|
66
|
+
maxTokens: 4000,
|
|
67
|
+
model: 'test-model',
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('findLastUserMessageIndex', () => {
|
|
72
|
+
it('should find the last user message', () => {
|
|
73
|
+
const provider = new TestEveryUserContentProvider();
|
|
74
|
+
const messages = [
|
|
75
|
+
{ content: 'Hello', role: 'user' },
|
|
76
|
+
{ content: 'Hi', role: 'assistant' },
|
|
77
|
+
{ content: 'Question', role: 'user' },
|
|
78
|
+
{ content: 'Answer', role: 'assistant' },
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
expect(provider.testFindLastUserMessageIndex(messages)).toBe(2);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should return -1 when no user messages exist', () => {
|
|
85
|
+
const provider = new TestEveryUserContentProvider();
|
|
86
|
+
const messages = [{ content: 'System', role: 'system' }];
|
|
87
|
+
|
|
88
|
+
expect(provider.testFindLastUserMessageIndex(messages)).toBe(-1);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('hasSystemContextWrapper', () => {
|
|
93
|
+
it('should detect existing system context wrapper in string content', () => {
|
|
94
|
+
const provider = new TestEveryUserContentProvider();
|
|
95
|
+
|
|
96
|
+
const withWrapper = `Question
|
|
97
|
+
<!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->
|
|
98
|
+
<test>content</test>
|
|
99
|
+
<!-- END SYSTEM CONTEXT -->`;
|
|
100
|
+
|
|
101
|
+
const withoutWrapper = 'Simple question';
|
|
102
|
+
|
|
103
|
+
expect(provider.testHasSystemContextWrapper(withWrapper)).toBe(true);
|
|
104
|
+
expect(provider.testHasSystemContextWrapper(withoutWrapper)).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should detect existing system context wrapper in array content', () => {
|
|
108
|
+
const provider = new TestEveryUserContentProvider();
|
|
109
|
+
|
|
110
|
+
const withWrapper = [
|
|
111
|
+
{
|
|
112
|
+
text: `Question
|
|
113
|
+
<!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->
|
|
114
|
+
<test>content</test>
|
|
115
|
+
<!-- END SYSTEM CONTEXT -->`,
|
|
116
|
+
type: 'text',
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
const withoutWrapper = [{ text: 'Simple question', type: 'text' }];
|
|
121
|
+
|
|
122
|
+
expect(provider.testHasSystemContextWrapper(withWrapper)).toBe(true);
|
|
123
|
+
expect(provider.testHasSystemContextWrapper(withoutWrapper)).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('wrapWithSystemContext', () => {
|
|
128
|
+
it('should wrap content with system context markers', () => {
|
|
129
|
+
const provider = new TestEveryUserContentProvider();
|
|
130
|
+
const result = provider.testWrapWithSystemContext('Test content', 'test_type');
|
|
131
|
+
|
|
132
|
+
expect(result).toContain('<!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->');
|
|
133
|
+
expect(result).toContain('<context.instruction>');
|
|
134
|
+
expect(result).toContain('<test_type>');
|
|
135
|
+
expect(result).toContain('Test content');
|
|
136
|
+
expect(result).toContain('</test_type>');
|
|
137
|
+
expect(result).toContain('<!-- END SYSTEM CONTEXT -->');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('createContextBlock', () => {
|
|
142
|
+
it('should create context block without wrapper', () => {
|
|
143
|
+
const provider = new TestEveryUserContentProvider();
|
|
144
|
+
const result = provider.testCreateContextBlock('Block content', 'block_type');
|
|
145
|
+
|
|
146
|
+
expect(result).toBe(`<block_type>
|
|
147
|
+
Block content
|
|
148
|
+
</block_type>`);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('appendToMessage', () => {
|
|
153
|
+
it('should append with new wrapper to string content without existing wrapper', () => {
|
|
154
|
+
const provider = new TestEveryUserContentProvider();
|
|
155
|
+
const message: Message = { content: 'Original question', role: 'user' };
|
|
156
|
+
|
|
157
|
+
const result = provider.testAppendToMessage(message, 'New content', 'new_type');
|
|
158
|
+
|
|
159
|
+
expect(result.content).toContain('Original question');
|
|
160
|
+
expect(result.content).toContain('<!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->');
|
|
161
|
+
expect(result.content).toContain('<new_type>');
|
|
162
|
+
expect(result.content).toContain('New content');
|
|
163
|
+
expect(result.content).toContain('<!-- END SYSTEM CONTEXT -->');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should insert into existing wrapper in string content', () => {
|
|
167
|
+
const provider = new TestEveryUserContentProvider();
|
|
168
|
+
const message: Message = {
|
|
169
|
+
content: `Original question
|
|
170
|
+
|
|
171
|
+
<!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->
|
|
172
|
+
<context.instruction>...</context.instruction>
|
|
173
|
+
<existing_type>
|
|
174
|
+
Existing content
|
|
175
|
+
</existing_type>
|
|
176
|
+
<!-- END SYSTEM CONTEXT -->`,
|
|
177
|
+
role: 'user',
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const result = provider.testAppendToMessage(message, 'New content', 'new_type');
|
|
181
|
+
|
|
182
|
+
// Should have only one SYSTEM CONTEXT wrapper
|
|
183
|
+
const content = result.content as string;
|
|
184
|
+
const startCount = (content.match(/<!-- SYSTEM CONTEXT/g) || []).length;
|
|
185
|
+
const endCount = (content.match(/<!-- END SYSTEM CONTEXT/g) || []).length;
|
|
186
|
+
|
|
187
|
+
expect(startCount).toBe(1);
|
|
188
|
+
expect(endCount).toBe(1);
|
|
189
|
+
expect(content).toContain('<existing_type>');
|
|
190
|
+
expect(content).toContain('<new_type>');
|
|
191
|
+
expect(content).toContain('New content');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should handle array content without existing wrapper', () => {
|
|
195
|
+
const provider = new TestEveryUserContentProvider();
|
|
196
|
+
const message: Message = {
|
|
197
|
+
content: [
|
|
198
|
+
{ text: 'Original question', type: 'text' },
|
|
199
|
+
{ image_url: { url: 'http://example.com/img.png' }, type: 'image_url' },
|
|
200
|
+
],
|
|
201
|
+
role: 'user',
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const result = provider.testAppendToMessage(message, 'New content', 'new_type');
|
|
205
|
+
|
|
206
|
+
expect(result.content[0].text).toContain('Original question');
|
|
207
|
+
expect(result.content[0].text).toContain('<!-- SYSTEM CONTEXT');
|
|
208
|
+
expect(result.content[0].text).toContain('<new_type>');
|
|
209
|
+
expect(result.content[1].type).toBe('image_url');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should add new text part when array content has no text part', () => {
|
|
213
|
+
const provider = new TestEveryUserContentProvider();
|
|
214
|
+
const message: Message = {
|
|
215
|
+
content: [{ image_url: { url: 'http://example.com/img.png' }, type: 'image_url' }],
|
|
216
|
+
role: 'user',
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const result = provider.testAppendToMessage(message, 'New content', 'new_type');
|
|
220
|
+
|
|
221
|
+
expect(result.content).toHaveLength(2);
|
|
222
|
+
expect(result.content[1].type).toBe('text');
|
|
223
|
+
expect(result.content[1].text).toContain('<!-- SYSTEM CONTEXT');
|
|
224
|
+
expect(result.content[1].text).toContain('New content');
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe('process integration', () => {
|
|
229
|
+
it('should inject content to all user messages', async () => {
|
|
230
|
+
const provider = new TestEveryUserContentProvider();
|
|
231
|
+
const context = createContext([
|
|
232
|
+
{ content: 'First question', role: 'user' },
|
|
233
|
+
{ content: 'First answer', role: 'assistant' },
|
|
234
|
+
{ content: 'Second question', role: 'user' },
|
|
235
|
+
{ content: 'Second answer', role: 'assistant' },
|
|
236
|
+
{ content: 'Third question', role: 'user' },
|
|
237
|
+
]);
|
|
238
|
+
|
|
239
|
+
const result = await provider.process(context);
|
|
240
|
+
|
|
241
|
+
// All user messages should have content injected
|
|
242
|
+
expect(result.messages[0].content).toContain('First question');
|
|
243
|
+
expect(result.messages[0].content).toContain('<test_context>');
|
|
244
|
+
expect(result.messages[0].content).toContain('Content for message 0');
|
|
245
|
+
|
|
246
|
+
expect(result.messages[2].content).toContain('Second question');
|
|
247
|
+
expect(result.messages[2].content).toContain('<test_context>');
|
|
248
|
+
expect(result.messages[2].content).toContain('Content for message 2');
|
|
249
|
+
|
|
250
|
+
expect(result.messages[4].content).toContain('Third question');
|
|
251
|
+
expect(result.messages[4].content).toContain('<test_context>');
|
|
252
|
+
expect(result.messages[4].content).toContain('Content for message 4');
|
|
253
|
+
|
|
254
|
+
// Assistant messages should be unchanged
|
|
255
|
+
expect(result.messages[1].content).toBe('First answer');
|
|
256
|
+
expect(result.messages[3].content).toBe('Second answer');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('should correctly identify isLastUser parameter', async () => {
|
|
260
|
+
const isLastUserCalls: boolean[] = [];
|
|
261
|
+
|
|
262
|
+
const provider = new TestEveryUserContentProvider((message, index, isLastUser) => {
|
|
263
|
+
isLastUserCalls.push(isLastUser);
|
|
264
|
+
return { content: `Content ${index}`, contextType: 'test' };
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const context = createContext([
|
|
268
|
+
{ content: 'First', role: 'user' },
|
|
269
|
+
{ content: 'Answer', role: 'assistant' },
|
|
270
|
+
{ content: 'Second', role: 'user' },
|
|
271
|
+
{ content: 'Answer', role: 'assistant' },
|
|
272
|
+
{ content: 'Third (last)', role: 'user' },
|
|
273
|
+
]);
|
|
274
|
+
|
|
275
|
+
await provider.process(context);
|
|
276
|
+
|
|
277
|
+
expect(isLastUserCalls).toEqual([false, false, true]);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should skip injection when buildContentForMessage returns null', async () => {
|
|
281
|
+
const provider = new TestEveryUserContentProvider((message, index) => {
|
|
282
|
+
// Only inject for first user message
|
|
283
|
+
if (index === 0) {
|
|
284
|
+
return { content: 'First only', contextType: 'test' };
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const context = createContext([
|
|
290
|
+
{ content: 'First question', role: 'user' },
|
|
291
|
+
{ content: 'Answer', role: 'assistant' },
|
|
292
|
+
{ content: 'Second question', role: 'user' },
|
|
293
|
+
]);
|
|
294
|
+
|
|
295
|
+
const result = await provider.process(context);
|
|
296
|
+
|
|
297
|
+
expect(result.messages[0].content).toContain('<test>');
|
|
298
|
+
expect(result.messages[0].content).toContain('First only');
|
|
299
|
+
expect(result.messages[2].content).toBe('Second question');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should update metadata with injection count', async () => {
|
|
303
|
+
const provider = new TestEveryUserContentProvider();
|
|
304
|
+
const context = createContext([
|
|
305
|
+
{ content: 'First', role: 'user' },
|
|
306
|
+
{ content: 'Second', role: 'user' },
|
|
307
|
+
{ content: 'Third', role: 'user' },
|
|
308
|
+
]);
|
|
309
|
+
|
|
310
|
+
const result = await provider.process(context);
|
|
311
|
+
|
|
312
|
+
expect(result.metadata.TestEveryUserContentProviderInjectedCount).toBe(3);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('should not set metadata when no injections made', async () => {
|
|
316
|
+
const provider = new TestEveryUserContentProvider(() => null);
|
|
317
|
+
const context = createContext([{ content: 'Question', role: 'user' }]);
|
|
318
|
+
|
|
319
|
+
const result = await provider.process(context);
|
|
320
|
+
|
|
321
|
+
expect(result.metadata.TestEveryUserContentProviderInjectedCount).toBeUndefined();
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe('integration with BaseLastUserContentProvider', () => {
|
|
326
|
+
it('should allow BaseLastUserContentProvider to reuse wrapper created by BaseEveryUserContentProvider', async () => {
|
|
327
|
+
// First: BaseEveryUserContentProvider injects to last user message
|
|
328
|
+
const everyProvider = new TestEveryUserContentProvider((message, index, isLastUser) => {
|
|
329
|
+
if (isLastUser) {
|
|
330
|
+
return { content: 'Selection content', contextType: 'user_selections' };
|
|
331
|
+
}
|
|
332
|
+
return null;
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const context = createContext([
|
|
336
|
+
{ content: 'First question', role: 'user' },
|
|
337
|
+
{ content: 'Answer', role: 'assistant' },
|
|
338
|
+
{ content: 'Last question', role: 'user' },
|
|
339
|
+
]);
|
|
340
|
+
|
|
341
|
+
const result = await everyProvider.process(context);
|
|
342
|
+
|
|
343
|
+
// The last user message should have a SYSTEM CONTEXT wrapper
|
|
344
|
+
const lastUserContent = result.messages[2].content as string;
|
|
345
|
+
expect(lastUserContent).toContain('<!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->');
|
|
346
|
+
expect(lastUserContent).toContain('<user_selections>');
|
|
347
|
+
expect(lastUserContent).toContain('Selection content');
|
|
348
|
+
expect(lastUserContent).toContain('<!-- END SYSTEM CONTEXT -->');
|
|
349
|
+
|
|
350
|
+
// Now BaseLastUserContentProvider can detect and reuse this wrapper
|
|
351
|
+
expect(everyProvider.testHasSystemContextWrapper(lastUserContent)).toBe(true);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared constants for context injection
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* System context wrapper markers
|
|
7
|
+
* Used to wrap injected context content so models can distinguish it from user content
|
|
8
|
+
*/
|
|
9
|
+
export const SYSTEM_CONTEXT_START = '<!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->';
|
|
10
|
+
export const SYSTEM_CONTEXT_END = '<!-- END SYSTEM CONTEXT -->';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Context instruction text
|
|
14
|
+
* Provides guidance to the model on how to handle injected context
|
|
15
|
+
*/
|
|
16
|
+
export const CONTEXT_INSTRUCTION = `<context.instruction>following part contains context information injected by the system. Please follow these instructions:
|
|
17
|
+
|
|
18
|
+
1. Always prioritize handling user-visible content.
|
|
19
|
+
2. the context is only required when user's queries rely on it.
|
|
20
|
+
</context.instruction>`;
|