| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580 |
- """
- 数据模型模块
- 定义投书生成过程中使用的所有结构化数据类型。
- 使用 Pydantic 进行数据验证和序列化。
- """
- from __future__ import annotations
- from dataclasses import dataclass, field
- from enum import Enum
- from typing import Any, Dict, List, Optional, Tuple
- # ============================================================
- # 文档读取结果
- # ============================================================
- @dataclass
- class ReferenceCase:
- """结构化参考案例,明确指定每个参考项目中各文件的具体路径
- 用于替代原先的目录扫描方式,减少自动检测的负担。
- 每个字段对应一个参考项目中的特定文件。
- Keys:
- tender_file: 招标文件路径 (.pdf/.docx)
- procurement_file: 采购需求文件路径 (.pdf/.docx)
- clarification_file: 澄清公告文件路径 (.pdf/.docx)
- bid_file: 投书文件路径 (.pdf/.docx)
- """
- tender_file: str = ""
- procurement_file: str = ""
- clarification_file: str = ""
- bid_file: str = ""
- @classmethod
- def from_dict(cls, d: dict) -> "ReferenceCase":
- """从字典创建,key 支持中文和英文两种格式"""
- key_map = {
- "招标文件": "tender_file",
- "采购需求": "procurement_file",
- "澄清公告": "clarification_file",
- "投书文件": "bid_file",
- }
- kwargs = {}
- for k, v in d.items():
- field_name = key_map.get(k, k)
- if isinstance(v, str):
- kwargs[field_name] = v
- return cls(**kwargs)
- @dataclass
- class ParsedDocument:
- """解析后的文档"""
- file_path: str # 原始文件路径
- file_name: str # 文件名
- content: str # 文本内容
- doc_type: str = "unknown" # tender / procurement / reference_bid / general_info / clarification
- metadata: Dict[str, Any] = field(default_factory=dict)
- @dataclass
- class ProjectData:
- """一个投标项目的数据合集"""
- project_id: str # 如 "1159"
- project_name: str # 如 "上海理工大学"
- data_dir: str = "" # 数据目录路径(可选,显式传文件时无需指定)
- # 各类型文档
- tender_docs: List[ParsedDocument] = field(default_factory=list) # 招标文件
- procurement_docs: List[ParsedDocument] = field(default_factory=list) # 采购需求
- clarification_docs: List[ParsedDocument] = field(default_factory=list) # 澄清公告
- reference_bids: List[ParsedDocument] = field(default_factory=list) # 参考标书
- general_materials: List[ParsedDocument] = field(default_factory=list) # 通用资料
- # Step 1 提取的表/函/表格项目
- extracted_items: List["ExtractedItem"] = field(default_factory=list)
- # 参考投书中提取的表格(招标文件/模板均无对应表时的兜底来源)
- reference_tables: List["ExtractedTable"] = field(default_factory=list)
- # 分包信息
- packages: List["PackageInfo"] = field(default_factory=list)
- # V2: 扩展元数据(存储格式比对结果等中间产物)
- metadata: Dict[str, Any] = field(default_factory=dict)
- # ============================================================
- # 招标分析结果
- # ============================================================
- @dataclass
- class ScoringCriterion:
- """评分细则条目"""
- id: str # 序号
- category: str # 评分大类(如"技术方案"、"人员配置")
- name: str # 评分项名称
- description: str # 详细说明
- max_score: float # 满分
- weight: Optional[float] = None # 权重(如有)
- section_ref: str = "" # 在招标文件中的章节位置
- @dataclass
- class RejectionItem:
- """废标项(投标无效条款)"""
- id: str # 序号
- description: str # 废标条件描述
- category: str = "" # 类别(资格条件/符合性检查等)
- @dataclass
- class ResponseRequirement:
- """响应文件清单要求"""
- id: str
- category: str # "商务" / "技术"
- name: str # 条目名称
- description: str # 要求描述
- required: bool = True # 是否必须提供
- template_ref: str = "" # 模板参考(如有指定模板)
- @dataclass
- class DocumentTemplate:
- """招标文件中指定的文档模板"""
- name: str # 模板名称(如"投标函"、"法定代表人授权书")
- content: str # 模板内容
- page_ref: str = "" # 在招标文件中的页码
- @dataclass
- class PackageInfo:
- """包件信息"""
- package_id: str # 包件编号(如"包件一")
- name: str # 包件名称
- description: str = "" # 描述
- requirements: str = "" # 特殊要求
- @dataclass
- class TenderAnalysis:
- """招标文件分析结果"""
- project_name: str # 项目名称
- tender_project_id: str = "" # 招标编号/项目编号(从招标文件提取)
- buyer_name: str = "" # 采购人名称
- packages: List[PackageInfo] = field(default_factory=list)
- # 核心分析结果
- scoring_criteria: List[ScoringCriterion] = field(default_factory=list) # 评分细则
- rejection_items: List[RejectionItem] = field(default_factory=list) # 废标项
- response_requirements: List[ResponseRequirement] = field(default_factory=list) # 响应清单
- templates: List[DocumentTemplate] = field(default_factory=list) # 模板
- # 更多招标信息
- agency_name: str = "" # 采购代理机构名称
- # 供需分析
- service_requirements: str = "" # 服务需求概述
- qualification_requirements: str = "" # 资格要求概述
- procurement_requirements: str = "" # 采购需求关键要求(结构化摘要)
- # 可直接填入投标模板普通文本/表格单元格的项目字段。键使用模板可读名称,
- # 如“服务内容”“服务要求”“服务期限”;禁止把 %%表名%% 整表授权放入此映射。
- project_fields: Dict[str, str] = field(default_factory=dict)
- # 招标表格(ExtractedTable = 单元格级,保留合并信息和格式)
- tender_tables: List[ExtractedTable] = field(default_factory=list)
- raw_text_snippets: Dict[str, str] = field(default_factory=dict) # 关键原文片段
- # ============================================================
- # 目录与内容
- # ============================================================
- class ChapterType(Enum):
- """章节类型"""
- BUSINESS = "business" # 商务标
- TECHNICAL = "technical" # 技术标
- APPENDIX = "appendix" # 附件
- @dataclass
- class Chapter:
- """投书章节"""
- id: str # 章节编号(如 "1", "2.1", "3.1.2")
- title: str # 章节标题
- chapter_type: ChapterType = ChapterType.TECHNICAL
- level: int = 1 # 层级(1=章, 2=节, 3=小节)
- description: str = "" # 内容描述/写作要求
- word_count_target: int = 0 # 目标字数
- children: List[Chapter] = field(default_factory=list) # 子章节
- # 关联的评分项/要求/废标项
- related_criteria: List[str] = field(default_factory=list)
- related_requirements: List[str] = field(default_factory=list)
- related_rejections: List[str] = field(default_factory=list)
- # 依赖标记(V2 并发撰写)
- depends_on_all: bool = False # True = 需等所有其他章节完成后再撰写(如索引表)
- # 子节是否来自模板骨架(True = 保留模板结构,LLM 只改内容不改子节)
- from_template: bool = False
- # 顶层 generated_content 是否明确由模板章节原文直接载入。
- # True/False 由 Step 4 写入;None 表示旧缓存未知,Step 6 才使用有界兜底判断。
- preserve_template_layout: Optional[bool] = None
- # 子节是否属于附件区(附:/附件 之后的制度内容,不参与目录、按正文处理)
- is_attachment: bool = False
- # 确定性业务规则锁定的标题树(如评分项结构);LLM 只能填写正文,不能改标题。
- structure_locked: bool = False
- # 评分小项经 LLM 判定由本标题本身完整覆盖。Step6 只向该现有标题正文补充响应,
- # 不再为这些评分项创建同名或近义子标题。
- direct_scoring_criteria: List[str] = field(default_factory=list)
- # Step3 将一个复合评分项拆成多个可写作维度时,保存
- # "SC-xx#n" 绑定。direct_scoring_criteria 仍保留评分 ID,
- # 供 Step4-Step6 现有逻辑兼容使用。
- direct_scoring_bindings: List[str] = field(default_factory=list)
- # 废标项的唯一直接落点。父章的 related_rejections 仅表示上下文相关,
- # Step4-Step6 必须使用本字段定位实际承诺正文,禁止重新猜测。
- direct_rejection_bindings: List[str] = field(default_factory=list)
- # Step3/Step4 之间的显式写作协议。复用模板标题后即使展示名
- # 改为评分小类,仍可通过 template_original_title 追溯原标题。
- template_original_title: str = ""
- # 评分/废标增强前的模板节点 ID。最终大纲重编号后仍保留,用于
- # 将新旧目录节点一一关联。
- template_original_id: str = ""
- content_generation_mode: str = "" # template_project / template_project_scoring / project_scoring
- # Step4 块式生成协议:模板基块先完成项目/表格填充计划,再追加评分或废标补充块。
- # 原生表格、图片、签名等仍由 Step6 在模板 XML 上执行,Step4 不重建这些块。
- template_fill_completed: bool = False
- content_blocks: List[Dict[str, object]] = field(default_factory=list)
- supplement_content: str = ""
- # 仅标记“评分大类无法匹配模板、因此获准新增”的一级章。不能用
- # structure_locked/related_criteria 间接推断,因为模板原有商务章也可能
- # 因绑定评分项而同时具备这两个字段。
- is_scoring_inserted: bool = False
- # 生成后的内容
- generated_content: str = ""
- # Step 4 完整生成后落盘的独立章节 DOCX;Step 6 拼接前必须存在
- artifact_path: str = ""
- # 引用来源(用于追踪)
- source_refs: List[str] = field(default_factory=list)
- # Step 3 建立并持久化的“章节 ↔ 依据来源”映射(Step 4 直接复用,避免重复检索)
- template_section: str = "" # 模板中匹配到的区域名
- template_chapter_id: str = "" # 插入评分章前的模板原章号
- tender_snippets: List[str] = field(default_factory=list) # 招标文件相关原文片段
- reference_snippets: List[str] = field(default_factory=list) # 参考投标书相关原文片段
- @dataclass
- class BidOutline:
- """投书目录结构"""
- project_name: str
- chapters: List[Chapter] = field(default_factory=list)
- total_word_count_target: int = 170000
- part_labels: List[Tuple[str, int]] = field(default_factory=list)
- # Step3 加入评分项/废标项之前的正式模板 Heading 1-4 快照。
- # 用于生成可读对照报告及进入 Step4 前的结构门禁,不参与后续写作。
- template_chapters: List[Chapter] = field(default_factory=list)
- # Step3 批量 LLM 拆分并经程序校验后的评分维度。key 为
- # SC-xx#n,value 保存 criterion_id/title/requirement,供报告、
- # Step4 写作和 Step5-Step6 闭环共用,禁止各阶段重新拆分。
- scoring_dimensions: Dict[str, Dict[str, str]] = field(default_factory=dict)
- # Step3 最终节点与模板原节点的显式关系表。每项保存最终/模板节点 ID、
- # 父节点、关系类型、生成模式以及评分/废标直接绑定,供 Step4-Step6 共用。
- heading_mappings: List[Dict[str, object]] = field(default_factory=list)
- # “与评标有关的投标文件主要内容索引表”的持久化数据源。
- # 每条评分维度和废标项均指向最终标题及模板来源,后续不得重新拆分或猜测。
- evaluation_index_entries: List[Dict[str, object]] = field(default_factory=list)
- # part_labels 示例:[("商务部分", 0), ("技术部分", 2)]
- # 表示 chapters[0]~chapters[1] 属于"商务部分",chapters[2]~end 属于"技术部分"
- def flatten(self) -> List[Chapter]:
- """展平所有章节(含子章节)"""
- result = []
- for ch in self.chapters:
- result.append(ch)
- result.extend(_flatten_children(ch))
- return result
- def business_chapters(self) -> List[Chapter]:
- """获取商务标章节(前2章)"""
- return [ch for ch in self.chapters if ch.chapter_type == ChapterType.BUSINESS]
- def technical_chapters(self) -> List[Chapter]:
- """获取技术标章节"""
- return [ch for ch in self.chapters if ch.chapter_type == ChapterType.TECHNICAL]
- def _flatten_children(chapter: Chapter) -> List[Chapter]:
- """递归展平子章节"""
- result = []
- for child in chapter.children:
- result.append(child)
- result.extend(_flatten_children(child))
- return result
- # ============================================================
- # 审核结果
- # ============================================================
- @dataclass
- class ReviewIssue:
- """审核发现的问题"""
- chapter_id: str
- severity: str # "error" / "warning" / "suggestion"
- issue_type: str # "missing_criterion" / "rejection_involved" / "word_count" / "format" / "style"
- description: str
- suggestion: str = ""
- @dataclass
- class ReviewReport:
- """审核报告"""
- passed: bool = True
- issues: List[ReviewIssue] = field(default_factory=list)
- total_word_count: int = 0
- missing_criteria: List[str] = field(default_factory=list)
- rejection_violations: List[str] = field(default_factory=list)
- template_missing: List[str] = field(default_factory=list)
- # ============================================================
- # 模板解析结果
- # ============================================================
- @dataclass
- class TemplatePlaceholder:
- """模板中的占位符"""
- key: str # 占位符键名(如 "项目名称")
- full_text: str # 完整占位符文本(如 "%%项目名称%%")
- location: str = "paragraph" # "paragraph" | "table_cell"
- paragraph_idx: int = -1 # 所在段落索引
- run_idx: int = -1 # 所在 run 索引(paragraph 模式)
- @dataclass
- class TemplateTable:
- """模板中的可填充表格"""
- table_idx: int # 表格在 docx 中的索引
- caption: str = "" # 表格标题/说明
- headers: List[str] = field(default_factory=list) # 表头
- row_count: int = 0 # 总行数
- fillable: bool = False # 是否需要填充
- fill_strategy: str = "" # "replace_placeholder" | "fill_from_material" | "llm_row_by_row"
- @dataclass
- class TemplateSection:
- """模板中的一个可编辑区域"""
- name: str # 区域名(如"资格条件响应表"、"投标函")
- start_para: int = -1 # 起始段落索引
- end_para: int = -1 # 结束段落索引
- section_type: str = "" # "placeholder" | "table_form" | "static_text" | "prose"
- placeholders: List[TemplatePlaceholder] = field(default_factory=list)
- tables: List[TemplateTable] = field(default_factory=list)
- @dataclass
- class TemplateStructure:
- """模板完整结构"""
- file_path: str = ""
- sections: List[TemplateSection] = field(default_factory=list)
- total_paragraphs: int = 0
- total_tables: int = 0
- # ============================================================
- # V2 新模型:表格提取 & 格式比对
- # ============================================================
- @dataclass
- class TableCell:
- """单个表格单元格(含合并信息)"""
- text: str = "" # 单元格文本内容
- row: int = 0 # 行索引(0-based)
- col: int = 0 # 列索引(0-based)
- rowspan: int = 1 # 跨行数
- colspan: int = 1 # 跨列数
- is_merged_origin: bool = False # 是否是合并单元格的起始格
- is_merged_continuation: bool = False # 是否是被合并覆盖的格(内容为空是正常的)
- font_size: Optional[float] = None # 字号(从 DOCX 提取时有值)
- bold: bool = False # 是否加粗
- alignment: str = "" # "left" / "center" / "right"
- @dataclass
- class ExtractedTable:
- """从 PDF 或 DOCX 中提取的表格(单元格级数据)
- 特性:
- - 保留合并单元格信息(rowspan/colspan)
- - 保留原始单元格格式(字号、加粗、对齐)
- - 标记占位符(从模板 DOCX 提取时)
- - 可双向溯源(来源页/段落索引)
- """
- table_id: str = "" # 唯一标识
- source_file: str = "" # 来源文件路径
- source_type: str = "" # "tender_pdf" | "template_docx" | "reference_docx"
- page_num: int = 0 # PDF 页码(0-based)
- para_idx: int = -1 # DOCX 段落索引(模板/参考文件)
- rows: int = 0
- cols: int = 0
- cells: List[List[TableCell]] = field(default_factory=list) # cells[row][col]
- caption: str = "" # 表格标题/前导文字
- title_hint: str = "" # 表格上方标题(如"5.实质性要求响应表"),用于分类
- header_fields: List[str] = field(default_factory=list) # 表头与标题间的项目信息行("项目名称:"等)
- postamble: str = "" # 表格下方紧邻的说明/备注原文
- col_widths: List[float] = field(default_factory=list) # 每列宽度(相对比例,与原始表格一致)
- has_placeholders: bool = False # �Ƿ���� %%...%% ռλ��
- table_type: str = "" # scoring / qualification_response / bid_form / reference_format / other
- is_fragment: bool = False # 是否是需要整体保留的“文字+多张表”片段
- fragment_xml: List[bytes] = field(default_factory=list) # 片段内正文段/表格的 OOXML 字节
- fragment_docx_path: str = "" # 片段单独导出的 DOCX 路径
- fragment_source_indices: List[int] = field(default_factory=list) # 组成片段的物理表序号
- artifact_path: str = "" # Step1 独立提取 DOCX;Step6 用于原样复制格式
- @dataclass
- class ExtractedItem:
- """Step 1 统一提取结果:一个"表/函/表格"项
- 同时包含 LLM 识别信息(名称、类型、内容)和结构化表格数据(如有)。
- """
- name: str # 项名称(如"资格条件响应表"、"投标函")
- item_type: str # "表" / "函" / "表格"
- source_file: str = "" # 来源文件路径
- pages: List[int] = field(default_factory=list) # 涉及的页码
- preamble: str = "" # 【仅表/表格】表格前说明文字
- postamble: str = "" # 【仅表/表格】表格后说明/备注文字
- content: str = "" # 【仅函】完整函件文本
- headers: List[str] = field(default_factory=list) # 【仅表/表格】列标题
- rows: List[List[str]] = field(default_factory=list) # 【仅表/表格】数据行
- source_table: Optional[ExtractedTable] = None # 关联的结构化表格数据
- artifact_path: str = "" # Step1 写出的独立 DOCX 产物路径
- @dataclass
- class SkeletonParagraph:
- """参考投标文件中的骨架段落(通过 paragraph diff 识别)
- 用于理解模板 DOCX 的标题层级——模板中的标题没有 Word Heading 样式,
- 通过参考文件中 ≥75% 相似的段落来推断该段落的层级角色。
- """
- para_idx: int # 在参考文件中的段落索引
- text: str # 段落文本
- similarity: float # 与模板段落的相似度(0.0-1.0)
- inferred_level: int = 0 # 推断的标题层级(0=正文, 1=章, 2=节, 3=小节)
- font_size: Optional[float] = None # 字号
- bold: bool = False # 是否加粗
- is_heading: bool = False # 是否是标题段落
- class ReconciliationDecision(Enum):
- """格式比对决策"""
- REPLACE_WITH_TENDER = "replace_with_tender"
- """用招标文件中的格式/表格完整替换模板中的对应表格"""
- KEEP_TEMPLATE = "keep_template"
- """保留模板表格,仅填充占位符"""
- KEEP_TEMPLATE_WITH_TENDER_DATA = "keep_template_with_tender_data"
- """保留模板表格结构,但用招标文件的数据填充内容"""
- APPEND_AS_NEW = "append_as_new"
- """招标文件中有、模板中没有的表格/函件,追加到文档"""
- @dataclass
- class FormatReconciliation:
- """格式比对结果:一个模板表格/函件与招标要求的比对结论"""
- reconciliation_id: str = "" # FR-01, FR-02, ...
- section_name: str = "" # 模板区域名称(如"资格条件响应表")
- section_type: str = "" # "table" | "form_letter" | "static_text"
- # 模板侧
- template_table_idx: int = -1 # 模板 DOCX 中的表格索引
- template_table: Optional[ExtractedTable] = None # 模板表格数据
- # 招标文件侧
- tender_tables: List[ExtractedTable] = field(default_factory=list) # 匹配的招标表格
- best_match: Optional[ExtractedTable] = None # 最佳匹配的招标表格
- similarity_score: float = 0.0 # Jaccard 或其他相似度分数
- # 决策
- decision: ReconciliationDecision = ReconciliationDecision.KEEP_TEMPLATE
- decision_reason: str = "" # 决策理由(含 LLM 判断结果)
- # 执行信息
- replace_with_tender_table: Optional[ExtractedTable] = None # 替换时使用的招标表格
- fill_data: Dict[str, str] = field(default_factory=dict) # 填充时使用的数据映射
- llm_judgment: str = "" # LLM 对格式差异的判断原文
- # 标记
- is_confirmed: bool = False # 是否已经 LLM 确认
- needs_manual_review: bool = False # 是否需要人工审核
- @dataclass
- class ReconciliationReport:
- """格式比对汇总报告"""
- total_template_tables: int = 0
- total_tender_tables: int = 0
- reconciliations: List[FormatReconciliation] = field(default_factory=list)
- replace_count: int = 0 # 需要替换的表格数
- keep_count: int = 0 # 保留模板的表格数
- append_count: int = 0 # 需要追加的表格数
- unpaired_tender_tables: List[ExtractedTable] = field(default_factory=list) # 招标文件中未配对的表格
- summary: str = "" # LLM 生成的比对总结
- # ============================================================
- # 公司信息
- # ============================================================
- @dataclass
- class CompanyInfo:
- """结构化公司基本信息(从通用资料中提取)"""
- full_name: str = "" # 公司全称
- short_name: str = "" # 公司简称
- address: str = ""
- phone: str = ""
- fax: str = ""
- zip_code: str = ""
- bank_name: str = ""
- bank_account: str = ""
- legal_rep: str = "" # 法定代表人姓名
- legal_rep_id: str = "" # 法定代表人身份证号
- registration_capital: str = ""
- unified_code: str = "" # 统一社会信用代码
- website: str = ""
- # ============================================================
- # 工作流状态
- # ============================================================
- @dataclass
- class AgentState:
- """LangGraph 工作流的状态对象"""
- # 输入(二选一:project_dir 传统模式,或五类显式参数)
- project_dir: str = "" # 项目数据目录路径
- project_id: str = "" # 项目编号
- project_name: str = "" # 项目名称
- package_ids: List[str] = field(default_factory=list) # 指定的包件(空=全部)
- # 五类显式参数(优先级高于 project_dir)
- tender_files: List[str] = field(default_factory=list)
- procurement_files: List[str] = field(default_factory=list)
- clarification_files: List[str] = field(default_factory=list)
- reference_bid_dirs: List[str] = field(default_factory=list) # 向后兼容,旧版目录模式
- reference_bids: List[ReferenceCase] = field(default_factory=list) # 新版结构化参考案例
- company_info_dirs: List[str] = field(default_factory=list)
- # 中间结果
- project_data: Optional[ProjectData] = None
- tender_analysis: Optional[TenderAnalysis] = None
- outline: Optional[BidOutline] = None
- review_report: Optional[ReviewReport] = None
- # 输出
- output_path: str = "" # 生成的 DOCX 文件路径
- error: str = "" # 错误信息
- # V2 新字段
- template_path: str = "" # 投标模板 DOCX 路径
- extracted_tables: Dict[str, Any] = field(default_factory=dict) # 见 step1_parsing/table_extractor
- reconciliation_report: Optional[Any] = None # FormatReconciliation 报告(延迟导入)
- heading_levels: Dict[int, int] = field(default_factory=dict) # para_idx → heading_level
- # 工作流控制
- current_step: str = "" # 当前执行步骤
- max_review_iterations: int = 2 # 最大审核迭代次数
- review_iteration: int = 0 # 当前审核轮次
|