Skip to content

Commit 85b7ddf

Browse files
authored
fix(genkit-tools/evals): Switch to JSON index in EvalStore (#3196)
1 parent 032ef9b commit 85b7ddf

File tree

5 files changed

+215
-186
lines changed

5 files changed

+215
-186
lines changed

genkit-tools/common/src/eval/evaluate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ export async function runEvaluation(params: {
207207
};
208208

209209
logger.info('Finished evaluation, writing key...');
210-
const evalStore = getEvalStore();
210+
const evalStore = await getEvalStore();
211211
await evalStore.save(evalRun);
212212
return evalRun;
213213
}

genkit-tools/common/src/eval/index.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,8 @@ export * from './exporter';
2323
export * from './parser';
2424
export * from './validate';
2525

26-
export function getEvalStore(): EvalStore {
27-
// TODO: This should provide EvalStore, based on tools config.
28-
return LocalFileEvalStore.getEvalStore();
26+
export async function getEvalStore(): Promise<EvalStore> {
27+
return await LocalFileEvalStore.getEvalStore();
2928
}
3029

3130
export function getDatasetStore(): DatasetStore {

genkit-tools/common/src/eval/localFileEvalStore.ts

Lines changed: 61 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
*/
1616

1717
import fs from 'fs';
18-
import { appendFile, readFile, unlink, writeFile } from 'fs/promises';
18+
import { readFile, unlink, writeFile } from 'fs/promises';
1919
import path from 'path';
20-
import * as readline from 'readline';
20+
import { createInterface } from 'readline';
2121
import type { ListEvalKeysRequest, ListEvalKeysResponse } from '../types/apis';
2222
import {
2323
EvalRunKeySchema,
@@ -32,26 +32,26 @@ import { logger } from '../utils/logger';
3232
* A local, file-based EvalStore implementation.
3333
*/
3434
export class LocalFileEvalStore implements EvalStore {
35-
private readonly storeRoot;
36-
private readonly indexFile;
37-
private readonly INDEX_DELIMITER = '\n';
35+
private storeRoot: string = '';
36+
private indexFile: string = '';
3837
private static cachedEvalStore: LocalFileEvalStore | null = null;
3938

40-
private constructor() {
39+
private async init() {
4140
this.storeRoot = this.generateRootPath();
42-
this.indexFile = this.getIndexFilePath();
41+
this.indexFile = await this.resolveIndexFile();
4342
fs.mkdirSync(this.storeRoot, { recursive: true });
4443
if (!fs.existsSync(this.indexFile)) {
45-
fs.writeFileSync(path.resolve(this.indexFile), '');
44+
fs.writeFileSync(path.resolve(this.indexFile), JSON.stringify({}));
4645
}
4746
logger.debug(
4847
`Initialized local file eval store at root: ${this.storeRoot}`
4948
);
5049
}
5150

52-
static getEvalStore() {
51+
static async getEvalStore() {
5352
if (!this.cachedEvalStore) {
5453
this.cachedEvalStore = new LocalFileEvalStore();
54+
await this.cachedEvalStore.init();
5555
}
5656
return this.cachedEvalStore;
5757
}
@@ -61,7 +61,7 @@ export class LocalFileEvalStore implements EvalStore {
6161
}
6262

6363
async save(evalRun: EvalRun): Promise<void> {
64-
const fileName = this.generateFileName(evalRun.key.evalRunId);
64+
const fileName = this.resolveEvalFilename(evalRun.key.evalRunId);
6565

6666
logger.debug(
6767
`Saving EvalRun ${evalRun.key.evalRunId} to ` +
@@ -72,20 +72,18 @@ export class LocalFileEvalStore implements EvalStore {
7272
JSON.stringify(evalRun)
7373
);
7474

75-
logger.debug(
76-
`Save EvalRunKey ${JSON.stringify(evalRun.key)} to ` +
77-
path.resolve(this.indexFile)
78-
);
79-
await appendFile(
75+
const index = await this.getEvalsIndex();
76+
index[evalRun.key.evalRunId] = evalRun.key;
77+
await writeFile(
8078
path.resolve(this.indexFile),
81-
JSON.stringify(evalRun.key) + this.INDEX_DELIMITER
79+
JSON.stringify(index, null, 2)
8280
);
8381
}
8482

8583
async load(evalRunId: string): Promise<EvalRun | undefined> {
8684
const filePath = path.resolve(
8785
this.storeRoot,
88-
this.generateFileName(evalRunId)
86+
this.resolveEvalFilename(evalRunId)
8987
);
9088
if (!fs.existsSync(filePath)) {
9189
return undefined;
@@ -98,22 +96,11 @@ export class LocalFileEvalStore implements EvalStore {
9896
async list(
9997
query?: ListEvalKeysRequest | undefined
10098
): Promise<ListEvalKeysResponse> {
101-
let keys = await readFile(this.indexFile, 'utf8').then((data) => {
102-
if (!data) {
103-
return [];
104-
}
105-
// strip the final carriage return before parsing all lines
106-
return data
107-
.slice(0, -1)
108-
.split(this.INDEX_DELIMITER)
109-
.map(this.parseLineToKey);
110-
});
111-
112-
logger.debug(`Found keys: ${JSON.stringify(keys)}`);
99+
logger.debug(`Listing keys for filter: ${JSON.stringify(query)}`);
100+
let keys = await this.getEvalsIndex().then((index) => Object.values(index));
113101

114102
if (query?.filter?.actionRef) {
115103
keys = keys.filter((key) => key.actionRef === query?.filter?.actionRef);
116-
logger.debug(`Filtered keys: ${JSON.stringify(keys)}`);
117104
}
118105

119106
return {
@@ -124,50 +111,66 @@ export class LocalFileEvalStore implements EvalStore {
124111
async delete(evalRunId: string): Promise<void> {
125112
const filePath = path.resolve(
126113
this.storeRoot,
127-
this.generateFileName(evalRunId)
114+
this.resolveEvalFilename(evalRunId)
128115
);
129-
if (!fs.existsSync(filePath)) {
130-
throw new Error(`Cannot find evalRun with id '${evalRunId}'`);
116+
if (fs.existsSync(filePath)) {
117+
await unlink(filePath);
118+
119+
const index = await this.getEvalsIndex();
120+
delete index[evalRunId];
121+
await writeFile(
122+
path.resolve(this.indexFile),
123+
JSON.stringify(index, null, 2)
124+
);
131125
}
132-
return await unlink(filePath).then(() =>
133-
this.deleteEvalRunFromIndex(evalRunId)
134-
);
135126
}
136127

137-
private generateFileName(evalRunId: string): string {
128+
private resolveEvalFilename(evalRunId: string): string {
138129
return `${evalRunId}.json`;
139130
}
140131

141-
private getIndexFilePath(): string {
142-
return path.resolve(this.storeRoot, 'index.txt');
132+
private async resolveIndexFile(): Promise<string> {
133+
const txtPath = path.resolve(this.storeRoot, 'index.txt');
134+
const jsonPath = path.resolve(this.storeRoot, 'index.json');
135+
if (fs.existsSync(txtPath)) {
136+
// Copy over index, delete txt file
137+
const keys = await this.processLineByLine(txtPath);
138+
await writeFile(path.resolve(jsonPath), JSON.stringify(keys, null, 2));
139+
await unlink(txtPath);
140+
}
141+
return jsonPath;
143142
}
144143

145-
private parseLineToKey(key: string): EvalRunKey {
146-
return EvalRunKeySchema.parse(JSON.parse(key));
147-
}
144+
private async processLineByLine(filePath: string) {
145+
const fileStream = fs.createReadStream(filePath);
146+
const keys: Record<string, EvalRunKey> = {};
148147

149-
private generateRootPath(): string {
150-
return path.resolve(process.cwd(), `.genkit/evals`);
151-
}
152-
153-
private async deleteEvalRunFromIndex(evalRunId: string): Promise<void> {
154-
const entries = [];
155-
const fileStream = fs.createReadStream(this.getIndexFilePath());
156-
const rl = readline.createInterface({
148+
const rl = createInterface({
157149
input: fileStream,
150+
crlfDelay: Infinity,
158151
});
159-
160152
for await (const line of rl) {
161-
const entry = EvalRunKeySchema.parse(JSON.parse(line));
162-
if (entry.evalRunId !== evalRunId) {
163-
entries.push(line);
153+
try {
154+
const entry = JSON.parse(line);
155+
const runKey = EvalRunKeySchema.parse(entry);
156+
keys[runKey.evalRunId] = runKey;
157+
} catch (e) {
158+
logger.debug(`Error parsing ${line}:\n`, JSON.stringify(e));
164159
}
165160
}
161+
return keys;
162+
}
166163

167-
await writeFile(
168-
this.getIndexFilePath(),
169-
// end with delimiter to parse correctly
170-
entries.join(this.INDEX_DELIMITER) + this.INDEX_DELIMITER
164+
private generateRootPath(): string {
165+
return path.resolve(process.cwd(), '.genkit', 'evals');
166+
}
167+
168+
private async getEvalsIndex(): Promise<Record<string, EvalRunKey>> {
169+
if (!fs.existsSync(this.indexFile)) {
170+
return Promise.resolve({} as any);
171+
}
172+
return await readFile(path.resolve(this.indexFile), 'utf8').then((data) =>
173+
JSON.parse(data)
171174
);
172175
}
173176
}

genkit-tools/common/src/server/router.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,8 @@ export const TOOLS_SERVER_ROUTER = (manager: RuntimeManager) =>
169169
.input(apis.ListEvalKeysRequestSchema)
170170
.output(apis.ListEvalKeysResponseSchema)
171171
.query(async ({ input }) => {
172-
const response = await getEvalStore().list(input);
172+
const store = await getEvalStore();
173+
const response = await store.list(input);
173174
return {
174175
evalRunKeys: response.evalRunKeys,
175176
};
@@ -182,7 +183,8 @@ export const TOOLS_SERVER_ROUTER = (manager: RuntimeManager) =>
182183
.query(async ({ input }) => {
183184
const parts = input.name.split('/');
184185
const evalRunId = parts[1];
185-
const evalRun = await getEvalStore().load(evalRunId);
186+
const store = await getEvalStore();
187+
const evalRun = await store.load(evalRunId);
186188
if (!evalRun) {
187189
throw new TRPCError({
188190
code: 'NOT_FOUND',
@@ -198,7 +200,8 @@ export const TOOLS_SERVER_ROUTER = (manager: RuntimeManager) =>
198200
.mutation(async ({ input }) => {
199201
const parts = input.name.split('/');
200202
const evalRunId = parts[1];
201-
await getEvalStore().delete(evalRunId);
203+
const store = await getEvalStore();
204+
await store.delete(evalRunId);
202205
}),
203206

204207
/** Retrieves all eval datasets */

0 commit comments

Comments
 (0)