@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
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
HistorySummaryProvider,
|
|
29
29
|
KnowledgeInjector,
|
|
30
30
|
PageEditorContextInjector,
|
|
31
|
+
PageSelectionsInjector,
|
|
31
32
|
SystemRoleInjector,
|
|
32
33
|
ToolSystemRoleProvider,
|
|
33
34
|
UserMemoryInjector,
|
|
@@ -143,19 +144,19 @@ export class MessagesEngine {
|
|
|
143
144
|
|
|
144
145
|
return [
|
|
145
146
|
// =============================================
|
|
146
|
-
// Phase
|
|
147
|
+
// Phase 1: System Role Injection
|
|
147
148
|
// =============================================
|
|
148
149
|
|
|
149
|
-
//
|
|
150
|
+
// 1. System role injection (agent's system role)
|
|
150
151
|
new SystemRoleInjector({ systemRole }),
|
|
151
152
|
|
|
152
153
|
// =============================================
|
|
153
|
-
// Phase 2
|
|
154
|
+
// Phase 2: First User Message Context Injection
|
|
154
155
|
// These providers inject content before the first user message
|
|
155
156
|
// Order matters: first executed = first in content
|
|
156
157
|
// =============================================
|
|
157
158
|
|
|
158
|
-
//
|
|
159
|
+
// 2. User memory injection (conditionally added, injected first)
|
|
159
160
|
...(isUserMemoryEnabled ? [new UserMemoryInjector(userMemory)] : []),
|
|
160
161
|
|
|
161
162
|
// 3. Group context injection (agent identity and group info for multi-agent chat)
|
|
@@ -169,7 +170,7 @@ export class MessagesEngine {
|
|
|
169
170
|
systemPrompt: agentGroup?.systemPrompt,
|
|
170
171
|
}),
|
|
171
172
|
|
|
172
|
-
// 4.
|
|
173
|
+
// 4. GTD Plan injection (conditionally added, after user memory, before knowledge)
|
|
173
174
|
...(isGTDPlanEnabled ? [new GTDPlanInjector({ enabled: true, plan: gtd.plan })] : []),
|
|
174
175
|
|
|
175
176
|
// 5. Knowledge injection (full content for agent files + metadata for knowledge bases)
|
|
@@ -179,7 +180,7 @@ export class MessagesEngine {
|
|
|
179
180
|
}),
|
|
180
181
|
|
|
181
182
|
// =============================================
|
|
182
|
-
// Phase
|
|
183
|
+
// Phase 3: Additional System Context
|
|
183
184
|
// =============================================
|
|
184
185
|
|
|
185
186
|
// 6. Agent Builder context injection (current agent config/meta for editing)
|
|
@@ -212,7 +213,10 @@ export class MessagesEngine {
|
|
|
212
213
|
historySummary,
|
|
213
214
|
}),
|
|
214
215
|
|
|
215
|
-
//
|
|
216
|
+
// 12. Page Selections injection (inject user-selected text into each user message that has them)
|
|
217
|
+
new PageSelectionsInjector({ enabled: isPageEditorEnabled }),
|
|
218
|
+
|
|
219
|
+
// 10. Page Editor context injection (inject current page content to last user message)
|
|
216
220
|
new PageEditorContextInjector({
|
|
217
221
|
enabled: isPageEditorEnabled,
|
|
218
222
|
// Use direct pageContentContext if provided (server-side), otherwise build from initialContext + stepContext (frontend)
|
|
@@ -232,37 +236,37 @@ export class MessagesEngine {
|
|
|
232
236
|
: undefined,
|
|
233
237
|
}),
|
|
234
238
|
|
|
235
|
-
//
|
|
239
|
+
// 11. GTD Todo injection (conditionally added, at end of last user message)
|
|
236
240
|
...(isGTDTodoEnabled ? [new GTDTodoInjector({ enabled: true, todos: gtd.todos })] : []),
|
|
237
241
|
|
|
238
242
|
// =============================================
|
|
239
|
-
// Phase
|
|
243
|
+
// Phase 4: Message Transformation
|
|
240
244
|
// =============================================
|
|
241
245
|
|
|
242
|
-
//
|
|
246
|
+
// 13. Input template processing
|
|
243
247
|
new InputTemplateProcessor({ inputTemplate }),
|
|
244
248
|
|
|
245
|
-
//
|
|
249
|
+
// 14. Placeholder variables processing
|
|
246
250
|
new PlaceholderVariablesProcessor({
|
|
247
251
|
variableGenerators: variableGenerators || {},
|
|
248
252
|
}),
|
|
249
253
|
|
|
250
|
-
//
|
|
254
|
+
// 15. AgentCouncil message flatten (convert role=agentCouncil to standard assistant + tool messages)
|
|
251
255
|
new AgentCouncilFlattenProcessor(),
|
|
252
256
|
|
|
253
|
-
//
|
|
257
|
+
// 16. Group message flatten (convert role=assistantGroup to standard assistant + tool messages)
|
|
254
258
|
new GroupMessageFlattenProcessor(),
|
|
255
259
|
|
|
256
|
-
//
|
|
260
|
+
// 17. Tasks message flatten (convert role=tasks to individual task messages)
|
|
257
261
|
new TasksFlattenProcessor(),
|
|
258
262
|
|
|
259
|
-
//
|
|
263
|
+
// 18. Task message processing (convert role=task to assistant with instruction + content)
|
|
260
264
|
new TaskMessageProcessor(),
|
|
261
265
|
|
|
262
|
-
//
|
|
266
|
+
// 19. Supervisor role restore (convert role=supervisor back to role=assistant for model)
|
|
263
267
|
new SupervisorRoleRestoreProcessor(),
|
|
264
268
|
|
|
265
|
-
//
|
|
269
|
+
// 20. Group orchestration filter (remove supervisor's orchestration messages like broadcast/speak)
|
|
266
270
|
// This must be BEFORE GroupRoleTransformProcessor so we filter based on original agentId/tools
|
|
267
271
|
...(isAgentGroupEnabled && agentGroup.agentMap && agentGroup.currentAgentId
|
|
268
272
|
? [
|
|
@@ -277,7 +281,7 @@ export class MessagesEngine {
|
|
|
277
281
|
]
|
|
278
282
|
: []),
|
|
279
283
|
|
|
280
|
-
//
|
|
284
|
+
// 21. Group role transform (convert other agents' messages to user role with speaker tags)
|
|
281
285
|
// This must be BEFORE ToolCallProcessor so other agents' tool messages are converted first
|
|
282
286
|
...(isAgentGroupEnabled && agentGroup.currentAgentId
|
|
283
287
|
? [
|
|
@@ -289,10 +293,10 @@ export class MessagesEngine {
|
|
|
289
293
|
: []),
|
|
290
294
|
|
|
291
295
|
// =============================================
|
|
292
|
-
// Phase
|
|
296
|
+
// Phase 5: Content Processing
|
|
293
297
|
// =============================================
|
|
294
298
|
|
|
295
|
-
//
|
|
299
|
+
// 22. Message content processing (image encoding, etc.)
|
|
296
300
|
new MessageContentProcessor({
|
|
297
301
|
fileContext: fileContext || { enabled: true, includeFileUrl: true },
|
|
298
302
|
isCanUseVideo: capabilities?.isCanUseVideo || (() => false),
|
|
@@ -301,7 +305,7 @@ export class MessagesEngine {
|
|
|
301
305
|
provider,
|
|
302
306
|
}),
|
|
303
307
|
|
|
304
|
-
//
|
|
308
|
+
// 23. Tool call processing
|
|
305
309
|
new ToolCallProcessor({
|
|
306
310
|
genToolCallingName: this.toolNameResolver.generate.bind(this.toolNameResolver),
|
|
307
311
|
isCanUseFC: capabilities?.isCanUseFC || (() => true),
|
|
@@ -309,10 +313,10 @@ export class MessagesEngine {
|
|
|
309
313
|
provider,
|
|
310
314
|
}),
|
|
311
315
|
|
|
312
|
-
//
|
|
316
|
+
// 24. Tool message reordering
|
|
313
317
|
new ToolMessageReorder(),
|
|
314
318
|
|
|
315
|
-
//
|
|
319
|
+
// 25. Message cleanup (final step, keep only necessary fields)
|
|
316
320
|
new MessageCleanupProcessor(),
|
|
317
321
|
];
|
|
318
322
|
}
|
|
@@ -407,4 +407,368 @@ describe('MessagesEngine', () => {
|
|
|
407
407
|
expect(userMessage?.content).toBe('Please respond to: user input');
|
|
408
408
|
});
|
|
409
409
|
});
|
|
410
|
+
|
|
411
|
+
describe('Page Editor context', () => {
|
|
412
|
+
it('should inject page content to the last user message when pageContentContext is provided', async () => {
|
|
413
|
+
const messages: UIChatMessage[] = [
|
|
414
|
+
{
|
|
415
|
+
content: 'First question',
|
|
416
|
+
createdAt: Date.now(),
|
|
417
|
+
id: 'msg-1',
|
|
418
|
+
role: 'user',
|
|
419
|
+
updatedAt: Date.now(),
|
|
420
|
+
} as UIChatMessage,
|
|
421
|
+
{
|
|
422
|
+
content: 'Answer',
|
|
423
|
+
createdAt: Date.now(),
|
|
424
|
+
id: 'msg-2',
|
|
425
|
+
role: 'assistant',
|
|
426
|
+
updatedAt: Date.now(),
|
|
427
|
+
} as UIChatMessage,
|
|
428
|
+
{
|
|
429
|
+
content: 'Second question about the page',
|
|
430
|
+
createdAt: Date.now(),
|
|
431
|
+
id: 'msg-3',
|
|
432
|
+
role: 'user',
|
|
433
|
+
updatedAt: Date.now(),
|
|
434
|
+
} as UIChatMessage,
|
|
435
|
+
];
|
|
436
|
+
|
|
437
|
+
const params = createBasicParams({
|
|
438
|
+
messages,
|
|
439
|
+
pageContentContext: {
|
|
440
|
+
markdown: '# Document Title\n\nDocument content here.',
|
|
441
|
+
metadata: {
|
|
442
|
+
charCount: 40,
|
|
443
|
+
lineCount: 3,
|
|
444
|
+
title: 'Test Document',
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
const engine = new MessagesEngine(params);
|
|
449
|
+
|
|
450
|
+
const result = await engine.process();
|
|
451
|
+
|
|
452
|
+
expect(result.messages).toEqual([
|
|
453
|
+
{ content: 'First question', role: 'user' },
|
|
454
|
+
{ content: 'Answer', role: 'assistant' },
|
|
455
|
+
{
|
|
456
|
+
content: `Second question about the page
|
|
457
|
+
|
|
458
|
+
<!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->
|
|
459
|
+
<context.instruction>following part contains context information injected by the system. Please follow these instructions:
|
|
460
|
+
|
|
461
|
+
1. Always prioritize handling user-visible content.
|
|
462
|
+
2. the context is only required when user's queries rely on it.
|
|
463
|
+
</context.instruction>
|
|
464
|
+
<current_page_context>
|
|
465
|
+
<current_page title="Test Document">
|
|
466
|
+
<markdown chars="40" lines="3">
|
|
467
|
+
# Document Title
|
|
468
|
+
|
|
469
|
+
Document content here.
|
|
470
|
+
</markdown>
|
|
471
|
+
</current_page>
|
|
472
|
+
</current_page_context>
|
|
473
|
+
<!-- END SYSTEM CONTEXT -->`,
|
|
474
|
+
role: 'user',
|
|
475
|
+
},
|
|
476
|
+
]);
|
|
477
|
+
|
|
478
|
+
expect(result.metadata.pageEditorContextInjected).toBe(true);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('should not inject page content when not enabled', async () => {
|
|
482
|
+
const messages: UIChatMessage[] = [
|
|
483
|
+
{
|
|
484
|
+
content: 'Question',
|
|
485
|
+
createdAt: Date.now(),
|
|
486
|
+
id: 'msg-1',
|
|
487
|
+
role: 'user',
|
|
488
|
+
updatedAt: Date.now(),
|
|
489
|
+
} as UIChatMessage,
|
|
490
|
+
];
|
|
491
|
+
|
|
492
|
+
const params = createBasicParams({ messages });
|
|
493
|
+
const engine = new MessagesEngine(params);
|
|
494
|
+
|
|
495
|
+
const result = await engine.process();
|
|
496
|
+
|
|
497
|
+
expect(result.messages).toEqual([{ content: 'Question', role: 'user' }]);
|
|
498
|
+
expect(result.metadata.pageEditorContextInjected).toBeUndefined();
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
describe('Page Selections', () => {
|
|
503
|
+
it('should inject page selections to each user message that has them', async () => {
|
|
504
|
+
const messages: UIChatMessage[] = [
|
|
505
|
+
{
|
|
506
|
+
content: 'First question with selection',
|
|
507
|
+
createdAt: Date.now(),
|
|
508
|
+
id: 'msg-1',
|
|
509
|
+
metadata: {
|
|
510
|
+
pageSelections: [
|
|
511
|
+
{
|
|
512
|
+
content: 'Selected paragraph 1',
|
|
513
|
+
id: 'sel-1',
|
|
514
|
+
pageId: 'page-1',
|
|
515
|
+
xml: '<p>Selected paragraph 1</p>',
|
|
516
|
+
},
|
|
517
|
+
],
|
|
518
|
+
},
|
|
519
|
+
role: 'user',
|
|
520
|
+
updatedAt: Date.now(),
|
|
521
|
+
} as UIChatMessage,
|
|
522
|
+
{
|
|
523
|
+
content: 'Answer to first',
|
|
524
|
+
createdAt: Date.now(),
|
|
525
|
+
id: 'msg-2',
|
|
526
|
+
role: 'assistant',
|
|
527
|
+
updatedAt: Date.now(),
|
|
528
|
+
} as UIChatMessage,
|
|
529
|
+
{
|
|
530
|
+
content: 'Second question with different selection',
|
|
531
|
+
createdAt: Date.now(),
|
|
532
|
+
id: 'msg-3',
|
|
533
|
+
metadata: {
|
|
534
|
+
pageSelections: [
|
|
535
|
+
{
|
|
536
|
+
content: 'Selected paragraph 2',
|
|
537
|
+
id: 'sel-2',
|
|
538
|
+
pageId: 'page-1',
|
|
539
|
+
xml: '<p>Selected paragraph 2</p>',
|
|
540
|
+
},
|
|
541
|
+
],
|
|
542
|
+
},
|
|
543
|
+
role: 'user',
|
|
544
|
+
updatedAt: Date.now(),
|
|
545
|
+
} as UIChatMessage,
|
|
546
|
+
];
|
|
547
|
+
|
|
548
|
+
const params = createBasicParams({
|
|
549
|
+
messages,
|
|
550
|
+
pageContentContext: {
|
|
551
|
+
markdown: '# Doc',
|
|
552
|
+
metadata: { title: 'Doc' },
|
|
553
|
+
},
|
|
554
|
+
});
|
|
555
|
+
const engine = new MessagesEngine(params);
|
|
556
|
+
|
|
557
|
+
const result = await engine.process();
|
|
558
|
+
|
|
559
|
+
expect(result.messages).toEqual([
|
|
560
|
+
{
|
|
561
|
+
content: `First question with selection
|
|
562
|
+
|
|
563
|
+
<!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->
|
|
564
|
+
<context.instruction>following part contains context information injected by the system. Please follow these instructions:
|
|
565
|
+
|
|
566
|
+
1. Always prioritize handling user-visible content.
|
|
567
|
+
2. the context is only required when user's queries rely on it.
|
|
568
|
+
</context.instruction>
|
|
569
|
+
<user_page_selections>
|
|
570
|
+
<user_selections count="1">
|
|
571
|
+
<selection >
|
|
572
|
+
<p>Selected paragraph 1</p>
|
|
573
|
+
</selection>
|
|
574
|
+
</user_selections>
|
|
575
|
+
</user_page_selections>
|
|
576
|
+
<!-- END SYSTEM CONTEXT -->`,
|
|
577
|
+
role: 'user',
|
|
578
|
+
},
|
|
579
|
+
{ content: 'Answer to first', role: 'assistant' },
|
|
580
|
+
{
|
|
581
|
+
content: `Second question with different selection
|
|
582
|
+
|
|
583
|
+
<!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->
|
|
584
|
+
<context.instruction>following part contains context information injected by the system. Please follow these instructions:
|
|
585
|
+
|
|
586
|
+
1. Always prioritize handling user-visible content.
|
|
587
|
+
2. the context is only required when user's queries rely on it.
|
|
588
|
+
</context.instruction>
|
|
589
|
+
<user_page_selections>
|
|
590
|
+
<user_selections count="1">
|
|
591
|
+
<selection >
|
|
592
|
+
<p>Selected paragraph 2</p>
|
|
593
|
+
</selection>
|
|
594
|
+
</user_selections>
|
|
595
|
+
</user_page_selections>
|
|
596
|
+
<current_page_context>
|
|
597
|
+
<current_page title="Doc">
|
|
598
|
+
<markdown chars="5" lines="1">
|
|
599
|
+
# Doc
|
|
600
|
+
</markdown>
|
|
601
|
+
</current_page>
|
|
602
|
+
</current_page_context>
|
|
603
|
+
<!-- END SYSTEM CONTEXT -->`,
|
|
604
|
+
role: 'user',
|
|
605
|
+
},
|
|
606
|
+
]);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it('should skip user messages without pageSelections', async () => {
|
|
610
|
+
const messages: UIChatMessage[] = [
|
|
611
|
+
{
|
|
612
|
+
content: 'No selection here',
|
|
613
|
+
createdAt: Date.now(),
|
|
614
|
+
id: 'msg-1',
|
|
615
|
+
role: 'user',
|
|
616
|
+
updatedAt: Date.now(),
|
|
617
|
+
} as UIChatMessage,
|
|
618
|
+
{
|
|
619
|
+
content: 'Answer',
|
|
620
|
+
createdAt: Date.now(),
|
|
621
|
+
id: 'msg-2',
|
|
622
|
+
role: 'assistant',
|
|
623
|
+
updatedAt: Date.now(),
|
|
624
|
+
} as UIChatMessage,
|
|
625
|
+
{
|
|
626
|
+
content: 'With selection',
|
|
627
|
+
createdAt: Date.now(),
|
|
628
|
+
id: 'msg-3',
|
|
629
|
+
metadata: {
|
|
630
|
+
pageSelections: [
|
|
631
|
+
{
|
|
632
|
+
content: 'Selected text',
|
|
633
|
+
id: 'sel-1',
|
|
634
|
+
pageId: 'page-1',
|
|
635
|
+
xml: '<span>Selected text</span>',
|
|
636
|
+
},
|
|
637
|
+
],
|
|
638
|
+
},
|
|
639
|
+
role: 'user',
|
|
640
|
+
updatedAt: Date.now(),
|
|
641
|
+
} as UIChatMessage,
|
|
642
|
+
];
|
|
643
|
+
|
|
644
|
+
const params = createBasicParams({
|
|
645
|
+
messages,
|
|
646
|
+
pageContentContext: {
|
|
647
|
+
markdown: '# Doc',
|
|
648
|
+
metadata: { title: 'Doc' },
|
|
649
|
+
},
|
|
650
|
+
});
|
|
651
|
+
const engine = new MessagesEngine(params);
|
|
652
|
+
|
|
653
|
+
const result = await engine.process();
|
|
654
|
+
|
|
655
|
+
expect(result.messages).toEqual([
|
|
656
|
+
{ content: 'No selection here', role: 'user' },
|
|
657
|
+
{ content: 'Answer', role: 'assistant' },
|
|
658
|
+
{
|
|
659
|
+
content: `With selection
|
|
660
|
+
|
|
661
|
+
<!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->
|
|
662
|
+
<context.instruction>following part contains context information injected by the system. Please follow these instructions:
|
|
663
|
+
|
|
664
|
+
1. Always prioritize handling user-visible content.
|
|
665
|
+
2. the context is only required when user's queries rely on it.
|
|
666
|
+
</context.instruction>
|
|
667
|
+
<user_page_selections>
|
|
668
|
+
<user_selections count="1">
|
|
669
|
+
<selection >
|
|
670
|
+
<span>Selected text</span>
|
|
671
|
+
</selection>
|
|
672
|
+
</user_selections>
|
|
673
|
+
</user_page_selections>
|
|
674
|
+
<current_page_context>
|
|
675
|
+
<current_page title="Doc">
|
|
676
|
+
<markdown chars="5" lines="1">
|
|
677
|
+
# Doc
|
|
678
|
+
</markdown>
|
|
679
|
+
</current_page>
|
|
680
|
+
</current_page_context>
|
|
681
|
+
<!-- END SYSTEM CONTEXT -->`,
|
|
682
|
+
role: 'user',
|
|
683
|
+
},
|
|
684
|
+
]);
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it('should have only one SYSTEM CONTEXT wrapper when both selections and page content are injected', async () => {
|
|
688
|
+
const messages: UIChatMessage[] = [
|
|
689
|
+
{
|
|
690
|
+
content: 'Question about selection',
|
|
691
|
+
createdAt: Date.now(),
|
|
692
|
+
id: 'msg-1',
|
|
693
|
+
metadata: {
|
|
694
|
+
pageSelections: [
|
|
695
|
+
{
|
|
696
|
+
content: 'Selected text',
|
|
697
|
+
id: 'sel-1',
|
|
698
|
+
pageId: 'page-1',
|
|
699
|
+
xml: '<p>Selected text</p>',
|
|
700
|
+
},
|
|
701
|
+
],
|
|
702
|
+
},
|
|
703
|
+
role: 'user',
|
|
704
|
+
updatedAt: Date.now(),
|
|
705
|
+
} as UIChatMessage,
|
|
706
|
+
];
|
|
707
|
+
|
|
708
|
+
const params = createBasicParams({
|
|
709
|
+
messages,
|
|
710
|
+
pageContentContext: {
|
|
711
|
+
markdown: '# Full Document',
|
|
712
|
+
metadata: { title: 'Full Doc' },
|
|
713
|
+
},
|
|
714
|
+
});
|
|
715
|
+
const engine = new MessagesEngine(params);
|
|
716
|
+
|
|
717
|
+
const result = await engine.process();
|
|
718
|
+
|
|
719
|
+
expect(result.messages).toEqual([
|
|
720
|
+
{
|
|
721
|
+
content: `Question about selection
|
|
722
|
+
|
|
723
|
+
<!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->
|
|
724
|
+
<context.instruction>following part contains context information injected by the system. Please follow these instructions:
|
|
725
|
+
|
|
726
|
+
1. Always prioritize handling user-visible content.
|
|
727
|
+
2. the context is only required when user's queries rely on it.
|
|
728
|
+
</context.instruction>
|
|
729
|
+
<user_page_selections>
|
|
730
|
+
<user_selections count="1">
|
|
731
|
+
<selection >
|
|
732
|
+
<p>Selected text</p>
|
|
733
|
+
</selection>
|
|
734
|
+
</user_selections>
|
|
735
|
+
</user_page_selections>
|
|
736
|
+
<current_page_context>
|
|
737
|
+
<current_page title="Full Doc">
|
|
738
|
+
<markdown chars="15" lines="1">
|
|
739
|
+
# Full Document
|
|
740
|
+
</markdown>
|
|
741
|
+
</current_page>
|
|
742
|
+
</current_page_context>
|
|
743
|
+
<!-- END SYSTEM CONTEXT -->`,
|
|
744
|
+
role: 'user',
|
|
745
|
+
},
|
|
746
|
+
]);
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
it('should not inject selections when page editor is not enabled', async () => {
|
|
750
|
+
const messages: UIChatMessage[] = [
|
|
751
|
+
{
|
|
752
|
+
content: 'Question',
|
|
753
|
+
createdAt: Date.now(),
|
|
754
|
+
id: 'msg-1',
|
|
755
|
+
metadata: {
|
|
756
|
+
pageSelections: [
|
|
757
|
+
{ content: 'Selected', id: 'sel-1', pageId: 'page-1', xml: '<p>Selected</p>' },
|
|
758
|
+
],
|
|
759
|
+
},
|
|
760
|
+
role: 'user',
|
|
761
|
+
updatedAt: Date.now(),
|
|
762
|
+
} as UIChatMessage,
|
|
763
|
+
];
|
|
764
|
+
|
|
765
|
+
// No pageContentContext or initialContext.pageEditor means not enabled
|
|
766
|
+
const params = createBasicParams({ messages });
|
|
767
|
+
const engine = new MessagesEngine(params);
|
|
768
|
+
|
|
769
|
+
const result = await engine.process();
|
|
770
|
+
|
|
771
|
+
expect(result.messages).toEqual([{ content: 'Question', role: 'user' }]);
|
|
772
|
+
});
|
|
773
|
+
});
|
|
410
774
|
});
|
|
@@ -20,6 +20,9 @@ export interface PageEditorContextInjectorConfig {
|
|
|
20
20
|
* Page Editor Context Injector
|
|
21
21
|
* Responsible for injecting current page context at the end of the last user message
|
|
22
22
|
* This ensures the model receives the most up-to-date page/document state
|
|
23
|
+
*
|
|
24
|
+
* Note: Page selections (user-selected text regions) are handled separately by
|
|
25
|
+
* PageSelectionsInjector, which injects selections into each user message that has them
|
|
23
26
|
*/
|
|
24
27
|
export class PageEditorContextInjector extends BaseLastUserContentProvider {
|
|
25
28
|
readonly name = 'PageEditorContextInjector';
|
|
@@ -37,20 +40,11 @@ export class PageEditorContextInjector extends BaseLastUserContentProvider {
|
|
|
37
40
|
|
|
38
41
|
const clonedContext = this.cloneContext(context);
|
|
39
42
|
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
log('Page Editor not enabled or no pageContentContext, skipping injection');
|
|
43
|
-
return this.markAsExecuted(clonedContext);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Format page content context
|
|
47
|
-
const formattedContent = formatPageContentContext(this.config.pageContentContext);
|
|
48
|
-
|
|
49
|
-
log('Formatted content length:', formattedContent.length);
|
|
43
|
+
// Check if we have page content to inject
|
|
44
|
+
const hasPageContent = this.config.enabled && this.config.pageContentContext;
|
|
50
45
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
log('No content to inject after formatting');
|
|
46
|
+
if (!hasPageContent) {
|
|
47
|
+
log('No pageContentContext, skipping injection');
|
|
54
48
|
return this.markAsExecuted(clonedContext);
|
|
55
49
|
}
|
|
56
50
|
|
|
@@ -64,6 +58,16 @@ export class PageEditorContextInjector extends BaseLastUserContentProvider {
|
|
|
64
58
|
return this.markAsExecuted(clonedContext);
|
|
65
59
|
}
|
|
66
60
|
|
|
61
|
+
// Format page content
|
|
62
|
+
const formattedContent = formatPageContentContext(this.config.pageContentContext!);
|
|
63
|
+
|
|
64
|
+
if (!formattedContent) {
|
|
65
|
+
log('No content to inject after formatting');
|
|
66
|
+
return this.markAsExecuted(clonedContext);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
log('Page content formatted, length:', formattedContent.length);
|
|
70
|
+
|
|
67
71
|
// Check if system context wrapper already exists
|
|
68
72
|
// If yes, only insert context block; if no, use full wrapper
|
|
69
73
|
const hasExistingWrapper = this.hasExistingSystemContext(clonedContext);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { formatPageSelections } from '@lobechat/prompts';
|
|
2
|
+
import type { PageSelection } from '@lobechat/types';
|
|
3
|
+
import debug from 'debug';
|
|
4
|
+
|
|
5
|
+
import { BaseEveryUserContentProvider } from '../base/BaseEveryUserContentProvider';
|
|
6
|
+
import type { Message, ProcessorOptions } from '../types';
|
|
7
|
+
|
|
8
|
+
const log = debug('context-engine:provider:PageSelectionsInjector');
|
|
9
|
+
|
|
10
|
+
export interface PageSelectionsInjectorConfig {
|
|
11
|
+
/** Whether Page Selections injection is enabled */
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Page Selections Injector
|
|
17
|
+
* Responsible for injecting page selections into each user message that has them
|
|
18
|
+
* Unlike PageEditorContextInjector which only injects to the last user message,
|
|
19
|
+
* this processor handles selections attached to any user message in the conversation
|
|
20
|
+
*
|
|
21
|
+
* This injector runs BEFORE PageEditorContextInjector so that:
|
|
22
|
+
* - Each user message with selections gets a SYSTEM CONTEXT wrapper
|
|
23
|
+
* - PageEditorContextInjector can then reuse the wrapper for the last user message
|
|
24
|
+
*/
|
|
25
|
+
export class PageSelectionsInjector extends BaseEveryUserContentProvider {
|
|
26
|
+
readonly name = 'PageSelectionsInjector';
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
private config: PageSelectionsInjectorConfig = {},
|
|
30
|
+
options: ProcessorOptions = {},
|
|
31
|
+
) {
|
|
32
|
+
super(options);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
protected buildContentForMessage(
|
|
36
|
+
message: Message,
|
|
37
|
+
index: number,
|
|
38
|
+
): { content: string; contextType: string } | null {
|
|
39
|
+
// Skip if not enabled
|
|
40
|
+
if (!this.config.enabled) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check if message has pageSelections in metadata
|
|
45
|
+
const pageSelections = message.metadata?.pageSelections as PageSelection[] | undefined;
|
|
46
|
+
|
|
47
|
+
if (!pageSelections || pageSelections.length === 0) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Format the selections
|
|
52
|
+
const formattedSelections = formatPageSelections(pageSelections);
|
|
53
|
+
|
|
54
|
+
if (!formattedSelections) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
log(`Building content for message at index ${index} with ${pageSelections.length} selections`);
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
content: formattedSelections,
|
|
62
|
+
contextType: 'user_page_selections',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|