Add inline image utilities and integrate image handling into Location, WebFetch, and WebSearch tools.
Browse files
src/tools/LocationTool/LocationTool.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
| 9 |
renderToolUseMessage,
|
| 10 |
renderToolUseProgressMessage,
|
| 11 |
} from './UI.js'
|
|
|
|
| 12 |
|
| 13 |
// ── Region detection ────────────────────────────────────────────────────────
|
| 14 |
|
|
@@ -365,6 +366,7 @@ type Output = {
|
|
| 365 |
error?: string
|
| 366 |
rawData?: unknown
|
| 367 |
ipGeo?: IpGeoInfo
|
|
|
|
| 368 |
}
|
| 369 |
|
| 370 |
// ── Amap API helpers ─────────────────────────────────────────────────────────
|
|
@@ -912,6 +914,7 @@ export const LocationTool = buildTool({
|
|
| 912 |
let geocoding: LocationResult | undefined
|
| 913 |
let places: PlaceResult[] | undefined
|
| 914 |
let directions: DirectionsResult | undefined
|
|
|
|
| 915 |
|
| 916 |
switch (action) {
|
| 917 |
case 'geocode':
|
|
@@ -995,6 +998,15 @@ export const LocationTool = buildTool({
|
|
| 995 |
})
|
| 996 |
}
|
| 997 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 998 |
return {
|
| 999 |
data: {
|
| 1000 |
action,
|
|
@@ -1003,6 +1015,7 @@ export const LocationTool = buildTool({
|
|
| 1003 |
places,
|
| 1004 |
directions,
|
| 1005 |
ipGeo,
|
|
|
|
| 1006 |
} satisfies Output,
|
| 1007 |
}
|
| 1008 |
} catch (error) {
|
|
@@ -1094,7 +1107,17 @@ export const LocationTool = buildTool({
|
|
| 1094 |
return {
|
| 1095 |
tool_use_id: toolUseID,
|
| 1096 |
type: 'tool_result',
|
| 1097 |
-
content:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1098 |
}
|
| 1099 |
},
|
| 1100 |
}) satisfies ToolDef<ReturnType<typeof inputSchema>, Output, LocationToolProgress>
|
|
|
|
| 9 |
renderToolUseMessage,
|
| 10 |
renderToolUseProgressMessage,
|
| 11 |
} from './UI.js'
|
| 12 |
+
import { fetchImagesAsInline } from '../../utils/inlineImageUtils.js'
|
| 13 |
|
| 14 |
// ── Region detection ────────────────────────────────────────────────────────
|
| 15 |
|
|
|
|
| 366 |
error?: string
|
| 367 |
rawData?: unknown
|
| 368 |
ipGeo?: IpGeoInfo
|
| 369 |
+
images?: Array<{ url: string; base64: string; mediaType: string }>
|
| 370 |
}
|
| 371 |
|
| 372 |
// ── Amap API helpers ─────────────────────────────────────────────────────────
|
|
|
|
| 914 |
let geocoding: LocationResult | undefined
|
| 915 |
let places: PlaceResult[] | undefined
|
| 916 |
let directions: DirectionsResult | undefined
|
| 917 |
+
let images: Output['images']
|
| 918 |
|
| 919 |
switch (action) {
|
| 920 |
case 'geocode':
|
|
|
|
| 998 |
})
|
| 999 |
}
|
| 1000 |
|
| 1001 |
+
if (places && places.length > 0) {
|
| 1002 |
+
const photoUrls = places
|
| 1003 |
+
.flatMap((p) => p.photos ?? [])
|
| 1004 |
+
.slice(0, 6)
|
| 1005 |
+
if (photoUrls.length > 0) {
|
| 1006 |
+
images = await fetchImagesAsInline(photoUrls, 6)
|
| 1007 |
+
}
|
| 1008 |
+
}
|
| 1009 |
+
|
| 1010 |
return {
|
| 1011 |
data: {
|
| 1012 |
action,
|
|
|
|
| 1015 |
places,
|
| 1016 |
directions,
|
| 1017 |
ipGeo,
|
| 1018 |
+
images,
|
| 1019 |
} satisfies Output,
|
| 1020 |
}
|
| 1021 |
} catch (error) {
|
|
|
|
| 1107 |
return {
|
| 1108 |
tool_use_id: toolUseID,
|
| 1109 |
type: 'tool_result',
|
| 1110 |
+
content: [
|
| 1111 |
+
{ type: 'text', text: lines.join('\n') },
|
| 1112 |
+
...(output.images?.map((img) => ({
|
| 1113 |
+
type: 'image' as const,
|
| 1114 |
+
source: {
|
| 1115 |
+
type: 'base64' as const,
|
| 1116 |
+
data: img.base64,
|
| 1117 |
+
media_type: img.mediaType as 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp',
|
| 1118 |
+
},
|
| 1119 |
+
})) ?? []),
|
| 1120 |
+
],
|
| 1121 |
}
|
| 1122 |
},
|
| 1123 |
}) satisfies ToolDef<ReturnType<typeof inputSchema>, Output, LocationToolProgress>
|
src/tools/LocationTool/prompt.ts
CHANGED
|
@@ -24,7 +24,12 @@ This tool provides location-based services using two map providers:
|
|
| 24 |
**Combining with other tools:**
|
| 25 |
- After getting place/POI results from LocationTool, you can use **WebSearchTool** to search for travel guides, reviews, or additional information about specific places
|
| 26 |
- Use **WebFetchTool** to fetch detailed information from URLs found in search results (e.g., attraction pages, hotel booking sites, travel blog posts)
|
| 27 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
**Transport modes for directions:**
|
| 30 |
- For China (Amap): "driving", "walking", "transit" (公交/地铁)
|
|
|
|
| 24 |
**Combining with other tools:**
|
| 25 |
- After getting place/POI results from LocationTool, you can use **WebSearchTool** to search for travel guides, reviews, or additional information about specific places
|
| 26 |
- Use **WebFetchTool** to fetch detailed information from URLs found in search results (e.g., attraction pages, hotel booking sites, travel blog posts)
|
| 27 |
+
- **ImageShowTool**: When place results include photo URLs, the tool will automatically display images inline in the terminal. You can also explicitly call ImageShowTool with any image URL (local path or https:// URL) to display images.
|
| 28 |
+
- **Google Street View**: For any location (lat,lng), you can directly call ImageShowTool with a Google Street View Static API URL to display a street view panorama:
|
| 29 |
+
- URL format: \`https://maps.googleapis.com/maps/api/streetview?size=800x400&location=<lat>,<lng>&heading=0&pitch=0&key=<YOUR_API_KEY>\`
|
| 30 |
+
- Note: The free tier of Google Street View Static API requires an API key but has generous quotas
|
| 31 |
+
- Use this when users ask for "street view" or "街景" of a location
|
| 32 |
+
- Example: Search places → WebSearch for reviews → WebFetch for detailed info → ImageShow for photos → ImageShow with Google Street View URL for street panoramas
|
| 33 |
|
| 34 |
**Transport modes for directions:**
|
| 35 |
- For China (Amap): "driving", "walking", "transit" (公交/地铁)
|
src/tools/WebFetchTool/WebFetchTool.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
| 14 |
type FetchedContent,
|
| 15 |
getURLMarkdownContent,
|
| 16 |
} from './utils.js' // 抓网页, 处理 markdown
|
|
|
|
| 17 |
|
| 18 |
const inputSchema = lazySchema(() =>
|
| 19 |
z.strictObject({ // 严格对象 不允许多字段
|
|
@@ -40,7 +41,9 @@ const outputSchema = lazySchema(() =>
|
|
| 40 |
)
|
| 41 |
type OutputSchema = ReturnType<typeof outputSchema>
|
| 42 |
|
| 43 |
-
export type Output = z.infer<OutputSchema>
|
|
|
|
|
|
|
| 44 |
|
| 45 |
// Tool 定义开始
|
| 46 |
export const WebFetchTool = buildTool({
|
|
@@ -180,6 +183,13 @@ To complete your request, I need to fetch content from the redirected URL. Pleas
|
|
| 180 |
result += `\n\n[Binary content also saved to ${persistedPath}]`
|
| 181 |
}
|
| 182 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
const output: Output = {
|
| 184 |
bytes,
|
| 185 |
code,
|
|
@@ -187,6 +197,7 @@ To complete your request, I need to fetch content from the redirected URL. Pleas
|
|
| 187 |
result,
|
| 188 |
durationMs: Date.now() - start,
|
| 189 |
url,
|
|
|
|
| 190 |
}
|
| 191 |
|
| 192 |
return {
|
|
@@ -210,11 +221,21 @@ To complete your request, I need to fetch content from the redirected URL. Pleas
|
|
| 210 |
}
|
| 211 |
}
|
| 212 |
},
|
| 213 |
-
mapToolResultToToolResultBlockParam(
|
| 214 |
return {
|
| 215 |
tool_use_id: toolUseID,
|
| 216 |
type: 'tool_result',
|
| 217 |
-
content:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
}
|
| 219 |
},
|
| 220 |
} satisfies ToolDef<InputSchema, Output>)
|
|
|
|
| 14 |
type FetchedContent,
|
| 15 |
getURLMarkdownContent,
|
| 16 |
} from './utils.js' // 抓网页, 处理 markdown
|
| 17 |
+
import { fetchImagesAsInline } from '../../utils/inlineImageUtils.js'
|
| 18 |
|
| 19 |
const inputSchema = lazySchema(() =>
|
| 20 |
z.strictObject({ // 严格对象 不允许多字段
|
|
|
|
| 41 |
)
|
| 42 |
type OutputSchema = ReturnType<typeof outputSchema>
|
| 43 |
|
| 44 |
+
export type Output = z.infer<OutputSchema> & {
|
| 45 |
+
images?: Array<{ url: string; base64: string; mediaType: string }>
|
| 46 |
+
}
|
| 47 |
|
| 48 |
// Tool 定义开始
|
| 49 |
export const WebFetchTool = buildTool({
|
|
|
|
| 183 |
result += `\n\n[Binary content also saved to ${persistedPath}]`
|
| 184 |
}
|
| 185 |
|
| 186 |
+
const imageUrls = result
|
| 187 |
+
? result.match(/!\[.*?\]\((https?:\/\/[^)\s]+)\)/g)?.map((m) => m.match(/!\[[^\]]*\]\(([^)]+)\)/)?.[1] ?? []) ?? []
|
| 188 |
+
: []
|
| 189 |
+
|
| 190 |
+
const images =
|
| 191 |
+
imageUrls.length > 0 ? await fetchImagesAsInline(imageUrls.slice(0, 4), 4) : undefined
|
| 192 |
+
|
| 193 |
const output: Output = {
|
| 194 |
bytes,
|
| 195 |
code,
|
|
|
|
| 197 |
result,
|
| 198 |
durationMs: Date.now() - start,
|
| 199 |
url,
|
| 200 |
+
images,
|
| 201 |
}
|
| 202 |
|
| 203 |
return {
|
|
|
|
| 221 |
}
|
| 222 |
}
|
| 223 |
},
|
| 224 |
+
mapToolResultToToolResultBlockParam(output, toolUseID) {
|
| 225 |
return {
|
| 226 |
tool_use_id: toolUseID,
|
| 227 |
type: 'tool_result',
|
| 228 |
+
content: [
|
| 229 |
+
{ type: 'text', text: output.result },
|
| 230 |
+
...(output.images?.map((img) => ({
|
| 231 |
+
type: 'image' as const,
|
| 232 |
+
source: {
|
| 233 |
+
type: 'base64' as const,
|
| 234 |
+
data: img.base64,
|
| 235 |
+
media_type: img.mediaType as 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp',
|
| 236 |
+
},
|
| 237 |
+
})) ?? []),
|
| 238 |
+
],
|
| 239 |
}
|
| 240 |
},
|
| 241 |
} satisfies ToolDef<InputSchema, Output>)
|
src/tools/WebFetchTool/prompt.ts
CHANGED
|
@@ -18,6 +18,7 @@ Usage notes:
|
|
| 18 |
- Includes a self-cleaning 15-minute cache for faster responses when repeatedly accessing the same URL
|
| 19 |
- When a URL redirects to a different host, the tool will inform you and provide the redirect URL in a special format. You should then make a new WebFetch request with the redirect URL to fetch the content.
|
| 20 |
- For GitHub URLs, prefer using the gh CLI via Bash instead (e.g., gh pr view, gh issue view, gh api).
|
|
|
|
| 21 |
`
|
| 22 |
|
| 23 |
export function makeSecondaryModelPrompt(
|
|
|
|
| 18 |
- Includes a self-cleaning 15-minute cache for faster responses when repeatedly accessing the same URL
|
| 19 |
- When a URL redirects to a different host, the tool will inform you and provide the redirect URL in a special format. You should then make a new WebFetch request with the redirect URL to fetch the content.
|
| 20 |
- For GitHub URLs, prefer using the gh CLI via Bash instead (e.g., gh pr view, gh issue view, gh api).
|
| 21 |
+
- **ImageShowTool**: When fetched content contains image URLs (e.g., from a wiki page or travel blog), images will be automatically displayed inline in the terminal. You can also explicitly call ImageShowTool with any image URL to display it.
|
| 22 |
`
|
| 23 |
|
| 24 |
export function makeSecondaryModelPrompt(
|
src/tools/WebSearchTool/WebSearchTool.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
| 11 |
renderToolUseMessage,
|
| 12 |
renderToolUseProgressMessage,
|
| 13 |
} from './UI.js'
|
|
|
|
| 14 |
|
| 15 |
const inputSchema = lazySchema(() =>
|
| 16 |
z.strictObject({
|
|
@@ -44,7 +45,9 @@ const outputSchema = lazySchema(() =>
|
|
| 44 |
}),
|
| 45 |
)
|
| 46 |
|
| 47 |
-
export type Output = z.infer<ReturnType<typeof outputSchema>>
|
|
|
|
|
|
|
| 48 |
|
| 49 |
export type { WebSearchProgress } from '../../types/tools.js'
|
| 50 |
import type { WebSearchProgress } from '../../types/tools.js'
|
|
@@ -257,6 +260,14 @@ export const WebSearchTool = buildTool({
|
|
| 257 |
},
|
| 258 |
]
|
| 259 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
const duration = (performance.now() - start) / 1000
|
| 261 |
|
| 262 |
return {
|
|
@@ -264,6 +275,7 @@ export const WebSearchTool = buildTool({
|
|
| 264 |
query: input.query,
|
| 265 |
results: output,
|
| 266 |
durationSeconds: duration,
|
|
|
|
| 267 |
},
|
| 268 |
}
|
| 269 |
} catch (error) {
|
|
@@ -298,8 +310,7 @@ export const WebSearchTool = buildTool({
|
|
| 298 |
text += r + '\n\n'
|
| 299 |
} else {
|
| 300 |
r.content.forEach((item: any, i: number) => {
|
| 301 |
-
|
| 302 |
-
text += `${i + 1}. ${item.title}\n${item.url}\n${item.snippet || ''}${imgLine}\n`
|
| 303 |
})
|
| 304 |
}
|
| 305 |
}
|
|
@@ -307,7 +318,17 @@ export const WebSearchTool = buildTool({
|
|
| 307 |
return {
|
| 308 |
tool_use_id: toolUseID,
|
| 309 |
type: 'tool_result',
|
| 310 |
-
content:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
}
|
| 312 |
},
|
| 313 |
}) satisfies ToolDef<any, Output, WebSearchProgress>
|
|
|
|
| 11 |
renderToolUseMessage,
|
| 12 |
renderToolUseProgressMessage,
|
| 13 |
} from './UI.js'
|
| 14 |
+
import { fetchImagesAsInline } from '../../utils/inlineImageUtils.js'
|
| 15 |
|
| 16 |
const inputSchema = lazySchema(() =>
|
| 17 |
z.strictObject({
|
|
|
|
| 45 |
}),
|
| 46 |
)
|
| 47 |
|
| 48 |
+
export type Output = z.infer<ReturnType<typeof outputSchema>> & {
|
| 49 |
+
images?: Array<{ url: string; base64: string; mediaType: string }>
|
| 50 |
+
}
|
| 51 |
|
| 52 |
export type { WebSearchProgress } from '../../types/tools.js'
|
| 53 |
import type { WebSearchProgress } from '../../types/tools.js'
|
|
|
|
| 260 |
},
|
| 261 |
]
|
| 262 |
|
| 263 |
+
const imageUrls = cleaned
|
| 264 |
+
.map((r) => r.image)
|
| 265 |
+
.filter((url): url is string => !!url)
|
| 266 |
+
.slice(0, 4)
|
| 267 |
+
|
| 268 |
+
const images =
|
| 269 |
+
imageUrls.length > 0 ? await fetchImagesAsInline(imageUrls, 4) : undefined
|
| 270 |
+
|
| 271 |
const duration = (performance.now() - start) / 1000
|
| 272 |
|
| 273 |
return {
|
|
|
|
| 275 |
query: input.query,
|
| 276 |
results: output,
|
| 277 |
durationSeconds: duration,
|
| 278 |
+
images,
|
| 279 |
},
|
| 280 |
}
|
| 281 |
} catch (error) {
|
|
|
|
| 310 |
text += r + '\n\n'
|
| 311 |
} else {
|
| 312 |
r.content.forEach((item: any, i: number) => {
|
| 313 |
+
text += `${i + 1}. ${item.title}\n${item.url}\n${item.snippet || ''}\n`
|
|
|
|
| 314 |
})
|
| 315 |
}
|
| 316 |
}
|
|
|
|
| 318 |
return {
|
| 319 |
tool_use_id: toolUseID,
|
| 320 |
type: 'tool_result',
|
| 321 |
+
content: [
|
| 322 |
+
{ type: 'text', text },
|
| 323 |
+
...(output.images?.map((img) => ({
|
| 324 |
+
type: 'image' as const,
|
| 325 |
+
source: {
|
| 326 |
+
type: 'base64' as const,
|
| 327 |
+
data: img.base64,
|
| 328 |
+
media_type: img.mediaType as 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp',
|
| 329 |
+
},
|
| 330 |
+
})) ?? []),
|
| 331 |
+
],
|
| 332 |
}
|
| 333 |
},
|
| 334 |
}) satisfies ToolDef<any, Output, WebSearchProgress>
|
src/tools/WebSearchTool/prompt.ts
CHANGED
|
@@ -21,9 +21,8 @@ Search Strategy:
|
|
| 21 |
|
| 22 |
Search Result Format:
|
| 23 |
- Each search result includes: title, URL, snippet, and optionally an image URL (thumbnail)
|
| 24 |
-
-
|
| 25 |
-
-
|
| 26 |
-
- Images make responses much more readable — use them whenever available
|
| 27 |
|
| 28 |
CRITICAL REQUIREMENT - You MUST follow this:
|
| 29 |
- After answering the user's question, you MUST include a "Sources:" section at the end of your response
|
|
|
|
| 21 |
|
| 22 |
Search Result Format:
|
| 23 |
- Each search result includes: title, URL, snippet, and optionally an image URL (thumbnail)
|
| 24 |
+
- **Images are automatically displayed inline in the terminal** when search results include image thumbnails — no need to manually call ImageShowTool for search result images
|
| 25 |
+
- You can also explicitly call ImageShowTool with any image URL (https://... or local path) to display images
|
|
|
|
| 26 |
|
| 27 |
CRITICAL REQUIREMENT - You MUST follow this:
|
| 28 |
- After answering the user's question, you MUST include a "Sources:" section at the end of your response
|
src/utils/inlineImageUtils.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { downloadImage } from './imageUrlCache.js'
|
| 2 |
+
import { logForDebugging } from './debug.js'
|
| 3 |
+
|
| 4 |
+
const IMAGE_URL_PATTERNS = [
|
| 5 |
+
/https?:\/\/[^\s<>\"')\]]+\.(?:jpg|jpeg|png|gif|webp)(?:[^\s<>\"'\]]*)/gi,
|
| 6 |
+
/https?:\/\/[^\s<>\"')\]]+(?:store\.is\.autonavi\.com|maps\.google\.com|upload\.wikimedia\.org)[^\s<>\"'\]]*/gi,
|
| 7 |
+
]
|
| 8 |
+
|
| 9 |
+
export interface InlineImage {
|
| 10 |
+
url: string
|
| 11 |
+
base64: string
|
| 12 |
+
mediaType: string
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export function detectImageUrls(text: string): string[] {
|
| 16 |
+
const urls = new Set<string>()
|
| 17 |
+
for (const pattern of IMAGE_URL_PATTERNS) {
|
| 18 |
+
const matches = text.matchAll(pattern)
|
| 19 |
+
for (const match of matches) {
|
| 20 |
+
if (match[0]) {
|
| 21 |
+
urls.add(match[0])
|
| 22 |
+
}
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
return Array.from(urls)
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
export async function fetchImagesAsInline(
|
| 29 |
+
urls: string[],
|
| 30 |
+
maxImages = 4,
|
| 31 |
+
): Promise<InlineImage[]> {
|
| 32 |
+
const uniqueUrls = [...new Set(urls)].slice(0, maxImages)
|
| 33 |
+
const results: InlineImage[] = []
|
| 34 |
+
|
| 35 |
+
await Promise.all(
|
| 36 |
+
uniqueUrls.map(async (url) => {
|
| 37 |
+
try {
|
| 38 |
+
const cached = await downloadImage(url)
|
| 39 |
+
if (cached) {
|
| 40 |
+
results.push({
|
| 41 |
+
url,
|
| 42 |
+
base64: cached.buffer.toString('base64'),
|
| 43 |
+
mediaType: cached.mediaType,
|
| 44 |
+
})
|
| 45 |
+
}
|
| 46 |
+
} catch (err) {
|
| 47 |
+
logForDebugging(`fetchImagesAsInline: failed to fetch ${url}: ${err}`)
|
| 48 |
+
}
|
| 49 |
+
}),
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
return results
|
| 53 |
+
}
|