Agent 2.0 exdev 서버 배포 스택

- server-dev start/stop/deploy 및 Gitea push 자동 배포
- local-dev 로컬 개발 환경

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Macbook
2026-07-21 22:57:30 +09:00
commit 4b86b2a660
344 changed files with 45787 additions and 0 deletions
@@ -0,0 +1,399 @@
"""
프롬프트 빌더 모듈
────────────────
LLM 프롬프트 생성 로직
"""
from typing import List, Dict, Any, Optional
class PromptBuilder:
"""LLM 프롬프트 생성기"""
# 시스템 프롬프트 (일반 답변)
SYSTEM_PROMPT = (
"당신은 한국도로공사 채팅상담 챗봇입니다.\n"
"한국도로공사의 고객 문의에 답변하는 전문 상담원으로서 행동하세요.\n"
"\n"
"【대화 이력 활용】\n"
"1. 이전 대화는 현재 질문에 지시어가 있거나 주제가 명확히 이어질 때만 참고하세요.\n"
"2. '그럼', '그거', '그건', '아까', '방금', '그때' 등의 지시어가 있다면:\n"
" - 이전 대화에서 언급된 주제를 파악하세요.\n"
" - 해당 주제와 현재 질문을 연결하여 답변하세요.\n"
"3. 이전 대화와 현재 질문이 같은 주제라면 자연스럽게 이어서 답변하세요.\n"
"4. 현재 질문이 인사, 감사, 종료, 새 주제라면 이전 대화와 억지로 연결하지 말고 현재 질문만 기준으로 답변하세요.\n"
"5. 이전 대화가 없거나 무관하다면 현재 질문만 기준으로 답변하세요.\n"
"\n"
"【답변 작성 원칙】\n"
"1. 제공된 참고자료 중 고객의 상황에 일반적으로 적용 가능한 내용만 사용하세요.\n"
"2. 특정 개인, 특정 지역, 특수한 상황에만 해당하는 내용은 제외하세요.\n"
"3. 일반적인 정책, 절차, 규정에 관한 내용을 우선적으로 활용하세요.\n"
"4. 참고자료가 고객의 질문과 직접적으로 관련이 없거나 특수 사례만 있다면,\n"
" '관련 정보를 정확히 안내드리기 어렵습니다. 한국도로공사 콜센터(1588-2504)로 문의해 주시면 자세히 안내드리겠습니다.'라고 답변하세요.\n"
"5. 참고자료에 없는 내용은 추측하지 마세요.\n"
"\n"
"【특수 케이스 식별 기준 - 다음 내용이 포함된 참고자료는 제외】\n"
"❌ 특정 인명, 차량번호, 계좌번호, 주민등록번호 등 개인정보\n"
"❌ '00아파트', '00지역 주민만', '특정 구간 한정' 등 특정 지역 한정\n"
"❌ '2023년 특별 이벤트', '한시적 조치', '임시 운영' 등 기간 한정\n"
"❌ '귀하의 경우', '고객님만', '해당 건에 한해' 등 개별 맞춤 답변\n"
"❌ '예외적으로', '특별히', '이번 건에 한해서만' 등 특수 조건\n"
"❌ 과거 민원 처리 결과나 개별 사례의 구체적 내용\n"
"\n"
"【용어 및 형식】\n"
"- 답변 시 'KEC' 대신 '한국도로공사'라는 명칭을 사용하세요.\n"
"- 답변은 한국어로 작성하고, 존댓말을 사용하세요.\n"
"- 간결하고 명확하게 답변하세요.\n"
"- 카카오톡 챗봇 응답이므로 3~4문장 이내로 답변하세요.\n"
"- 인사말과 마무리 인사는 생략하세요.\n"
"- 최종 답변에서 '참고자료', '제공된 자료', '주어진 자료', '자료를 확인한 결과' 같은 내부 근거 표현을 사용하지 마세요.\n"
"- 개인정보나 특수 사례가 포함된 참고자료는 절대 언급하지 마세요."
)
DOMAIN_DATA_PRIORITY_PROMPT = (
"\n\n"
"【DB 조회 결과 우선 원칙】\n"
"1. 【DB 조회 결과】가 제공된 경우, 이 데이터는 시스템이 실시간으로 조회한 정확한 결과입니다.\n"
"2. DB 조회 결과가 고객 질문에 답할 수 있으면, 참고자료 유무와 관계없이 DB 조회 결과를 최우선으로 사용하세요.\n"
"3. 참고자료는 DB 조회 결과를 보완하는 용도로만 사용하고, DB 조회 결과와 충돌하면 DB 조회 결과를 따르세요.\n"
"4. DB 조회 결과에 없는 세부 내용은 추측하지 말고, 제공된 값만 자연스럽게 설명하세요."
)
def _format_reference(self, index: int, ref: Dict[str, Any], score: Any = None) -> str:
"""참고자료를 LLM 프롬프트용 텍스트로 변환"""
source = ref.get("source") or "unknown"
category = ref.get("category")
url = ref.get("url")
lines = [
f"[참고자료 {index}]",
f"출처: {source}",
]
if category:
lines.append(f"분류: {category}")
if score is not None:
lines.append(f"점수: {score}")
lines.extend([
f"질문: {ref['q']}",
f"답변: {ref['a']}",
])
if url:
lines.append(f"공식 FAQ URL: {url}")
lines.append("주의: 이 URL은 고객에게 상세 원문 확인 링크로 제공할 수 있습니다.")
return "\n".join(lines) + "\n"
# 시스템 프롬프트 (질문 유도)
GUIDANCE_SYSTEM_PROMPT = (
"당신은 한국도로공사 채팅상담 챗봇입니다.\n"
"한국도로공사 관련 문의를 돕는 챗봇이라는 역할 안에서만 답변하세요.\n"
"\n"
"【상황】\n"
"고객 질문에 대해 검색 가능한 DB/벡터 자료에서 정확한 근거를 찾지 못한 상황입니다.\n"
"\n"
"【중요: 절대 금지 사항】\n"
"❌ 한국도로공사 업무 정보, 제도, 요금, 정책, 운영 현황을 임의로 만들어내지 마세요.\n"
"❌ 추측하거나 가정하여 구체적인 정보를 답변하지 마세요.\n"
"❌ 새로운 질문을 추천하거나 다른 질문을 유도하지 마세요.\n"
"❌ '참고자료', '제공된 자료', '주어진 자료' 같은 내부 표현을 사용하지 마세요.\n"
"\n"
"【답변 원칙】\n"
"1. '넌 누구야?', '자기소개해줘', '뭐 하는 챗봇이야?' 같은 챗봇 정체성/가벼운 질문에는 짧게 답변하세요.\n"
"2. 업무 정보에 대한 질문인데 정확한 근거가 없으면, 현재 확인 가능한 정보가 없다고 안내하고 한국도로공사 콜센터(1588-2504)로 문의하도록 안내하세요.\n"
"3. 이전 대화는 현재 질문에 지시어가 있거나 주제가 명확히 이어질 때만 참고하세요.\n"
"4. 현재 질문이 인사, 감사, 종료, 새 주제이거나 이전 대화와 무관한 경우:\n"
" - 이전 대화와 억지로 연결하지 말고 현재 질문만 기준으로 답변하세요.\n"
"\n"
"【용어 및 형식】\n"
"- 정중하고 친절한 톤을 유지하세요.\n"
"- 답변은 한국어로 작성하고, 존댓말을 사용하세요.\n"
"- 간결하게 2~4문장으로 답변하세요.\n"
"- 인사말과 마무리 인사는 생략하세요.\n"
"- 콜센터 안내 시: 한국도로공사 콜센터(1588-2504)"
)
def build_answer_prompt_messages(
self,
original_query: str,
rewritten_query: Optional[str],
references: List[Dict[str, Any]],
scores: List[float],
conversation_history: List[Dict[str, str]] = None,
emotion_instruction: Optional[str] = None,
emotion_name: Optional[str] = None,
domain_data: Optional[Dict[str, Any]] = None
) -> List[Dict[str, str]]:
"""일반 답변 프롬프트 생성 (Messages Format)
Args:
original_query: 원본 질문
rewritten_query: 재작성된 질문 (없으면 None)
references: 참고자료 리스트
scores: 참고자료 점수
conversation_history: 대화 이력 (messages format)
emotion_instruction: 감정별 추가 지시사항
emotion_name: 감정 이름 (angry, frustrated, worried 등)
domain_data: chatbotApi 도메인 서비스 DB 조회 결과 (있으면 프롬프트에 포함)
Returns:
messages 리스트
"""
has_domain_data = self._has_usable_domain_data(domain_data)
# 시스템 프롬프트에 domainData/감정 지시사항 추가
system_prompt = self.SYSTEM_PROMPT
if has_domain_data:
system_prompt = system_prompt + self.DOMAIN_DATA_PRIORITY_PROMPT
if emotion_instruction:
system_prompt = system_prompt + emotion_instruction
messages = [
{"role": "system", "content": system_prompt}
]
# 대화 이력 추가 (messages format)
if conversation_history:
messages.extend(conversation_history)
# 현재 질문 구성
current_query_parts = []
# 감정 정보 추가 (부정적 감정인 경우)
if emotion_name and emotion_name in ["angry", "frustrated", "worried"]:
emotion_labels = {
"angry": "화난/불만",
"frustrated": "답답한/짜증난",
"worried": "걱정되는/불안한"
}
current_query_parts.extend([
f"【⚠️ 고객 감정 상태: {emotion_labels.get(emotion_name, emotion_name)}】",
"고객이 부정적인 감정을 느끼고 있습니다.",
"이전 대화나 현재 상황에서 무엇이 불편했는지 파악하고,",
"그 점에 대해 구체적으로 공감한 후 실질적인 해결책을 제시하세요.",
""
])
if rewritten_query:
# Query Rewriting이 적용된 경우
current_query_parts.extend([
"【원본 질문】",
original_query,
"",
"【맥락 기반 재작성 질문】",
rewritten_query,
"(이전 대화를 참고하여 재작성된 질문입니다)",
""
])
else:
# Query Rewriting이 없는 경우 원본 질문 명시
current_query_parts.extend([
"【고객 질문】",
original_query,
""
])
# DB 조회 결과 추가 (domain_data가 있을 때만, 참고자료보다 앞에 위치)
if has_domain_data:
summary = domain_data.get("llmSummary")
if summary is not None and str(summary).strip():
current_query_parts.extend([
"【DB 조회 결과】",
"(아래는 시스템 요약(llmSummary)입니다. 참고자료보다 우선하여 활용하세요.)",
str(summary).strip(),
""
])
else:
db_parts = []
for key, value in domain_data.items():
if key not in ("status", "statusMsg", "errorMsg") and value is not None:
db_parts.append(f"- {key}: {value}")
if db_parts:
current_query_parts.extend([
"【DB 조회 결과】",
"(아래는 시스템에서 조회한 정확한 데이터입니다. 참고자료보다 우선하여 활용하세요.)",
"\n".join(db_parts),
""
])
# 참고자료 추가
context_parts = []
for i, (ref, score) in enumerate(zip(references, scores), 1):
context_parts.append(self._format_reference(i, ref, score))
if context_parts:
current_query_parts.extend([
"【참고자료】",
"\n".join(context_parts),
"",
"【답변 지시사항】"
])
else:
current_query_parts.extend([
"【참고자료】",
"제공된 참고자료가 없습니다.",
"",
"【답변 지시사항】"
])
if emotion_name in ["angry", "frustrated", "worried"]:
current_query_parts.append(
f"⚠️ 고객이 {emotion_labels.get(emotion_name, emotion_name)} 상태입니다. "
"답변 시작 부분에 무엇이 불편했는지 구체적으로 언급하며 공감하고, "
"그 후 명확한 해결 방법을 제시하세요."
)
if rewritten_query:
current_query_parts.append("- 원본 질문은 간단하지만, 재작성된 질문의 의도를 파악하여 답변하세요.")
current_query_parts.append("- 답변 시에는 사용자가 실제로 물어본 질문에 대해 자연스럽게 답변하세요.")
if conversation_history:
current_query_parts.append("- 이전 대화는 현재 질문에 지시어가 있거나 주제가 명확히 이어질 때만 참고하세요.")
current_query_parts.append("- 현재 질문이 인사, 감사, 종료, 새 주제라면 이전 대화와 억지로 연결하지 말고 현재 질문만 기준으로 답변하세요.")
if has_domain_data:
current_query_parts.append("- 【DB 조회 결과】를 최우선 근거로 고객의 질문에 답변해주세요.")
current_query_parts.append("- 참고자료가 없거나 질문과 무관해도, DB 조회 결과가 질문에 답할 수 있으면 콜센터 안내로 대체하지 마세요.")
current_query_parts.append("- 참고자료는 DB 조회 결과를 보완할 때만 사용하고, DB 조회 결과와 충돌하면 DB 조회 결과를 따르세요.")
if domain_data and domain_data.get("msgMap"):
current_query_parts.append("- 휴게소 메뉴 목록은 전체를 모두 나열하지 말고, 대표 메뉴 3~5개만 짧게 언급한 뒤 상세 목록은 시스템 응답의 메뉴 목록을 확인하도록 안내하세요.")
current_query_parts.append("- 메뉴가 많거나 방향/매장이 여러 개인 경우에도 답변 본문은 5문장 이내로 간결하게 작성하세요.")
current_query_parts.append("- 공식 FAQ URL이 있는 참고자료가 관련 있다면, 답변 본문에는 간단히 안내하고 시스템 응답의 링크 버튼으로 상세 확인을 유도할 수 있습니다.")
else:
current_query_parts.append("- 위 참고자료를 바탕으로 고객의 질문에 답변해주세요.")
current_query_parts.append("- 참고자료가 질문과 무관하거나 특수 사례만 있다면 콜센터로 안내하세요.")
current_query_parts.append("- 공식 FAQ URL이 있는 참고자료가 관련 있다면, 답변 본문에는 간단히 안내하고 시스템 응답의 링크 버튼으로 상세 확인을 유도할 수 있습니다.")
current_query_parts.append("- 카카오톡 챗봇 응답이므로 3~4문장 이내로 답변하세요.")
current_query_parts.append("- 인사말과 마무리 인사는 생략하세요.")
current_query_parts.append("- 최종 답변에서 '참고자료', '제공된 자료', '주어진 자료', '자료를 확인한 결과' 같은 내부 근거 표현을 사용하지 마세요.")
# 현재 질문 추가
messages.append({
"role": "user",
"content": "\n".join(current_query_parts)
})
return messages
def _has_usable_domain_data(self, domain_data: Optional[Dict[str, Any]]) -> bool:
if not domain_data:
return False
status = domain_data.get("status")
if status is False:
return False
if isinstance(status, str) and status.lower() == "false":
return False
return True
def build_answer_prompt(
self,
original_query: str,
rewritten_query: Optional[str],
references: List[Dict[str, Any]],
scores: List[float],
conversation_context: Optional[str] = None,
emotion_instruction: Optional[str] = None
) -> tuple[str, str]:
"""일반 답변 프롬프트 생성
Args:
original_query: 원본 질문
rewritten_query: 재작성된 질문 (없으면 None)
references: 참고자료 리스트
scores: 참고자료 점수
conversation_context: 대화 이력 텍스트
emotion_instruction: 감정별 추가 지시사항
Returns:
(system_prompt, user_prompt)
"""
# 시스템 프롬프트에 감정 지시사항 추가
system_prompt = self.SYSTEM_PROMPT
if emotion_instruction:
system_prompt = system_prompt + emotion_instruction
user_prompt_parts = []
# 이전 대화 이력
if conversation_context:
user_prompt_parts.append("【이전 대화 이력】")
user_prompt_parts.append("(현재 질문과 명확히 이어지는 경우에만 참고하세요)")
user_prompt_parts.append(conversation_context)
user_prompt_parts.append("")
# 질문 (Query Rewriting 적용 여부에 따라)
if rewritten_query:
user_prompt_parts.extend([
"【사용자의 현재 질문】",
original_query,
"",
"【맥락을 고려한 완전한 질문】",
rewritten_query,
"(이전 대화를 참고하여 재작성된 질문입니다. 이 질문에 대해 답변해주세요.)",
"",
])
else:
user_prompt_parts.extend([
"【현재 질문】",
f"고객 질문: {original_query}",
"",
])
# 참고자료
context_parts = []
for i, (ref, score) in enumerate(zip(references, scores), 1):
context_parts.append(self._format_reference(i, ref, score))
user_prompt_parts.extend([
"【참고자료】",
"\n".join(context_parts),
"",
"【지시사항】"
])
# 추가 지시사항
if rewritten_query:
user_prompt_parts.append("- 사용자의 현재 질문은 간단하지만, 재작성된 완전한 질문의 의도를 파악하여 답변하세요.")
user_prompt_parts.append("- 답변 시에는 사용자가 실제로 물어본 현재 질문에 대해 자연스럽게 답변하세요.")
if conversation_context:
user_prompt_parts.append("- 이전 대화는 현재 질문에 지시어가 있거나 주제가 명확히 이어질 때만 참고하세요.")
user_prompt_parts.append("- 현재 질문이 인사, 감사, 종료, 새 주제라면 이전 대화와 억지로 연결하지 말고 현재 질문만 기준으로 답변하세요.")
user_prompt_parts.append("- 위 참고자료를 바탕으로 고객의 질문에 답변해주세요.")
user_prompt_parts.append("- 참고자료가 질문과 무관하거나 특수 사례만 있다면 콜센터로 안내하세요.")
user_prompt_parts.append("- 공식 FAQ URL이 있는 참고자료가 관련 있다면, 답변 본문에는 간단히 안내하고 시스템 응답의 링크 버튼으로 상세 확인을 유도할 수 있습니다.")
user_prompt_parts.append("- 최종 답변에서 '참고자료', '제공된 자료', '주어진 자료', '자료를 확인한 결과' 같은 내부 근거 표현을 사용하지 마세요.")
return system_prompt, "\n".join(user_prompt_parts)
def build_guidance_prompt(
self,
original_query: str,
conversation_context: Optional[str] = None
) -> tuple[str, str]:
"""질문 유도 프롬프트 생성
Args:
original_query: 원본 질문
conversation_context: 대화 이력 텍스트
Returns:
(system_prompt, user_prompt)
"""
user_prompt_parts = []
# 이전 대화 이력
if conversation_context:
user_prompt_parts.append(conversation_context)
user_prompt_parts.append("")
# 현재 상황
user_prompt_parts.extend([
"【현재 상황】",
f"고객 질문: {original_query}",
"",
"위 질문에 대해 검색 가능한 DB/벡터 자료에서 정확한 근거를 찾지 못했습니다.",
"",
"【지시사항】",
"- 챗봇 정체성이나 가벼운 질문이면 한국도로공사 채팅상담 챗봇이라는 역할 안에서 짧게 답변하세요.",
"- 업무 정보에 대한 질문이면 현재 정확한 정보를 확인하기 어렵다고 안내하고 콜센터로 연결하세요.",
"- 임의로 새로운 정보를 만들어내지 마세요.",
"- 새로운 질문을 추천하거나 다른 질문을 유도하지 마세요."
])
return self.GUIDANCE_SYSTEM_PROMPT, "\n".join(user_prompt_parts)