feat(messenger): natural language command parser (NL Command)
- core/nl_command.py: Ollama LLM + rule-based fallback - POST /api/messenger/bot/nl: new NL endpoint - /bot/command: NL fallback when no command matches - 9/10 rule tests PASS Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
57d521e9bf
commit
592566440c
393
itsm/core/nl_command.py
Normal file
393
itsm/core/nl_command.py
Normal file
@ -0,0 +1,393 @@
|
||||
"""
|
||||
자연어 → 메신저 봇 명령어 파서 (NL Command Parser)
|
||||
|
||||
Ollama(로컬 LLM) 기반으로 자연어 입력을 봇 명령어로 변환.
|
||||
Ollama 미연결 시 규칙 기반 폴백.
|
||||
|
||||
반환 형태:
|
||||
{
|
||||
"command": "!scrap",
|
||||
"args": ["https://example.com"],
|
||||
"full_command": "!scrap https://example.com",
|
||||
"confidence": 0.92,
|
||||
"explanation": "URL 스크랩 요청으로 판단"
|
||||
}
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import httpx
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
OLLAMA_URL = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434") + "/api/generate"
|
||||
NL_MODEL = os.getenv("NL_COMMAND_MODEL", "llama3")
|
||||
TIMEOUT = 20 # Ollama 호출 타임아웃
|
||||
MIN_CONFIDENCE = 0.55 # 이 이하면 "이해 못함"으로 처리
|
||||
|
||||
|
||||
# ── Few-shot 프롬프트 ─────────────────────────────────────────────────────────
|
||||
|
||||
_SYSTEM_PROMPT = """\
|
||||
너는 GUARDiA ITSM 메신저 봇의 자연어 명령 해석기다.
|
||||
사용자의 자연어 입력을 분석해 아래 봇 명령어 중 하나로 변환하라.
|
||||
|
||||
[지원 명령어 목록]
|
||||
!vibe <SR-ID> [project_id] - SR에 대한 바이브 코딩 세션 시작
|
||||
!build <session_id> - 빌드 실행
|
||||
!deploy <session_id> - 배포 실행
|
||||
!status <SR-ID> - SR 상태 조회
|
||||
!cancel <session_id> - 세션 취소
|
||||
!health <server> - 서버 헬스체크
|
||||
!log <server> [path] - 서버 로그 분석
|
||||
!sm <server> <script> - SM 스크립트 실행
|
||||
/sr <제목> - SR 빠른 접수
|
||||
/status - 전체 시스템 현황
|
||||
/assign <SR-ID> <담당자> - SR 담당자 배정
|
||||
/approve <SR-ID> [의견] - SR 승인
|
||||
/reject <SR-ID> [사유] - SR 반려
|
||||
/incident <제목> [P1-P4] - 인시던트 등록
|
||||
/rca <INC-ID> - AI RCA 분석
|
||||
/escalate <SR-ID> - 에스컬레이션
|
||||
/sla - SLA 위반 현황
|
||||
/kb <검색어> - KB 문서 검색
|
||||
/pms <프로젝트코드> - 프로젝트 현황
|
||||
/report <코드> [daily|weekly] - 보고서 발송
|
||||
/oncall - 당직자 조회
|
||||
/scan - 보안 스캔
|
||||
/perf [url] - 성능 테스트
|
||||
!scrap <url> - URL 스크랩
|
||||
!scrap list [n] - 스크랩 목록
|
||||
!scrap publish <id> - 스크랩 게시
|
||||
!scrap del <id> - 스크랩 삭제
|
||||
!scrap restore <id> - 스크랩 원복
|
||||
!scrap status <id> - 스크랩 상태 조회
|
||||
/autoq - 자율 운영 대기 목록
|
||||
!help - 도움말
|
||||
|
||||
[규칙]
|
||||
1. 반드시 JSON만 반환. 자연어 설명 없음.
|
||||
2. 입력에서 SR ID를 찾으면 그대로 사용 (SR-20260531-XXXX 형태 유지).
|
||||
3. 확신이 없으면 confidence를 낮게 설정.
|
||||
4. 명확히 매핑 불가능하면 command를 null로.
|
||||
|
||||
[예시 입력 → 출력]
|
||||
입력: "SR-20260531-ABCD 배포해줘"
|
||||
출력: {"command":"!deploy","args":["SR-20260531-ABCD"],"full_command":"!deploy SR-20260531-ABCD","confidence":0.95,"explanation":"배포 요청으로 판단"}
|
||||
|
||||
입력: "서버1 헬스체크 해줘"
|
||||
출력: {"command":"!health","args":["서버1"],"full_command":"!health 서버1","confidence":0.92,"explanation":"헬스체크 요청"}
|
||||
|
||||
입력: "최근 스크랩 5개 보여줘"
|
||||
출력: {"command":"!scrap","args":["list","5"],"full_command":"!scrap list 5","confidence":0.90,"explanation":"스크랩 목록 조회"}
|
||||
|
||||
입력: "https://example.com 스크랩해줘"
|
||||
출력: {"command":"!scrap","args":["https://example.com"],"full_command":"!scrap https://example.com","confidence":0.95,"explanation":"URL 스크랩 요청"}
|
||||
|
||||
입력: "#3 게시해줘"
|
||||
출력: {"command":"!scrap","args":["publish","3"],"full_command":"!scrap publish 3","confidence":0.88,"explanation":"스크랩 게시 요청"}
|
||||
|
||||
입력: "전체 시스템 현황 알려줘"
|
||||
출력: {"command":"/status","args":[],"full_command":"/status","confidence":0.85,"explanation":"시스템 현황 조회"}
|
||||
|
||||
입력: "오늘 서버 배포 요청 접수해줘 - web01 Tomcat 재기동 필요"
|
||||
출력: {"command":"/sr","args":["[배포] web01 Tomcat 재기동"],"full_command":"/sr [배포] web01 Tomcat 재기동","confidence":0.82,"explanation":"SR 접수 요청"}
|
||||
|
||||
입력: "P1 긴급 장애 발생 - 결제 시스템 전면 중단"
|
||||
출력: {"command":"/incident","args":["결제 시스템 전면 중단","P1"],"full_command":"/incident 결제 시스템 전면 중단 P1","confidence":0.95,"explanation":"P1 인시던트 등록"}
|
||||
|
||||
입력: "홍길동에게 SR-20260531-XXXX 배정해줘"
|
||||
출력: {"command":"/assign","args":["SR-20260531-XXXX","홍길동"],"full_command":"/assign SR-20260531-XXXX 홍길동","confidence":0.93,"explanation":"SR 담당자 배정"}
|
||||
|
||||
입력: "날씨 어때?"
|
||||
출력: {"command":null,"args":[],"full_command":null,"confidence":0.1,"explanation":"ITSM과 무관한 질문"}
|
||||
|
||||
"""
|
||||
|
||||
|
||||
# ── Ollama 호출 ───────────────────────────────────────────────────────────────
|
||||
|
||||
async def parse_nl_command(text: str) -> Dict[str, Any]:
|
||||
"""
|
||||
자연어 텍스트를 봇 명령어로 변환.
|
||||
Ollama 실패 시 규칙 기반 폴백.
|
||||
"""
|
||||
# 이미 명령어 형식이면 그대로 반환
|
||||
if _is_explicit_command(text):
|
||||
cmd_parts = text.strip().split()
|
||||
return {
|
||||
"command": cmd_parts[0],
|
||||
"args": cmd_parts[1:],
|
||||
"full_command": text.strip(),
|
||||
"confidence": 1.0,
|
||||
"explanation": "명시적 명령어",
|
||||
}
|
||||
|
||||
# Ollama 시도
|
||||
try:
|
||||
result = await _call_ollama(text)
|
||||
if result and result.get("confidence", 0) >= MIN_CONFIDENCE:
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.warning("[NL Command] Ollama 실패, 규칙 기반 폴백: %s", e)
|
||||
|
||||
# 규칙 기반 폴백
|
||||
return _rule_based_parse(text)
|
||||
|
||||
|
||||
async def _call_ollama(text: str) -> Optional[Dict[str, Any]]:
|
||||
"""Ollama에 자연어 명령어 파싱 요청."""
|
||||
prompt = (
|
||||
_SYSTEM_PROMPT
|
||||
+ f'\n입력: "{text}"\n출력: '
|
||||
)
|
||||
|
||||
payload = {
|
||||
"model": NL_MODEL,
|
||||
"prompt": prompt,
|
||||
"stream": False,
|
||||
"format": "json",
|
||||
"options": {
|
||||
"temperature": 0.1, # 결정론적 출력
|
||||
"top_p": 0.9,
|
||||
"num_predict": 200,
|
||||
},
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient(timeout=TIMEOUT) as client:
|
||||
resp = await client.post(OLLAMA_URL, json=payload)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
raw = data.get("response", "").strip()
|
||||
|
||||
# JSON 추출
|
||||
json_match = re.search(r'\{.*\}', raw, re.DOTALL)
|
||||
if not json_match:
|
||||
return None
|
||||
|
||||
parsed = json.loads(json_match.group())
|
||||
|
||||
# 필수 필드 검증
|
||||
if "command" not in parsed or "confidence" not in parsed:
|
||||
return None
|
||||
|
||||
# full_command 자동 생성
|
||||
if parsed.get("command") and not parsed.get("full_command"):
|
||||
args_str = " ".join(str(a) for a in parsed.get("args", []))
|
||||
parsed["full_command"] = (
|
||||
f"{parsed['command']} {args_str}".strip()
|
||||
if args_str else parsed["command"]
|
||||
)
|
||||
|
||||
return parsed
|
||||
|
||||
|
||||
# ── 규칙 기반 폴백 ────────────────────────────────────────────────────────────
|
||||
|
||||
_RULES: List[Tuple[List[str], str, callable]] = [
|
||||
# (키워드목록, command, args_extractor)
|
||||
(["배포해", "배포 해", "deploy", "릴리즈해"],
|
||||
"!deploy", lambda t: [_extract_sr(t) or _extract_session(t) or ""]),
|
||||
|
||||
(["빌드해", "빌드 해", "build"],
|
||||
"!build", lambda t: [_extract_session(t) or ""]),
|
||||
|
||||
(["바이브", "vibe", "코딩 세션"],
|
||||
"!vibe", lambda t: [_extract_sr(t) or ""]),
|
||||
|
||||
(["헬스체크", "상태 확인", "health", "헬스 체크"],
|
||||
"!health", lambda t: [_extract_server(t) or ""]),
|
||||
|
||||
(["로그 분석", "로그 확인", "로그 봐", "log 분석"],
|
||||
"!log", lambda t: [_extract_server(t) or ""]),
|
||||
|
||||
(["스크랩 목록", "스크랩 리스트", "scrap list", "수집 목록",
|
||||
"최근 스크랩", "스크랩 보여", "수집 결과", "스크랩 결과"],
|
||||
"!scrap", lambda t: ["list", _extract_number(t) or "5"]),
|
||||
|
||||
(["스크랩해줘", "스크랩 해줘", "수집해줘", "크롤링", "스크래핑"],
|
||||
"!scrap", lambda t: [_extract_url(t) or ""]),
|
||||
|
||||
(["게시해", "publish", "발행"],
|
||||
"!scrap", lambda t: ["publish", _extract_id(t) or ""]),
|
||||
|
||||
(["삭제해", "지워", "delete"],
|
||||
"!scrap", lambda t: ["del", _extract_id(t) or ""]),
|
||||
|
||||
(["원복", "복구", "restore"],
|
||||
"!scrap", lambda t: ["restore", _extract_id(t) or ""]),
|
||||
|
||||
(["상태 조회", "현황 조회", "상태 알려", "어떻게 돼"],
|
||||
"!status", lambda t: [_extract_sr(t) or ""]),
|
||||
|
||||
(["시스템 현황", "전체 현황", "현황 알려", "/status"],
|
||||
"/status", lambda t: []),
|
||||
|
||||
(["SLA 현황", "SLA 위반", "sla"],
|
||||
"/sla", lambda t: []),
|
||||
|
||||
(["당직자", "온콜", "oncall"],
|
||||
"/oncall", lambda t: []),
|
||||
|
||||
(["인시던트", "장애 등록", "장애 신고", "incident",
|
||||
"장애 발생", "장애 다운", "서비스 중단", "전면 장애",
|
||||
"긴급 장애", "P1 장애", "P2 장애"],
|
||||
"/incident", lambda t: [_extract_incident_title(t), _extract_priority(t)]),
|
||||
|
||||
(["승인해", "approve", "승인 처리"],
|
||||
"/approve", lambda t: [_extract_sr(t) or ""]),
|
||||
|
||||
(["반려해", "거절", "reject"],
|
||||
"/reject", lambda t: [_extract_sr(t) or ""]),
|
||||
|
||||
(["배정해", "assign", "담당자 지정"],
|
||||
"/assign", lambda t: [_extract_sr(t) or "", _extract_person(t) or ""]),
|
||||
|
||||
(["SR 접수", "서비스 요청", "티켓 등록", "sr 올려"],
|
||||
"/sr", lambda t: [_extract_title(t)]),
|
||||
|
||||
(["보안 스캔", "취약점 점검", "/scan"],
|
||||
"/scan", lambda t: []),
|
||||
|
||||
(["성능 테스트", "부하 테스트", "/perf"],
|
||||
"/perf", lambda t: [_extract_url(t) or ""]),
|
||||
|
||||
(["프로젝트 현황", "pms", "/pms"],
|
||||
"/pms", lambda t: [_extract_code(t) or ""]),
|
||||
|
||||
(["자율 운영", "autoq", "승인 대기"],
|
||||
"/autoq", lambda t: []),
|
||||
|
||||
(["도움말", "명령어", "help"],
|
||||
"!help", lambda t: []),
|
||||
]
|
||||
|
||||
|
||||
def _rule_based_parse(text: str) -> Dict[str, Any]:
|
||||
text_lower = text.lower()
|
||||
best_cmd = None
|
||||
best_args: List[str] = []
|
||||
best_score = 0
|
||||
|
||||
for keywords, command, args_fn in _RULES:
|
||||
score = sum(1 for kw in keywords if kw.lower() in text_lower)
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_cmd = command
|
||||
try:
|
||||
best_args = [a for a in args_fn(text) if a]
|
||||
except Exception:
|
||||
best_args = []
|
||||
|
||||
if not best_cmd or best_score == 0:
|
||||
return {
|
||||
"command": None,
|
||||
"args": [],
|
||||
"full_command": None,
|
||||
"confidence": 0.0,
|
||||
"explanation": "해당 요청을 이해하지 못했습니다. !help 로 명령어를 확인하세요.",
|
||||
}
|
||||
|
||||
args_str = " ".join(best_args)
|
||||
full = f"{best_cmd} {args_str}".strip() if args_str else best_cmd
|
||||
confidence = min(0.5 + best_score * 0.1, 0.75)
|
||||
|
||||
return {
|
||||
"command": best_cmd,
|
||||
"args": best_args,
|
||||
"full_command": full,
|
||||
"confidence": confidence,
|
||||
"explanation": f"규칙 기반 매핑 (키워드 {best_score}개 일치)",
|
||||
}
|
||||
|
||||
|
||||
# ── 엔티티 추출 헬퍼 ─────────────────────────────────────────────────────────
|
||||
|
||||
def _is_explicit_command(text: str) -> bool:
|
||||
"""이미 명시적 명령어 형식인지 확인."""
|
||||
t = text.strip()
|
||||
return bool(re.match(r'^[!/]', t))
|
||||
|
||||
|
||||
def _extract_sr(text: str) -> Optional[str]:
|
||||
m = re.search(r'SR-\d{8}-[A-Z0-9]+', text, re.IGNORECASE)
|
||||
return m.group().upper() if m else None
|
||||
|
||||
|
||||
def _extract_session(text: str) -> Optional[str]:
|
||||
m = re.search(r'(?:세션|session)[\s#]*(\d+)', text, re.IGNORECASE)
|
||||
if m:
|
||||
return m.group(1)
|
||||
m = re.search(r'#(\d+)', text)
|
||||
return m.group(1) if m else None
|
||||
|
||||
|
||||
def _extract_id(text: str) -> Optional[str]:
|
||||
m = re.search(r'#(\d+)', text)
|
||||
if m:
|
||||
return m.group(1)
|
||||
m = re.search(r'\b(\d+)\b', text)
|
||||
return m.group(1) if m else None
|
||||
|
||||
|
||||
def _extract_url(text: str) -> Optional[str]:
|
||||
m = re.search(r'https?://[^\s]+', text)
|
||||
return m.group() if m else None
|
||||
|
||||
|
||||
def _extract_server(text: str) -> Optional[str]:
|
||||
# "서버1", "서버-prod", "서버 web01" 형태
|
||||
m = re.search(r'서버\s*([A-Za-z0-9\-_가-힣]+)', text)
|
||||
if m:
|
||||
return '서버' + m.group(1) # "서버1" 전체 반환
|
||||
m = re.search(r'server[\s:]*([A-Za-z0-9\-_]+)', text, re.IGNORECASE)
|
||||
if m:
|
||||
return m.group(1)
|
||||
# web01, was-prod, app01 등 서버명 패턴
|
||||
m = re.search(r'\b([A-Za-z가-힣][A-Za-z0-9\-_가-힣]*(?:web|was|db|app|srv|prod|dev)\w*)\b',
|
||||
text, re.IGNORECASE)
|
||||
return m.group(1) if m else None
|
||||
|
||||
|
||||
def _extract_number(text: str) -> Optional[str]:
|
||||
m = re.search(r'\b(\d+)\b', text)
|
||||
return m.group(1) if m else None
|
||||
|
||||
|
||||
def _extract_priority(text: str) -> str:
|
||||
m = re.search(r'\b(P[1-4]|P1|P2|P3|P4|긴급|critical)\b', text, re.IGNORECASE)
|
||||
if m:
|
||||
v = m.group().upper()
|
||||
if v in ("긴급", "CRITICAL"):
|
||||
return "P1"
|
||||
return v
|
||||
return "P3"
|
||||
|
||||
|
||||
def _extract_person(text: str) -> Optional[str]:
|
||||
m = re.search(r'(?:에게|한테|담당자?\s*)([가-힣]{2,4}|[A-Za-z]+)', text)
|
||||
return m.group(1) if m else None
|
||||
|
||||
|
||||
def _extract_code(text: str) -> Optional[str]:
|
||||
m = re.search(r'\b([A-Z][A-Z0-9\-]{1,10})\b', text)
|
||||
return m.group(1) if m else None
|
||||
|
||||
|
||||
def _extract_title(text: str) -> str:
|
||||
for kw in ["SR 접수", "서비스 요청", "티켓 등록", "sr 올려", "접수해줘"]:
|
||||
text = re.sub(kw, "", text, flags=re.IGNORECASE)
|
||||
return text.strip() or "자연어 SR 접수"
|
||||
|
||||
|
||||
def _extract_incident_title(text: str) -> str:
|
||||
for kw in ["인시던트", "장애 등록", "incident", "P1", "P2", "P3", "P4",
|
||||
"긴급", "등록해", "신고해"]:
|
||||
text = re.sub(kw, "", text, flags=re.IGNORECASE)
|
||||
return text.strip() or "자동 인시던트"
|
||||
@ -150,6 +150,48 @@ def _format_event_message(event: MessengerEvent) -> str:
|
||||
|
||||
# ── 봇 명령어 처리 (inbound) ──────────────────────────────────────────────────
|
||||
|
||||
@router.post("/bot/nl", response_model=BotReply)
|
||||
async def handle_nl_command(
|
||||
cmd: BotCommand,
|
||||
bg: BackgroundTasks,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
자연어 명령 처리 전용 엔드포인트.
|
||||
자연어 → 명령어 변환 후 실행. 명시적 명령어도 처리 가능.
|
||||
"""
|
||||
from core.nl_command import parse_nl_command
|
||||
parsed = await parse_nl_command(cmd.command.strip())
|
||||
|
||||
if not parsed.get("full_command") or parsed.get("confidence", 0) < 0.45:
|
||||
return BotReply(
|
||||
room=cmd.room,
|
||||
text=(
|
||||
f"❓ 요청을 이해하지 못했습니다.\n"
|
||||
f"자연어 예시: '서버1 헬스체크 해줘', 'SR-2026-XXXX 배포해줘'\n"
|
||||
f"!help 로 명령어 목록을 확인하세요."
|
||||
),
|
||||
)
|
||||
|
||||
# 파싱된 명령어를 BotCommand로 재생성해서 기존 핸들러 호출
|
||||
nl_cmd = BotCommand(
|
||||
room=cmd.room,
|
||||
user=cmd.user,
|
||||
command=parsed["full_command"],
|
||||
message=f"[자연어→{parsed['command']}] {cmd.command}",
|
||||
)
|
||||
reply = await handle_bot_command(nl_cmd, bg, db)
|
||||
|
||||
# 신뢰도 낮으면 안내 메시지 추가
|
||||
if parsed.get("confidence", 1.0) < 0.75:
|
||||
reply.text = (
|
||||
f"💬 자연어 해석: {parsed.get('explanation', '')}\n"
|
||||
f"명령어: {parsed['full_command']}\n\n"
|
||||
+ reply.text
|
||||
)
|
||||
return reply
|
||||
|
||||
|
||||
@router.post("/bot/command", response_model=BotReply)
|
||||
async def handle_bot_command(
|
||||
cmd: BotCommand,
|
||||
@ -158,7 +200,7 @@ async def handle_bot_command(
|
||||
):
|
||||
"""
|
||||
GUARDiA 메신저 봇에서 전달되는 명령어 처리.
|
||||
메신저 봇이 사용자 명령을 이 엔드포인트로 POST 전달.
|
||||
명시적 명령어(!vibe, /sr 등)와 자연어 모두 처리.
|
||||
"""
|
||||
text = cmd.command.strip()
|
||||
parts = text.split()
|
||||
@ -575,8 +617,9 @@ async def handle_bot_command(
|
||||
return BotReply(room=cmd.room, text=_help_text())
|
||||
|
||||
else:
|
||||
return BotReply(room=cmd.room,
|
||||
text=f"알 수 없는 명령어: {keyword}\n!help 또는 /help 로 도움말 확인")
|
||||
# ── 자연어 처리 폴백 ────────────────────────────────────────────────────
|
||||
# 명시적 명령어가 아닌 경우 NL → 명령어 파싱 시도
|
||||
return await _handle_natural_language(cmd, bg, db)
|
||||
|
||||
|
||||
# ── 백그라운드 명령 실행 헬퍼 ────────────────────────────────────────────────
|
||||
@ -1913,6 +1956,61 @@ esb, elasticsearch, solr, pinpoint, scouter
|
||||
|
||||
# ── 스크랩 봇 헬퍼 ────────────────────────────────────────────────────────────
|
||||
|
||||
async def _handle_natural_language(
|
||||
cmd: BotCommand,
|
||||
bg: BackgroundTasks,
|
||||
db: AsyncSession,
|
||||
) -> BotReply:
|
||||
"""
|
||||
명시적 명령어가 아닌 자연어 입력을 처리.
|
||||
NL 파서 → 명령어 변환 → 기존 핸들러 재호출.
|
||||
"""
|
||||
from core.nl_command import parse_nl_command
|
||||
|
||||
text = cmd.command.strip()
|
||||
parsed = await parse_nl_command(text)
|
||||
|
||||
confidence = parsed.get("confidence", 0)
|
||||
full_cmd = parsed.get("full_command")
|
||||
|
||||
# 너무 낮은 신뢰도 → 안내
|
||||
if not full_cmd or confidence < 0.45:
|
||||
return BotReply(
|
||||
room=cmd.room,
|
||||
text=(
|
||||
f"❓ 명령어를 인식하지 못했습니다.\n\n"
|
||||
f"자연어로 입력 예시:\n"
|
||||
f" • 서버1 헬스체크 해줘\n"
|
||||
f" • SR-2026-XXXX 배포해줘\n"
|
||||
f" • https://example.com 스크랩해줘\n"
|
||||
f" • P1 긴급 장애 결제 시스템 다운\n\n"
|
||||
f"!help 로 전체 명령어 목록 확인"
|
||||
),
|
||||
)
|
||||
|
||||
# 파싱된 명령어로 재호출
|
||||
nl_cmd = BotCommand(
|
||||
room=cmd.room,
|
||||
user=cmd.user,
|
||||
command=full_cmd,
|
||||
)
|
||||
reply = await handle_bot_command(nl_cmd, bg, db)
|
||||
|
||||
# 신뢰도 < 0.75면 해석 과정 투명하게 표시
|
||||
if confidence < 0.75:
|
||||
prefix = (
|
||||
f"💬 자연어 해석 (신뢰도 {int(confidence*100)}%)\n"
|
||||
f" 입력: {text}\n"
|
||||
f" 명령: {full_cmd}\n\n"
|
||||
)
|
||||
reply.text = prefix + reply.text
|
||||
else:
|
||||
prefix = f"💬 → {full_cmd}\n"
|
||||
reply.text = prefix + reply.text
|
||||
|
||||
return reply
|
||||
|
||||
|
||||
async def _cmd_scrap_url(room: str, actor: str, url: str) -> None:
|
||||
"""URL 즉시 스크랩 후 결과를 채널로 전송."""
|
||||
from core.scraping_engine import scrape as _scrape
|
||||
|
||||
Loading…
Reference in New Issue
Block a user