@xl0/pi-lovely-web 0.1.4 → 0.2.1
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/README.md +60 -32
- package/extensions/lovely-web/command.ts +29 -163
- package/extensions/lovely-web/config.ts +117 -54
- package/extensions/lovely-web/constants.ts +1 -0
- package/extensions/lovely-web/format.ts +15 -4
- package/extensions/lovely-web/image.ts +49 -59
- package/extensions/lovely-web/index.ts +13 -4
- package/extensions/lovely-web/providers/brave.ts +29 -14
- package/extensions/lovely-web/providers/exa.ts +20 -10
- package/extensions/lovely-web/providers/firecrawl.ts +37 -6
- package/extensions/lovely-web/providers/tavily.ts +44 -10
- package/extensions/lovely-web/providers/types.ts +28 -14
- package/extensions/lovely-web/tools.ts +182 -52
- package/extensions/lovely-web/types.ts +7 -0
- package/package.json +11 -4
- package/extensions/lovely-web/tool-impl.ts +0 -95
|
@@ -1,14 +1,107 @@
|
|
|
1
|
-
import { StringEnum } from "@earendil-works/pi-ai"
|
|
2
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
|
|
3
2
|
import { Text } from "@earendil-works/pi-tui"
|
|
4
|
-
import { Type } from "typebox"
|
|
5
|
-
import {
|
|
6
|
-
|
|
3
|
+
import { type TSchema, Type } from "typebox"
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_PROVIDER_ID,
|
|
6
|
+
getImageMaxSize,
|
|
7
|
+
getProvider,
|
|
8
|
+
isFetchEnabled,
|
|
9
|
+
isImageEnabled,
|
|
10
|
+
isImageResizeEnabled,
|
|
11
|
+
loadConfig,
|
|
12
|
+
lovelyWebConfigSpec,
|
|
13
|
+
providers,
|
|
14
|
+
resolveApiKey,
|
|
15
|
+
type WebToolsConfig
|
|
16
|
+
} from "./config.js"
|
|
17
|
+
import { DEFAULT_TIMEOUT_MS } from "./constants.js"
|
|
18
|
+
import { asErrorMessage, formatSearchOutput, stringify } from "./format.js"
|
|
7
19
|
import { DEFAULT_MAX_IMAGE_BYTES, imageImpl, MAX_IMAGE_BYTES } from "./image.js"
|
|
20
|
+
import type { FetchOptions, SearchOptions } from "./providers/types.js"
|
|
8
21
|
import { renderTextResult } from "./render.js"
|
|
9
|
-
import {
|
|
22
|
+
import type { ToolResult } from "./types.js"
|
|
10
23
|
|
|
11
|
-
|
|
24
|
+
interface SearchToolArgs {
|
|
25
|
+
query: string
|
|
26
|
+
limit?: number
|
|
27
|
+
source?: string
|
|
28
|
+
fetchResult?: boolean
|
|
29
|
+
category?: string
|
|
30
|
+
location?: string
|
|
31
|
+
country?: string
|
|
32
|
+
tbs?: string
|
|
33
|
+
timeRange?: string
|
|
34
|
+
topic?: string
|
|
35
|
+
includeImages?: boolean
|
|
36
|
+
searchLang?: string
|
|
37
|
+
freshness?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface FetchToolArgs extends FetchOptions {
|
|
41
|
+
url: string
|
|
42
|
+
includeMetadata?: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function fetchSearchResultImage(config: WebToolsConfig, url: string, signal?: AbortSignal): Promise<ToolResult> {
|
|
46
|
+
return imageImpl(
|
|
47
|
+
{
|
|
48
|
+
url,
|
|
49
|
+
timeout: DEFAULT_TIMEOUT_MS,
|
|
50
|
+
resize: isImageResizeEnabled(config),
|
|
51
|
+
maxSize: getImageMaxSize(config)
|
|
52
|
+
},
|
|
53
|
+
signal
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function fetchSearchResultMarkdown(config: WebToolsConfig, url: string, signal?: AbortSignal): Promise<string> {
|
|
58
|
+
const fetchProvider = getProvider("fetch", config)
|
|
59
|
+
const fetchApiKey = resolveApiKey(fetchProvider, config)
|
|
60
|
+
const fetched = await fetchProvider.fetch(fetchApiKey, url, { timeout: DEFAULT_TIMEOUT_MS }, signal)
|
|
61
|
+
return fetched.markdown
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getSearchParameters(config: WebToolsConfig) {
|
|
65
|
+
const configured = config.webSearchProvider
|
|
66
|
+
const providerId = configured && providers[configured] ? configured : DEFAULT_PROVIDER_ID
|
|
67
|
+
const params: Record<string, TSchema> = {
|
|
68
|
+
query: Type.String({ description: "The search query." }),
|
|
69
|
+
limit: Type.Optional(
|
|
70
|
+
Type.Integer({
|
|
71
|
+
description: "Maximum number of results to return. Defaults to 5.",
|
|
72
|
+
minimum: 1,
|
|
73
|
+
maximum: 20
|
|
74
|
+
})
|
|
75
|
+
),
|
|
76
|
+
fetchResult: Type.Optional(
|
|
77
|
+
Type.Boolean({
|
|
78
|
+
description: "Whether to fetch the first result. Defaults to false; image searches fetch image content when enabled."
|
|
79
|
+
})
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
Object.assign(params, providers[providerId]?.searchParameters)
|
|
84
|
+
return Type.Object(params)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getFetchParameters(config: WebToolsConfig) {
|
|
88
|
+
const configured = config.webFetchProvider
|
|
89
|
+
const providerId = configured && providers[configured]?.fetch ? configured : "firecrawl"
|
|
90
|
+
const params: Record<string, TSchema> = {
|
|
91
|
+
url: Type.String({ description: "The URL to fetch.", format: "uri" }),
|
|
92
|
+
timeout: Type.Optional(Type.Integer({ description: "Request timeout in milliseconds. Defaults to 30000.", minimum: 1 })),
|
|
93
|
+
includeMetadata: Type.Optional(
|
|
94
|
+
Type.Boolean({
|
|
95
|
+
description: "Append verbose page metadata to the markdown output. Defaults to false. Full metadata is always available in details."
|
|
96
|
+
})
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
Object.assign(params, providers[providerId]?.fetchParameters)
|
|
101
|
+
return Type.Object(params)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function registerLovelyWebSearchTool(pi: ExtensionAPI, config: WebToolsConfig = lovelyWebConfigSpec.defaults) {
|
|
12
105
|
pi.registerTool({
|
|
13
106
|
name: "web_search",
|
|
14
107
|
label: "Web Search",
|
|
@@ -18,28 +111,15 @@ export function registerLovelyWebTools(pi: ExtensionAPI) {
|
|
|
18
111
|
"Use web_search when the user asks for current web information, discovery, or sources beyond the local workspace.",
|
|
19
112
|
"Use web_fetch after web_search when you need the full content of a specific page."
|
|
20
113
|
],
|
|
21
|
-
parameters:
|
|
22
|
-
query: Type.String({ description: "The web search query." }),
|
|
23
|
-
limit: Type.Optional(
|
|
24
|
-
Type.Integer({
|
|
25
|
-
description: "Maximum number of results to return. Defaults to 5.",
|
|
26
|
-
minimum: 1,
|
|
27
|
-
maximum: 20
|
|
28
|
-
})
|
|
29
|
-
),
|
|
30
|
-
source: Type.Optional(StringEnum(["web", "news", "images"] as const)),
|
|
31
|
-
fetchResult: Type.Optional(
|
|
32
|
-
Type.Boolean({
|
|
33
|
-
description: "Whether to fetch the first result and include markdown. Defaults to true."
|
|
34
|
-
})
|
|
35
|
-
)
|
|
36
|
-
}),
|
|
114
|
+
parameters: getSearchParameters(config),
|
|
37
115
|
renderCall(args, theme, context) {
|
|
38
116
|
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0)
|
|
39
|
-
const
|
|
40
|
-
|
|
117
|
+
const input = args as unknown as SearchToolArgs
|
|
118
|
+
const mode = input.includeImages ? "images" : (input.source ?? input.topic ?? input.category ?? "web")
|
|
119
|
+
const bits = [mode, `limit ${input.limit ?? 5}`]
|
|
120
|
+
if (input.fetchResult === true) bits.push("fetch first")
|
|
41
121
|
text.setText(
|
|
42
|
-
`${theme.fg("toolTitle", theme.bold("web_search "))}${theme.fg("muted", `"${
|
|
122
|
+
`${theme.fg("toolTitle", theme.bold("web_search "))}${theme.fg("muted", `"${input.query}"`)} ${theme.fg("dim", `(${bits.join(", ")})`)}`
|
|
43
123
|
)
|
|
44
124
|
return text
|
|
45
125
|
},
|
|
@@ -50,11 +130,45 @@ export function registerLovelyWebTools(pi: ExtensionAPI) {
|
|
|
50
130
|
try {
|
|
51
131
|
const config = loadConfig(ctx.cwd)
|
|
52
132
|
const searchProvider = getProvider("search", config)
|
|
133
|
+
const input = params as unknown as SearchToolArgs
|
|
53
134
|
onUpdate?.({
|
|
54
|
-
content: [{ type: "text", text: `Searching web with ${searchProvider.label} for: ${
|
|
55
|
-
details: undefined
|
|
135
|
+
content: [{ type: "text", text: `Searching web with ${searchProvider.label} for: ${input.query}` }],
|
|
136
|
+
details: undefined
|
|
56
137
|
})
|
|
57
|
-
|
|
138
|
+
|
|
139
|
+
const { query, fetchResult, limit, ...providerOptions } = input
|
|
140
|
+
const searchOptions: SearchOptions = { ...providerOptions, limit: limit ?? 5, timeout: DEFAULT_TIMEOUT_MS }
|
|
141
|
+
const searchResult = await searchProvider.search(resolveApiKey(searchProvider, config), query, searchOptions, signal)
|
|
142
|
+
if (signal?.aborted) throw new Error("Search cancelled")
|
|
143
|
+
|
|
144
|
+
const first = searchResult.results[0]
|
|
145
|
+
let fetchedImage: ToolResult | undefined
|
|
146
|
+
if (fetchResult === true && first?.url) {
|
|
147
|
+
onUpdate?.({ content: [{ type: "text", text: `Fetching first result: ${first.url}` }], details: undefined })
|
|
148
|
+
try {
|
|
149
|
+
const isImageSearch = input.source === "images" || input.includeImages === true
|
|
150
|
+
if (isImageSearch) {
|
|
151
|
+
try {
|
|
152
|
+
fetchedImage = await fetchSearchResultImage(config, first.url, signal)
|
|
153
|
+
} catch {
|
|
154
|
+
if (signal?.aborted) throw new Error("Search cancelled")
|
|
155
|
+
// Some image-search providers return source pages instead of direct image URLs.
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (!fetchedImage && isFetchEnabled(config)) first.markdown = await fetchSearchResultMarkdown(config, first.url, signal)
|
|
159
|
+
if (signal?.aborted) throw new Error("Search cancelled")
|
|
160
|
+
} catch (err) {
|
|
161
|
+
if (signal?.aborted) throw err
|
|
162
|
+
first.description = first.description || `[Fetch failed: ${asErrorMessage(err)}]`
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const result: ToolResult = {
|
|
167
|
+
content: [{ type: "text", text: formatSearchOutput(searchResult.results) }, ...(fetchedImage?.content || [])],
|
|
168
|
+
details: fetchedImage ? { search: searchResult.raw, image: fetchedImage.details } : searchResult.raw
|
|
169
|
+
}
|
|
170
|
+
onUpdate?.(result)
|
|
171
|
+
return result
|
|
58
172
|
} catch (error) {
|
|
59
173
|
return {
|
|
60
174
|
content: [{ type: "text", text: `Web search failed: ${asErrorMessage(error)}` }],
|
|
@@ -64,7 +178,9 @@ export function registerLovelyWebTools(pi: ExtensionAPI) {
|
|
|
64
178
|
}
|
|
65
179
|
}
|
|
66
180
|
})
|
|
181
|
+
}
|
|
67
182
|
|
|
183
|
+
export function registerLovelyWebStaticTools(pi: ExtensionAPI, config: WebToolsConfig = lovelyWebConfigSpec.defaults) {
|
|
68
184
|
pi.registerTool({
|
|
69
185
|
name: "web_fetch",
|
|
70
186
|
label: "Web Fetch",
|
|
@@ -74,30 +190,18 @@ export function registerLovelyWebTools(pi: ExtensionAPI) {
|
|
|
74
190
|
"Use web_fetch when you need the full readable markdown content of a known URL.",
|
|
75
191
|
"Prefer web_fetch over bash/curl for web pages because web_fetch returns cleaned markdown suitable for agent context."
|
|
76
192
|
],
|
|
77
|
-
parameters:
|
|
78
|
-
url: Type.String({ description: "The URL to fetch.", format: "uri" }),
|
|
79
|
-
waitFor: Type.Optional(
|
|
80
|
-
Type.Integer({
|
|
81
|
-
description: "Milliseconds to wait before capturing content, useful for JS-heavy pages.",
|
|
82
|
-
minimum: 0
|
|
83
|
-
})
|
|
84
|
-
),
|
|
85
|
-
timeout: Type.Optional(Type.Integer({ description: "Request timeout in milliseconds. Defaults to 30000.", minimum: 1 })),
|
|
86
|
-
includeMetadata: Type.Optional(
|
|
87
|
-
Type.Boolean({
|
|
88
|
-
description:
|
|
89
|
-
"Append verbose page metadata to the markdown output. Defaults to false. Full metadata is always available in details."
|
|
90
|
-
})
|
|
91
|
-
)
|
|
92
|
-
}),
|
|
193
|
+
parameters: getFetchParameters(config),
|
|
93
194
|
renderCall(args, theme, context) {
|
|
94
195
|
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0)
|
|
196
|
+
const input = args as unknown as FetchToolArgs
|
|
95
197
|
const bits: string[] = []
|
|
96
|
-
if (
|
|
97
|
-
if (
|
|
98
|
-
if (
|
|
198
|
+
if (input.waitFor !== undefined) bits.push(`wait ${input.waitFor}ms`)
|
|
199
|
+
if (input.maxAgeHours !== undefined) bits.push(`max age ${input.maxAgeHours}h`)
|
|
200
|
+
if (input.extractDepth !== undefined) bits.push(input.extractDepth)
|
|
201
|
+
if (input.timeout !== undefined) bits.push(`timeout ${input.timeout}ms`)
|
|
202
|
+
if (input.includeMetadata) bits.push("metadata")
|
|
99
203
|
const suffix = bits.length ? ` ${theme.fg("dim", `(${bits.join(", ")})`)}` : ""
|
|
100
|
-
text.setText(`${theme.fg("toolTitle", theme.bold("web_fetch "))}${theme.fg("muted",
|
|
204
|
+
text.setText(`${theme.fg("toolTitle", theme.bold("web_fetch "))}${theme.fg("muted", input.url)}${suffix}`)
|
|
101
205
|
return text
|
|
102
206
|
},
|
|
103
207
|
renderResult(result, { expanded, isPartial }, theme) {
|
|
@@ -105,13 +209,34 @@ export function registerLovelyWebTools(pi: ExtensionAPI) {
|
|
|
105
209
|
},
|
|
106
210
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
107
211
|
try {
|
|
212
|
+
const input = params as unknown as FetchToolArgs
|
|
108
213
|
const config = loadConfig(ctx.cwd)
|
|
109
214
|
const fetchProvider = getProvider("fetch", config)
|
|
110
215
|
onUpdate?.({
|
|
111
|
-
content: [{ type: "text", text: `Fetching page with ${fetchProvider.label}: ${
|
|
112
|
-
details: undefined
|
|
216
|
+
content: [{ type: "text", text: `Fetching page with ${fetchProvider.label}: ${input.url}` }],
|
|
217
|
+
details: undefined
|
|
113
218
|
})
|
|
114
|
-
|
|
219
|
+
|
|
220
|
+
const result = await fetchProvider.fetch(
|
|
221
|
+
resolveApiKey(fetchProvider, config),
|
|
222
|
+
input.url,
|
|
223
|
+
{
|
|
224
|
+
timeout: input.timeout ?? DEFAULT_TIMEOUT_MS,
|
|
225
|
+
...(input.waitFor !== undefined ? { waitFor: input.waitFor } : {}),
|
|
226
|
+
...(input.maxAgeHours !== undefined ? { maxAgeHours: input.maxAgeHours } : {}),
|
|
227
|
+
...(input.extractDepth !== undefined ? { extractDepth: input.extractDepth } : {})
|
|
228
|
+
},
|
|
229
|
+
signal
|
|
230
|
+
)
|
|
231
|
+
if (signal?.aborted) throw new Error("Fetch cancelled")
|
|
232
|
+
|
|
233
|
+
const metadata = input.includeMetadata && result.metadata ? `\n\nMetadata:\n${stringify(result.metadata)}` : ""
|
|
234
|
+
const toolResult: ToolResult = {
|
|
235
|
+
content: [{ type: "text", text: `${result.markdown}${metadata}` }],
|
|
236
|
+
details: result.raw
|
|
237
|
+
}
|
|
238
|
+
onUpdate?.(toolResult)
|
|
239
|
+
return toolResult
|
|
115
240
|
} catch (error) {
|
|
116
241
|
return {
|
|
117
242
|
content: [{ type: "text", text: `Web fetch failed: ${asErrorMessage(error)}` }],
|
|
@@ -157,7 +282,7 @@ export function registerLovelyWebTools(pi: ExtensionAPI) {
|
|
|
157
282
|
if (!isImageEnabled(config)) throw new Error("web_image is disabled. Enable it via /lovely-web.")
|
|
158
283
|
onUpdate?.({
|
|
159
284
|
content: [{ type: "text", text: `Fetching image: ${params.url}` }],
|
|
160
|
-
details: undefined
|
|
285
|
+
details: undefined
|
|
161
286
|
})
|
|
162
287
|
return await imageImpl(
|
|
163
288
|
{
|
|
@@ -180,3 +305,8 @@ export function registerLovelyWebTools(pi: ExtensionAPI) {
|
|
|
180
305
|
}
|
|
181
306
|
})
|
|
182
307
|
}
|
|
308
|
+
|
|
309
|
+
export function registerLovelyWebTools(pi: ExtensionAPI, config: WebToolsConfig = lovelyWebConfigSpec.defaults) {
|
|
310
|
+
registerLovelyWebSearchTool(pi, config)
|
|
311
|
+
registerLovelyWebStaticTools(pi, config)
|
|
312
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xl0/pi-lovely-web",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Pi extension package for web_search, web_fetch, and web_image via Firecrawl, Exa, Tavily, and Brave.",
|
|
4
5
|
"keywords": [
|
|
5
6
|
"pi-package",
|
|
6
7
|
"firecrawl",
|
|
@@ -36,14 +37,20 @@
|
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
38
39
|
"@biomejs/biome": "^2.4.14",
|
|
40
|
+
"@earendil-works/pi-ai": "^0.79.10",
|
|
41
|
+
"@earendil-works/pi-coding-agent": "^0.79.10",
|
|
42
|
+
"@earendil-works/pi-tui": "^0.79.10",
|
|
39
43
|
"@typescript/native-preview": "^7.0.0-dev.20260502.1",
|
|
40
44
|
"bun-types": "^1.3.13",
|
|
41
45
|
"typescript": "^6.0.3"
|
|
42
46
|
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@xl0/pi-lovely-config": "^0.1.0"
|
|
49
|
+
},
|
|
43
50
|
"peerDependencies": {
|
|
44
|
-
"@earendil-works/pi-ai": ">=0.
|
|
45
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
46
|
-
"@earendil-works/pi-tui": ">=0.
|
|
51
|
+
"@earendil-works/pi-ai": ">=0.79.10",
|
|
52
|
+
"@earendil-works/pi-coding-agent": ">=0.79.10",
|
|
53
|
+
"@earendil-works/pi-tui": ">=0.79.10",
|
|
47
54
|
"typebox": "*"
|
|
48
55
|
},
|
|
49
56
|
"pi": {
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import type { ImageContent, TextContent } from "@earendil-works/pi-ai"
|
|
2
|
-
import { DEFAULT_TIMEOUT_MS, getProvider, resolveApiKey } from "./config.js"
|
|
3
|
-
import { asErrorMessage, formatSearchOutput, stringify } from "./format.js"
|
|
4
|
-
import type { WebToolsConfig } from "./providers/types.js"
|
|
5
|
-
|
|
6
|
-
export interface ToolResult {
|
|
7
|
-
content: Array<TextContent | ImageContent>
|
|
8
|
-
details: unknown
|
|
9
|
-
isError?: boolean
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export async function searchImpl(
|
|
13
|
-
config: WebToolsConfig,
|
|
14
|
-
params: { query: string; limit?: number; source?: string; fetchResult?: boolean },
|
|
15
|
-
signal?: AbortSignal,
|
|
16
|
-
onUpdate?: (result: ToolResult) => void
|
|
17
|
-
): Promise<ToolResult> {
|
|
18
|
-
const searchProvider = getProvider("search", config)
|
|
19
|
-
const apiKey = resolveApiKey(searchProvider, config)
|
|
20
|
-
|
|
21
|
-
const searchResult = await searchProvider.search(
|
|
22
|
-
apiKey,
|
|
23
|
-
params.query,
|
|
24
|
-
{
|
|
25
|
-
limit: params.limit ?? 5,
|
|
26
|
-
timeout: DEFAULT_TIMEOUT_MS,
|
|
27
|
-
...(params.source !== undefined ? { source: params.source } : {})
|
|
28
|
-
},
|
|
29
|
-
signal
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
if (signal?.aborted) throw new Error("Search cancelled")
|
|
33
|
-
|
|
34
|
-
const shouldFetch = params.fetchResult ?? true
|
|
35
|
-
const first = searchResult.results[0]
|
|
36
|
-
if (shouldFetch && first?.url) {
|
|
37
|
-
onUpdate?.({
|
|
38
|
-
content: [{ type: "text" as const, text: `Fetching first result: ${first.url}` }],
|
|
39
|
-
details: undefined as unknown
|
|
40
|
-
})
|
|
41
|
-
try {
|
|
42
|
-
const fetchProvider = getProvider("fetch", config)
|
|
43
|
-
const fetchApiKey = resolveApiKey(fetchProvider, config)
|
|
44
|
-
const fetched = await fetchProvider.fetch(fetchApiKey, first.url, { timeout: DEFAULT_TIMEOUT_MS }, signal)
|
|
45
|
-
|
|
46
|
-
if (signal?.aborted) throw new Error("Search cancelled")
|
|
47
|
-
first.markdown = fetched.markdown
|
|
48
|
-
if (fetched.metadata) (first as { metadata?: unknown }).metadata = fetched.metadata
|
|
49
|
-
} catch (err) {
|
|
50
|
-
first.description = first.description || `[Fetch failed: ${asErrorMessage(err)}]`
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const result: ToolResult = {
|
|
55
|
-
content: [{ type: "text" as const, text: formatSearchOutput(searchResult.results) }],
|
|
56
|
-
details: searchResult.raw
|
|
57
|
-
}
|
|
58
|
-
onUpdate?.(result)
|
|
59
|
-
return result
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export async function fetchImpl(
|
|
63
|
-
config: WebToolsConfig,
|
|
64
|
-
params: { url: string; waitFor?: number; timeout?: number; includeMetadata?: boolean },
|
|
65
|
-
signal?: AbortSignal,
|
|
66
|
-
onUpdate?: (result: ToolResult) => void
|
|
67
|
-
): Promise<ToolResult> {
|
|
68
|
-
const fetchProvider = getProvider("fetch", config)
|
|
69
|
-
const apiKey = resolveApiKey(fetchProvider, config)
|
|
70
|
-
|
|
71
|
-
const result = await fetchProvider.fetch(
|
|
72
|
-
apiKey,
|
|
73
|
-
params.url,
|
|
74
|
-
{
|
|
75
|
-
timeout: params.timeout ?? DEFAULT_TIMEOUT_MS,
|
|
76
|
-
...(params.waitFor !== undefined ? { waitFor: params.waitFor } : {})
|
|
77
|
-
},
|
|
78
|
-
signal
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
if (signal?.aborted) throw new Error("Fetch cancelled")
|
|
82
|
-
|
|
83
|
-
const warning =
|
|
84
|
-
["exa", "tavily"].includes(fetchProvider.id) && params.waitFor !== undefined
|
|
85
|
-
? `Warning: ${fetchProvider.label} ignores waitFor; request sent without any extra page-load delay.\n\n`
|
|
86
|
-
: ""
|
|
87
|
-
const metadata = params.includeMetadata && result.metadata ? `\n\nMetadata:\n${stringify(result.metadata)}` : ""
|
|
88
|
-
|
|
89
|
-
const toolResult: ToolResult = {
|
|
90
|
-
content: [{ type: "text" as const, text: `${warning}${result.markdown}${metadata}` }],
|
|
91
|
-
details: result.raw
|
|
92
|
-
}
|
|
93
|
-
onUpdate?.(toolResult)
|
|
94
|
-
return toolResult
|
|
95
|
-
}
|