208 lines
6.9 KiB
Python
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]}
|