CMDB 자동 발견 (4개): - autodiscovery.py: SSH 네트워크 스캔 + CMDB 자동 등록 - snmp_discovery.py: SNMP v2c/v3 장비 자동 발견 - dependency_map.py: 서비스 의존성 자동 매핑 (netstat) - config_inventory.py: 서버 인벤토리 자동 수집 (SSH) NL 쿼리 엔진 (3개): - nlquery.py: Text-to-SQL (SELECT 전용, DML 차단) - op_assistant.py: Multi-turn 대화형 운영 어시스턴트 - query_history.py: 쿼리 이력·즐겨찾기·공유 구성 드리프트 (3개): - drift_detection.py: 골든 구성 vs 실제 비교·SR 자동 생성 - golden_config.py: 내장 CSAP 템플릿 + 버전 관리 - auto_remediation.py: 승인 기반 자동 교정 + 롤백 멀티클라우드 (4개): - multicloud.py: 통합 관제 (NCloud+AWS+KT) - aws_connector.py: AWS SigV4 직접 서명 연동 - cost_optimizer.py: AI 비용 최적화 권고 - cloud_migration.py: On-prem→K-Cloud 체크리스트 공공기관 특화 (6개): - narasajang.py: 나라장터 OpenAPI 연동 - public_api_hub.py: data.go.kr KISA·기상청 허브 - isp_support.py: ISP 수립 지원 + AI 보고서 - network_zone.py: 행정망/인터넷망 분리 관리 - k_cloud.py: 정부 K-Cloud 전환 자동화 - e_procurement.py: 전자조달 계약·검수·납품 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
209 lines
6.5 KiB
Python
209 lines
6.5 KiB
Python
"""
|
|
대화형 운영 어시스턴트 — Multi-turn 컨텍스트 유지
|
|
|
|
여러 번의 대화로 복잡한 운영 쿼리를 처리한다.
|
|
이전 대화 내용을 기억하고 후속 질문에 맥락 있게 답변.
|
|
|
|
엔드포인트:
|
|
POST /api/assistant/chat — 대화 메시지 전송
|
|
GET /api/assistant/sessions — 대화 세션 목록
|
|
GET /api/assistant/sessions/{id} — 특정 세션 대화 이력
|
|
DELETE /api/assistant/sessions/{id} — 세션 종료
|
|
POST /api/assistant/quick — 빠른 질의 (단일 턴)
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
import httpx
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel, Field
|
|
from sqlalchemy import select, desc
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from core.auth import get_current_user
|
|
from database import get_db
|
|
from models import User, AssistantSession, AssistantMessage
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/api/assistant", tags=["운영 어시스턴트"])
|
|
|
|
OLLAMA_URL = "http://localhost:11434"
|
|
CHAT_MODEL = "llama3"
|
|
MAX_HISTORY = 10 # 컨텍스트에 포함할 최근 메시지 수
|
|
|
|
SYSTEM_PROMPT = """당신은 GUARDiA ITSM 운영 전문 어시스턴트입니다.
|
|
|
|
역할:
|
|
- IT 운영 문제 진단 및 해결 방안 제시
|
|
- SR·인시던트·KPI 데이터 분석 및 해석
|
|
- 장애 예방을 위한 선제적 권고
|
|
- 공공기관 IT 운영 모범 사례 안내
|
|
|
|
원칙:
|
|
- 온프레미스 환경 우선 (외부 클라우드 API 제안 금지)
|
|
- 에이전트리스 접근 권장 (SSH/SFTP만 사용)
|
|
- 보안 정보(IP, 비밀번호) 응답에 포함 금지
|
|
- 한국어로만 답변
|
|
- 3~5문장으로 간결하게"""
|
|
|
|
|
|
class ChatRequest(BaseModel):
|
|
session_id: Optional[int] = None # None이면 새 세션 생성
|
|
message: str = Field(..., min_length=1, max_length=2000)
|
|
context_data: Optional[dict] = None # 추가 컨텍스트 (SR ID, 서버명 등)
|
|
|
|
|
|
async def _call_ollama(messages: list[dict]) -> str:
|
|
try:
|
|
async with httpx.AsyncClient(timeout=30) as client:
|
|
r = await client.post(f"{OLLAMA_URL}/api/chat", json={
|
|
"model": CHAT_MODEL,
|
|
"messages": messages,
|
|
"stream": False,
|
|
})
|
|
if r.status_code == 200:
|
|
return r.json().get("message", {}).get("content", "").strip()
|
|
except Exception as e:
|
|
logger.warning(f"Ollama 호출 실패: {e}")
|
|
return "현재 AI 어시스턴트를 사용할 수 없습니다. 잠시 후 다시 시도하세요."
|
|
|
|
|
|
@router.post("/chat")
|
|
async def chat(
|
|
req: ChatRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user),
|
|
):
|
|
"""대화 메시지 전송 — Multi-turn 컨텍스트 유지."""
|
|
# 세션 조회 또는 생성
|
|
if req.session_id:
|
|
sess_row = await db.execute(
|
|
select(AssistantSession).where(
|
|
AssistantSession.id == req.session_id,
|
|
AssistantSession.user_id == user.id,
|
|
)
|
|
)
|
|
session = sess_row.scalar_one_or_none()
|
|
if not session:
|
|
raise HTTPException(404, "세션 없음")
|
|
else:
|
|
session = AssistantSession(
|
|
user_id=user.id,
|
|
title=req.message[:50],
|
|
created_at=datetime.utcnow(),
|
|
)
|
|
db.add(session)
|
|
await db.commit()
|
|
await db.refresh(session)
|
|
|
|
# 이전 대화 이력 로드
|
|
hist_rows = await db.execute(
|
|
select(AssistantMessage).where(AssistantMessage.session_id == session.id)
|
|
.order_by(AssistantMessage.created_at).limit(MAX_HISTORY * 2)
|
|
)
|
|
history = hist_rows.scalars().all()
|
|
|
|
# Ollama messages 구성
|
|
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
|
|
if req.context_data:
|
|
ctx = f"\n현재 컨텍스트: {req.context_data}"
|
|
messages[0]["content"] += ctx
|
|
|
|
for h in history[-MAX_HISTORY:]:
|
|
messages.append({"role": h.role, "content": h.content})
|
|
messages.append({"role": "user", "content": req.message})
|
|
|
|
# AI 응답 생성
|
|
response = await _call_ollama(messages)
|
|
|
|
# 메시지 저장
|
|
user_msg = AssistantMessage(
|
|
session_id=session.id, role="user",
|
|
content=req.message, created_at=datetime.utcnow()
|
|
)
|
|
ai_msg = AssistantMessage(
|
|
session_id=session.id, role="assistant",
|
|
content=response, created_at=datetime.utcnow()
|
|
)
|
|
db.add(user_msg)
|
|
db.add(ai_msg)
|
|
session.updated_at = datetime.utcnow()
|
|
await db.commit()
|
|
|
|
return {
|
|
"session_id": session.id,
|
|
"response": response,
|
|
"turn": len(history) // 2 + 1,
|
|
}
|
|
|
|
|
|
@router.post("/quick")
|
|
async def quick_query(
|
|
message: str,
|
|
user: User = Depends(get_current_user),
|
|
):
|
|
"""단일 턴 빠른 질의 (세션 저장 없음)."""
|
|
messages = [
|
|
{"role": "system", "content": SYSTEM_PROMPT},
|
|
{"role": "user", "content": message},
|
|
]
|
|
response = await _call_ollama(messages)
|
|
return {"response": response}
|
|
|
|
|
|
@router.get("/sessions")
|
|
async def list_sessions(
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user),
|
|
):
|
|
rows = await db.execute(
|
|
select(AssistantSession).where(AssistantSession.user_id == user.id)
|
|
.order_by(desc(AssistantSession.updated_at)).limit(20)
|
|
)
|
|
sessions = rows.scalars().all()
|
|
return [
|
|
{"id": s.id, "title": s.title,
|
|
"created_at": s.created_at, "updated_at": s.updated_at}
|
|
for s in sessions
|
|
]
|
|
|
|
|
|
@router.get("/sessions/{session_id}")
|
|
async def get_session(
|
|
session_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user),
|
|
):
|
|
rows = await db.execute(
|
|
select(AssistantMessage).where(AssistantMessage.session_id == session_id)
|
|
.order_by(AssistantMessage.created_at)
|
|
)
|
|
msgs = rows.scalars().all()
|
|
return [
|
|
{"role": m.role, "content": m.content, "created_at": m.created_at}
|
|
for m in msgs
|
|
]
|
|
|
|
|
|
@router.delete("/sessions/{session_id}")
|
|
async def delete_session(
|
|
session_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user),
|
|
):
|
|
sess_row = await db.execute(
|
|
select(AssistantSession).where(
|
|
AssistantSession.id == session_id,
|
|
AssistantSession.user_id == user.id,
|
|
)
|
|
)
|
|
session = sess_row.scalar_one_or_none()
|
|
if not session:
|
|
raise HTTPException(404)
|
|
await db.delete(session)
|
|
await db.commit()
|
|
return {"ok": True}
|