ForcePilot/test/gaia_eval/file_handler.py

192 lines
5.4 KiB
Python
Raw Normal View History

"""GAIA 评估文件处理模块
GAIA 数据集中的附件文件转换为 Agent 可识别的 attachments + state files 格式
"""
from __future__ import annotations
import base64
import mimetypes
import os
from datetime import datetime
from src.utils import logger
# 支持直接读取内容的文本类文件扩展名
TEXT_EXTENSIONS = {
".txt", ".md", ".csv", ".tsv", ".json", ".jsonl",
".xml", ".html", ".htm", ".yaml", ".yml", ".log",
".py", ".js", ".ts", ".java", ".c", ".cpp", ".h",
".rb", ".go", ".rs", ".sh", ".bat", ".sql",
".ini", ".cfg", ".conf", ".toml",
}
# 支持用 PDF 解析的扩展名
PDF_EXTENSIONS = {".pdf"}
# 支持用 pandas 等读取的表格扩展名
SPREADSHEET_EXTENSIONS = {".xlsx", ".xls"}
# 图片类扩展名(用于多模态消息)
IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp"}
def _read_text_file(file_path: str) -> str | None:
"""读取纯文本文件"""
encodings = ["utf-8", "latin-1", "gbk"]
for enc in encodings:
try:
with open(file_path, encoding=enc) as f:
return f.read()
except (UnicodeDecodeError, OSError):
continue
return None
def _read_pdf_file(file_path: str) -> str | None:
"""解析 PDF 文件为文本"""
try:
import fitz # PyMuPDF
doc = fitz.open(file_path)
pages = []
for page in doc:
pages.append(page.get_text())
doc.close()
return "\n\n".join(pages)
except ImportError:
logger.warning("PyMuPDF (fitz) 未安装,无法解析 PDF。请安装: pip install PyMuPDF")
return None
except Exception as e:
logger.warning(f"PDF 解析失败 {file_path}: {e}")
return None
def _read_spreadsheet_file(file_path: str) -> str | None:
"""读取 Excel 文件并转为 CSV 文本"""
try:
import pandas as pd
df = pd.read_excel(file_path)
return df.to_csv(index=False)
except ImportError:
logger.warning("pandas/openpyxl 未安装,无法解析 Excel。请安装: pip install pandas openpyxl")
return None
except Exception as e:
logger.warning(f"Excel 解析失败 {file_path}: {e}")
return None
def _read_image_as_base64(file_path: str) -> str | None:
"""读取图片为 base64"""
try:
with open(file_path, "rb") as f:
data = f.read()
return base64.b64encode(data).decode("utf-8")
except Exception as e:
logger.warning(f"图片读取失败 {file_path}: {e}")
return None
def read_file_content(file_path: str) -> str | None:
"""根据文件类型读取内容,返回文本"""
if not file_path or not os.path.exists(file_path):
logger.warning(f"文件不存在: {file_path}")
return None
_, ext = os.path.splitext(file_path)
ext = ext.lower()
if ext in TEXT_EXTENSIONS:
return _read_text_file(file_path)
elif ext in PDF_EXTENSIONS:
return _read_pdf_file(file_path)
elif ext in SPREADSHEET_EXTENSIONS:
return _read_spreadsheet_file(file_path)
else:
# 尝试作为文本读取
content = _read_text_file(file_path)
if content:
return content
logger.warning(f"不支持的文件类型: {ext} ({file_path})")
return None
def build_attachments_and_files(
file_path: str,
file_name: str | None = None,
) -> tuple[list[dict], dict]:
"""将文件转换为 Agent 的 attachments 和 state files 格式
返回:
(attachments, files) 元组
- attachments: 附件元数据列表用于 AttachmentMiddleware
- files: StateBackend 格式的文件字典用于 read_file 工具
"""
if not file_path or not os.path.exists(file_path):
return [], {}
if not file_name:
file_name = os.path.basename(file_path)
_, ext = os.path.splitext(file_path)
ext = ext.lower()
# 图片使用多模态消息,不走 attachment 机制
if ext in IMAGE_EXTENSIONS:
return [], {}
content = read_file_content(file_path)
if not content:
return [], {}
now = datetime.utcnow().isoformat() + "+00:00"
virtual_path = f"/attachments/{file_name}"
# 构建 attachment 元数据
attachment = {
"file_name": file_name,
"file_path": virtual_path,
"status": "parsed",
"markdown": content,
"uploaded_at": now,
"truncated": False,
}
# 构建 state files与 _build_state_files 格式一致)
content_lines = content.split("\n")
files = {
virtual_path: {
"content": content_lines,
"created_at": now,
"modified_at": now,
}
}
return [attachment], files
def build_image_message_content(file_path: str, question: str) -> list[dict] | None:
"""为图片文件构建多模态消息内容
返回 HumanMessage content多模态格式 None 如果不是图片
"""
if not file_path or not os.path.exists(file_path):
return None
_, ext = os.path.splitext(file_path)
if ext.lower() not in IMAGE_EXTENSIONS:
return None
base64_data = _read_image_as_base64(file_path)
if not base64_data:
return None
mime_type = mimetypes.guess_type(file_path)[0] or "image/jpeg"
return [
{"type": "text", "text": question},
{"type": "image_url", "image_url": {"url": f"data:{mime_type};base64,{base64_data}"}},
]