192 lines
5.4 KiB
Python
192 lines
5.4 KiB
Python
"""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}"}},
|
||
]
|