--- name: guardia-messenger description: > GUARDiA 프로젝트에서 메신저 연동 및 sLLM 파서 코드를 작성하거나 수정할 때 사용하는 스킬. 다음 경우에 반드시 먼저 읽어라: - FastAPI Webhook 엔드포인트 구현 - WebSocket 실시간 중계 서버 코드 - sLLM 자연어 파서 프롬프트 엔지니어링 - 메신저 응답 포맷 및 인터랙티브 버튼 구현 - Proactive 챗봇 대화 맥락 분석 --- # GUARDiA 메신저 연동 스킬 ## sLLM 파서 원칙 ### 출력 포맷 강제 (절대 준수) ``` - JSON 외 텍스트 출력 금지 - 부연 설명, 인삿말 포함 금지 - temperature: 0.1 (결정론적 출력) - response_format: {"type": "json_object"} ``` ### 시스템 프롬프트 기본 구조 ```python SYSTEM_PROMPT = """ 당신은 인프라 자동화 시스템의 핵심 파서입니다. 1. 사용자의 자연어를 분석하여 JSON 출력물만 생성합니다. 2. 부연 설명, 인삿말, 안내 텍스트는 절대 포함하지 않습니다. 3. CMDB에 정의된 정확한 서버 ID와 매핑되지 않는 정보는 "UNKNOWN"으로 처리합니다. 4. 배포 명령 시 반드시 'requires_approval'(boolean) 값을 판단합니다. 출력 포맷 (반드시 이 구조만 사용): { "intent_type": "DEPLOY_UPGRADE|LOG_ANALYSIS|SERVICE_RESTART|SSL_CHECK|DB_QUERY|CRON_MANAGE", "institution": "기관명 또는 UNKNOWN", "system_name": "시스템명 또는 UNKNOWN", "infrastructure_layer": "WEB|WAS|DB|ESB", "target_nodes": [], "action_sequence": [], "deploy_artifacts": [], "requires_approval": false, "priority": "HIGH|MEDIUM|LOW" } """ ``` ## Webhook 수신 표준 ```python @app.post("/messenger/webhook") async def receive_message(request: Request): # 1. 시크릿 키 검증 signature = request.headers.get("X-Messenger-Signature") if not verify_signature(signature, await request.body()): raise HTTPException(status_code=403) payload = await request.json() user_id = payload.get("user_id") text = payload.get("text", "") files = payload.get("files", []) # 2. 봇 자신의 메시지는 무시 (무한 루프 방지) if payload.get("bot") or not text.strip(): return {"status": "IGNORED"} # 3. 파일 스테이징 staged = await stage_uploaded_files(files, user_id) # 4. sLLM 파싱 parsed = parse_natural_language(text) # 5. SR 생성 및 라우팅 sr_id = create_ops_task(parsed, user_id, staged) route_to_engine(sr_id, parsed) return {"status": "ACCEPTED", "sr_id": sr_id} ``` ## 메신저 응답 포맷 ### 텍스트 응답 ```python def send_text_response(room_id: str, text: str): messenger.post_message(room_id, { "sender": "GUARDiA-Bot", "text": text, "msg_type": "CHAT" }) ``` ### 인터랙티브 버튼 응답 ```python def send_interactive_response(room_id: str, text: str, action_code: str, label: str): messenger.post_message(room_id, { "sender": "GUARDiA-Bot", "text": text, "is_widget": True, "interactive_action": { "type": "BUTTON", "label": label, "command_code": action_code } }) # 사용 예 send_interactive_response( room_id, "🤖 DB 지연 징후 감지. 대기 쿼리를 확인할까요?", "FETCH_DB_LOCKS", "대기 쿼리 확인" ) ``` ### 승인 요청 응답 ```python def request_approval(approver_id: str, sr_id: str, summary: str): messenger.post_direct_message(approver_id, { "sender": "GUARDiA-Bot", "text": f"[{sr_id}] 배포 승인 요청\n{summary}", "is_widget": True, "interactive_action": { "type": "APPROVAL_BUTTONS", "approve_url": f"/api/sr/approve/{sr_id}", "reject_url": f"/api/sr/reject/{sr_id}" } }) ``` ## 금지 사항 - 메신저 응답에 서버 IP, 비밀번호, SSH 계정 노출 금지 - 봇 응답 메시지에 다시 파서 실행 금지 (무한 루프) - 에러 메시지에 스택트레이스 전체 노출 금지 → SR ID와 요약만 전달 - 파일 스테이징 없이 바로 원격 서버 전송 금지 → 반드시 임시 디렉토리 거침 ## 파일 스테이징 표준 ```python import aiofiles from pathlib import Path STAGING_BASE = Path("/app/guardia/staging") async def stage_uploaded_files(files: list, user_id: str) -> list: """메신저 첨부 파일을 임시 디렉토리에 다운로드""" staged = [] sr_dir = STAGING_BASE / user_id / datetime.now().strftime("%Y%m%d_%H%M%S") sr_dir.mkdir(parents=True, exist_ok=True) for f in files: filename = f["name"] url = f["url"] dest = sr_dir / filename async with aiohttp.ClientSession() as session: async with session.get(url) as resp: async with aiofiles.open(dest, "wb") as fp: await fp.write(await resp.read()) staged.append({"name": filename, "path": str(dest), "type": classify_artifact(filename)}) return staged def classify_artifact(filename: str) -> str: ext = Path(filename).suffix.lower() if ext in {".class", ".jar", ".war"}: return "DYNAMIC" if ext in {".html", ".js", ".css"}: return "STATIC" if ext in {".png", ".jpg", ".gif"}: return "STATIC" return "UNKNOWN" ``` ## Nginx 업스트림 설정 (WebSocket) ```nginx location /ws { proxy_pass http://was_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; proxy_read_timeout 86400s; proxy_buffering off; client_max_body_size 100M; } ``` ## 라이선스 주의사항 메신저 봇이 SR 생성 또는 기관·서버 등록 명령을 처리할 때 라이선스 제한을 확인해야 한다. | 기능 | 필요 에디션 | 한도 초과 시 봇 응답 | |------|-----------|-------------------| | SR 생성 | 모든 에디션 | 정상 처리 | | 기관 등록 명령 | STANDARD+(50개), ENTERPRISE(무제한) | "라이선스 한도 초과: 업그레이드가 필요합니다." | | AI 에이전트 연동 | STANDARD 이상 | "AI_AGENTS 기능은 STANDARD 이상 에디션에서만 사용 가능합니다." | 봇이 ITSM API로부터 HTTP 403을 받으면 해당 오류 메시지를 사용자에게 전달한다: ```python if resp.status_code == 403: detail = resp.json().get("detail", "라이선스 제한으로 요청이 거부되었습니다.") messenger.reply(room_id, f"⚠️ {detail}") ```