mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-08 12:45:55 +08:00
feat(server): update gql endpoint & workspace doc match test (#11104)
This commit is contained in:
@@ -12,4 +12,4 @@ yarn install
|
|||||||
yarn affine @affine/server-native build
|
yarn affine @affine/server-native build
|
||||||
|
|
||||||
# Create database
|
# Create database
|
||||||
yarn affine @affine/server prisma db push
|
yarn affine @affine/server prisma migrate reset -f
|
||||||
|
|||||||
@@ -19,5 +19,5 @@ runs:
|
|||||||
NODE_ENV: test
|
NODE_ENV: test
|
||||||
run: |
|
run: |
|
||||||
yarn affine @affine/server prisma generate
|
yarn affine @affine/server prisma generate
|
||||||
yarn affine @affine/server prisma db push
|
yarn affine @affine/server prisma migrate reset -f
|
||||||
yarn affine @affine/server data-migration run
|
yarn affine @affine/server data-migration run
|
||||||
|
|||||||
@@ -444,7 +444,7 @@ model AiContextEmbedding {
|
|||||||
// a file can be divided into multiple chunks and embedded separately.
|
// a file can be divided into multiple chunks and embedded separately.
|
||||||
chunk Int @db.Integer
|
chunk Int @db.Integer
|
||||||
content String @db.VarChar
|
content String @db.VarChar
|
||||||
embedding Unsupported("vector(512)")
|
embedding Unsupported("vector(1024)")
|
||||||
|
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||||
@@ -462,7 +462,7 @@ model AiWorkspaceEmbedding {
|
|||||||
// a doc can be divided into multiple chunks and embedded separately.
|
// a doc can be divided into multiple chunks and embedded separately.
|
||||||
chunk Int @db.Integer
|
chunk Int @db.Integer
|
||||||
content String @db.VarChar
|
content String @db.VarChar
|
||||||
embedding Unsupported("vector(512)")
|
embedding Unsupported("vector(1024)")
|
||||||
|
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||||
|
|||||||
@@ -40,7 +40,10 @@ Generated by [AVA](https://avajs.dev).
|
|||||||
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
id: 'docId1',
|
blobId: 'fileId1',
|
||||||
|
chunkSize: 0,
|
||||||
|
name: 'sample.pdf',
|
||||||
|
status: 'processing',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -48,9 +51,6 @@ Generated by [AVA](https://avajs.dev).
|
|||||||
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
blobId: 'fileId1',
|
id: 'docId1',
|
||||||
chunkSize: 0,
|
|
||||||
name: 'sample.pdf',
|
|
||||||
status: 'processing',
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
Binary file not shown.
@@ -1,6 +1,7 @@
|
|||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
|
|
||||||
import { ProjectRoot } from '@affine-tools/utils/path';
|
import { ProjectRoot } from '@affine-tools/utils/path';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
import type { TestFn } from 'ava';
|
import type { TestFn } from 'ava';
|
||||||
import ava from 'ava';
|
import ava from 'ava';
|
||||||
import Sinon from 'sinon';
|
import Sinon from 'sinon';
|
||||||
@@ -8,6 +9,7 @@ import Sinon from 'sinon';
|
|||||||
import { JobQueue } from '../base';
|
import { JobQueue } from '../base';
|
||||||
import { ConfigModule } from '../base/config';
|
import { ConfigModule } from '../base/config';
|
||||||
import { AuthService } from '../core/auth';
|
import { AuthService } from '../core/auth';
|
||||||
|
import { DocReader } from '../core/doc';
|
||||||
import { WorkspaceModule } from '../core/workspaces';
|
import { WorkspaceModule } from '../core/workspaces';
|
||||||
import { CopilotModule } from '../plugins/copilot';
|
import { CopilotModule } from '../plugins/copilot';
|
||||||
import {
|
import {
|
||||||
@@ -41,14 +43,16 @@ import {
|
|||||||
chatWithText,
|
chatWithText,
|
||||||
chatWithTextStream,
|
chatWithTextStream,
|
||||||
chatWithWorkflow,
|
chatWithWorkflow,
|
||||||
|
cleanObject,
|
||||||
createCopilotContext,
|
createCopilotContext,
|
||||||
createCopilotMessage,
|
createCopilotMessage,
|
||||||
createCopilotSession,
|
createCopilotSession,
|
||||||
forkCopilotSession,
|
forkCopilotSession,
|
||||||
getHistories,
|
getHistories,
|
||||||
listContext,
|
listContext,
|
||||||
listContextFiles,
|
listContextDocAndFiles,
|
||||||
matchContext,
|
matchFiles,
|
||||||
|
matchWorkspaceDocs,
|
||||||
MockCopilotTestProvider,
|
MockCopilotTestProvider,
|
||||||
sse2array,
|
sse2array,
|
||||||
textToEventStream,
|
textToEventStream,
|
||||||
@@ -59,6 +63,7 @@ import {
|
|||||||
const test = ava as TestFn<{
|
const test = ava as TestFn<{
|
||||||
auth: AuthService;
|
auth: AuthService;
|
||||||
app: TestingApp;
|
app: TestingApp;
|
||||||
|
db: PrismaClient;
|
||||||
context: CopilotContextService;
|
context: CopilotContextService;
|
||||||
jobs: CopilotContextDocJob;
|
jobs: CopilotContextDocJob;
|
||||||
prompt: PromptService;
|
prompt: PromptService;
|
||||||
@@ -92,16 +97,26 @@ test.before(async t => {
|
|||||||
tapModule: m => {
|
tapModule: m => {
|
||||||
// use real JobQueue for testing
|
// use real JobQueue for testing
|
||||||
m.overrideProvider(JobQueue).useClass(JobQueue);
|
m.overrideProvider(JobQueue).useClass(JobQueue);
|
||||||
|
m.overrideProvider(DocReader).useValue({
|
||||||
|
getFullDocContent() {
|
||||||
|
return {
|
||||||
|
title: '1',
|
||||||
|
summary: '1',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const auth = app.get(AuthService);
|
const auth = app.get(AuthService);
|
||||||
|
const db = app.get(PrismaClient);
|
||||||
const context = app.get(CopilotContextService);
|
const context = app.get(CopilotContextService);
|
||||||
const prompt = app.get(PromptService);
|
const prompt = app.get(PromptService);
|
||||||
const storage = app.get(CopilotStorage);
|
const storage = app.get(CopilotStorage);
|
||||||
const jobs = app.get(CopilotContextDocJob);
|
const jobs = app.get(CopilotContextDocJob);
|
||||||
|
|
||||||
t.context.app = app;
|
t.context.app = app;
|
||||||
|
t.context.db = db;
|
||||||
t.context.auth = auth;
|
t.context.auth = auth;
|
||||||
t.context.context = context;
|
t.context.context = context;
|
||||||
t.context.prompt = prompt;
|
t.context.prompt = prompt;
|
||||||
@@ -513,15 +528,6 @@ test('should be able to retry with api', async t => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cleanObject = (obj: any[]) =>
|
|
||||||
JSON.parse(
|
|
||||||
JSON.stringify(obj, (k, v) =>
|
|
||||||
['id', 'sessionId', 'createdAt'].includes(k) || v === null
|
|
||||||
? undefined
|
|
||||||
: v
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// retry chat
|
// retry chat
|
||||||
{
|
{
|
||||||
const { id } = await createWorkspace(app);
|
const { id } = await createWorkspace(app);
|
||||||
@@ -771,6 +777,7 @@ test('should be able to manage context', async t => {
|
|||||||
ProjectRoot.join('packages/common/native/fixtures/sample.pdf').toFileUrl()
|
ProjectRoot.join('packages/common/native/fixtures/sample.pdf').toFileUrl()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// match files
|
||||||
{
|
{
|
||||||
const contextId = await createCopilotContext(app, workspaceId, sessionId);
|
const contextId = await createCopilotContext(app, workspaceId, sessionId);
|
||||||
|
|
||||||
@@ -781,34 +788,98 @@ test('should be able to manage context', async t => {
|
|||||||
'sample.pdf',
|
'sample.pdf',
|
||||||
buffer
|
buffer
|
||||||
);
|
);
|
||||||
await addContextDoc(app, contextId, 'docId1');
|
|
||||||
|
|
||||||
const { docs, files } =
|
const { files } =
|
||||||
(await listContextFiles(app, workspaceId, sessionId, contextId)) || {};
|
(await listContextDocAndFiles(app, workspaceId, sessionId, contextId)) ||
|
||||||
|
{};
|
||||||
t.snapshot(
|
t.snapshot(
|
||||||
docs?.map(({ createdAt: _, ...d }) => d),
|
cleanObject(files, ['id', 'error', 'createdAt']),
|
||||||
'should list context files'
|
'should list context files'
|
||||||
);
|
);
|
||||||
t.snapshot(
|
|
||||||
files?.map(({ createdAt: _, id: __, ...f }) => f),
|
|
||||||
'should list context docs'
|
|
||||||
);
|
|
||||||
|
|
||||||
// wait for processing
|
// wait for processing
|
||||||
{
|
{
|
||||||
let { files } =
|
let { files } =
|
||||||
(await listContextFiles(app, workspaceId, sessionId, contextId)) || {};
|
(await listContextDocAndFiles(
|
||||||
|
app,
|
||||||
|
workspaceId,
|
||||||
|
sessionId,
|
||||||
|
contextId
|
||||||
|
)) || {};
|
||||||
|
|
||||||
while (files?.[0].status !== 'finished') {
|
while (files?.[0].status !== 'finished') {
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
({ files } =
|
({ files } =
|
||||||
(await listContextFiles(app, workspaceId, sessionId, contextId)) ||
|
(await listContextDocAndFiles(
|
||||||
{});
|
app,
|
||||||
|
workspaceId,
|
||||||
|
sessionId,
|
||||||
|
contextId
|
||||||
|
)) || {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = (await matchContext(app, contextId, 'test', 1))!;
|
const result = (await matchFiles(app, contextId, 'test', 1))!;
|
||||||
t.is(result.length, 1, 'should match context');
|
t.is(result.length, 1, 'should match context');
|
||||||
t.is(result[0].fileId, fileId, 'should match file id');
|
t.is(result[0].fileId, fileId, 'should match file id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// match docs
|
||||||
|
{
|
||||||
|
const sessionId = await createCopilotSession(
|
||||||
|
app,
|
||||||
|
workspaceId,
|
||||||
|
randomUUID(),
|
||||||
|
promptName
|
||||||
|
);
|
||||||
|
const contextId = await createCopilotContext(app, workspaceId, sessionId);
|
||||||
|
|
||||||
|
const docId = 'docId1';
|
||||||
|
await t.context.db.snapshot.create({
|
||||||
|
data: {
|
||||||
|
workspaceId: workspaceId,
|
||||||
|
id: docId,
|
||||||
|
blob: Buffer.from([1, 1]),
|
||||||
|
state: Buffer.from([1, 1]),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await addContextDoc(app, contextId, docId);
|
||||||
|
|
||||||
|
const { docs } =
|
||||||
|
(await listContextDocAndFiles(app, workspaceId, sessionId, contextId)) ||
|
||||||
|
{};
|
||||||
|
t.snapshot(
|
||||||
|
cleanObject(docs, ['error', 'createdAt']),
|
||||||
|
'should list context docs'
|
||||||
|
);
|
||||||
|
|
||||||
|
// wait for processing
|
||||||
|
{
|
||||||
|
let { docs } =
|
||||||
|
(await listContextDocAndFiles(
|
||||||
|
app,
|
||||||
|
workspaceId,
|
||||||
|
sessionId,
|
||||||
|
contextId
|
||||||
|
)) || {};
|
||||||
|
|
||||||
|
while (docs?.[0].status !== 'finished') {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
({ docs } =
|
||||||
|
(await listContextDocAndFiles(
|
||||||
|
app,
|
||||||
|
workspaceId,
|
||||||
|
sessionId,
|
||||||
|
contextId
|
||||||
|
)) || {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = (await matchWorkspaceDocs(app, contextId, 'test', 1))!;
|
||||||
|
t.is(result.length, 1, 'should match context');
|
||||||
|
t.is(result[0].docId, docId, 'should match doc id');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -104,14 +104,14 @@ test('should insert embedding by doc id', async t => {
|
|||||||
{
|
{
|
||||||
index: 0,
|
index: 0,
|
||||||
content: 'content',
|
content: 'content',
|
||||||
embedding: Array.from({ length: 512 }, () => 1),
|
embedding: Array.from({ length: 1024 }, () => 1),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
{
|
{
|
||||||
const ret = await t.context.copilotContext.matchContentEmbedding(
|
const ret = await t.context.copilotContext.matchContentEmbedding(
|
||||||
Array.from({ length: 512 }, () => 0.9),
|
Array.from({ length: 1024 }, () => 0.9),
|
||||||
contextId,
|
contextId,
|
||||||
1,
|
1,
|
||||||
1
|
1
|
||||||
@@ -123,7 +123,7 @@ test('should insert embedding by doc id', async t => {
|
|||||||
{
|
{
|
||||||
await t.context.copilotContext.deleteEmbedding(contextId, 'file-id');
|
await t.context.copilotContext.deleteEmbedding(contextId, 'file-id');
|
||||||
const ret = await t.context.copilotContext.matchContentEmbedding(
|
const ret = await t.context.copilotContext.matchContentEmbedding(
|
||||||
Array.from({ length: 512 }, () => 0.9),
|
Array.from({ length: 1024 }, () => 0.9),
|
||||||
contextId,
|
contextId,
|
||||||
1,
|
1,
|
||||||
1
|
1
|
||||||
@@ -151,7 +151,7 @@ test('should insert embedding by doc id', async t => {
|
|||||||
{
|
{
|
||||||
index: 0,
|
index: 0,
|
||||||
content: 'content',
|
content: 'content',
|
||||||
embedding: Array.from({ length: 512 }, () => 1),
|
embedding: Array.from({ length: 1024 }, () => 1),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -166,7 +166,7 @@ test('should insert embedding by doc id', async t => {
|
|||||||
|
|
||||||
{
|
{
|
||||||
const ret = await t.context.copilotContext.matchWorkspaceEmbedding(
|
const ret = await t.context.copilotContext.matchWorkspaceEmbedding(
|
||||||
Array.from({ length: 512 }, () => 0.9),
|
Array.from({ length: 1024 }, () => 0.9),
|
||||||
workspace.id,
|
workspace.id,
|
||||||
1,
|
1,
|
||||||
1
|
1
|
||||||
|
|||||||
@@ -156,6 +156,16 @@ export class MockCopilotTestProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const cleanObject = (
|
||||||
|
obj: any[] | undefined,
|
||||||
|
condition = ['id', 'status', 'error', 'sessionId', 'createdAt']
|
||||||
|
) =>
|
||||||
|
JSON.parse(
|
||||||
|
JSON.stringify(obj || [], (k, v) =>
|
||||||
|
condition.includes(k) || v === null ? undefined : v
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
export async function createCopilotSession(
|
export async function createCopilotSession(
|
||||||
app: TestingApp,
|
app: TestingApp,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
@@ -224,7 +234,7 @@ export async function createCopilotContext(
|
|||||||
return res.createCopilotContext;
|
return res.createCopilotContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function matchContext(
|
export async function matchFiles(
|
||||||
app: TestingApp,
|
app: TestingApp,
|
||||||
contextId: string,
|
contextId: string,
|
||||||
content: string,
|
content: string,
|
||||||
@@ -240,11 +250,11 @@ export async function matchContext(
|
|||||||
> {
|
> {
|
||||||
const res = await app.gql(
|
const res = await app.gql(
|
||||||
`
|
`
|
||||||
query matchContext($contextId: String!, $content: String!, $limit: SafeInt, $threshold: Float) {
|
query matchFiles($contextId: String!, $content: String!, $limit: SafeInt, $threshold: Float) {
|
||||||
currentUser {
|
currentUser {
|
||||||
copilot {
|
copilot {
|
||||||
contexts(contextId: $contextId) {
|
contexts(contextId: $contextId) {
|
||||||
matchContext(content: $content, limit: $limit, threshold: $threshold) {
|
matchFiles(content: $content, limit: $limit, threshold: $threshold) {
|
||||||
fileId
|
fileId
|
||||||
chunk
|
chunk
|
||||||
content
|
content
|
||||||
@@ -258,7 +268,44 @@ export async function matchContext(
|
|||||||
{ contextId, content, limit, threshold: 1 }
|
{ contextId, content, limit, threshold: 1 }
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.currentUser?.copilot?.contexts?.[0]?.matchContext;
|
return res.currentUser?.copilot?.contexts?.[0]?.matchFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function matchWorkspaceDocs(
|
||||||
|
app: TestingApp,
|
||||||
|
contextId: string,
|
||||||
|
content: string,
|
||||||
|
limit: number
|
||||||
|
): Promise<
|
||||||
|
| {
|
||||||
|
docId: string;
|
||||||
|
chunk: number;
|
||||||
|
content: string;
|
||||||
|
distance: number | null;
|
||||||
|
}[]
|
||||||
|
| undefined
|
||||||
|
> {
|
||||||
|
const res = await app.gql(
|
||||||
|
`
|
||||||
|
query matchWorkspaceDocs($contextId: String!, $content: String!, $limit: SafeInt, $threshold: Float) {
|
||||||
|
currentUser {
|
||||||
|
copilot {
|
||||||
|
contexts(contextId: $contextId) {
|
||||||
|
matchWorkspaceDocs(content: $content, limit: $limit, threshold: $threshold) {
|
||||||
|
docId
|
||||||
|
chunk
|
||||||
|
content
|
||||||
|
distance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
{ contextId, content, limit, threshold: 1 }
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.currentUser?.copilot?.contexts?.[0]?.matchWorkspaceDocs;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listContext(
|
export async function listContext(
|
||||||
@@ -376,7 +423,7 @@ export async function removeContextDoc(
|
|||||||
return res.removeContextDoc;
|
return res.removeContextDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listContextFiles(
|
export async function listContextDocAndFiles(
|
||||||
app: TestingApp,
|
app: TestingApp,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
@@ -385,6 +432,8 @@ export async function listContextFiles(
|
|||||||
| {
|
| {
|
||||||
docs: {
|
docs: {
|
||||||
id: string;
|
id: string;
|
||||||
|
status: string;
|
||||||
|
error: string | null;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
}[];
|
}[];
|
||||||
files: {
|
files: {
|
||||||
@@ -393,6 +442,7 @@ export async function listContextFiles(
|
|||||||
blobId: string;
|
blobId: string;
|
||||||
chunkSize: number;
|
chunkSize: number;
|
||||||
status: string;
|
status: string;
|
||||||
|
error: string | null;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
@@ -405,6 +455,8 @@ export async function listContextFiles(
|
|||||||
contexts(sessionId: "${sessionId}", contextId: "${contextId}") {
|
contexts(sessionId: "${sessionId}", contextId: "${contextId}") {
|
||||||
docs {
|
docs {
|
||||||
id
|
id
|
||||||
|
status
|
||||||
|
error
|
||||||
createdAt
|
createdAt
|
||||||
}
|
}
|
||||||
files {
|
files {
|
||||||
@@ -413,6 +465,7 @@ export async function listContextFiles(
|
|||||||
blobId
|
blobId
|
||||||
chunkSize
|
chunkSize
|
||||||
status
|
status
|
||||||
|
error
|
||||||
createdAt
|
createdAt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export class MockEmbeddingClient extends EmbeddingClient {
|
|||||||
return input.map((_, i) => ({
|
return input.map((_, i) => ({
|
||||||
index: i,
|
index: i,
|
||||||
content: input[i],
|
content: input[i],
|
||||||
embedding: Array.from({ length: 512 }, () => Math.random()),
|
embedding: Array.from({ length: 1024 }, () => Math.random()),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -656,10 +656,10 @@ export class CopilotContextResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ResolveField(() => [ContextMatchedFileChunk], {
|
@ResolveField(() => [ContextMatchedFileChunk], {
|
||||||
description: 'match file context',
|
description: 'match file in context',
|
||||||
})
|
})
|
||||||
@CallMetric('ai', 'context_file_remove')
|
@CallMetric('ai', 'context_file_remove')
|
||||||
async matchContext(
|
async matchFiles(
|
||||||
@Context() ctx: { req: Request },
|
@Context() ctx: { req: Request },
|
||||||
@Parent() context: CopilotContextType,
|
@Parent() context: CopilotContextType,
|
||||||
@Args('content') content: string,
|
@Args('content') content: string,
|
||||||
@@ -667,16 +667,11 @@ export class CopilotContextResolver {
|
|||||||
limit?: number,
|
limit?: number,
|
||||||
@Args('threshold', { type: () => Float, nullable: true })
|
@Args('threshold', { type: () => Float, nullable: true })
|
||||||
threshold?: number
|
threshold?: number
|
||||||
) {
|
): Promise<ContextMatchedFileChunk[]> {
|
||||||
if (!this.context.canEmbedding) {
|
if (!this.context.canEmbedding) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const lockFlag = `${COPILOT_LOCKER}:context:${context.id}`;
|
|
||||||
await using lock = await this.mutex.acquire(lockFlag);
|
|
||||||
if (!lock) {
|
|
||||||
return new TooManyRequest('Server is busy');
|
|
||||||
}
|
|
||||||
const session = await this.context.get(context.id);
|
const session = await this.context.get(context.id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -696,18 +691,20 @@ export class CopilotContextResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ResolveField(() => ContextMatchedDocChunk, {
|
@ResolveField(() => [ContextMatchedDocChunk], {
|
||||||
description: 'match workspace doc content',
|
description: 'match workspace docs',
|
||||||
})
|
})
|
||||||
@CallMetric('ai', 'context_match_workspace_doc')
|
@CallMetric('ai', 'context_match_workspace_doc')
|
||||||
async matchWorkspaceContext(
|
async matchWorkspaceDocs(
|
||||||
@CurrentUser() user: CurrentUser,
|
@CurrentUser() user: CurrentUser,
|
||||||
@Context() ctx: { req: Request },
|
@Context() ctx: { req: Request },
|
||||||
@Parent() context: CopilotContextType,
|
@Parent() context: CopilotContextType,
|
||||||
@Args('content') content: string,
|
@Args('content') content: string,
|
||||||
@Args('limit', { type: () => SafeIntResolver, nullable: true })
|
@Args('limit', { type: () => SafeIntResolver, nullable: true })
|
||||||
limit?: number
|
limit?: number,
|
||||||
) {
|
@Args('threshold', { type: () => Float, nullable: true })
|
||||||
|
threshold?: number
|
||||||
|
): Promise<ContextMatchedDocChunk[]> {
|
||||||
if (!this.context.canEmbedding) {
|
if (!this.context.canEmbedding) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -723,7 +720,8 @@ export class CopilotContextResolver {
|
|||||||
return await session.matchWorkspaceChunks(
|
return await session.matchWorkspaceChunks(
|
||||||
content,
|
content,
|
||||||
limit,
|
limit,
|
||||||
this.getSignal(ctx.req)
|
this.getSignal(ctx.req),
|
||||||
|
threshold
|
||||||
);
|
);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
throw new CopilotFailedToMatchContext({
|
throw new CopilotFailedToMatchContext({
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ export class ContextSession implements AsyncDisposable {
|
|||||||
|
|
||||||
return this.models.copilotContext.matchWorkspaceEmbedding(
|
return this.models.copilotContext.matchWorkspaceEmbedding(
|
||||||
embedding,
|
embedding,
|
||||||
this.id,
|
this.workspaceId,
|
||||||
topK,
|
topK,
|
||||||
threshold
|
threshold
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -109,11 +109,11 @@ type CopilotContext {
|
|||||||
files: [CopilotContextFile!]!
|
files: [CopilotContextFile!]!
|
||||||
id: ID!
|
id: ID!
|
||||||
|
|
||||||
"""match file context"""
|
"""match file in context"""
|
||||||
matchContext(content: String!, limit: SafeInt, threshold: Float): [ContextMatchedFileChunk!]!
|
matchFiles(content: String!, limit: SafeInt, threshold: Float): [ContextMatchedFileChunk!]!
|
||||||
|
|
||||||
"""match workspace doc content"""
|
"""match workspace docs"""
|
||||||
matchWorkspaceContext(content: String!, limit: SafeInt): ContextMatchedDocChunk!
|
matchWorkspaceDocs(content: String!, limit: SafeInt, threshold: Float): [ContextMatchedDocChunk!]!
|
||||||
|
|
||||||
"""list tags in context"""
|
"""list tags in context"""
|
||||||
tags: [CopilotContextCategory!]!
|
tags: [CopilotContextCategory!]!
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
query matchContext($contextId: String!, $content: String!, $limit: SafeInt, $threshold: Float) {
|
||||||
|
currentUser {
|
||||||
|
copilot {
|
||||||
|
contexts(contextId: $contextId) {
|
||||||
|
matchFiles(content: $content, limit: $limit, threshold: $threshold) {
|
||||||
|
fileId
|
||||||
|
chunk
|
||||||
|
content
|
||||||
|
distance
|
||||||
|
}
|
||||||
|
matchWorkspaceDocs(content: $content, limit: $limit, threshold: $threshold) {
|
||||||
|
docId
|
||||||
|
chunk
|
||||||
|
content
|
||||||
|
distance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
query matchWorkspaceContext($contextId: String!, $content: String!, $limit: SafeInt) {
|
query matchWorkspaceDocs($contextId: String!, $content: String!, $limit: SafeInt) {
|
||||||
currentUser {
|
currentUser {
|
||||||
copilot {
|
copilot {
|
||||||
contexts(contextId: $contextId) {
|
contexts(contextId: $contextId) {
|
||||||
matchWorkspaceContext(content: $content, limit: $limit) {
|
matchWorkspaceDocs(content: $content, limit: $limit) {
|
||||||
docId
|
docId
|
||||||
chunk
|
chunk
|
||||||
content
|
content
|
||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
query matchContext($contextId: String!, $content: String!, $limit: SafeInt) {
|
query matchFiles($contextId: String!, $content: String!, $limit: SafeInt) {
|
||||||
currentUser {
|
currentUser {
|
||||||
copilot {
|
copilot {
|
||||||
contexts(contextId: $contextId) {
|
contexts(contextId: $contextId) {
|
||||||
matchContext(content: $content, limit: $limit) {
|
matchFiles(content: $content, limit: $limit) {
|
||||||
fileId
|
fileId
|
||||||
chunk
|
chunk
|
||||||
content
|
content
|
||||||
@@ -205,25 +205,6 @@ export const addContextFileMutation = {
|
|||||||
file: true,
|
file: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const matchContextQuery = {
|
|
||||||
id: 'matchContextQuery' as const,
|
|
||||||
op: 'matchContext',
|
|
||||||
query: `query matchContext($contextId: String!, $content: String!, $limit: SafeInt) {
|
|
||||||
currentUser {
|
|
||||||
copilot {
|
|
||||||
contexts(contextId: $contextId) {
|
|
||||||
matchContext(content: $content, limit: $limit) {
|
|
||||||
fileId
|
|
||||||
chunk
|
|
||||||
content
|
|
||||||
distance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeContextFileMutation = {
|
export const removeContextFileMutation = {
|
||||||
id: 'removeContextFileMutation' as const,
|
id: 'removeContextFileMutation' as const,
|
||||||
op: 'removeContextFile',
|
op: 'removeContextFile',
|
||||||
@@ -295,14 +276,20 @@ export const listContextQuery = {
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const matchWorkspaceContextQuery = {
|
export const matchContextQuery = {
|
||||||
id: 'matchWorkspaceContextQuery' as const,
|
id: 'matchContextQuery' as const,
|
||||||
op: 'matchWorkspaceContext',
|
op: 'matchContext',
|
||||||
query: `query matchWorkspaceContext($contextId: String!, $content: String!, $limit: SafeInt) {
|
query: `query matchContext($contextId: String!, $content: String!, $limit: SafeInt, $threshold: Float) {
|
||||||
currentUser {
|
currentUser {
|
||||||
copilot {
|
copilot {
|
||||||
contexts(contextId: $contextId) {
|
contexts(contextId: $contextId) {
|
||||||
matchWorkspaceContext(content: $content, limit: $limit) {
|
matchFiles(content: $content, limit: $limit, threshold: $threshold) {
|
||||||
|
fileId
|
||||||
|
chunk
|
||||||
|
content
|
||||||
|
distance
|
||||||
|
}
|
||||||
|
matchWorkspaceDocs(content: $content, limit: $limit, threshold: $threshold) {
|
||||||
docId
|
docId
|
||||||
chunk
|
chunk
|
||||||
content
|
content
|
||||||
@@ -314,6 +301,44 @@ export const matchWorkspaceContextQuery = {
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const matchWorkspaceDocsQuery = {
|
||||||
|
id: 'matchWorkspaceDocsQuery' as const,
|
||||||
|
op: 'matchWorkspaceDocs',
|
||||||
|
query: `query matchWorkspaceDocs($contextId: String!, $content: String!, $limit: SafeInt) {
|
||||||
|
currentUser {
|
||||||
|
copilot {
|
||||||
|
contexts(contextId: $contextId) {
|
||||||
|
matchWorkspaceDocs(content: $content, limit: $limit) {
|
||||||
|
docId
|
||||||
|
chunk
|
||||||
|
content
|
||||||
|
distance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const matchFilesQuery = {
|
||||||
|
id: 'matchFilesQuery' as const,
|
||||||
|
op: 'matchFiles',
|
||||||
|
query: `query matchFiles($contextId: String!, $content: String!, $limit: SafeInt) {
|
||||||
|
currentUser {
|
||||||
|
copilot {
|
||||||
|
contexts(contextId: $contextId) {
|
||||||
|
matchFiles(content: $content, limit: $limit) {
|
||||||
|
fileId
|
||||||
|
chunk
|
||||||
|
content
|
||||||
|
distance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
|
||||||
export const getWorkspaceEmbeddingStatusQuery = {
|
export const getWorkspaceEmbeddingStatusQuery = {
|
||||||
id: 'getWorkspaceEmbeddingStatusQuery' as const,
|
id: 'getWorkspaceEmbeddingStatusQuery' as const,
|
||||||
op: 'getWorkspaceEmbeddingStatus',
|
op: 'getWorkspaceEmbeddingStatus',
|
||||||
|
|||||||
@@ -172,24 +172,25 @@ export interface CopilotContext {
|
|||||||
/** list files in context */
|
/** list files in context */
|
||||||
files: Array<CopilotContextFile>;
|
files: Array<CopilotContextFile>;
|
||||||
id: Scalars['ID']['output'];
|
id: Scalars['ID']['output'];
|
||||||
/** match file context */
|
/** match file in context */
|
||||||
matchContext: Array<ContextMatchedFileChunk>;
|
matchFiles: Array<ContextMatchedFileChunk>;
|
||||||
/** match workspace doc content */
|
/** match workspace docs */
|
||||||
matchWorkspaceContext: ContextMatchedDocChunk;
|
matchWorkspaceDocs: Array<ContextMatchedDocChunk>;
|
||||||
/** list tags in context */
|
/** list tags in context */
|
||||||
tags: Array<CopilotContextCategory>;
|
tags: Array<CopilotContextCategory>;
|
||||||
workspaceId: Scalars['String']['output'];
|
workspaceId: Scalars['String']['output'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CopilotContextMatchContextArgs {
|
export interface CopilotContextMatchFilesArgs {
|
||||||
content: Scalars['String']['input'];
|
content: Scalars['String']['input'];
|
||||||
limit?: InputMaybe<Scalars['SafeInt']['input']>;
|
limit?: InputMaybe<Scalars['SafeInt']['input']>;
|
||||||
threshold?: InputMaybe<Scalars['Float']['input']>;
|
threshold?: InputMaybe<Scalars['Float']['input']>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CopilotContextMatchWorkspaceContextArgs {
|
export interface CopilotContextMatchWorkspaceDocsArgs {
|
||||||
content: Scalars['String']['input'];
|
content: Scalars['String']['input'];
|
||||||
limit?: InputMaybe<Scalars['SafeInt']['input']>;
|
limit?: InputMaybe<Scalars['SafeInt']['input']>;
|
||||||
|
threshold?: InputMaybe<Scalars['Float']['input']>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CopilotContextCategory {
|
export interface CopilotContextCategory {
|
||||||
@@ -2562,32 +2563,6 @@ export type AddContextFileMutation = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MatchContextQueryVariables = Exact<{
|
|
||||||
contextId: Scalars['String']['input'];
|
|
||||||
content: Scalars['String']['input'];
|
|
||||||
limit?: InputMaybe<Scalars['SafeInt']['input']>;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export type MatchContextQuery = {
|
|
||||||
__typename?: 'Query';
|
|
||||||
currentUser: {
|
|
||||||
__typename?: 'UserType';
|
|
||||||
copilot: {
|
|
||||||
__typename?: 'Copilot';
|
|
||||||
contexts: Array<{
|
|
||||||
__typename?: 'CopilotContext';
|
|
||||||
matchContext: Array<{
|
|
||||||
__typename?: 'ContextMatchedFileChunk';
|
|
||||||
fileId: string;
|
|
||||||
chunk: number;
|
|
||||||
content: string;
|
|
||||||
distance: number | null;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
} | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RemoveContextFileMutationVariables = Exact<{
|
export type RemoveContextFileMutationVariables = Exact<{
|
||||||
options: RemoveContextFileInput;
|
options: RemoveContextFileInput;
|
||||||
}>;
|
}>;
|
||||||
@@ -2677,13 +2652,14 @@ export type ListContextQuery = {
|
|||||||
} | null;
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MatchWorkspaceContextQueryVariables = Exact<{
|
export type MatchContextQueryVariables = Exact<{
|
||||||
contextId: Scalars['String']['input'];
|
contextId: Scalars['String']['input'];
|
||||||
content: Scalars['String']['input'];
|
content: Scalars['String']['input'];
|
||||||
limit?: InputMaybe<Scalars['SafeInt']['input']>;
|
limit?: InputMaybe<Scalars['SafeInt']['input']>;
|
||||||
|
threshold?: InputMaybe<Scalars['Float']['input']>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type MatchWorkspaceContextQuery = {
|
export type MatchContextQuery = {
|
||||||
__typename?: 'Query';
|
__typename?: 'Query';
|
||||||
currentUser: {
|
currentUser: {
|
||||||
__typename?: 'UserType';
|
__typename?: 'UserType';
|
||||||
@@ -2691,13 +2667,72 @@ export type MatchWorkspaceContextQuery = {
|
|||||||
__typename?: 'Copilot';
|
__typename?: 'Copilot';
|
||||||
contexts: Array<{
|
contexts: Array<{
|
||||||
__typename?: 'CopilotContext';
|
__typename?: 'CopilotContext';
|
||||||
matchWorkspaceContext: {
|
matchFiles: Array<{
|
||||||
|
__typename?: 'ContextMatchedFileChunk';
|
||||||
|
fileId: string;
|
||||||
|
chunk: number;
|
||||||
|
content: string;
|
||||||
|
distance: number | null;
|
||||||
|
}>;
|
||||||
|
matchWorkspaceDocs: Array<{
|
||||||
__typename?: 'ContextMatchedDocChunk';
|
__typename?: 'ContextMatchedDocChunk';
|
||||||
docId: string;
|
docId: string;
|
||||||
chunk: number;
|
chunk: number;
|
||||||
content: string;
|
content: string;
|
||||||
distance: number | null;
|
distance: number | null;
|
||||||
};
|
}>;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MatchWorkspaceDocsQueryVariables = Exact<{
|
||||||
|
contextId: Scalars['String']['input'];
|
||||||
|
content: Scalars['String']['input'];
|
||||||
|
limit?: InputMaybe<Scalars['SafeInt']['input']>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type MatchWorkspaceDocsQuery = {
|
||||||
|
__typename?: 'Query';
|
||||||
|
currentUser: {
|
||||||
|
__typename?: 'UserType';
|
||||||
|
copilot: {
|
||||||
|
__typename?: 'Copilot';
|
||||||
|
contexts: Array<{
|
||||||
|
__typename?: 'CopilotContext';
|
||||||
|
matchWorkspaceDocs: Array<{
|
||||||
|
__typename?: 'ContextMatchedDocChunk';
|
||||||
|
docId: string;
|
||||||
|
chunk: number;
|
||||||
|
content: string;
|
||||||
|
distance: number | null;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MatchFilesQueryVariables = Exact<{
|
||||||
|
contextId: Scalars['String']['input'];
|
||||||
|
content: Scalars['String']['input'];
|
||||||
|
limit?: InputMaybe<Scalars['SafeInt']['input']>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type MatchFilesQuery = {
|
||||||
|
__typename?: 'Query';
|
||||||
|
currentUser: {
|
||||||
|
__typename?: 'UserType';
|
||||||
|
copilot: {
|
||||||
|
__typename?: 'Copilot';
|
||||||
|
contexts: Array<{
|
||||||
|
__typename?: 'CopilotContext';
|
||||||
|
matchFiles: Array<{
|
||||||
|
__typename?: 'ContextMatchedFileChunk';
|
||||||
|
fileId: string;
|
||||||
|
chunk: number;
|
||||||
|
content: string;
|
||||||
|
distance: number | null;
|
||||||
|
}>;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
} | null;
|
} | null;
|
||||||
@@ -4315,11 +4350,6 @@ export type Queries =
|
|||||||
variables: ListBlobsQueryVariables;
|
variables: ListBlobsQueryVariables;
|
||||||
response: ListBlobsQuery;
|
response: ListBlobsQuery;
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
name: 'matchContextQuery';
|
|
||||||
variables: MatchContextQueryVariables;
|
|
||||||
response: MatchContextQuery;
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
name: 'listContextObjectQuery';
|
name: 'listContextObjectQuery';
|
||||||
variables: ListContextObjectQueryVariables;
|
variables: ListContextObjectQueryVariables;
|
||||||
@@ -4331,9 +4361,19 @@ export type Queries =
|
|||||||
response: ListContextQuery;
|
response: ListContextQuery;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
name: 'matchWorkspaceContextQuery';
|
name: 'matchContextQuery';
|
||||||
variables: MatchWorkspaceContextQueryVariables;
|
variables: MatchContextQueryVariables;
|
||||||
response: MatchWorkspaceContextQuery;
|
response: MatchContextQuery;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
name: 'matchWorkspaceDocsQuery';
|
||||||
|
variables: MatchWorkspaceDocsQueryVariables;
|
||||||
|
response: MatchWorkspaceDocsQuery;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
name: 'matchFilesQuery';
|
||||||
|
variables: MatchFilesQueryVariables;
|
||||||
|
response: MatchFilesQuery;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
name: 'getWorkspaceEmbeddingStatusQuery';
|
name: 'getWorkspaceEmbeddingStatusQuery';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
ChatHistoryOrder,
|
ChatHistoryOrder,
|
||||||
|
ContextMatchedDocChunk,
|
||||||
ContextMatchedFileChunk,
|
ContextMatchedFileChunk,
|
||||||
CopilotContextCategory,
|
CopilotContextCategory,
|
||||||
CopilotContextDoc,
|
CopilotContextDoc,
|
||||||
@@ -312,7 +313,10 @@ declare global {
|
|||||||
contextId: string,
|
contextId: string,
|
||||||
content: string,
|
content: string,
|
||||||
limit?: number
|
limit?: number
|
||||||
) => Promise<ContextMatchedFileChunk[] | undefined>;
|
) => Promise<{
|
||||||
|
files?: ContextMatchedFileChunk[];
|
||||||
|
docs?: ContextMatchedDocChunk[];
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(@Peng): should be refactored to get rid of implement details (like messages, action, role, etc.)
|
// TODO(@Peng): should be refactored to get rid of implement details (like messages, action, role, etc.)
|
||||||
|
|||||||
@@ -556,9 +556,12 @@ export class ChatPanelInput extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
|
|
||||||
private async _getMatchedContexts(userInput: string) {
|
private async _getMatchedContexts(userInput: string) {
|
||||||
const contextId = await this.getContextId();
|
const contextId = await this.getContextId();
|
||||||
const matched = contextId
|
// TODO(@akumatus): adapt workspace docs
|
||||||
? (await AIProvider.context?.matchContext(contextId, userInput)) || []
|
const { files: matched = [] } =
|
||||||
: [];
|
(contextId &&
|
||||||
|
(await AIProvider.context?.matchContext(contextId, userInput))) ||
|
||||||
|
{};
|
||||||
|
|
||||||
const contexts = this.chatContextValue.chips.reduce(
|
const contexts = this.chatContextValue.chips.reduce(
|
||||||
(acc, chip, index) => {
|
(acc, chip, index) => {
|
||||||
if (chip.state !== 'finished') {
|
if (chip.state !== 'finished') {
|
||||||
|
|||||||
@@ -341,7 +341,9 @@ export class CopilotClient {
|
|||||||
limit,
|
limit,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return res.currentUser?.copilot?.contexts?.[0]?.matchContext;
|
const { matchFiles: files, matchWorkspaceDocs: docs } =
|
||||||
|
res.currentUser?.copilot?.contexts?.[0] || {};
|
||||||
|
return { files, docs };
|
||||||
}
|
}
|
||||||
|
|
||||||
async chatText({
|
async chatText({
|
||||||
|
|||||||
Reference in New Issue
Block a user