""" pipeline.py - 流程编排模块 串联招标文件读取、招标要求分析、投标内容生成和DOCX输出, 提供统一的 run_pipeline() 入口函数。 """ import logging import os from typing import Optional from .config import get_config from .models import AnalysisResult from .proposal_generator import ProposalGenerator from .proposal_writer import write_proposal_docx logger = logging.getLogger(__name__) def run_pipeline( pdf_path: str, output_path: Optional[str] = None, api_key: Optional[str] = None, model: Optional[str] = None, analysis: Optional[AnalysisResult] = None, ) -> str: """运行完整的招标文件分析到投标文件生成流程。 流程步骤: 1. 读取招标文件 PDF(通过 read_bid_pdf) 2. 分析招标要求(通过 RequirementAnalyzer.analyze) 3. 生成投标内容(通过 ProposalGenerator.generate) 4. 输出格式化 DOCX 文件(通过 write_proposal_docx) Args: pdf_path: 招标文件 PDF 路径 output_path: 输出 DOCX 文件路径 (默认:在 PDF 同目录下生成 {pdf_stem}_投标文件.docx) api_key: OpenAI API 密钥(默认从配置 / 环境变量读取) model: OpenAI 模型名(默认 gpt-4o) analysis: 可选的预分析结果。如果提供,则跳过分析和 PDF 读取步骤。 Returns: str: 输出 DOCX 文件的绝对路径 Raises: FileNotFoundError: pdf_path 不存在 ValueError: 配置无效或 API 密钥未设置 RuntimeError: 流程中某一步骤执行失败 """ # ---- 参数校验 ---- if not pdf_path: raise ValueError("pdf_path 不能为空") pdf_path = os.path.abspath(pdf_path) if not os.path.isfile(pdf_path): raise FileNotFoundError(f"招标文件不存在: {pdf_path}") if not pdf_path.lower().endswith(".pdf"): logger.warning("文件扩展名不是 .pdf,但仍将尝试处理: %s", pdf_path) # 设置默认输出路径 if output_path is None: base_dir = os.path.dirname(pdf_path) pdf_stem = os.path.splitext(os.path.basename(pdf_path))[0] output_path = os.path.join(base_dir, f"{pdf_stem}_投标文件.docx") # 确保输出目录存在 output_dir = os.path.dirname(output_path) if output_dir: os.makedirs(output_dir, exist_ok=True) # ---- 步骤1: 读取招标文件 ---- if analysis is None: logger.info("步骤 1/3: 读取招标文件...") try: from bid_proposal.bid_reader import read_bid_pdf bid_doc = read_bid_pdf(pdf_path) logger.info( "招标文件读取完成:%s(%d 页,%d 个表格)", os.path.basename(pdf_path), bid_doc.total_pages, bid_doc.table_count, ) except Exception as e: raise RuntimeError(f"读取招标文件失败: {e}") from e # ---- 步骤2: 分析招标要求 ---- logger.info("步骤 2/3: 分析招标要求...") try: from bid_proposal.requirement_analyzer import RequirementAnalyzer analyzer = RequirementAnalyzer() analysis = analyzer.analyze(bid_doc) logger.info( "招标要求分析完成:共 %d 项要求", analysis.total_requirements, ) except Exception as e: raise RuntimeError(f"招标要求分析失败: {e}") from e else: logger.info( "跳过分析和 PDF 读取,使用提供的预分析结果(%d 项要求)", analysis.total_requirements, ) # ---- 步骤3: 生成投标内容 ---- logger.info("步骤 3/4: 生成投标内容...") try: generator = ProposalGenerator(api_key=api_key, model=model) proposal_content = generator.generate(analysis) logger.info( "投标内容生成完成:共 %d 个章节", proposal_content.total_sections, ) except Exception as e: raise RuntimeError(f"投标内容生成失败: {e}") from e # ---- 步骤4: 输出 DOCX ---- logger.info("步骤 4/4: 输出 DOCX 文件...") try: result_path = write_proposal_docx(proposal_content, output_path) logger.info("投标文件已生成: %s", result_path) except Exception as e: raise RuntimeError(f"DOCX 文件输出失败: {e}") from e return os.path.abspath(result_path)