| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126 |
- """
- 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)
|