Skip to content

Commit 2b23415

Browse files
authored
knowpro: Better diagnostics, replayability (#1310)
For debugging, testing and co-dev with Python: * Automatically dump kpSearch and kpAnswer results with timestamps in log folder * kpSearch: Run previously saved, or hand edited NLP SearchQuery expr from file * Better search expression comparison - for diffs * Save validation run report * Some comments * Wrapper API to run multiple searches
1 parent 9ed6d2e commit 2b23415

File tree

13 files changed

+313
-55
lines changed

13 files changed

+313
-55
lines changed

ts/examples/chat/src/memory/knowproMemory.ts

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ import {
2323
parseFreeAndNamedArguments,
2424
keyValuesFromNamedArgs,
2525
} from "../common.js";
26-
import { collections, dateTime, ensureDir } from "typeagent";
26+
import {
27+
collections,
28+
dateTime,
29+
ensureDir,
30+
isFilePath,
31+
readJsonFile,
32+
} from "typeagent";
2733
import chalk from "chalk";
2834
import { KnowProPrinter } from "./knowproPrinter.js";
2935
import { createKnowproDataFrameCommands } from "./knowproDataFrame.js";
@@ -277,16 +283,38 @@ export async function createKnowproCommands(
277283
return;
278284
}
279285
const namedArgs = parseNamedArguments(args, searchDef());
286+
let savedQuery: kp.querySchema.SearchQuery | undefined;
287+
if (isFilePath(namedArgs.query)) {
288+
const queryPath = namedArgs.query;
289+
const savedContext = await loadSavedDebugContext(queryPath);
290+
if (!savedContext) {
291+
return;
292+
}
293+
namedArgs.query = savedContext.searchText;
294+
savedQuery = savedContext.searchQuery;
295+
if (savedQuery) {
296+
context.printer.writeSearchQuery(savedQuery);
297+
}
298+
}
299+
280300
const searchResponse = await kpTest.execSearchRequest(
281301
context,
282302
namedArgs,
303+
savedQuery,
283304
);
284305
const searchResults = searchResponse.searchResults;
285306
const debugContext = searchResponse.debugContext;
286307
if (!searchResults.success) {
287308
context.printer.writeError(searchResults.message);
288309
return;
289310
}
311+
// Log any new queries
312+
if (!savedQuery) {
313+
context.log.writeFile("kpSearch", {
314+
searchText: debugContext.searchText,
315+
searchQuery: debugContext.searchQuery,
316+
});
317+
}
290318
if (namedArgs.debug) {
291319
context.printer.writeInColor(chalk.gray, () => {
292320
context.printer.writeLine();
@@ -359,14 +387,15 @@ export async function createKnowproCommands(
359387
);
360388
getAnswerRequest.searchResponse = searchResponse;
361389
getAnswerRequest.knowledgeTopK = options.entitiesTopK;
362-
await kpTest.execGetAnswerRequest(
390+
const answerResponse = await kpTest.execGetAnswerRequest(
363391
context,
364392
getAnswerRequest,
365393
(i: number, q: string, answer) => {
366394
writeAnswer(i, answer, debugContext);
367395
return;
368396
},
369397
);
398+
context.log.writeFile("kpAnswer", answerResponse);
370399
context.printer.writeLine();
371400
}
372401

@@ -754,7 +783,7 @@ export async function createKnowproCommands(
754783
function writeAnswer(
755784
queryIndex: number,
756785
answerResult: Result<kp.AnswerResponse>,
757-
debugContext: AnswerDebugContext,
786+
debugContext: kpTest.AnswerDebugContext,
758787
) {
759788
context.printer.writeLine();
760789
if (answerResult.success) {
@@ -943,8 +972,21 @@ export async function createKnowproCommands(
943972
}
944973
return [undefined, ""];
945974
}
946-
}
947975

948-
export interface AnswerDebugContext extends kp.LanguageSearchDebugContext {
949-
searchText: string;
976+
async function loadSavedDebugContext(
977+
filePath: string,
978+
): Promise<kpTest.AnswerDebugContext | undefined> {
979+
// Load a saved or logged query
980+
const savedContext =
981+
await loadObject<kpTest.AnswerDebugContext>(filePath);
982+
return savedContext;
983+
}
984+
985+
async function loadObject<T>(filePath: string) {
986+
const obj = await readJsonFile<T>(filePath);
987+
if (obj === undefined) {
988+
context.printer.writeError(`${filePath} not found`);
989+
}
990+
return obj;
991+
}
950992
}

ts/examples/chat/src/memory/knowproPrinter.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,11 @@ export class KnowProPrinter extends MemoryConsoleWriter {
569569
return this;
570570
}
571571

572+
public writeSearchQuery(searchQuery: kp.querySchema.SearchQuery) {
573+
this.writeHeading("Search Query");
574+
this.writeJson(searchQuery);
575+
}
576+
572577
public writeDebugContext(context: kp.LanguageSearchDebugContext) {
573578
if (context.searchQuery) {
574579
this.writeHeading("Search Query");

ts/examples/chat/src/memory/knowproTest.ts

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ import * as kpTest from "knowpro-test";
1818
import * as cm from "conversation-memory";
1919
import {
2020
changeFileExt,
21+
ensureDir,
2122
getAbsolutePath,
2223
getFileName,
2324
htmlToMd,
2425
readAllText,
2526
simplifyHtml,
2627
simplifyText,
28+
writeJsonFile,
2729
} from "typeagent";
2830
import chalk from "chalk";
2931
import { openai } from "aiclient";
@@ -203,8 +205,6 @@ export async function createKnowproTestCommands(
203205
srcPath: argSourceFile(),
204206
},
205207
options: {
206-
startAt: argNum("Start at this query", 0),
207-
count: argNum("Number to run"),
208208
verbose: argBool("Verbose error output", false),
209209
},
210210
};
@@ -219,27 +219,23 @@ export async function createKnowproTestCommands(
219219

220220
const namedArgs = parseNamedArguments(args, verifySearchBatchDef());
221221
const srcPath = namedArgs.srcPath;
222-
let errorCount = 0;
223222
const results = await kpTest.verifyLangSearchResultsBatch(
224223
context,
225224
srcPath,
226225
(result, index, total) => {
227226
context.printer.writeProgress(index + 1, total);
228227
if (result.success) {
229-
if (!writeSearchScore(result.data, namedArgs.verbose)) {
230-
errorCount++;
231-
}
228+
writeSearchScore(result.data, namedArgs.verbose);
232229
} else {
233230
context.printer.writeError(result.message);
234231
}
235232
},
236233
);
237234
if (!results.success) {
238235
context.printer.writeError(results.message);
236+
return;
239237
}
240-
if (errorCount > 0) {
241-
context.printer.writeLine(`${errorCount} errors`);
242-
}
238+
await writeSearchValidationReport(results.data, srcPath);
243239
} finally {
244240
endTestBatch();
245241
}
@@ -442,11 +438,12 @@ export async function createKnowproTestCommands(
442438
): boolean {
443439
const error = result.error;
444440
if (error !== undefined && error.length > 0) {
445-
context.printer.writeInColor(
446-
chalk.redBright,
447-
`[${error}]: ${result.expected.cmd!}`,
441+
context.printer.writeLineInColor(
442+
chalk.gray,
443+
result.actual.searchText,
448444
);
449-
context.printer.writeInColor(chalk.red, `Error: ${error}`);
445+
context.printer.writeInColor(chalk.red, `${result.expected.cmd!}`);
446+
context.printer.writeInColor(chalk.red, `Error:\n ${error}`);
450447
if (verbose) {
451448
context.printer.writeLine("===========");
452449
context.printer.writeJsonInColor(
@@ -477,6 +474,25 @@ export async function createKnowproTestCommands(
477474
}
478475
}
479476

477+
async function writeSearchValidationReport(
478+
results: kpTest.Comparison<kpTest.LangSearchResults>[],
479+
srcPath?: string,
480+
) {
481+
const errorResults = results.filter(
482+
(c) => c.error !== undefined && c.error.length > 0,
483+
);
484+
if (errorResults.length === 0) {
485+
context.printer.writeLineInColor(chalk.green, "No errors");
486+
return;
487+
}
488+
489+
context.printer.writeLine(`${errorResults.length} errors`);
490+
context.printer.writeList(errorResults.map((e) => e.expected.cmd));
491+
if (srcPath) {
492+
await saveReport(srcPath, errorResults);
493+
}
494+
}
495+
480496
function writeAnswerScore(
481497
result: kpTest.SimilarityComparison<kpTest.QuestionAnswer>,
482498
threshold: number,
@@ -498,6 +514,13 @@ export async function createKnowproTestCommands(
498514
}
499515
}
500516

517+
async function saveReport(srcPath: string, report: any) {
518+
const outputDir = path.join(context.basePath, "logs/testReports");
519+
await ensureDir(outputDir);
520+
const outputPath = path.join(outputDir, getFileName(srcPath) + ".json");
521+
await writeJsonFile(outputPath, report);
522+
}
523+
501524
function beginTestBatch() {
502525
context.retryNoAnswer = true;
503526
}

ts/packages/knowPro/src/search.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,27 @@ export async function runSearchQuery(
228228
return results;
229229
}
230230

231+
/**
232+
* Run multiple queries
233+
* @param conversation
234+
* @param queries queries to run
235+
* @param options
236+
* @returns
237+
*/
238+
export async function runSearchQueries(
239+
conversation: IConversation,
240+
queries: SearchQueryExpr[],
241+
options?: SearchOptions,
242+
): Promise<ConversationSearchResult[][]> {
243+
// FUTURE: do these in parallel
244+
const results: ConversationSearchResult[][] = [];
245+
for (let i = 0; i < queries.length; ++i) {
246+
const result = await runSearchQuery(conversation, queries[i], options);
247+
results.push(result);
248+
}
249+
return results;
250+
}
251+
231252
/**
232253
* Run the search query. For each selectExpr:
233254
* - only match messages using similarity to the rawQuery on each expression

ts/packages/knowPro/src/searchLang.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,23 @@ export async function searchConversationWithLanguage(
145145
}
146146
return success(searchResults);
147147

148+
//
149+
// Scoping queries can be precise. However, there may be random variations in how LLMs
150+
// translate some user utterances into queries.. .particularly verbs. They verbs
151+
// may not match action verbs actually in the index.. related terms may not meet the similarity
152+
// cutoff.
153+
// If configured (compileOptions.exactScope == false), we can do a fallback query that does
154+
// not enforce verb matching. This improves recall while still providing a reasonable level of scoping because it
155+
//
148156
function compileFallbackQuery(
149157
query: querySchema.SearchQuery,
150158
compileOptions: LanguageQueryCompileOptions,
151159
langSearchFilter?: LanguageSearchFilter,
152160
): SearchQueryExpr[] | undefined {
153161
const verbScope = compileOptions.verbScope;
162+
//
163+
// If no exact scope... and verbScope is not provided or true,
164+
// then we can build a fallback query that is more forgiving
154165
if (
155166
!compileOptions.exactScope &&
156167
(verbScope == undefined || verbScope)
@@ -165,6 +176,9 @@ export async function searchConversationWithLanguage(
165176
langSearchFilter,
166177
);
167178
}
179+
//
180+
// No fallback query currently possible
181+
//
168182
return undefined;
169183
}
170184

@@ -249,6 +263,9 @@ export type LanguageQueryCompileOptions = {
249263
* Is fuzzy matching enabled when applying scope?
250264
*/
251265
exactScope?: boolean | undefined;
266+
/**
267+
* Should use verbs in scoping expressions (default true)
268+
*/
252269
verbScope?: boolean | undefined;
253270
// Use to ignore noise terms etc.
254271
termFilter?: (text: string) => boolean;

ts/packages/knowProTest/src/common.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,26 @@ export function ensureDirSync(folderPath: string): string {
1414
return folderPath;
1515
}
1616

17+
export function ensureUniqueFilePath(filePath: string): string {
18+
if (!fs.existsSync(filePath)) {
19+
return filePath;
20+
}
21+
22+
for (let i = 1; i < 1000; ++i) {
23+
const tempPath = addFileNameSuffixToPath(filePath, `_${i}`);
24+
if (!fs.existsSync(tempPath)) {
25+
return tempPath;
26+
}
27+
}
28+
29+
throw new Error(`Could not ensure unique ${filePath}`);
30+
}
31+
32+
export function writeObjectToUniqueFile(filePath: string, obj: any): void {
33+
filePath = ensureUniqueFilePath(filePath);
34+
fs.writeFileSync(filePath, stringifyReadable(obj));
35+
}
36+
1737
export function getCommandArgs(line: string | undefined): string[] {
1838
if (line !== undefined && line.length > 0) {
1939
const args = parseCommandLine(line);
@@ -102,6 +122,17 @@ export function isJsonEqual(x: any | undefined, y: any | undefined): boolean {
102122
return false;
103123
}
104124

125+
export function compareObject(
126+
x: any,
127+
y: any,
128+
label: string,
129+
): string | undefined {
130+
if (!isJsonEqual(x, y)) {
131+
return `${label}: ${stringifyReadable(x)}\n !== \n${stringifyReadable(y)}`;
132+
}
133+
return undefined;
134+
}
135+
105136
export function compareArray(
106137
name: string,
107138
x: any[] | undefined,
@@ -129,3 +160,7 @@ export function compareArray(
129160
export function queryError(query: string, result: Error): Error {
130161
return error(`${query}\n${result.message}`);
131162
}
163+
164+
export function stringifyReadable(value: any): string {
165+
return JSON.stringify(value, undefined, 2);
166+
}

ts/packages/knowProTest/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
export * from "./types.js";
55
export * from "./models.js";
6+
export * from "./logging.js";
67
export * from "./knowproContext.js";
78
export * from "./searchTest.js";
89
export * from "./answerTest.js";

0 commit comments

Comments
 (0)