feat: add @agentic/brave-search package

pull/701/head
Travis Fischer 2025-03-25 21:51:27 +08:00
rodzic 0d8f59eef8
commit 4d85f2211d
7 zmienionych plików z 358 dodań i 0 usunięć

Wyświetl plik

@ -0,0 +1,36 @@
---
title: Brave Search
description: Brave web search and local places search.
---
- package: `@agentic/brave-search`
- exports: `class BraveSearchClient`, `namespace braveSearch`
- env vars: `BRAVE_SEARCH_API_KEY`
- [source](https://github.com/transitive-bullshit/agentic/blob/main/packages/brave-search/src/brave-search-client.ts)
- [brave search docs](https://brave.com/search/api)
## Install
<CodeGroup>
```bash npm
npm install @agentic/brave-search
```
```bash yarn
yarn add @agentic/brave-search
```
```bash pnpm
pnpm add @agentic/brave-search
```
</CodeGroup>
## Usage
```ts
import { BraveSearchClient } from '@agentic/brave-search'
const braveSearch = new BraveSearchClient()
const res = await braveSearch.search('latest news about openai')
```

Wyświetl plik

@ -0,0 +1,47 @@
{
"name": "@agentic/brave-search",
"version": "0.1.0",
"description": "Agentic SDK for the Brave search engine.",
"author": "Travis Fischer <travis@transitivebullsh.it>",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/transitive-bullshit/agentic.git",
"directory": "packages/brave-search"
},
"type": "module",
"source": "./src/index.ts",
"types": "./dist/index.d.ts",
"sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"clean": "del dist",
"test": "run-s test:*",
"test:lint": "eslint .",
"test:typecheck": "tsc --noEmit"
},
"dependencies": {
"@agentic/core": "workspace:*",
"ky": "catalog:"
},
"peerDependencies": {
"zod": "catalog:"
},
"devDependencies": {
"@agentic/tsconfig": "workspace:*"
},
"publishConfig": {
"access": "public"
}
}

Wyświetl plik

@ -0,0 +1,24 @@
<p align="center">
<a href="https://agentic.so">
<img alt="Agentic" src="https://raw.githubusercontent.com/transitive-bullshit/agentic/main/docs/media/agentic-header.jpg" width="308">
</a>
</p>
<p align="center">
<em>AI agent stdlib that works with any LLM and TypeScript AI SDK.</em>
</p>
<p align="center">
<a href="https://github.com/transitive-bullshit/agentic/actions/workflows/main.yml"><img alt="Build Status" src="https://github.com/transitive-bullshit/agentic/actions/workflows/main.yml/badge.svg" /></a>
<a href="https://www.npmjs.com/package/@agentic/stdlib"><img alt="NPM" src="https://img.shields.io/npm/v/@agentic/stdlib.svg" /></a>
<a href="https://github.com/transitive-bullshit/agentic/blob/main/license"><img alt="MIT License" src="https://img.shields.io/badge/license-MIT-blue" /></a>
<a href="https://prettier.io"><img alt="Prettier Code Formatting" src="https://img.shields.io/badge/code_style-prettier-brightgreen.svg" /></a>
</p>
# Agentic
**See the [github repo](https://github.com/transitive-bullshit/agentic) or [docs](https://agentic.so) for more info.**
## License
MIT © [Travis Fischer](https://x.com/transitive_bs)

Wyświetl plik

@ -0,0 +1,154 @@
import {
aiFunction,
AIFunctionsProvider,
assert,
getEnv,
sanitizeSearchParams
} from '@agentic/core'
import defaultKy, { type KyInstance } from 'ky'
import { bravesearch } from './brave-search'
/**
* Agentic client for the Brave search engine.
*
* @see https://brave.com/search/api
*/
export class BraveSearchClient extends AIFunctionsProvider {
protected readonly ky: KyInstance
protected readonly apiKey: string
protected readonly apiBaseUrl: string
constructor({
apiKey = getEnv('BRAVE_SEARCH_API_KEY'),
apiBaseUrl = bravesearch.apiBaseUrl,
ky = defaultKy
}: {
apiKey?: string
apiBaseUrl?: string
ky?: KyInstance
} = {}) {
assert(
apiKey,
'BraveSearchClient missing required "apiKey" (defaults to "BRAVE_SEARCH_API_KEY")'
)
super()
this.apiKey = apiKey
this.apiBaseUrl = apiBaseUrl
this.ky = ky.extend({
prefixUrl: apiBaseUrl,
headers: {
'X-Subscription-Token': apiKey
}
})
}
/**
* Brave web search.
*/
@aiFunction({
name: 'brave_search',
description:
'Performs a web search using the Brave Search API, ideal for general queries, news, articles, and online content. ' +
'Use this for broad information gathering, recent events, or when you need diverse web sources. ' +
'Supports pagination, content filtering, and freshness controls. ' +
'Maximum 20 results per request, with offset for pagination. ',
inputSchema: bravesearch.SearchParamsSchema
})
async search(
queryOrParams: string | bravesearch.SearchParams
): Promise<bravesearch.SearchResponse> {
const { query: q, ...params } =
typeof queryOrParams === 'string'
? { query: queryOrParams }
: queryOrParams
return this.ky
.get('res/v1/web/search', {
searchParams: sanitizeSearchParams({
...params,
q
})
})
.json<bravesearch.SearchResponse>()
}
/**
* Brave local search for businesses and places.
*/
@aiFunction({
name: 'brave_local_search',
description:
"Searches for local businesses and places using Brave's Local Search API. " +
'Best for queries related to physical locations, businesses, restaurants, services, etc. ' +
'Returns detailed information including:\n' +
'- Business names and addresses\n' +
'- Ratings and review counts\n' +
'- Phone numbers and opening hours\n' +
"Use this when the query implies 'near me' or mentions specific locations. " +
'Automatically falls back to web search if no local results are found.',
inputSchema: bravesearch.LocalSearchParamsSchema
})
async localSearch(
queryOrParams: string | bravesearch.LocalSearchParams
): Promise<bravesearch.LocalSearchResponse | bravesearch.SearchResponse> {
const { query: q, ...params } =
typeof queryOrParams === 'string'
? { query: queryOrParams }
: queryOrParams
const webData = await this.ky
.get('res/v1/web/search', {
searchParams: sanitizeSearchParams({
search_lang: 'en',
result_filter: 'locations',
...params,
q
})
})
.json<bravesearch.SearchResponse>()
const locationIds = webData.locations?.results
?.filter((r) => !!r.id)
.map((r) => r.id)
if (!locationIds?.length) {
return this.search(queryOrParams)
}
// Get POI details and descriptions in parallel
const [pois, descriptions] = await Promise.all([
this.getPoisData(locationIds),
this.getDescriptionsData(locationIds)
])
const desc = descriptions.descriptions
return Object.entries(desc).map(([id, description]) => ({
description,
...pois.results.find((r) => r.id === id)!
}))
}
async getPoisData(ids: string[]): Promise<bravesearch.PoiResponse> {
return this.ky
.get('res/v1/local/pois', {
searchParams: sanitizeSearchParams({
ids: ids.filter(Boolean)
})
})
.json<bravesearch.PoiResponse>()
}
async getDescriptionsData(ids: string[]): Promise<bravesearch.Description> {
return this.ky
.get('res/v1/local/descriptions', {
searchParams: sanitizeSearchParams({
ids: ids.filter(Boolean)
})
})
.json<bravesearch.Description>()
}
}

Wyświetl plik

@ -0,0 +1,90 @@
import { z } from 'zod'
export namespace bravesearch {
export const apiBaseUrl = 'https://api.search.brave.com'
export const SearchParamsSchema = z.object({
query: z
.string()
.describe('Search query (max 400 chars, 50 words)')
.optional(),
count: z
.number()
.int()
.min(1)
.max(20)
.describe('Number of results (1-20, default 10)')
.optional(),
offset: z
.number()
.describe('Pagination offset (max 9, default 0)')
.optional()
})
export type SearchParams = z.infer<typeof SearchParamsSchema>
export const LocalSearchParamsSchema = z.object({
query: z
.string()
.describe('Search query (max 400 chars, 50 words)')
.optional(),
count: z
.number()
.describe('Number of results (1-20, default 10)')
.int()
.min(1)
.max(20)
.optional()
})
export type LocalSearchParams = z.infer<typeof LocalSearchParamsSchema>
export interface SearchResponse {
web?: {
results?: Array<{
title: string
description: string
url: string
language?: string
published?: string
rank?: number
}>
}
locations?: {
results?: Array<{
id: string // Required by API
title?: string
}>
}
}
export interface Location {
id: string
name: string
address: {
streetAddress?: string
addressLocality?: string
addressRegion?: string
postalCode?: string
}
coordinates?: {
latitude: number
longitude: number
}
phone?: string
rating?: {
ratingValue?: number
ratingCount?: number
}
openingHours?: string[]
priceRange?: string
}
export interface PoiResponse {
results: Location[]
}
export interface Description {
descriptions: { [id: string]: string }
}
export type LocalSearchResponse = Array<Location & { description: string }>
}

Wyświetl plik

@ -0,0 +1,2 @@
export * from './brave-search'
export * from './brave-search-client'

Wyświetl plik

@ -0,0 +1,5 @@
{
"extends": "@agentic/tsconfig/base.json",
"include": ["src"],
"exclude": ["node_modules", "dist"]
}