Files
ims/app/api/incidents/[id]/ai/rca-draft/route.ts
T
adminandClaude 7bf3b6a409 fix: rename deepseek model from deepseek-chat to deepseek-v4-pro
DeepSeek API deprecated deepseek-chat model. Returns error:
'the supported API model names are deepseek-v4-pro or deepseek-v4-flash'

Affected all 4 AI endpoints:
- risk-flags (90-day dashboard)
- triage-suggest
- rca-draft
- quality-check

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-26 10:17:26 +08:00

144 lines
5.4 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 { getSession } from '@/lib/auth/get-session'
import { withUser, asAdmin } from '@/lib/db/with-user'
import { writeAuditLog } from '@/lib/db/audit'
import { incidents, auditLog, sites, zones } from '@/lib/db/schema'
import { eq, and, gte, sql } from 'drizzle-orm'
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 })
// Rate limit
const since = new Date(Date.now() - 60_000)
const [rateRow] = await asAdmin(db =>
db.select({ cnt: sql<number>`count(*)` }).from(auditLog)
.where(and(
eq(auditLog.changedBy, session.sub),
eq(auditLog.action, 'ai_rca_draft'),
gte(auditLog.changedAt, since),
))
)
if (Number(rateRow?.cnt ?? 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 [incident] = await withUser(session.sub, async tx =>
tx.select({
incidentType: incidents.incidentType,
description: incidents.description,
severity: incidents.severity,
injuryInvolved: incidents.injuryInvolved,
medicalStatus: incidents.medicalStatus,
isFatality: incidents.isFatality,
isSeriousBodilyInjury: incidents.isSeriousBodilyInjury,
triageNotes: incidents.triageNotes,
siteName: sites.name,
zoneName: zones.name,
})
.from(incidents)
.leftJoin(sites, eq(incidents.siteId, sites.id))
.leftJoin(zones, eq(incidents.zoneId, zones.id))
.where(eq(incidents.id, id))
.limit(1)
)
if (!incident) return NextResponse.json({ error: 'Not found' }, { status: 404 })
let res: Awaited<ReturnType<typeof client.chat.completions.create>>
try {
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'],
},
},
}],
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 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 withUser(session.sub, async tx => {
await writeAuditLog(tx, 'incidents', id, 'ai_rca_draft', {
root_cause_summary: draft.root_cause_summary as string,
model: 'deepseek-v4-pro',
})
})
return NextResponse.json(draft)
}