update ai insight root cause
This commit is contained in:
1 parent
7e8c06d36f
commit
15ebdcce78
7 files changed
+297
-48
No files matched your search
@@ -478,3 +478,92 @@ class ErdPrimaryKeyTests(TestCase):
|
||||
live_pks = [row[1] for row in cursor.fetchall() if row[5]]
|
||||
self.assertEqual(live_pks, [pk_name], table)
|
||||
|
||||
|
||||
class AIInsightAPITests(TestCase):
|
||||
def setUp(self):
|
||||
import json
|
||||
self.user = User.objects.create_user(user_name="insight_tester", password="secret")
|
||||
self.client = APIClient()
|
||||
self.client.force_authenticate(self.user)
|
||||
site = _linked_site(self.user, site_name="Site A", code="site-a")
|
||||
self.kandang = Kandang.objects.create(kandang_name="Kandang 01", site=site)
|
||||
self.cycle = Cycle.objects.create(
|
||||
kandang=self.kandang,
|
||||
total_days=35,
|
||||
start_date=date.today() - timedelta(days=5),
|
||||
doc_in_weight=40,
|
||||
doc_in_count=10000,
|
||||
status=Cycle.STATUS_ACTIVE,
|
||||
)
|
||||
|
||||
def test_cached_extracts_akar_masalah_from_daily_json(self):
|
||||
import json
|
||||
from apps.operations.models import AIInsight
|
||||
|
||||
clean_insight = "Lakukan penyesuaian tirai kandang dan ventilasi minimal."
|
||||
akar_masalah = "Suhu brooding terlalu dingin di malam hari (26C vs 32C)."
|
||||
daily_json = json.dumps({"akar_masalah": akar_masalah, "insight": clean_insight}, ensure_ascii=False)
|
||||
stored_text = f"{clean_insight}\n\n<!--daily_json-->\n{daily_json}"
|
||||
|
||||
AIInsight.objects.create(
|
||||
cycle=self.cycle,
|
||||
kandang=self.kandang,
|
||||
topic="fcr",
|
||||
report_type="page",
|
||||
report_period="current",
|
||||
date=date.today(),
|
||||
summary="FCR tinggi pada hari ke-7.",
|
||||
insight_text=stored_text,
|
||||
alert="warning",
|
||||
)
|
||||
|
||||
response = self.client.get(
|
||||
"/api/v1/ai-insights/cached/",
|
||||
{
|
||||
"cycle_id": self.cycle.pk,
|
||||
"kandang_id": self.kandang.pk,
|
||||
"topic": "fcr",
|
||||
"report_type": "page",
|
||||
"report_period": "current",
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(response.data["success"])
|
||||
self.assertEqual(response.data["summary"], "FCR tinggi pada hari ke-7.")
|
||||
self.assertEqual(response.data["akar_masalah"], akar_masalah)
|
||||
self.assertEqual(response.data["insight"], clean_insight)
|
||||
self.assertIn(akar_masalah, response.data["insight_text"])
|
||||
self.assertIn(clean_insight, response.data["insight_text"])
|
||||
|
||||
def test_cached_legacy_fallback_without_akar_masalah(self):
|
||||
from apps.operations.models import AIInsight
|
||||
|
||||
AIInsight.objects.create(
|
||||
cycle=self.cycle,
|
||||
kandang=self.kandang,
|
||||
topic="fcr",
|
||||
report_type="page",
|
||||
report_period="current",
|
||||
date=date.today(),
|
||||
summary="Data normal.",
|
||||
insight_text="Pertahankan SOP saat ini.",
|
||||
alert="healthy",
|
||||
)
|
||||
|
||||
response = self.client.get(
|
||||
"/api/v1/ai-insights/cached/",
|
||||
{
|
||||
"cycle_id": self.cycle.pk,
|
||||
"kandang_id": self.kandang.pk,
|
||||
"topic": "fcr",
|
||||
"report_type": "page",
|
||||
"report_period": "current",
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(response.data["success"])
|
||||
self.assertEqual(response.data["summary"], "Data normal.")
|
||||
self.assertEqual(response.data["akar_masalah"], "")
|
||||
self.assertEqual(response.data["insight"], "Pertahankan SOP saat ini.")
|
||||
|
||||
|
||||
@@ -254,7 +254,6 @@ class AIInsightViewSet(CycleScopedViewSet):
|
||||
|
||||
@action(detail=False, methods=["get"], url_path="cached")
|
||||
def cached(self, request):
|
||||
import json
|
||||
params = request.query_params
|
||||
cycle_id = params.get("cycle_id")
|
||||
kandang_id = params.get("kandang_id")
|
||||
@@ -280,27 +279,7 @@ class AIInsightViewSet(CycleScopedViewSet):
|
||||
if row is None:
|
||||
return Response({"detail": "No cached insight."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
raw_cites = row.citations
|
||||
if isinstance(raw_cites, list):
|
||||
cites = raw_cites
|
||||
elif isinstance(raw_cites, str) and raw_cites.strip():
|
||||
try:
|
||||
cites = json.loads(raw_cites)
|
||||
except Exception:
|
||||
cites = []
|
||||
else:
|
||||
cites = []
|
||||
|
||||
return Response({
|
||||
"success": True,
|
||||
"insight_text": row.insight_text,
|
||||
"summary": row.summary,
|
||||
"alert": row.alert,
|
||||
"citations": cites,
|
||||
"topic": row.topic,
|
||||
"report_type": row.report_type,
|
||||
"report_period": row.report_period,
|
||||
})
|
||||
return Response(self._serialize_row(row))
|
||||
|
||||
@action(detail=False, methods=["post"], url_path="generate")
|
||||
def generate(self, request):
|
||||
@@ -327,27 +306,74 @@ class AIInsightViewSet(CycleScopedViewSet):
|
||||
.first()
|
||||
)
|
||||
if row is not None:
|
||||
import json
|
||||
raw_cites = row.citations
|
||||
try:
|
||||
cites = json.loads(raw_cites) if isinstance(raw_cites, str) else (raw_cites or [])
|
||||
except Exception:
|
||||
cites = []
|
||||
return Response({
|
||||
"success": True,
|
||||
"insight_text": row.insight_text,
|
||||
"summary": row.summary,
|
||||
"alert": row.alert,
|
||||
"citations": cites,
|
||||
"topic": row.topic,
|
||||
"report_type": row.report_type,
|
||||
"report_period": row.report_period,
|
||||
})
|
||||
return Response(self._serialize_row(row))
|
||||
return Response(
|
||||
{"detail": "Insight belum tersedia pada mirror HQ. Pastikan insight telah di-generate pada site dashboard dan disinkronkan."},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
def _serialize_row(self, row: AIInsight) -> dict:
|
||||
import json
|
||||
|
||||
raw_cites = row.citations
|
||||
if isinstance(raw_cites, list):
|
||||
cites = raw_cites
|
||||
elif isinstance(raw_cites, str) and raw_cites.strip():
|
||||
try:
|
||||
cites = json.loads(raw_cites)
|
||||
except Exception:
|
||||
cites = []
|
||||
else:
|
||||
cites = []
|
||||
|
||||
insight_raw = row.insight_text or ""
|
||||
akar_masalah = ""
|
||||
insight_body = insight_raw
|
||||
structured_end_cycle = None
|
||||
|
||||
if "<!--daily_json-->" in insight_raw:
|
||||
raw = insight_raw.split("<!--daily_json-->", 1)[1].strip()
|
||||
try:
|
||||
embedded = json.loads(raw)
|
||||
if isinstance(embedded, dict):
|
||||
akar_masalah = str(embedded.get("akar_masalah") or "").strip()
|
||||
insight_body = str(
|
||||
embedded.get("insight") or insight_raw.split("<!--daily_json-->", 1)[0]
|
||||
).strip()
|
||||
except Exception:
|
||||
insight_body = insight_raw.split("<!--daily_json-->", 1)[0].strip()
|
||||
elif "<!--end_cycle_json-->" in insight_raw:
|
||||
raw = insight_raw.split("<!--end_cycle_json-->", 1)[1].strip()
|
||||
try:
|
||||
structured_end_cycle = json.loads(raw)
|
||||
except Exception:
|
||||
pass
|
||||
insight_body = insight_raw.split("<!--end_cycle_json-->", 1)[0].strip()
|
||||
|
||||
parts = [row.summary]
|
||||
if akar_masalah:
|
||||
parts.append(akar_masalah)
|
||||
if insight_body:
|
||||
parts.append(insight_body)
|
||||
full_text = "\n\n".join(p for p in parts if p) or insight_body or ""
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"id": row.pk,
|
||||
"cycle": row.cycle_id,
|
||||
"kandang": row.kandang_id,
|
||||
"topic": row.topic,
|
||||
"report_type": row.report_type,
|
||||
"report_period": row.report_period,
|
||||
"summary": row.summary,
|
||||
"akar_masalah": akar_masalah,
|
||||
"insight": insight_body,
|
||||
"insight_text": full_text,
|
||||
"structured_end_cycle": structured_end_cycle,
|
||||
"alert": row.alert,
|
||||
"citations": cites,
|
||||
}
|
||||
|
||||
|
||||
class HealthView(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
Reference in new issue
Block a user