1+ # 다국어(한국어) 시스템 프롬프트 문자열이 길어, 이 파일은 줄길이(E501) 검사를 예외 처리한다.
2+ # ruff: noqa: E501
13import logging
24import re
35
911# q) 이후의 질문을 추출하는 정규식
1012QUESTION_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
1728class 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