@lobehub/lobehub 2.0.0-next.360 → 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.
Files changed (76) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/Dockerfile +2 -1
  3. package/changelog/v1.json +14 -0
  4. package/locales/en-US/chat.json +3 -1
  5. package/locales/zh-CN/chat.json +2 -0
  6. package/package.json +1 -1
  7. package/packages/const/src/userMemory.ts +1 -0
  8. package/packages/context-engine/src/base/BaseEveryUserContentProvider.ts +204 -0
  9. package/packages/context-engine/src/base/BaseLastUserContentProvider.ts +1 -8
  10. package/packages/context-engine/src/base/__tests__/BaseEveryUserContentProvider.test.ts +354 -0
  11. package/packages/context-engine/src/base/constants.ts +20 -0
  12. package/packages/context-engine/src/engine/messages/MessagesEngine.ts +27 -23
  13. package/packages/context-engine/src/engine/messages/__tests__/MessagesEngine.test.ts +364 -0
  14. package/packages/context-engine/src/providers/PageEditorContextInjector.ts +17 -13
  15. package/packages/context-engine/src/providers/PageSelectionsInjector.ts +65 -0
  16. package/packages/context-engine/src/providers/__tests__/PageSelectionsInjector.test.ts +333 -0
  17. package/packages/context-engine/src/providers/index.ts +3 -1
  18. package/packages/database/src/models/userMemory/model.ts +178 -3
  19. package/packages/database/src/models/userMemory/sources/benchmarkLoCoMo.ts +1 -1
  20. package/packages/memory-user-memory/package.json +2 -1
  21. package/packages/memory-user-memory/promptfoo/evals/activity/basic/buildMessages.ts +40 -0
  22. package/packages/memory-user-memory/promptfoo/evals/activity/basic/eval.yaml +13 -0
  23. package/packages/memory-user-memory/promptfoo/evals/activity/basic/prompt.ts +5 -0
  24. package/packages/memory-user-memory/promptfoo/evals/activity/basic/tests/cases.ts +106 -0
  25. package/packages/memory-user-memory/promptfoo/evals/activity/locomo/buildMessages.ts +104 -0
  26. package/packages/memory-user-memory/promptfoo/evals/activity/locomo/eval.yaml +13 -0
  27. package/packages/memory-user-memory/promptfoo/evals/activity/locomo/prompt.ts +5 -0
  28. package/packages/memory-user-memory/promptfoo/evals/activity/locomo/tests/benchmark-locomo-payload-conv-26.json +149 -0
  29. package/packages/memory-user-memory/promptfoo/evals/activity/locomo/tests/cases.ts +72 -0
  30. package/packages/memory-user-memory/promptfoo/response-formats/activity.json +370 -0
  31. package/packages/memory-user-memory/promptfoo/response-formats/experience.json +14 -0
  32. package/packages/memory-user-memory/promptfoo/response-formats/identity.json +281 -255
  33. package/packages/memory-user-memory/promptfooconfig.yaml +1 -0
  34. package/packages/memory-user-memory/scripts/generate-response-formats.ts +26 -2
  35. package/packages/memory-user-memory/src/extractors/activity.ts +44 -0
  36. package/packages/memory-user-memory/src/extractors/gatekeeper.test.ts +2 -1
  37. package/packages/memory-user-memory/src/extractors/gatekeeper.ts +2 -1
  38. package/packages/memory-user-memory/src/extractors/index.ts +1 -0
  39. package/packages/memory-user-memory/src/prompts/gatekeeper.ts +3 -3
  40. package/packages/memory-user-memory/src/prompts/index.ts +7 -1
  41. package/packages/memory-user-memory/src/prompts/layers/activity.ts +90 -0
  42. package/packages/memory-user-memory/src/prompts/layers/index.ts +1 -0
  43. package/packages/memory-user-memory/src/providers/existingUserMemory.test.ts +25 -1
  44. package/packages/memory-user-memory/src/providers/existingUserMemory.ts +113 -0
  45. package/packages/memory-user-memory/src/schemas/activity.ts +315 -0
  46. package/packages/memory-user-memory/src/schemas/experience.ts +5 -5
  47. package/packages/memory-user-memory/src/schemas/gatekeeper.ts +1 -0
  48. package/packages/memory-user-memory/src/schemas/index.ts +1 -0
  49. package/packages/memory-user-memory/src/services/extractExecutor.ts +29 -0
  50. package/packages/memory-user-memory/src/types.ts +7 -0
  51. package/packages/prompts/src/agents/index.ts +1 -0
  52. package/packages/prompts/src/agents/pageSelectionContext.ts +28 -0
  53. package/packages/types/src/aiChat.ts +4 -0
  54. package/packages/types/src/message/common/index.ts +1 -0
  55. package/packages/types/src/message/common/metadata.ts +8 -0
  56. package/packages/types/src/message/common/pageSelection.ts +36 -0
  57. package/packages/types/src/message/ui/params.ts +16 -0
  58. package/packages/types/src/serverConfig.ts +1 -1
  59. package/packages/types/src/userMemory/layers.ts +52 -0
  60. package/packages/types/src/userMemory/list.ts +20 -2
  61. package/packages/types/src/userMemory/shared.ts +22 -1
  62. package/packages/types/src/userMemory/trace.ts +1 -0
  63. package/packages/types/src/util.ts +9 -1
  64. package/scripts/prebuild.mts +1 -0
  65. package/src/features/ChatInput/Desktop/ContextContainer/ContextList.tsx +1 -1
  66. package/src/features/Conversation/ChatInput/index.tsx +9 -1
  67. package/src/features/Conversation/Messages/User/components/MessageContent.tsx +7 -1
  68. package/src/features/Conversation/Messages/User/components/PageSelections.tsx +62 -0
  69. package/src/features/PageEditor/EditorCanvas/useAskCopilotItem.tsx +5 -1
  70. package/src/libs/next/proxy/define-config.ts +1 -0
  71. package/src/locales/default/chat.ts +3 -2
  72. package/src/server/globalConfig/parseMemoryExtractionConfig.ts +7 -1
  73. package/src/server/routers/lambda/aiChat.ts +7 -0
  74. package/src/server/services/memory/userMemory/__tests__/extract.runtime.test.ts +2 -0
  75. package/src/server/services/memory/userMemory/extract.ts +108 -7
  76. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +5 -19
@@ -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
- // Skip if Page Editor is not enabled or no page content context
41
- if (!this.config.enabled || !this.config.pageContentContext) {
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
- // Skip if no content to inject
52
- if (!formattedContent) {
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
+ }