feat: AI 서비스 및 AI 어시스턴트 전용 페이지 추가
- ai-service: Ollama 기반 AI 서비스 (분류, 시맨틱 검색, RAG Q&A, 패턴 분석) - AI 어시스턴트 페이지: 채팅형 Q&A, 시맨틱 검색, 패턴 분석, 분류 테스트 - 권한 시스템에 ai_assistant 페이지 등록 (기본 비활성) - 기존 페이지에 AI 기능 통합 (대시보드, 수신함, 관리함) - docker-compose, gateway, nginx 설정 업데이트 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
164
ai-service/services/rag_service.py
Normal file
164
ai-service/services/rag_service.py
Normal file
@@ -0,0 +1,164 @@
|
||||
from services.ollama_client import ollama_client
|
||||
from services.embedding_service import search_similar_by_text, build_document_text
|
||||
from services.db_client import get_issue_by_id
|
||||
|
||||
|
||||
def _load_prompt(path: str) -> str:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def _format_retrieved_issues(results: list[dict]) -> str:
|
||||
if not results:
|
||||
return "관련 과거 사례가 없습니다."
|
||||
lines = []
|
||||
for i, r in enumerate(results, 1):
|
||||
meta = r.get("metadata", {})
|
||||
similarity = round(r.get("similarity", 0) * 100)
|
||||
doc = (r.get("document", ""))[:500]
|
||||
cat = meta.get("category", "")
|
||||
dept = meta.get("responsible_department", "")
|
||||
status = meta.get("review_status", "")
|
||||
has_sol = meta.get("has_solution", "false")
|
||||
date = meta.get("report_date", "")
|
||||
issue_id = meta.get("issue_id", r["id"])
|
||||
lines.append(
|
||||
f"[사례 {i}] No.{issue_id} (유사도 {similarity}%)\n"
|
||||
f" 분류: {cat} | 부서: {dept} | 상태: {status} | 날짜: {date} | 해결여부: {'O' if has_sol == 'true' else 'X'}\n"
|
||||
f" 내용: {doc}"
|
||||
)
|
||||
return "\n\n".join(lines)
|
||||
|
||||
|
||||
async def rag_suggest_solution(issue_id: int) -> dict:
|
||||
"""과거 유사 이슈의 해결 사례를 참고하여 해결방안을 제안"""
|
||||
issue = get_issue_by_id(issue_id)
|
||||
if not issue:
|
||||
return {"available": False, "error": "이슈를 찾을 수 없습니다"}
|
||||
|
||||
doc_text = build_document_text(issue)
|
||||
if not doc_text.strip():
|
||||
return {"available": False, "error": "이슈 내용이 비어있습니다"}
|
||||
|
||||
# 해결 완료된 유사 이슈 검색
|
||||
similar = await search_similar_by_text(
|
||||
doc_text, n_results=5, filters={"has_solution": "true"}
|
||||
)
|
||||
# 해결 안 된 것도 포함 (참고용)
|
||||
if len(similar) < 3:
|
||||
all_similar = await search_similar_by_text(doc_text, n_results=5)
|
||||
seen = {r["id"] for r in similar}
|
||||
for r in all_similar:
|
||||
if r["id"] not in seen:
|
||||
similar.append(r)
|
||||
if len(similar) >= 5:
|
||||
break
|
||||
|
||||
context = _format_retrieved_issues(similar)
|
||||
template = _load_prompt("prompts/rag_suggest_solution.txt")
|
||||
prompt = template.format(
|
||||
description=issue.get("description", ""),
|
||||
detail_notes=issue.get("detail_notes", ""),
|
||||
category=issue.get("category", ""),
|
||||
retrieved_cases=context,
|
||||
)
|
||||
|
||||
response = await ollama_client.generate_text(prompt)
|
||||
return {
|
||||
"available": True,
|
||||
"issue_id": issue_id,
|
||||
"suggestion": response,
|
||||
"referenced_issues": [
|
||||
{
|
||||
"id": r.get("metadata", {}).get("issue_id", r["id"]),
|
||||
"similarity": round(r.get("similarity", 0) * 100),
|
||||
"has_solution": r.get("metadata", {}).get("has_solution", "false") == "true",
|
||||
}
|
||||
for r in similar
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
async def rag_ask(question: str, project_id: int = None) -> dict:
|
||||
"""부적합 데이터를 기반으로 자연어 질문에 답변"""
|
||||
# 프로젝트 필터 없이 전체 데이터에서 검색 (과거 미지정 데이터 포함)
|
||||
results = await search_similar_by_text(
|
||||
question, n_results=15, filters=None
|
||||
)
|
||||
context = _format_retrieved_issues(results)
|
||||
|
||||
template = _load_prompt("prompts/rag_qa.txt")
|
||||
prompt = template.format(
|
||||
question=question,
|
||||
retrieved_cases=context,
|
||||
)
|
||||
|
||||
response = await ollama_client.generate_text(prompt)
|
||||
return {
|
||||
"available": True,
|
||||
"answer": response,
|
||||
"sources": [
|
||||
{
|
||||
"id": r.get("metadata", {}).get("issue_id", r["id"]),
|
||||
"similarity": round(r.get("similarity", 0) * 100),
|
||||
"snippet": (r.get("document", ""))[:100],
|
||||
}
|
||||
for r in results
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
async def rag_analyze_pattern(description: str, n_results: int = 10) -> dict:
|
||||
"""유사 부적합 패턴 분석 — 반복되는 문제인지, 근본 원인은 무엇인지"""
|
||||
results = await search_similar_by_text(description, n_results=n_results)
|
||||
context = _format_retrieved_issues(results)
|
||||
|
||||
template = _load_prompt("prompts/rag_pattern.txt")
|
||||
prompt = template.format(
|
||||
description=description,
|
||||
retrieved_cases=context,
|
||||
total_similar=len(results),
|
||||
)
|
||||
|
||||
response = await ollama_client.generate_text(prompt)
|
||||
return {
|
||||
"available": True,
|
||||
"analysis": response,
|
||||
"similar_count": len(results),
|
||||
"sources": [
|
||||
{
|
||||
"id": r.get("metadata", {}).get("issue_id", r["id"]),
|
||||
"similarity": round(r.get("similarity", 0) * 100),
|
||||
"category": r.get("metadata", {}).get("category", ""),
|
||||
}
|
||||
for r in results
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
async def rag_classify_with_context(description: str, detail_notes: str = "") -> dict:
|
||||
"""과거 사례를 참고하여 더 정확한 분류 수행 (기존 classify 강화)"""
|
||||
query = f"{description} {detail_notes}".strip()
|
||||
similar = await search_similar_by_text(query, n_results=5)
|
||||
context = _format_retrieved_issues(similar)
|
||||
|
||||
template = _load_prompt("prompts/rag_classify.txt")
|
||||
prompt = template.format(
|
||||
description=description,
|
||||
detail_notes=detail_notes,
|
||||
retrieved_cases=context,
|
||||
)
|
||||
|
||||
raw = await ollama_client.generate_text(prompt)
|
||||
import json
|
||||
try:
|
||||
start = raw.find("{")
|
||||
end = raw.rfind("}") + 1
|
||||
if start >= 0 and end > start:
|
||||
result = json.loads(raw[start:end])
|
||||
result["rag_enhanced"] = True
|
||||
result["referenced_count"] = len(similar)
|
||||
return {"available": True, **result}
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return {"available": True, "raw_response": raw, "rag_enhanced": True}
|
||||
Reference in New Issue
Block a user