@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.
- package/CHANGELOG.md +50 -0
- package/Dockerfile +2 -1
- package/changelog/v1.json +14 -0
- package/locales/en-US/chat.json +3 -1
- package/locales/zh-CN/chat.json +2 -0
- package/package.json +1 -1
- package/packages/const/src/userMemory.ts +1 -0
- 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/database/src/models/userMemory/model.ts +178 -3
- package/packages/database/src/models/userMemory/sources/benchmarkLoCoMo.ts +1 -1
- package/packages/memory-user-memory/package.json +2 -1
- package/packages/memory-user-memory/promptfoo/evals/activity/basic/buildMessages.ts +40 -0
- package/packages/memory-user-memory/promptfoo/evals/activity/basic/eval.yaml +13 -0
- package/packages/memory-user-memory/promptfoo/evals/activity/basic/prompt.ts +5 -0
- package/packages/memory-user-memory/promptfoo/evals/activity/basic/tests/cases.ts +106 -0
- package/packages/memory-user-memory/promptfoo/evals/activity/locomo/buildMessages.ts +104 -0
- package/packages/memory-user-memory/promptfoo/evals/activity/locomo/eval.yaml +13 -0
- package/packages/memory-user-memory/promptfoo/evals/activity/locomo/prompt.ts +5 -0
- package/packages/memory-user-memory/promptfoo/evals/activity/locomo/tests/benchmark-locomo-payload-conv-26.json +149 -0
- package/packages/memory-user-memory/promptfoo/evals/activity/locomo/tests/cases.ts +72 -0
- package/packages/memory-user-memory/promptfoo/response-formats/activity.json +370 -0
- package/packages/memory-user-memory/promptfoo/response-formats/experience.json +14 -0
- package/packages/memory-user-memory/promptfoo/response-formats/identity.json +281 -255
- package/packages/memory-user-memory/promptfooconfig.yaml +1 -0
- package/packages/memory-user-memory/scripts/generate-response-formats.ts +26 -2
- package/packages/memory-user-memory/src/extractors/activity.ts +44 -0
- package/packages/memory-user-memory/src/extractors/gatekeeper.test.ts +2 -1
- package/packages/memory-user-memory/src/extractors/gatekeeper.ts +2 -1
- package/packages/memory-user-memory/src/extractors/index.ts +1 -0
- package/packages/memory-user-memory/src/prompts/gatekeeper.ts +3 -3
- package/packages/memory-user-memory/src/prompts/index.ts +7 -1
- package/packages/memory-user-memory/src/prompts/layers/activity.ts +90 -0
- package/packages/memory-user-memory/src/prompts/layers/index.ts +1 -0
- package/packages/memory-user-memory/src/providers/existingUserMemory.test.ts +25 -1
- package/packages/memory-user-memory/src/providers/existingUserMemory.ts +113 -0
- package/packages/memory-user-memory/src/schemas/activity.ts +315 -0
- package/packages/memory-user-memory/src/schemas/experience.ts +5 -5
- package/packages/memory-user-memory/src/schemas/gatekeeper.ts +1 -0
- package/packages/memory-user-memory/src/schemas/index.ts +1 -0
- package/packages/memory-user-memory/src/services/extractExecutor.ts +29 -0
- package/packages/memory-user-memory/src/types.ts +7 -0
- 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/packages/types/src/serverConfig.ts +1 -1
- package/packages/types/src/userMemory/layers.ts +52 -0
- package/packages/types/src/userMemory/list.ts +20 -2
- package/packages/types/src/userMemory/shared.ts +22 -1
- package/packages/types/src/userMemory/trace.ts +1 -0
- package/packages/types/src/util.ts +9 -1
- 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/libs/next/proxy/define-config.ts +1 -0
- package/src/locales/default/chat.ts +3 -2
- package/src/server/globalConfig/parseMemoryExtractionConfig.ts +7 -1
- package/src/server/routers/lambda/aiChat.ts +7 -0
- package/src/server/services/memory/userMemory/__tests__/extract.runtime.test.ts +2 -0
- package/src/server/services/memory/userMemory/extract.ts +108 -7
- 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
|
-
|
|
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"
|