mirror of
https://github.com/zenlm/zen-agent.git
synced 2026-10-30 14:58:52 +00:00
213 lines
8.0 KiB
Python
213 lines
8.0 KiB
Python
# Copyright 2023 The Qwen team, Alibaba Group. All rights reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import json
|
|
import os
|
|
import random
|
|
import time
|
|
from typing import Dict, List, Optional, OrderedDict, Tuple, Union
|
|
import requests
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
import socket
|
|
import requests.packages.urllib3.util.connection as connection
|
|
from qwen_agent.tools.base import BaseTool, register_tool
|
|
from qwen_agent.log import logger
|
|
from qwen_agent.llm.schema import Message, ContentItem
|
|
from qwen_agent.utils.utils import extract_images_from_messages
|
|
|
|
|
|
SERPAPI_IMAGE_SEARCH_KEY = os.getenv('SERPAPI_IMAGE_SEARCH_KEY', '')
|
|
QWEN_IMAGE_SEARCH_MAX_RETRY_TIMES = int(os.getenv('QWEN_IMAGE_SEARCH_MAX_RETRY_TIMES', '3'))
|
|
SERPAPI_URL = 'https://serpapi.com/search.json'
|
|
|
|
_orig_getaddrinfo = socket.getaddrinfo
|
|
|
|
def _new_getaddrinfo(*args, **kwargs):
|
|
responses = _orig_getaddrinfo(*args, **kwargs)
|
|
return [r for r in responses if r[0] == socket.AF_INET]
|
|
|
|
socket.getaddrinfo = _new_getaddrinfo
|
|
|
|
class ImageResult(BaseModel):
|
|
"""
|
|
Represents an image search result with URL, title, and metadata.
|
|
"""
|
|
id: str = Field(..., description='Unique identifier for the image')
|
|
title: str = Field(..., description='Title or caption of the image')
|
|
imgurl: str = Field(..., description='Direct URL to the image')
|
|
url: str = Field(..., description='Source page URL where the image was found')
|
|
width: str = Field(..., description='Image width in pixels')
|
|
height: str = Field(..., description='Image height in pixels')
|
|
content: str = Field(default='', description='Additional content or description')
|
|
|
|
def __str__(self):
|
|
result = {}
|
|
if self.id:
|
|
result['id'] = self.id
|
|
if self.title:
|
|
result['title'] = self.title
|
|
if self.imgurl:
|
|
result['imgurl'] = self.imgurl
|
|
if self.content:
|
|
result['description'] = self.content
|
|
|
|
return json.dumps(result, ensure_ascii=False)
|
|
|
|
def __getitem__(self, item):
|
|
return getattr(self, item)
|
|
|
|
def __setitem__(self, key, value):
|
|
setattr(self, key, value)
|
|
|
|
|
|
def serper_search(image_url: str, check_accessibility: bool = True, max_retry: int = QWEN_IMAGE_SEARCH_MAX_RETRY_TIMES) -> dict:
|
|
"""
|
|
Image Search with SerpApi
|
|
"""
|
|
if not SERPAPI_IMAGE_SEARCH_KEY:
|
|
raise ValueError(
|
|
'SERPAPI_IMAGE_SEARCH_KEY is None! Please Apply for an apikey from https://serper.dev and set it as an environment variable by `export SERPAPI_IMAGE_SEARCH_KEY=xxxxxx`'
|
|
)
|
|
|
|
payload = {
|
|
'engine': 'google_reverse_image',
|
|
'image_url': image_url,
|
|
'api_key': SERPAPI_IMAGE_SEARCH_KEY,
|
|
'hl': 'zh-CN',
|
|
'gl': 'cn',
|
|
}
|
|
|
|
for _ in range(max_retry):
|
|
success = False
|
|
start_time = time.perf_counter()
|
|
response = None
|
|
try:
|
|
response = requests.get(SERPAPI_URL, params=payload)
|
|
response.raise_for_status()
|
|
json_response = response.json()
|
|
items_data = json_response.get('image_results', []) + json_response.get('inline_images', [])
|
|
results: Dict[str, ImageResult] = OrderedDict()
|
|
for item_data in items_data:
|
|
try:
|
|
image_direct_url = item_data.get('original', item_data.get('thumbnail'))
|
|
source_page_url = item_data.get('link', '')
|
|
if not image_direct_url:
|
|
continue
|
|
image = ImageResult(id=str(item_data.get('position', '')),
|
|
title=item_data.get('title', ''),
|
|
imgurl=image_direct_url,
|
|
url=source_page_url,
|
|
width=item_data.get('width', ''),
|
|
height=item_data.get('height', ''),
|
|
content=item_data.get('snippet', ''))
|
|
if image.imgurl in results and len(results[image.imgurl].title) > len(image.title):
|
|
logger.debug(f"Duplicate image found: {image.imgurl}, skipping")
|
|
else:
|
|
if check_accessibility:
|
|
_, is_accessible = check_image_url_accessibility(image.imgurl)
|
|
if is_accessible:
|
|
results[image.imgurl] = image
|
|
else:
|
|
results[image.imgurl] = image
|
|
success = True
|
|
except Exception as e:
|
|
logger.warning(f"Failed to parse image item: {e}")
|
|
continue
|
|
return [x for x in results.values()]
|
|
except Exception as e:
|
|
response_text = response.text if response and response.text else None
|
|
logger.error(f'image_search_fail, Error: {e}')
|
|
time.sleep(random.uniform(0.1, 1))
|
|
finally:
|
|
cost_time = int((time.perf_counter() - start_time) * 1000)
|
|
return []
|
|
|
|
@register_tool('image_search', allow_overwrite=True)
|
|
class ImageSearch(BaseTool):
|
|
name = 'image_search'
|
|
description = 'Image search engine, input the image and search for similar images with image information.'
|
|
parameters = {
|
|
'type': 'object',
|
|
'properties': {
|
|
'img_idx': {
|
|
'type': 'number',
|
|
'description': 'The index of the image (starting from 0)'
|
|
}
|
|
},
|
|
'required': ['img_idx']
|
|
}
|
|
|
|
def call(self, params: Union[str, dict], **kwargs) -> str:
|
|
params = self._verify_json_format_args(params)
|
|
image_id = int(params['img_idx'])
|
|
images = extract_images_from_messages(kwargs.get('messages', []))
|
|
if not images:
|
|
return 'Error: no images found in the messages.'
|
|
if image_id >= len(images):
|
|
image_id = len(images) - 1
|
|
|
|
image_url = images[image_id]
|
|
try:
|
|
search_results = serper_search(image_url=image_url, check_accessibility=True)
|
|
content = []
|
|
for i, r in enumerate(search_results):
|
|
|
|
txt = f'[{str(i+1)}] "{r.imgurl}" {r.title}\n{r.content}'
|
|
txt = txt.strip('\n')
|
|
if txt:
|
|
content.append(ContentItem(text=txt))
|
|
if r.imgurl:
|
|
content.append(ContentItem(image=r.imgurl))
|
|
return content
|
|
except Exception as e:
|
|
logger.info(f'Exception in ImageSearch.call: {repr(e)}')
|
|
content = []
|
|
return content
|
|
|
|
|
|
def check_image_url_accessibility(url: str, timeout: int = 10) -> Tuple[str, bool]:
|
|
"""
|
|
Check if an image URL is accessible (synchronous version)
|
|
|
|
Args:
|
|
url: Image URL to check
|
|
timeout: Request timeout in seconds
|
|
|
|
Returns:
|
|
Tuple[str, bool]: A tuple containing the URL and a boolean indicating if it's accessible (True if status is 200)
|
|
"""
|
|
try:
|
|
response = requests.head(url, timeout=timeout, allow_redirects=True)
|
|
return url, response.status_code == 200
|
|
except Exception as e:
|
|
logger.debug(f"Image URL not accessible: {url}, error: {e}")
|
|
return url, False
|
|
|
|
|
|
if __name__ == '__main__':
|
|
print(ImageSearch().call(
|
|
params={'img_idx': 0},
|
|
messages=[
|
|
Message(
|
|
role='user',
|
|
content=[
|
|
ContentItem(
|
|
image=
|
|
'https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20241022/emyrja/dog_and_girl.jpeg')
|
|
])
|
|
]))
|
|
|