Skip to content

Commit d7b57c7

Browse files
authored
knowpro: pipeline for NLP -> scoped queries (#1353)
* Commands: Answer generation etc. issues NLP -> scoped queries * Pipeline for scoped query testing * Refactor * Bugs
1 parent d08b508 commit d7b57c7

File tree

10 files changed

+290
-34
lines changed

10 files changed

+290
-34
lines changed

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

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ export async function createKnowproCommands(
122122
commands.kpAnswerTerms = answerTerms;
123123
commands.kpEntities = entities;
124124
commands.kpTopics = topics;
125+
commands.kpTags = tags;
125126
commands.kpMessages = showMessages;
126127
commands.kpAbstractMessage = abstract;
127128
commands.kpAlias = addAlias;
@@ -571,10 +572,7 @@ export async function createKnowproCommands(
571572
}
572573

573574
function topicsDef(): CommandMetadata {
574-
return searchTermsDef(
575-
"Search topics only in current conversation",
576-
"topic",
577-
);
575+
return searchTermsDef("Search topics in current conversation", "topic");
578576
}
579577
commands.kpTopics.metadata = topicsDef();
580578
async function topics(args: string[]): Promise<void> {
@@ -602,6 +600,33 @@ export async function createKnowproCommands(
602600
}
603601
}
604602

603+
function tagsDef(): CommandMetadata {
604+
return searchTermsDef("Search tags in current conversation", "tag");
605+
}
606+
commands.kpTags.metadata = tagsDef();
607+
async function tags(args: string[]): Promise<void> {
608+
const conversation = ensureConversationLoaded();
609+
if (!conversation) {
610+
return;
611+
}
612+
if (args.length > 0) {
613+
args.push("--ktype");
614+
args.push("tag");
615+
await searchTerms(args);
616+
} else {
617+
if (conversation.semanticRefs !== undefined) {
618+
const tagRefs = kp.filterCollection(
619+
conversation.semanticRefs,
620+
(sr) => sr.knowledgeType === "tag",
621+
);
622+
let tags = tagRefs.map((t) => (t.knowledge as kp.Topic).text);
623+
let tagStrings = kp.mergeTopics(tags);
624+
tagStrings.sort();
625+
context.printer.writeList(tagStrings, { type: "ol" });
626+
}
627+
}
628+
}
629+
605630
function abstractDef(): CommandMetadata {
606631
return {
607632
description: "Return an abstract of the message",

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,9 @@ export class KnowProPrinter extends MemoryConsoleWriter {
189189
case "topic":
190190
this.writeTopic(semanticRef.knowledge as kp.Topic);
191191
break;
192+
case "tag":
193+
this.writeTag(semanticRef.knowledge as kp.Tag);
194+
break;
192195
}
193196
return this;
194197
}

ts/packages/knowPro/src/knowledge.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { error, Result } from "typechat";
77
import { ChatModel } from "aiclient";
88
import { createKnowledgeModel } from "./conversationIndex.js";
99
import { BatchTask, runInBatches } from "./taskQueue.js";
10-
import { SearchSelectExpr } from "./interfaces.js";
10+
import { SearchSelectExpr, Tag } from "./interfaces.js";
1111
import {
1212
createOrMaxTermGroup,
1313
createOrTermGroup,
@@ -66,6 +66,14 @@ export function mergeTopics(topics: string[]): string[] {
6666
return [...mergedTopics.values()];
6767
}
6868

69+
export function mergeTags(tags: Tag[]): string[] {
70+
let mergedText = new Set<string>();
71+
for (let tag of tags) {
72+
mergedText.add(tag.text);
73+
}
74+
return [...mergedText.values()];
75+
}
76+
6977
export async function extractKnowledgeForTextBatchQ(
7078
knowledgeExtractor: kpLib.KnowledgeExtractor,
7179
textBatch: string[],

ts/packages/knowPro/src/searchLang.ts

Lines changed: 173 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
import {
2323
searchQueryFromLanguage,
2424
SearchQueryTranslator,
25+
searchQueryWithScopeFromLanguage,
2526
} from "./searchQueryTranslator.js";
2627
import * as querySchema from "./searchQuerySchema.js";
2728
import * as querySchema2 from "./searchQuerySchema_v2.js";
@@ -182,16 +183,6 @@ export async function searchConversationWithLanguage(
182183
//
183184
return undefined;
184185
}
185-
186-
function createTextQueryOptions(
187-
options: LanguageSearchOptions,
188-
): SearchOptions {
189-
const ragOptions: SearchOptions = {
190-
...options,
191-
...options.fallbackRagOptions,
192-
};
193-
return ragOptions;
194-
}
195186
}
196187

197188
export type LanguageQueryExpr = {
@@ -1013,6 +1004,170 @@ export function compileSearchQuery2(
10131004
return searchQueryExprs;
10141005
}
10151006

1007+
export async function searchQueryExprFromLanguage2(
1008+
conversation: IConversation,
1009+
translator: SearchQueryTranslator,
1010+
queryText: string,
1011+
options?: LanguageSearchOptions,
1012+
languageSearchFilter?: LanguageSearchFilter,
1013+
debugContext?: LanguageSearchDebugContext,
1014+
): Promise<Result<LanguageQueryExpr>> {
1015+
const queryResult = await searchQueryWithScopeFromLanguage(
1016+
conversation,
1017+
translator,
1018+
queryText,
1019+
options?.modelInstructions,
1020+
);
1021+
if (queryResult.success) {
1022+
const query = queryResult.data;
1023+
if (debugContext) {
1024+
debugContext.searchQuery = query;
1025+
}
1026+
options ??= createLanguageSearchOptions();
1027+
const queryExpressions = compileSearchQuery2(
1028+
conversation,
1029+
query,
1030+
options.compileOptions,
1031+
languageSearchFilter,
1032+
);
1033+
return success({
1034+
queryText,
1035+
query,
1036+
queryExpressions,
1037+
});
1038+
}
1039+
return queryResult;
1040+
}
1041+
1042+
/**
1043+
* Search a conversation using natural language. Returns {@link ConversationSearchResult} containing
1044+
* relevant knowledge and messages.
1045+
*
1046+
* @param conversation - The conversation object to search within.
1047+
* @param searchText - The natural language search phrase.
1048+
* @param queryTranslator - Translates natural language to a {@link querySchema.SearchQuery} structured query.
1049+
* @param options - Optional search options.
1050+
* @param langSearchFilter - Optional filter options for the search.
1051+
* @param debugContext - Optional context for debugging the search process.
1052+
* @returns {ConversationSearchResult} Conversation search results.
1053+
*/
1054+
export async function searchConversationWithLanguage2(
1055+
conversation: IConversation,
1056+
searchText: string,
1057+
queryTranslator: SearchQueryTranslator,
1058+
options?: LanguageSearchOptions,
1059+
langSearchFilter?: LanguageSearchFilter,
1060+
debugContext?: LanguageSearchDebugContext,
1061+
): Promise<Result<ConversationSearchResult[]>> {
1062+
options ??= createLanguageSearchOptions();
1063+
const langQueryResult = await searchQueryExprFromLanguage2(
1064+
conversation,
1065+
queryTranslator,
1066+
searchText,
1067+
options,
1068+
langSearchFilter,
1069+
debugContext,
1070+
);
1071+
if (!langQueryResult.success) {
1072+
return langQueryResult;
1073+
}
1074+
const searchQueryExprs = langQueryResult.data.queryExpressions;
1075+
if (debugContext) {
1076+
debugContext.searchQueryExpr = searchQueryExprs;
1077+
debugContext.usedSimilarityFallback = new Array<boolean>(
1078+
searchQueryExprs.length,
1079+
);
1080+
debugContext.usedSimilarityFallback.fill(false);
1081+
}
1082+
let fallbackQueryExpr = compileFallbackQuery(
1083+
langQueryResult.data.query,
1084+
options.compileOptions,
1085+
langSearchFilter,
1086+
);
1087+
1088+
const searchResults: ConversationSearchResult[] = [];
1089+
for (let i = 0; i < searchQueryExprs.length; ++i) {
1090+
const searchQuery = searchQueryExprs[i];
1091+
const fallbackQuery = fallbackQueryExpr
1092+
? fallbackQueryExpr[i]
1093+
: undefined;
1094+
let queryResult = await runSearchQuery(
1095+
conversation,
1096+
searchQuery,
1097+
options,
1098+
);
1099+
if (!hasConversationResults(queryResult) && fallbackQuery) {
1100+
// Rerun the query but with verb matching turned off for scopes
1101+
queryResult = await runSearchQuery(
1102+
conversation,
1103+
fallbackQuery,
1104+
options,
1105+
);
1106+
}
1107+
//
1108+
// If no matches and fallback enabled... run the raw query
1109+
//
1110+
if (
1111+
!hasConversationResults(queryResult) &&
1112+
searchQuery.rawQuery &&
1113+
options.fallbackRagOptions
1114+
) {
1115+
const textSearchOptions = createTextQueryOptions(options);
1116+
const ragMatches = await runSearchQueryTextSimilarity(
1117+
conversation,
1118+
fallbackQuery ?? searchQuery,
1119+
textSearchOptions,
1120+
);
1121+
if (ragMatches) {
1122+
searchResults.push(...ragMatches);
1123+
if (debugContext?.usedSimilarityFallback) {
1124+
debugContext.usedSimilarityFallback![i] = true;
1125+
}
1126+
}
1127+
} else {
1128+
searchResults.push(...queryResult);
1129+
}
1130+
}
1131+
return success(searchResults);
1132+
1133+
//
1134+
// Scoping queries can be precise. However, there may be random variations in how LLMs
1135+
// translate some user utterances into queries.. .particularly verbs. They verbs
1136+
// may not match action verbs actually in the index.. related terms may not meet the similarity
1137+
// cutoff.
1138+
// If configured (compileOptions.exactScope == false), we can do a fallback query that does
1139+
// not enforce verb matching. This improves recall while still providing a reasonable level of scoping because it
1140+
//
1141+
function compileFallbackQuery(
1142+
query: querySchema.SearchQuery,
1143+
compileOptions: LanguageQueryCompileOptions,
1144+
langSearchFilter?: LanguageSearchFilter,
1145+
): SearchQueryExpr[] | undefined {
1146+
const verbScope = compileOptions.verbScope;
1147+
//
1148+
// If no exact scope... and verbScope is not provided or true,
1149+
// then we can build a fallback query that is more forgiving
1150+
if (
1151+
!compileOptions.exactScope &&
1152+
(verbScope == undefined || verbScope)
1153+
) {
1154+
return compileSearchQuery2(
1155+
conversation,
1156+
query,
1157+
{
1158+
...compileOptions,
1159+
verbScope: false,
1160+
},
1161+
langSearchFilter,
1162+
);
1163+
}
1164+
//
1165+
// No fallback query currently possible
1166+
//
1167+
return undefined;
1168+
}
1169+
}
1170+
10161171
const Wildcard = "*";
10171172

10181173
function isEntityTermArray(
@@ -1042,3 +1197,11 @@ function optimizeOrMax(termGroup: SearchTermGroup) {
10421197
}
10431198
return termGroup;
10441199
}
1200+
1201+
function createTextQueryOptions(options: LanguageSearchOptions): SearchOptions {
1202+
const ragOptions: SearchOptions = {
1203+
...options,
1204+
...options.fallbackRagOptions,
1205+
};
1206+
return ragOptions;
1207+
}

ts/packages/knowPro/src/searchQueryTranslator.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
PromptSection,
77
Result,
88
TypeChatLanguageModel,
9+
error,
910
} from "typechat";
1011
import * as querySchema from "./searchQuerySchema.js";
1112
import * as querySchema2 from "./searchQuerySchema_v2.js";
@@ -85,3 +86,21 @@ export async function searchQueryFromLanguage(
8586
const result = await queryTranslator.translate(text, queryContext);
8687
return result;
8788
}
89+
90+
export async function searchQueryWithScopeFromLanguage(
91+
conversation: IConversation,
92+
queryTranslator: SearchQueryTranslator,
93+
text: string,
94+
promptPreamble?: PromptSection[],
95+
): Promise<Result<querySchema2.SearchQuery>> {
96+
if (!queryTranslator.translateWithScope) {
97+
return error("Scoped queries not supported");
98+
}
99+
const timeRange = getTimeRangePromptSectionForConversation(conversation);
100+
let queryContext: PromptSection[] =
101+
promptPreamble && promptPreamble.length > 0
102+
? [...promptPreamble, ...timeRange]
103+
: timeRange;
104+
const result = await queryTranslator.translateWithScope(text, queryContext);
105+
return result;
106+
}

0 commit comments

Comments
 (0)