zioinfo-mail/itsm/core/nlu.py
DESKTOP-TKLFCPR\ython e228faabf5 feat(itsm): G-1~G-12 확장 기능 + 하네스/봇/설치스크립트 구현
G-1: 메신저 Webhook Relay + _send_to_room 실제 httpx 호출 구현
G-2: POST /api/tasks/bulk SR 대량작업 엔드포인트 (최대 100건)
G-3: 라이선스 만료 알림 스케줄러 (매일 09:00 KST)
G-4: 체험판 upgrade_banner 필드 + license.py 배너 로직
G-5: core/auto_rca.py + incidents/problem auto-rca 엔드포인트
G-6: core/deploy_impact.py + vibe impact-analysis 엔드포인트
G-7: core/ticket_classifier.py + SR 생성 시 AI 분류 + ai-suggestion API
G-8: VulnPatchRecord 모델 + vuln_scan 패치추적 4개 엔드포인트
G-9: core/jira_sync.py + gateway Jira/Confluence 연동 엔드포인트
G-10: core/push_notify.py + routers/push.py + PushSubscription 모델
G-11: approvals 다중승인 (위임/서명/기한초과/마감연장)
G-12: alembic.ini + migrations/ + cicd/migrate_to_postgres.sh

하네스: guardia-orchestrator 확장기능 Phase 반영
봇명령어: /sr /status /license /bulk 슬래시 명령어 추가
설치스크립트: setup/ (Ubuntu, CentOS, RHEL, Windows) --test 옵션 포함

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 18:18:52 +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