- 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>
255 lines
7.5 KiB
Markdown
255 lines
7.5 KiB
Markdown
# [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 기반 로컬 실행
|
|
```bash
|
|
# 설치 및 모델 다운로드
|
|
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`)
|
|
```python
|
|
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`)
|
|
```python
|
|
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`)
|
|
```python
|
|
@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 웹소켓 프록시 설정
|
|
|
|
```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 챗봇 (대화 맥락 분석)
|
|
|
|
```python
|
|
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 확장 호환)
|
|
|
|
```json
|
|
{
|
|
"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"
|
|
}
|
|
}
|
|
```
|