Skip to content

Commit bdb3eb4

Browse files
authored
♻️ refactor: improve memory data with experience and identity (#11717)
* fix scope issue * fix memory data * update memory * update * hide edit
1 parent 3c70dfa commit bdb3eb4

File tree

45 files changed

+854
-199
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+854
-199
lines changed

packages/database/src/models/__tests__/userMemories.test.ts

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
66

77
import { idGenerator } from '@/database/utils/idGenerator';
88

9+
import { getTestDB } from '../../core/getTestDB';
910
import {
1011
topics,
1112
userMemories,
@@ -24,7 +25,6 @@ import {
2425
CreateUserMemoryPreferenceParams,
2526
UserMemoryModel,
2627
} from '../userMemory';
27-
import { getTestDB } from '../../core/getTestDB';
2828

2929
const serverDB: LobeChatDatabase = await getTestDB();
3030

@@ -753,20 +753,27 @@ describe('UserMemoryModel', () => {
753753

754754
await userMemoryModel.addIdentityEntry({
755755
base: { lastAccessedAt: now, tags: [] },
756-
identity: { role: 'engineer', tags: ['alpha', 'beta'] },
756+
identity: { relationship: 'self', role: 'engineer', tags: ['alpha', 'beta'] },
757757
});
758758
await userMemoryModel.addIdentityEntry({
759759
base: { lastAccessedAt: now, tags: [] },
760-
identity: { role: 'engineer', tags: ['alpha'] },
760+
identity: { relationship: 'self', role: 'engineer', tags: ['alpha'] },
761761
});
762762
await userMemoryModel.addIdentityEntry({
763763
base: { lastAccessedAt: now, tags: [] },
764-
identity: { role: 'manager', tags: [] },
764+
identity: { relationship: 'self', role: 'manager', tags: [] },
765765
});
766766

767+
// This should not be counted (different user)
767768
await anotherUserModel.addIdentityEntry({
768769
base: { lastAccessedAt: now, tags: [] },
769-
identity: { role: 'engineer', tags: ['alpha'] },
770+
identity: { relationship: 'self', role: 'engineer', tags: ['alpha'] },
771+
});
772+
773+
// This should not be counted (relationship is not 'self')
774+
await userMemoryModel.addIdentityEntry({
775+
base: { lastAccessedAt: now, tags: [] },
776+
identity: { relationship: 'friend', role: 'designer', tags: ['gamma'] },
770777
});
771778

772779
const result = await userMemoryModel.queryIdentityRoles({ size: 5 });
@@ -1062,6 +1069,56 @@ describe('UserMemoryModel', () => {
10621069
expect(identityItem.identity.userMemoryId).toBe(identityMemoryId);
10631070
expect(identityItem.identity.type).toBe(identity?.type);
10641071
});
1072+
1073+
it('should order identity memories by capturedAt desc and include capturedAt and title in response', async () => {
1074+
const olderCapturedAt = new Date('2024-01-01T10:00:00Z');
1075+
const newerCapturedAt = new Date('2024-01-15T10:00:00Z');
1076+
1077+
await userMemoryModel.addIdentityEntry({
1078+
base: { summary: 'older identity', title: 'Older Title' },
1079+
identity: {
1080+
capturedAt: olderCapturedAt,
1081+
description: 'Older identity description',
1082+
relationship: 'friend',
1083+
type: 'personal',
1084+
},
1085+
});
1086+
1087+
await userMemoryModel.addIdentityEntry({
1088+
base: { summary: 'newer identity', title: 'Newer Title' },
1089+
identity: {
1090+
capturedAt: newerCapturedAt,
1091+
description: 'Newer identity description',
1092+
relationship: 'self',
1093+
type: 'personal',
1094+
},
1095+
});
1096+
1097+
const result = await userMemoryModel.queryMemories({
1098+
layer: LayersEnum.Identity,
1099+
});
1100+
1101+
expect(result.total).toBe(2);
1102+
expect(result.items).toHaveLength(2);
1103+
1104+
const firstItem = result.items[0] as any;
1105+
const secondItem = result.items[1] as any;
1106+
1107+
// Verify order by capturedAt desc
1108+
expect(firstItem.identity.capturedAt).toEqual(newerCapturedAt);
1109+
expect(secondItem.identity.capturedAt).toEqual(olderCapturedAt);
1110+
1111+
expect(firstItem.identity.description).toBe('Newer identity description');
1112+
expect(secondItem.identity.description).toBe('Older identity description');
1113+
1114+
// Verify title comes from memory schema
1115+
expect(firstItem.identity.title).toBe('Newer Title');
1116+
expect(secondItem.identity.title).toBe('Older Title');
1117+
1118+
// Verify relationship is included
1119+
expect(firstItem.identity.relationship).toBe('self');
1120+
expect(secondItem.identity.relationship).toBe('friend');
1121+
});
10651122
});
10661123

10671124
describe('findById', () => {

packages/database/src/models/agentCronJob.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { and, desc, eq, gt, isNull, or, sql } from 'drizzle-orm';
1+
import { and, desc, eq, gt, inArray, isNull, or, sql } from 'drizzle-orm';
22

33
import {
44
type AgentCronJob,
@@ -25,8 +25,8 @@ export class AgentCronJobModel {
2525
.values({
2626
...data,
2727
// Initialize remaining executions to match max executions
28-
remainingExecutions: data.maxExecutions,
29-
28+
remainingExecutions: data.maxExecutions,
29+
3030
userId: this.userId,
3131
} as NewAgentCronJob)
3232
.returning();
@@ -149,11 +149,11 @@ remainingExecutions: data.maxExecutions,
149149
.set({
150150
enabled: true,
151151
// Re-enable job when resetting
152-
lastExecutedAt: null,
153-
154-
maxExecutions: newMaxExecutions,
155-
156-
remainingExecutions: newMaxExecutions,
152+
lastExecutedAt: null,
153+
154+
maxExecutions: newMaxExecutions,
155+
156+
remainingExecutions: newMaxExecutions,
157157
totalExecutions: 0,
158158
updatedAt: new Date(),
159159
})
@@ -227,7 +227,7 @@ remainingExecutions: newMaxExecutions,
227227
enabled,
228228
updatedAt: new Date(),
229229
})
230-
.where(and(sql`${agentCronJobs.id} = ANY(${ids})`, eq(agentCronJobs.userId, this.userId)))
230+
.where(and(inArray(agentCronJobs.id, ids), eq(agentCronJobs.userId, this.userId)))
231231
.returning();
232232

233233
return result.length;

packages/database/src/models/userMemory/__tests__/identity.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
import { RelationshipEnum } from '@lobechat/types';
33
import { beforeEach, describe, expect, it } from 'vitest';
44

5+
import { getTestDB } from '../../../core/getTestDB';
56
import {
67
NewUserMemoryIdentity,
78
userMemories,
89
userMemoriesIdentities,
910
users,
1011
} from '../../../schemas';
1112
import { LobeChatDatabase } from '../../../type';
12-
import { getTestDB } from '../../../core/getTestDB';
1313
import { UserMemoryIdentityModel } from '../identity';
1414

1515
const userId = 'identity-test-user';
@@ -68,21 +68,21 @@ describe('UserMemoryIdentityModel', () => {
6868
userId,
6969
type: 'personal',
7070
description: 'Identity 1',
71-
createdAt: new Date('2024-01-01T10:00:00Z'),
71+
capturedAt: new Date('2024-01-01T10:00:00Z'),
7272
},
7373
{
7474
id: 'identity-2',
7575
userId,
7676
type: 'professional',
7777
description: 'Identity 2',
78-
createdAt: new Date('2024-01-02T10:00:00Z'),
78+
capturedAt: new Date('2024-01-02T10:00:00Z'),
7979
},
8080
{
8181
id: 'other-identity',
8282
userId: otherUserId,
8383
type: 'personal',
8484
description: 'Other Identity',
85-
createdAt: new Date('2024-01-03T10:00:00Z'),
85+
capturedAt: new Date('2024-01-03T10:00:00Z'),
8686
},
8787
]);
8888
});
@@ -94,7 +94,7 @@ describe('UserMemoryIdentityModel', () => {
9494
expect(result.every((i) => i.userId === userId)).toBe(true);
9595
});
9696

97-
it('should order by createdAt desc', async () => {
97+
it('should order by capturedAt desc', async () => {
9898
const result = await identityModel.query();
9999

100100
expect(result[0].id).toBe('identity-2'); // Most recent first

packages/database/src/models/userMemory/experience.ts

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { and, desc, eq } from 'drizzle-orm';
1+
import type { ExperienceListParams, ExperienceListResult } from '@lobechat/types';
2+
import { type SQL, and, asc, desc, eq, ilike, inArray, or, sql } from 'drizzle-orm';
23

34
import {
45
NewUserMemoryExperience,
@@ -64,6 +65,95 @@ export class UserMemoryExperienceModel {
6465
});
6566
};
6667

68+
/**
69+
* Query experience list with pagination, search, and sorting
70+
* Returns a flat structure optimized for frontend display
71+
*/
72+
queryList = async (params: ExperienceListParams = {}): Promise<ExperienceListResult> => {
73+
const { order = 'desc', page = 1, pageSize = 20, q, sort, tags, types } = params;
74+
75+
const normalizedPage = Math.max(1, page);
76+
const normalizedPageSize = Math.min(Math.max(pageSize, 1), 100);
77+
const offset = (normalizedPage - 1) * normalizedPageSize;
78+
const normalizedQuery = typeof q === 'string' ? q.trim() : '';
79+
80+
// Build WHERE conditions
81+
const conditions: Array<SQL | undefined> = [
82+
eq(userMemoriesExperiences.userId, this.userId),
83+
// Full-text search across title, situation, keyLearning, action
84+
normalizedQuery
85+
? or(
86+
ilike(userMemories.title, `%${normalizedQuery}%`),
87+
ilike(userMemoriesExperiences.situation, `%${normalizedQuery}%`),
88+
ilike(userMemoriesExperiences.keyLearning, `%${normalizedQuery}%`),
89+
ilike(userMemoriesExperiences.action, `%${normalizedQuery}%`),
90+
)
91+
: undefined,
92+
types && types.length > 0 ? inArray(userMemoriesExperiences.type, types) : undefined,
93+
tags && tags.length > 0
94+
? or(...tags.map((tag) => sql<boolean>`${tag} = ANY(${userMemoriesExperiences.tags})`))
95+
: undefined,
96+
];
97+
98+
const filters = conditions.filter((condition): condition is SQL => condition !== undefined);
99+
const whereClause = filters.length > 0 ? and(...filters) : undefined;
100+
101+
// Build ORDER BY
102+
const applyOrder = order === 'asc' ? asc : desc;
103+
const sortColumn =
104+
sort === 'scoreConfidence'
105+
? userMemoriesExperiences.scoreConfidence
106+
: userMemoriesExperiences.capturedAt;
107+
108+
const orderByClauses = [
109+
applyOrder(sortColumn),
110+
applyOrder(userMemoriesExperiences.updatedAt),
111+
applyOrder(userMemoriesExperiences.createdAt),
112+
];
113+
114+
// JOIN condition
115+
const joinCondition = and(
116+
eq(userMemories.id, userMemoriesExperiences.userMemoryId),
117+
eq(userMemories.userId, this.userId),
118+
);
119+
120+
// Execute queries in parallel
121+
const [rows, totalResult] = await Promise.all([
122+
this.db
123+
.select({
124+
action: userMemoriesExperiences.action,
125+
capturedAt: userMemoriesExperiences.capturedAt,
126+
createdAt: userMemoriesExperiences.createdAt,
127+
id: userMemoriesExperiences.id,
128+
keyLearning: userMemoriesExperiences.keyLearning,
129+
scoreConfidence: userMemoriesExperiences.scoreConfidence,
130+
situation: userMemoriesExperiences.situation,
131+
tags: userMemoriesExperiences.tags,
132+
title: userMemories.title,
133+
type: userMemoriesExperiences.type,
134+
updatedAt: userMemoriesExperiences.updatedAt,
135+
})
136+
.from(userMemoriesExperiences)
137+
.innerJoin(userMemories, joinCondition)
138+
.where(whereClause)
139+
.orderBy(...orderByClauses)
140+
.limit(normalizedPageSize)
141+
.offset(offset),
142+
this.db
143+
.select({ count: sql<number>`COUNT(*)::int` })
144+
.from(userMemoriesExperiences)
145+
.innerJoin(userMemories, joinCondition)
146+
.where(whereClause),
147+
]);
148+
149+
return {
150+
items: rows,
151+
page: normalizedPage,
152+
pageSize: normalizedPageSize,
153+
total: Number(totalResult[0]?.count ?? 0),
154+
};
155+
};
156+
67157
findById = async (id: string) => {
68158
return this.db.query.userMemoriesExperiences.findFirst({
69159
where: and(

0 commit comments

Comments
 (0)