guardia-messenger/hooks/useAIClassify.ts

87 lines
2.8 KiB
TypeScript

import { useEffect, useState } from 'react'
import Constants from 'expo-constants'
/**
* 기능 #11 — Ollama AI 자동 분류 훅 (온프레미스 sLLM, 외부 API 미사용)
* 제목 입력 시 debounce 후 Ollama /api/generate 호출 →
* { category, priority } JSON 파싱하여 입력 필드 자동 제안.
*/
const OLLAMA_HOST: string =
Constants.expoConfig?.extra?.ollamaUrl ?? 'http://localhost:11434'
const OLLAMA_MODEL: string =
Constants.expoConfig?.extra?.ollamaModel ?? 'llama3'
const CATEGORIES = ['DEPLOY', 'RESTART', 'LOG', 'INQUIRY', 'OTHER']
const PRIORITIES = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']
export interface AIClassifyResult {
category: string
priority: string
loading: boolean
error: boolean
}
function extractJson(text: string): { category?: string; priority?: string } | null {
try {
const match = text.match(/\{[\s\S]*\}/)
if (!match) return null
return JSON.parse(match[0])
} catch { return null }
}
export function useAIClassify(title: string): AIClassifyResult {
const [category, setCategory] = useState('')
const [priority, setPriority] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState(false)
useEffect(() => {
const trimmed = title.trim()
if (trimmed.length < 4) {
setCategory(''); setPriority(''); setLoading(false); setError(false)
return
}
let alive = true
setLoading(true); setError(false)
const controller = new AbortController()
const timer = setTimeout(async () => {
const prompt =
`다음 SR 제목의 카테고리와 우선순위를 JSON으로만 답하라. ` +
`category 는 [${CATEGORIES.join(', ')}] 중 하나, ` +
`priority 는 [${PRIORITIES.join(', ')}] 중 하나. ` +
`제목: "${trimmed}"`
try {
const res = await fetch(`${OLLAMA_HOST}/api/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: OLLAMA_MODEL, prompt, stream: false, format: 'json' }),
signal: controller.signal,
})
const data = await res.json()
const parsed = extractJson(data?.response ?? '')
if (!alive) return
if (parsed) {
const cat = (parsed.category ?? '').toUpperCase()
const pri = (parsed.priority ?? '').toUpperCase()
setCategory(CATEGORIES.includes(cat) ? cat : '')
setPriority(PRIORITIES.includes(pri) ? pri : '')
} else {
setError(true)
}
} catch {
if (alive) setError(true)
} finally {
if (alive) setLoading(false)
}
}, 700)
return () => { alive = false; clearTimeout(timer); controller.abort() }
}, [title])
return { category, priority, loading, error }
}