zioinfo-mail/workspace/guardia-itsm/core/nlu.py
DESKTOP-TKLFCPR\ython cfe2901a55 refactor(structure): consolidate all projects under workspace/
- itsm/    -> workspace/guardia-itsm/
- manager/ -> workspace/guardia-manager/
- app/     -> workspace/guardia-messenger/
- manual/  -> workspace/guardia-docs/

workspace/zioinfo-web/ unchanged.
git mv preserves full commit history.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 23:50:56 +09:00

184 lines
6.1 KiB
Python

"""
GUARDiA NLU Engine — 순수 규칙 기반 자연어 이해 (외부 API 없음).
처리 흐름:
1. 정규식 패턴 매칭으로 인텐트(의도) 분류
2. 키워드/패턴으로 엔티티 추출
3. (sr_id, engineer, status, sr_type, inst_code, priority, keyword)
"""
import re
from dataclasses import dataclass, field
from typing import Optional
# ── 인텐트 정의 ───────────────────────────────────────────────────────────────
INTENT_PATTERNS: list[tuple[str, list[str]]] = [
# 우선순위 높은 인텐트부터
# SR 작업 시뮬레이션 실행
("SIMULATE", [
r"(시뮬레이션|simulate|작업\s*실행|자동\s*실행|AI\s*실행)",
r"(실행|run).*(SR|작업)",
]),
# 담당자 배정·변경
("ASSIGN_ENGINEER", [
r"담당자.*(변경|바꿔|배정|교체|지정)",
r"(배정|assign).*(engineer|엔지니어)",
r"engineer\d+.*(맡겨|담당|배정|지정)",
r"(자동\s*배정|auto\s*assign)",
]),
# 승인
("APPROVE_SR", [
r"(승인|approve)\s*(해줘|처리|완료|부탁)",
r"결재\s*(해줘|처리)",
]),
# 반려
("REJECT_SR", [
r"(반려|거절|reject)\s*(해줘|처리|할게)",
]),
# KB 검색
("SEARCH_KB", [
r"(KB|기술\s*문서|지식|매뉴얼).*(검색|찾아|조회)",
r"(검색|찾아줘|알려줘).*(오류|error|fault|장애|문제)",
r"(해결\s*방법|원인|솔루션).*(알려줘|찾아줘)",
r"(oom|outofmemory|connection\s*pool|ssl|nginx|502|undo|gc\s*overhead|디스크)",
]),
# 워크로드 조회
("QUERY_WORKLOAD", [
r"(워크로드|workload|작업\s*현황|담당\s*현황)",
r"engineer.*(몇\s*건|현황|상황|바빠)",
r"(엔지니어|engineer).*(상황|현황|보여줘)",
]),
# SR 상세 조회
("QUERY_SR_DETAIL", [
r"SR-\d{8}-[A-Z0-9]{6}.*(상태|정보|내용|어떻게|확인)",
r"(상태|정보|현황).*(알려줘|보여줘|확인).*(SR|서비스)",
]),
# SR 목록 조회
("QUERY_SR_LIST", [
r"(목록|리스트|list|조회|보여줘).*(SR|서비스|요청)",
r"(SR|서비스\s*요청).*(목록|있어|있나|몇\s*건|현황|리스트)",
r"(승인\s*대기|진행\s*중|완료|긴급|접수).*(SR|서비스|있어|있나|보여)",
r"(어떤|무슨)\s*SR",
]),
# 통계/요약
("QUERY_STATS", [
r"(통계|요약|summary|stats|현황\s*요약|전체\s*현황)",
r"(총|전체).*(SR|건수|몇\s*건)",
r"(오늘|이번\s*주).*(SR|처리|완료)",
]),
]
# ── 엔티티 추출 ───────────────────────────────────────────────────────────────
_SR_ID_RE = re.compile(r'SR-\d{8}-[A-Z0-9]{6}', re.IGNORECASE)
_ENG_USER_RE = re.compile(r'engineer\d+', re.IGNORECASE)
_PRIORITY_MAP = {"긴급": "CRITICAL", "높음": "HIGH", "보통": "MEDIUM", "낮음": "LOW",
"critical": "CRITICAL", "high": "HIGH", "medium": "MEDIUM", "low": "LOW"}
_STATUS_MAP = {
"승인": "APPROVED", "반려": "REJECTED", "완료": "COMPLETED",
"진행": "IN_PROGRESS", "접수": "RECEIVED", "대기": "PENDING_APPROVAL",
}
_TYPE_MAP = {
"배포": "DEPLOY", "deploy": "DEPLOY",
"재기동": "RESTART", "restart": "RESTART",
"로그": "LOG", "log": "LOG",
"문의": "INQUIRY", "inquiry": "INQUIRY",
}
_INST_CODES = ["MOF", "MOIS", "MSS"]
_ENG_ALIAS = {"김엔지니어": "engineer1", "이엔지니어": "engineer2"}
@dataclass
class ParseResult:
intent: str = "UNKNOWN"
sr_id: Optional[str] = None
engineer: Optional[str] = None
status: Optional[str] = None
sr_type: Optional[str] = None
inst_code: Optional[str] = None
priority: Optional[str] = None
keyword: Optional[str] = None
raw_text: str = ""
def parse(text: str) -> ParseResult:
"""자연어 텍스트를 분석해 인텐트 + 엔티티를 반환."""
r = ParseResult(raw_text=text)
lower = text.lower()
# ── 인텐트 분류 ──────────────────────────────────────
for intent_name, patterns in INTENT_PATTERNS:
for pat in patterns:
if re.search(pat, text, re.IGNORECASE):
r.intent = intent_name
break
if r.intent != "UNKNOWN":
break
# ── 엔티티 추출 ──────────────────────────────────────
# SR ID
m = _SR_ID_RE.search(text)
if m:
r.sr_id = m.group().upper()
# 엔지니어 (username 또는 한글 별칭)
m = _ENG_USER_RE.search(text)
if m:
r.engineer = m.group().lower()
else:
for alias, uname in _ENG_ALIAS.items():
if alias in text:
r.engineer = uname
break
# 상태
for k, v in _STATUS_MAP.items():
if k in lower:
r.status = v
break
# SR 유형
for k, v in _TYPE_MAP.items():
if k in lower:
r.sr_type = v
break
# 기관 코드
for inst in _INST_CODES:
if inst in text.upper():
r.inst_code = inst
break
# 우선순위
for k, v in _PRIORITY_MAP.items():
if k in lower:
r.priority = v
break
# 키워드 (KB 검색용) — 따옴표 안 또는 '검색:' 뒤
qm = re.search(r'["\'](.+?)["\']', text)
if qm:
r.keyword = qm.group(1)
else:
# fallback: 큰 단어 추출
kb_kw = re.search(
r'(oom|outofmemory|connection\s*pool|ssl|nginx|502|undo|gc overhead'
r'|디스크|메모리|커넥션|인증서|tomcat|oracle|ora-\d+)',
text, re.IGNORECASE
)
if kb_kw:
r.keyword = kb_kw.group(1)
return r