adjust ai insight and seed data
This commit is contained in:
1 parent
68e0d1a0bb
commit
a2b5b10977
18 files changed
+1835
-140
No files matched your search
@@ -0,0 +1,604 @@
|
||||
"""Deterministic root-cause candidates and next-cycle actions for end-cycle insights.
|
||||
|
||||
LLM may only narrate/refine wording from these lists — never invent new causes.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
CONFIDENCE_HIGH = "high"
|
||||
CONFIDENCE_MED = "med"
|
||||
CONFIDENCE_LOW = "low"
|
||||
|
||||
PHASE_BROODING = "brooding"
|
||||
PHASE_GROWTH = "growth"
|
||||
PHASE_FINISHER = "finisher"
|
||||
|
||||
END_CYCLE_JSON_MARKER = "<!--end_cycle_json-->"
|
||||
|
||||
_CONF_RANK = {CONFIDENCE_LOW: 1, CONFIDENCE_MED: 2, CONFIDENCE_HIGH: 3}
|
||||
|
||||
|
||||
def _as_float(value: Any) -> float | None:
|
||||
try:
|
||||
if value is None or value == "":
|
||||
return None
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _as_int(value: Any) -> int | None:
|
||||
try:
|
||||
if value is None or value == "":
|
||||
return None
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _analysis(graded: dict[str, Any] | None, key: str) -> dict[str, Any]:
|
||||
analyses = (graded or {}).get("analyses") or {}
|
||||
block = analyses.get(key) if isinstance(analyses, dict) else None
|
||||
return block if isinstance(block, dict) else {}
|
||||
|
||||
|
||||
def _weekly_rows(context: dict[str, Any] | None) -> list[dict[str, Any]]:
|
||||
rows = (context or {}).get("weekly_summaries")
|
||||
if not isinstance(rows, list):
|
||||
return []
|
||||
return [r for r in rows if isinstance(r, dict)]
|
||||
|
||||
|
||||
def _phase_rows(context: dict[str, Any] | None) -> dict[str, Any]:
|
||||
raw = (context or {}).get("phase_summaries")
|
||||
return raw if isinstance(raw, dict) else {}
|
||||
|
||||
|
||||
def _mortality_delta_by_week(weeks: list[dict[str, Any]]) -> dict[int, float]:
|
||||
out: dict[int, float] = {}
|
||||
for row in weeks:
|
||||
week = _as_int(row.get("minggu_ke") or row.get("week"))
|
||||
delta = _as_float(
|
||||
row.get("mortalitas_delta_persen")
|
||||
or row.get("mortality_delta_pct")
|
||||
or row.get("mortalitas_delta")
|
||||
)
|
||||
if week is not None and delta is not None:
|
||||
out[week] = delta
|
||||
return out
|
||||
|
||||
|
||||
def _iot_cold_hours_early(weeks: list[dict[str, Any]]) -> float:
|
||||
total = 0.0
|
||||
for row in weeks:
|
||||
week = _as_int(row.get("minggu_ke") or row.get("week"))
|
||||
if week is None or week > 2:
|
||||
continue
|
||||
hours = _as_float(
|
||||
row.get("jam_suhu_di_bawah_standar")
|
||||
or row.get("temp_below_hours")
|
||||
or row.get("hours_temp_below")
|
||||
)
|
||||
if hours is not None and hours > 0:
|
||||
total += hours
|
||||
return total
|
||||
|
||||
|
||||
def _iot_humid_or_ammonia_late(weeks: list[dict[str, Any]]) -> tuple[float, float]:
|
||||
humid = 0.0
|
||||
ammonia = 0.0
|
||||
for row in weeks:
|
||||
week = _as_int(row.get("minggu_ke") or row.get("week"))
|
||||
if week is None or week < 4:
|
||||
continue
|
||||
h = _as_float(
|
||||
row.get("jam_kelembapan_di_atas_standar")
|
||||
or row.get("humidity_above_hours")
|
||||
or row.get("hours_humidity_above")
|
||||
)
|
||||
a = _as_float(
|
||||
row.get("jam_amonia_tinggi")
|
||||
or row.get("ammonia_high_hours")
|
||||
or row.get("hours_ammonia_high")
|
||||
)
|
||||
if h is not None and h > 0:
|
||||
humid += h
|
||||
if a is not None and a > 0:
|
||||
ammonia += a
|
||||
return humid, ammonia
|
||||
|
||||
|
||||
def build_masalah_from_graded(graded: dict[str, Any] | None) -> list[str]:
|
||||
"""Critical/warning graded messages become the Masalah bullet list."""
|
||||
analyses = (graded or {}).get("analyses") or {}
|
||||
if not isinstance(analyses, dict):
|
||||
return []
|
||||
masalah: list[str] = []
|
||||
for key in ("mortality", "fcr", "bw", "environment"):
|
||||
block = analyses.get(key)
|
||||
if not isinstance(block, dict):
|
||||
continue
|
||||
status = str(block.get("status") or "").lower()
|
||||
if status not in ("warning", "critical"):
|
||||
continue
|
||||
msg = str(block.get("message") or "").strip()
|
||||
if msg:
|
||||
masalah.append(msg)
|
||||
return masalah
|
||||
|
||||
|
||||
def build_root_cause_hypotheses(
|
||||
graded: dict[str, Any] | None,
|
||||
context: dict[str, Any] | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Rank evidence-backed root-cause candidates for end-cycle narration."""
|
||||
ctx = context or {}
|
||||
weeks = _weekly_rows(ctx)
|
||||
phases = _phase_rows(ctx)
|
||||
mort = _analysis(graded, "mortality")
|
||||
fcr = _analysis(graded, "fcr")
|
||||
bw = _analysis(graded, "bw")
|
||||
env = _analysis(graded, "environment")
|
||||
|
||||
mort_status = str(mort.get("status") or "").lower()
|
||||
fcr_status = str(fcr.get("status") or "").lower()
|
||||
bw_status = str(bw.get("status") or "").lower()
|
||||
env_status = str(env.get("status") or "").lower()
|
||||
|
||||
candidates: list[dict[str, Any]] = []
|
||||
|
||||
mort_by_week = _mortality_delta_by_week(weeks)
|
||||
early_mort = (mort_by_week.get(1) or 0) + (mort_by_week.get(2) or 0)
|
||||
brooding_phase = (
|
||||
phases.get(PHASE_BROODING) if isinstance(phases.get(PHASE_BROODING), dict) else {}
|
||||
)
|
||||
brooding_mort = _as_float(
|
||||
brooding_phase.get("mortalitas_delta_persen")
|
||||
or brooding_phase.get("mortalitas_kumulatif_persen")
|
||||
)
|
||||
cold_hours = _iot_cold_hours_early(weeks)
|
||||
env_cold = env_status in ("warning", "critical") and "bawah" in str(
|
||||
env.get("message") or ""
|
||||
).lower()
|
||||
|
||||
if (early_mort >= 2.0 or (brooding_mort is not None and brooding_mort >= 2.0)) and (
|
||||
cold_hours >= 4 or env_cold
|
||||
):
|
||||
bukti: list[str] = []
|
||||
if early_mort >= 2.0:
|
||||
bukti.append(f"Lonjakan mortalitas minggu 1–2: +{early_mort:.2f} poin persen")
|
||||
elif brooding_mort is not None:
|
||||
bukti.append(f"Mortalitas fase brooding: {brooding_mort:.2f}%")
|
||||
if cold_hours >= 4:
|
||||
bukti.append(
|
||||
f"Jumlah pembacaan data suhu di bawah standar (minggu 1–2): {cold_hours:.0f}"
|
||||
)
|
||||
if env_cold and env.get("message"):
|
||||
bukti.append(str(env["message"]))
|
||||
candidates.append(
|
||||
{
|
||||
"id": "brooding_suhu",
|
||||
"hipotesis": "Brooding / kontrol suhu",
|
||||
"bukti": bukti,
|
||||
"confidence": CONFIDENCE_HIGH,
|
||||
"fase": PHASE_BROODING,
|
||||
}
|
||||
)
|
||||
elif early_mort >= 3.0 or (brooding_mort is not None and brooding_mort >= 3.0):
|
||||
bukti = []
|
||||
if early_mort >= 3.0:
|
||||
bukti.append(f"Lonjakan mortalitas minggu 1–2: +{early_mort:.2f} poin persen")
|
||||
if brooding_mort is not None:
|
||||
bukti.append(f"Mortalitas fase brooding: {brooding_mort:.2f}%")
|
||||
candidates.append(
|
||||
{
|
||||
"id": "brooding_manajemen",
|
||||
"hipotesis": "Manajemen brooding (tanpa sinyal suhu lengkap)",
|
||||
"bukti": bukti,
|
||||
"confidence": CONFIDENCE_MED,
|
||||
"fase": PHASE_BROODING,
|
||||
}
|
||||
)
|
||||
|
||||
fcr_lag = fcr_status in ("warning", "critical")
|
||||
bw_lag = bw_status in ("warning", "critical") or str(
|
||||
bw.get("direction") or ""
|
||||
) == "di_bawah_standar"
|
||||
if fcr_lag and bw_lag:
|
||||
bukti = []
|
||||
if fcr.get("message"):
|
||||
bukti.append(str(fcr["message"]))
|
||||
if bw.get("message"):
|
||||
bukti.append(str(bw["message"]))
|
||||
mid_fcr = None
|
||||
for row in weeks:
|
||||
week = _as_int(row.get("minggu_ke") or row.get("week"))
|
||||
if week in (3, 4):
|
||||
mid_fcr = _as_float(row.get("fcr_akhir_rasio") or row.get("fcr_rata_rasio"))
|
||||
if mid_fcr is not None:
|
||||
bukti.append(f"FCR pertengahan siklus (minggu 3–4): {mid_fcr:.3f}")
|
||||
candidates.append(
|
||||
{
|
||||
"id": "pakan_akses",
|
||||
"hipotesis": "Pakan / akses pakan",
|
||||
"bukti": bukti or ["FCR dan bobot menyimpang dari standar CP 707"],
|
||||
"confidence": (
|
||||
CONFIDENCE_HIGH
|
||||
if fcr_status == "critical" or bw_status == "critical"
|
||||
else CONFIDENCE_MED
|
||||
),
|
||||
"fase": PHASE_GROWTH,
|
||||
}
|
||||
)
|
||||
elif fcr_lag:
|
||||
bukti = [str(fcr["message"])] if fcr.get("message") else ["FCR di atas standar CP 707"]
|
||||
candidates.append(
|
||||
{
|
||||
"id": "fcr_tinggi",
|
||||
"hipotesis": "Efisiensi pakan menurun",
|
||||
"bukti": bukti,
|
||||
"confidence": CONFIDENCE_MED,
|
||||
"fase": PHASE_GROWTH,
|
||||
}
|
||||
)
|
||||
|
||||
humid_h, ammonia_h = _iot_humid_or_ammonia_late(weeks)
|
||||
late_mort = sum(v for w, v in mort_by_week.items() if w >= 4)
|
||||
env_humid_or_ammonia = env_status in ("warning", "critical") and any(
|
||||
k in str(env.get("message") or "").lower() for k in ("kelembapan", "amonia", "ammonia")
|
||||
)
|
||||
if (humid_h >= 4 or ammonia_h >= 2 or env_humid_or_ammonia) and (
|
||||
late_mort >= 1.5 or mort_status in ("warning", "critical")
|
||||
):
|
||||
bukti = []
|
||||
if late_mort >= 1.5:
|
||||
bukti.append(f"Kenaikan mortalitas minggu 4+: +{late_mort:.2f} poin persen")
|
||||
if humid_h >= 4:
|
||||
bukti.append(
|
||||
f"Jumlah pembacaan data kelembapan di atas standar (minggu 4+): {humid_h:.0f}"
|
||||
)
|
||||
if ammonia_h >= 2:
|
||||
bukti.append(
|
||||
f"Jumlah pembacaan data amonia tinggi (minggu 4+): {ammonia_h:.0f}"
|
||||
)
|
||||
if env.get("message"):
|
||||
bukti.append(str(env["message"]))
|
||||
candidates.append(
|
||||
{
|
||||
"id": "litter_ventilasi",
|
||||
"hipotesis": "Litter / ventilasi",
|
||||
"bukti": bukti,
|
||||
"confidence": CONFIDENCE_HIGH if ammonia_h >= 2 or humid_h >= 8 else CONFIDENCE_MED,
|
||||
"fase": PHASE_FINISHER,
|
||||
}
|
||||
)
|
||||
|
||||
uniformity = _as_float(ctx.get("uniformity_persen") or ctx.get("uniformity"))
|
||||
if uniformity is not None and uniformity < 80:
|
||||
candidates.append(
|
||||
{
|
||||
"id": "kerapatan_feeder",
|
||||
"hipotesis": "Kerapatan / ruang tempat pakan",
|
||||
"bukti": [f"Uniformity rendah: {uniformity:.1f}% (<80%)"],
|
||||
"confidence": CONFIDENCE_MED,
|
||||
"fase": PHASE_GROWTH,
|
||||
}
|
||||
)
|
||||
|
||||
has_specific = any(
|
||||
c["id"] in ("brooding_suhu", "brooding_manajemen", "litter_ventilasi", "pakan_akses")
|
||||
for c in candidates
|
||||
)
|
||||
if mort_status in ("warning", "critical") and not has_specific:
|
||||
bukti = []
|
||||
if mort.get("message"):
|
||||
bukti.append(str(mort["message"]))
|
||||
actual = mort.get("actual_pct")
|
||||
if isinstance(actual, (int, float)):
|
||||
bukti.append(f"Mortalitas kumulatif {float(actual):.2f}%")
|
||||
candidates.append(
|
||||
{
|
||||
"id": "penyakit_biosekuriti",
|
||||
"hipotesis": "Penyakit / biosekuriti",
|
||||
"bukti": bukti
|
||||
or ["Mortalitas di atas ambang CP 707 tanpa sinyal fase/IoT spesifik"],
|
||||
"confidence": CONFIDENCE_LOW,
|
||||
"fase": PHASE_BROODING,
|
||||
}
|
||||
)
|
||||
|
||||
candidates.sort(
|
||||
key=lambda c: (-_CONF_RANK.get(str(c.get("confidence")), 0), str(c.get("id")))
|
||||
)
|
||||
return candidates[:5]
|
||||
|
||||
|
||||
_ACTION_TEMPLATES: dict[str, dict[str, str]] = {
|
||||
"brooding_suhu": {
|
||||
"fase": PHASE_BROODING,
|
||||
"aksi": (
|
||||
"Perketat kontrol suhu brooding hari 1–14 sesuai target CP 707; "
|
||||
"cek heater dan distribusi panas."
|
||||
),
|
||||
"metrik_pantau": "jumlah pembacaan suhu out-of-range; mortalitas harian minggu 1–2",
|
||||
},
|
||||
"brooding_manajemen": {
|
||||
"fase": PHASE_BROODING,
|
||||
"aksi": (
|
||||
"Audit SOP brooding (kepadatan DOC, air minum, pencahayaan) "
|
||||
"sebelum chick-in siklus berikutnya."
|
||||
),
|
||||
"metrik_pantau": "mortalitas kumulatif hari 1–14; keseragaman DOC",
|
||||
},
|
||||
"pakan_akses": {
|
||||
"fase": PHASE_GROWTH,
|
||||
"aksi": (
|
||||
"Pastikan ketersediaan dan akses pakan (feeder space, jadwal isi, "
|
||||
"kualitas fisik pakan) di fase growth."
|
||||
),
|
||||
"metrik_pantau": "FCR harian vs CP 707; bobot rata-rata vs target",
|
||||
},
|
||||
"fcr_tinggi": {
|
||||
"fase": PHASE_GROWTH,
|
||||
"aksi": (
|
||||
"Review program pakan dan cegah waste; bandingkan FCR mingguan dengan standar CP 707."
|
||||
),
|
||||
"metrik_pantau": "FCR akhir minggu; konsumsi karung per 1000 ekor",
|
||||
},
|
||||
"litter_ventilasi": {
|
||||
"fase": PHASE_FINISHER,
|
||||
"aksi": (
|
||||
"Perbaiki manajemen litter dan ventilasi finisher; "
|
||||
"jaga kelembapan 50–70% dan amonia rendah."
|
||||
),
|
||||
"metrik_pantau": "jumlah pembacaan RH/amonia out-of-range; mortalitas minggu 4+",
|
||||
},
|
||||
"kerapatan_feeder": {
|
||||
"fase": PHASE_GROWTH,
|
||||
"aksi": "Sesuaikan kerapatan dan jumlah tempat pakan/minum agar uniformity naik.",
|
||||
"metrik_pantau": "uniformity %; CV bobot",
|
||||
},
|
||||
"penyakit_biosekuriti": {
|
||||
"fase": PHASE_BROODING,
|
||||
"aksi": (
|
||||
"Perketat biosekuriti, siapkan protokol nekropsi jika mortalitas kritis, "
|
||||
"dan tinjau vaksinasi/traffic kandang."
|
||||
),
|
||||
"metrik_pantau": "mortalitas harian; pola kematian per zona kandang",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def build_next_cycle_actions(
|
||||
hypotheses: list[dict[str, Any]] | None,
|
||||
*,
|
||||
limit: int = 3,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Map top hypotheses to phase-grouped next-cycle checklist items."""
|
||||
actions: list[dict[str, Any]] = []
|
||||
seen: set[str] = set()
|
||||
for hyp in hypotheses or []:
|
||||
hid = str(hyp.get("id") or "")
|
||||
template = _ACTION_TEMPLATES.get(hid)
|
||||
if not template or hid in seen:
|
||||
continue
|
||||
seen.add(hid)
|
||||
actions.append(
|
||||
{
|
||||
"id": hid,
|
||||
"fase": template["fase"],
|
||||
"aksi": template["aksi"],
|
||||
"metrik_pantau": template["metrik_pantau"],
|
||||
}
|
||||
)
|
||||
if len(actions) >= limit:
|
||||
break
|
||||
return actions
|
||||
|
||||
|
||||
def sanitize_end_cycle_payload(
|
||||
parsed: dict[str, Any] | None,
|
||||
*,
|
||||
hypotheses: list[dict[str, Any]],
|
||||
actions: list[dict[str, Any]],
|
||||
masalah: list[str],
|
||||
) -> dict[str, Any]:
|
||||
"""Keep LLM wording only when causes stay inside the candidate list."""
|
||||
allowed_ids = {str(h.get("id")) for h in hypotheses if h.get("id")}
|
||||
allowed_labels = {
|
||||
str(h.get("hipotesis")).strip().lower() for h in hypotheses if h.get("hipotesis")
|
||||
}
|
||||
|
||||
raw = parsed if isinstance(parsed, dict) else {}
|
||||
kesimpulan = str(
|
||||
raw.get("kesimpulan") or raw.get("summary") or raw.get("ringkasan") or ""
|
||||
).strip()
|
||||
if not kesimpulan and masalah:
|
||||
kesimpulan = masalah[0]
|
||||
if not kesimpulan:
|
||||
kesimpulan = "Ringkasan akhir siklus tersedia dari graded facts."
|
||||
|
||||
raw_masalah = raw.get("masalah")
|
||||
if isinstance(raw_masalah, list) and raw_masalah:
|
||||
cleaned_masalah = [str(x).strip() for x in raw_masalah if str(x).strip()]
|
||||
else:
|
||||
cleaned_masalah = list(masalah)
|
||||
|
||||
akar: list[dict[str, Any]] = []
|
||||
raw_akar = raw.get("akar_penyebab")
|
||||
if isinstance(raw_akar, list):
|
||||
for item in raw_akar:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
hid = str(item.get("id") or "").strip()
|
||||
label = str(item.get("hipotesis") or "").strip()
|
||||
if hid and hid not in allowed_ids:
|
||||
continue
|
||||
if not hid and label.lower() not in allowed_labels:
|
||||
continue
|
||||
match = next(
|
||||
(
|
||||
h
|
||||
for h in hypotheses
|
||||
if str(h.get("id")) == hid
|
||||
or str(h.get("hipotesis")).lower() == label.lower()
|
||||
),
|
||||
None,
|
||||
)
|
||||
if match is None:
|
||||
continue
|
||||
bukti_raw = item.get("bukti")
|
||||
if isinstance(bukti_raw, list) and bukti_raw:
|
||||
bukti = [str(b).strip() for b in bukti_raw if str(b).strip()]
|
||||
else:
|
||||
bukti = list(match.get("bukti") or [])
|
||||
akar.append(
|
||||
{
|
||||
"id": match.get("id"),
|
||||
"hipotesis": label or match.get("hipotesis"),
|
||||
"bukti": bukti,
|
||||
"confidence": match.get("confidence") or CONFIDENCE_LOW,
|
||||
"fase": match.get("fase"),
|
||||
}
|
||||
)
|
||||
if not akar:
|
||||
akar = [
|
||||
{
|
||||
"id": h.get("id"),
|
||||
"hipotesis": h.get("hipotesis"),
|
||||
"bukti": list(h.get("bukti") or []),
|
||||
"confidence": h.get("confidence"),
|
||||
"fase": h.get("fase"),
|
||||
}
|
||||
for h in hypotheses
|
||||
]
|
||||
|
||||
perbaikan: list[dict[str, Any]] = []
|
||||
raw_fix = raw.get("perbaikan_siklus_berikutnya")
|
||||
allowed_action_ids = {str(a.get("id")) for a in actions if a.get("id")}
|
||||
if isinstance(raw_fix, list):
|
||||
for item in raw_fix:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
aid = str(item.get("id") or "").strip()
|
||||
aksi = str(item.get("aksi") or "").strip()
|
||||
if aid and aid not in allowed_action_ids:
|
||||
continue
|
||||
match = next((a for a in actions if str(a.get("id")) == aid), None)
|
||||
if match is None and aksi:
|
||||
match = next(
|
||||
(a for a in actions if aksi[:40].lower() in str(a.get("aksi") or "").lower()),
|
||||
None,
|
||||
)
|
||||
if match is None:
|
||||
continue
|
||||
perbaikan.append(
|
||||
{
|
||||
"id": match.get("id"),
|
||||
"fase": str(item.get("fase") or match.get("fase")),
|
||||
"aksi": aksi or match.get("aksi"),
|
||||
"metrik_pantau": str(
|
||||
item.get("metrik_pantau") or match.get("metrik_pantau") or ""
|
||||
),
|
||||
}
|
||||
)
|
||||
if not perbaikan:
|
||||
perbaikan = [
|
||||
{
|
||||
"id": a.get("id"),
|
||||
"fase": a.get("fase"),
|
||||
"aksi": a.get("aksi"),
|
||||
"metrik_pantau": a.get("metrik_pantau"),
|
||||
}
|
||||
for a in actions
|
||||
]
|
||||
|
||||
insight = str(raw.get("insight") or "").strip()
|
||||
if not insight and perbaikan:
|
||||
insight = "Prioritaskan perbaikan fase sesuai daftar tindakan siklus berikutnya."
|
||||
|
||||
return {
|
||||
"kesimpulan": kesimpulan,
|
||||
"masalah": cleaned_masalah,
|
||||
"akar_penyebab": akar,
|
||||
"perbaikan_siklus_berikutnya": perbaikan,
|
||||
"insight": insight,
|
||||
}
|
||||
|
||||
|
||||
def format_end_cycle_insight_text(payload: dict[str, Any]) -> str:
|
||||
"""Human-readable body + embedded JSON for the FE structured renderer."""
|
||||
lines: list[str] = []
|
||||
masalah = payload.get("masalah") or []
|
||||
if masalah:
|
||||
lines.append("Masalah:")
|
||||
for item in masalah:
|
||||
lines.append(f"• {item}")
|
||||
lines.append("")
|
||||
|
||||
akar = payload.get("akar_penyebab") or []
|
||||
if akar:
|
||||
lines.append("Akar penyebab:")
|
||||
for item in akar:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
conf = item.get("confidence") or ""
|
||||
label = item.get("hipotesis") or ""
|
||||
lines.append(f"• {label}" + (f" ({conf})" if conf else ""))
|
||||
for bukti in item.get("bukti") or []:
|
||||
lines.append(f" - {bukti}")
|
||||
lines.append("")
|
||||
|
||||
perbaikan = payload.get("perbaikan_siklus_berikutnya") or []
|
||||
if perbaikan:
|
||||
lines.append("Perbaikan siklus berikutnya:")
|
||||
for item in perbaikan:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
fase = item.get("fase") or ""
|
||||
aksi = item.get("aksi") or ""
|
||||
metrik = item.get("metrik_pantau") or ""
|
||||
lines.append(f"• [{fase}] {aksi}")
|
||||
if metrik:
|
||||
lines.append(f" Pantau: {metrik}")
|
||||
lines.append("")
|
||||
|
||||
closing = str(payload.get("insight") or "").strip()
|
||||
if closing:
|
||||
lines.append(closing)
|
||||
|
||||
readable = "\n".join(lines).strip()
|
||||
embedded = json.dumps(payload, ensure_ascii=False, default=str)
|
||||
if readable:
|
||||
return f"{readable}\n\n{END_CYCLE_JSON_MARKER}\n{embedded}"
|
||||
return f"{END_CYCLE_JSON_MARKER}\n{embedded}"
|
||||
|
||||
|
||||
def parse_embedded_end_cycle(text: str) -> dict[str, Any] | None:
|
||||
"""Extract structured end-cycle payload from stored insight_text."""
|
||||
if not text or END_CYCLE_JSON_MARKER not in text:
|
||||
return None
|
||||
raw = text.split(END_CYCLE_JSON_MARKER, 1)[1].strip()
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
return data if isinstance(data, dict) else None
|
||||
|
||||
|
||||
def local_fallback_end_cycle(
|
||||
graded: dict[str, Any] | None,
|
||||
context: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
hypotheses = build_root_cause_hypotheses(graded, context)
|
||||
actions = build_next_cycle_actions(hypotheses)
|
||||
masalah = build_masalah_from_graded(graded)
|
||||
return sanitize_end_cycle_payload(
|
||||
None,
|
||||
hypotheses=hypotheses,
|
||||
actions=actions,
|
||||
masalah=masalah,
|
||||
)
|
||||
Reference in new issue
Block a user