87 lines
2.8 KiB
TypeScript
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 }
|
|
}
|