| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180 |
- """
- Phase 2: 投标大纲生成
- 基于招标文件关键章节 + 历史投标文件目录,使用 LLM 生成结构化投标目录大纲。
- 用法:
- python -m phase2_outline.generate_outline
- 或:
- from phase2_outline.generate_outline import generate_outline
- outline = generate_outline(tender_text, bid_toc)
- """
- import sys
- import os
- import json
- sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
- from llm.llm_utils import LLM
- # ── 提取招标关键章节 ─────────────────────────────────
- def collect_tender_key_sections(tender_text: str) -> str:
- """提取招标文件中与大纲生成最相关的章节(格式要求、评分标准、需求)"""
- parts = []
- markers = [
- ("第六章 投标文件有关格式", 8000),
- ("第五章 评标方法与程序", 6000),
- ("附件:项目采购需求", 10000),
- ("第四章 招标需求", 6000),
- ("第一章 投标邀请", 4000),
- ]
- for keyword, length in markers:
- idx = tender_text.find(keyword)
- if idx != -1:
- parts.append(f"\n【{keyword.strip()}】\n{tender_text[idx:idx+length]}")
- return "\n".join(parts) if parts else tender_text[:20000]
- def format_bid_toc(bid_toc: list) -> str:
- """将历史投标文件目录列表格式化为可读文本"""
- lines = []
- for item in bid_toc[:80]: # 取前80行
- indent = " " * (item.get("level", 1) - 1)
- page_str = f" (p.{item['page']})" if item.get("page") else ""
- lines.append(f"{indent}- {item['title']}{page_str}")
- return "\n".join(lines)
- # ── LLM 调用 ────────────────────────────────────────
- def build_prompt(tender_sections: str, bid_toc_text: str) -> tuple:
- """构造 LLM 提示词"""
- system = (
- "你是一个投标文件架构师。你的任务是基于招标文件的要求和格式规定,"
- "参考历史投标文件的结构,生成完整的投标文件目录大纲。\n\n"
- "输出 JSON 格式:\n"
- "{\n"
- ' "bid_title": "投标文件标题",\n'
- ' "packages": ["包件列表"],\n'
- ' "outline": [\n'
- " {\n"
- ' "title": "章节标题",\n'
- ' "level": 1-6,\n'
- ' "source": {\n'
- ' "tender_section": "招标文件对应章节",\n'
- ' "bid_reference": "历史投标文件对应章节"\n'
- " },\n"
- ' "needs_company_info": false,\n'
- ' "children": []\n'
- " }\n"
- " ]\n"
- "}\n\n"
- "要求:\n"
- "1. 必须覆盖招标文件第六章要求的所有格式表格\n"
- "2. 必须覆盖第五章评分标准的所有评分项\n"
- "3. 参考历史投标文件的章节划分方式\n"
- "4. 每项标明参考来源\n"
- '5. 需要投标公司提供材料的节点 needs_company_info=true\n'
- "6. 只输出 JSON,不要额外的说明文字"
- )
- user = (
- "请生成投标文件目录大纲。\n\n"
- "===== 招标文件关键内容 =====\n"
- f"{tender_sections[:25000]}\n\n"
- "===== 历史投标文件目录(参考) =====\n"
- f"{bid_toc_text[:15000]}\n\n"
- "项目背景: 松江区机关事务管理局物业管理服务(SJJCZB2025026)\n"
- "标项: 包件一(办公中心, 1739.51万) + 包件二(中山中路38号院, 1427.86万)\n"
- "服务期: 2026.1.1-2027.12.31 | 评标: 综合评分法 | 价格分10%\n"
- "大纲需包含: 商务响应文件 + 技术响应文件两卷。"
- )
- return system, user
- def generate_outline(tender_text: str, bid_toc: list, output_path: str = None) -> dict:
- """
- 生成投标目录大纲。
- Args:
- tender_text: 招标文件全文文本
- bid_toc: extract_bid_toc() 返回的目录列表
- output_path: 可选,保存路径
- Returns:
- 大纲 JSON 字典
- """
- llm = LLM()
- tender_sections = collect_tender_key_sections(tender_text)
- bid_toc_text = format_bid_toc(bid_toc)
- print(f"[Phase2] 招标关键章节: {len(tender_sections)} 字符")
- print(f"[Phase2] 历史投标目录: {len(bid_toc)} 行, {len(bid_toc_text)} 字符")
- system, user = build_prompt(tender_sections, bid_toc_text)
- result_str = llm._call(system, user, max_tokens=16384, temperature=0.2,
- response_format={"type": "json_object"})
- outline = json.loads(result_str)
- outline["_phase"] = "phase2_outline"
- outline["_generated_by"] = "generate_outline.py"
- if output_path:
- os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
- with open(output_path, "w", encoding="utf-8") as f:
- json.dump(outline, f, ensure_ascii=False, indent=2)
- print(f"[Phase2] 大纲已保存: {output_path}")
- return outline
- def print_outline_preview(outline: dict):
- """打印大纲预览"""
- def print_nodes(nodes, indent=0):
- for n in nodes:
- prefix = " " * indent
- src = n.get("source", {})
- src_str = ""
- if isinstance(src, dict) and src.get("tender_section"):
- src_str = " <- " + src["tender_section"]
- if n.get("needs_company_info"):
- src_str += " [需公司信息]"
- print(f"{prefix}{'-' if indent > 0 else ''} {n['title']}{src_str}")
- if n.get("children"):
- print_nodes(n["children"], indent + 1)
- for vol in outline.get("outline", []):
- print(f"\n{vol['title']} <- {vol.get('source', {}).get('tender_section','')}")
- if vol.get("children"):
- print_nodes(vol["children"], 1)
- # ── CLI ──────────────────────────────────────────────
- if __name__ == "__main__":
- import sys
- sys.stdout.reconfigure(encoding="utf-8")
- base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
- tender_file = os.path.join(base_dir, "松江区机关事务管理局物业管理服务招标文件_extracted.txt")
- bid_file = os.path.join(base_dir, "松江区机关事务管理局物业管理服务项目投标文件_extracted.txt")
- if not os.path.exists(tender_file):
- print("[Phase2] 未找到提取文本,先运行 Phase1 提取")
- sys.exit(1)
- with open(tender_file, "r", encoding="utf-8") as f:
- tender_text = f.read()
- from phase1_parsing.extract_bid_toc import extract_toc
- with open(bid_file, "r", encoding="utf-8") as f:
- bid_text = f.read()
- bid_toc = extract_toc(bid_text)
- outline = generate_outline(tender_text, bid_toc,
- output_path="output/phase2_bid_outline.json")
- print_outline_preview(outline)
|