Skip to content

Commit b0ca9c9

Browse files
authored
URL Resolver Updates (#1436)
- Added Wikipedia Resolver - Resolvers can now be individually turned on or off using _@browser resolver history_ <img width="557" height="469" alt="image" src="https://github.com/user-attachments/assets/f810ad22-9161-4620-a26f-d17bb507220a" />
1 parent 9bf84d2 commit b0ca9c9

File tree

7 files changed

+401
-43
lines changed

7 files changed

+401
-43
lines changed

ts/packages/agents/browser/src/agent/actionHandler.mts

Lines changed: 191 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import { TabTitleIndex, createTabTitleIndex } from "./tabTitleIndex.mjs";
3737
import { ChildProcess, fork } from "child_process";
3838
import { fileURLToPath } from "node:url";
3939
import path from "node:path";
40-
import fs from "node:fs";
40+
import fs, { readFileSync } from "node:fs";
4141

4242
import {
4343
CommandHandler,
@@ -84,7 +84,7 @@ import { ShoppingActions } from "./commerce/schema/userActions.mjs";
8484
import { SchemaDiscoveryActions } from "./discovery/schema/discoveryActions.mjs";
8585
import { ExternalBrowserActions } from "./externalBrowserActionSchema.mjs";
8686
import { BrowserControl } from "../common/browserControl.mjs";
87-
import { openai, TextEmbeddingModel } from "aiclient";
87+
import { openai, TextEmbeddingModel, wikipedia } from "aiclient";
8888
import { urlResolver, bingWithGrounding } from "azure-ai-foundry";
8989
import { createExternalBrowserClient } from "./rpc/externalBrowserControlClient.mjs";
9090
import { deleteCachedSchema } from "./crossword/cachedSchema.mjs";
@@ -123,6 +123,12 @@ export type BrowserActionContext = {
123123
localHostPort: number;
124124
macrosStore?: MacroStore | undefined; // Add MacroStore instance
125125
currentWebSearchResults?: Map<string, any[]> | undefined; // Store search results for follow-up actions
126+
resolverSettings: {
127+
searchResolver?: boolean | undefined;
128+
keywordResolver?: boolean | undefined;
129+
wikipediaResolver?: boolean | undefined;
130+
historyResolver?: boolean | undefined;
131+
};
126132
};
127133

128134
export interface urlResolutionAction {
@@ -151,6 +157,12 @@ async function initializeBrowserContext(
151157
index: undefined,
152158
localHostPort,
153159
macrosStore: undefined, // Will be initialized in updateBrowserContext
160+
resolverSettings: {
161+
searchResolver: true,
162+
keywordResolver: true,
163+
wikipediaResolver: true,
164+
historyResolver: false,
165+
},
154166
};
155167
}
156168

@@ -244,6 +256,26 @@ async function updateBrowserContext(
244256
}
245257
});
246258
}
259+
260+
// rehydrate resolver settings
261+
const sessionDir: string | undefined =
262+
await getSessionFolderPath(context);
263+
const contents: string = await readFileSync(
264+
path.join(sessionDir!, "settings.json"),
265+
"utf-8",
266+
);
267+
268+
if (contents.length > 0) {
269+
const config = JSON.parse(contents);
270+
context.agentContext.resolverSettings.searchResolver =
271+
config.searchResolver;
272+
context.agentContext.resolverSettings.keywordResolver =
273+
config.keywordResolver;
274+
context.agentContext.resolverSettings.wikipediaResolver =
275+
config.wikipediaResolver;
276+
context.agentContext.resolverSettings.historyResolver =
277+
config.historyResolver;
278+
}
247279
} else {
248280
const webSocket = context.agentContext.webSocket;
249281
if (webSocket) {
@@ -614,6 +646,13 @@ async function getSessionFolderPath(
614646
return sessionDir;
615647
}
616648

649+
async function saveSettings(context: SessionContext<BrowserActionContext>) {
650+
await context.sessionStorage?.write(
651+
"settings.json",
652+
JSON.stringify(context.agentContext.resolverSettings),
653+
);
654+
}
655+
617656
async function resolveEntity(
618657
type: string,
619658
name: string,
@@ -701,43 +740,55 @@ async function resolveWebPage(
701740
}
702741

703742
// try to resolve URL using website visit history first
704-
const historyUrl = await resolveURLWithHistory(context, site);
705-
if (historyUrl) {
706-
debug(`Resolved URL from history: ${historyUrl}`);
707-
return historyUrl;
743+
if (context.agentContext.resolverSettings.historyResolver) {
744+
const historyUrl = await resolveURLWithHistory(context, site);
745+
if (historyUrl) {
746+
debug(`Resolved URL from history: ${historyUrl}`);
747+
return historyUrl;
748+
}
708749
}
709-
const cachehitUrl = await urlResolver.resolveURLByKeyword(site);
710-
if (cachehitUrl) {
711-
debug(`Resolved URL from cache: ${cachehitUrl}`);
712-
713-
if (
714-
cachehitUrl.indexOf("https://") !== 0 &&
715-
cachehitUrl.indexOf("http://") !== 0
716-
) {
717-
return "https://" + cachehitUrl;
718-
} else {
719-
return cachehitUrl;
750+
751+
// try to resolve URL string using known keyword matching
752+
if (context.agentContext.resolverSettings.keywordResolver) {
753+
const cachehitUrl = await urlResolver.resolveURLByKeyword(site);
754+
if (cachehitUrl) {
755+
debug(`Resolved URL from cache: ${cachehitUrl}`);
756+
757+
if (
758+
cachehitUrl.indexOf("https://") !== 0 &&
759+
cachehitUrl.indexOf("http://") !== 0
760+
) {
761+
return "https://" + cachehitUrl;
762+
} else {
763+
return cachehitUrl;
764+
}
765+
}
766+
}
767+
768+
// try to resolve URL by using the Wikipedia API
769+
if (context.agentContext.resolverSettings.wikipediaResolver) {
770+
debug(`Resolving URL using Wikipedia for: ${site}`);
771+
const wikiPediaUrl = await urlResolver.resolveURLWithWikipedia(
772+
site,
773+
wikipedia.apiSettingsFromEnv(),
774+
);
775+
if (wikiPediaUrl) {
776+
debug(`Resolved URL using Wikipedia: ${wikiPediaUrl}`);
777+
return wikiPediaUrl;
720778
}
721779
}
722-
// TODO: reenable
723-
// const wikiPediaUrl = await urlResolver.resolveURLWithWikipedia(
724-
// site,
725-
// wikipedia.apiSettingsFromEnv(),
726-
// );
727-
// if (wikiPediaUrl) {
728-
// debug(`Resolved URL using Wikipedia: ${wikiPediaUrl}`);
729-
// return wikiPediaUrl;
730-
// }
731780

732781
// try to resolve URL using LLM + internet search
733-
const url = await urlResolver.resolveURLWithSearch(
734-
site,
735-
bingWithGrounding.apiSettingsFromEnv(),
736-
);
782+
if (context.agentContext.resolverSettings.searchResolver) {
783+
const url = await urlResolver.resolveURLWithSearch(
784+
site,
785+
bingWithGrounding.apiSettingsFromEnv(),
786+
);
737787

738-
if (url) {
739-
debug(`Resolved URL using Bing with Grounding: ${url}`);
740-
return url;
788+
if (url) {
789+
debug(`Resolved URL using Bing with Grounding: ${url}`);
790+
return url;
791+
}
741792
}
742793

743794
// can't get a URL
@@ -2086,5 +2137,112 @@ export const handlers: CommandHandlerTable = {
20862137
},
20872138
},
20882139
},
2140+
resolver: {
2141+
description: "Toggle URL resolver methods",
2142+
defaultSubCommand: "list",
2143+
commands: {
2144+
list: {
2145+
description: "List all available URL resolvers",
2146+
run: async (
2147+
context: ActionContext<BrowserActionContext>,
2148+
) => {
2149+
const agentContext =
2150+
context.sessionContext.agentContext;
2151+
const resolvers = Object.entries(
2152+
agentContext.resolverSettings,
2153+
)
2154+
.filter(([, enabled]) => enabled !== undefined)
2155+
.map(([name, enabled]) => ({
2156+
name,
2157+
enabled,
2158+
}));
2159+
displaySuccess(
2160+
`Available resolvers: ${JSON.stringify(resolvers)}`,
2161+
context,
2162+
);
2163+
},
2164+
},
2165+
search: {
2166+
description: "Toggle search resolver",
2167+
run: async (
2168+
context: ActionContext<BrowserActionContext>,
2169+
) => {
2170+
const agentContext =
2171+
context.sessionContext.agentContext;
2172+
agentContext.resolverSettings.searchResolver =
2173+
!agentContext.resolverSettings.searchResolver;
2174+
displaySuccess(
2175+
`Search resolver is now ${
2176+
agentContext.resolverSettings.searchResolver
2177+
? "enabled"
2178+
: "disabled"
2179+
}`,
2180+
context,
2181+
);
2182+
saveSettings(context.sessionContext);
2183+
},
2184+
},
2185+
keyword: {
2186+
description: "Toggle keyword resolver",
2187+
run: async (
2188+
context: ActionContext<BrowserActionContext>,
2189+
) => {
2190+
const agentContext =
2191+
context.sessionContext.agentContext;
2192+
agentContext.resolverSettings.keywordResolver =
2193+
!agentContext.resolverSettings.keywordResolver;
2194+
displaySuccess(
2195+
`Keyword resolver is now ${
2196+
agentContext.resolverSettings.keywordResolver
2197+
? "enabled"
2198+
: "disabled"
2199+
}`,
2200+
context,
2201+
);
2202+
saveSettings(context.sessionContext);
2203+
},
2204+
},
2205+
wikipedia: {
2206+
description: "Toggle Wikipedia resolver",
2207+
run: async (
2208+
context: ActionContext<BrowserActionContext>,
2209+
) => {
2210+
const agentContext =
2211+
context.sessionContext.agentContext;
2212+
agentContext.resolverSettings.wikipediaResolver =
2213+
!agentContext.resolverSettings.wikipediaResolver;
2214+
displaySuccess(
2215+
`Wikipedia resolver is now ${
2216+
agentContext.resolverSettings.wikipediaResolver
2217+
? "enabled"
2218+
: "disabled"
2219+
}`,
2220+
context,
2221+
);
2222+
saveSettings(context.sessionContext);
2223+
},
2224+
},
2225+
history: {
2226+
description: "Toggle history resolver",
2227+
run: async (
2228+
context: ActionContext<BrowserActionContext>,
2229+
) => {
2230+
const agentContext =
2231+
context.sessionContext.agentContext;
2232+
agentContext.resolverSettings.historyResolver =
2233+
!agentContext.resolverSettings.historyResolver;
2234+
displaySuccess(
2235+
`History resolver is now ${
2236+
agentContext.resolverSettings.historyResolver
2237+
? "enabled"
2238+
: "disabled"
2239+
}`,
2240+
context,
2241+
);
2242+
saveSettings(context.sessionContext);
2243+
},
2244+
},
2245+
},
2246+
},
20892247
},
20902248
};

ts/packages/aiclient/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ export {
1313
getChatModelMaxConcurrency,
1414
} from "./modelResource.js";
1515
export * as wikipedia from "./wikipedia.js";
16+
export * as wikipediaSchemas from "./wikipediaSchemas.js";

ts/packages/aiclient/src/wikipedia.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
// Licensed under the MIT License.
33

44
import { getEnvSetting } from "./common.js";
5+
import { wikipedia } from "./index.js";
56

67
export type WikipediaApiSettings = {
78
endpoint?: string;
89
accessToken?: string;
910
clientId?: string;
1011
clientSecret?: string;
1112
getToken(): Promise<string>;
13+
getAPIHeaders(): Promise<any>;
1214
};
1315

1416
/**
@@ -73,5 +75,72 @@ export function apiSettingsFromEnv(
7375

7476
return wikiToken!;
7577
},
78+
getAPIHeaders: async (): Promise<any> => {
79+
// get the token first
80+
if (!wikiToken) {
81+
await apiSettingsFromEnv(env).getToken();
82+
}
83+
84+
return {
85+
Authorization: `Bearer ${wikiToken}`,
86+
"Api-User-Agent": `TypeAgent/https://github.com/microsoft/TypeAgent`,
87+
};
88+
},
7689
};
7790
}
91+
92+
/**
93+
* Get the page object of the item with the supplied title.
94+
* @param title - The title of the page to retrieve
95+
* @param config - The wikipedia API configuration
96+
* @returns The page object.
97+
*/
98+
export async function getPageObject(
99+
title: string,
100+
config: wikipedia.WikipediaApiSettings,
101+
) {
102+
// TODO: localization (e.g. en, de, fr, etc.)
103+
const response = await fetch(
104+
`${config.endpoint}core/v1/wikipedia/en/page/${title}/bare`,
105+
{ method: "GET", headers: await config.getAPIHeaders() },
106+
);
107+
108+
if (response.ok) {
109+
return response.json();
110+
} else {
111+
return undefined;
112+
}
113+
}
114+
115+
/**
116+
*
117+
* @param title - The title of the page whose content to get.
118+
* @param config - The wikipedia API configuration
119+
* @returns - The content of the requetsed page or undefined if there was a problem
120+
*/
121+
export async function getPageMarkdown(
122+
title: string,
123+
config: wikipedia.WikipediaApiSettings,
124+
): Promise<string | undefined> {
125+
// TODO: localization (e.g. en, de, fr, etc.)
126+
const url: string = `${config.endpoint}en/page/${encodeWikipediaTitle(title)}`;
127+
const response = await fetch(url, {
128+
method: "GET",
129+
headers: await config.getAPIHeaders(),
130+
});
131+
132+
if (response.ok) {
133+
return response.text();
134+
} else {
135+
return undefined;
136+
}
137+
}
138+
139+
/**
140+
* Encodes a non-modified (human readable) Wikipedia title for use in a URL.
141+
* @param title - The title of the page to encode.
142+
* @returns - The encoded title suitable for use in a URL.
143+
*/
144+
export function encodeWikipediaTitle(title: string): string {
145+
return encodeURIComponent(title.replace(/ /g, "_"));
146+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
export type WikipediaPageExternalLinks = {
5+
officialWebsite?: WebPageLink;
6+
//officialLinks?: WebPageLink[];
7+
//additionalLinks?: WebPageLink[];
8+
//images?: WebPageLink[];
9+
};
10+
11+
export type WebPageLink = {
12+
url: string;
13+
title?: string;
14+
};

0 commit comments

Comments
 (0)