Skip to content

Commit 550acc2

Browse files
authored
πŸ’„ style: improve auto scroll and group profile (#11725)
* refactor the group context injector * improve agent tool * refactor AutoScroll to fix auto scroll in tool use * fix broadcast mode * update * improve * fix lobe-ai builtin tools issue
1 parent 395595a commit 550acc2

File tree

15 files changed

+527
-187
lines changed

15 files changed

+527
-187
lines changed

β€Žpackages/context-engine/src/engine/messages/MessagesEngine.tsβ€Ž

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,15 @@ export class MessagesEngine {
162162
// 2. System role injection (agent's system role)
163163
new SystemRoleInjector({ systemRole }),
164164

165+
// =============================================
166+
// Phase 2.5: First User Message Context Injection
167+
// These providers inject content before the first user message
168+
// Order matters: first executed = first in content
169+
// =============================================
170+
171+
// 4. User memory injection (conditionally added, injected first)
172+
...(isUserMemoryEnabled ? [new UserMemoryInjector(userMemory)] : []),
173+
165174
// 3. Group context injection (agent identity and group info for multi-agent chat)
166175
new GroupContextInjector({
167176
currentAgentId: agentGroup?.currentAgentId,
@@ -173,15 +182,6 @@ export class MessagesEngine {
173182
systemPrompt: agentGroup?.systemPrompt,
174183
}),
175184

176-
// =============================================
177-
// Phase 2.5: First User Message Context Injection
178-
// These providers inject content before the first user message
179-
// Order matters: first executed = first in content
180-
// =============================================
181-
182-
// 4. User memory injection (conditionally added, injected first)
183-
...(isUserMemoryEnabled ? [new UserMemoryInjector(userMemory)] : []),
184-
185185
// 4.5. GTD Plan injection (conditionally added, after user memory, before knowledge)
186186
...(isGTDPlanEnabled ? [new GTDPlanInjector({ enabled: true, plan: gtd.plan })] : []),
187187

β€Žpackages/context-engine/src/providers/GroupContextInjector.tsβ€Ž

Lines changed: 19 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
} from '@lobechat/prompts';
66
import debug from 'debug';
77

8-
import { BaseProvider } from '../base/BaseProvider';
8+
import { BaseFirstUserContentProvider } from '../base/BaseFirstUserContentProvider';
99
import type { PipelineContext, ProcessorOptions } from '../types';
1010

1111
const log = debug('context-engine:provider:GroupContextInjector');
@@ -59,13 +59,13 @@ export interface GroupContextInjectorConfig {
5959
/**
6060
* Group Context Injector
6161
*
62-
* Responsible for injecting group context information into the system role
62+
* Responsible for injecting group context information before the first user message
6363
* for multi-agent group chat scenarios. This helps the model understand:
6464
* - Its own identity within the group
6565
* - The group composition and other members
6666
* - Rules for handling system metadata
6767
*
68-
* The injector appends a GROUP CONTEXT block at the end of the system message,
68+
* The injector creates a system injection message before the first user message,
6969
* containing:
7070
* - Agent's identity (name, role, ID)
7171
* - Group info (name, member list)
@@ -87,7 +87,7 @@ export interface GroupContextInjectorConfig {
8787
* });
8888
* ```
8989
*/
90-
export class GroupContextInjector extends BaseProvider {
90+
export class GroupContextInjector extends BaseFirstUserContentProvider {
9191
readonly name = 'GroupContextInjector';
9292

9393
constructor(
@@ -97,44 +97,32 @@ export class GroupContextInjector extends BaseProvider {
9797
super(options);
9898
}
9999

100-
protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
101-
const clonedContext = this.cloneContext(context);
102-
103-
// Skip if not enabled or missing required config
100+
protected buildContent(): string | null {
101+
// Skip if not enabled
104102
if (!this.config.enabled) {
105103
log('Group context injection disabled, skipping');
106-
return this.markAsExecuted(clonedContext);
107-
}
108-
109-
// Find the system message to append to
110-
const systemMessageIndex = clonedContext.messages.findIndex((msg) => msg.role === 'system');
111-
112-
if (systemMessageIndex === -1) {
113-
log('No system message found, skipping group context injection');
114-
return this.markAsExecuted(clonedContext);
104+
return null;
115105
}
116106

117-
const systemMessage = clonedContext.messages[systemMessageIndex];
118-
const groupContext = this.buildGroupContextBlock();
107+
const content = this.buildGroupContextBlock();
108+
log('Group context prepared for injection');
119109

120-
// Append group context to system message content
121-
if (typeof systemMessage.content === 'string') {
122-
clonedContext.messages[systemMessageIndex] = {
123-
...systemMessage,
124-
content: systemMessage.content + groupContext,
125-
};
110+
return content;
111+
}
126112

127-
log('Group context injected into system message');
128-
}
113+
protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
114+
const result = await super.doProcess(context);
129115

130116
// Update metadata
131-
clonedContext.metadata.groupContextInjected = true;
117+
if (this.config.enabled) {
118+
result.metadata.groupContextInjected = true;
119+
}
132120

133-
return this.markAsExecuted(clonedContext);
121+
return result;
134122
}
135123

136124
/**
137-
* Build the group context block to append to system message
125+
* Build the group context block
138126
* Uses template from @lobechat/prompts with direct variable replacement
139127
*/
140128
private buildGroupContextBlock(): string {
@@ -159,9 +147,7 @@ export class GroupContextInjector extends BaseProvider {
159147
.replace('{{SYSTEM_PROMPT}}', systemPrompt || '')
160148
.replace('{{GROUP_MEMBERS}}', membersText);
161149

162-
return `
163-
164-
<group_context>
150+
return `<group_context>
165151
${groupContextContent}
166152
</group_context>`;
167153
}

β€Žpackages/context-engine/src/providers/__tests__/GroupContextInjector.test.tsβ€Ž

Lines changed: 79 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ describe('GroupContextInjector', () => {
1212
});
1313

1414
describe('Basic Scenarios', () => {
15-
it('should inject group context into system message', async () => {
15+
it('should inject group context before first user message', async () => {
1616
const injector = new GroupContextInjector({
1717
currentAgentId: 'agt_editor',
1818
currentAgentName: 'Editor',
@@ -35,31 +35,39 @@ describe('GroupContextInjector', () => {
3535
const context = createContext(input);
3636
const result = await injector.process(context);
3737

38-
const systemContent = result.messages[0].content;
38+
// System message should be unchanged
39+
expect(result.messages[0].content).toBe('You are a helpful editor.');
40+
41+
// Should have 3 messages now (system, injected, user)
42+
expect(result.messages).toHaveLength(3);
3943

40-
// Original content should be preserved
41-
expect(systemContent).toContain('You are a helpful editor.');
44+
// Check injected message (second message)
45+
const injectedContent = result.messages[1].content;
46+
expect(result.messages[1].role).toBe('user');
4247

4348
// Agent identity (plain text, no wrapper)
44-
expect(systemContent).toContain('You are "Editor"');
45-
expect(systemContent).toContain('acting as a participant');
46-
expect(systemContent).toContain('"Writing Team"');
47-
expect(systemContent).toContain('agt_editor');
48-
expect(systemContent).not.toContain('<agent_identity>');
49+
expect(injectedContent).toContain('You are "Editor"');
50+
expect(injectedContent).toContain('acting as a participant');
51+
expect(injectedContent).toContain('"Writing Team"');
52+
expect(injectedContent).toContain('agt_editor');
53+
expect(injectedContent).not.toContain('<agent_identity>');
4954

5055
// Group context section with system prompt
51-
expect(systemContent).toContain('<group_context>');
52-
expect(systemContent).toContain('A team for collaborative writing');
56+
expect(injectedContent).toContain('<group_context>');
57+
expect(injectedContent).toContain('A team for collaborative writing');
5358

5459
// Participants section with XML format
55-
expect(systemContent).toContain('<group_participants>');
56-
expect(systemContent).toContain('<member name="Supervisor" id="agt_supervisor" />');
57-
expect(systemContent).toContain('<member name="Writer" id="agt_writer" />');
58-
expect(systemContent).toContain('<member name="Editor" id="agt_editor" you="true" />');
60+
expect(injectedContent).toContain('<group_participants>');
61+
expect(injectedContent).toContain('<member name="Supervisor" id="agt_supervisor" />');
62+
expect(injectedContent).toContain('<member name="Writer" id="agt_writer" />');
63+
expect(injectedContent).toContain('<member name="Editor" id="agt_editor" you="true" />');
5964

6065
// Identity rules
61-
expect(systemContent).toContain('<identity_rules>');
62-
expect(systemContent).toContain('NEVER expose or display agent IDs');
66+
expect(injectedContent).toContain('<identity_rules>');
67+
expect(injectedContent).toContain('NEVER expose or display agent IDs');
68+
69+
// Original user message should be third
70+
expect(result.messages[2].content).toBe('Please review this.');
6371

6472
// Metadata should be updated
6573
expect(result.metadata.groupContextInjected).toBe(true);
@@ -72,35 +80,37 @@ describe('GroupContextInjector', () => {
7280
enabled: false, // Disabled
7381
});
7482

75-
const input: any[] = [{ role: 'system', content: 'You are a helpful editor.' }];
83+
const input: any[] = [
84+
{ role: 'system', content: 'You are a helpful editor.' },
85+
{ role: 'user', content: 'Hello' },
86+
];
7687

7788
const context = createContext(input);
7889
const result = await injector.process(context);
7990

80-
// Should be unchanged
91+
// Should be unchanged - no injection
92+
expect(result.messages).toHaveLength(2);
8193
expect(result.messages[0].content).toBe('You are a helpful editor.');
94+
expect(result.messages[1].content).toBe('Hello');
8295
expect(result.metadata.groupContextInjected).toBeUndefined();
8396
});
8497

85-
it('should skip injection when no system message exists', async () => {
98+
it('should skip injection when no user message exists', async () => {
8699
const injector = new GroupContextInjector({
87100
currentAgentId: 'agt_editor',
88101
currentAgentName: 'Editor',
89102
enabled: true,
90103
});
91104

92-
const input: any[] = [
93-
{ role: 'user', content: 'Hello' },
94-
{ role: 'assistant', content: 'Hi there!' },
95-
];
105+
const input: any[] = [{ role: 'system', content: 'You are a helpful editor.' }];
96106

97107
const context = createContext(input);
98108
const result = await injector.process(context);
99109

100-
// Messages should be unchanged
101-
expect(result.messages[0].content).toBe('Hello');
102-
expect(result.messages[1].content).toBe('Hi there!');
103-
expect(result.metadata.groupContextInjected).toBeUndefined();
110+
// Messages should be unchanged - no user message to inject before
111+
expect(result.messages).toHaveLength(1);
112+
expect(result.messages[0].content).toBe('You are a helpful editor.');
113+
expect(result.metadata.groupContextInjected).toBe(true);
104114
});
105115
});
106116

@@ -113,12 +123,16 @@ describe('GroupContextInjector', () => {
113123
enabled: true,
114124
});
115125

116-
const input: any[] = [{ content: 'You are an editor.', role: 'system' }];
126+
const input: any[] = [
127+
{ content: 'You are an editor.', role: 'system' },
128+
{ content: 'Hello', role: 'user' },
129+
];
117130

118131
const context = createContext(input);
119132
const result = await injector.process(context);
120133

121-
expect(result.messages[0].content).toMatchSnapshot();
134+
// Check injected message content
135+
expect(result.messages[1].content).toMatchSnapshot();
122136
});
123137

124138
it('should handle config with only group info', async () => {
@@ -129,25 +143,33 @@ describe('GroupContextInjector', () => {
129143
systemPrompt: 'Test group description',
130144
});
131145

132-
const input: any[] = [{ content: 'System prompt.', role: 'system' }];
146+
const input: any[] = [
147+
{ content: 'System prompt.', role: 'system' },
148+
{ content: 'Hello', role: 'user' },
149+
];
133150

134151
const context = createContext(input);
135152
const result = await injector.process(context);
136153

137-
expect(result.messages[0].content).toMatchSnapshot();
154+
// Check injected message content
155+
expect(result.messages[1].content).toMatchSnapshot();
138156
});
139157

140158
it('should handle empty config', async () => {
141159
const injector = new GroupContextInjector({
142160
enabled: true,
143161
});
144162

145-
const input: any[] = [{ content: 'Base prompt.', role: 'system' }];
163+
const input: any[] = [
164+
{ content: 'Base prompt.', role: 'system' },
165+
{ content: 'Hello', role: 'user' },
166+
];
146167

147168
const context = createContext(input);
148169
const result = await injector.process(context);
149170

150-
expect(result.messages[0].content).toMatchSnapshot();
171+
// Check injected message content
172+
expect(result.messages[1].content).toMatchSnapshot();
151173
});
152174
});
153175

@@ -158,15 +180,19 @@ describe('GroupContextInjector', () => {
158180
// Minimal config
159181
});
160182

161-
const input: any[] = [{ role: 'system', content: 'Base prompt.' }];
183+
const input: any[] = [
184+
{ role: 'system', content: 'Base prompt.' },
185+
{ role: 'user', content: 'Hello' },
186+
];
162187

163188
const context = createContext(input);
164189
const result = await injector.process(context);
165190

166-
const systemContent = result.messages[0].content;
191+
// Check injected message content
192+
const injectedContent = result.messages[1].content;
167193

168194
// Even with minimal config, identity rules should be present
169-
expect(systemContent).toMatchSnapshot();
195+
expect(injectedContent).toMatchSnapshot();
170196
});
171197
});
172198

@@ -179,12 +205,16 @@ describe('GroupContextInjector', () => {
179205
systemPrompt: 'Empty group description',
180206
});
181207

182-
const input: any[] = [{ content: 'Prompt.', role: 'system' }];
208+
const input: any[] = [
209+
{ content: 'Prompt.', role: 'system' },
210+
{ content: 'Hello', role: 'user' },
211+
];
183212

184213
const context = createContext(input);
185214
const result = await injector.process(context);
186215

187-
expect(result.messages[0].content).toMatchSnapshot();
216+
// Check injected message content
217+
expect(result.messages[1].content).toMatchSnapshot();
188218
});
189219

190220
it('should preserve other messages unchanged', async () => {
@@ -204,10 +234,16 @@ describe('GroupContextInjector', () => {
204234
const context = createContext(input);
205235
const result = await injector.process(context);
206236

207-
// Only system message should be modified
208-
expect(result.messages[0].content).toContain('<group_context>');
209-
expect(result.messages[1].content).toBe('User message.');
210-
expect(result.messages[2].content).toBe('Assistant response.');
237+
// System message should be unchanged
238+
expect(result.messages[0].content).toBe('System prompt.');
239+
240+
// Injected message should be second
241+
expect(result.messages[1].role).toBe('user');
242+
expect(result.messages[1].content).toContain('<group_context>');
243+
244+
// Original messages should be preserved
245+
expect(result.messages[2].content).toBe('User message.');
246+
expect(result.messages[3].content).toBe('Assistant response.');
211247
});
212248
});
213249
});

0 commit comments

Comments
Β (0)