models.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. """统一业务事实。单位、来源和人工确认均显式记录。"""
  2. from datetime import date, datetime
  3. from typing import Literal
  4. from pydantic import AliasChoices, BaseModel, ConfigDict, Field, field_validator
  5. class FactModel(BaseModel):
  6. model_config = ConfigDict(extra="forbid", populate_by_name=True, allow_inf_nan=False,
  7. str_strip_whitespace=True)
  8. class Evidence(FactModel):
  9. source: str = ""
  10. page: int | None = Field(default=None, ge=1)
  11. label: str = ""
  12. excerpt: str = ""
  13. sha256: str = ""
  14. method: Literal["api", "manual_transcription", "model_extraction", "text_extraction", "synthetic"] = "api"
  15. class Device(FactModel):
  16. device_code: str = Field(min_length=1, validation_alias=AliasChoices("device_code", "deviceCode"))
  17. name: str = Field(min_length=1, validation_alias=AliasChoices("name", "deviceName"))
  18. mine_code: str = Field(default="", validation_alias=AliasChoices("mine_code", "mineCode"))
  19. mine_name: str = Field(default="", validation_alias=AliasChoices("mine_name", "mineName"))
  20. location: str = ""
  21. state: Literal["running", "stopped", "unknown"] = "unknown"
  22. fan_role: Literal["in_service", "standby", "upgrading", "replacing", "unknown"] = "unknown"
  23. availability_status: Literal["available", "unavailable", "unknown"] = "unknown"
  24. observed_at: datetime | None = None
  25. evidence: Evidence = Field(default_factory=Evidence)
  26. @field_validator("state", mode="before")
  27. @classmethod
  28. def state_label(cls, value):
  29. return {"开": "running", "停": "stopped"}.get(value, value)
  30. Metric = Literal["velocity", "flow", "static_pressure", "pressure", "shaft_power",
  31. "input_power", "temperature", "current", "voltage", "speed", "frequency", "unknown"]
  32. class MonitorPoint(FactModel):
  33. point_code: str = Field(min_length=1, validation_alias=AliasChoices("point_code", "pointCode"))
  34. name: str = Field(min_length=1, validation_alias=AliasChoices("name", "pointName"))
  35. mine_code: str = Field(default="", validation_alias=AliasChoices("mine_code", "mineCode"))
  36. mine_name: str = Field(default="", validation_alias=AliasChoices("mine_name", "mineName"))
  37. device_name: str = ""
  38. location: str = ""
  39. metric: Metric = "unknown"
  40. value: float | None = None
  41. unit: str = ""
  42. status: str = "unknown"
  43. observed_at: datetime | None = None
  44. evidence: Evidence = Field(default_factory=Evidence)
  45. class ConfirmedBinding(FactModel):
  46. point_code: str = Field(min_length=1)
  47. fan_id: str = Field(min_length=1)
  48. confirmed_by: str = Field(min_length=1)
  49. reason: str = Field(min_length=1)
  50. class PerformancePoint(FactModel):
  51. label: str = ""
  52. frequency_hz: float | None = Field(default=None, gt=0)
  53. blade_angle_deg: float | None = None
  54. # 现场实测与折算值不可混用;每条记录都是同一行的量。
  55. condition: Literal["measured", "converted"] = "measured"
  56. flow_m3_s: float = Field(ge=0)
  57. static_pressure_pa: float = Field(ge=0)
  58. shaft_power_kw: float = Field(gt=0)
  59. input_power_kw: float | None = Field(default=None, gt=0)
  60. reported_efficiency_pct: float | None = Field(default=None, ge=0, le=100)
  61. evidence: Evidence = Field(default_factory=Evidence)
  62. class PerformanceReport(FactModel):
  63. report_id: str = Field(min_length=1)
  64. mine_name: str = Field(min_length=1)
  65. mine_code: str = ""
  66. fan_name: str = Field(min_length=1)
  67. serial_number: str = ""
  68. model: str = ""
  69. motor_model: str = ""
  70. rated_power_raw: str = Field(default="", description="铭牌额定功率原表达式,如2×185 kW;不以运行功率代替")
  71. rated_power_kw: float | None = Field(default=None, gt=0, description="仅直接载明的总额定功率;组合表达式由工具计算")
  72. rated_speed_rpm: float | None = Field(default=None, gt=0)
  73. rated_voltage_raw: str = ""
  74. manufacturer: str = ""
  75. impeller_diameter_m: float | None = Field(default=None, gt=0)
  76. ventilation_mode: str = ""
  77. structure_type: str = ""
  78. rated_flow_range_m3_min: list[float] = Field(default_factory=list, max_length=2)
  79. rated_pressure_range_pa: list[float] = Field(default_factory=list, max_length=2)
  80. blade_angle_deg: float | None = Field(default=None, description="测试叶片安装角;原表/或空白表示未载明,不填0")
  81. inspected_on: date | None = None
  82. recommended_next_on: date | None = None
  83. original_conclusion: str = ""
  84. basis: list[str] = Field(default_factory=list)
  85. points: list[PerformancePoint] = Field(default_factory=list, max_length=250)
  86. curves: list[PerformancePoint] = Field(default_factory=list, max_length=30,
  87. description="只录曲线图注印出的工况数字,不从曲线像素猜读,和数据表分开保存")
  88. metadata_evidence: dict[str, Evidence] = Field(default_factory=dict,
  89. description="每个身份/日期字段的原文与物理页码,日期须含检测/建议下次标签")
  90. evidence: Evidence = Field(default_factory=Evidence)
  91. class DocumentFacts(FactModel):
  92. reports: list[PerformanceReport] = Field(default_factory=list)
  93. devices: list[Device] = Field(default_factory=list)
  94. points: list[MonitorPoint] = Field(default_factory=list)
  95. warnings: list[str] = Field(default_factory=list)
  96. extraction_audit: dict = Field(default_factory=dict, description="仅由服务端填写,记录路由、缓存和模型调用")
  97. class SeriesSample(FactModel):
  98. observed_at: datetime
  99. value: float
  100. status: str = "normal"
  101. class OperatingSeries(FactModel):
  102. mine_code: str = ""
  103. mine_name: str = ""
  104. fan_id: str = Field(min_length=1)
  105. metric: Metric
  106. unit: str
  107. sensor_id: str = Field(min_length=1)
  108. mapping_confirmed: bool = False
  109. samples: list[SeriesSample] = Field(default_factory=list, max_length=200000)
  110. evidence: Evidence = Field(default_factory=Evidence)
  111. class SwitchRecord(FactModel):
  112. mine_code: str = ""
  113. mine_name: str = ""
  114. event_id: str = Field(min_length=1)
  115. switched_at: datetime
  116. from_fan_id: str = Field(min_length=1)
  117. to_fan_id: str = Field(min_length=1)
  118. from_stopped_at: datetime | None = None
  119. to_started_at: datetime | None = None
  120. reason: str = "unknown"
  121. evidence: Evidence = Field(default_factory=Evidence)
  122. class FanStateRecord(FactModel):
  123. mine_code: str = ""
  124. mine_name: str = ""
  125. fan_id: str = Field(min_length=1)
  126. observed_at: datetime
  127. state: Literal["running", "stopped", "unknown"]
  128. complete_fan_state: bool = False
  129. frequency_hz: float | None = Field(default=None, gt=0)
  130. blade_angle_deg: float | None = None
  131. event_type: Literal["state", "start_stop", "frequency_change", "angle_change", "fault"] = "state"
  132. evidence: Evidence = Field(default_factory=Evidence)
  133. class ScreeningInput(FactModel):
  134. mine_code: str = Field(min_length=1, validation_alias=AliasChoices("mine_code", "mineCode"))
  135. mine_name: str = Field(min_length=1, validation_alias=AliasChoices("mine_name", "mineName"))
  136. fan_id: str = Field(default="", validation_alias=AliasChoices("fan_id", "fanId"))
  137. fan_name: str = ""
  138. fan_pair: list[str] = Field(default_factory=list, max_length=2, description="两台风机的明确标识;不表示谁主用谁备用")
  139. operating_series: list[OperatingSeries] = Field(default_factory=list, max_length=200)
  140. switch_records: list[SwitchRecord] = Field(default_factory=list, max_length=1000)
  141. fan_state_records: list[FanStateRecord] = Field(default_factory=list, max_length=200000)
  142. evaluation_time: datetime | None = None
  143. devices: list[Device] = Field(default_factory=list, max_length=500)
  144. points: list[MonitorPoint] = Field(default_factory=list, max_length=3000)
  145. reports: list[PerformanceReport] = Field(default_factory=list, max_length=50)
  146. confirmed_bindings: list[ConfirmedBinding] = Field(default_factory=list)
  147. class Finding(FactModel):
  148. code: str
  149. severity: Literal["info", "review", "blocking"]
  150. message: str
  151. evidence: list[Evidence] = Field(default_factory=list)
  152. class Association(FactModel):
  153. point_code: str
  154. candidate_fan: str
  155. state: Literal["candidate", "confirmed", "unmatched", "ambiguous", "excluded"]
  156. confidence: Literal["none", "low", "medium", "confirmed"]
  157. basis: list[str]
  158. eligible_for_calculation: bool = False
  159. class Calculation(FactModel):
  160. scope: Literal["historical_report", "current_confirmed"]
  161. label: str
  162. formula: str = "eta_static = Q[m3/s] * p_static[Pa] / (1000 * P_shaft[kW]) * 100%"
  163. values: dict[str, float]
  164. efficiency_pct: float
  165. reported_efficiency_pct: float | None = None
  166. difference_percentage_points: float | None = None
  167. evidence: list[Evidence] = Field(default_factory=list)
  168. class Screening(FactModel):
  169. scope: Literal["preliminary_screening"] = "preliminary_screening"
  170. engineering_status: Literal["insufficient_data", "review_required"]
  171. has_in_scope_data: bool
  172. findings: list[Finding]
  173. associations: list[Association]
  174. calculations: list[Calculation]
  175. missing_materials: list[str]
  176. document_audits: list[dict] = Field(default_factory=list)