callcenter 프로젝트 완료
- 상담 기록 분석 및 요약 - 상담 기록 db 저장 - 상담 평가 - 상담 평가 db 저장
This commit is contained in:
@@ -3,6 +3,9 @@ from starlette.staticfiles import StaticFiles
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from backend.repository.db_init import Base, SessionLocal, engine
|
||||
|
||||
from backend.routers.evalu_router import router as evalu_router
|
||||
from backend.routers.assistant_router import router as assistant_router
|
||||
from backend.routers.call_router import router as call_router
|
||||
from backend.repository.seed import seed_customers
|
||||
|
||||
@@ -31,4 +34,6 @@ app = FastAPI(title="상담 LLM", version="1.0", lifespan=lifespan)
|
||||
app.mount("/static", StaticFiles(directory="backend/static"), name="static")
|
||||
|
||||
# 라우터 등록
|
||||
app.include_router(call_router)
|
||||
app.include_router(call_router)
|
||||
app.include_router(assistant_router)
|
||||
app.include_router(evalu_router)
|
||||
@@ -25,4 +25,48 @@ SUMMARY_SYSTEM_PROMPT = """
|
||||
|
||||
7. resolution
|
||||
- 상담사가 제공한 해결 방법
|
||||
"""
|
||||
|
||||
CALL_ASSISTANT_PROMPT = """
|
||||
당신은 콜센터 상담 지원 ai 입니다.
|
||||
|
||||
제공된 정보만 활용하여 답변하세요.
|
||||
참고 정보가 부족하면 추측하지 말고 추가 확인이 필요하다고 답변하세요.
|
||||
|
||||
[지식문서]
|
||||
{sim_context}
|
||||
|
||||
[고객 상담 이력]
|
||||
{customer_text}
|
||||
|
||||
[질문]
|
||||
{question}
|
||||
"""
|
||||
|
||||
CALL_EVALUATION_PROMPT = """
|
||||
당신은 콜센터 QA 평가 전문가입니다.
|
||||
|
||||
다음 항목을 평가하세요.
|
||||
1. 고객 신원 확인
|
||||
2. 공감 표현
|
||||
3. 문제 해결
|
||||
4. 설문 조사 안내
|
||||
|
||||
각 항목에 대해
|
||||
- True / False
|
||||
- 판단 근거(reason)
|
||||
|
||||
를 제공하세요.
|
||||
|
||||
특히 공감 표현은 아래와 같이 명시적 표현이 있을 때만 True로 판단하세요.
|
||||
|
||||
예시:
|
||||
- 불편을 드려 죄송합니다.
|
||||
- 많이 답답하셨겠습니다.
|
||||
- 불편을 겪으셔서 죄송합니다.
|
||||
|
||||
단순히 문제 해결 절차를 안내하는 경우는 공감 표현으로 간주하지 마세요.
|
||||
|
||||
상담기록:
|
||||
{transcript}
|
||||
"""
|
||||
@@ -27,4 +27,27 @@ class CallHistory(Base):
|
||||
customer_issue:Mapped[str]
|
||||
resolution:Mapped[str]
|
||||
# created_at:Mapped[datetime] = mapped_column(server_default=func.now(), nullable=False)
|
||||
created_at:Mapped[datetime] = mapped_column(default=datetime.now, nullable=False)
|
||||
created_at:Mapped[datetime] = mapped_column(default=datetime.now, nullable=False)
|
||||
|
||||
# 상담 평가 저장
|
||||
class CallEvaluation(Base):
|
||||
__tablename__ = 'call_evaluation'
|
||||
|
||||
evaluation_id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
call_id: Mapped[int] = mapped_column(ForeignKey('call_history.call_id'))
|
||||
|
||||
# 평가 기준
|
||||
# 고객 신원 확인
|
||||
identity_verification:Mapped[bool] = mapped_column(default=False)
|
||||
identity_verification_reason:Mapped[str]
|
||||
# 공감 표현
|
||||
empathy:Mapped[bool] = mapped_column(default=False)
|
||||
empathy_reason:Mapped[str]
|
||||
# 문제 해결
|
||||
issue_resolution:Mapped[bool] = mapped_column(default=False)
|
||||
issue_resolution_reason:Mapped[str]
|
||||
# 설문 조사 안내
|
||||
survey_guidance:Mapped[bool] = mapped_column(default=False)
|
||||
survey_guidance_reason:Mapped[str]
|
||||
score:Mapped[int]
|
||||
created_at:Mapped[datetime] = mapped_column(default=datetime.now, nullable=False)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from backend.schemas.assistant_schema import AssistantResponse, AssistantRequest
|
||||
from backend.schemas.summary_schema import SummaryRequest, CallSummary, CallRequest
|
||||
from backend.services.assistant_service import answer_assistant_question
|
||||
from backend.services.call_service import summary_call, create_call_history
|
||||
from sqlalchemy.orm import Session
|
||||
from backend.repository.db_init import get_db
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/assistant", tags=["Assistant"])
|
||||
|
||||
@router.post("", response_model=AssistantResponse)
|
||||
def ask_assistant(req:AssistantRequest, db:Session = Depends(get_db)):
|
||||
return answer_assistant_question(customer_id = req.customer_id, question = req.question, db = db)
|
||||
@@ -13,6 +13,5 @@ def generate_summary(req:SummaryRequest):
|
||||
return summary_call(req.transcript)
|
||||
|
||||
@router.post("/save", response_model=CallSummary)
|
||||
# @router.post("/save", response_model=CallRequest)
|
||||
def create_call(req: CallRequest, db:Session = Depends(get_db)):
|
||||
return create_call_history(req, db)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from backend.schemas.evaluation_schema import EvaluationResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from backend.repository.db_init import get_db
|
||||
from backend.services.evalu_service import get_call_evaluation
|
||||
|
||||
router = APIRouter(prefix="/api/evaluation", tags=["evaluation"])
|
||||
|
||||
@router.get("/{call_id}", response_model=EvaluationResponse)
|
||||
def read_evaluation(call_id : int, db:Session = Depends(get_db)):
|
||||
return get_call_evaluation(call_id = call_id, db = db)
|
||||
@@ -0,0 +1,8 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
class AssistantRequest(BaseModel):
|
||||
customer_id: int
|
||||
question: str
|
||||
|
||||
class AssistantResponse(BaseModel):
|
||||
answer: str
|
||||
@@ -0,0 +1,14 @@
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
|
||||
class EvaluationResponse(BaseModel):
|
||||
identity_verification:bool
|
||||
identity_verification_reason:str
|
||||
empathy:bool
|
||||
empathy_reason:str
|
||||
issue_resolution:bool
|
||||
issue_resolution_reason:str
|
||||
survey_guidance:bool
|
||||
survey_guidance_reason:str
|
||||
score: int
|
||||
created_at: datetime
|
||||
@@ -25,4 +25,15 @@ class CallCreate(BaseModel):
|
||||
category: str
|
||||
sentiment: str
|
||||
customer_issue: str
|
||||
resolution: str
|
||||
resolution: str
|
||||
|
||||
# 평가 스키마
|
||||
class CallEvaluationResponse(BaseModel):
|
||||
identity_verification:bool
|
||||
identity_verification_reason:str
|
||||
empathy:bool
|
||||
empathy_reason:str
|
||||
issue_resolution:bool
|
||||
issue_resolution_reason:str
|
||||
survey_guidance:bool
|
||||
survey_guidance_reason:str
|
||||
@@ -1,4 +1,4 @@
|
||||
from langchain_community.vectorstores import Chroma
|
||||
from langchain_chroma import Chroma
|
||||
from backend.ai.embedding import watson_embedding
|
||||
from langchain_core.documents import Document
|
||||
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
from backend.ai.llm import hugging_llm
|
||||
from langchain_core.prompts import ChatPromptTemplate
|
||||
from backend.prompts.all_prompt import SUMMARY_SYSTEM_PROMPT, CALL_ASSISTANT_PROMPT
|
||||
from backend.repository.models import CallHistory
|
||||
from backend.schemas.summary_schema import CallSummary, CallCreate
|
||||
from sqlalchemy.orm import Session
|
||||
from backend.schemas.assistant_schema import AssistantRequest
|
||||
from langchain_chroma import Chroma
|
||||
from backend.ai.embedding import watson_embedding
|
||||
from langchain_core.output_parsers import StrOutputParser
|
||||
|
||||
# 질의 응답
|
||||
def answer_assistant_question(customer_id:int, question:str, db:Session):
|
||||
# 1 단계 : 벡터 DB에서 질의
|
||||
# 벡터db 불러오기
|
||||
vectorstore = Chroma(embedding_function=watson_embedding, persist_directory="./vectordb")
|
||||
# as_retriever()
|
||||
retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
|
||||
# invoke() = docs => page_content join
|
||||
docs = retriever.invoke(question)
|
||||
sim_context = "\n\n".join(doc.page_content for doc in docs)
|
||||
# 2 단계 : DB 검색
|
||||
# 고객이 이전에 질문한 내역을 추출
|
||||
if customer_id:
|
||||
histories = db.query(CallHistory).filter(CallHistory.customer_id == customer_id).order_by(CallHistory.created_at.desc()).limit(5).all()
|
||||
|
||||
# 문제, 해결 컬럼만 문자열로 추출
|
||||
customer_text ="\n".join([f"""
|
||||
문제: {h.customer_issue}\n
|
||||
해결: {h.resolution}
|
||||
""" for h in histories])
|
||||
|
||||
# 1, 2 단계 => LLM => 답변 생성
|
||||
prompt = ChatPromptTemplate.from_template(CALL_ASSISTANT_PROMPT)
|
||||
chain = prompt | hugging_llm | StrOutputParser()
|
||||
result = chain.invoke({"sim_context": sim_context, "customer_text": customer_text, "question": question})
|
||||
|
||||
return {"answer" : result}
|
||||
# return AssistantRequest(answer=result)
|
||||
@@ -1,8 +1,8 @@
|
||||
from backend.ai.llm import hugging_llm
|
||||
from langchain_core.prompts import ChatPromptTemplate
|
||||
from backend.prompts.all_prompt import SUMMARY_SYSTEM_PROMPT
|
||||
from backend.repository.models import CallHistory
|
||||
from backend.schemas.summary_schema import CallSummary, CallCreate
|
||||
from backend.prompts.all_prompt import SUMMARY_SYSTEM_PROMPT, CALL_EVALUATION_PROMPT
|
||||
from backend.repository.models import CallHistory, CallEvaluation
|
||||
from backend.schemas.summary_schema import CallSummary, CallCreate, CallEvaluationResponse
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from backend.schemas.summary_schema import CallRequest
|
||||
@@ -41,15 +41,51 @@ def save_call_history(db, data):
|
||||
|
||||
return history
|
||||
|
||||
def evaluate_call():
|
||||
def evaluate_call(transcript:str):
|
||||
"""상담 평가"""
|
||||
structured_llm = hugging_llm.with_structured_output(CallEvaluationResponse)
|
||||
prompt = ChatPromptTemplate.from_template(CALL_EVALUATION_PROMPT)
|
||||
|
||||
pass
|
||||
chain = prompt | structured_llm
|
||||
return chain.invoke({"transcript":transcript})
|
||||
|
||||
def save_call_evaluation():
|
||||
def calculate_score(evaluation):
|
||||
score = 0
|
||||
|
||||
if evaluation.identity_verification:
|
||||
score += 25
|
||||
|
||||
if evaluation.empathy:
|
||||
score += 25
|
||||
|
||||
if evaluation.issue_resolution:
|
||||
score += 25
|
||||
|
||||
if evaluation.survey_guidance:
|
||||
score += 25
|
||||
|
||||
return score
|
||||
|
||||
def save_call_evaluation(db:Session, call_id:int, evaluation:CallEvaluationResponse):
|
||||
"""상담 평가 내용 저장"""
|
||||
score = calculate_score(evaluation)
|
||||
|
||||
entity = CallEvaluation(
|
||||
call_id = call_id,
|
||||
identity_verification = evaluation.identity_verification,
|
||||
identity_verification_reason = evaluation.identity_verification_reason,
|
||||
empathy = evaluation.empathy,
|
||||
empathy_reason = evaluation.empathy_reason,
|
||||
issue_resolution = evaluation.issue_resolution,
|
||||
issue_resolution_reason = evaluation.issue_resolution_reason,
|
||||
survey_guidance = evaluation.survey_guidance,
|
||||
survey_guidance_reason = evaluation.survey_guidance_reason,
|
||||
score = score,
|
||||
)
|
||||
db.add(entity)
|
||||
db.commit()
|
||||
db.refresh(entity)
|
||||
|
||||
pass
|
||||
|
||||
def create_call_history(req: CallRequest, db:Session):
|
||||
# 요약
|
||||
@@ -66,7 +102,19 @@ def create_call_history(req: CallRequest, db:Session):
|
||||
customer_issue = summary.customer_issue,
|
||||
resolution = summary.resolution
|
||||
)
|
||||
history = save_call_history(db = db, data = call_data)
|
||||
|
||||
return save_call_history(db = db, data = call_data)
|
||||
|
||||
# 상담 평가
|
||||
evaluation = evaluate_call(req.transcript)
|
||||
# 평가 저장
|
||||
save_call_evaluation(db = db, call_id=history.call_id, evaluation=evaluation)
|
||||
|
||||
return CallSummary(
|
||||
summary=summary.summary,
|
||||
keywords=summary.keywords,
|
||||
category=summary.category,
|
||||
sentiment=summary.sentiment,
|
||||
action_items=summary.action_items,
|
||||
customer_issue=summary.customer_issue,
|
||||
resolution=summary.resolution,
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
from sqlalchemy.orm import Session
|
||||
from backend.repository.models import CallEvaluation
|
||||
from backend.schemas.evaluation_schema import EvaluationResponse
|
||||
|
||||
|
||||
# 질의 응답
|
||||
def get_call_evaluation(call_id:int, db:Session):
|
||||
# DB 검색 - call_id와 일치하는 정보 추출
|
||||
if call_id:
|
||||
evaluation = db.query(CallEvaluation).filter(CallEvaluation.call_id == call_id).first()
|
||||
|
||||
return EvaluationResponse(
|
||||
identity_verification=evaluation.identity_verification,
|
||||
identity_verification_reason=evaluation.identity_verification_reason,
|
||||
empathy=evaluation.empathy,
|
||||
empathy_reason=evaluation.empathy_reason,
|
||||
issue_resolution=evaluation.issue_resolution,
|
||||
issue_resolution_reason=evaluation.issue_resolution_reason,
|
||||
survey_guidance=evaluation.survey_guidance,
|
||||
survey_guidance_reason=evaluation.survey_guidance_reason,
|
||||
score=evaluation.score,
|
||||
created_at=evaluation.created_at,
|
||||
)
|
||||
Reference in New Issue
Block a user