@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
@@ -0,0 +1,333 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import type { PipelineContext } from '../../types';
4
+ import { PageSelectionsInjector } from '../PageSelectionsInjector';
5
+
6
+ describe('PageSelectionsInjector', () => {
7
+ const createContext = (messages: any[] = []): PipelineContext => ({
8
+ initialState: {
9
+ messages: [],
10
+ model: 'test-model',
11
+ provider: 'test-provider',
12
+ },
13
+ isAborted: false,
14
+ messages,
15
+ metadata: {
16
+ maxTokens: 4000,
17
+ model: 'test-model',
18
+ },
19
+ });
20
+
21
+ const createPageSelection = (id: string, xmlContent: string, pageId = 'page-1') => ({
22
+ content: xmlContent, // preview content
23
+ id,
24
+ pageId,
25
+ xml: xmlContent, // actual content used by formatPageSelections
26
+ });
27
+
28
+ describe('enabled/disabled', () => {
29
+ it('should skip injection when disabled', async () => {
30
+ const injector = new PageSelectionsInjector({ enabled: false });
31
+
32
+ const context = createContext([
33
+ {
34
+ content: 'Question',
35
+ metadata: {
36
+ pageSelections: [createPageSelection('sel-1', 'Selected text')],
37
+ },
38
+ role: 'user',
39
+ },
40
+ ]);
41
+
42
+ const result = await injector.process(context);
43
+
44
+ expect(result.messages[0].content).toBe('Question');
45
+ expect(result.metadata.PageSelectionsInjectorInjectedCount).toBeUndefined();
46
+ });
47
+
48
+ it('should inject when enabled', async () => {
49
+ const injector = new PageSelectionsInjector({ enabled: true });
50
+
51
+ const context = createContext([
52
+ {
53
+ content: 'Question',
54
+ metadata: {
55
+ pageSelections: [createPageSelection('sel-1', 'Selected text')],
56
+ },
57
+ role: 'user',
58
+ },
59
+ ]);
60
+
61
+ const result = await injector.process(context);
62
+
63
+ expect(result.messages[0].content).toContain('Question');
64
+ expect(result.messages[0].content).toContain('<user_page_selections>');
65
+ expect(result.messages[0].content).toContain('Selected text');
66
+ });
67
+ });
68
+
69
+ describe('injection to every user message', () => {
70
+ it('should inject selections to each user message that has them', async () => {
71
+ const injector = new PageSelectionsInjector({ enabled: true });
72
+
73
+ const context = createContext([
74
+ {
75
+ content: 'First question',
76
+ metadata: {
77
+ pageSelections: [createPageSelection('sel-1', 'First selection')],
78
+ },
79
+ role: 'user',
80
+ },
81
+ { content: 'First answer', role: 'assistant' },
82
+ {
83
+ content: 'Second question',
84
+ metadata: {
85
+ pageSelections: [createPageSelection('sel-2', 'Second selection')],
86
+ },
87
+ role: 'user',
88
+ },
89
+ { content: 'Second answer', role: 'assistant' },
90
+ {
91
+ content: 'Third question without selection',
92
+ role: 'user',
93
+ },
94
+ ]);
95
+
96
+ const result = await injector.process(context);
97
+
98
+ // First user message should have first selection
99
+ expect(result.messages[0].content).toContain('First question');
100
+ expect(result.messages[0].content).toContain('First selection');
101
+ expect(result.messages[0].content).toContain('<user_page_selections>');
102
+
103
+ // Second user message should have second selection
104
+ expect(result.messages[2].content).toContain('Second question');
105
+ expect(result.messages[2].content).toContain('Second selection');
106
+ expect(result.messages[2].content).toContain('<user_page_selections>');
107
+
108
+ // Third user message should NOT have injection (no selections)
109
+ expect(result.messages[4].content).toBe('Third question without selection');
110
+
111
+ // Assistant messages should be unchanged
112
+ expect(result.messages[1].content).toBe('First answer');
113
+ expect(result.messages[3].content).toBe('Second answer');
114
+
115
+ // Metadata should show 2 injections
116
+ expect(result.metadata.PageSelectionsInjectorInjectedCount).toBe(2);
117
+ });
118
+
119
+ it('should skip user messages without pageSelections', async () => {
120
+ const injector = new PageSelectionsInjector({ enabled: true });
121
+
122
+ const context = createContext([
123
+ { content: 'No selections here', role: 'user' },
124
+ { content: 'Answer', role: 'assistant' },
125
+ {
126
+ content: 'With selections',
127
+ metadata: {
128
+ pageSelections: [createPageSelection('sel-1', 'Some text')],
129
+ },
130
+ role: 'user',
131
+ },
132
+ ]);
133
+
134
+ const result = await injector.process(context);
135
+
136
+ expect(result.messages[0].content).toBe('No selections here');
137
+ expect(result.messages[2].content).toContain('With selections');
138
+ expect(result.messages[2].content).toContain('Some text');
139
+ });
140
+
141
+ it('should skip user messages with empty pageSelections array', async () => {
142
+ const injector = new PageSelectionsInjector({ enabled: true });
143
+
144
+ const context = createContext([
145
+ {
146
+ content: 'Empty selections',
147
+ metadata: { pageSelections: [] },
148
+ role: 'user',
149
+ },
150
+ ]);
151
+
152
+ const result = await injector.process(context);
153
+
154
+ expect(result.messages[0].content).toBe('Empty selections');
155
+ });
156
+ });
157
+
158
+ describe('SYSTEM CONTEXT wrapper', () => {
159
+ it('should wrap selection content with SYSTEM CONTEXT markers', async () => {
160
+ const injector = new PageSelectionsInjector({ enabled: true });
161
+
162
+ const context = createContext([
163
+ {
164
+ content: 'Question',
165
+ metadata: {
166
+ pageSelections: [createPageSelection('sel-1', 'Selected text')],
167
+ },
168
+ role: 'user',
169
+ },
170
+ ]);
171
+
172
+ const result = await injector.process(context);
173
+ const content = result.messages[0].content as string;
174
+
175
+ expect(content).toContain('<!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->');
176
+ expect(content).toContain('<context.instruction>');
177
+ expect(content).toContain('<user_page_selections>');
178
+ expect(content).toContain('</user_page_selections>');
179
+ expect(content).toContain('<!-- END SYSTEM CONTEXT -->');
180
+ });
181
+
182
+ it('should have only one SYSTEM CONTEXT wrapper per message even with multiple selections', async () => {
183
+ const injector = new PageSelectionsInjector({ enabled: true });
184
+
185
+ const context = createContext([
186
+ {
187
+ content: 'Question',
188
+ metadata: {
189
+ pageSelections: [
190
+ createPageSelection('sel-1', 'First selection'),
191
+ createPageSelection('sel-2', 'Second selection'),
192
+ ],
193
+ },
194
+ role: 'user',
195
+ },
196
+ ]);
197
+
198
+ const result = await injector.process(context);
199
+ const content = result.messages[0].content as string;
200
+
201
+ const startCount = (content.match(/<!-- SYSTEM CONTEXT/g) || []).length;
202
+ const endCount = (content.match(/<!-- END SYSTEM CONTEXT/g) || []).length;
203
+
204
+ expect(startCount).toBe(1);
205
+ expect(endCount).toBe(1);
206
+ });
207
+
208
+ it('should create separate SYSTEM CONTEXT wrappers for each user message', async () => {
209
+ const injector = new PageSelectionsInjector({ enabled: true });
210
+
211
+ const context = createContext([
212
+ {
213
+ content: 'First question',
214
+ metadata: {
215
+ pageSelections: [createPageSelection('sel-1', 'First selection')],
216
+ },
217
+ role: 'user',
218
+ },
219
+ { content: 'Answer', role: 'assistant' },
220
+ {
221
+ content: 'Second question',
222
+ metadata: {
223
+ pageSelections: [createPageSelection('sel-2', 'Second selection')],
224
+ },
225
+ role: 'user',
226
+ },
227
+ ]);
228
+
229
+ const result = await injector.process(context);
230
+
231
+ // Each user message should have its own SYSTEM CONTEXT wrapper
232
+ const firstContent = result.messages[0].content as string;
233
+ const secondContent = result.messages[2].content as string;
234
+
235
+ expect(firstContent).toContain('<!-- SYSTEM CONTEXT');
236
+ expect(firstContent).toContain('First selection');
237
+
238
+ expect(secondContent).toContain('<!-- SYSTEM CONTEXT');
239
+ expect(secondContent).toContain('Second selection');
240
+ });
241
+ });
242
+
243
+ describe('multimodal messages', () => {
244
+ it('should handle array content with text parts', async () => {
245
+ const injector = new PageSelectionsInjector({ enabled: true });
246
+
247
+ const context = createContext([
248
+ {
249
+ content: [
250
+ { text: 'Question with image', type: 'text' },
251
+ { image_url: { url: 'http://example.com/img.png' }, type: 'image_url' },
252
+ ],
253
+ metadata: {
254
+ pageSelections: [createPageSelection('sel-1', 'Selected text')],
255
+ },
256
+ role: 'user',
257
+ },
258
+ ]);
259
+
260
+ const result = await injector.process(context);
261
+
262
+ expect(result.messages[0].content[0].text).toContain('Question with image');
263
+ expect(result.messages[0].content[0].text).toContain('Selected text');
264
+ expect(result.messages[0].content[0].text).toContain('<user_page_selections>');
265
+ expect(result.messages[0].content[1]).toEqual({
266
+ image_url: { url: 'http://example.com/img.png' },
267
+ type: 'image_url',
268
+ });
269
+ });
270
+ });
271
+
272
+ describe('integration with PageEditorContextInjector', () => {
273
+ it('should create wrapper that PageEditorContextInjector can reuse', async () => {
274
+ const injector = new PageSelectionsInjector({ enabled: true });
275
+
276
+ const context = createContext([
277
+ {
278
+ content: 'Question about the page',
279
+ metadata: {
280
+ pageSelections: [createPageSelection('sel-1', 'Selected paragraph')],
281
+ },
282
+ role: 'user',
283
+ },
284
+ ]);
285
+
286
+ const result = await injector.process(context);
287
+ const content = result.messages[0].content as string;
288
+
289
+ // Verify the wrapper structure is correct for reuse
290
+ expect(content).toContain('<!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->');
291
+ expect(content).toContain('<context.instruction>');
292
+ expect(content).toContain('<!-- END SYSTEM CONTEXT -->');
293
+
294
+ // Verify the content is in the right position (between instruction and end marker)
295
+ const instructionIndex = content.indexOf('</context.instruction>');
296
+ const selectionsIndex = content.indexOf('<user_page_selections>');
297
+ const endIndex = content.indexOf('<!-- END SYSTEM CONTEXT -->');
298
+
299
+ expect(instructionIndex).toBeLessThan(selectionsIndex);
300
+ expect(selectionsIndex).toBeLessThan(endIndex);
301
+ });
302
+ });
303
+
304
+ describe('metadata', () => {
305
+ it('should set metadata when injections are made', async () => {
306
+ const injector = new PageSelectionsInjector({ enabled: true });
307
+
308
+ const context = createContext([
309
+ {
310
+ content: 'Question',
311
+ metadata: {
312
+ pageSelections: [createPageSelection('sel-1', 'Text')],
313
+ },
314
+ role: 'user',
315
+ },
316
+ ]);
317
+
318
+ const result = await injector.process(context);
319
+
320
+ expect(result.metadata.PageSelectionsInjectorInjectedCount).toBe(1);
321
+ });
322
+
323
+ it('should not set metadata when no injections are made', async () => {
324
+ const injector = new PageSelectionsInjector({ enabled: true });
325
+
326
+ const context = createContext([{ content: 'No selections', role: 'user' }]);
327
+
328
+ const result = await injector.process(context);
329
+
330
+ expect(result.metadata.PageSelectionsInjectorInjectedCount).toBeUndefined();
331
+ });
332
+ });
333
+ });
@@ -7,6 +7,7 @@ export { GTDTodoInjector } from './GTDTodoInjector';
7
7
  export { HistorySummaryProvider } from './HistorySummary';
8
8
  export { KnowledgeInjector } from './KnowledgeInjector';
9
9
  export { PageEditorContextInjector } from './PageEditorContextInjector';
10
+ export { PageSelectionsInjector } from './PageSelectionsInjector';
10
11
  export { SystemRoleInjector } from './SystemRoleInjector';
11
12
  export { ToolSystemRoleProvider } from './ToolSystemRole';
12
13
  export { UserMemoryInjector } from './UserMemoryInjector';
@@ -27,11 +28,12 @@ export type {
27
28
  GroupContextInjectorConfig,
28
29
  GroupMemberInfo as GroupContextMemberInfo,
29
30
  } from './GroupContextInjector';
30
- export type { HistorySummaryConfig } from './HistorySummary';
31
31
  export type { GTDPlan, GTDPlanInjectorConfig } from './GTDPlanInjector';
32
32
  export type { GTDTodoInjectorConfig, GTDTodoItem, GTDTodoList } from './GTDTodoInjector';
33
+ export type { HistorySummaryConfig } from './HistorySummary';
33
34
  export type { KnowledgeInjectorConfig } from './KnowledgeInjector';
34
35
  export type { PageEditorContextInjectorConfig } from './PageEditorContextInjector';
36
+ export type { PageSelectionsInjectorConfig } from './PageSelectionsInjector';
35
37
  export type { SystemRoleInjectorConfig } from './SystemRoleInjector';
36
38
  export type { ToolSystemRoleConfig } from './ToolSystemRole';
37
39
  export type { UserMemoryInjectorConfig } from './UserMemoryInjector';
@@ -1,5 +1,6 @@
1
1
  import { AssociatedObjectSchema } from '@lobechat/memory-user-memory';
2
2
  import {
3
+ ActivityTypeEnum,
3
4
  IdentityTypeEnum,
4
5
  LayersEnum,
5
6
  MemorySourceType,
@@ -40,7 +41,10 @@ import {
40
41
  UserMemoryIdentity,
41
42
  UserMemoryItem,
42
43
  UserMemoryPreference,
44
+ UserMemoryActivitiesWithoutVectors,
45
+ UserMemoryActivity,
43
46
  userMemories,
47
+ userMemoriesActivities,
44
48
  userMemoriesContexts,
45
49
  userMemoriesExperiences,
46
50
  userMemoriesIdentities,
@@ -104,6 +108,16 @@ export interface CreateUserMemoryContextParams extends BaseCreateUserMemoryParam
104
108
  >;
105
109
  }
106
110
 
111
+ export interface CreateUserMemoryActivityParams extends BaseCreateUserMemoryParams {
112
+ activity: Optional<
113
+ Omit<
114
+ UserMemoryActivity,
115
+ 'id' | 'userId' | 'createdAt' | 'updatedAt' | 'accessedAt' | 'userMemoryId'
116
+ >,
117
+ 'capturedAt'
118
+ >;
119
+ }
120
+
107
121
  export interface CreateUserMemoryExperienceParams extends BaseCreateUserMemoryParams {
108
122
  experience: Optional<
109
123
  Omit<
@@ -135,6 +149,7 @@ export interface CreateUserMemoryPreferenceParams extends BaseCreateUserMemoryPa
135
149
  }
136
150
 
137
151
  export type CreateUserMemoryParams =
152
+ | CreateUserMemoryActivityParams
138
153
  | CreateUserMemoryContextParams
139
154
  | CreateUserMemoryExperienceParams
140
155
  | CreateUserMemoryIdentityParams
@@ -143,7 +158,7 @@ export type CreateUserMemoryParams =
143
158
  export interface SearchUserMemoryParams {
144
159
  embedding?: number[];
145
160
  limit?: number;
146
- limits?: Partial<Record<'contexts' | 'experiences' | 'preferences', number>>;
161
+ limits?: Partial<Record<'activities' | 'contexts' | 'experiences' | 'preferences', number>>;
147
162
  memoryCategory?: string;
148
163
  memoryType?: string;
149
164
  query?: string;
@@ -151,12 +166,13 @@ export interface SearchUserMemoryParams {
151
166
 
152
167
  export interface SearchUserMemoryWithEmbeddingParams {
153
168
  embedding?: number[];
154
- limits?: Partial<Record<'contexts' | 'experiences' | 'preferences', number>>;
169
+ limits?: Partial<Record<'activities' | 'contexts' | 'experiences' | 'preferences', number>>;
155
170
  memoryCategory?: string;
156
171
  memoryType?: string;
157
172
  }
158
173
 
159
174
  export interface UserMemorySearchAggregatedResult {
175
+ activities: UserMemoryActivitiesWithoutVectors[];
160
176
  contexts: UserMemoryContextWithoutVectors[];
161
177
  experiences: UserMemoryExperienceWithoutVectors[];
162
178
  preferences: UserMemoryPreferenceWithoutVectors[];
@@ -380,12 +396,67 @@ export class UserMemoryModel {
380
396
  const extra = JSON.parse(parsed.data.extra || '{}');
381
397
  parsed.data.extra = extra;
382
398
  associations.push(parsed.data);
399
+ return;
400
+ }
401
+
402
+ if (
403
+ item &&
404
+ typeof item === 'object' &&
405
+ 'name' in item &&
406
+ typeof (item as any).name === 'string'
407
+ ) {
408
+ associations.push({ name: (item as any).name });
383
409
  }
384
410
  });
385
411
 
386
412
  return associations.length > 0 ? associations : [];
387
413
  }
388
414
 
415
+ static parseAssociatedLocations(
416
+ value?:
417
+ | {
418
+ address?: unknown;
419
+ name?: unknown;
420
+ tags?: unknown;
421
+ type?: unknown;
422
+ }[]
423
+ | Record<string, unknown>,
424
+ ) {
425
+ if (!value) return [];
426
+
427
+ const raw = Array.isArray(value) ? value : [value];
428
+ const locations: {
429
+ address?: string;
430
+ name?: string;
431
+ tags?: string[];
432
+ type?: string;
433
+ }[] = [];
434
+
435
+ raw.forEach((item) => {
436
+ if (!item || typeof item !== 'object') return;
437
+
438
+ const address = typeof (item as any).address === 'string' ? (item as any).address : undefined;
439
+ const name = typeof (item as any).name === 'string' ? (item as any).name : undefined;
440
+ const type = typeof (item as any).type === 'string' ? (item as any).type : undefined;
441
+ const tagsRaw = (item as any).tags;
442
+ const tags =
443
+ Array.isArray(tagsRaw) && tagsRaw.every((tag) => typeof tag === 'string')
444
+ ? (tagsRaw as string[])
445
+ : undefined;
446
+
447
+ if (address || name || type || tags) {
448
+ locations.push({
449
+ address,
450
+ name,
451
+ tags,
452
+ type,
453
+ });
454
+ }
455
+ });
456
+
457
+ return locations;
458
+ }
459
+
389
460
  static parseDateFromString(value?: string | Date | null): Date | null {
390
461
  if (!value) return null;
391
462
  if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
@@ -537,6 +608,46 @@ export class UserMemoryModel {
537
608
  });
538
609
  };
539
610
 
611
+ createActivityMemory = async (
612
+ params: CreateUserMemoryActivityParams,
613
+ ): Promise<{ activity: UserMemoryActivity; memory: UserMemoryItem }> => {
614
+ return this.db.transaction(async (tx) => {
615
+ const baseValues = this.buildBaseMemoryInsertValues(params, {
616
+ metadata: params.activity.metadata ?? null,
617
+ status: params.activity.status ?? 'pending',
618
+ tags: params.activity.tags ?? null,
619
+ });
620
+
621
+ const [memory] = await tx.insert(userMemories).values(baseValues).returning();
622
+ if (!memory) throw new Error('Failed to create user memory activity');
623
+
624
+ const activityValues = {
625
+ associatedLocations: params.activity.associatedLocations ?? null,
626
+ associatedObjects: params.activity.associatedObjects ?? [],
627
+ associatedSubjects: params.activity.associatedSubjects ?? [],
628
+ capturedAt: params.activity.capturedAt,
629
+ endsAt: coerceDate(params.activity.endsAt),
630
+ feedback: params.activity.feedback ?? null,
631
+ feedbackVector: params.activity.feedbackVector ?? null,
632
+ metadata: params.activity.metadata ?? null,
633
+ narrative: params.activity.narrative ?? null,
634
+ narrativeVector: params.activity.narrativeVector ?? null,
635
+ notes: params.activity.notes ?? null,
636
+ startsAt: coerceDate(params.activity.startsAt),
637
+ status: params.activity.status ?? null,
638
+ tags: params.activity.tags ?? [],
639
+ timezone: params.activity.timezone ?? null,
640
+ type: params.activity.type ?? ActivityTypeEnum.Other,
641
+ userId: this.userId,
642
+ userMemoryId: memory.id,
643
+ } satisfies typeof userMemoriesActivities.$inferInsert;
644
+
645
+ const [activity] = await tx.insert(userMemoriesActivities).values(activityValues).returning();
646
+
647
+ return { activity, memory };
648
+ });
649
+ };
650
+
540
651
  createPreferenceMemory = async (
541
652
  params: CreateUserMemoryPreferenceParams,
542
653
  ): Promise<{ memory: UserMemoryItem; preference: UserMemoryPreference }> => {
@@ -575,12 +686,13 @@ export class UserMemoryModel {
575
686
  const { embedding, limits } = params;
576
687
 
577
688
  const resolvedLimits = {
689
+ activities: limits?.activities,
578
690
  contexts: limits?.contexts,
579
691
  experiences: limits?.experiences,
580
692
  preferences: limits?.preferences,
581
693
  };
582
694
 
583
- const [experiences, contexts, preferences] = await Promise.all([
695
+ const [experiences, contexts, preferences, activities] = await Promise.all([
584
696
  this.searchExperiences({
585
697
  embedding,
586
698
  limit: resolvedLimits.experiences,
@@ -593,6 +705,10 @@ export class UserMemoryModel {
593
705
  embedding,
594
706
  limit: resolvedLimits.preferences,
595
707
  }),
708
+ this.searchActivities({
709
+ embedding,
710
+ limit: resolvedLimits.activities,
711
+ }),
596
712
  ]);
597
713
 
598
714
  const accessedMemoryIds = new Set<string>();
@@ -602,6 +718,9 @@ export class UserMemoryModel {
602
718
  preferences.forEach((preference) => {
603
719
  if (preference.userMemoryId) accessedMemoryIds.add(preference.userMemoryId);
604
720
  });
721
+ activities.forEach((activity) => {
722
+ if (activity.userMemoryId) accessedMemoryIds.add(activity.userMemoryId);
723
+ });
605
724
  const contextLinkIds: string[] = [];
606
725
  contexts.forEach((context) => {
607
726
  const ids = Array.isArray(context.userMemoryIds) ? (context.userMemoryIds as string[]) : [];
@@ -617,6 +736,7 @@ export class UserMemoryModel {
617
736
  }
618
737
 
619
738
  return {
739
+ activities,
620
740
  contexts,
621
741
  experiences,
622
742
  preferences,
@@ -1795,6 +1915,61 @@ export class UserMemoryModel {
1795
1915
  await this.db.delete(userMemories).where(eq(userMemories.userId, this.userId));
1796
1916
  };
1797
1917
 
1918
+ searchActivities = async (params: {
1919
+ embedding?: number[];
1920
+ limit?: number;
1921
+ type?: string;
1922
+ }): Promise<UserMemoryActivitiesWithoutVectors[]> => {
1923
+ const { embedding, limit = 5, type } = params;
1924
+ if (limit <= 0) {
1925
+ return [];
1926
+ }
1927
+
1928
+ let query = this.db
1929
+ .select({
1930
+ accessedAt: userMemoriesActivities.accessedAt,
1931
+ associatedLocations: userMemoriesActivities.associatedLocations,
1932
+ associatedObjects: userMemoriesActivities.associatedObjects,
1933
+ associatedSubjects: userMemoriesActivities.associatedSubjects,
1934
+ capturedAt: userMemoriesActivities.capturedAt,
1935
+ createdAt: userMemoriesActivities.createdAt,
1936
+ endsAt: userMemoriesActivities.endsAt,
1937
+ feedback: userMemoriesActivities.feedback,
1938
+ id: userMemoriesActivities.id,
1939
+ metadata: userMemoriesActivities.metadata,
1940
+ narrative: userMemoriesActivities.narrative,
1941
+ notes: userMemoriesActivities.notes,
1942
+ startsAt: userMemoriesActivities.startsAt,
1943
+ status: userMemoriesActivities.status,
1944
+ tags: userMemoriesActivities.tags,
1945
+ timezone: userMemoriesActivities.timezone,
1946
+ type: userMemoriesActivities.type,
1947
+ updatedAt: userMemoriesActivities.updatedAt,
1948
+ userId: userMemoriesActivities.userId,
1949
+ userMemoryId: userMemoriesActivities.userMemoryId,
1950
+ ...(embedding && {
1951
+ similarity: sql<number>`1 - (${cosineDistance(userMemoriesActivities.narrativeVector, embedding)}) AS similarity`,
1952
+ }),
1953
+ })
1954
+ .from(userMemoriesActivities)
1955
+ .$dynamic();
1956
+
1957
+ const conditions = [eq(userMemoriesActivities.userId, this.userId)];
1958
+ if (type) {
1959
+ conditions.push(eq(userMemoriesActivities.type, type));
1960
+ }
1961
+
1962
+ query = query.where(and(...conditions));
1963
+
1964
+ if (embedding) {
1965
+ query = query.orderBy(desc(sql`similarity`));
1966
+ } else {
1967
+ query = query.orderBy(desc(userMemoriesActivities.createdAt));
1968
+ }
1969
+
1970
+ return query.limit(limit) as Promise<UserMemoryActivitiesWithoutVectors[]>;
1971
+ };
1972
+
1798
1973
  searchContexts = async (params: {
1799
1974
  embedding?: number[];
1800
1975
  limit?: number;
@@ -63,7 +63,7 @@ export class UserMemorySourceBenchmarkLoCoMoModel {
63
63
  return { id };
64
64
  }
65
65
 
66
- async replaceParts(sourceId: string, parts: BenchmarkLoCoMoPart[]) {
66
+ replaceParts(sourceId: string, parts: BenchmarkLoCoMoPart[]) {
67
67
  const store = this.getPartStore();
68
68
  store.delete(sourceId);
69
69
  if (!parts.length) return;
@@ -20,8 +20,8 @@
20
20
  "@lobechat/context-engine": "workspace:*",
21
21
  "@lobechat/model-runtime": "workspace:*",
22
22
  "@lobechat/prompts": "workspace:*",
23
- "dotenv": "^17.2.3",
24
23
  "dayjs": "^1.11.11",
24
+ "dotenv": "^17.2.3",
25
25
  "ora": "^9.0.0",
26
26
  "unist-builder": "^4.0.0",
27
27
  "xast-util-to-xml": "^4.0.0",
@@ -30,6 +30,7 @@
30
30
  },
31
31
  "devDependencies": {
32
32
  "@lobechat/types": "workspace:*",
33
+ "@types/json-schema": "^7.0.15",
33
34
  "@types/xast": "^2.0.4",
34
35
  "promptfoo": "^0.118.17",
35
36
  "tsx": "^4.20.6"