@open-mercato/web-research-tavily 0.7.1-develop.7194.1.ab4fc81f82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -0
- package/build.mjs +10 -0
- package/dist/adapter.js +126 -0
- package/dist/adapter.js.map +7 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +7 -0
- package/jest.config.cjs +28 -0
- package/package.json +59 -0
- package/src/__tests__/tavily.test.ts +73 -0
- package/src/adapter.ts +136 -0
- package/src/index.ts +3 -0
- package/tsconfig.json +10 -0
- package/watch.mjs +7 -0
package/build.mjs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { dirname } from 'node:path'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { buildPackage } from '../../scripts/build-package.mjs'
|
|
4
|
+
|
|
5
|
+
const packageDir = dirname(fileURLToPath(import.meta.url))
|
|
6
|
+
|
|
7
|
+
await buildPackage(packageDir, {
|
|
8
|
+
name: 'web-research-tavily',
|
|
9
|
+
entryPoints: 'src/**/*.ts',
|
|
10
|
+
})
|
package/dist/adapter.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CONTRACT_VERSION,
|
|
3
|
+
READY,
|
|
4
|
+
defineAdapterModule,
|
|
5
|
+
notReady,
|
|
6
|
+
searchOk,
|
|
7
|
+
stripTrailingSlashes,
|
|
8
|
+
toOutcomeFailure
|
|
9
|
+
} from "@open-mercato/web-research";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
const tavilyOptionsSchema = z.object({
|
|
12
|
+
apiKey: z.string().min(1).optional(),
|
|
13
|
+
baseUrl: z.url().optional(),
|
|
14
|
+
searchDepth: z.enum(["basic", "advanced"]).optional(),
|
|
15
|
+
includeContent: z.boolean().optional(),
|
|
16
|
+
timeoutMs: z.number().int().min(1e3).max(12e4).optional()
|
|
17
|
+
});
|
|
18
|
+
const DEFAULT_BASE_URL = "https://api.tavily.com";
|
|
19
|
+
const DEFAULT_TIMEOUT_MS = 15e3;
|
|
20
|
+
const responseSchema = z.object({
|
|
21
|
+
answer: z.string().nullish(),
|
|
22
|
+
results: z.array(
|
|
23
|
+
z.object({
|
|
24
|
+
url: z.string(),
|
|
25
|
+
title: z.string().nullish(),
|
|
26
|
+
content: z.string().nullish(),
|
|
27
|
+
raw_content: z.string().nullish(),
|
|
28
|
+
score: z.number().nullish(),
|
|
29
|
+
published_date: z.string().nullish()
|
|
30
|
+
})
|
|
31
|
+
).nullish()
|
|
32
|
+
});
|
|
33
|
+
function parseJson(body) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(body);
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function createTavilyAdapter(options) {
|
|
41
|
+
const apiKey = options.apiKey?.trim() ?? "";
|
|
42
|
+
const baseUrl = stripTrailingSlashes(options.baseUrl ?? DEFAULT_BASE_URL);
|
|
43
|
+
const searchDepth = options.searchDepth ?? "basic";
|
|
44
|
+
const includeContent = options.includeContent ?? false;
|
|
45
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
46
|
+
return {
|
|
47
|
+
id: "tavily",
|
|
48
|
+
kind: "api",
|
|
49
|
+
capabilities: {
|
|
50
|
+
search: true,
|
|
51
|
+
fetch: false,
|
|
52
|
+
snippets: true,
|
|
53
|
+
content: includeContent,
|
|
54
|
+
freshness: false,
|
|
55
|
+
siteFilter: true,
|
|
56
|
+
cost: "metered"
|
|
57
|
+
},
|
|
58
|
+
/** `billable` — issues a real /search query. */
|
|
59
|
+
probeCost: "billable",
|
|
60
|
+
readiness: () => apiKey.length > 0 ? READY : notReady("Tavily API key is not set"),
|
|
61
|
+
async search(request, context) {
|
|
62
|
+
if (apiKey.length === 0) return { status: "unavailable", reason: "Tavily API key is not set" };
|
|
63
|
+
try {
|
|
64
|
+
const response = await context.http.request(`${baseUrl}/search`, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" },
|
|
67
|
+
body: JSON.stringify({
|
|
68
|
+
query: request.site ? `${request.query} site:${request.site}` : request.query,
|
|
69
|
+
max_results: request.limit,
|
|
70
|
+
search_depth: searchDepth,
|
|
71
|
+
include_answer: true,
|
|
72
|
+
include_raw_content: includeContent
|
|
73
|
+
}),
|
|
74
|
+
accept: ["application/json"],
|
|
75
|
+
timeoutMs,
|
|
76
|
+
signal: context.signal
|
|
77
|
+
});
|
|
78
|
+
const parsed = responseSchema.safeParse(parseJson(response.body));
|
|
79
|
+
if (!parsed.success) {
|
|
80
|
+
return { status: "error", reason: "Tavily returned an unrecognized response shape", retriable: false };
|
|
81
|
+
}
|
|
82
|
+
const results = (parsed.data.results ?? []).map((item) => ({
|
|
83
|
+
url: item.url,
|
|
84
|
+
title: item.title ?? null,
|
|
85
|
+
snippet: item.content ?? null,
|
|
86
|
+
content: item.raw_content ?? null,
|
|
87
|
+
publishedAt: item.published_date ?? null,
|
|
88
|
+
score: item.score ?? null
|
|
89
|
+
}));
|
|
90
|
+
context.report("ok", `Tavily returned ${results.length} result(s)`, { resultCount: results.length });
|
|
91
|
+
return searchOk(results, parsed.data.answer ?? void 0);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
return toOutcomeFailure(error);
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
async healthCheck(context) {
|
|
97
|
+
if (apiKey.length === 0) return { ok: false, detail: "Tavily API key is not set" };
|
|
98
|
+
try {
|
|
99
|
+
await context.http.request(`${baseUrl}/search`, {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" },
|
|
102
|
+
body: JSON.stringify({ query: "open mercato health check", max_results: 1 }),
|
|
103
|
+
accept: ["application/json"],
|
|
104
|
+
timeoutMs,
|
|
105
|
+
signal: context.signal
|
|
106
|
+
});
|
|
107
|
+
return { ok: true };
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return { ok: false, detail: error instanceof Error ? error.message : String(error) };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const tavilyAdapterModule = defineAdapterModule({
|
|
115
|
+
id: "tavily",
|
|
116
|
+
kind: "api",
|
|
117
|
+
contractVersion: CONTRACT_VERSION,
|
|
118
|
+
optionsSchema: tavilyOptionsSchema,
|
|
119
|
+
createAdapter: createTavilyAdapter
|
|
120
|
+
});
|
|
121
|
+
export {
|
|
122
|
+
createTavilyAdapter,
|
|
123
|
+
tavilyAdapterModule,
|
|
124
|
+
tavilyOptionsSchema
|
|
125
|
+
};
|
|
126
|
+
//# sourceMappingURL=adapter.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/adapter.ts"],
|
|
4
|
+
"sourcesContent": ["import {\n CONTRACT_VERSION,\n READY,\n defineAdapterModule,\n notReady,\n searchOk,\n stripTrailingSlashes,\n toOutcomeFailure,\n type RawResult,\n type SearchAdapter,\n type SearchOutcome,\n} from '@open-mercato/web-research'\nimport { z } from 'zod'\n\nexport const tavilyOptionsSchema = z.object({\n apiKey: z.string().min(1).optional(),\n baseUrl: z.url().optional(),\n searchDepth: z.enum(['basic', 'advanced']).optional(),\n includeContent: z.boolean().optional(),\n timeoutMs: z.number().int().min(1000).max(120_000).optional(),\n})\n\nexport type TavilyOptions = z.infer<typeof tavilyOptionsSchema>\n\nconst DEFAULT_BASE_URL = 'https://api.tavily.com'\nconst DEFAULT_TIMEOUT_MS = 15_000\n\nconst responseSchema = z.object({\n answer: z.string().nullish(),\n results: z\n .array(\n z.object({\n url: z.string(),\n title: z.string().nullish(),\n content: z.string().nullish(),\n raw_content: z.string().nullish(),\n score: z.number().nullish(),\n published_date: z.string().nullish(),\n }),\n )\n .nullish(),\n})\n\nfunction parseJson(body: string): unknown {\n try {\n return JSON.parse(body)\n } catch {\n return null\n }\n}\n\nexport function createTavilyAdapter(options: TavilyOptions): SearchAdapter {\n const apiKey = options.apiKey?.trim() ?? ''\n const baseUrl = stripTrailingSlashes(options.baseUrl ?? DEFAULT_BASE_URL)\n const searchDepth = options.searchDepth ?? 'basic'\n const includeContent = options.includeContent ?? false\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS\n\n return {\n id: 'tavily',\n kind: 'api',\n capabilities: {\n search: true,\n fetch: false,\n snippets: true,\n content: includeContent,\n freshness: false,\n siteFilter: true,\n cost: 'metered',\n },\n /** `billable` \u2014 issues a real /search query. */\n probeCost: 'billable' as const,\n readiness: () => (apiKey.length > 0 ? READY : notReady('Tavily API key is not set')),\n\n async search(request, context): Promise<SearchOutcome> {\n if (apiKey.length === 0) return { status: 'unavailable', reason: 'Tavily API key is not set' }\n try {\n const response = await context.http.request(`${baseUrl}/search`, {\n method: 'POST',\n headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' },\n body: JSON.stringify({\n query: request.site ? `${request.query} site:${request.site}` : request.query,\n max_results: request.limit,\n search_depth: searchDepth,\n include_answer: true,\n include_raw_content: includeContent,\n }),\n accept: ['application/json'],\n timeoutMs,\n signal: context.signal,\n })\n const parsed = responseSchema.safeParse(parseJson(response.body))\n if (!parsed.success) {\n return { status: 'error', reason: 'Tavily returned an unrecognized response shape', retriable: false }\n }\n const results: RawResult[] = (parsed.data.results ?? []).map((item) => ({\n url: item.url,\n title: item.title ?? null,\n snippet: item.content ?? null,\n content: item.raw_content ?? null,\n publishedAt: item.published_date ?? null,\n score: item.score ?? null,\n }))\n context.report('ok', `Tavily returned ${results.length} result(s)`, { resultCount: results.length })\n return searchOk(results, parsed.data.answer ?? undefined)\n } catch (error) {\n return toOutcomeFailure(error)\n }\n },\n\n async healthCheck(context) {\n if (apiKey.length === 0) return { ok: false, detail: 'Tavily API key is not set' }\n try {\n await context.http.request(`${baseUrl}/search`, {\n method: 'POST',\n headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' },\n body: JSON.stringify({ query: 'open mercato health check', max_results: 1 }),\n accept: ['application/json'],\n timeoutMs,\n signal: context.signal,\n })\n return { ok: true }\n } catch (error) {\n return { ok: false, detail: error instanceof Error ? error.message : String(error) }\n }\n },\n }\n}\n\nexport const tavilyAdapterModule = defineAdapterModule({\n id: 'tavily',\n kind: 'api',\n contractVersion: CONTRACT_VERSION,\n optionsSchema: tavilyOptionsSchema,\n createAdapter: createTavilyAdapter,\n})\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createTavilyAdapter, tavilyAdapterModule, tavilyOptionsSchema } from "./adapter.js";
|
|
2
|
+
import { tavilyAdapterModule as tavilyAdapterModule2 } from "./adapter.js";
|
|
3
|
+
export {
|
|
4
|
+
createTavilyAdapter,
|
|
5
|
+
tavilyAdapterModule2 as default,
|
|
6
|
+
tavilyAdapterModule,
|
|
7
|
+
tavilyOptionsSchema
|
|
8
|
+
};
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/index.ts"],
|
|
4
|
+
"sourcesContent": ["export { createTavilyAdapter, tavilyAdapterModule, tavilyOptionsSchema, type TavilyOptions } from './adapter'\n\nexport { tavilyAdapterModule as default } from './adapter'\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,qBAAqB,qBAAqB,2BAA+C;AAElG,SAAgC,uBAAvBA,4BAAsC;",
|
|
6
|
+
"names": ["tavilyAdapterModule"]
|
|
7
|
+
}
|
package/jest.config.cjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** @type {import('jest').Config} */
|
|
2
|
+
const base = require('../../jest.config.base.cjs')
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
...base,
|
|
6
|
+
preset: 'ts-jest',
|
|
7
|
+
testEnvironment: 'node',
|
|
8
|
+
watchman: false,
|
|
9
|
+
rootDir: '.',
|
|
10
|
+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
|
|
11
|
+
moduleNameMapper: {
|
|
12
|
+
'^@open-mercato/web-research/(.*)$': '<rootDir>/../web-research/src/$1',
|
|
13
|
+
'^@open-mercato/web-research$': '<rootDir>/../web-research/src/index.ts',
|
|
14
|
+
},
|
|
15
|
+
transform: {
|
|
16
|
+
'^.+\\.(t|j)sx?$': [
|
|
17
|
+
'ts-jest',
|
|
18
|
+
{
|
|
19
|
+
tsconfig: {
|
|
20
|
+
rootDir: '.',
|
|
21
|
+
ignoreDeprecations: '6.0',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
testMatch: ['<rootDir>/src/**/__tests__/**/*.test.(ts|tsx)'],
|
|
27
|
+
passWithNoTests: true,
|
|
28
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@open-mercato/web-research-tavily",
|
|
3
|
+
"version": "0.7.1-develop.7194.1.ab4fc81f82",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"openMercato": {
|
|
8
|
+
"webResearchAdapter": {
|
|
9
|
+
"id": "tavily"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "node build.mjs",
|
|
14
|
+
"watch": "node watch.mjs",
|
|
15
|
+
"test": "jest --config jest.config.cjs",
|
|
16
|
+
"typecheck": "tsc --noEmit"
|
|
17
|
+
},
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./src/index.ts",
|
|
21
|
+
"default": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./*.ts": {
|
|
24
|
+
"types": "./src/*.ts",
|
|
25
|
+
"default": "./dist/*.js"
|
|
26
|
+
},
|
|
27
|
+
"./*": {
|
|
28
|
+
"types": [
|
|
29
|
+
"./src/*.ts"
|
|
30
|
+
],
|
|
31
|
+
"default": "./dist/*.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@open-mercato/web-research": "^0.6.0"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"zod": "^4.4.3"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@open-mercato/web-research": "0.7.1-develop.7194.1.ab4fc81f82",
|
|
42
|
+
"@types/jest": "^30.0.0",
|
|
43
|
+
"@types/node": "^26.0.1",
|
|
44
|
+
"esbuild": "^0.28.1",
|
|
45
|
+
"glob": "^13.0.6",
|
|
46
|
+
"jest": "^30.4.2",
|
|
47
|
+
"ts-jest": "^29.4.11",
|
|
48
|
+
"typescript": "^6.0.3"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
},
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "https://github.com/open-mercato/open-mercato",
|
|
56
|
+
"directory": "packages/web-research-tavily"
|
|
57
|
+
},
|
|
58
|
+
"stableVersion": "0.7.0"
|
|
59
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createStubHttpClient, createTestContext, describeAdapterContract } from '@open-mercato/web-research/testing'
|
|
2
|
+
import { createTavilyAdapter, tavilyAdapterModule } from '../adapter'
|
|
3
|
+
|
|
4
|
+
const BODY = JSON.stringify({
|
|
5
|
+
answer: 'Open Mercato is a modular commerce platform.',
|
|
6
|
+
results: [
|
|
7
|
+
{ url: 'https://a.com/1', title: 'Alpha', content: 'Alpha snippet', score: 0.9 },
|
|
8
|
+
{ url: 'https://b.com/2', title: 'Beta', content: 'Beta snippet', score: 0.5 },
|
|
9
|
+
],
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
describe('tavily adapter', () => {
|
|
13
|
+
it('is unavailable without a key and makes no call', async () => {
|
|
14
|
+
const http = createStubHttpClient(() => ({ body: BODY }))
|
|
15
|
+
const outcome = await createTavilyAdapter({}).search({ query: 'x', limit: 5 }, createTestContext({ http }))
|
|
16
|
+
expect(outcome).toMatchObject({ status: 'unavailable' })
|
|
17
|
+
expect(http.calls).toHaveLength(0)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('maps results and keeps the vendor answer', async () => {
|
|
21
|
+
const http = createStubHttpClient(() => ({ body: BODY }))
|
|
22
|
+
const outcome = await createTavilyAdapter({ apiKey: 'k' }).search(
|
|
23
|
+
{ query: 'x', limit: 5 },
|
|
24
|
+
createTestContext({ http }),
|
|
25
|
+
)
|
|
26
|
+
expect(outcome.status).toBe('ok')
|
|
27
|
+
if (outcome.status !== 'ok') return
|
|
28
|
+
expect(outcome.results[0]).toMatchObject({ url: 'https://a.com/1', snippet: 'Alpha snippet', score: 0.9 })
|
|
29
|
+
expect(outcome.answer).toContain('modular commerce platform')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('sends the key as a bearer token and honours the site filter', async () => {
|
|
33
|
+
const seen: Array<{ headers?: Record<string, string>; body?: string }> = []
|
|
34
|
+
const http = createStubHttpClient((_url, init) => {
|
|
35
|
+
seen.push({ headers: init?.headers as Record<string, string>, body: init?.body })
|
|
36
|
+
return { body: BODY }
|
|
37
|
+
})
|
|
38
|
+
await createTavilyAdapter({ apiKey: 'secret' }).search(
|
|
39
|
+
{ query: 'pricing', limit: 5, site: 'example.com' },
|
|
40
|
+
createTestContext({ http }),
|
|
41
|
+
)
|
|
42
|
+
expect(seen[0].headers?.authorization).toBe('Bearer secret')
|
|
43
|
+
expect(JSON.parse(seen[0].body ?? '{}').query).toBe('pricing site:example.com')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('reports an unrecognized payload as an error', async () => {
|
|
47
|
+
const http = createStubHttpClient(() => ({ body: '{"results":"nope"}' }))
|
|
48
|
+
const outcome = await createTavilyAdapter({ apiKey: 'k' }).search(
|
|
49
|
+
{ query: 'x', limit: 5 },
|
|
50
|
+
createTestContext({ http }),
|
|
51
|
+
)
|
|
52
|
+
expect(outcome).toMatchObject({ status: 'error' })
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('returns empty when the vendor found nothing and offered no answer', async () => {
|
|
56
|
+
const http = createStubHttpClient(() => ({ body: JSON.stringify({ results: [] }) }))
|
|
57
|
+
const outcome = await createTavilyAdapter({ apiKey: 'k' }).search(
|
|
58
|
+
{ query: 'x', limit: 5 },
|
|
59
|
+
createTestContext({ http }),
|
|
60
|
+
)
|
|
61
|
+
expect(outcome.status).toBe('empty')
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describeAdapterContract(
|
|
66
|
+
tavilyAdapterModule,
|
|
67
|
+
{
|
|
68
|
+
unconfiguredOptions: {},
|
|
69
|
+
configuredOptions: { apiKey: 'test-key' },
|
|
70
|
+
context: () => createTestContext({ http: createStubHttpClient(() => ({ body: BODY })) }),
|
|
71
|
+
},
|
|
72
|
+
{ describe, it, expect: expect as never },
|
|
73
|
+
)
|
package/src/adapter.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CONTRACT_VERSION,
|
|
3
|
+
READY,
|
|
4
|
+
defineAdapterModule,
|
|
5
|
+
notReady,
|
|
6
|
+
searchOk,
|
|
7
|
+
stripTrailingSlashes,
|
|
8
|
+
toOutcomeFailure,
|
|
9
|
+
type RawResult,
|
|
10
|
+
type SearchAdapter,
|
|
11
|
+
type SearchOutcome,
|
|
12
|
+
} from '@open-mercato/web-research'
|
|
13
|
+
import { z } from 'zod'
|
|
14
|
+
|
|
15
|
+
export const tavilyOptionsSchema = z.object({
|
|
16
|
+
apiKey: z.string().min(1).optional(),
|
|
17
|
+
baseUrl: z.url().optional(),
|
|
18
|
+
searchDepth: z.enum(['basic', 'advanced']).optional(),
|
|
19
|
+
includeContent: z.boolean().optional(),
|
|
20
|
+
timeoutMs: z.number().int().min(1000).max(120_000).optional(),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export type TavilyOptions = z.infer<typeof tavilyOptionsSchema>
|
|
24
|
+
|
|
25
|
+
const DEFAULT_BASE_URL = 'https://api.tavily.com'
|
|
26
|
+
const DEFAULT_TIMEOUT_MS = 15_000
|
|
27
|
+
|
|
28
|
+
const responseSchema = z.object({
|
|
29
|
+
answer: z.string().nullish(),
|
|
30
|
+
results: z
|
|
31
|
+
.array(
|
|
32
|
+
z.object({
|
|
33
|
+
url: z.string(),
|
|
34
|
+
title: z.string().nullish(),
|
|
35
|
+
content: z.string().nullish(),
|
|
36
|
+
raw_content: z.string().nullish(),
|
|
37
|
+
score: z.number().nullish(),
|
|
38
|
+
published_date: z.string().nullish(),
|
|
39
|
+
}),
|
|
40
|
+
)
|
|
41
|
+
.nullish(),
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
function parseJson(body: string): unknown {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(body)
|
|
47
|
+
} catch {
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createTavilyAdapter(options: TavilyOptions): SearchAdapter {
|
|
53
|
+
const apiKey = options.apiKey?.trim() ?? ''
|
|
54
|
+
const baseUrl = stripTrailingSlashes(options.baseUrl ?? DEFAULT_BASE_URL)
|
|
55
|
+
const searchDepth = options.searchDepth ?? 'basic'
|
|
56
|
+
const includeContent = options.includeContent ?? false
|
|
57
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
id: 'tavily',
|
|
61
|
+
kind: 'api',
|
|
62
|
+
capabilities: {
|
|
63
|
+
search: true,
|
|
64
|
+
fetch: false,
|
|
65
|
+
snippets: true,
|
|
66
|
+
content: includeContent,
|
|
67
|
+
freshness: false,
|
|
68
|
+
siteFilter: true,
|
|
69
|
+
cost: 'metered',
|
|
70
|
+
},
|
|
71
|
+
/** `billable` — issues a real /search query. */
|
|
72
|
+
probeCost: 'billable' as const,
|
|
73
|
+
readiness: () => (apiKey.length > 0 ? READY : notReady('Tavily API key is not set')),
|
|
74
|
+
|
|
75
|
+
async search(request, context): Promise<SearchOutcome> {
|
|
76
|
+
if (apiKey.length === 0) return { status: 'unavailable', reason: 'Tavily API key is not set' }
|
|
77
|
+
try {
|
|
78
|
+
const response = await context.http.request(`${baseUrl}/search`, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' },
|
|
81
|
+
body: JSON.stringify({
|
|
82
|
+
query: request.site ? `${request.query} site:${request.site}` : request.query,
|
|
83
|
+
max_results: request.limit,
|
|
84
|
+
search_depth: searchDepth,
|
|
85
|
+
include_answer: true,
|
|
86
|
+
include_raw_content: includeContent,
|
|
87
|
+
}),
|
|
88
|
+
accept: ['application/json'],
|
|
89
|
+
timeoutMs,
|
|
90
|
+
signal: context.signal,
|
|
91
|
+
})
|
|
92
|
+
const parsed = responseSchema.safeParse(parseJson(response.body))
|
|
93
|
+
if (!parsed.success) {
|
|
94
|
+
return { status: 'error', reason: 'Tavily returned an unrecognized response shape', retriable: false }
|
|
95
|
+
}
|
|
96
|
+
const results: RawResult[] = (parsed.data.results ?? []).map((item) => ({
|
|
97
|
+
url: item.url,
|
|
98
|
+
title: item.title ?? null,
|
|
99
|
+
snippet: item.content ?? null,
|
|
100
|
+
content: item.raw_content ?? null,
|
|
101
|
+
publishedAt: item.published_date ?? null,
|
|
102
|
+
score: item.score ?? null,
|
|
103
|
+
}))
|
|
104
|
+
context.report('ok', `Tavily returned ${results.length} result(s)`, { resultCount: results.length })
|
|
105
|
+
return searchOk(results, parsed.data.answer ?? undefined)
|
|
106
|
+
} catch (error) {
|
|
107
|
+
return toOutcomeFailure(error)
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
async healthCheck(context) {
|
|
112
|
+
if (apiKey.length === 0) return { ok: false, detail: 'Tavily API key is not set' }
|
|
113
|
+
try {
|
|
114
|
+
await context.http.request(`${baseUrl}/search`, {
|
|
115
|
+
method: 'POST',
|
|
116
|
+
headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' },
|
|
117
|
+
body: JSON.stringify({ query: 'open mercato health check', max_results: 1 }),
|
|
118
|
+
accept: ['application/json'],
|
|
119
|
+
timeoutMs,
|
|
120
|
+
signal: context.signal,
|
|
121
|
+
})
|
|
122
|
+
return { ok: true }
|
|
123
|
+
} catch (error) {
|
|
124
|
+
return { ok: false, detail: error instanceof Error ? error.message : String(error) }
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const tavilyAdapterModule = defineAdapterModule({
|
|
131
|
+
id: 'tavily',
|
|
132
|
+
kind: 'api',
|
|
133
|
+
contractVersion: CONTRACT_VERSION,
|
|
134
|
+
optionsSchema: tavilyOptionsSchema,
|
|
135
|
+
createAdapter: createTavilyAdapter,
|
|
136
|
+
})
|
package/src/index.ts
ADDED
package/tsconfig.json
ADDED