Refactor docx file paths and update test fixtures for workspace integration; add caching for KB fixture vectors
This commit is contained in:
1 parent
47f93e03e3
commit
6f51d22c95
10 files changed
+205
-9
No files matched your search
@@ -6,4 +6,5 @@ out
|
||||
.eslintcache
|
||||
*.log*
|
||||
tests/logs
|
||||
tests/.cache
|
||||
artifacts/ai-test-results
|
||||
@@ -287,8 +287,7 @@ export function registerHandlers(services: {
|
||||
process.cwd(),
|
||||
'tests',
|
||||
'fixtures',
|
||||
'ai',
|
||||
'docx',
|
||||
'workspace',
|
||||
'sample-edit.docx'
|
||||
)
|
||||
|
||||
|
||||
@@ -45,8 +45,8 @@ type DocxToolSpec<
|
||||
activityActionType?: string
|
||||
}
|
||||
|
||||
const DOCX_SOURCE_PATH_EXAMPLE = 'ai/docx/图形学.docx'
|
||||
const DOCX_CREATE_TARGET_EXAMPLE = 'ai/docx/深度学习与图形学复习总结.docx'
|
||||
const DOCX_SOURCE_PATH_EXAMPLE = '学习笔记/图形学.docx'
|
||||
const DOCX_CREATE_TARGET_EXAMPLE = '深度学习与图形学复习总结.docx'
|
||||
const DOCX_SEARCH_LITERAL_EXAMPLE: JsonObject = {
|
||||
sourcePath: DOCX_SOURCE_PATH_EXAMPLE,
|
||||
query: '反走样'
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
type DocxSearchResult
|
||||
} from './testUtils'
|
||||
|
||||
const SAMPLE_DOCX_FIXTURE = path.resolve('tests/fixtures/ai/docx/sample-edit.docx')
|
||||
const SAMPLE_DOCX_FIXTURE = path.resolve('tests/fixtures/workspace/sample-edit.docx')
|
||||
|
||||
describe('ai tool docx backend', () => {
|
||||
it('docx-minimal-edit-flow', async () => {
|
||||
|
||||
@@ -20,7 +20,7 @@ type PositionedOutlineResult = {
|
||||
}>
|
||||
}
|
||||
|
||||
const POSITIONED_FIXTURE_PATH = 'tests/fixtures/ai/docx/图形学.docx'
|
||||
const POSITIONED_FIXTURE_PATH = 'tests/fixtures/workspace/学习笔记/图形学.docx'
|
||||
const POSITIONED_READ_ONLY_MESSAGE = '当前命中复杂定位排版区域,暂不支持自动编辑'
|
||||
|
||||
describe('ai tool docx backend', () => {
|
||||
|
||||
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,181 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { access, mkdir, readFile, writeFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import type { EmbeddingService } from '../../../src/main/services/vector/EmbeddingService'
|
||||
import type { SeedKbDocumentInput } from './test-types'
|
||||
|
||||
const KB_FIXTURE_CACHE_SCHEMA_VERSION = 1
|
||||
const KB_FIXTURE_CACHE_ROOT = path.resolve(process.cwd(), 'tests', '.cache', 'kb-fixtures')
|
||||
|
||||
type CacheableKbChunk = {
|
||||
id: string
|
||||
text: string
|
||||
page: number
|
||||
}
|
||||
|
||||
type CacheableKbDocument = {
|
||||
id: string
|
||||
name: string
|
||||
sourceType: 'file' | 'web'
|
||||
chunks: CacheableKbChunk[]
|
||||
}
|
||||
|
||||
type CachedKbChunkVector = CacheableKbChunk & {
|
||||
vector: number[]
|
||||
}
|
||||
|
||||
type CachedKbDocumentVectors = {
|
||||
id: string
|
||||
chunks: CachedKbChunkVector[]
|
||||
}
|
||||
|
||||
type KbFixtureCacheFile = {
|
||||
schemaVersion: 1
|
||||
modelName: string
|
||||
documents: CachedKbDocumentVectors[]
|
||||
}
|
||||
|
||||
type KbFixtureVectorSnapshot = {
|
||||
documents: CachedKbDocumentVectors[]
|
||||
}
|
||||
|
||||
const normalizeSourceType = (value?: 'file' | 'web'): 'file' | 'web' => value || 'file'
|
||||
|
||||
const toCacheableDocuments = (documents: readonly SeedKbDocumentInput[]): CacheableKbDocument[] =>
|
||||
[...documents]
|
||||
.map((document) => ({
|
||||
id: document.id,
|
||||
name: document.name,
|
||||
sourceType: normalizeSourceType(document.sourceType),
|
||||
chunks: [...document.chunks]
|
||||
.map((chunk) => ({
|
||||
id: chunk.id,
|
||||
text: chunk.text,
|
||||
page: chunk.page || 1
|
||||
}))
|
||||
.sort((left, right) => left.id.localeCompare(right.id))
|
||||
}))
|
||||
.sort((left, right) => left.id.localeCompare(right.id))
|
||||
|
||||
const buildCacheKey = (
|
||||
modelName: string,
|
||||
documents: readonly CacheableKbDocument[]
|
||||
): string =>
|
||||
createHash('sha256')
|
||||
.update(
|
||||
JSON.stringify({
|
||||
schemaVersion: KB_FIXTURE_CACHE_SCHEMA_VERSION,
|
||||
modelName,
|
||||
documents
|
||||
})
|
||||
)
|
||||
.digest('hex')
|
||||
|
||||
const buildCacheFilePath = (cacheKey: string): string =>
|
||||
path.join(KB_FIXTURE_CACHE_ROOT, `${cacheKey}.json`)
|
||||
|
||||
const loadCachedSnapshot = async (
|
||||
cacheFilePath: string,
|
||||
modelName: string
|
||||
): Promise<KbFixtureVectorSnapshot | null> => {
|
||||
try {
|
||||
await access(cacheFilePath)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
|
||||
const file = JSON.parse(await readFile(cacheFilePath, 'utf8')) as KbFixtureCacheFile
|
||||
if (
|
||||
file.schemaVersion !== KB_FIXTURE_CACHE_SCHEMA_VERSION ||
|
||||
file.modelName !== modelName
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
documents: file.documents
|
||||
}
|
||||
}
|
||||
|
||||
const persistCachedSnapshot = async (
|
||||
cacheFilePath: string,
|
||||
modelName: string,
|
||||
snapshot: KbFixtureVectorSnapshot
|
||||
): Promise<void> => {
|
||||
const payload: KbFixtureCacheFile = {
|
||||
schemaVersion: KB_FIXTURE_CACHE_SCHEMA_VERSION,
|
||||
modelName,
|
||||
documents: snapshot.documents
|
||||
}
|
||||
|
||||
await mkdir(path.dirname(cacheFilePath), { recursive: true })
|
||||
await writeFile(cacheFilePath, JSON.stringify(payload), 'utf8')
|
||||
}
|
||||
|
||||
const buildVectorSnapshot = async (
|
||||
documents: readonly CacheableKbDocument[],
|
||||
embeddingService: EmbeddingService
|
||||
): Promise<KbFixtureVectorSnapshot> => {
|
||||
if (documents.length === 0) {
|
||||
return {
|
||||
documents: []
|
||||
}
|
||||
}
|
||||
|
||||
const flattenedChunks = documents.flatMap((document) =>
|
||||
document.chunks.map((chunk) => ({
|
||||
documentId: document.id,
|
||||
chunk
|
||||
}))
|
||||
)
|
||||
if (flattenedChunks.length === 0) {
|
||||
return {
|
||||
documents: documents.map((document) => ({
|
||||
id: document.id,
|
||||
chunks: []
|
||||
}))
|
||||
}
|
||||
}
|
||||
const embeddingTexts = flattenedChunks.map((item) => item.chunk.text)
|
||||
const embeddings = await embeddingService.embed(embeddingTexts)
|
||||
const vectorSize = embeddings.dims[1]
|
||||
|
||||
const vectors = flattenedChunks.map((_, index) =>
|
||||
Array.from(
|
||||
embeddings.data.slice(index * vectorSize, (index + 1) * vectorSize)
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
documents: documents.map((document) => ({
|
||||
id: document.id,
|
||||
chunks: document.chunks.map((chunk) => {
|
||||
const flattenedIndex = flattenedChunks.findIndex(
|
||||
(item) => item.documentId === document.id && item.chunk.id === chunk.id
|
||||
)
|
||||
return {
|
||||
...chunk,
|
||||
vector: vectors[flattenedIndex] || []
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
export const loadOrBuildKbFixtureVectors = async (input: {
|
||||
embeddingService: EmbeddingService
|
||||
documents: readonly SeedKbDocumentInput[]
|
||||
}): Promise<KbFixtureVectorSnapshot> => {
|
||||
const cacheableDocuments = toCacheableDocuments(input.documents)
|
||||
const modelName = input.embeddingService.getModelName()
|
||||
const cacheKey = buildCacheKey(modelName, cacheableDocuments)
|
||||
const cacheFilePath = buildCacheFilePath(cacheKey)
|
||||
const cachedSnapshot = await loadCachedSnapshot(cacheFilePath, modelName)
|
||||
if (cachedSnapshot) {
|
||||
return cachedSnapshot
|
||||
}
|
||||
|
||||
const builtSnapshot = await buildVectorSnapshot(cacheableDocuments, input.embeddingService)
|
||||
await persistCachedSnapshot(cacheFilePath, modelName, builtSnapshot)
|
||||
return builtSnapshot
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { DocumentsStore } from '../../../src/main/services/storage/domains/DocumentsStore'
|
||||
import type { EmbeddingService } from '../../../src/main/services/vector/EmbeddingService'
|
||||
import type { SeedKbDocumentInput } from './test-types'
|
||||
import { loadOrBuildKbFixtureVectors } from './kbFixtureCache'
|
||||
|
||||
export const seedKbFixture = async (input: {
|
||||
documentsStore: DocumentsStore
|
||||
@@ -17,6 +18,11 @@ export const seedKbFixture = async (input: {
|
||||
)
|
||||
}
|
||||
|
||||
const vectorSnapshot = await loadOrBuildKbFixtureVectors({
|
||||
embeddingService: input.embeddingService,
|
||||
documents: input.documents
|
||||
})
|
||||
|
||||
for (const document of input.documents) {
|
||||
const workspaceId = String(document.workspaceId || 'test-workspace').trim()
|
||||
await input.documentsStore.addDocument({
|
||||
@@ -40,10 +46,19 @@ export const seedKbFixture = async (input: {
|
||||
page: chunk.page || 1
|
||||
}
|
||||
}))
|
||||
const embeddings = await input.embeddingService.embed(chunks.map((chunk) => chunk.text))
|
||||
const vectorList = chunks.map((_, index) =>
|
||||
embeddings.data.slice(index * embeddings.dims[1], (index + 1) * embeddings.dims[1])
|
||||
const cachedDocument = vectorSnapshot.documents.find(
|
||||
(item) => item.id === document.id
|
||||
)
|
||||
if (!cachedDocument) {
|
||||
throw new Error(`cached KB vectors missing document: ${document.id}`)
|
||||
}
|
||||
const vectorList = document.chunks.map((chunk) => {
|
||||
const cachedChunk = cachedDocument.chunks.find((item) => item.id === chunk.id)
|
||||
if (!cachedChunk) {
|
||||
throw new Error(`cached KB vectors missing chunk: ${document.id}/${chunk.id}`)
|
||||
}
|
||||
return Float32Array.from(cachedChunk.vector)
|
||||
})
|
||||
|
||||
await input.documentsStore.addChunks({
|
||||
vectors: vectorList,
|
||||
|
||||
Reference in new issue
Block a user