Skip to content

Commit

Permalink
knowpro: Natural Language querying (#787)
Browse files Browse the repository at this point in the history
First cut of natural language querying natively in knowpro:
* searchSchema.ts, searchTranslator.ts to translate natural language to
query filter
* Compile/transform returned filter to knowpro terms and 'when/scope'
filter (time ranges etc.)
* kpSearch command in test app
* Ongoing work, testing/refinement

Testing:
* Also implemented natural language search for knowpro using V1
translators and kpSearchV1 command
  • Loading branch information
umeshma authored Mar 5, 2025
1 parent 32c3b75 commit f249c38
Show file tree
Hide file tree
Showing 7 changed files with 341 additions and 62 deletions.
55 changes: 46 additions & 9 deletions ts/examples/chat/src/memory/knowproCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,33 @@ export function textLocationToString(location: kp.TextLocation): string {
export async function matchFilterToConversation(
conversation: kp.IConversation,
filter: knowLib.conversation.TermFilterV2,
knowledgeType?: kp.KnowledgeType | undefined,
knowledgeType: kp.KnowledgeType | undefined,
searchOptions: kp.SearchOptions,
useAnd: boolean = false,
) {
let searchTermGroup: kp.SearchTermGroup = termFilterToSearchGroup(
filter,
useAnd,
);
let termGroup: kp.SearchTermGroup = termFilterToSearchGroup(filter, useAnd);
if (filter.action) {
let actionGroup: kp.SearchTermGroup = actionFilterToSearchGroup(
filter.action,
useAnd,
);
// Just flatten for now...
termGroup.terms.push(...actionGroup.terms);
}
let when: kp.WhenFilter = termFilterToWhenFilter(filter);
when.knowledgeType = knowledgeType;
let searchResults = await kp.searchConversation(
conversation,
searchTermGroup,
termGroup,
when,
searchOptions,
);
if (useAnd && (!searchResults || searchResults.size === 0)) {
// Try again with OR
searchTermGroup = termFilterToSearchGroup(filter, false);
termGroup = termFilterToSearchGroup(filter, false);
searchResults = await kp.searchConversation(
conversation,
searchTermGroup,
termGroup,
when,
);
}
Expand All @@ -62,7 +69,7 @@ export function termFilterToSearchGroup(

export function termFilterToWhenFilter(
filter: knowLib.conversation.TermFilterV2,
) {
): kp.WhenFilter {
let when: kp.WhenFilter = {};
if (filter.timeRange) {
when.dateRange = {
Expand All @@ -72,3 +79,33 @@ export function termFilterToWhenFilter(
}
return when;
}

export function actionFilterToSearchGroup(
action: knowLib.conversation.ActionTerm,
and: boolean,
): kp.SearchTermGroup {
const searchTermGroup: kp.SearchTermGroup = {
booleanOp: and ? "and" : "or",
terms: [],
};

if (action.verbs) {
searchTermGroup.terms.push(
...action.verbs.words.map((v) => {
return kp.createPropertySearchTerm(kp.PropertyNames.Verb, v);
}),
);
}
if (action.subject !== "none") {
searchTermGroup.terms.push(
kp.createPropertySearchTerm(
kp.PropertyNames.Subject,
action.subject.subject,
),
);
}
if (action.object) {
searchTermGroup.terms.push(kp.createSearchTerm(action.object));
}
return searchTermGroup;
}
88 changes: 60 additions & 28 deletions ts/examples/chat/src/memory/knowproMemory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,17 @@ import { KnowProPrinter } from "./knowproPrinter.js";
import * as cm from "conversation-memory";
import * as im from "image-memory";
import { matchFilterToConversation } from "./knowproCommon.js";
import { TypeChatJsonTranslator } from "typechat";

type KnowProContext = {
knowledgeModel: ChatModel;
knowledgeExtractor: knowLib.conversation.KnowledgeExtractor;
knowledgeActions: knowLib.conversation.KnowledgeActionTranslator;
basePath: string;
printer: KnowProPrinter;
podcast?: cm.Podcast | undefined;
images?: im.ImageCollection | undefined;
conversation?: kp.IConversation | undefined;
searchTranslator: TypeChatJsonTranslator<kp.SearchFilter>;
};

export async function createKnowproCommands(
Expand All @@ -51,11 +52,11 @@ export async function createKnowproCommands(
const knowledgeModel = chatContext.models.chatModel;
const context: KnowProContext = {
knowledgeModel,
knowledgeExtractor: kp.createKnowledgeProcessor(knowledgeModel),
knowledgeActions:
knowLib.conversation.createKnowledgeActionTranslator(
knowledgeModel,
),
searchTranslator: kp.createSearchTranslator(knowledgeModel),
basePath: "/data/testChat/knowpro",
printer: new KnowProPrinter(),
};
Expand All @@ -66,8 +67,8 @@ export async function createKnowproCommands(
commands.kpPodcastSave = podcastSave;
commands.kpPodcastLoad = podcastLoad;
commands.kpSearchTerms = searchTerms;
commands.kpSearchV1 = searchV1;
commands.kpSearch = search;
commands.kpSearchK = knowledgeSearch;
commands.kpEntities = entities;
commands.kpPodcastBuildIndex = podcastBuildIndex;

Expand Down Expand Up @@ -364,7 +365,8 @@ export async function createKnowproCommands(
): CommandMetadata {
const meta: CommandMetadata = {
description:
description ?? "Search current knowPro conversation by terms",
description ??
"Search current knowPro conversation by manually providing terms as arguments",
options: {
maxToDisplay: argNum("Maximum matches to display", 25),
displayAsc: argBool("Display results in ascending order", true),
Expand Down Expand Up @@ -448,20 +450,21 @@ export async function createKnowproCommands(

function searchDef(): CommandMetadata {
return {
description: "Search using natural language",
description:
"Search using natural language and knowlege-processor search filters",
args: {
query: arg("Search query"),
},
options: {
maxToDisplay: argNum("Maximum matches to display", 25),
exact: argBool("Exact match only. No related terms", false),
ktype: arg("Knowledge type"),
},
};
}
commands.kpSearch.metadata = searchDef();
async function search(args: string[]): Promise<void> {
async function searchV1(args: string[]): Promise<void> {
if (!ensureConversationLoaded()) {
context.printer.writeError("No conversation loaded");
return;
}
const namedArgs = parseNamedArguments(args, searchDef());
Expand All @@ -485,38 +488,67 @@ export async function createKnowproCommands(
context.conversation!,
filter,
namedArgs.ktype,
{
exactMatch: namedArgs.exact,
},
);
if (searchResults) {
context.printer.writeSearchResults(
context.conversation!,
searchResults,
namedArgs.maxToDisplay,
);
} else {
context.printer.writeLine("No matches");
}
}
}

commands.kpSearchK.metadata = searchDef();
async function knowledgeSearch(args: string[]): Promise<void> {
const namedArgs = parseNamedArguments(args, searchDef());
function searchDefNew(): CommandMetadata {
const def = searchDef();
def.description =
"Search using natural language and new knowpro filter";
return def;
}

commands.kpSearch.metadata = searchDefNew();
async function search(args: string[]): Promise<void> {
if (!ensureConversationLoaded()) {
return;
}
const namedArgs = parseNamedArguments(args, searchDefNew());
const query = namedArgs.query;
const knowledge = await context.knowledgeExtractor.extract(query);
if (knowledge) {
context.printer.writeTitle("Topics");
for (const topic of knowledge.topics) {
context.printer.writeLine(topic);
context.printer.writeLine();
}
context.printer.writeTitle("Actions");
for (const action of knowledge.actions) {
context.printer.writeAction(action);
context.printer.writeLine();
}
context.printer.writeTitle("Entities");
for (const entity of knowledge.entities) {
context.printer.writeEntity(entity);
context.printer.writeLine();
}
const result = await context.searchTranslator.translate(
query,
kp.getTimeRangeSectionForConversation(context.conversation!),
);
if (!result.success) {
context.printer.writeError(result.message);
return;
}

const filter = result.data;
if (filter) {
context.printer.writeJson(filter, true);
}
const terms = kp.createSearchGroupFromSearchFilter(filter);
const when = kp.createWhenFromSearchFilter(filter);
const searchResults = await kp.searchConversation(
context.conversation!,
terms,
when,
{
exactMatch: namedArgs.exact,
},
);
if (searchResults) {
context.printer.writeSearchResults(
context.conversation!,
searchResults,
namedArgs.maxToDisplay,
);
} else {
context.printer.writeLine("No matches");
}
}

Expand Down Expand Up @@ -555,7 +587,7 @@ export async function createKnowproCommands(
const allValues = splitTermValues(keyValues[propertyName]);
for (const value of allValues) {
propertySearchTerms.push(
kp.propertySearchTermFromKeyValue(propertyName, value),
kp.createPropertySearchTerm(propertyName, value),
);
}
}
Expand Down
25 changes: 25 additions & 0 deletions ts/packages/knowPro/src/dateTimeSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

export type DateVal = {
day: number;
month: number;
year: number;
};

export type TimeVal = {
// In 24 hour form
hour: number;
minute: number;
seconds: number;
};

export type DateTime = {
date: DateVal;
time?: TimeVal | undefined;
};

export type DateTimeRange = {
startDate: DateTime;
stopDate?: DateTime | undefined;
};
3 changes: 3 additions & 0 deletions ts/packages/knowPro/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ export * from "./fuzzyIndex.js";
export * from "./propertyIndex.js";
export * from "./timestampIndex.js";
export * from "./serialization.js";
export * from "./dateTimeSchema.js";
export * from "./searchSchema.js";
export * from "./searchTranslator.js";
50 changes: 25 additions & 25 deletions ts/packages/knowPro/src/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export type PropertySearchTerm = {
propertyValue: SearchTerm;
};

function createSearchTerm(text: string, score?: number): SearchTerm {
export function createSearchTerm(text: string, score?: number): SearchTerm {
return {
term: {
text,
Expand All @@ -79,6 +79,30 @@ function createSearchTerm(text: string, score?: number): SearchTerm {
};
}

export function createPropertySearchTerm(
key: string,
value: string,
): PropertySearchTerm {
let propertyName: KnowledgePropertyName | SearchTerm;
let propertyValue: SearchTerm;
switch (key) {
default:
propertyName = createSearchTerm(key);
break;
case "name":
case "type":
case "verb":
case "subject":
case "object":
case "indirectObject":
case "tag":
propertyName = key;
break;
}
propertyValue = createSearchTerm(value);
return { propertyName, propertyValue };
}

export type WhenFilter = {
knowledgeType?: KnowledgeType | undefined;
dateRange?: DateRange | undefined;
Expand Down Expand Up @@ -413,30 +437,6 @@ class SearchQueryBuilder {
}
}

export function propertySearchTermFromKeyValue(
key: string,
value: string,
): PropertySearchTerm {
let propertyName: KnowledgePropertyName | SearchTerm;
let propertyValue: SearchTerm;
switch (key) {
default:
propertyName = createSearchTerm(key);
break;
case "name":
case "type":
case "verb":
case "subject":
case "object":
case "indirectObject":
case "tag":
propertyName = key;
break;
}
propertyValue = createSearchTerm(value);
return { propertyName, propertyValue };
}

function isPropertyTerm(
term: SearchTerm | PropertySearchTerm,
): term is PropertySearchTerm {
Expand Down
Loading

0 comments on commit f249c38

Please sign in to comment.