@lobehub/lobehub 2.0.0-next.360 → 2.0.0-next.361

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 (47) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/changelog/v1.json +5 -0
  3. package/package.json +1 -1
  4. package/packages/const/src/userMemory.ts +1 -0
  5. package/packages/database/src/models/userMemory/model.ts +178 -3
  6. package/packages/database/src/models/userMemory/sources/benchmarkLoCoMo.ts +1 -1
  7. package/packages/memory-user-memory/package.json +2 -1
  8. package/packages/memory-user-memory/promptfoo/evals/activity/basic/buildMessages.ts +40 -0
  9. package/packages/memory-user-memory/promptfoo/evals/activity/basic/eval.yaml +13 -0
  10. package/packages/memory-user-memory/promptfoo/evals/activity/basic/prompt.ts +5 -0
  11. package/packages/memory-user-memory/promptfoo/evals/activity/basic/tests/cases.ts +106 -0
  12. package/packages/memory-user-memory/promptfoo/evals/activity/locomo/buildMessages.ts +104 -0
  13. package/packages/memory-user-memory/promptfoo/evals/activity/locomo/eval.yaml +13 -0
  14. package/packages/memory-user-memory/promptfoo/evals/activity/locomo/prompt.ts +5 -0
  15. package/packages/memory-user-memory/promptfoo/evals/activity/locomo/tests/benchmark-locomo-payload-conv-26.json +149 -0
  16. package/packages/memory-user-memory/promptfoo/evals/activity/locomo/tests/cases.ts +72 -0
  17. package/packages/memory-user-memory/promptfoo/response-formats/activity.json +370 -0
  18. package/packages/memory-user-memory/promptfoo/response-formats/experience.json +14 -0
  19. package/packages/memory-user-memory/promptfoo/response-formats/identity.json +281 -255
  20. package/packages/memory-user-memory/promptfooconfig.yaml +1 -0
  21. package/packages/memory-user-memory/scripts/generate-response-formats.ts +26 -2
  22. package/packages/memory-user-memory/src/extractors/activity.ts +44 -0
  23. package/packages/memory-user-memory/src/extractors/gatekeeper.test.ts +2 -1
  24. package/packages/memory-user-memory/src/extractors/gatekeeper.ts +2 -1
  25. package/packages/memory-user-memory/src/extractors/index.ts +1 -0
  26. package/packages/memory-user-memory/src/prompts/gatekeeper.ts +3 -3
  27. package/packages/memory-user-memory/src/prompts/index.ts +7 -1
  28. package/packages/memory-user-memory/src/prompts/layers/activity.ts +90 -0
  29. package/packages/memory-user-memory/src/prompts/layers/index.ts +1 -0
  30. package/packages/memory-user-memory/src/providers/existingUserMemory.test.ts +25 -1
  31. package/packages/memory-user-memory/src/providers/existingUserMemory.ts +113 -0
  32. package/packages/memory-user-memory/src/schemas/activity.ts +315 -0
  33. package/packages/memory-user-memory/src/schemas/experience.ts +5 -5
  34. package/packages/memory-user-memory/src/schemas/gatekeeper.ts +1 -0
  35. package/packages/memory-user-memory/src/schemas/index.ts +1 -0
  36. package/packages/memory-user-memory/src/services/extractExecutor.ts +29 -0
  37. package/packages/memory-user-memory/src/types.ts +7 -0
  38. package/packages/types/src/serverConfig.ts +1 -1
  39. package/packages/types/src/userMemory/layers.ts +52 -0
  40. package/packages/types/src/userMemory/list.ts +20 -2
  41. package/packages/types/src/userMemory/shared.ts +22 -1
  42. package/packages/types/src/userMemory/trace.ts +1 -0
  43. package/packages/types/src/util.ts +9 -1
  44. package/src/libs/next/proxy/define-config.ts +1 -0
  45. package/src/server/globalConfig/parseMemoryExtractionConfig.ts +7 -1
  46. package/src/server/services/memory/userMemory/__tests__/extract.runtime.test.ts +2 -0
  47. package/src/server/services/memory/userMemory/extract.ts +108 -7
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.361](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.360...v2.0.0-next.361)
6
+
7
+ <sup>Released on **2026-01-24**</sup>
8
+
9
+ #### ✨ Features
10
+
11
+ - **userMemories**: Added memory layer activity.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's improved
19
+
20
+ - **userMemories**: Added memory layer activity, closes [#11747](https://github.com/lobehub/lobe-chat/issues/11747) ([2021b1c](https://github.com/lobehub/lobe-chat/commit/2021b1c))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ## [Version 2.0.0-next.360](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.359...v2.0.0-next.360)
6
31
 
7
32
  <sup>Released on **2026-01-24**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,9 @@
1
1
  [
2
+ {
3
+ "children": {},
4
+ "date": "2026-01-24",
5
+ "version": "2.0.0-next.361"
6
+ },
2
7
  {
3
8
  "children": {
4
9
  "fixes": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.360",
3
+ "version": "2.0.0-next.361",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -3,6 +3,7 @@ import { DEFAULT_EMBEDDING_PROVIDER } from '@lobechat/business-const';
3
3
  import { DEFAULT_EMBEDDING_MODEL } from './settings';
4
4
 
5
5
  export const DEFAULT_SEARCH_USER_MEMORY_TOP_K = {
6
+ activities: 3,
6
7
  contexts: 0,
7
8
  experiences: 0,
8
9
  preferences: 3,
@@ -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"
@@ -0,0 +1,40 @@
1
+ import { renderPlaceholderTemplate } from '@lobechat/context-engine';
2
+
3
+ import { activityPrompt } from '../../../../src/prompts';
4
+ import type { ExtractorTemplateProps } from '../../../../src/types';
5
+
6
+ export interface PromptVars extends ExtractorTemplateProps {
7
+ conversation: string;
8
+ }
9
+
10
+ export const buildActivityMessages = (vars: PromptVars) => {
11
+ const retrievedContext =
12
+ Array.isArray(vars.retrievedContexts) && vars.retrievedContexts.length > 0
13
+ ? vars.retrievedContexts.join('\n\n')
14
+ : typeof vars.retrievedContexts === 'string'
15
+ ? vars.retrievedContexts
16
+ : 'No similar memories retrieved.';
17
+
18
+ const rendered = renderPlaceholderTemplate(activityPrompt, {
19
+ availableCategories: vars.availableCategories,
20
+ language: vars.language || 'English',
21
+ retrievedContext,
22
+ sessionDate: vars.sessionDate || new Date().toISOString(),
23
+ topK: vars.topK ?? 5,
24
+ username: vars.username || 'User',
25
+ });
26
+
27
+ const messages = [
28
+ { content: rendered, role: 'system' as const },
29
+ { content: rendered, role: 'user' as const },
30
+ ];
31
+
32
+ if (vars.conversation) {
33
+ messages.push({
34
+ content: `Conversation:\n${vars.conversation}`,
35
+ role: 'user' as const,
36
+ });
37
+ }
38
+
39
+ return messages;
40
+ };
@@ -0,0 +1,13 @@
1
+ description: Regression benchmark for activity layer structured extraction
2
+
3
+ providers:
4
+ - id: openai:chat:gpt-5-mini
5
+ config:
6
+ response_format: file://../../../response-formats/activity.json
7
+ temperature: 0
8
+
9
+ prompts:
10
+ - file://./prompt.ts
11
+
12
+ tests:
13
+ - file://./tests/cases.ts
@@ -0,0 +1,5 @@
1
+ import { buildActivityMessages, PromptVars } from './buildMessages';
2
+
3
+ export default function generatePrompt({ vars }: { vars: PromptVars }) {
4
+ return buildActivityMessages(vars);
5
+ }
@@ -0,0 +1,106 @@
1
+ type PromptfooAssert =
2
+ | { type: 'javascript'; value: string }
3
+ | { provider?: string; type: 'llm-rubric'; value: string };
4
+
5
+ interface PromptfooTestCase {
6
+ assert: PromptfooAssert[];
7
+ description?: string;
8
+ vars: Record<string, unknown>;
9
+ }
10
+
11
+ const baseSchemaAssert: PromptfooAssert = {
12
+ type: 'javascript',
13
+ value: `
14
+ let parsed;
15
+ try {
16
+ parsed = JSON.parse(output);
17
+ } catch (error) {
18
+ console.error('Failed to parse JSON output', error);
19
+ return false;
20
+ }
21
+
22
+ if (!parsed || !Array.isArray(parsed.memories)) return false;
23
+
24
+ return parsed.memories.every((memory) => {
25
+ return (
26
+ memory.memoryType === 'activity' &&
27
+ memory.title &&
28
+ memory.summary &&
29
+ memory.withActivity?.type &&
30
+ memory.withActivity?.narrative
31
+ );
32
+ });
33
+ `,
34
+ };
35
+
36
+ const baseVars = {
37
+ availableCategories: ['work', 'health', 'personal'],
38
+ language: 'English',
39
+ topK: 5,
40
+ username: 'User',
41
+ };
42
+
43
+ const testCases: PromptfooTestCase[] = [
44
+ {
45
+ assert: [
46
+ baseSchemaAssert,
47
+ {
48
+ type: 'javascript',
49
+ value: `
50
+ const data = JSON.parse(output);
51
+ const first = data.memories?.[0];
52
+ if (!first) return false;
53
+
54
+ const activity = first.withActivity || {};
55
+ return Boolean(activity.startsAt && activity.endsAt && activity.timezone && activity.associatedLocations?.[0]?.name);
56
+ `,
57
+ },
58
+ {
59
+ provider: 'openai:gpt-5-mini',
60
+ type: 'llm-rubric',
61
+ value:
62
+ 'Should extract a meeting activity including timing (start/end/timezone), location name ACME HQ, status completed when implied, and feedback reflecting the positive tone.',
63
+ },
64
+ ],
65
+ description: 'Meeting with explicit time and location',
66
+ vars: {
67
+ ...baseVars,
68
+ conversation:
69
+ 'User: I met with Alice at ACME HQ on 2024-05-03 from 14:00-15:00 America/New_York. We reviewed Q2 renewal scope and agreed to send revised pricing next week. I felt positive and collaborative about the call.',
70
+ retrievedContexts: ['Previous similar memory: met with Alice about renewal last month.'],
71
+ sessionDate: '2024-05-03',
72
+ },
73
+ },
74
+ {
75
+ assert: [
76
+ baseSchemaAssert,
77
+ {
78
+ type: 'javascript',
79
+ value: `
80
+ const data = JSON.parse(output);
81
+ const first = data.memories?.[0];
82
+ if (!first) return false;
83
+
84
+ const activity = first.withActivity || {};
85
+ return Boolean(activity.narrative && activity.feedback);
86
+ `,
87
+ },
88
+ {
89
+ provider: 'openai:gpt-5-mini',
90
+ type: 'llm-rubric',
91
+ value:
92
+ 'Should capture an exercise activity without inventing exact timestamps or timezones; keep the narrative and feedback about the yoga session at home and omit temporal fields that were not provided.',
93
+ },
94
+ ],
95
+ description: 'Exercise without explicit time or timezone',
96
+ vars: {
97
+ ...baseVars,
98
+ conversation:
99
+ 'User: Over the weekend I did a 30-minute yoga session at home with my roommate. No specific time was set, it was just a casual stretch and it left me feeling calm.',
100
+ retrievedContexts: [],
101
+ sessionDate: '2025-05-05 10:02:00',
102
+ },
103
+ },
104
+ ];
105
+
106
+ export default testCases;
@@ -0,0 +1,104 @@
1
+ import { renderPlaceholderTemplate } from '@lobechat/context-engine';
2
+ import { MemorySourceType } from '@lobechat/types';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { isAbsolute, join } from 'node:path';
5
+
6
+ import { BenchmarkLocomoContextProvider, BenchmarkLocomoPart } from '../../../../src/providers';
7
+ import type { IngestPayload } from '../../../../src/converters/locomo';
8
+ import { activityPrompt } from '../../../../src/prompts';
9
+ import type { ExtractorTemplateProps, MemoryExtractionJob } from '../../../../src/types';
10
+
11
+ export interface PromptVars extends ExtractorTemplateProps {
12
+ payloadPath: string;
13
+ sessionId?: string;
14
+ userId?: string;
15
+ }
16
+
17
+ const resolvePath = (payloadPath: string) =>
18
+ isAbsolute(payloadPath) ? payloadPath : join(process.cwd(), payloadPath);
19
+
20
+ const buildParts = (payload: IngestPayload, sessionId?: string): BenchmarkLocomoPart[] => {
21
+ let partIndex = 0;
22
+ const sessions = payload.sessions.filter(
23
+ (session) => !sessionId || session.sessionId === sessionId,
24
+ );
25
+
26
+ return sessions.flatMap((session) =>
27
+ session.turns.map((turn) => {
28
+ const metadata = {
29
+ diaId: turn.diaId,
30
+ imageCaption: turn.imageCaption,
31
+ imageUrls: turn.imageUrls,
32
+ sessionId: session.sessionId,
33
+ };
34
+
35
+ return {
36
+ content: turn.text,
37
+ createdAt: turn.createdAt || session.timestamp,
38
+ metadata,
39
+ partIndex: partIndex++,
40
+ sessionId: session.sessionId,
41
+ speaker: turn.speaker,
42
+ };
43
+ }),
44
+ );
45
+ };
46
+
47
+ const resolveSessionDate = (payload: IngestPayload, parts: BenchmarkLocomoPart[], sessionId?: string) => {
48
+ const sessionDate =
49
+ payload.sessions.find((session) => session.sessionId === sessionId)?.timestamp ||
50
+ payload.sessions[0]?.timestamp;
51
+
52
+ if (sessionDate) return sessionDate;
53
+
54
+ const latestCreatedAt = parts
55
+ .map((part) => (part.createdAt ? new Date(part.createdAt) : null))
56
+ .filter(Boolean)
57
+ .sort((a, b) => (a!.getTime() > b!.getTime() ? 1 : -1))
58
+ .at(-1);
59
+
60
+ return latestCreatedAt ? latestCreatedAt.toISOString() : new Date().toISOString();
61
+ };
62
+
63
+ export const buildLocomoActivityMessages = async (vars: PromptVars) => {
64
+ const payloadPath = resolvePath(vars.payloadPath);
65
+ const payloadRaw = await readFile(payloadPath, 'utf8');
66
+ const payload = JSON.parse(payloadRaw) as IngestPayload;
67
+
68
+ const parts = buildParts(payload, vars.sessionId);
69
+ if (parts.length === 0) {
70
+ throw new Error(`No matching parts found in ${payload.sampleId} for session ${vars.sessionId || 'all'}`);
71
+ }
72
+ const userId = vars.userId || `locomo-user-${payload.sampleId}`;
73
+ const sourceId = payload.topicId || `sample_${payload.sampleId}`;
74
+ const sessionDate = vars.sessionDate || resolveSessionDate(payload, parts, vars.sessionId);
75
+
76
+ const provider = new BenchmarkLocomoContextProvider({
77
+ parts,
78
+ sampleId: payload.sampleId,
79
+ sourceId,
80
+ userId,
81
+ });
82
+
83
+ const extractionJob: MemoryExtractionJob = {
84
+ source: MemorySourceType.BenchmarkLocomo,
85
+ sourceId,
86
+ userId,
87
+ };
88
+
89
+ const { context } = await provider.buildContext(extractionJob);
90
+
91
+ const rendered = renderPlaceholderTemplate(activityPrompt, {
92
+ availableCategories: vars.availableCategories,
93
+ language: vars.language || 'English',
94
+ retrievedContext: context || 'No similar memories retrieved.',
95
+ sessionDate,
96
+ topK: vars.topK ?? 5,
97
+ username: vars.username || 'User',
98
+ });
99
+
100
+ return [
101
+ { content: rendered, role: 'system' as const },
102
+ { content: rendered, role: 'user' as const },
103
+ ];
104
+ };
@@ -0,0 +1,13 @@
1
+ description: LoCoMo regression for activity layer with relative time resolution
2
+
3
+ providers:
4
+ - id: openai:chat:gpt-5-mini
5
+ config:
6
+ response_format: file://../../../response-formats/activity.json
7
+ temperature: 0
8
+
9
+ prompts:
10
+ - file://./prompt.ts
11
+
12
+ tests:
13
+ - file://./tests/cases.ts
@@ -0,0 +1,5 @@
1
+ import { buildLocomoActivityMessages, PromptVars } from './buildMessages';
2
+
3
+ export default async function generatePrompt({ vars }: { vars: PromptVars }) {
4
+ return buildLocomoActivityMessages(vars);
5
+ }