fix day 0 insight main dashboard page
This commit is contained in:
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)
|
||||
|
||||
|
||||
Reference in new issue
Block a user