- 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>
7.5 KiB
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"
}
}