Files
2025-10-01 18:40:05 +08:00

741 lines
29 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import sys
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
import json
import re
import asyncio
import requests
from openai import OpenAI
from config import Global
from bs4 import BeautifulSoup
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor, as_completed
from playwright.async_api import async_playwright
from playwright.sync_api import sync_playwright
from urllib.parse import urljoin
from rich.console import Console
from rich.panel import Panel
from rich.status import Status
console = Console()
# if Global.modelscope['has_proxy']:
# with open('assets\www_bing_cookie.json', 'r', encoding='utf-8') as f:
# bing_cookie = json.load(f)
# else:
# with open('assets\cn_bing_cookie.json', 'r', encoding='utf-8') as f:
# bing_cookie = json.load(f)
# bing_cookie = '; '.join([f"{cookie['name']}={cookie['value']}" for cookie in bing_cookie])
if Global.modelscope['has_proxy']:
bing_cookie = Global.modelscope['www_bing_cookie']
else:
bing_cookie = Global.modelscope['cn_bing_cookie']
tools = [
{
"type": "function",
"function": {
"name": "search_web",
"description": "基于bing搜索,可以搜索网页或学术内容,返回搜索结果列表",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "搜索关键词或问题"
},
"search_type": {
"type": "string",
"enum": ["网页", "学术"],
"description": "搜索类型,默认为网页搜索",
"default": "网页"
}
},
"required": ["query"]
}
}
},
{
"type": "function",
"function": {
"name": "get_webpage_text",
"description": "获取指定URL列表的网页详细文本内容,专门用于提取网页中的文字信息。如果需要获取视频、图片、链接等其他元素,请使用get_website_elements",
"parameters": {
"type": "object",
"properties": {
"urls": {
"type": "array",
"items": {
"type": "string",
"format": "uri"
},
"description": "要获取文本内容的URL列表(可以多输入几个URL,这样获取的信息更全面)"
}
},
"required": ["urls"]
}
}
},
{
"type": "function",
"function": {
"name": "get_website_elements",
"description": "获取指定URL的网站内容,包括标题、图片、视频、链接、导航和meta信息。专门用于提取网页中的媒体资源和跳转链接,方便在特定网站中跳转网页。如果只需要获取网页文本内容,请使用get_webpage_text",
"parameters": {
"type": "object",
"properties": {
"url": {
"type": "string",
"format": "uri",
"description": "要获取内容的网址"
}
},
"required": ["url"]
}
}
},
{
"type": "function",
"function": {
"name": "download_media",
"description": "下载给定 URL 列表中的媒体文件(视频和图片)到指定目录",
"parameters": {
"type": "object",
"properties": {
"urls": {
"type": "array",
"items": {
"type": "string",
"format": "uri"
},
"description": "要下载的媒体文件 URL 列表"
},
"output_dir": {
"type": "string",
"description": "下载文件的输出目录,默认为 './downloads'",
"default": "./downloads"
}
},
"required": ["urls"]
}
}
}
]
SYSTEM_PROMPT = """
今天是%s
你是WebAgent,一个专业的信息搜索和分析助手。你的核心使命是帮助用户完成各种搜索任务,通过多轮对话和工具调用来收集全面、准确、最新的信息。
1. **get_website_elements**(媒体资源和链接发现工具)
- 专门用于获取网页上的图片、视频、跳转链接、导航元素等
- 适用于需要获取媒体资源或在特定网站内部进行深度导航
- 帮助发现网站内的相关页面、子页面和媒体资源
- 注意:这个工具主要获取页面元素和资源,无法获取详细文本内容
2. **组合使用模式**
- 使用get_website_elements发现新的有价值链接和媒体资源
- 对新发现的重要链接继续使用get_webpage_text获取文本内容
3. **信息整合**:
- 持续收集和验证信息
- 交叉对比不同来源的内容
- 确保信息的准确性和时效性
4. **任务完成**:
- 当收集到足够全面的信息后
- 输出 [任务完成] 标记
## 搜索原则
- **权威性优先**:优先选择官方网站、知名媒体、学术机构等权威来源
- **时效性关注**:重点关注最新发布的信息和数据
- **全面性保证**:确保从多个角度和维度收集信息
- **准确性验证**:通过多个来源验证关键信息的准确性
- **精选策略**:从大量搜索结果中挑选最有价值的网站进行深度分析
## 工具功能说明
1. **search_web**:网络搜索工具
- 功能:通过关键词发现相关网页
- 策略:获得搜索结果后,要从中挑选最有价值的网站进行后续分析
2. **get_webpage_text**:文本内容获取工具
- 功能:专门获取网页的详细文本内容
- 用途:深入分析网页文字内容,提取有价值的文本信息
- 使用策略:对从search_web中挑选出的优质网站使用
- 局限:只能获取文本信息,无法获取HTML、图片、视频、链接等其他元素
3. **get_website_elements**:媒体资源和链接发现工具
- 功能:获取网页上的图片、视频、跳转链接、导航元素、meta信息等页面结构
- 用途:获取媒体资源、在特定网站内部进行导航、发现更多相关页面
- 使用场景:需要获取图片、视频、链接等非文本内容时使用
- 局限:主要获取页面元素和资源,无法获取详细文本内容
4. **download_media**:媒体下载工具,用于下载相关媒体文件
记住:search_web用于"发现网站",get_webpage_text用于"获取文本",get_website_elements用于"获取媒体和链接"
""" % (f"{datetime.today().strftime('%Y年%m月%d日')},{datetime.today().strftime('%A')}")
class Agent:
def __init__(self):
self.client = OpenAI(
api_key=Global.modelscope['api_key'],
base_url=Global.modelscope['base_url']
)
self.messages = [{'role':'system', 'content':SYSTEM_PROMPT}]
def request(self, task_content):
self.messages.append({'role':'user', 'content':task_content})
while True:
response = self.client.chat.completions.create(
model = 'Qwen/Qwen3-235B-A22B-Thinking-2507',
messages=self.messages,
stream=True,
tools=tools,
temperature=0.6,
top_p=0.95
)
content = ''
reasoning = True
assistant_message = {'role': 'assistant', 'content': ''}
for chunk in response:
if chunk.choices:
delta = chunk.choices[0].delta
if delta.content:
if reasoning:
reasoning = False
console.print("\n[bold blue]Agent Response:[/bold blue]")
console.print(delta.content, end='')
content += delta.content
elif delta.reasoning_content:
console.print(delta.reasoning_content, end='', style="dim")
if delta.tool_calls:
if 'tool_calls' not in assistant_message:
assistant_message['tool_calls'] = []
for tool_call in delta.tool_calls:
if len(assistant_message['tool_calls']) <= tool_call.index:
assistant_message['tool_calls'].append({
'id': tool_call.id,
'type': 'function',
'function': {
'name': tool_call.function.name,
'arguments': tool_call.function.arguments or ''
}
})
else:
assistant_message['tool_calls'][tool_call.index]['function']['arguments'] += tool_call.function.arguments or ''
console.print()
for message in self.messages:
if message['role'] == 'tool':
if "get_website_elements" in message['content'][:100]:
if len(message['content']) > 1000:
message['content'] = message['content'][:1000]+'...'
self.messages.append(assistant_message)
if assistant_message.get('tool_calls'):
for tool_call in assistant_message['tool_calls']:
try:
function_name = tool_call['function']['name']
if tool_call['function']['arguments']:
args = json.loads(tool_call['function']['arguments'])
else:
args = {}
console.print(f"\n[bold magenta]Executing Tool: {function_name}[/bold magenta]")
with Status(f"[bold green]Running {function_name}...", console=console):
if function_name == "search_web":
result = get_search(**args)
elif function_name == "get_webpage_text":
result = get_webpage_content(args['urls'])
elif function_name == "get_website_elements":
async def extract_content():
async with WebContentExtractor() as extractor:
return await extractor.extract_page_content(args['url'])
result = asyncio.run(extract_content())
elif function_name == "download_media":
result = download_media(**args)
else:
raise Exception("错误的函数名")
console.print(f"\n[bold cyan]{function_name.upper()} Output:[/bold cyan]")
console.print(Panel(str(result)[:2000] + "..." if len(str(result)) > 2000 else str(result), border_style="cyan"))
self.messages.append({
'role': 'tool',
'content': f"执行成功: {function_name} - {str(result)[:30000]}",
'tool_call_id': tool_call['id']
})
except Exception as e:
error_message = f"执行失败: {str(e)}"
console.print(f"\n[bold red]{function_name.upper()} Error:[/bold red]")
console.print(Panel(error_message, border_style="red"))
self.messages.append({
'role': 'tool',
'content': error_message,
'tool_call_id': tool_call['id']
})
if '[任务完成]' in content:
console.print("\n[bold green]Task Completed[/bold green]")
break
if not assistant_message.get('tool_calls'):
user_input = console.input('\n[bold yellow]Reply:[/bold yellow] ')
self.messages.append({'role':'user', 'content':user_input})
try:
port = Global.modelscope['port']
response = requests.post(f'http://127.0.0.1:{port}/return', json={"function_name":"Web_Agent", "function_id":agent_id, "result":content})
if response.status_code != 200:
raise response.content
except:
raise "无法与server通信"
class WebContentExtractor:
def __init__(self):
self.playwright = None
self.browser = None
async def __aenter__(self):
self.playwright = await async_playwright().start()
self.browser = await self.playwright.chromium.launch(headless=Global.modelscope['headless'], args=['--start-minimized'])
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
if self.context:
await self.context.close()
if self.browser:
await self.browser.close()
if self.playwright:
await self.playwright.stop()
async def extract_page_content(self, url, wait_time=3000):
"""提取网页核心内容"""
self.context = await self.browser.new_context()
page = await self.context.new_page()
try:
try:
await page.goto(url, wait_until='networkidle')
await page.wait_for_timeout(wait_time)
except:
pass
html_content = await page.content()
extracted_data = self._preprocess_html(html_content, url)
return extracted_data
finally:
await page.close()
def _preprocess_html(self, html_content, base_url):
"""预处理HTML,提取核心信息"""
soup = BeautifulSoup(html_content, 'html.parser')
self._remove_unwanted_elements(soup)
extracted_data = {
'url': base_url,
'title': self._extract_title(soup),
'content': self._extract_main_content(soup)[:10000],
'meta_info': self._extract_meta_info(soup),
'navigation': self._extract_navigation(soup),
'links': self._extract_links(soup, base_url),
'images': self._extract_images(soup, base_url)
}
return extracted_data
def _remove_unwanted_elements(self, soup):
"""移除不必要的元素以减少token消耗"""
unwanted_tags = [
'script', 'style', 'noscript', 'iframe', 'embed', 'object',
'form', 'input', 'button', 'select', 'textarea',
'svg', 'canvas', 'video', 'audio'
]
for tag in unwanted_tags:
for element in soup.find_all(tag):
element.decompose()
for comment in soup.find_all(string=lambda text: isinstance(text, str) and text.strip().startswith('<!--')):
comment.extract()
ad_classes = ['ad', 'ads', 'advertisement', 'banner', 'popup', 'modal', 'overlay']
for class_name in ad_classes:
for element in soup.find_all(class_=re.compile(class_name, re.I)):
element.decompose()
important_attrs = ['href', 'src', 'alt', 'title', 'id', 'class']
for tag in soup.find_all():
attrs_to_remove = [attr for attr in tag.attrs if attr not in important_attrs]
for attr in attrs_to_remove:
del tag[attr]
def _extract_title(self, soup):
"""提取页面标题"""
title_tag = soup.find('title')
if title_tag:
return title_tag.get_text().strip()
h1_tag = soup.find('h1')
if h1_tag:
return h1_tag.get_text().strip()
return "无标题"
def _extract_meta_info(self, soup):
"""提取meta信息"""
meta_info = {}
description = soup.find('meta', attrs={'name': 'description'})
if description and description.get('content'):
meta_info['description'] = description['content']
keywords = soup.find('meta', attrs={'name': 'keywords'})
if keywords and keywords.get('content'):
meta_info['keywords'] = keywords['content']
author = soup.find('meta', attrs={'name': 'author'})
if author and author.get('content'):
meta_info['author'] = author['content']
return meta_info
def _extract_main_content(self, soup):
"""提取主要内容"""
main_selectors = [
'main', 'article', '[role="main"]',
'.content', '.main-content', '.post-content',
'#content', '#main-content', '#post-content'
]
for selector in main_selectors:
main_element = soup.select_one(selector)
if main_element:
return self._clean_text(main_element.get_text())
body = soup.find('body')
if body:
for unwanted in body.find_all(['nav', 'aside', 'footer', 'header']):
unwanted.decompose()
return self._clean_text(body.get_text())
return self._clean_text(soup.get_text())
def _extract_navigation(self, soup):
"""提取导航信息"""
nav_links = []
nav_elements = soup.find_all(['nav', '[role="navigation"]'])
for nav in nav_elements:
links = nav.find_all('a', href=True)
for link in links:
nav_links.append({
'text': self._clean_text(link.get_text()),
'href': link['href']
})
return nav_links
def _extract_links(self, soup, base_url):
"""提取所有链接"""
links = []
for link in soup.find_all('a', href=True):
href = link['href']
text = self._clean_text(link.get_text())
if text and href:
absolute_url = urljoin(base_url, href)
links.append({
'text': text,
'href': href,
'absolute_url': absolute_url
})
return links
def _extract_images(self, soup, base_url):
"""提取图片信息"""
images = []
for img in soup.find_all('img', src=True):
src = img['src']
alt = img.get('alt', '')
absolute_url = urljoin(base_url, src)
images.append({
'src': src,
'alt': alt,
'absolute_url': absolute_url
})
return images
def _extract_headings(self, soup):
"""提取标题结构"""
headings = []
for i in range(1, 7):
for heading in soup.find_all(f'h{i}'):
headings.append({
'level': i,
'text': self._clean_text(heading.get_text())
})
return headings
def _clean_text(self, text):
"""清理文本"""
if not text:
return ""
text = re.sub(r'\s+', ' ', text.strip())
text = re.sub(r'[^\w\s\u4e00-\u9fff.,!?;:()\-]', '', text)
return text
def is_html_content(filepath):
try:
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read(1024)
content_lower = content.lower().strip()
return (content_lower.startswith('<!doctype html') or
'<html' in content_lower or
'<head>' in content_lower or
'<body>' in content_lower)
except:
return False
def download_with_requests(url, output_path):
"""使用requests下载文件"""
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
}
try:
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
with open(output_path, 'wb') as f:
f.write(response.content)
return True
except Exception:
return False
def download_with_browser(page, url, output_path, filename):
"""使用浏览器下载文件"""
try:
page.goto(url)
page.evaluate(f"""
fetch('{url}')
.then(response => response.blob())
.then(blob => {{
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = '{filename}';
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
}});
""")
with page.expect_download(timeout=10000) as download_info:
pass
download = download_info.value
download.save_as(output_path)
return True
except Exception:
return False
def download_media(urls, output_dir='./downloads'):
output_dir = os.path.join(WORKDIR, output_dir)
os.makedirs(output_dir, exist_ok=True)
results = []
p = sync_playwright().start()
browser = p.chromium.launch(headless=False, args=['--start-minimized'])
context = browser.new_context()
page = context.new_page()
try:
for url in urls:
try:
filename = os.path.basename(url)
output_path = os.path.join(output_dir, filename)
if download_with_requests(url, output_path):
results.append(f'成功下载,已保存到 {output_path}')
else:
if download_with_browser(page, url, output_path, filename):
if is_html_content(output_path):
results.append(f'警告: {url} 下载的是HTML文件,可能不是预期的媒体文件')
else:
results.append(f'成功下载,已保存到 {output_path}')
else:
results.append(f'下载 {url} 时出现错误: requests和浏览器都失败')
except Exception as e:
results.append(f'下载 {url} 时出现错误: {e}')
finally:
browser.close()
p.stop()
return results
def get_search(query, search_type='网页'):
with ThreadPoolExecutor(max_workers=5) as executor:
futures = [executor.submit(bing_search, query, i, search_type) for i in range(Global.modelscope['search_pages'])]
results = []
for future in as_completed(futures):
results.append(future.result())
search_results = []
for result in results:
search_results += result
return search_results
def get_webpage_content(urls):
results = []
with ThreadPoolExecutor(max_workers=5) as executor:
futures = []
for x,url in enumerate(urls):
futures.append(executor.submit(get_url_content, url))
for future in as_completed(futures):
results.append(future.result())
response_data = []
for url, content in results:
response_data.append({
'url': url,
'content': content if content and len(content) > 200 else None
})
for x in range(len(response_data)-1, -1, -1):
if not response_data[x]['content']:
response_data.pop(x)
if response_data:
text = ''
lenth = int(Global.modelscope['search_max_context']/len(response_data))
for i,x in enumerate(response_data):
text += f"[webpage {i} URL]: {x['url']}\n"
content = x['content']
if len(content) > lenth:
content = content[:lenth]+'...'
text += f"[webpage {i} begin]{content}[webpage {i} end]\n"
return text
else:
return None
def bing_search(query, page, search_type):
results = []
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
'Accept-Encoding': 'gzip, deflate',
'Cookie': bing_cookie
}
if search_type == '网页':
url_insert = ''
select = 'li.b_algo'
elif search_type == '学术':
url_insert = '/academic'
select = 'li.aca_algo'
if Global.modelscope['has_proxy']:
url = f'https://www.bing.com{url_insert}/search?q={query}&first={page*10+1}&ensearch=1'
else:
url = f'https://cn.bing.com{url_insert}/search?q={query}&first={page*10}'
response = requests.get(url, headers=headers, timeout=10)
soup = BeautifulSoup(response.text, 'html.parser')
for item in soup.select(select):
title_elem = item.select_one('h2')
link_elem = item.select_one('a')
desc_elem = item.select_one('.b_caption p')
if title_elem and link_elem:
result = {
'title': title_elem.text.strip(),
'url': link_elem.get('href'),
'introduction': desc_elem.text.strip() if desc_elem else ""
}
results.append(result)
return results
def get_url_content(url, proxy_dict=None):
try:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
response = requests.get(
url,
headers=headers,
proxies=proxy_dict,
timeout=2
)
if response.encoding.lower() == 'iso-8859-1':
response.encoding = response.apparent_encoding
soup = BeautifulSoup(response.text, 'html.parser')
for element in soup(["script", "style"]):
element.decompose()
text = soup.get_text()
lines = (line.strip() for line in text.splitlines())
chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
return url, ' '.join(chunk for chunk in chunks if chunk)
except Exception as e:
return url, '网络错误'
def get_proxies(protocol, count):
response = requests.get(f'https://proxy.scdn.io/api/get_proxy.php?protocol={protocol}&count={count}')
results = response.json()["data"]["proxies"]
return results
import sys
if sys.argv:
with open(sys.argv[1], 'r', encoding='utf-8') as f:
data = json.load(f)
task_content = data["task_content"]
WORKDIR = 'temp'
agent_id = data["agent_id"]
os.unlink(sys.argv[1])
console.print(f"[bold blue]WebAgent Starting[/bold blue]")
console.print(f"Task: {task_content}")
console.print(f"Working Directory: {WORKDIR}")
console.print("-" * 50)
agent = Agent()
agent.request(task_content)