mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
fix(image-search): keep the source page URL in DuckDuckGo and Serper results (#6915)
* test(image-search): expect source_url in DuckDuckGo and Serper results * fix(image-search): keep the source page URL in DuckDuckGo and Serper results The DuckDuckGo and Serper image_search tools returned only title, image_url and thumbnail_url and dropped the page each image comes from (DDGS `url`, Serper `link`). Brave already returns it as `source_url`, and the workspace UI links every image thumbnail through `source_url`, so DuckDuckGo (the default provider) and Serper results rendered as "Unsafe link omitted" and the agent lost the image's source page. Return `source_url` from both providers, screened with `safe_public_result_url` like the image and thumbnail URLs.
This commit is contained in:
parent
2dc92a2e4a
commit
d06fc14be9
@ -1,12 +1,12 @@
|
||||
# Image Search
|
||||
|
||||
DDG, Brave, and Serper `image_search` screen returned image and thumbnail URLs with
|
||||
`community.url_safety.safe_public_result_url`. This pure string check does not
|
||||
resolve hostnames; any later downloader must enforce its own connection-time
|
||||
address policy. The screen reuses `is_blocked_address` for literal and embedded
|
||||
IPv4 addresses, including multicast/reserved forms. Shared regression coverage
|
||||
is in `tests/test_image_result_url_safety.py`; DDG tool coverage is in
|
||||
`tests/test_ddg_image_search_urls.py`.
|
||||
DDG, Brave, and Serper `image_search` screen returned image, thumbnail and
|
||||
source page URLs with `community.url_safety.safe_public_result_url`. This pure
|
||||
string check does not resolve hostnames; any later downloader must enforce its
|
||||
own connection-time address policy. The screen reuses `is_blocked_address` for
|
||||
literal and embedded IPv4 addresses, including multicast/reserved forms. Shared
|
||||
regression coverage is in `tests/test_image_result_url_safety.py`; DDG tool
|
||||
coverage is in `tests/test_ddg_image_search_urls.py`.
|
||||
|
||||
Normalize IDNA's Unicode dot separators before checking blocked hostnames or IP
|
||||
literals; preserve the original URL for allowed results. Client-normalization
|
||||
|
||||
@ -147,6 +147,7 @@ async def image_search_tool(
|
||||
"title": title if isinstance(title, str) else "",
|
||||
"image_url": image_url,
|
||||
"thumbnail_url": thumbnail_url,
|
||||
"source_url": safe_public_result_url(row.get("url")),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@ -497,6 +497,7 @@ def image_search_tool(query: str, max_results: int = 5) -> str:
|
||||
"title": r.get("title", ""),
|
||||
"image_url": image_url,
|
||||
"thumbnail_url": thumbnail_url,
|
||||
"source_url": _safe_public_url(r.get("link")),
|
||||
}
|
||||
)
|
||||
if len(normalized_results) >= max_results:
|
||||
|
||||
@ -10,8 +10,8 @@ from deerflow.community.url_safety import safe_public_result_url
|
||||
@pytest.mark.asyncio
|
||||
async def test_image_search_drops_unsafe_result_urls() -> None:
|
||||
rows = [
|
||||
{"title": "safe", "image": "https://images.example/full.jpg", "thumbnail": "https://images.example/thumb.jpg"},
|
||||
{"title": "unsafe original", "image": "http://127.0.0.1/private", "thumbnail": "https://images.example/thumb2.jpg"},
|
||||
{"title": "safe", "image": "https://images.example/full.jpg", "thumbnail": "https://images.example/thumb.jpg", "url": "https://images.example/page"},
|
||||
{"title": "unsafe original", "image": "http://127.0.0.1/private", "thumbnail": "https://images.example/thumb2.jpg", "url": "http://10.0.0.1/page"},
|
||||
{"title": "unsafe thumbnail", "image": "https://images.example/full3.jpg", "thumbnail": "javascript:alert(1)"},
|
||||
{"title": "both unsafe", "image": "http://2130706433/private", "thumbnail": "http://localhost/thumb"},
|
||||
]
|
||||
@ -21,9 +21,9 @@ async def test_image_search_drops_unsafe_result_urls() -> None:
|
||||
|
||||
assert result["total_results"] == 3
|
||||
assert result["results"] == [
|
||||
{"title": "safe", "image_url": "https://images.example/full.jpg", "thumbnail_url": "https://images.example/thumb.jpg"},
|
||||
{"title": "unsafe original", "image_url": "", "thumbnail_url": "https://images.example/thumb2.jpg"},
|
||||
{"title": "unsafe thumbnail", "image_url": "https://images.example/full3.jpg", "thumbnail_url": ""},
|
||||
{"title": "safe", "image_url": "https://images.example/full.jpg", "thumbnail_url": "https://images.example/thumb.jpg", "source_url": "https://images.example/page"},
|
||||
{"title": "unsafe original", "image_url": "", "thumbnail_url": "https://images.example/thumb2.jpg", "source_url": ""},
|
||||
{"title": "unsafe thumbnail", "image_url": "https://images.example/full3.jpg", "thumbnail_url": "", "source_url": ""},
|
||||
]
|
||||
|
||||
|
||||
|
||||
@ -49,4 +49,4 @@ async def test_ddg_image_search_keeps_safe_thumbnail_when_idna_image_is_private(
|
||||
result = json.loads(await tools.image_search_tool.ainvoke({"query": "reference"}))
|
||||
|
||||
assert result["total_results"] == 1
|
||||
assert result["results"] == [{"title": "reference", "image_url": "", "thumbnail_url": "https://images.example/thumb.jpg"}]
|
||||
assert result["results"] == [{"title": "reference", "image_url": "", "thumbnail_url": "https://images.example/thumb.jpg", "source_url": ""}]
|
||||
|
||||
@ -18,6 +18,7 @@ async def test_image_search_uses_full_image_url_not_thumbnail():
|
||||
"title": "a cat",
|
||||
"image": "https://example.com/full.jpg",
|
||||
"thumbnail": "https://example.com/thumb.jpg",
|
||||
"url": "https://example.com/cats/page",
|
||||
}
|
||||
]
|
||||
cfg = MagicMock()
|
||||
@ -32,6 +33,8 @@ async def test_image_search_uses_full_image_url_not_thumbnail():
|
||||
result = output["results"][0]
|
||||
assert result["image_url"] == "https://example.com/full.jpg"
|
||||
assert result["thumbnail_url"] == "https://example.com/thumb.jpg"
|
||||
# The web UI links each thumbnail through `source_url`, as Brave's image_search emits it.
|
||||
assert result["source_url"] == "https://example.com/cats/page"
|
||||
|
||||
|
||||
async def _search_kwargs(query="a cat", **kwargs):
|
||||
|
||||
@ -846,11 +846,13 @@ class TestImageSearchTool:
|
||||
"title": "Cat 1",
|
||||
"imageUrl": "https://example.com/cat1.jpg",
|
||||
"thumbnailUrl": "https://example.com/cat1_thumb.jpg",
|
||||
"link": "https://example.com/cats/1",
|
||||
},
|
||||
{
|
||||
"title": "Cat 2",
|
||||
"imageUrl": "https://example.com/cat2.jpg",
|
||||
"thumbnailUrl": "https://example.com/cat2_thumb.jpg",
|
||||
"link": "https://example.com/cats/2",
|
||||
},
|
||||
]
|
||||
mock_resp = _make_serper_images_response(images)
|
||||
@ -868,6 +870,7 @@ class TestImageSearchTool:
|
||||
assert parsed["results"][0]["title"] == "Cat 1"
|
||||
assert parsed["results"][0]["image_url"] == "https://example.com/cat1.jpg"
|
||||
assert parsed["results"][0]["thumbnail_url"] == "https://example.com/cat1_thumb.jpg"
|
||||
assert parsed["results"][0]["source_url"] == "https://example.com/cats/1"
|
||||
assert parsed["usage_hint"] == "Use the 'image_url' values as reference images in image generation. Download them first if needed."
|
||||
|
||||
def test_sends_correct_headers_and_payload_to_images_endpoint(self, mock_config_with_key):
|
||||
@ -1277,7 +1280,7 @@ class TestImageSearchTool:
|
||||
images = [
|
||||
{"title": "Local", "imageUrl": "file:///etc/passwd", "thumbnailUrl": "http://127.0.0.1/thumb.jpg"},
|
||||
{"title": "Data", "imageUrl": "data:image/png;base64,abc", "thumbnailUrl": "http://10.0.0.1/thumb.jpg"},
|
||||
{"title": "Safe", "imageUrl": "https://example.com/i.jpg", "thumbnailUrl": "http://example.com/t.jpg"},
|
||||
{"title": "Safe", "imageUrl": "https://example.com/i.jpg", "thumbnailUrl": "http://example.com/t.jpg", "link": "http://127.0.0.1/page"},
|
||||
]
|
||||
mock_resp = _make_serper_images_response(images)
|
||||
|
||||
@ -1293,6 +1296,7 @@ class TestImageSearchTool:
|
||||
assert parsed["results"][0]["title"] == "Safe"
|
||||
assert parsed["results"][0]["image_url"] == "https://example.com/i.jpg"
|
||||
assert parsed["results"][0]["thumbnail_url"] == "http://example.com/t.jpg"
|
||||
assert parsed["results"][0]["source_url"] == ""
|
||||
|
||||
def test_all_unsafe_image_urls_return_error(self, mock_config_with_key):
|
||||
images = [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user