guardia-itsm/routers/multimodal.py
2026-06-02 06:07:36 +09:00

208 lines
6.9 KiB
Python

"""
멀티모달 AI — 이미지·로그 파일 분석 → 에러 자동 분류 + SR 생성
Ollama llava 모델로 스크린샷·에러 이미지를 분석하여
에러 유형을 자동 분류하고 SR을 생성한다.
엔드포인트:
POST /api/multimodal/analyze-image — 이미지 분석 (base64)
POST /api/multimodal/analyze-log — 로그 텍스트 분석
POST /api/multimodal/upload-and-analyze — 파일 업로드 + 분석
POST /api/multimodal/auto-sr — 분석 결과 → SR 자동 생성
"""
from __future__ import annotations
import base64
import logging
from datetime import datetime
from typing import Optional
import httpx
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from core.auth import get_current_user
from database import get_db
from models import User, SRRequest, SRStatus
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/multimodal", tags=["Multimodal AI"])
OLLAMA_URL = "http://localhost:11434"
VISION_MODEL = "llava" # Ollama 비전 모델
TEXT_MODEL = "llama3"
class ImageAnalysisRequest(BaseModel):
image_b64: str
context: Optional[str] = None # 추가 컨텍스트 (서버명, 시스템명 등)
class LogAnalysisRequest(BaseModel):
log_text: str
log_type: str = "application" # application | system | nginx | java
async def _call_vision(image_b64: str, prompt: str) -> str:
try:
async with httpx.AsyncClient(timeout=60) as client:
r = await client.post(f"{OLLAMA_URL}/api/generate", json={
"model": VISION_MODEL,
"prompt": prompt,
"images": [image_b64],
"stream": False,
})
if r.status_code == 200:
return r.json().get("response", "").strip()
except Exception as e:
logger.warning(f"llava 호출 실패: {e}")
return ""
async def _call_llm(prompt: str) -> str:
try:
async with httpx.AsyncClient(timeout=30) as client:
r = await client.post(f"{OLLAMA_URL}/api/generate", json={
"model": TEXT_MODEL,
"system": "IT 운영 전문가. 에러 분석 후 JSON 형식으로만 답변.",
"prompt": prompt,
"stream": False,
})
if r.status_code == 200:
return r.json().get("response", "").strip()
except Exception as e:
logger.warning(f"llm 호출 실패: {e}")
return ""
@router.post("/analyze-image")
async def analyze_image(
req: ImageAnalysisRequest,
user: User = Depends(get_current_user),
):
"""이미지(스크린샷·에러화면) → 에러 분석."""
context_hint = f"\n참고: {req.context}" if req.context else ""
prompt = (
"이 IT 시스템 화면에서 에러나 문제를 찾아주세요. "
"한국어로 답변하고 다음 항목을 분석해주세요: "
"1) 에러 유형, 2) 예상 원인, 3) 권고 조치, 4) 심각도(LOW/MEDIUM/HIGH)"
+ context_hint
)
# llava 모델 존재 확인
try:
async with httpx.AsyncClient(timeout=5) as c:
r = await c.post(f"{OLLAMA_URL}/api/show", json={"name": VISION_MODEL})
has_vision = r.status_code == 200
except Exception:
has_vision = False
if not has_vision:
# llava 없으면 llama3로 텍스트 분석 대체
analysis = await _call_llm(
f"이미지를 분석할 수 없습니다. 이미지 첨부된 IT 에러에 대한 일반적 안내를 제공하세요."
)
return {"model": TEXT_MODEL, "analysis": analysis or "llava 모델 미설치. `ollama pull llava` 실행 후 재시도.", "has_vision": False}
analysis = await _call_vision(req.image_b64, prompt)
return {
"model": VISION_MODEL,
"analysis": analysis,
"has_vision": True,
"context": req.context,
}
@router.post("/analyze-log")
async def analyze_log(
req: LogAnalysisRequest,
user: User = Depends(get_current_user),
):
"""로그 텍스트 → 에러 패턴 분석 + 심각도 분류."""
log_sample = req.log_text[:3000] # 3000자로 제한
prompt = (
f"다음 {req.log_type} 로그를 분석해주세요:\n\n{log_sample}\n\n"
"JSON으로만 답변: "
'{"error_type": "오류유형", "severity": "LOW|MEDIUM|HIGH|CRITICAL", '
'"root_cause": "근본원인", "recommendation": "조치방안", '
'"keywords": ["에러키워드1", "에러키워드2"]}'
)
result_text = await _call_llm(prompt)
# JSON 추출
import json, re
match = re.search(r'\{.*\}', result_text, re.DOTALL)
try:
result = json.loads(match.group()) if match else {}
except Exception:
result = {"raw": result_text}
return {
"log_type": req.log_type,
"analysis": result,
"log_length": len(req.log_text),
}
@router.post("/upload-and-analyze")
async def upload_and_analyze(
file: UploadFile = File(...),
user: User = Depends(get_current_user),
):
"""파일 업로드 후 유형에 따라 분석."""
content = await file.read()
filename = file.filename or ""
if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp')):
# 이미지 분석
image_b64 = base64.b64encode(content).decode()
result = await analyze_image(
ImageAnalysisRequest(image_b64=image_b64, context=f"파일: {filename}"),
user
)
else:
# 로그/텍스트 분석
try:
text = content.decode('utf-8', errors='replace')
except Exception:
raise HTTPException(400, "파일을 텍스트로 읽을 수 없습니다")
result = await analyze_log(
LogAnalysisRequest(log_text=text, log_type="application"),
user
)
result["filename"] = filename
return result
@router.post("/auto-sr")
async def create_sr_from_analysis(
req: ImageAnalysisRequest,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""이미지 분석 → SR 자동 생성."""
analysis_result = await analyze_image(req, user)
analysis_text = analysis_result.get("analysis", "")
if not analysis_text:
raise HTTPException(400, "분석 결과가 없습니다")
# 심각도 추출
priority = "HIGH" if "HIGH" in analysis_text.upper() or "CRITICAL" in analysis_text.upper() else "MEDIUM"
sr = SRRequest(
title=f"[AI 자동감지] 이미지 분석 이상 감지",
description=f"멀티모달 AI 분석 결과:\n{analysis_text[:500]}\n\n컨텍스트: {req.context or '없음'}",
category="MONITORING",
priority=priority,
status=SRStatus.OPEN,
created_at=datetime.utcnow(),
)
db.add(sr)
await db.commit()
await db.refresh(sr)
return {"ok": True, "sr_id": sr.id, "priority": priority, "analysis_summary": analysis_text[:200]}