diff --git a/app/api/dashboard/ai/risk-flags/route.ts b/app/api/dashboard/ai/risk-flags/route.ts
index c9b3f1e..f5a99d2 100644
--- a/app/api/dashboard/ai/risk-flags/route.ts
+++ b/app/api/dashboard/ai/risk-flags/route.ts
@@ -111,56 +111,46 @@ export async function POST() {
res = await client.chat.completions.create({
model: 'deepseek-v4-pro',
max_tokens: 2048,
- tools: [{
- type: 'function',
- function: {
- name: 'flag_rising_risk',
- description: 'Flag warehouse zones showing rising safety risk from 90-day incident aggregates',
- parameters: {
- type: 'object',
- properties: {
- flags: {
- type: 'array',
- items: {
- type: 'object',
- properties: {
- zone: { type: 'string' },
- site: { type: 'string' },
- risk_level: { type: 'string', enum: ['low', 'medium', 'high'] },
- rationale: { type: 'string', description: 'One or two sentences citing the numbers' },
- recommended_action: { type: 'string', description: 'One concrete preventive action' },
- },
- required: ['zone', 'site', 'risk_level', 'rationale', 'recommended_action'],
- },
- },
- summary: { type: 'string', description: 'Two-sentence overall risk picture' },
- },
- required: ['flags', 'summary'],
- },
+ messages: [
+ {
+ role: 'system',
+ content: `You are an HSE risk analyst for a Malaysian 3PL warehouse operator. You must respond with valid JSON only — no markdown, no explanation outside the JSON object.
+
+Output exactly this JSON structure:
+{
+ "flags": [
+ {
+ "zone": "string",
+ "site": "string",
+ "risk_level": "low" | "medium" | "high",
+ "rationale": "One or two sentences citing the numbers",
+ "recommended_action": "One concrete preventive action"
+ }
+ ],
+ "summary": "Two-sentence overall risk picture"
+}
+
+Flag only zones with rising or elevated risk (at most 5 flags; do not flag healthy zones). Base every rationale strictly on the numbers given. "first_half" is incidents in days 90-46, "second_half" is days 45-0 — a rising second_half means worsening trend. Near-miss and hazard reports are leading indicators; injuries are lagging.`,
},
- }],
- tool_choice: { type: 'function', function: { name: 'flag_rising_risk' } },
- messages: [{
- role: 'user',
- content: `You are an HSE risk analyst for a Malaysian 3PL warehouse operator. Below are 90-day incident aggregates per zone. "first_half" is incidents in days 90-46, "second_half" is days 45-0 — a rising second_half means worsening trend. Near-miss and hazard reports are leading indicators; injuries are lagging.
-
-Flag zones with rising or elevated risk (at most 5 flags; do not flag healthy zones). Base every rationale strictly on the numbers given.
-
-
-${JSON.stringify(aggregates, null, 2)}
-`,
- }],
+ {
+ role: 'user',
+ content: `Below are 90-day incident aggregates per zone. Respond with JSON only.\n\n\n${JSON.stringify(aggregates, null, 2)}\n`,
+ },
+ ],
})
} catch (err) {
console.error('DeepSeek risk-flags error:', err)
return NextResponse.json({ error: 'AI service unavailable' }, { status: 503 })
}
- const call = res.choices[0]?.message?.tool_calls?.[0]
- if (!call || call.type !== 'function') return NextResponse.json({ error: 'AI suggestion failed' }, { status: 500 })
+ const raw = res.choices[0]?.message?.content
+ if (!raw) return NextResponse.json({ error: 'AI returned empty response' }, { status: 500 })
+
+ // Strip markdown code fences if present
+ const json = raw.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '').trim()
let input: { flags?: unknown; summary?: unknown }
- try { input = JSON.parse(call.function.arguments) }
+ try { input = JSON.parse(json) }
catch { return NextResponse.json({ error: 'AI returned unexpected structure' }, { status: 500 }) }
if (!Array.isArray(input.flags) || typeof input.summary !== 'string')
diff --git a/app/api/incidents/[id]/ai/rca-draft/route.ts b/app/api/incidents/[id]/ai/rca-draft/route.ts
index 45901a2..24e32af 100644
--- a/app/api/incidents/[id]/ai/rca-draft/route.ts
+++ b/app/api/incidents/[id]/ai/rca-draft/route.ts
@@ -61,67 +61,39 @@ export async function POST(
res = await client.chat.completions.create({
model: 'deepseek-v4-pro',
max_tokens: 2048,
- tools: [{
- type: 'function',
- function: {
- name: 'draft_rca',
- description: 'Draft a 5-Why root cause analysis and CAPA suggestions for an HSE incident',
- parameters: {
- type: 'object',
- properties: {
- five_why_steps: {
- type: 'array',
- items: {
- type: 'object',
- properties: {
- why: { type: 'string', description: 'The why question' },
- answer: { type: 'string', description: 'The finding or answer' },
- },
- required: ['why', 'answer'],
- },
- description: '3 to 5 why steps',
- },
- root_cause_summary: {
- type: 'string',
- description: 'One-sentence root cause statement',
- },
- capa_suggestions: {
- type: 'array',
- items: { type: 'string' },
- description: 'Up to 3 corrective/preventive action suggestions',
- },
- },
- required: ['five_why_steps', 'root_cause_summary', 'capa_suggestions'],
- },
+ messages: [
+ {
+ role: 'system',
+ content: `You are an experienced HSE investigator for a Malaysian 3PL warehouse. Draft a 5-Why root cause analysis for incidents. You must respond with valid JSON only — no markdown, no explanation outside the JSON object.
+
+Output exactly this JSON structure:
+{
+ "five_why_steps": [
+ { "why": "", "answer": "" }
+ ],
+ "root_cause_summary": "",
+ "capa_suggestions": [""]
+}
+
+Provide 3–5 Why steps drilling from immediate cause to root cause. Frame CAPA suggestions appropriate for a Malaysian warehouse context.`,
},
- }],
- tool_choice: { type: 'function', function: { name: 'draft_rca' } },
- messages: [{
- role: 'user',
- content: `You are an experienced HSE investigator for a Malaysian 3PL warehouse. Draft a 5-Why root cause analysis for this incident.
-
-Site: ${incident.siteName ?? 'Unknown'}
-Zone: ${incident.zoneName ?? 'Unknown'}
-Incident type: ${incident.incidentType}
-Description: ${incident.description}
-Severity: ${incident.severity ?? 'not yet assigned'}/5
-Injury involved: ${incident.injuryInvolved ? `yes — ${incident.medicalStatus}` : 'no'}
-Fatality: ${incident.isFatality ? 'yes' : 'no'}
-Serious bodily injury: ${incident.isSeriousBodilyInjury ? 'yes' : 'no'}
-Triage notes: ${incident.triageNotes ?? 'none'}
-
-Provide 3–5 Why steps drilling from immediate cause to root cause. Give a one-sentence root cause statement. Suggest 3 corrective/preventive actions appropriate for a Malaysian warehouse context.`,
- }],
+ {
+ role: 'user',
+ content: `Draft a 5-Why root cause analysis for this incident. Respond with JSON only.\n\nSite: ${incident.siteName ?? 'Unknown'}\nZone: ${incident.zoneName ?? 'Unknown'}\nIncident type: ${incident.incidentType}\nDescription: ${incident.description}\nSeverity: ${incident.severity ?? 'not yet assigned'}/5\nInjury involved: ${incident.injuryInvolved ? `yes — ${incident.medicalStatus}` : 'no'}\nFatality: ${incident.isFatality ? 'yes' : 'no'}\nSerious bodily injury: ${incident.isSeriousBodilyInjury ? 'yes' : 'no'}\nTriage notes: ${incident.triageNotes ?? 'none'}`,
+ },
+ ],
})
} catch {
return NextResponse.json({ error: 'AI service unavailable' }, { status: 503 })
}
- const call = res.choices[0]?.message?.tool_calls?.[0]
- if (!call || call.type !== 'function') return NextResponse.json({ error: 'AI draft failed' }, { status: 500 })
+ const raw = res.choices[0]?.message?.content
+ if (!raw) return NextResponse.json({ error: 'AI returned empty response' }, { status: 500 })
+
+ const json = raw.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '').trim()
let draft: { five_why_steps?: unknown; root_cause_summary?: unknown; capa_suggestions?: unknown }
- try { draft = JSON.parse(call.function.arguments) }
+ try { draft = JSON.parse(json) }
catch { return NextResponse.json({ error: 'AI returned unexpected structure' }, { status: 500 }) }
if (
diff --git a/app/api/incidents/[id]/ai/triage-suggest/route.ts b/app/api/incidents/[id]/ai/triage-suggest/route.ts
index ffa05a8..2cae6ce 100644
--- a/app/api/incidents/[id]/ai/triage-suggest/route.ts
+++ b/app/api/incidents/[id]/ai/triage-suggest/route.ts
@@ -52,48 +52,35 @@ export async function POST(
res = await client.chat.completions.create({
model: 'deepseek-v4-pro',
max_tokens: 1024,
- tools: [{
- type: 'function',
- function: {
- name: 'suggest_triage',
- description: 'Suggest severity rating and NADOPOD 2004 DOSH classification for a warehouse incident',
- parameters: {
- type: 'object',
- properties: {
- severity: { type: 'number', description: '1=minor, 2=low, 3=moderate, 4=serious, 5=critical/fatality' },
- is_fatality: { type: 'boolean' },
- is_serious_bodily_injury: { type: 'boolean', description: 'Fracture, amputation, blindness, serious burn, or similar' },
- is_dangerous_occurrence: { type: 'boolean', description: 'Structural collapse, explosion, fire, scaffold collapse, etc.' },
- is_occupational_disease: { type: 'boolean', description: 'Disease arising from workplace exposure' },
- rationale: { type: 'string', description: 'One-sentence rationale citing NADOPOD 2004 where applicable' },
- },
- required: [
- 'severity', 'is_fatality', 'is_serious_bodily_injury',
- 'is_dangerous_occurrence', 'is_occupational_disease', 'rationale',
- ],
- },
+ messages: [
+ {
+ role: 'system',
+ content: `You are an HSE triage specialist for a Malaysian 3PL warehouse. Assess incidents under NADOPOD 2004. You must respond with valid JSON only — no markdown, no explanation outside the JSON object.
+
+Output exactly this JSON structure:
+{
+ "severity": ,
+ "is_fatality": ,
+ "is_serious_bodily_injury": ,
+ "is_dangerous_occurrence": ,
+ "is_occupational_disease": ,
+ "rationale": ""
+}`,
},
- }],
- tool_choice: { type: 'function', function: { name: 'suggest_triage' } },
- messages: [{
- role: 'user',
- content: `You are an HSE triage specialist for a Malaysian 3PL warehouse. Assess this incident under NADOPOD 2004.
-
-Incident type: ${incident.incidentType}
-Description: ${incident.description}
-Injury involved: ${incident.injuryInvolved ? 'yes' : 'no'}
-Medical status: ${incident.medicalStatus ?? 'N/A'}
-Asset/equipment involved: ${incident.assetInvolved ? 'yes' : 'no'}
-
-Suggest severity (1–5) and tick the appropriate NADOPOD 2004 flags. Give a one-sentence rationale.`,
- }],
+ {
+ role: 'user',
+ content: `Assess this incident under NADOPOD 2004. Respond with JSON only.\n\nIncident type: ${incident.incidentType}\nDescription: ${incident.description}\nInjury involved: ${incident.injuryInvolved ? 'yes' : 'no'}\nMedical status: ${incident.medicalStatus ?? 'N/A'}\nAsset/equipment involved: ${incident.assetInvolved ? 'yes' : 'no'}`,
+ },
+ ],
})
} catch {
return NextResponse.json({ error: 'AI service unavailable' }, { status: 503 })
}
- const call = res.choices[0]?.message?.tool_calls?.[0]
- if (!call || call.type !== 'function') return NextResponse.json({ error: 'AI suggestion failed' }, { status: 500 })
+ const raw = res.choices[0]?.message?.content
+ if (!raw) return NextResponse.json({ error: 'AI returned empty response' }, { status: 500 })
+
+ const json = raw.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '').trim()
let input: {
severity?: unknown
@@ -103,7 +90,7 @@ Suggest severity (1–5) and tick the appropriate NADOPOD 2004 flags. Give a one
is_occupational_disease?: unknown
rationale?: unknown
}
- try { input = JSON.parse(call.function.arguments) }
+ try { input = JSON.parse(json) }
catch { return NextResponse.json({ error: 'AI returned unexpected structure' }, { status: 500 }) }
if (
diff --git a/app/api/incidents/ai/quality-check/route.ts b/app/api/incidents/ai/quality-check/route.ts
index 67fc5f2..b608351 100644
--- a/app/api/incidents/ai/quality-check/route.ts
+++ b/app/api/incidents/ai/quality-check/route.ts
@@ -44,47 +44,38 @@ export async function POST(request: NextRequest) {
res = await client.chat.completions.create({
model: 'deepseek-v4-pro',
max_tokens: 1024,
- tools: [{
- type: 'function',
- function: {
- name: 'assess_quality',
- description: 'Assess HSE incident report description quality',
- parameters: {
- type: 'object',
- properties: {
- score: { type: 'number', description: '1-10 quality score' },
- passes: { type: 'boolean', description: 'True when score is 6 or above' },
- feedback: { type: 'string', description: 'One-sentence quality summary' },
- suggestions: {
- type: 'array',
- items: { type: 'string' },
- description: 'Up to 3 concrete suggestions to improve the description',
- },
- },
- required: ['score', 'passes', 'feedback', 'suggestions'],
- },
+ messages: [
+ {
+ role: 'system',
+ content: `You are an HSE reporting assistant for a Malaysian 3PL warehouse. You must respond with valid JSON only — no markdown, no explanation outside the JSON object.
+
+Output exactly this JSON structure:
+{
+ "score": ,
+ "passes": = 6>,
+ "feedback": "",
+ "suggestions": ["= 6>"]
+}
+
+Scoring criteria: specificity (location, time, persons involved), completeness (what happened + immediate actions), clarity. Score 6 or above passes.`,
},
- }],
- tool_choice: { type: 'function', function: { name: 'assess_quality' } },
- messages: [{
- role: 'user',
- content: `You are an HSE reporting assistant for a Malaysian 3PL warehouse. Assess this incident report description.
-
-Incident type: ${body.incident_type}
-Description: ${body.description}
-
-Score 1–10 based on: specificity (location, time, persons involved), completeness (what happened + immediate actions), and clarity. Score 6 or above passes. If score is below 6, give up to 3 actionable suggestions.`,
- }],
+ {
+ role: 'user',
+ content: `Assess this incident report description. Respond with JSON only.\n\nIncident type: ${body.incident_type}\nDescription: ${body.description}`,
+ },
+ ],
})
} catch {
return NextResponse.json({ error: 'AI service unavailable' }, { status: 503 })
}
- const call = res.choices[0]?.message?.tool_calls?.[0]
- if (!call || call.type !== 'function') return NextResponse.json({ error: 'AI assessment failed' }, { status: 500 })
+ const raw = res.choices[0]?.message?.content
+ if (!raw) return NextResponse.json({ error: 'AI returned empty response' }, { status: 500 })
+
+ const json = raw.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '').trim()
let input: { score?: unknown; passes?: unknown; feedback?: unknown; suggestions?: unknown }
- try { input = JSON.parse(call.function.arguments) }
+ try { input = JSON.parse(json) }
catch { return NextResponse.json({ error: 'AI returned unexpected structure' }, { status: 500 }) }
if (
@@ -96,7 +87,6 @@ Score 1–10 based on: specificity (location, time, persons involved), completen
return NextResponse.json({ error: 'AI returned unexpected structure' }, { status: 500 })
}
- // audit write uses session.sub as a pseudo record-id (no incident_id at this stage)
await withUser(session.sub, async tx => {
await writeAuditLog(tx, 'incidents', session.sub, 'ai_quality_check', {
score: input.score, passes: input.passes, model: 'deepseek-v4-pro',