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
| package cn.hollis.llm.mentor.agent.tools; import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.text.PDFTextStripper; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.hwpf.HWPFDocument; import org.apache.poi.hwpf.extractor.WordExtractor; import org.apache.poi.sl.usermodel.TextShape; import org.apache.poi.ss.usermodel.*; import org.apache.poi.xslf.usermodel.XMLSlideShow; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.apache.poi.xwpf.usermodel.XWPFParagraph; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.core.io.ClassPathResource; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URI; import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; public class FileReaderTool { private static final Logger log = LoggerFactory.getLogger(FileReaderTool.class); private static final int MAX_CHARS = 8000;
@Tool(description = """ 读取文件并返回其文本内容。支持从绝对路径、classpath 或网络地址读取。 支持格式: - PDF (.pdf) - Word (.docx / .doc) - Excel (.xlsx / .xls) —— 返回所有 Sheet 的表格文本 - PowerPoint (.pptx) —— 返回所有幻灯片的文字 - 纯文本 (.txt / .md / .csv / .log / .xml / .json / .yaml / .yml / .properties) 参数 path 可以是: - 文件的绝对路径,如:/Users/hollis/data/sample.txt - classpath 路径,以 classpath: 开头,如:classpath:templates/sample.txt - 网络地址,以 http:// 或 https:// 开头,如:http://localhost:9001/api/v1/download-shared-object/xxx 对于 PDF 文件,可选参数 startPage / endPage 指定读取页码范围(从 1 开始);其他格式忽略此参数。 若文本过长会自动截断并在末尾注明剩余字符数。 """) public String read_file( @ToolParam(description = "文件路径,可以是绝对路径、classpath: 开头或 http(s):// 开头的 URL,如 /Users/hollis/data/sample.txt、classpath:templates/sample.txt 或 http://example.com/file.pdf") String path, @ToolParam(required = false, description = "【仅 PDF 有效】起始页码(从 1 开始,默认第 1 页)") Integer startPage, @ToolParam(required = false, description = "【仅 PDF 有效】结束页码(含,默认最后一页)") Integer endPage) { log.info("Starting execution of tool: read_file, path: {}, startPage: {}, endPage: {}", path, startPage, endPage); if (path == null || path.isBlank()) { return "Error: path 不能为空"; } private String readFromClasspath(String resourcePath, Integer startPage, Integer endPage) { ClassPathResource resource = new ClassPathResource(resourcePath); if (!resource.exists()) { return "Error: classpath 资源不存在 -> " + resourcePath; } String ext = getExtension(resourcePath).toLowerCase(); try (InputStream is = resource.getInputStream()) { return switch (ext) { case "pdf" -> readPdfFromStream(is, startPage, endPage); case "docx" -> readDocxFromStream(is); case "doc" -> readDocFromStream(is); case "xlsx" -> readExcelFromStream(is, false); case "xls" -> readExcelFromStream(is, true); case "pptx" -> readPptxFromStream(is); default -> readTextFromStream(is, ext); }; } catch (Exception e) { log.error("读取 classpath 文件失败: {}", resourcePath, e); return "Error: 读取 classpath 文件失败 -> " + e.getMessage(); } } private String readFromFilesystem(String filePath, Integer startPage, Integer endPage) { File file = new File(filePath); if (!file.exists()) { return "Error: 文件不存在 -> " + filePath; } if (!file.canRead()) { return "Error: 文件无读取权限 -> " + filePath; } String ext = getExtension(file.getName()).toLowerCase(); try { return switch (ext) { case "pdf" -> readPdf(file, startPage, endPage); case "docx" -> readDocx(file); case "doc" -> readDoc(file); case "xlsx" -> readExcel(file, false); case "xls" -> readExcel(file, true); case "pptx" -> readPptx(file); default -> readText(file); }; } catch (Exception e) { log.error("读取文件失败: {}", filePath, e); return "Error: 读取文件失败 -> " + e.getMessage(); } } private String readFromUrl(String url String, Integer startPage, Integer endPage) { try { URL url = URI.create(url String).toURL(); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); connection.setConnectTimeout(30000); int responseCode = connection.getResponseCode(); if (responseCode != HttpURLConnection.HTTP_OK) { return "Error: HTTP 请求失败,状态码 -> " + responseCode; } String ext = getExtension(filename).toLowerCase(); try (InputStream is = connection.getInputStream()) { return switch (ext) { case "pdf" -> readPdfFromStream(is, startPage, endPage); case "docx" -> readDocxFromStream(is); case "doc" -> readDocFromStream(is); case "xlsx" -> readExcelFromStream(is, false); case "xls" -> readExcelFromStream(is, true); case "pptx" -> readPptxFromStream(is); default -> readTextFromStream(is, ext); }; } } catch (Exception e) { log.error("从网络地址读取文件失败: {}", url String, e); return "Error: 从网络地址读取文件失败 -> " + e.getMessage(); } } private String readPdfFromStream(InputStream is, Integer startPage, Integer endPage) throws IOException { try (PDDocument doc = Loader.loadPDF(is.readAllBytes())) { return readPdfDocument(doc, startPage, endPage); } } private String readPdfDocument(PDDocument doc, Integer startPage, Integer endPage) throws IOException { int total = doc.getNumberOfPages(); int from = (startPage != null && startPage >= 1) ? startPage : 1; int to = (endPage != null && endPage >= 1) ? Math.min(endPage, total) : total; if (from > total) { return String.format("Error: startPage(%d) 超过文件总页数(%d)", from, total); } PDFTextStripper stripper = new PDFTextStripper(); stripper.setStartPage(from); stripper.setEndPage(to); stripper.setSortByPosition(true); String text = stripper.getText(doc); String header = String.format("[PDF 共 %d 页,本次读取第 %d-%d 页]\n\n", total, from, to); return header + truncate(text); } private String readDocxFromStream(InputStream is) throws IOException { try (XWPFDocument doc = new XWPFDocument(is)) { return readDocxDocument(doc); } } private String readDocxDocument(XWPFDocument doc) { StringBuilder sb = new StringBuilder(); for (XWPFParagraph para : doc.getParagraphs()) { String text = para.getText(); if (text != null && !text.isBlank()) { sb.append(text).append("\n"); } } return "[Word (.docx)]\n\n" + truncate(sb.to String()); } private String readDocFromStream(InputStream is) throws IOException { try (HWPFDocument doc = new HWPFDocument(is); WordExtractor extractor = new WordExtractor(doc)) { return readDocExtractor(extractor); } } private String readDocExtractor(WordExtractor extractor) { String text = String.join("\n", extractor.getParagraphText()); return "[Word (.doc)]\n\n" + truncate(text); } private String readExcelFromStream(InputStream is, boolean isOld) throws IOException { try (Workbook wb = isOld ? new HSSFWorkbook(is) : new XSSFWorkbook(is)) { return readExcelWorkbook(wb, isOld); } } private String readExcelWorkbook(Workbook wb, boolean isOld) { StringBuilder sb = new StringBuilder(); DataFormatter formatter = new DataFormatter(); for (int si = 0; si < wb.getNumberOfSheets(); si++) { Sheet sheet = wb.getSheetAt(si); sb.append("=== Sheet: ").append(sheet.getSheetName()).append(" ===\n"); for (Row row : sheet) { StringBuilder rowSb = new StringBuilder(); for (Cell cell : row) { if (rowSb.length() > 0) rowSb.append("\t"); rowSb.append(formatter.formatCellValue(cell)); } String rowStr = rowSb.to String().trim(); if (!rowStr.isEmpty()) { sb.append(rowStr).append("\n"); } } sb.append("\n"); } String ext = isOld ? ".xls" : ".xlsx"; return "[Excel (" + ext + ") 共 " + wb.getNumberOfSheets() + " 个 Sheet]\n\n" + truncate(sb.to String()); } private String readPptxFromStream(InputStream is) throws IOException { try (XMLSlideShow ppt = new XMLSlideShow(is)) { return readPptxShow(ppt); } } private String readPptxShow(XMLSlideShow ppt) { StringBuilder sb = new StringBuilder(); int slideNum = 1; for (var slide : ppt.getSlides()) { sb.append("--- 第 ").append(slideNum++).append(" 页 ---\n"); for (var shape : slide.getShapes()) { if (shape instanceof TextShape<?, ?> ts) { String text = ts.getText(); if (text != null && !text.isBlank()) { sb.append(text).append("\n"); } } } sb.append("\n"); } return "[PowerPoint (.pptx) 共 " + ppt.getSlides().size() + " 页]\n\n" + truncate(sb.to String()); } private String readTextFromStream(InputStream is, String ext) throws IOException { String content = new String(is.readAllBytes(), StandardCharsets.UTF_8); return "[文本文件 (." + ext.toLowerCase() + ")]\n\n" + truncate(content); } private String getExtension(String filename) { int dot = filename.lastIndexOf('.'); return (dot >= 0 && dot < filename.length() - 1) ? filename.substring(dot + 1) : ""; } }
|