zioinfo-mail/docs/messenger_integration.md
DESKTOP-TKLFCPR\ython 45f96176a6 Initial commit: GUARDiA project setup
- CLAUDE.md: project context and architecture spec
- docs/: system specs, DB schema, messenger integration, deployment engine
- skills/: guardia-deploy, guardia-agent, guardia-messenger
- .claude/settings.json: project-level permissions
- .gitignore: Python/FastAPI project

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 18:50:19 +09:00

7.5 KiB

[Specification] 메신저 연동 & sLLM 통합 명세


1. 아키텍처 개요

[메신저 (슬랙/잔디/자체앱)]
        │ Webhook (HTTPS POST)
        ▼
[FastAPI Webhook Server :80]
        │ Nginx → proxy_pass → :3000 (Node.js/NestJS) 또는 :8000 (FastAPI)
        ▼
[sLLM Parser :11434 (Ollama) or :8000 (vLLM)]
        │ JSON 정형 데이터
        ▼
[CMDB 조회 → 권한 검증 → 승인 라우팅]
        │
        ▼
[SSH/SFTP 배포 엔진]

2. sLLM 서버 구성

2.1. 모델 선택

권장 1순위: Solar-10.7B-Instruct (한국어 특화)
권장 2순위: Llama-3-8B-Instruct (경량)
실행 엔진: Ollama (빠른 구축) 또는 vLLM (고성능)
양자화: 4-bit AWQ 또는 GGUF (GPU VRAM 절감)

2.2. Ollama 기반 로컬 실행

# 설치 및 모델 다운로드
ollama pull llama3:8b-instruct-q4_K_M

# API 호출 (OpenAI 호환)
curl http://localhost:11434/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model": "llama3:8b-instruct-q4_K_M", "messages": [...]}'

2.3. Python 클라이언트 모듈 (src/llm/parser.py)

import openai

client = openai.OpenAI(
    base_url="http://localhost:11434/v1",  # 온프레미스
    api_key="local"                        # 더미 키
)

SYSTEM_PROMPT = """
당신은 인프라 자동화 시스템의 핵심 파서입니다.
1. 사용자의 자연어를 분석하여 JSON 출력물만 생성합니다.
2. 부연 설명, 인삿말은 절대 포함하지 않습니다.
3. CMDB에 없는 기관/서버는 "UNKNOWN"으로 처리합니다.
4. 배포 명령 시 requires_approval(boolean)을 반드시 판단합니다.

출력 포맷:
{"intent_type": "...", "institution": "...", "system_name": "...",
 "infrastructure_layer": "WAS|WEB|DB", "target_nodes": [],
 "action_sequence": [], "deploy_artifacts": [], "requires_approval": false}
"""

def parse_natural_language(user_text: str) -> dict:
    response = client.chat.completions.create(
        model="llama3:8b-instruct-q4_K_M",
        response_format={"type": "json_object"},
        temperature=0.1,
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": user_text}
        ]
    )
    return json.loads(response.choices[0].message.content)

3. FastAPI Webhook 서버

3.1. 메신저 수신 엔드포인트 (src/api/webhook.py)

from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from typing import Dict, List
import json

app = FastAPI()

# 연결된 세션 관리
room_channels: Dict[str, List[WebSocket]] = {}
connected_agents: Dict[str, WebSocket] = {}

@app.post("/messenger/webhook")
async def receive_message(payload: dict):
    """메신저 Webhook 수신 — 메시지 파싱 & SR 생성"""
    user_id   = payload.get("user_id")
    text      = payload.get("text", "")
    files     = payload.get("files", [])
    room_id   = payload.get("room_id")

    # 파일 스테이징
    staged_files = await stage_files(files)

    # sLLM 파싱
    parsed = parse_natural_language(text)

    # CMDB 조회 → 서버 정보 매핑
    server_list = db.query_servers(parsed["institution"], parsed["system_name"])
    if not server_list:
        return {"status": "ERROR", "message": "CMDB에 해당 서버 정보 없음"}

    # SR 생성
    sr_id = create_ops_task(parsed, user_id, staged_files)

    # 승인 라우팅
    if parsed.get("requires_approval"):
        initiate_approval_process(sr_id)
        return {"status": "PENDING_APPROVAL", "sr_id": sr_id}

    # 즉시 실행
    trigger_execution_engine(sr_id)
    return {"status": "IN_PROGRESS", "sr_id": sr_id}

3.2. WebSocket 실시간 중계 (src/api/ws_relay.py)

@app.websocket("/ws/chat/{room_id}/{client_id}/{client_type}")
async def chat_endpoint(ws: WebSocket, room_id: str, client_id: str, client_type: str):
    await ws.accept()

    if client_type == "HUMAN":
        room_channels.setdefault(room_id, []).append(ws)
    elif client_type == "AGENT":
        connected_agents[client_id] = ws

    try:
        while True:
            data = json.loads(await ws.receive_text())

            if client_type == "HUMAN" and data.get("is_command"):
                # AI 봇 멘션 → 파싱 → 역방향 에이전트로 라우팅
                target_agent = data.get("target_agent_id", "pc-01")
                if target_agent in connected_agents:
                    await connected_agents[target_agent].send_text(
                        json.dumps({"task_id": str(uuid4()), "action": data["command_code"]})
                    )
            elif client_type == "AGENT" and data.get("event") == "TASK_FINISHED":
                # 에이전트 결과 → 방 전체에 브로드캐스트
                for ws in room_channels.get(room_id, []):
                    await ws.send_text(json.dumps({"sender": "BOT", "result": data["payload"]}))
    except WebSocketDisconnect:
        if client_type == "HUMAN":
            room_channels.get(room_id, []).remove(ws)
        else:
            connected_agents.pop(client_id, None)

4. Nginx 웹소켓 프록시 설정

upstream was_backend {
    server 127.0.0.1:8000;
    keepalive 32;
}

server {
    listen 80;
    server_name localhost;

    location /ws {
        proxy_pass http://was_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
        proxy_buffering off;
        client_max_body_size 100M;  # 배포 파일 업로드
    }

    location / {
        proxy_pass http://was_backend;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
    }
}

5. Proactive AI 챗봇 (대화 맥락 분석)

CONTEXT_RULES = {
    "db_delay": {
        "keywords": ["DB", "디비", "쿼리", "느리", "타임아웃", "락"],
        "action": "FETCH_DB_LOCKS",
        "message": "DB 지연 징후가 감지되었습니다. 대기 쿼리를 조회할까요?"
    },
    "disk_full": {
        "keywords": ["용량", "디스크", "로그", "쌓였"],
        "action": "CHECK_DISK_SPACE",
        "message": "디스크 공간 관련 문제가 감지되었습니다. 용량을 확인할까요?"
    }
}

async def analyze_context(room_id: str, sender: str, text: str):
    """대화 맥락 분석 → 선제적 조언 Push"""
    if any(kw in text for kw in ["@bot", "AI", "조회", "확인"]):
        return  # 이미 명시적 명령

    for rule_name, rule in CONTEXT_RULES.items():
        if any(kw in text for kw in rule["keywords"]):
            await asyncio.sleep(1)
            await broadcast_to_room(room_id, {
                "sender": "GUARDiA-Bot",
                "sender_type": "BOT",
                "message_content": f"🤖 {rule['message']}",
                "interactive_action": {
                    "type": "BUTTON",
                    "label": "즉시 확인",
                    "command_code": rule["action"]
                }
            })
            break  # 첫 매칭만

6. 메시지 프로토콜 스키마 (ITSM 확장 호환)

{
  "message_id": "MSG-20260523-0001",
  "timestamp": "2026-05-23T19:00:00Z",
  "sender": "ENGINEER_01",
  "sender_type": "HUMAN",
  "msg_type": "CHAT",
  "content": "기재부 예산시스템 WAS 재기동해줘",
  "is_widget": false,
  "itsm_metadata": {
    "itsm_ticket_id": null,
    "asset_code": "MOF-WAS-01",
    "severity": "MEDIUM"
  }
}