From ab5321b641abeceac28576a4c5d674b44850c54a Mon Sep 17 00:00:00 2001 From: Travis Fischer Date: Mon, 12 Jun 2023 18:50:47 -0700 Subject: [PATCH] feat: add bing web search service --- src/index.ts | 11 +- src/services/bing-web-search.ts | 282 ++++++++++++++++++++++++++++++++ src/services/index.ts | 6 + src/tools/index.ts | 3 + test/bing.test.ts | 18 ++ 5 files changed, 312 insertions(+), 8 deletions(-) create mode 100644 src/services/bing-web-search.ts create mode 100644 src/services/index.ts create mode 100644 src/tools/index.ts create mode 100644 test/bing.test.ts diff --git a/src/index.ts b/src/index.ts index 9dac1686..debc9a1a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,9 @@ export * from './agentic' export * from './task' -export * from './llms' export * from './errors' export * from './tokenizer' export * from './human-feedback' -export * from './services/metaphor' -export * from './services/serpapi' -export * from './services/novu' - -export * from './tools/calculator' -export * from './tools/metaphor' -export * from './tools/novu' +export * from './llms' +export * from './services' +export * from './tools' diff --git a/src/services/bing-web-search.ts b/src/services/bing-web-search.ts new file mode 100644 index 00000000..532d0af4 --- /dev/null +++ b/src/services/bing-web-search.ts @@ -0,0 +1,282 @@ +import ky from 'ky' + +export const BING_BASE_URL = 'https://api.bing.microsoft.com' + +export interface BingWebSearchQuery { + q: string + mkt?: string + offset?: number + count?: number + safeSearch?: 'Off' | 'Moderate' | 'Strict' + textDecorations?: boolean + textFormat?: 'Raw' | 'HTML' +} + +// export type BingWebSearchInput = z.infer + +export interface BingWebSearchResponse { + _type: string + entities: Entities + images: Images + places: Places + queryContext: QueryContext + rankingResponse: RankingResponse + relatedSearches: RelatedSearches + videos: Videos + webPages: WebPages +} + +interface Entities { + value: EntitiesValue[] +} + +interface EntitiesValue { + bingId: string + contractualRules: PurpleContractualRule[] + description: string + entityPresentationInfo: EntityPresentationInfo + id: string + image: Image + name: string + webSearchUrl: string +} + +interface PurpleContractualRule { + _type: string + license?: DeepLink + licenseNotice?: string + mustBeCloseToContent: boolean + targetPropertyName: string + text?: string + url?: string +} + +interface DeepLink { + name: string + url: string +} + +interface EntityPresentationInfo { + entityScenario: string + entityTypeHints: string[] +} + +interface Image { + height: number + hostPageUrl: string + name: string + provider: Provider[] + sourceHeight: number + sourceWidth: number + thumbnailUrl: string + width: number +} + +interface Provider { + _type: string + url: string +} + +interface Images { + id: string + isFamilyFriendly: boolean + readLink: string + value: ImagesValue[] + webSearchUrl: string +} + +interface ImagesValue { + contentSize: string + contentUrl: string + encodingFormat: string + height: number + hostPageDisplayUrl: string + hostPageUrl: string + name: string + thumbnail: Thumbnail + thumbnailUrl: string + webSearchUrl: string + width: number +} + +interface Thumbnail { + height: number + width: number +} + +interface Places { + value: PlacesValue[] +} + +interface PlacesValue { + _type: string + address: Address + entityPresentationInfo: EntityPresentationInfo + id: string + name: string + telephone: string + url: string + webSearchUrl: string +} + +interface Address { + addressCountry: string + addressLocality: string + addressRegion: string + neighborhood: string + postalCode: string +} + +interface QueryContext { + askUserForLocation: boolean + originalQuery: string +} + +interface RankingResponse { + mainline: Mainline + sidebar: Mainline +} + +interface Mainline { + items: Item[] +} + +interface Item { + answerType: string + resultIndex?: number + value?: ItemValue +} + +interface ItemValue { + id: string +} + +interface RelatedSearches { + id: string + value: RelatedSearchesValue[] +} + +interface RelatedSearchesValue { + displayText: string + text: string + webSearchUrl: string +} + +interface Videos { + id: string + isFamilyFriendly: boolean + readLink: string + scenario: string + value: VideosValue[] + webSearchUrl: string +} + +interface VideosValue { + allowHttpsEmbed: boolean + allowMobileEmbed: boolean + contentUrl: string + creator: Creator + datePublished: Date + description: string + duration: string + embedHtml: string + encodingFormat: EncodingFormat + height: number + hostPageDisplayUrl: string + hostPageUrl: string + isAccessibleForFree: boolean + isSuperfresh: boolean + motionThumbnailUrl: string + name: string + publisher: Creator[] + thumbnail: Thumbnail + thumbnailUrl: string + viewCount: number + webSearchUrl: string + width: number +} + +interface Creator { + name: string +} + +enum EncodingFormat { + Mp4 = 'mp4' +} + +interface WebPages { + totalEstimatedMatches: number + value: WebPagesValue[] + webSearchUrl: string +} + +interface WebPagesValue { + dateLastCrawled: Date + deepLinks?: DeepLink[] + displayUrl: string + id: string + isFamilyFriendly: boolean + isNavigational: boolean + language: string + name: string + snippet: string + thumbnailUrl?: string + url: string + contractualRules?: FluffyContractualRule[] +} + +interface FluffyContractualRule { + _type: string + license: DeepLink + licenseNotice: string + mustBeCloseToContent: boolean + targetPropertyIndex: number + targetPropertyName: string +} + +export class BingWebSearchClient { + apiKey: string + baseUrl: string + + constructor({ + apiKey = process.env.BING_API_KEY, + baseUrl = BING_BASE_URL + }: { + apiKey?: string + baseUrl?: string + } = {}) { + if (!apiKey) { + throw new Error(`Error BingWebSearchClient missing required "apiKey"`) + } + + this.apiKey = apiKey + this.baseUrl = baseUrl + } + + async search(queryOrOpts: string | BingWebSearchQuery) { + const defaultQuery: Partial = { + mkt: 'en-US' + } + + const searchParams = + typeof queryOrOpts === 'string' + ? { + ...defaultQuery, + q: queryOrOpts + } + : { + ...defaultQuery, + ...queryOrOpts + } + + console.log(searchParams) + return ky + .get(`${this.baseUrl}/v7.0/search`, { + headers: { + 'Ocp-Apim-Subscription-Key': this.apiKey + }, + searchParams + }) + .json() + } +} diff --git a/src/services/index.ts b/src/services/index.ts new file mode 100644 index 00000000..1620288e --- /dev/null +++ b/src/services/index.ts @@ -0,0 +1,6 @@ +export * from './bing-web-search' +export * from './metaphor' +export * from './novu' +export * from './serpapi' +export * from './slack' +export * from './twilio-conversation' diff --git a/src/tools/index.ts b/src/tools/index.ts new file mode 100644 index 00000000..d349762f --- /dev/null +++ b/src/tools/index.ts @@ -0,0 +1,3 @@ +export * from './calculator' +export * from './metaphor' +export * from './novu' diff --git a/test/bing.test.ts b/test/bing.test.ts new file mode 100644 index 00000000..2569c804 --- /dev/null +++ b/test/bing.test.ts @@ -0,0 +1,18 @@ +import test from 'ava' + +import { BingWebSearchClient } from '@/services' + +import './_utils' + +test('BingWebSearchClient.search', async (t) => { + if (!process.env.BING_API_KEY) { + return t.pass() + } + + t.timeout(2 * 60 * 1000) + const client = new BingWebSearchClient() + + const result = await client.search('coffee') + // console.log(result) + t.truthy(result?.webPages) +})