pipeline.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. """
  2. pipeline.py - 流程编排模块
  3. 串联招标文件读取、招标要求分析、投标内容生成和DOCX输出,
  4. 提供统一的 run_pipeline() 入口函数。
  5. """
  6. import logging
  7. import os
  8. from typing import Optional
  9. from .config import get_config
  10. from .models import AnalysisResult
  11. from .proposal_generator import ProposalGenerator
  12. from .proposal_writer import write_proposal_docx
  13. logger = logging.getLogger(__name__)
  14. def run_pipeline(
  15. pdf_path: str,
  16. output_path: Optional[str] = None,
  17. api_key: Optional[str] = None,
  18. model: Optional[str] = None,
  19. analysis: Optional[AnalysisResult] = None,
  20. ) -> str:
  21. """运行完整的招标文件分析到投标文件生成流程。
  22. 流程步骤:
  23. 1. 读取招标文件 PDF(通过 read_bid_pdf)
  24. 2. 分析招标要求(通过 RequirementAnalyzer.analyze)
  25. 3. 生成投标内容(通过 ProposalGenerator.generate)
  26. 4. 输出格式化 DOCX 文件(通过 write_proposal_docx)
  27. Args:
  28. pdf_path: 招标文件 PDF 路径
  29. output_path: 输出 DOCX 文件路径
  30. (默认:在 PDF 同目录下生成 {pdf_stem}_投标文件.docx)
  31. api_key: OpenAI API 密钥(默认从配置 / 环境变量读取)
  32. model: OpenAI 模型名(默认 gpt-4o)
  33. analysis: 可选的预分析结果。如果提供,则跳过分析和 PDF 读取步骤。
  34. Returns:
  35. str: 输出 DOCX 文件的绝对路径
  36. Raises:
  37. FileNotFoundError: pdf_path 不存在
  38. ValueError: 配置无效或 API 密钥未设置
  39. RuntimeError: 流程中某一步骤执行失败
  40. """
  41. # ---- 参数校验 ----
  42. if not pdf_path:
  43. raise ValueError("pdf_path 不能为空")
  44. pdf_path = os.path.abspath(pdf_path)
  45. if not os.path.isfile(pdf_path):
  46. raise FileNotFoundError(f"招标文件不存在: {pdf_path}")
  47. if not pdf_path.lower().endswith(".pdf"):
  48. logger.warning("文件扩展名不是 .pdf,但仍将尝试处理: %s", pdf_path)
  49. # 设置默认输出路径
  50. if output_path is None:
  51. base_dir = os.path.dirname(pdf_path)
  52. pdf_stem = os.path.splitext(os.path.basename(pdf_path))[0]
  53. output_path = os.path.join(base_dir, f"{pdf_stem}_投标文件.docx")
  54. # 确保输出目录存在
  55. output_dir = os.path.dirname(output_path)
  56. if output_dir:
  57. os.makedirs(output_dir, exist_ok=True)
  58. # ---- 步骤1: 读取招标文件 ----
  59. if analysis is None:
  60. logger.info("步骤 1/3: 读取招标文件...")
  61. try:
  62. from bid_proposal.bid_reader import read_bid_pdf
  63. bid_doc = read_bid_pdf(pdf_path)
  64. logger.info(
  65. "招标文件读取完成:%s(%d 页,%d 个表格)",
  66. os.path.basename(pdf_path),
  67. bid_doc.total_pages,
  68. bid_doc.table_count,
  69. )
  70. except Exception as e:
  71. raise RuntimeError(f"读取招标文件失败: {e}") from e
  72. # ---- 步骤2: 分析招标要求 ----
  73. logger.info("步骤 2/3: 分析招标要求...")
  74. try:
  75. from bid_proposal.requirement_analyzer import RequirementAnalyzer
  76. analyzer = RequirementAnalyzer()
  77. analysis = analyzer.analyze(bid_doc)
  78. logger.info(
  79. "招标要求分析完成:共 %d 项要求",
  80. analysis.total_requirements,
  81. )
  82. except Exception as e:
  83. raise RuntimeError(f"招标要求分析失败: {e}") from e
  84. else:
  85. logger.info(
  86. "跳过分析和 PDF 读取,使用提供的预分析结果(%d 项要求)",
  87. analysis.total_requirements,
  88. )
  89. # ---- 步骤3: 生成投标内容 ----
  90. logger.info("步骤 3/4: 生成投标内容...")
  91. try:
  92. generator = ProposalGenerator(api_key=api_key, model=model)
  93. proposal_content = generator.generate(analysis)
  94. logger.info(
  95. "投标内容生成完成:共 %d 个章节",
  96. proposal_content.total_sections,
  97. )
  98. except Exception as e:
  99. raise RuntimeError(f"投标内容生成失败: {e}") from e
  100. # ---- 步骤4: 输出 DOCX ----
  101. logger.info("步骤 4/4: 输出 DOCX 文件...")
  102. try:
  103. result_path = write_proposal_docx(proposal_content, output_path)
  104. logger.info("投标文件已生成: %s", result_path)
  105. except Exception as e:
  106. raise RuntimeError(f"DOCX 文件输出失败: {e}") from e
  107. return os.path.abspath(result_path)