Skip to content

Commit e18c741

Browse files
Merge pull request #107 from AUSG/feat/qa-thread-context-prompt
feat(qa): 현재 스레드 맥락 반영 + ANNA 페르소나 프롬프트 개선
2 parents 86931e4 + a214544 commit e18c741

1 file changed

Lines changed: 66 additions & 4 deletions

File tree

src/handler/bigchat/question_response.py

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# 다국어(한국어) 시스템 프롬프트 문자열이 길어, 이 파일은 줄길이(E501) 검사를 예외 처리한다.
2+
# ruff: noqa: E501
13
import logging
24
import re
35

@@ -9,15 +11,30 @@
911
# q) 이후의 질문을 추출하는 정규식
1012
QUESTION_PATTERN = re.compile(r"q\)\s*(.+)", re.IGNORECASE | re.DOTALL)
1113

12-
DEFAULT_SYSTEM_PROMPT = """당신은 AUSG(AWSKRUG University Student Group) 커뮤니티의 친절한 도우미 ANNA입니다.
13-
질문에 대해 명확하고 도움이 되는 답변을 제공해주세요. 질문에 맞게 영어 또는 한국어로 답변해주세요.
14-
답변은 간결하면서도 충분한 정보를 담아주세요."""
14+
DEFAULT_SYSTEM_PROMPT = """너는 AUSG(AWSKRUG University Student Group) 커뮤니티의 멤버 같은 AI, ANNA야.
15+
딱딱한 봇이 아니라 센스 있고 유쾌한 커뮤니티 멤버 한 명처럼 답해.
16+
함께 주어지는 '현재 진행 중인 대화'와 '과거 커뮤니티 대화 기록'을 근거로 답한다.
17+
18+
원칙:
19+
- 기본은 한국어로 친근하고 위트 있게 답한다. 다만 한국어만 고집할 필요는 없어 — 영어로 물으면 영어로 답해도 되고, 기술 용어·고유명사는 원어 그대로 써도 된다. 단, 과하거나 억지 드립은 금물.
20+
- 재미는 양념이고 정확도가 우선이다. 근거를 종합해 핵심을 먼저 짚고, 링크·일정 등 구체 정보는 정확히 옮긴다.
21+
- '현재 진행 중인 대화'가 있으면 그 맥락을 우선 반영해 질문 의도에 맞게 답한다.
22+
- 근거에 답이 없거나 불충분하면 지어내지 말고, 유쾌하게라도 "그건 기록에 없네요 ㅎㅎ"처럼 솔직히 모른다고 한다.
23+
- 'Context'·'문서'·'ID' 같은 내부 표현은 노출하지 말고 자연스러운 문장으로 답한다.
24+
- 인사·정체성 질문('살아있어?' 등)엔 근거 뒤지지 말고 ANNA답게 센스 있게 짧게 받아친다.
25+
- 장황하지 않게, 간결하게."""
1526

1627

1728
class QuestionResponse(MentionHandler):
29+
# 스레드 맥락 과다 방지: 가장 최근부터 이 글자 수까지만 포함
30+
THREAD_CONTEXT_MAX_CHARS = 4000
31+
1832
def __init__(self, event, slack_client, qa_client: QAClient):
1933
self.text = event["text"]
2034
self.ts = event["ts"]
35+
self.channel = event.get("channel")
36+
# 스레드 안에서 멘션된 경우에만 thread_ts 가 존재
37+
self.thread_ts = event.get("thread_ts")
2138
self.slack_client = slack_client
2239
self.qa_client = qa_client
2340

@@ -35,13 +52,58 @@ def handle_mention(self):
3552

3653
logger.info(f"Processing question: {question[:100]}...")
3754

38-
answer = self.qa_client.chat(question=question, system_prompt=DEFAULT_SYSTEM_PROMPT)
55+
thread_context = self._fetch_thread_context()
56+
if thread_context:
57+
logger.info(
58+
"[q)] thread context (%d chars):\n%s",
59+
len(thread_context),
60+
thread_context,
61+
)
62+
augmented = (
63+
f"[현재 진행 중인 대화]\n{thread_context}\n\n" f"[위 대화에 대한 질문] {question}"
64+
)
65+
else:
66+
logger.info("[q)] no thread context (top-level mention)")
67+
augmented = question
68+
69+
answer = self.qa_client.chat(
70+
question=augmented, system_prompt=DEFAULT_SYSTEM_PROMPT
71+
)
3972
if answer is None:
4073
answer = "흐음~ 나도 잘 모르는 일인걸? 오거나이저를 찾아가볼까?"
74+
logger.info("[q)] question=%r | answer=%r", question, answer)
4175
self.slack_client.send_message(msg=answer, ts=self.ts)
4276

4377
return True
4478

79+
def _fetch_thread_context(self) -> str:
80+
"""멘션이 스레드 안에서 일어난 경우, 그 스레드의 대화를 맥락으로 수집."""
81+
if not self.channel or not self.thread_ts:
82+
return ""
83+
try:
84+
messages = self.slack_client.get_replies(
85+
channel=self.channel, thread_ts=self.thread_ts
86+
)
87+
except Exception as e: # noqa: BLE001
88+
logger.warning("Failed to fetch thread context: %s", e)
89+
return ""
90+
91+
lines = []
92+
for m in messages:
93+
text = re.sub(r"<@[A-Z0-9]+>", "", m.text or "").strip()
94+
if text:
95+
lines.append(f"{m.user}: {text}")
96+
97+
# 과다 방지: 글자수가 아니라 메시지 단위로 자른다 (메시지 중간이 끊기지 않도록).
98+
# 최근 메시지부터 채우고, budget 을 넘기는 오래된 메시지는 통째로 제외.
99+
kept, total = [], 0
100+
for line in reversed(lines):
101+
if kept and total + len(line) + 1 > self.THREAD_CONTEXT_MAX_CHARS:
102+
break
103+
kept.append(line)
104+
total += len(line) + 1
105+
return "\n".join(reversed(kept))
106+
45107
def can_handle(self):
46108
text_lower = self.text.lower()
47109
return "q)" in text_lower

0 commit comments

Comments
 (0)