Skip to content

Commit 96bbd7e

Browse files
authored
Merge pull request #61 from zestones/5-m14-pydantic-equipmentkb-complet
5 m14 pydantic equipmentkb complet
2 parents e27115c + 629c244 commit 96bbd7e

2 files changed

Lines changed: 405 additions & 0 deletions

File tree

backend/modules/kb/kb_schema.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
"""Domain model for equipment_kb.structured_data (jsonb blob).
2+
3+
This is NOT an API DTO — it is the structured schema that agents read/write.
4+
The outer equipment_kb row uses EquipmentKbOut (schemas.py) for the API layer;
5+
structured_data is decoded from jsonb and validated against EquipmentKB here.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from datetime import date
11+
from typing import Any, Optional
12+
13+
from pydantic import BaseModel, ConfigDict, Field
14+
15+
16+
class ThresholdValue(BaseModel):
17+
"""Per-signal threshold configuration.
18+
19+
Supports two alert patterns:
20+
- single-sided: ``alert`` (e.g. vibration, temperature)
21+
- double-sided: ``low_alert`` / ``high_alert`` (e.g. flow, pressure)
22+
"""
23+
24+
model_config = ConfigDict(extra="allow")
25+
26+
nominal: Optional[float] = None
27+
# single-sided threshold
28+
alert: Optional[float] = None
29+
trip: Optional[float] = None
30+
# double-sided threshold
31+
low_alert: Optional[float] = None
32+
high_alert: Optional[float] = None
33+
unit: Optional[str] = None
34+
source: Optional[str] = None
35+
confidence: Optional[float] = None
36+
37+
@property
38+
def is_filled(self) -> bool:
39+
"""True when at least one alert bound is defined."""
40+
return self.alert is not None or self.low_alert is not None or self.high_alert is not None
41+
42+
43+
class FailurePattern(BaseModel):
44+
model_config = ConfigDict(extra="allow")
45+
46+
mode: str
47+
symptoms: Optional[str] = None
48+
mtbf_months: Optional[int] = None
49+
signal_signature: Optional[dict[str, Any]] = None
50+
51+
52+
class MaintenanceProcedure(BaseModel):
53+
model_config = ConfigDict(extra="allow")
54+
55+
action: str
56+
interval_months: Optional[int] = None
57+
duration_min: Optional[int] = None
58+
parts: list[str] = Field(default_factory=list)
59+
60+
61+
class EquipmentMeta(BaseModel):
62+
"""Identifying metadata for the equipment."""
63+
64+
model_config = ConfigDict(extra="allow")
65+
66+
cell_id: Optional[int] = None
67+
equipment_type: Optional[str] = None
68+
manufacturer: Optional[str] = None
69+
model: Optional[str] = None
70+
installation_date: Optional[date] = None
71+
service_description: Optional[str] = None
72+
motor_power_kw: Optional[float] = None
73+
rpm_nominal: Optional[int] = None
74+
75+
76+
class KbMeta(BaseModel):
77+
model_config = ConfigDict(extra="allow")
78+
79+
version: int = 1
80+
completeness_score: float = 0.0
81+
onboarding_complete: bool = False
82+
last_calibrated_by: Optional[str] = None
83+
84+
85+
# Fields used to score the equipment section of completeness.
86+
_EQUIPMENT_SCORED_FIELDS = (
87+
"cell_id",
88+
"equipment_type",
89+
"manufacturer",
90+
"model",
91+
"installation_date",
92+
"service_description",
93+
"motor_power_kw",
94+
"rpm_nominal",
95+
)
96+
97+
# Expected minimum counts per section for a "complete" KB.
98+
_EXPECTED_THRESHOLDS = 3
99+
_EXPECTED_FAILURE_PATTERNS = 3
100+
_EXPECTED_PROCEDURES = 3
101+
102+
103+
class EquipmentKB(BaseModel):
104+
"""Top-level KB blob stored in ``equipment_kb.structured_data``.
105+
106+
All sections default to empty so a partial KB (e.g. after a PDF-only
107+
import before operator calibration) is still valid.
108+
"""
109+
110+
model_config = ConfigDict(extra="allow")
111+
112+
equipment: EquipmentMeta = Field(default_factory=EquipmentMeta)
113+
thresholds: dict[str, ThresholdValue] = Field(default_factory=dict)
114+
failure_patterns: list[FailurePattern] = Field(default_factory=list)
115+
maintenance_procedures: list[MaintenanceProcedure] = Field(default_factory=list)
116+
kb_meta: KbMeta = Field(default_factory=KbMeta)
117+
118+
def compute_completeness(self) -> float:
119+
"""Return a weighted completeness score in [0.0, 1.0].
120+
121+
Weights:
122+
- thresholds 50 % (Sentinel uses them directly)
123+
- failure_patterns 20 % (Investigator pattern matching)
124+
- maintenance_procedures 20 % (Work Order Generator)
125+
- equipment 10 % (identifying metadata)
126+
"""
127+
weights = {
128+
"thresholds": 0.50,
129+
"failure_patterns": 0.20,
130+
"maintenance_procedures": 0.20,
131+
"equipment": 0.10,
132+
}
133+
134+
# Equipment: fraction of key metadata fields that are non-None.
135+
filled_eq = sum(
136+
1 for f in _EQUIPMENT_SCORED_FIELDS if getattr(self.equipment, f, None) is not None
137+
)
138+
eq_score = filled_eq / len(_EQUIPMENT_SCORED_FIELDS)
139+
140+
# Thresholds: count thresholds that have at least one alert bound.
141+
filled_thr = sum(1 for t in self.thresholds.values() if t.is_filled)
142+
thr_score = min(filled_thr, _EXPECTED_THRESHOLDS) / _EXPECTED_THRESHOLDS
143+
144+
# Failure patterns: existence of known failure modes.
145+
fp_score = (
146+
min(len(self.failure_patterns), _EXPECTED_FAILURE_PATTERNS) / _EXPECTED_FAILURE_PATTERNS
147+
)
148+
149+
# Maintenance procedures: existence of scheduled maintenance.
150+
mp_score = (
151+
min(len(self.maintenance_procedures), _EXPECTED_PROCEDURES) / _EXPECTED_PROCEDURES
152+
)
153+
154+
return round(
155+
weights["equipment"] * eq_score
156+
+ weights["thresholds"] * thr_score
157+
+ weights["failure_patterns"] * fp_score
158+
+ weights["maintenance_procedures"] * mp_score,
159+
4,
160+
)

0 commit comments

Comments
 (0)