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