Skip to content

Commit 460e3e6

Browse files
committed
feat: record createdAt for docs and files
1 parent e9a566c commit 460e3e6

File tree

12 files changed

+154
-35
lines changed

12 files changed

+154
-35
lines changed

packages/backend/server/src/__tests__/copilot.e2e.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -763,7 +763,11 @@ test('should be able to manage context', async t => {
763763
await t.notThrowsAsync(context, 'should create context with chat session');
764764

765765
const list = await listContext(app, token, workspaceId, sessionId);
766-
t.deepEqual(list, [{ id: await context }], 'should list context');
766+
t.deepEqual(
767+
list.map(f => ({ id: f.id })),
768+
[{ id: await context }],
769+
'should list context'
770+
);
767771
}
768772

769773
const fs = await import('node:fs');

packages/backend/server/src/__tests__/copilot.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,8 +1294,8 @@ test('should be able to manage context', async t => {
12941294
{
12951295
const session = await context.create(chatSession);
12961296

1297-
const fileId = await session.add(file, randomUUID());
1298-
const list = await session.listFiles();
1297+
const fileId = await session.addFile(file, randomUUID());
1298+
const list = session.listFiles();
12991299
t.deepEqual(
13001300
list.map(f => f.chunk_size),
13011301
[3],
@@ -1309,10 +1309,10 @@ test('should be able to manage context', async t => {
13091309

13101310
const docId = randomUUID();
13111311
await session.addDocRecord(randomUUID());
1312-
const docs = await session.listDocs();
1312+
const docs = session.listDocs();
13131313
t.deepEqual(docs, [docId], 'should list doc id');
13141314

1315-
const result = await session.match('test', 2);
1315+
const result = await session.matchFileChunks('test', 2);
13161316
t.is(result.length, 2, 'should match context');
13171317
t.is(result[0].fileId, fileId!, 'should match file id');
13181318
}

packages/backend/server/src/__tests__/utils/copilot.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,12 @@ export async function listContext(
306306
userToken: string,
307307
workspaceId: string,
308308
sessionId: string
309-
): Promise<{ id: string }[]> {
309+
): Promise<
310+
{
311+
id: string;
312+
createdAt: number;
313+
}[]
314+
> {
310315
const res = await request(app.getHttpServer())
311316
.post(gql)
312317
.auth(userToken, { type: 'bearer' })
@@ -318,6 +323,7 @@ export async function listContext(
318323
copilot(workspaceId: "${workspaceId}") {
319324
contexts(sessionId: "${sessionId}") {
320325
id
326+
createdAt
321327
}
322328
}
323329
}
@@ -407,6 +413,7 @@ export async function listContextFiles(
407413
blobId: string;
408414
chunk_size: number;
409415
status: string;
416+
createdAt: number;
410417
}[]
411418
| undefined
412419
> {
@@ -426,6 +433,7 @@ export async function listContextFiles(
426433
blobId
427434
chunk_size
428435
status
436+
createdAt
429437
}
430438
}
431439
}

packages/backend/server/src/plugins/copilot/context/resolver.ts

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { COPILOT_LOCKER, CopilotType } from '../resolver';
3535
import { ChatSessionService } from '../session';
3636
import { CopilotContextService } from './service';
3737
import {
38+
ContextDoc,
3839
type ContextFile,
3940
ContextFileStatus,
4041
DocChunkSimilarity,
@@ -75,10 +76,22 @@ class RemoveContextFileInput {
7576
export class CopilotContextType {
7677
@Field(() => ID)
7778
id!: string;
79+
80+
@Field(() => SafeIntResolver)
81+
createdAt!: number;
7882
}
7983

8084
registerEnumType(ContextFileStatus, { name: 'ContextFileStatus' });
8185

86+
@ObjectType()
87+
class CopilotContextDoc implements ContextDoc {
88+
@Field(() => ID)
89+
id!: string;
90+
91+
@Field(() => SafeIntResolver)
92+
createdAt!: number;
93+
}
94+
8295
@ObjectType()
8396
class CopilotContextFile implements ContextFile {
8497
@Field(() => ID)
@@ -95,6 +108,9 @@ class CopilotContextFile implements ContextFile {
95108

96109
@Field(() => String)
97110
blobId!: string;
111+
112+
@Field(() => SafeIntResolver)
113+
createdAt!: number;
98114
}
99115

100116
@ObjectType()
@@ -252,17 +268,17 @@ export class CopilotContextResolver {
252268
return controller.signal;
253269
}
254270

255-
@ResolveField(() => [String], {
271+
@ResolveField(() => [CopilotContextDoc], {
256272
description: 'list files in context',
257273
})
258274
@CallMetric('ai', 'context_file_list')
259275
async docs(
260276
@Parent() context: CopilotContextType,
261277
@Args('contextId', { nullable: true }) contextId?: string
262-
): Promise<string[]> {
278+
): Promise<ContextDoc[]> {
263279
const id = contextId || context.id;
264280
const session = await this.context.get(id);
265-
return await session.listDocs();
281+
return session.listDocs();
266282
}
267283

268284
@Mutation(() => SafeIntResolver, {
@@ -325,7 +341,7 @@ export class CopilotContextResolver {
325341
): Promise<CopilotContextFile[]> {
326342
const id = contextId || context.id;
327343
const session = await this.context.get(id);
328-
return await session.listFiles();
344+
return session.listFiles();
329345
}
330346

331347
@Mutation(() => String, {
@@ -378,7 +394,7 @@ export class CopilotContextResolver {
378394
const session = await this.context.get(options.contextId);
379395

380396
try {
381-
return await session.remove(options.fileId);
397+
return await session.removeFile(options.fileId);
382398
} catch (e: any) {
383399
throw new CopilotFailedToModifyContext({
384400
contextId: options.contextId,
@@ -406,7 +422,11 @@ export class CopilotContextResolver {
406422
const session = await this.context.get(contextId);
407423

408424
try {
409-
return await session.match(content, limit, this.getSignal(ctx.req));
425+
return await session.matchFileChunks(
426+
content,
427+
limit,
428+
this.getSignal(ctx.req)
429+
);
410430
} catch (e: any) {
411431
throw new CopilotFailedToMatchContext({
412432
contextId,
@@ -433,7 +453,11 @@ export class CopilotContextResolver {
433453
await this.permissions.checkCloudWorkspace(session.workspaceId, user.id);
434454

435455
try {
436-
return await session.match(content, limit, this.getSignal(ctx.req));
456+
return await session.matchFileChunks(
457+
content,
458+
limit,
459+
this.getSignal(ctx.req)
460+
);
437461
} catch (e: any) {
438462
throw new CopilotFailedToMatchContext({
439463
contextId,

packages/backend/server/src/plugins/copilot/context/service.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,14 @@ export class CopilotContextService {
8484
throw new CopilotInvalidContext({ contextId: id });
8585
}
8686

87-
async list(sessionId: string): Promise<{ id: string }[]> {
87+
async list(sessionId: string): Promise<{ id: string; createdAt: number }[]> {
8888
const contexts = await this.db.aiContext.findMany({
8989
where: { sessionId },
90-
select: { id: true },
90+
select: { id: true, createdAt: true },
9191
});
92-
return contexts;
92+
return contexts.map(c => ({
93+
id: c.id,
94+
createdAt: c.createdAt.getTime(),
95+
}));
9396
}
9497
}

packages/backend/server/src/plugins/copilot/context/session.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import { nanoid } from 'nanoid';
88
import { BlobQuotaExceeded, PrismaTransaction } from '../../../base';
99
import { OneMB } from '../../../core/quota/constant';
1010
import {
11+
ChunkSimilarity,
1112
ContextConfig,
13+
ContextDoc,
1214
ContextFile,
1315
ContextFileStatus,
1416
DocChunkSimilarity,
@@ -33,11 +35,11 @@ export class ContextSession implements AsyncDisposable {
3335
return this.config.workspaceId;
3436
}
3537

36-
async listDocs() {
38+
listDocs(): ContextDoc[] {
3739
return [...this.config.docs];
3840
}
3941

40-
async listFiles() {
42+
listFiles() {
4143
return this.config.files.map(f => ({ ...f }));
4244
}
4345

@@ -65,6 +67,7 @@ export class ContextSession implements AsyncDisposable {
6567
blobId,
6668
chunk_size: embeddings.length,
6769
name,
70+
createdAt: Date.now(),
6871
}));
6972

7073
const values = this.processEmbeddings(fileId, embeddings);
@@ -116,15 +119,15 @@ export class ContextSession implements AsyncDisposable {
116119
}
117120

118121
async addDocRecord(docId: string) {
119-
if (!this.config.docs.includes(docId)) {
120-
this.config.docs.push(docId);
122+
if (!this.config.docs.some(f => f.id === docId)) {
123+
this.config.docs.push({ id: docId, createdAt: Date.now() });
121124
await this.save();
122125
}
123-
return this.config.docs.length;
126+
return this.config.docs;
124127
}
125128

126129
async removeDocRecord(docId: string) {
127-
const index = this.config.docs.indexOf(docId);
130+
const index = this.config.docs.findIndex(f => f.id === docId);
128131
if (index >= 0) {
129132
this.config.docs.splice(index, 1);
130133
await this.save();
@@ -142,10 +145,10 @@ export class ContextSession implements AsyncDisposable {
142145
if (signal?.aborted) return;
143146
const buffer = await this.readStream(readable, 50 * OneMB);
144147
const file = new File([buffer], name);
145-
return await this.add(file, blobId, signal);
148+
return await this.addFile(file, blobId, signal);
146149
}
147150

148-
async add(
151+
async addFile(
149152
file: File,
150153
blobId: string,
151154
signal?: AbortSignal
@@ -157,7 +160,7 @@ export class ContextSession implements AsyncDisposable {
157160
return undefined;
158161
}
159162

160-
async remove(fileId: string) {
163+
async removeFile(fileId: string) {
161164
return await this.db.$transaction(async tx => {
162165
const ret = await tx.aiContextEmbedding.deleteMany({
163166
where: { contextId: this.contextId, fileId },
@@ -168,7 +171,7 @@ export class ContextSession implements AsyncDisposable {
168171
});
169172
}
170173

171-
async match(
174+
async matchFileChunks(
172175
content: string,
173176
topK: number = 5,
174177
signal?: AbortSignal
@@ -185,11 +188,11 @@ export class ContextSession implements AsyncDisposable {
185188
`;
186189
}
187190

188-
async matchWorkspace(
191+
async matchWorkspaceChunks(
189192
content: string,
190193
topK: number = 5,
191194
signal?: AbortSignal
192-
) {
195+
): Promise<ChunkSimilarity[]> {
193196
const embedding = await this.client
194197
.getEmbeddings([content], signal)
195198
.then(r => r?.[0]?.embedding);

packages/backend/server/src/plugins/copilot/context/types.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,19 @@ export const ContextConfigSchema = z.object({
3232
ContextFileStatus.failed,
3333
]),
3434
blobId: z.string(),
35+
createdAt: z.number(),
36+
})
37+
.array(),
38+
docs: z
39+
.object({
40+
id: z.string(),
41+
createdAt: z.number(),
3542
})
3643
.array(),
37-
docs: z.string().array(),
3844
});
3945

4046
export type ContextConfig = z.infer<typeof ContextConfigSchema>;
47+
export type ContextDoc = z.infer<typeof ContextConfigSchema>['docs'][number];
4148
export type ContextFile = z.infer<typeof ContextConfigSchema>['files'][number];
4249

4350
export type ChunkSimilarity = {

packages/backend/server/src/schema.gql

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,17 +78,25 @@ type Copilot {
7878
}
7979

8080
type CopilotContext {
81+
createdAt: SafeInt!
82+
8183
"""list files in context"""
82-
docs(contextId: String): [String!]!
84+
docs(contextId: String): [CopilotContextDoc!]!
8385

8486
"""list files in context"""
8587
files(contextId: String): [CopilotContextFile!]!
8688
id: ID!
8789
}
8890

91+
type CopilotContextDoc {
92+
createdAt: SafeInt!
93+
id: ID!
94+
}
95+
8996
type CopilotContextFile {
9097
blobId: String!
9198
chunk_size: SafeInt!
99+
createdAt: SafeInt!
92100
id: ID!
93101
name: String!
94102
status: ContextFileStatus!

packages/frontend/graphql/src/graphql/copilot-context-file-list.gql

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@ query listContextFiles(
66
currentUser {
77
copilot(workspaceId: $workspaceId) {
88
contexts(sessionId: $sessionId) {
9-
docs(contextId: $contextId)
9+
docs(contextId: $contextId) {
10+
id
11+
createdAt
12+
}
1013
files(contextId: $contextId) {
1114
id
1215
name
1316
blobId
1417
chunk_size
1518
status
19+
createdAt
1620
}
1721
}
1822
}

packages/frontend/graphql/src/graphql/copilot-context-list.gql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ query listContext($workspaceId: String!, $sessionId: String!) {
33
copilot(workspaceId: $workspaceId) {
44
contexts(sessionId: $sessionId) {
55
id
6+
createdAt
67
}
78
}
89
}

0 commit comments

Comments
 (0)