File size: 10,741 Bytes
358eb7e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
"""
扩展工具模块 - GAIA Agent 扩展功能
包含:parse_pdf, parse_excel, image_ocr, transcribe_audio

注意:这些工具需要额外的依赖库,如果导入失败会优雅降级。
"""

import os
from typing import Optional, List
from langchain_core.tools import tool

from config import MAX_FILE_SIZE, TOOL_TIMEOUT


# ========================================
# PDF 解析工具
# ========================================

@tool
def parse_pdf(file_path: str, page_numbers: str = "all") -> str:
    """
    解析 PDF 文件,提取文本内容。

    Args:
        file_path: PDF 文件路径
        page_numbers: 页码范围
            - "all": 所有页面
            - "1": 第 1 页
            - "1-5": 第 1 到 5 页
            - "1,3,5": 第 1、3、5 页

    Returns:
        PDF 文本内容

    限制:
        - 扫描版 PDF 需配合 OCR
        - 复杂排版可能顺序错乱
    """
    try:
        import pdfplumber
    except ImportError:
        return "PDF 解析不可用:请安装 pdfplumber 库 (pip install pdfplumber)"

    if not os.path.exists(file_path):
        return f"文件不存在: {file_path}"

    if not file_path.lower().endswith('.pdf'):
        return f"不是 PDF 文件: {file_path}"

    try:
        with pdfplumber.open(file_path) as pdf:
            total_pages = len(pdf.pages)

            # 解析页码范围
            if page_numbers == "all":
                pages_to_read = range(total_pages)
            elif "-" in page_numbers:
                start, end = map(int, page_numbers.split("-"))
                pages_to_read = range(start - 1, min(end, total_pages))
            elif "," in page_numbers:
                pages_to_read = [int(p) - 1 for p in page_numbers.split(",")]
                pages_to_read = [p for p in pages_to_read if 0 <= p < total_pages]
            else:
                page_num = int(page_numbers) - 1
                if 0 <= page_num < total_pages:
                    pages_to_read = [page_num]
                else:
                    return f"页码超出范围,PDF 共有 {total_pages} 页"

            # 提取文本
            text_parts = []
            for i in pages_to_read:
                page = pdf.pages[i]
                text = page.extract_text()
                if text:
                    text_parts.append(f"--- 第 {i + 1} 页 ---\n{text}")

            if not text_parts:
                return "PDF 中没有提取到文本内容(可能是扫描版,请尝试使用 OCR)"

            result = "\n\n".join(text_parts)

            # 限制长度
            if len(result) > MAX_FILE_SIZE:
                return result[:MAX_FILE_SIZE] + f"\n\n... [内容已截断,共 {len(result)} 字符]"

            return result

    except Exception as e:
        return f"PDF 解析出错: {type(e).__name__}: {str(e)}"


# ========================================
# Excel 解析工具
# ========================================

@tool
def parse_excel(file_path: str, sheet_name: str = None, max_rows: int = 100) -> str:
    """
    解析 Excel 文件内容。

    Args:
        file_path: Excel 文件路径(.xlsx, .xls)
        sheet_name: 工作表名称,默认第一个
        max_rows: 最大读取行数,默认 100

    Returns:
        表格内容(Markdown 格式)
    """
    try:
        import pandas as pd
    except ImportError:
        return "Excel 解析不可用:请安装 pandas 和 openpyxl 库"

    if not os.path.exists(file_path):
        return f"文件不存在: {file_path}"

    try:
        # 读取 Excel
        if sheet_name:
            df = pd.read_excel(file_path, sheet_name=sheet_name, nrows=max_rows)
        else:
            df = pd.read_excel(file_path, nrows=max_rows)

        # 获取工作表信息
        excel_file = pd.ExcelFile(file_path)
        sheet_names = excel_file.sheet_names

        # 构建输出
        output = []
        output.append(f"工作表: {sheet_names}")
        output.append(f"当前读取: {sheet_name or sheet_names[0]}")
        output.append(f"数据形状: {df.shape[0]} 行 x {df.shape[1]} 列")
        output.append("")

        # 转换为 Markdown 表格
        output.append(df.to_markdown(index=False))

        result = "\n".join(output)

        # 限制长度
        if len(result) > MAX_FILE_SIZE:
            return result[:MAX_FILE_SIZE] + f"\n\n... [内容已截断]"

        return result

    except Exception as e:
        return f"Excel 解析出错: {type(e).__name__}: {str(e)}"


# ========================================
# 图片 OCR 工具
# ========================================

@tool
def image_ocr(file_path: str, language: str = "eng") -> str:
    """
    对图片进行 OCR 文字识别。

    Args:
        file_path: 图片路径(png/jpg/jpeg/bmp/gif/tiff)
        language: 识别语言
            - "eng": 英文
            - "chi_sim": 简体中文
            - "chi_tra": 繁体中文
            - "eng+chi_sim": 多语言

    Returns:
        识别出的文字

    注意:
        需要安装 Tesseract OCR 引擎
    """
    try:
        import pytesseract
        from PIL import Image
    except ImportError:
        return "OCR 不可用:请安装 pytesseract 和 Pillow 库"

    if not os.path.exists(file_path):
        return f"文件不存在: {file_path}"

    # 检查文件格式
    valid_extensions = {'.png', '.jpg', '.jpeg', '.bmp', '.gif', '.tiff', '.tif'}
    ext = os.path.splitext(file_path)[1].lower()
    if ext not in valid_extensions:
        return f"不支持的图片格式: {ext},支持: {', '.join(valid_extensions)}"

    try:
        # 打开图片
        image = Image.open(file_path)

        # 执行 OCR
        text = pytesseract.image_to_string(image, lang=language)

        if not text.strip():
            return "图片中没有识别到文字内容"

        # 清理文本
        text = text.strip()

        # 限制长度
        if len(text) > MAX_FILE_SIZE:
            return text[:MAX_FILE_SIZE] + f"\n\n... [内容已截断]"

        return text

    except pytesseract.TesseractNotFoundError:
        return "OCR 引擎未安装:请安装 Tesseract OCR (https://github.com/tesseract-ocr/tesseract)"
    except Exception as e:
        return f"OCR 识别出错: {type(e).__name__}: {str(e)}"


# ========================================
# 音频转写工具
# ========================================

@tool
def transcribe_audio(file_path: str, language: str = "auto") -> str:
    """
    将音频文件转写为文字。

    使用 OpenAI Whisper 模型进行转写。

    Args:
        file_path: 音频路径(mp3/wav/m4a/ogg/flac)
        language: 语言代码
            - "auto": 自动检测
            - "en": 英文
            - "zh": 中文
            - "ja": 日文
            等等

    Returns:
        转写的文字内容
    """
    try:
        import whisper
    except ImportError:
        return "音频转写不可用:请安装 openai-whisper 库 (pip install openai-whisper)"

    if not os.path.exists(file_path):
        return f"文件不存在: {file_path}"

    # 检查文件格式
    valid_extensions = {'.mp3', '.wav', '.m4a', '.ogg', '.flac', '.wma', '.aac'}
    ext = os.path.splitext(file_path)[1].lower()
    if ext not in valid_extensions:
        return f"不支持的音频格式: {ext},支持: {', '.join(valid_extensions)}"

    try:
        # 加载模型(使用 base 模型平衡速度和准确性)
        model = whisper.load_model("base")

        # 转写配置
        options = {}
        if language != "auto":
            options["language"] = language

        # 执行转写
        result = model.transcribe(file_path, **options)

        text = result.get("text", "").strip()

        if not text:
            return "音频中没有识别到语音内容"

        # 添加语言检测信息
        detected_lang = result.get("language", "unknown")
        output = f"[检测到语言: {detected_lang}]\n\n{text}"

        # 限制长度
        if len(output) > MAX_FILE_SIZE:
            return output[:MAX_FILE_SIZE] + f"\n\n... [内容已截断]"

        return output

    except Exception as e:
        return f"音频转写出错: {type(e).__name__}: {str(e)}"


# ========================================
# 视觉分析工具(可选,基于多模态 LLM)
# ========================================

@tool
def analyze_image(file_path: str, question: str = "请描述这张图片的内容") -> str:
    """
    使用多模态 LLM 分析图片内容。

    适用于:
    - 图片内容描述
    - 图表数据提取
    - 图片中的文字识别(比 OCR 更智能)

    Args:
        file_path: 图片路径
        question: 关于图片的问题

    Returns:
        LLM 对图片的分析结果
    """
    try:
        import base64
        from langchain_openai import ChatOpenAI
        from langchain_core.messages import HumanMessage
        from config import OPENAI_BASE_URL, OPENAI_API_KEY, MODEL
    except ImportError:
        return "图片分析不可用:缺少必要的依赖"

    if not os.path.exists(file_path):
        return f"文件不存在: {file_path}"

    try:
        # 读取图片并编码
        with open(file_path, "rb") as f:
            image_data = base64.b64encode(f.read()).decode("utf-8")

        # 检测图片格式
        ext = os.path.splitext(file_path)[1].lower()
        mime_types = {
            '.png': 'image/png',
            '.jpg': 'image/jpeg',
            '.jpeg': 'image/jpeg',
            '.gif': 'image/gif',
            '.webp': 'image/webp',
        }
        mime_type = mime_types.get(ext, 'image/png')

        # 构建多模态消息
        message = HumanMessage(
            content=[
                {"type": "text", "text": question},
                {
                    "type": "image_url",
                    "image_url": {
                        "url": f"data:{mime_type};base64,{image_data}"
                    }
                }
            ]
        )

        # 调用 LLM(添加超时保护)
        llm = ChatOpenAI(
            model=MODEL,
            base_url=OPENAI_BASE_URL,
            api_key=OPENAI_API_KEY,
            timeout=60,  # 60秒超时
            max_retries=1,
        )

        response = llm.invoke([message])
        return response.content

    except Exception as e:
        return f"图片分析出错: {type(e).__name__}: {str(e)}"


# ========================================
# 导出扩展工具列表
# ========================================
EXTENSION_TOOLS = [
    parse_pdf,
    parse_excel,
    image_ocr,
    transcribe_audio,
    analyze_image,
]