generate_outline.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. """
  2. Phase 2: 投标大纲生成
  3. 基于招标文件关键章节 + 历史投标文件目录,使用 LLM 生成结构化投标目录大纲。
  4. 用法:
  5. python -m phase2_outline.generate_outline
  6. 或:
  7. from phase2_outline.generate_outline import generate_outline
  8. outline = generate_outline(tender_text, bid_toc)
  9. """
  10. import sys
  11. import os
  12. import json
  13. sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  14. from llm.llm_utils import LLM
  15. # ── 提取招标关键章节 ─────────────────────────────────
  16. def collect_tender_key_sections(tender_text: str) -> str:
  17. """提取招标文件中与大纲生成最相关的章节(格式要求、评分标准、需求)"""
  18. parts = []
  19. markers = [
  20. ("第六章 投标文件有关格式", 8000),
  21. ("第五章 评标方法与程序", 6000),
  22. ("附件:项目采购需求", 10000),
  23. ("第四章 招标需求", 6000),
  24. ("第一章 投标邀请", 4000),
  25. ]
  26. for keyword, length in markers:
  27. idx = tender_text.find(keyword)
  28. if idx != -1:
  29. parts.append(f"\n【{keyword.strip()}】\n{tender_text[idx:idx+length]}")
  30. return "\n".join(parts) if parts else tender_text[:20000]
  31. def format_bid_toc(bid_toc: list) -> str:
  32. """将历史投标文件目录列表格式化为可读文本"""
  33. lines = []
  34. for item in bid_toc[:80]: # 取前80行
  35. indent = " " * (item.get("level", 1) - 1)
  36. page_str = f" (p.{item['page']})" if item.get("page") else ""
  37. lines.append(f"{indent}- {item['title']}{page_str}")
  38. return "\n".join(lines)
  39. # ── LLM 调用 ────────────────────────────────────────
  40. def build_prompt(tender_sections: str, bid_toc_text: str) -> tuple:
  41. """构造 LLM 提示词"""
  42. system = (
  43. "你是一个投标文件架构师。你的任务是基于招标文件的要求和格式规定,"
  44. "参考历史投标文件的结构,生成完整的投标文件目录大纲。\n\n"
  45. "输出 JSON 格式:\n"
  46. "{\n"
  47. ' "bid_title": "投标文件标题",\n'
  48. ' "packages": ["包件列表"],\n'
  49. ' "outline": [\n'
  50. " {\n"
  51. ' "title": "章节标题",\n'
  52. ' "level": 1-6,\n'
  53. ' "source": {\n'
  54. ' "tender_section": "招标文件对应章节",\n'
  55. ' "bid_reference": "历史投标文件对应章节"\n'
  56. " },\n"
  57. ' "needs_company_info": false,\n'
  58. ' "children": []\n'
  59. " }\n"
  60. " ]\n"
  61. "}\n\n"
  62. "要求:\n"
  63. "1. 必须覆盖招标文件第六章要求的所有格式表格\n"
  64. "2. 必须覆盖第五章评分标准的所有评分项\n"
  65. "3. 参考历史投标文件的章节划分方式\n"
  66. "4. 每项标明参考来源\n"
  67. '5. 需要投标公司提供材料的节点 needs_company_info=true\n'
  68. "6. 只输出 JSON,不要额外的说明文字"
  69. )
  70. user = (
  71. "请生成投标文件目录大纲。\n\n"
  72. "===== 招标文件关键内容 =====\n"
  73. f"{tender_sections[:25000]}\n\n"
  74. "===== 历史投标文件目录(参考) =====\n"
  75. f"{bid_toc_text[:15000]}\n\n"
  76. "项目背景: 松江区机关事务管理局物业管理服务(SJJCZB2025026)\n"
  77. "标项: 包件一(办公中心, 1739.51万) + 包件二(中山中路38号院, 1427.86万)\n"
  78. "服务期: 2026.1.1-2027.12.31 | 评标: 综合评分法 | 价格分10%\n"
  79. "大纲需包含: 商务响应文件 + 技术响应文件两卷。"
  80. )
  81. return system, user
  82. def generate_outline(tender_text: str, bid_toc: list, output_path: str = None) -> dict:
  83. """
  84. 生成投标目录大纲。
  85. Args:
  86. tender_text: 招标文件全文文本
  87. bid_toc: extract_bid_toc() 返回的目录列表
  88. output_path: 可选,保存路径
  89. Returns:
  90. 大纲 JSON 字典
  91. """
  92. llm = LLM()
  93. tender_sections = collect_tender_key_sections(tender_text)
  94. bid_toc_text = format_bid_toc(bid_toc)
  95. print(f"[Phase2] 招标关键章节: {len(tender_sections)} 字符")
  96. print(f"[Phase2] 历史投标目录: {len(bid_toc)} 行, {len(bid_toc_text)} 字符")
  97. system, user = build_prompt(tender_sections, bid_toc_text)
  98. result_str = llm._call(system, user, max_tokens=16384, temperature=0.2,
  99. response_format={"type": "json_object"})
  100. outline = json.loads(result_str)
  101. outline["_phase"] = "phase2_outline"
  102. outline["_generated_by"] = "generate_outline.py"
  103. if output_path:
  104. os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
  105. with open(output_path, "w", encoding="utf-8") as f:
  106. json.dump(outline, f, ensure_ascii=False, indent=2)
  107. print(f"[Phase2] 大纲已保存: {output_path}")
  108. return outline
  109. def print_outline_preview(outline: dict):
  110. """打印大纲预览"""
  111. def print_nodes(nodes, indent=0):
  112. for n in nodes:
  113. prefix = " " * indent
  114. src = n.get("source", {})
  115. src_str = ""
  116. if isinstance(src, dict) and src.get("tender_section"):
  117. src_str = " <- " + src["tender_section"]
  118. if n.get("needs_company_info"):
  119. src_str += " [需公司信息]"
  120. print(f"{prefix}{'-' if indent > 0 else ''} {n['title']}{src_str}")
  121. if n.get("children"):
  122. print_nodes(n["children"], indent + 1)
  123. for vol in outline.get("outline", []):
  124. print(f"\n{vol['title']} <- {vol.get('source', {}).get('tender_section','')}")
  125. if vol.get("children"):
  126. print_nodes(vol["children"], 1)
  127. # ── CLI ──────────────────────────────────────────────
  128. if __name__ == "__main__":
  129. import sys
  130. sys.stdout.reconfigure(encoding="utf-8")
  131. base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  132. tender_file = os.path.join(base_dir, "松江区机关事务管理局物业管理服务招标文件_extracted.txt")
  133. bid_file = os.path.join(base_dir, "松江区机关事务管理局物业管理服务项目投标文件_extracted.txt")
  134. if not os.path.exists(tender_file):
  135. print("[Phase2] 未找到提取文本,先运行 Phase1 提取")
  136. sys.exit(1)
  137. with open(tender_file, "r", encoding="utf-8") as f:
  138. tender_text = f.read()
  139. from phase1_parsing.extract_bid_toc import extract_toc
  140. with open(bid_file, "r", encoding="utf-8") as f:
  141. bid_text = f.read()
  142. bid_toc = extract_toc(bid_text)
  143. outline = generate_outline(tender_text, bid_toc,
  144. output_path="output/phase2_bid_outline.json")
  145. print_outline_preview(outline)