update ai insight to give reasoning for the insight
CI / lint-and-test (push) Canceled after 0s

This commit is contained in:
Alberto-Audrix committed 2026-08-27 14:26:19 +07:00
1 parent 34336b9bd1
commit 333b1cf370
16 files changed
+2079 -130

No files matched your search

+120 -3
View File
@@ -280,6 +280,84 @@ async function fetchLiveIotSensorRange(
throw new Error('Invalid data structure from API');
}
/** One cycle day of environment readings, averaged from every sensor sample. */
export interface DailyEnvironmentPoint {
hari: number;
tanggal: string;
suhu_C: number;
suhuMin_C: number;
suhuMax_C: number;
kelembapan_persen: number;
co2_ppm: number | null;
jumlahSampel: number;
}
/**
* Environment history aggregated per CYCLE DAY, for root-cause analysis.
*
* `fetchLiveIotSensorRange` cannot serve this: it throws away everything older
* than 12 hours and reduces the timestamp to "HH:MM", which is right for the
* live chart and useless for asking why the birds died on day 13. This one
* keeps the API's own `day` field — verified to be the cycle day, and already
* cut at the WIB midnight boundary rather than UTC's.
*/
async function fetchIotDailyEnvironment(
startDate: string,
endDate: string
): Promise<DailyEnvironmentPoint[]> {
const response = await fetch(
`${IOT_API_BASE}/flocks/sensors/range/?start_date=${startDate}&end_date=${endDate}`
);
if (response.status === 404) return [];
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
const json = await response.json();
if (!json?.success || !Array.isArray(json.data)) return [];
const buckets = new Map<
number,
{ tanggal: string; suhu: number[]; rh: number[]; co2: number[] }
>();
for (const item of json.data) {
const hari = Number(item?.day);
const suhu = Number(item?.suhu);
const rh = Number(item?.kelembaban);
// A dead sensor reports 0 for both; averaging those in drags the day's mean
// below the standard and manufactures a "kandang kedinginan" finding.
if (!Number.isFinite(hari) || hari <= 0 || !(suhu > 0) || !(rh > 0)) continue;
const bucket =
buckets.get(hari) ??
(buckets.set(hari, {
tanggal: String(item?.update_at ?? '').slice(0, 10),
suhu: [],
rh: [],
co2: [],
}),
buckets.get(hari)!);
bucket.suhu.push(suhu);
bucket.rh.push(rh);
const co2 = Number(item?.co2);
if (Number.isFinite(co2) && co2 > 0) bucket.co2.push(co2);
}
const mean = (values: number[]) =>
values.length === 0 ? 0 : values.reduce((sum, v) => sum + v, 0) / values.length;
const round1 = (value: number) => Math.round(value * 10) / 10;
return [...buckets.entries()]
.map(([hari, bucket]) => ({
hari,
tanggal: bucket.tanggal,
suhu_C: round1(mean(bucket.suhu)),
suhuMin_C: round1(Math.min(...bucket.suhu)),
suhuMax_C: round1(Math.max(...bucket.suhu)),
kelembapan_persen: round1(mean(bucket.rh)),
co2_ppm: bucket.co2.length > 0 ? round1(mean(bucket.co2)) : null,
jumlahSampel: bucket.suhu.length,
}))
.sort((a, b) => a.hari - b.hari);
}
/**
* Returns mock IoT sensor range data
*/
@@ -675,6 +753,20 @@ export const apiClient = {
}
},
/**
* Environment readings averaged per cycle day, for the AI Insight
* root-cause analysis. DEMO mode has no dated history to aggregate, so it
* returns nothing rather than mock days that would invent correlations.
*/
getIotDailyEnvironment: async (
startDate: string,
endDate: string,
mode: 'LIVE' | 'DEMO'
): Promise<DailyEnvironmentPoint[]> => {
if (mode !== 'LIVE') return [];
return fetchIotDailyEnvironment(startDate, endDate);
},
/**
* Gets feed sack counting data.
* In LIVE mode, throws errors (no fallback to mock).
@@ -1530,8 +1622,24 @@ export const apiClient = {
* @param kandangId - Kandang ID (optional, null means all kandangs)
* @returns Promise<{success: boolean, data?: any, message?: string}>
*/
getAiInsight: async (cycleId: string, kandangId: string | null = null) => {
const queryParams = kandangId ? `?kandangId=${kandangId}` : '';
/**
* Reads a STORED insight. Never generates one.
*
* `reportType`/`reportPeriod` scope the lookup the same way the generate
* endpoint stores it — without them a card asking for "Hari 12" gets whatever
* report happens to be newest for the cycle.
*/
getAiInsight: async (
cycleId: string,
kandangId: string | null = null,
reportType: 'daily' | 'weekly' | 'end_cycle' | null = null,
reportPeriod: string | null = null
) => {
const params = new URLSearchParams();
if (kandangId) params.set('kandangId', kandangId);
if (reportType) params.set('reportType', reportType);
if (reportPeriod) params.set('reportPeriod', reportPeriod);
const queryParams = params.toString() ? `?${params.toString()}` : '';
const response = await fetch(`${DB_API_BASE_URL}/ai-insights/${cycleId}${queryParams}`);
if (response.status === 404) {
@@ -1634,7 +1742,15 @@ export const apiClient = {
temperature = 0.2,
topic = '',
context_query = '',
contextData: Record<string, any> | null = null
contextData: Record<string, any> | null = null,
/**
* Teks insight yang sedang tampil, dikirim HANYA ketika operator menekan
* generate lagi untuk periode data yang sama. Jalur proxy tidak punya
* cache, jadi backend tidak bisa tahu sendiri ini permintaan ulang —
* kehadiran field inilah penandanya. Backend memakainya untuk meminta
* sudut pandang dan tindakan yang berbeda, dengan angka yang tetap sama.
*/
previousInsight: string | null = null
) => {
const response = await fetch(`${DB_API_BASE_URL}/ai-insights/proxy`, {
method: 'POST',
@@ -1646,6 +1762,7 @@ export const apiClient = {
topic,
context_query,
contextData,
previous_insight: previousInsight,
}),
});