fix day 0 insight main dashboard page

This commit is contained in:
Alberto-Audrix committed 2026-10-01 15:55:09 +07:00
1 parent e1987d6999
commit bc2b260065
6 files changed
+165 -36

No files matched your search

@@ -100,7 +100,7 @@ PERFORMANCE_STANDARD_DAILY: list[dict[str, Any]] = [
# ─── Target Suhu Pemeliharaan per Umur (Buku CP 707) ────────────────────────
TEMPERATURE_STANDARD: list[dict[str, Any]] = [
{"ageDay_from": 1, "ageDay_to": 2, "temp_C": 32, "humidity_pct_min": 50, "humidity_pct_max": 70},
{"ageDay_from": 0, "ageDay_to": 2, "temp_C": 32, "humidity_pct_min": 50, "humidity_pct_max": 70},
{"ageDay_from": 3, "ageDay_to": 4, "temp_C": 31, "humidity_pct_min": 50, "humidity_pct_max": 70},
{"ageDay_from": 5, "ageDay_to": 7, "temp_C": 30, "humidity_pct_min": 50, "humidity_pct_max": 70},
{"ageDay_from": 8, "ageDay_to": 14, "temp_C": 29, "humidity_pct_min": 50, "humidity_pct_max": 70},
@@ -112,7 +112,7 @@ TEMPERATURE_STANDARD: list[dict[str, Any]] = [
# ─── Target Efektif Temperatur (TET) per Umur ───────────────────────────────
TARGET_EFFECTIVE_TEMPERATURE: list[dict[str, Any]] = [
{"ageDay_from": 1, "ageDay_to": 2, "tet_C": 32},
{"ageDay_from": 0, "ageDay_to": 2, "tet_C": 32},
{"ageDay_from": 3, "ageDay_to": 4, "tet_C": 31},
{"ageDay_from": 5, "ageDay_to": 7, "tet_C": 30},
{"ageDay_from": 8, "ageDay_to": 14, "tet_C": 29},
@@ -197,7 +197,7 @@ WATER_INCREASE_PER_DEGREE_ABOVE_21C_PCT = 6.5
# ─── Program Pencahayaan CP 707 ───────────────────────────────────────────────
LIGHTING_PROGRAM: list[dict[str, Any]] = [
{"ageDay_from": 2, "ageDay_to": 7, "onHours": 23, "darkFrom": "20:00", "darkTo": "21:00"},
{"ageDay_from": 0, "ageDay_to": 7, "onHours": 23, "darkFrom": "20:00", "darkTo": "21:00"},
{"ageDay_from": 8, "ageDay_to": 14, "onHours": 22, "darkFrom": "20:00", "darkTo": "22:00"},
{"ageDay_from": 15, "ageDay_to": 20, "onHours": 20, "darkFrom": "20:00", "darkTo": "23:00"},
{"ageDay_from": 21, "ageDay_to": 28, "onHours": 20, "darkFrom": "20:00", "darkTo": "24:00"},
@@ -247,6 +247,8 @@ def get_fcr_standard_by_day(day_age: Any) -> float | None:
day = int(day_age)
except (TypeError, ValueError):
return None
if day == 0:
return None
if day > 37:
return 1.65
return None
@@ -260,6 +262,8 @@ def get_bw_standard_by_day(day_age: Any) -> float | None:
day = int(day_age)
except (TypeError, ValueError):
return None
if day == 0:
return None
if day > 37:
return 2500.0
return None
@@ -318,12 +322,28 @@ def get_mortality_cum_std_by_day(day_age: Any) -> float | None:
day = int(day_age)
except (TypeError, ValueError):
return None
if day == 0:
return 0.40
if day > 37:
return 4.65 + ((day - 37) * 0.1)
return None
def analyze_fcr(actual_fcr: Any, day_age: Any) -> dict[str, Any]:
try:
day_int = int(day_age)
except (TypeError, ValueError):
day_int = None
if day_int == 0:
return {
"actual": _as_float(actual_fcr),
"standard": None,
"delta_pct": None,
"direction": "unknown",
"status": "ok",
"message": "FCR belum dihitung pada hari ke-0 (DOC baru tiba / masa awal brooding).",
}
actual = _as_float(actual_fcr)
# FCR 0 / negative is not a valid reading — treat as missing.
if actual is not None and actual <= 0:
@@ -375,6 +395,27 @@ def analyze_fcr(actual_fcr: Any, day_age: Any) -> dict[str, Any]:
def analyze_bw(actual_bw_g: Any, day_age: Any) -> dict[str, Any]:
try:
day_int = int(day_age)
except (TypeError, ValueError):
day_int = None
if day_int == 0:
actual = _as_float(actual_bw_g)
if actual is not None and actual <= 0:
actual = None
return {
"actual": actual,
"standard": actual,
"delta_pct": None,
"direction": "sesuai_standar" if actual is not None else "unknown",
"status": "ok" if actual is not None else "unknown",
"message": (
f"Bobot hari ke-0 adalah bobot awal DOC masuk ({actual:g}g) yang menjadi acuan standar baseline siklus."
if actual is not None
else "Bobot awal DOC masuk hari ke-0 tidak tersedia."
),
}
actual = _as_float(actual_bw_g)
# 0 g is a missing/placeholder reading, not a real bird weight.
if actual is not None and actual <= 0:
@@ -586,6 +627,20 @@ def analyze_environment(
def analyze_adg(actual_adg: Any, day_age: Any) -> dict[str, Any]:
"""ADG (Average Daily Gain) vs daily standard from book."""
try:
day_int = int(day_age)
except (TypeError, ValueError):
day_int = None
if day_int == 0:
return {
"actual": _as_float(actual_adg),
"standard": None,
"delta_pct": None,
"direction": "unknown",
"status": "ok",
"message": "ADG belum dihitung pada hari ke-0 (hari kedatangan DOC).",
}
actual = _as_float(actual_adg)
if actual is not None and actual <= 0:
actual = None
@@ -737,6 +792,22 @@ def analyze_eef(
2. No book standard: manager absolute scale (<200/200-300/300-350/350-400/400-500, >500=invalid)
3. Trend always computed from eef_seri; sharp drop escalates ladder verdict one level.
"""
try:
day_int = int(day_age)
except (TypeError, ValueError):
day_int = None
if day_int == 0:
return {
"actual": None,
"standard": None,
"delta_pct": None,
"direction": "unknown",
"status": "ok",
"message": "EEF/IP belum dihitung pada hari ke-0 (DOC baru tiba / masa awal brooding).",
"trend": None,
"source": "none",
}
eef_val = _as_float(eef_terakhir)
if eef_val is not None and eef_val <= 0:
eef_val = None
@@ -971,10 +1042,15 @@ def build_book_reference_context(day_age: Any) -> str:
temp_std = get_temp_standard_by_day(day_age)
tet = get_tet_by_day(day_age)
bw_target_str = (
"Mengikuti bobot awal DOC masuk (bukan standar baku tabel)"
if day_age == 0
else (f"{std_bw} gram" if std_bw is not None else "N/A")
)
return f"""
=== REFERENSI STANDAR BUKU CP 707 (SUMBER TUNGGAL - PT CHAROEN POKPHAND INDONESIA) ===
Umur ayam hari ke-{day_age}:
- Target bobot badan: {std_bw if std_bw is not None else 'N/A'} gram
- Target bobot badan: {bw_target_str}
- Target FCR: {std_fcr if std_fcr is not None else 'N/A'}
- Target mortalitas kumulatif normal: <5% (standar CP 707)
- Target mortalitas kumulatif standar hari ke-{day_age}: {str(std_mortality) + '%' if std_mortality is not None else 'N/A'}
@@ -1007,7 +1083,7 @@ def build_cp707_standard_block(context_data: dict[str, Any] | None) -> str:
raw = context_data.get(key)
break
day_f = _as_float(raw)
if day_f is None or day_f <= 0:
if day_f is None or day_f < 0:
return ""
day = int(day_f)
@@ -1017,8 +1093,14 @@ def build_cp707_standard_block(context_data: dict[str, Any] | None) -> str:
fcr = get_fcr_standard_by_day(day)
mort = get_mortality_cum_std_by_day(day)
bw_line = (
"- Bobot badan standar hari ke-0: Mengikuti bobot awal DOC masuk (bukan standar baku tabel)"
if day == 0
else f"- Bobot badan standar hari ke-{day}: {str(bw) + ' gram' if bw is not None else 'tidak tercantum di buku'}"
)
lines = [
f"- Bobot badan standar hari ke-{day}: {str(bw) + ' gram' if bw is not None else 'tidak tercantum di buku'}",
bw_line,
f"- FCR standar hari ke-{day}: {str(fcr) + ' (rasio, tanpa satuan)' if fcr is not None else 'tidak tercantum di buku'}",
f"- Mortalitas kumulatif standar hari ke-{day}: {str(mort) + ' %' if mort is not None else 'tidak tercantum di buku'}",
]
@@ -1100,7 +1182,7 @@ def _resolve_day_age(context_pack: dict[str, Any]) -> int | None:
context_pack.get("currentDay"),
context_pack.get("dayAge"),
)
if day is None or day <= 0:
if day is None or day < 0:
return None
return int(day)
@@ -326,17 +326,16 @@ def strip_headerless_tables(chunk: str) -> tuple[str, int]:
def _day_age(context: dict[str, Any]) -> int | None:
raw = (
context.get("hari_ke")
or context.get("hari_terakhir")
or context.get("currentDay")
or context.get("dayAge")
)
try:
day = int(raw)
except (TypeError, ValueError):
return None
return day if day > 0 else None
for key in ("hari_ke", "hari_terakhir", "currentDay", "dayAge"):
if key in context and context[key] is not None:
raw = context[key]
try:
day = int(raw)
if day >= 0:
return day
except (TypeError, ValueError):
continue
return None
def prune_context_by_period(context: dict[str, Any], report_type: str, report_period: str) -> dict[str, Any]:
@@ -538,9 +537,10 @@ def _build_phase_summaries(weekly: list[dict[str, Any]]) -> dict[str, Any]:
def grade_context(context: dict[str, Any], topic: str | None = None) -> dict[str, Any]:
analysis = cp707.analyze_with_cp707_standards(context, topic)
day_val = analysis.get("dayAge") if analysis.get("dayAge") is not None else _day_age(context)
graded: dict[str, Any] = {
"kandangName": context.get("kandangName") or context.get("kandang_name"),
"hari_ke": analysis.get("dayAge") or _day_age(context),
"hari_ke": day_val,
"analyses": analysis.get("analyses") or {},
}
# Surface daily mortality headcount so the model cannot confuse the 5%
@@ -1012,10 +1012,11 @@ def generate_insight(
t_after_grade = time.monotonic()
_notify(stage_cb, "retrieving")
day = graded.get("hari_ke")
day_display = day if day is not None else '?'
if topic == "hitung_karung":
standard_block = (
"=== STANDAR MANAJEMEN PAKAN DAN SALDO KARUNG (CP 707) ===\n"
f"Umur ayam hari ke-{day or '?'}:\n"
f"Umur ayam hari ke-{day_display}:\n"
"- Stok pakan: Saldo karung pakan di kandang harus selalu positif dan mencukupi kebutuhan harian ayam.\n"
"- Pencatatan: Mutasi karung masuk, saldo awal, pakan dituang, dan karung keluar harus tercatat tertib.\n"
"- Waspada: Saldo karung menipis (di bawah kebutuhan konsumsi harian) atau saldo negatif menunjukkan perlunya restock atau audit mutasi karung.\n"
@@ -1023,7 +1024,7 @@ def generate_insight(
"=== END STANDAR PAKAN ==="
)
else:
standard_block = cp707.build_cp707_standard_block(ctx) or cp707.build_book_reference_context(day or 1)
standard_block = cp707.build_cp707_standard_block(ctx) or cp707.build_book_reference_context(day if day is not None else 1)
is_end_cycle = report_type == "end_cycle"
hypotheses: list[dict[str, Any]] = []
@@ -1037,12 +1038,12 @@ def generate_insight(
top_fase = next((h.get("fase") for h in hypotheses if h.get("fase")), None)
rag_query = (
f"panduan CP 707 perbaikan {top_fase or 'broiler'} siklus berikutnya "
f"umur hari ke-{day or '?'}"
f"umur hari ke-{day_display}"
)
elif topic == "hitung_karung":
rag_query = f"manajemen stok pakan karung pakan harian konsumsi tempat pakan umur hari ke-{day or '?'}"
rag_query = f"manajemen stok pakan karung pakan harian konsumsi tempat pakan umur hari ke-{day_display}"
else:
rag_query = f"panduan manajemen broiler CP 707 untuk {topic} umur hari ke-{day or '?'}"
rag_query = f"panduan manajemen broiler CP 707 untuk {topic} umur hari ke-{day_display}"
rag_query = f"{rag_query} {diagnose_rag_keywords(graded)}".strip()
chunks, citations = fetch_rag_chunks(rag_query, topic)
t_after_rag = time.monotonic()
@@ -211,3 +211,38 @@ class FeedBalanceGradingTests(SimpleTestCase):
self.assertEqual(feed["status"], "unknown")
self.assertIn("tidak tersedia", feed["message"].lower())
class DayZeroInsightTests(SimpleTestCase):
def test_day_zero_grading_dashboard(self):
ctx = {
"hari_ke": 0,
"bobot_avg_gram": 32.0,
"persen_hidup": 99.8,
"doc_in_ekor": 10000,
"kematian_kumulatif_ekor": 20,
"fcr_terakhir": None,
}
graded = grade_context(ctx, "dashboard")
self.assertEqual(graded["hari_ke"], 0)
analyses = graded["analyses"]
self.assertIn("bw", analyses)
self.assertIn("fcr", analyses)
self.assertIn("mortality", analyses)
# BW 32g is baseline DOC weight -> ok
self.assertEqual(analyses["bw"]["status"], "ok")
self.assertEqual(analyses["bw"]["standard"], 32.0)
self.assertIn("bobot awal doc", analyses["bw"]["message"].lower())
# FCR not applicable on day 0 -> ok with explanatory message
self.assertEqual(analyses["fcr"]["status"], "ok")
self.assertIn("belum dihitung", analyses["fcr"]["message"].lower())
# Mortality 0.20% vs std 0.40% -> ok
self.assertEqual(analyses["mortality"]["status"], "ok")
def test_day_zero_standard_block_generation(self):
ctx = {"hari_ke": 0}
std_block = cp707.build_cp707_standard_block(ctx)
self.assertIn("STANDAR CP 707 UNTUK HARI INI", std_block)
self.assertIn("Bobot badan standar hari ke-0: Mengikuti bobot awal DOC masuk", std_block)
self.assertIn("Mortalitas kumulatif standar hari ke-0: 0.4 %", std_block)