Files
ims/app/api/incidents/[id]/ai/rca-draft/route.ts
T
adminandClaude Sonnet 4.6 25f923f530 feat(db): phase 4 group 1 — lib/ settings + notifications to Drizzle
Convert lib/settings.ts, lib/notifications/{in-app,email,capa-escalation,
effectiveness-recheck}.ts from Supabase PostgREST to Drizzle asAdmin queries.
Drop supabase arg from all call sites in app/api/ and cron routes. Rewrite
notification unit tests to mock @/lib/db/with-user instead of SupabaseClient.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 16:37:51 +08:00

146 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export const dynamic = 'force-dynamic'
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import { getSession } from '@/lib/auth/get-session'
import { createDeepSeekClient } from '@/lib/claude/client'
import { getApiKey } from '@/lib/settings'
export async function POST(
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
const session = await getSession()
if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
if (!['hse', 'admin'].includes(session.role))
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
const supabase = await createClient()
const since = new Date(Date.now() - 60_000).toISOString()
const { count: recentCount } = await supabase
.from('audit_log')
.select('id', { count: 'exact', head: true })
.eq('changed_by', session.sub)
.eq('action', 'ai_rca_draft')
.gte('changed_at', since)
if ((recentCount ?? 0) > 0)
return NextResponse.json({ error: 'Rate limited — please wait 60 seconds' }, { status: 429 })
const deepseekKey = await getApiKey('DEEPSEEK_API_KEY')
const client = createDeepSeekClient(deepseekKey)
const { data: incident } = await supabase
.from('incidents')
.select(`
id, incident_type, description, severity, injury_involved, medical_status,
is_fatality, is_serious_bodily_injury, triage_notes,
sites (name), zones (name)
`)
.eq('id', id)
.single()
if (!incident) return NextResponse.json({ error: 'Not found' }, { status: 404 })
const inc = incident as {
incident_type: string
description: string
severity: number | null
injury_involved: boolean
medical_status: string | null
is_fatality: boolean
is_serious_bodily_injury: boolean
triage_notes: string | null
}
const siteName = (incident.sites as unknown as { name: string } | null)?.name ?? 'Unknown'
const zoneName = (incident.zones as unknown as { name: string } | null)?.name ?? 'Unknown'
let res: Awaited<ReturnType<typeof client.chat.completions.create>>
try {
res = await client.chat.completions.create({
model: 'deepseek-chat',
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'],
},
},
}],
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: ${siteName}
Zone: ${zoneName}
Incident type: ${inc.incident_type}
Description: ${inc.description}
Severity: ${inc.severity ?? 'not yet assigned'}/5
Injury involved: ${inc.injury_involved ? `yes — ${inc.medical_status}` : 'no'}
Fatality: ${inc.is_fatality ? 'yes' : 'no'}
Serious bodily injury: ${inc.is_serious_bodily_injury ? 'yes' : 'no'}
Triage notes: ${inc.triage_notes ?? 'none'}
Provide 35 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.`,
}],
})
} 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 })
let draft: { five_why_steps?: unknown; root_cause_summary?: unknown; capa_suggestions?: unknown }
try { draft = JSON.parse(call.function.arguments) }
catch { return NextResponse.json({ error: 'AI returned unexpected structure' }, { status: 500 }) }
if (
!Array.isArray(draft.five_why_steps) ||
typeof draft.root_cause_summary !== 'string' ||
!Array.isArray(draft.capa_suggestions)
) {
return NextResponse.json({ error: 'AI returned unexpected structure' }, { status: 500 })
}
await supabase.rpc('write_audit_log', {
p_table_name: 'incidents',
p_record_id: id,
p_action: 'ai_rca_draft',
p_new_value: {
root_cause_summary: draft.root_cause_summary,
model: 'deepseek-chat',
} as never,
})
return NextResponse.json(draft)
}