models.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. """
  2. models.py - 投标书生成系统的数据结构定义
  3. 定义了系统中所有核心数据类,包括:
  4. - BidPage / BidDocument: 招标文件解析结果
  5. - Requirement / AnalysisResult: 招标要求分析结果
  6. - ProposalSection / ProposalDocument: 投标内容生成结果
  7. """
  8. from dataclasses import dataclass, field
  9. from typing import Optional
  10. from pdf_table_to_docx.table_parser import TableInfo
  11. @dataclass
  12. class BidPage:
  13. """招标文件中的一页"""
  14. page_num: int # 页码(1-indexed)
  15. text: str # 页面文本内容
  16. def __str__(self) -> str:
  17. return f"第{self.page_num}页 ({len(self.text)}字符)"
  18. @dataclass
  19. class BidDocument:
  20. """招标文件结构化内容"""
  21. pdf_path: str # PDF 文件路径
  22. file_name: str = "" # 文件名
  23. total_pages: int = 0 # 总页数
  24. pages: list[BidPage] = field(default_factory=list) # 所有页面文本
  25. full_text: str = "" # 全文拼接文本(供 AI 分析使用)
  26. tables: list[TableInfo] = field(default_factory=list) # 从 PDF 提取的表格
  27. table_count: int = 0 # 表格数量
  28. def __post_init__(self):
  29. """自动计算派生字段"""
  30. if not self.file_name and self.pdf_path:
  31. import os
  32. self.file_name = os.path.basename(self.pdf_path)
  33. if not self.total_pages:
  34. self.total_pages = len(self.pages)
  35. if not self.full_text:
  36. self.full_text = "\n\n".join(p.text for p in self.pages)
  37. if not self.table_count:
  38. self.table_count = len(self.tables)
  39. def __str__(self) -> str:
  40. return (f"招标文件: {self.file_name}\n"
  41. f" 共 {self.total_pages} 页, {self.table_count} 个表格")
  42. @dataclass
  43. class Requirement:
  44. """单个招标要求项"""
  45. category: str # 类别(evaluation/service/qualification/other)
  46. title: str # 要求标题
  47. description: str = "" # 详细描述
  48. score: Optional[float] = None # 分值(评分项专用)
  49. detail: str = "" # 评分标准详细说明
  50. source_table_id: Optional[int] = None # 来源表格ID
  51. def __str__(self) -> str:
  52. score_str = f" [{self.score}分]" if self.score is not None else ""
  53. return f"[{self.category}]{score_str} {self.title[:50]}"
  54. @dataclass
  55. class AnalysisResult:
  56. """招标文件分析结果"""
  57. summary: str = "" # 招标概要
  58. project_name: str = "" # 项目名称
  59. evaluation_criteria: list[Requirement] = field(default_factory=list) # 评审标准
  60. service_requirements: list[Requirement] = field(default_factory=list) # 服务要求
  61. qualification_requirements: list[Requirement] = field(default_factory=list) # 资质要求
  62. other_requirements: list[Requirement] = field(default_factory=list) # 其他要求
  63. raw_response: str = "" # AI 原始响应(调试用)
  64. @property
  65. def all_requirements(self) -> list[Requirement]:
  66. """获取所有要求(合并列表)"""
  67. return (self.evaluation_criteria
  68. + self.service_requirements
  69. + self.qualification_requirements
  70. + self.other_requirements)
  71. @property
  72. def total_requirements(self) -> int:
  73. return len(self.all_requirements)
  74. def by_category(self, category: str) -> list[Requirement]:
  75. """按类别筛选要求"""
  76. return [r for r in self.all_requirements if r.category == category]
  77. def __str__(self) -> str:
  78. return (f"招标分析结果: {self.project_name or '未命名项目'}\n"
  79. f" 评审标准: {len(self.evaluation_criteria)}项, "
  80. f"服务要求: {len(self.service_requirements)}项, "
  81. f"资质要求: {len(self.qualification_requirements)}项")
  82. @dataclass
  83. class ProposalSection:
  84. """投标文件的一个章节"""
  85. title: str # 章节标题
  86. level: int = 1 # 层级(1=一级标题, 2=二级标题)
  87. content: str = "" # 章节正文
  88. requirement_ref: str = "" # 对应的招标要求引用
  89. def __str__(self) -> str:
  90. return f"{' ' * (self.level-1)}#{self.level} {self.title} ({len(self.content)}字符)"
  91. @dataclass
  92. class ProposalDocument:
  93. """完整的投标文件内容"""
  94. title: str = "" # 文档标题
  95. project_name: str = "" # 项目名称
  96. sections: list[ProposalSection] = field(default_factory=list) # 所有章节
  97. summary: str = "" # 投标概要
  98. @property
  99. def total_sections(self) -> int:
  100. return len(self.sections)
  101. def by_level(self, level: int) -> list[ProposalSection]:
  102. """按层级筛选章节"""
  103. return [s for s in self.sections if s.level == level]
  104. def __str__(self) -> str:
  105. return (f"投标内容: {self.title}\n"
  106. f" 共 {self.total_sections} 个章节")