models.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. """
  2. 数据模型模块
  3. 定义投书生成过程中使用的所有结构化数据类型。
  4. 使用 Pydantic 进行数据验证和序列化。
  5. """
  6. from __future__ import annotations
  7. from dataclasses import dataclass, field
  8. from enum import Enum
  9. from typing import Any, Dict, List, Optional, Tuple
  10. # ============================================================
  11. # 文档读取结果
  12. # ============================================================
  13. @dataclass
  14. class ReferenceCase:
  15. """结构化参考案例,明确指定每个参考项目中各文件的具体路径
  16. 用于替代原先的目录扫描方式,减少自动检测的负担。
  17. 每个字段对应一个参考项目中的特定文件。
  18. Keys:
  19. tender_file: 招标文件路径 (.pdf/.docx)
  20. procurement_file: 采购需求文件路径 (.pdf/.docx)
  21. clarification_file: 澄清公告文件路径 (.pdf/.docx)
  22. bid_file: 投书文件路径 (.pdf/.docx)
  23. """
  24. tender_file: str = ""
  25. procurement_file: str = ""
  26. clarification_file: str = ""
  27. bid_file: str = ""
  28. @classmethod
  29. def from_dict(cls, d: dict) -> "ReferenceCase":
  30. """从字典创建,key 支持中文和英文两种格式"""
  31. key_map = {
  32. "招标文件": "tender_file",
  33. "采购需求": "procurement_file",
  34. "澄清公告": "clarification_file",
  35. "投书文件": "bid_file",
  36. }
  37. kwargs = {}
  38. for k, v in d.items():
  39. field_name = key_map.get(k, k)
  40. if isinstance(v, str):
  41. kwargs[field_name] = v
  42. return cls(**kwargs)
  43. @dataclass
  44. class ParsedDocument:
  45. """解析后的文档"""
  46. file_path: str # 原始文件路径
  47. file_name: str # 文件名
  48. content: str # 文本内容
  49. doc_type: str = "unknown" # tender / procurement / reference_bid / general_info / clarification
  50. metadata: Dict[str, Any] = field(default_factory=dict)
  51. @dataclass
  52. class ProjectData:
  53. """一个投标项目的数据合集"""
  54. project_id: str # 如 "1159"
  55. project_name: str # 如 "上海理工大学"
  56. data_dir: str = "" # 数据目录路径(可选,显式传文件时无需指定)
  57. # 各类型文档
  58. tender_docs: List[ParsedDocument] = field(default_factory=list) # 招标文件
  59. procurement_docs: List[ParsedDocument] = field(default_factory=list) # 采购需求
  60. clarification_docs: List[ParsedDocument] = field(default_factory=list) # 澄清公告
  61. reference_bids: List[ParsedDocument] = field(default_factory=list) # 参考标书
  62. general_materials: List[ParsedDocument] = field(default_factory=list) # 通用资料
  63. # Step 1 提取的表/函/表格项目
  64. extracted_items: List["ExtractedItem"] = field(default_factory=list)
  65. # 参考投书中提取的表格(招标文件/模板均无对应表时的兜底来源)
  66. reference_tables: List["ExtractedTable"] = field(default_factory=list)
  67. # 分包信息
  68. packages: List["PackageInfo"] = field(default_factory=list)
  69. # V2: 扩展元数据(存储格式比对结果等中间产物)
  70. metadata: Dict[str, Any] = field(default_factory=dict)
  71. # ============================================================
  72. # 招标分析结果
  73. # ============================================================
  74. @dataclass
  75. class ScoringCriterion:
  76. """评分细则条目"""
  77. id: str # 序号
  78. category: str # 评分大类(如"技术方案"、"人员配置")
  79. name: str # 评分项名称
  80. description: str # 详细说明
  81. max_score: float # 满分
  82. weight: Optional[float] = None # 权重(如有)
  83. section_ref: str = "" # 在招标文件中的章节位置
  84. @dataclass
  85. class RejectionItem:
  86. """废标项(投标无效条款)"""
  87. id: str # 序号
  88. description: str # 废标条件描述
  89. category: str = "" # 类别(资格条件/符合性检查等)
  90. @dataclass
  91. class ResponseRequirement:
  92. """响应文件清单要求"""
  93. id: str
  94. category: str # "商务" / "技术"
  95. name: str # 条目名称
  96. description: str # 要求描述
  97. required: bool = True # 是否必须提供
  98. template_ref: str = "" # 模板参考(如有指定模板)
  99. @dataclass
  100. class DocumentTemplate:
  101. """招标文件中指定的文档模板"""
  102. name: str # 模板名称(如"投标函"、"法定代表人授权书")
  103. content: str # 模板内容
  104. page_ref: str = "" # 在招标文件中的页码
  105. @dataclass
  106. class PackageInfo:
  107. """包件信息"""
  108. package_id: str # 包件编号(如"包件一")
  109. name: str # 包件名称
  110. description: str = "" # 描述
  111. requirements: str = "" # 特殊要求
  112. @dataclass
  113. class TenderAnalysis:
  114. """招标文件分析结果"""
  115. project_name: str # 项目名称
  116. tender_project_id: str = "" # 招标编号/项目编号(从招标文件提取)
  117. buyer_name: str = "" # 采购人名称
  118. packages: List[PackageInfo] = field(default_factory=list)
  119. # 核心分析结果
  120. scoring_criteria: List[ScoringCriterion] = field(default_factory=list) # 评分细则
  121. rejection_items: List[RejectionItem] = field(default_factory=list) # 废标项
  122. response_requirements: List[ResponseRequirement] = field(default_factory=list) # 响应清单
  123. templates: List[DocumentTemplate] = field(default_factory=list) # 模板
  124. # 更多招标信息
  125. agency_name: str = "" # 采购代理机构名称
  126. # 供需分析
  127. service_requirements: str = "" # 服务需求概述
  128. qualification_requirements: str = "" # 资格要求概述
  129. procurement_requirements: str = "" # 采购需求关键要求(结构化摘要)
  130. # 可直接填入投标模板普通文本/表格单元格的项目字段。键使用模板可读名称,
  131. # 如“服务内容”“服务要求”“服务期限”;禁止把 %%表名%% 整表授权放入此映射。
  132. project_fields: Dict[str, str] = field(default_factory=dict)
  133. # 招标表格(ExtractedTable = 单元格级,保留合并信息和格式)
  134. tender_tables: List[ExtractedTable] = field(default_factory=list)
  135. raw_text_snippets: Dict[str, str] = field(default_factory=dict) # 关键原文片段
  136. # ============================================================
  137. # 目录与内容
  138. # ============================================================
  139. class ChapterType(Enum):
  140. """章节类型"""
  141. BUSINESS = "business" # 商务标
  142. TECHNICAL = "technical" # 技术标
  143. APPENDIX = "appendix" # 附件
  144. @dataclass
  145. class Chapter:
  146. """投书章节"""
  147. id: str # 章节编号(如 "1", "2.1", "3.1.2")
  148. title: str # 章节标题
  149. chapter_type: ChapterType = ChapterType.TECHNICAL
  150. level: int = 1 # 层级(1=章, 2=节, 3=小节)
  151. description: str = "" # 内容描述/写作要求
  152. word_count_target: int = 0 # 目标字数
  153. children: List[Chapter] = field(default_factory=list) # 子章节
  154. # 关联的评分项/要求/废标项
  155. related_criteria: List[str] = field(default_factory=list)
  156. related_requirements: List[str] = field(default_factory=list)
  157. related_rejections: List[str] = field(default_factory=list)
  158. # 依赖标记(V2 并发撰写)
  159. depends_on_all: bool = False # True = 需等所有其他章节完成后再撰写(如索引表)
  160. # 子节是否来自模板骨架(True = 保留模板结构,LLM 只改内容不改子节)
  161. from_template: bool = False
  162. # 顶层 generated_content 是否明确由模板章节原文直接载入。
  163. # True/False 由 Step 4 写入;None 表示旧缓存未知,Step 6 才使用有界兜底判断。
  164. preserve_template_layout: Optional[bool] = None
  165. # 子节是否属于附件区(附:/附件 之后的制度内容,不参与目录、按正文处理)
  166. is_attachment: bool = False
  167. # 确定性业务规则锁定的标题树(如评分项结构);LLM 只能填写正文,不能改标题。
  168. structure_locked: bool = False
  169. # 评分小项经 LLM 判定由本标题本身完整覆盖。Step6 只向该现有标题正文补充响应,
  170. # 不再为这些评分项创建同名或近义子标题。
  171. direct_scoring_criteria: List[str] = field(default_factory=list)
  172. # Step3 将一个复合评分项拆成多个可写作维度时,保存
  173. # "SC-xx#n" 绑定。direct_scoring_criteria 仍保留评分 ID,
  174. # 供 Step4-Step6 现有逻辑兼容使用。
  175. direct_scoring_bindings: List[str] = field(default_factory=list)
  176. # 废标项的唯一直接落点。父章的 related_rejections 仅表示上下文相关,
  177. # Step4-Step6 必须使用本字段定位实际承诺正文,禁止重新猜测。
  178. direct_rejection_bindings: List[str] = field(default_factory=list)
  179. # Step3/Step4 之间的显式写作协议。复用模板标题后即使展示名
  180. # 改为评分小类,仍可通过 template_original_title 追溯原标题。
  181. template_original_title: str = ""
  182. # 评分/废标增强前的模板节点 ID。最终大纲重编号后仍保留,用于
  183. # 将新旧目录节点一一关联。
  184. template_original_id: str = ""
  185. content_generation_mode: str = "" # template_project / template_project_scoring / project_scoring
  186. # Step4 块式生成协议:模板基块先完成项目/表格填充计划,再追加评分或废标补充块。
  187. # 原生表格、图片、签名等仍由 Step6 在模板 XML 上执行,Step4 不重建这些块。
  188. template_fill_completed: bool = False
  189. content_blocks: List[Dict[str, object]] = field(default_factory=list)
  190. supplement_content: str = ""
  191. # 仅标记“评分大类无法匹配模板、因此获准新增”的一级章。不能用
  192. # structure_locked/related_criteria 间接推断,因为模板原有商务章也可能
  193. # 因绑定评分项而同时具备这两个字段。
  194. is_scoring_inserted: bool = False
  195. # 生成后的内容
  196. generated_content: str = ""
  197. # Step 4 完整生成后落盘的独立章节 DOCX;Step 6 拼接前必须存在
  198. artifact_path: str = ""
  199. # 引用来源(用于追踪)
  200. source_refs: List[str] = field(default_factory=list)
  201. # Step 3 建立并持久化的“章节 ↔ 依据来源”映射(Step 4 直接复用,避免重复检索)
  202. template_section: str = "" # 模板中匹配到的区域名
  203. template_chapter_id: str = "" # 插入评分章前的模板原章号
  204. tender_snippets: List[str] = field(default_factory=list) # 招标文件相关原文片段
  205. reference_snippets: List[str] = field(default_factory=list) # 参考投标书相关原文片段
  206. @dataclass
  207. class BidOutline:
  208. """投书目录结构"""
  209. project_name: str
  210. chapters: List[Chapter] = field(default_factory=list)
  211. total_word_count_target: int = 170000
  212. part_labels: List[Tuple[str, int]] = field(default_factory=list)
  213. # Step3 加入评分项/废标项之前的正式模板 Heading 1-4 快照。
  214. # 用于生成可读对照报告及进入 Step4 前的结构门禁,不参与后续写作。
  215. template_chapters: List[Chapter] = field(default_factory=list)
  216. # Step3 批量 LLM 拆分并经程序校验后的评分维度。key 为
  217. # SC-xx#n,value 保存 criterion_id/title/requirement,供报告、
  218. # Step4 写作和 Step5-Step6 闭环共用,禁止各阶段重新拆分。
  219. scoring_dimensions: Dict[str, Dict[str, str]] = field(default_factory=dict)
  220. # Step3 最终节点与模板原节点的显式关系表。每项保存最终/模板节点 ID、
  221. # 父节点、关系类型、生成模式以及评分/废标直接绑定,供 Step4-Step6 共用。
  222. heading_mappings: List[Dict[str, object]] = field(default_factory=list)
  223. # “与评标有关的投标文件主要内容索引表”的持久化数据源。
  224. # 每条评分维度和废标项均指向最终标题及模板来源,后续不得重新拆分或猜测。
  225. evaluation_index_entries: List[Dict[str, object]] = field(default_factory=list)
  226. # part_labels 示例:[("商务部分", 0), ("技术部分", 2)]
  227. # 表示 chapters[0]~chapters[1] 属于"商务部分",chapters[2]~end 属于"技术部分"
  228. def flatten(self) -> List[Chapter]:
  229. """展平所有章节(含子章节)"""
  230. result = []
  231. for ch in self.chapters:
  232. result.append(ch)
  233. result.extend(_flatten_children(ch))
  234. return result
  235. def business_chapters(self) -> List[Chapter]:
  236. """获取商务标章节(前2章)"""
  237. return [ch for ch in self.chapters if ch.chapter_type == ChapterType.BUSINESS]
  238. def technical_chapters(self) -> List[Chapter]:
  239. """获取技术标章节"""
  240. return [ch for ch in self.chapters if ch.chapter_type == ChapterType.TECHNICAL]
  241. def _flatten_children(chapter: Chapter) -> List[Chapter]:
  242. """递归展平子章节"""
  243. result = []
  244. for child in chapter.children:
  245. result.append(child)
  246. result.extend(_flatten_children(child))
  247. return result
  248. # ============================================================
  249. # 审核结果
  250. # ============================================================
  251. @dataclass
  252. class ReviewIssue:
  253. """审核发现的问题"""
  254. chapter_id: str
  255. severity: str # "error" / "warning" / "suggestion"
  256. issue_type: str # "missing_criterion" / "rejection_involved" / "word_count" / "format" / "style"
  257. description: str
  258. suggestion: str = ""
  259. @dataclass
  260. class ReviewReport:
  261. """审核报告"""
  262. passed: bool = True
  263. issues: List[ReviewIssue] = field(default_factory=list)
  264. total_word_count: int = 0
  265. missing_criteria: List[str] = field(default_factory=list)
  266. rejection_violations: List[str] = field(default_factory=list)
  267. template_missing: List[str] = field(default_factory=list)
  268. # ============================================================
  269. # 模板解析结果
  270. # ============================================================
  271. @dataclass
  272. class TemplatePlaceholder:
  273. """模板中的占位符"""
  274. key: str # 占位符键名(如 "项目名称")
  275. full_text: str # 完整占位符文本(如 "%%项目名称%%")
  276. location: str = "paragraph" # "paragraph" | "table_cell"
  277. paragraph_idx: int = -1 # 所在段落索引
  278. run_idx: int = -1 # 所在 run 索引(paragraph 模式)
  279. @dataclass
  280. class TemplateTable:
  281. """模板中的可填充表格"""
  282. table_idx: int # 表格在 docx 中的索引
  283. caption: str = "" # 表格标题/说明
  284. headers: List[str] = field(default_factory=list) # 表头
  285. row_count: int = 0 # 总行数
  286. fillable: bool = False # 是否需要填充
  287. fill_strategy: str = "" # "replace_placeholder" | "fill_from_material" | "llm_row_by_row"
  288. @dataclass
  289. class TemplateSection:
  290. """模板中的一个可编辑区域"""
  291. name: str # 区域名(如"资格条件响应表"、"投标函")
  292. start_para: int = -1 # 起始段落索引
  293. end_para: int = -1 # 结束段落索引
  294. section_type: str = "" # "placeholder" | "table_form" | "static_text" | "prose"
  295. placeholders: List[TemplatePlaceholder] = field(default_factory=list)
  296. tables: List[TemplateTable] = field(default_factory=list)
  297. @dataclass
  298. class TemplateStructure:
  299. """模板完整结构"""
  300. file_path: str = ""
  301. sections: List[TemplateSection] = field(default_factory=list)
  302. total_paragraphs: int = 0
  303. total_tables: int = 0
  304. # ============================================================
  305. # V2 新模型:表格提取 & 格式比对
  306. # ============================================================
  307. @dataclass
  308. class TableCell:
  309. """单个表格单元格(含合并信息)"""
  310. text: str = "" # 单元格文本内容
  311. row: int = 0 # 行索引(0-based)
  312. col: int = 0 # 列索引(0-based)
  313. rowspan: int = 1 # 跨行数
  314. colspan: int = 1 # 跨列数
  315. is_merged_origin: bool = False # 是否是合并单元格的起始格
  316. is_merged_continuation: bool = False # 是否是被合并覆盖的格(内容为空是正常的)
  317. font_size: Optional[float] = None # 字号(从 DOCX 提取时有值)
  318. bold: bool = False # 是否加粗
  319. alignment: str = "" # "left" / "center" / "right"
  320. @dataclass
  321. class ExtractedTable:
  322. """从 PDF 或 DOCX 中提取的表格(单元格级数据)
  323. 特性:
  324. - 保留合并单元格信息(rowspan/colspan)
  325. - 保留原始单元格格式(字号、加粗、对齐)
  326. - 标记占位符(从模板 DOCX 提取时)
  327. - 可双向溯源(来源页/段落索引)
  328. """
  329. table_id: str = "" # 唯一标识
  330. source_file: str = "" # 来源文件路径
  331. source_type: str = "" # "tender_pdf" | "template_docx" | "reference_docx"
  332. page_num: int = 0 # PDF 页码(0-based)
  333. para_idx: int = -1 # DOCX 段落索引(模板/参考文件)
  334. rows: int = 0
  335. cols: int = 0
  336. cells: List[List[TableCell]] = field(default_factory=list) # cells[row][col]
  337. caption: str = "" # 表格标题/前导文字
  338. title_hint: str = "" # 表格上方标题(如"5.实质性要求响应表"),用于分类
  339. header_fields: List[str] = field(default_factory=list) # 表头与标题间的项目信息行("项目名称:"等)
  340. postamble: str = "" # 表格下方紧邻的说明/备注原文
  341. col_widths: List[float] = field(default_factory=list) # 每列宽度(相对比例,与原始表格一致)
  342. has_placeholders: bool = False # �Ƿ���� %%...%% ռλ��
  343. table_type: str = "" # scoring / qualification_response / bid_form / reference_format / other
  344. is_fragment: bool = False # 是否是需要整体保留的“文字+多张表”片段
  345. fragment_xml: List[bytes] = field(default_factory=list) # 片段内正文段/表格的 OOXML 字节
  346. fragment_docx_path: str = "" # 片段单独导出的 DOCX 路径
  347. fragment_source_indices: List[int] = field(default_factory=list) # 组成片段的物理表序号
  348. artifact_path: str = "" # Step1 独立提取 DOCX;Step6 用于原样复制格式
  349. @dataclass
  350. class ExtractedItem:
  351. """Step 1 统一提取结果:一个"表/函/表格"项
  352. 同时包含 LLM 识别信息(名称、类型、内容)和结构化表格数据(如有)。
  353. """
  354. name: str # 项名称(如"资格条件响应表"、"投标函")
  355. item_type: str # "表" / "函" / "表格"
  356. source_file: str = "" # 来源文件路径
  357. pages: List[int] = field(default_factory=list) # 涉及的页码
  358. preamble: str = "" # 【仅表/表格】表格前说明文字
  359. postamble: str = "" # 【仅表/表格】表格后说明/备注文字
  360. content: str = "" # 【仅函】完整函件文本
  361. headers: List[str] = field(default_factory=list) # 【仅表/表格】列标题
  362. rows: List[List[str]] = field(default_factory=list) # 【仅表/表格】数据行
  363. source_table: Optional[ExtractedTable] = None # 关联的结构化表格数据
  364. artifact_path: str = "" # Step1 写出的独立 DOCX 产物路径
  365. @dataclass
  366. class SkeletonParagraph:
  367. """参考投标文件中的骨架段落(通过 paragraph diff 识别)
  368. 用于理解模板 DOCX 的标题层级——模板中的标题没有 Word Heading 样式,
  369. 通过参考文件中 ≥75% 相似的段落来推断该段落的层级角色。
  370. """
  371. para_idx: int # 在参考文件中的段落索引
  372. text: str # 段落文本
  373. similarity: float # 与模板段落的相似度(0.0-1.0)
  374. inferred_level: int = 0 # 推断的标题层级(0=正文, 1=章, 2=节, 3=小节)
  375. font_size: Optional[float] = None # 字号
  376. bold: bool = False # 是否加粗
  377. is_heading: bool = False # 是否是标题段落
  378. class ReconciliationDecision(Enum):
  379. """格式比对决策"""
  380. REPLACE_WITH_TENDER = "replace_with_tender"
  381. """用招标文件中的格式/表格完整替换模板中的对应表格"""
  382. KEEP_TEMPLATE = "keep_template"
  383. """保留模板表格,仅填充占位符"""
  384. KEEP_TEMPLATE_WITH_TENDER_DATA = "keep_template_with_tender_data"
  385. """保留模板表格结构,但用招标文件的数据填充内容"""
  386. APPEND_AS_NEW = "append_as_new"
  387. """招标文件中有、模板中没有的表格/函件,追加到文档"""
  388. @dataclass
  389. class FormatReconciliation:
  390. """格式比对结果:一个模板表格/函件与招标要求的比对结论"""
  391. reconciliation_id: str = "" # FR-01, FR-02, ...
  392. section_name: str = "" # 模板区域名称(如"资格条件响应表")
  393. section_type: str = "" # "table" | "form_letter" | "static_text"
  394. # 模板侧
  395. template_table_idx: int = -1 # 模板 DOCX 中的表格索引
  396. template_table: Optional[ExtractedTable] = None # 模板表格数据
  397. # 招标文件侧
  398. tender_tables: List[ExtractedTable] = field(default_factory=list) # 匹配的招标表格
  399. best_match: Optional[ExtractedTable] = None # 最佳匹配的招标表格
  400. similarity_score: float = 0.0 # Jaccard 或其他相似度分数
  401. # 决策
  402. decision: ReconciliationDecision = ReconciliationDecision.KEEP_TEMPLATE
  403. decision_reason: str = "" # 决策理由(含 LLM 判断结果)
  404. # 执行信息
  405. replace_with_tender_table: Optional[ExtractedTable] = None # 替换时使用的招标表格
  406. fill_data: Dict[str, str] = field(default_factory=dict) # 填充时使用的数据映射
  407. llm_judgment: str = "" # LLM 对格式差异的判断原文
  408. # 标记
  409. is_confirmed: bool = False # 是否已经 LLM 确认
  410. needs_manual_review: bool = False # 是否需要人工审核
  411. @dataclass
  412. class ReconciliationReport:
  413. """格式比对汇总报告"""
  414. total_template_tables: int = 0
  415. total_tender_tables: int = 0
  416. reconciliations: List[FormatReconciliation] = field(default_factory=list)
  417. replace_count: int = 0 # 需要替换的表格数
  418. keep_count: int = 0 # 保留模板的表格数
  419. append_count: int = 0 # 需要追加的表格数
  420. unpaired_tender_tables: List[ExtractedTable] = field(default_factory=list) # 招标文件中未配对的表格
  421. summary: str = "" # LLM 生成的比对总结
  422. # ============================================================
  423. # 公司信息
  424. # ============================================================
  425. @dataclass
  426. class CompanyInfo:
  427. """结构化公司基本信息(从通用资料中提取)"""
  428. full_name: str = "" # 公司全称
  429. short_name: str = "" # 公司简称
  430. address: str = ""
  431. phone: str = ""
  432. fax: str = ""
  433. zip_code: str = ""
  434. bank_name: str = ""
  435. bank_account: str = ""
  436. legal_rep: str = "" # 法定代表人姓名
  437. legal_rep_id: str = "" # 法定代表人身份证号
  438. registration_capital: str = ""
  439. unified_code: str = "" # 统一社会信用代码
  440. website: str = ""
  441. # ============================================================
  442. # 工作流状态
  443. # ============================================================
  444. @dataclass
  445. class AgentState:
  446. """LangGraph 工作流的状态对象"""
  447. # 输入(二选一:project_dir 传统模式,或五类显式参数)
  448. project_dir: str = "" # 项目数据目录路径
  449. project_id: str = "" # 项目编号
  450. project_name: str = "" # 项目名称
  451. package_ids: List[str] = field(default_factory=list) # 指定的包件(空=全部)
  452. # 五类显式参数(优先级高于 project_dir)
  453. tender_files: List[str] = field(default_factory=list)
  454. procurement_files: List[str] = field(default_factory=list)
  455. clarification_files: List[str] = field(default_factory=list)
  456. reference_bid_dirs: List[str] = field(default_factory=list) # 向后兼容,旧版目录模式
  457. reference_bids: List[ReferenceCase] = field(default_factory=list) # 新版结构化参考案例
  458. company_info_dirs: List[str] = field(default_factory=list)
  459. # 中间结果
  460. project_data: Optional[ProjectData] = None
  461. tender_analysis: Optional[TenderAnalysis] = None
  462. outline: Optional[BidOutline] = None
  463. review_report: Optional[ReviewReport] = None
  464. # 输出
  465. output_path: str = "" # 生成的 DOCX 文件路径
  466. error: str = "" # 错误信息
  467. # V2 新字段
  468. template_path: str = "" # 投标模板 DOCX 路径
  469. extracted_tables: Dict[str, Any] = field(default_factory=dict) # 见 step1_parsing/table_extractor
  470. reconciliation_report: Optional[Any] = None # FormatReconciliation 报告(延迟导入)
  471. heading_levels: Dict[int, int] = field(default_factory=dict) # para_idx → heading_level
  472. # 工作流控制
  473. current_step: str = "" # 当前执行步骤
  474. max_review_iterations: int = 2 # 最大审核迭代次数
  475. review_iteration: int = 0 # 当前审核轮次