import hmac import hashlib import logging import threading from flask import request, abort, Blueprint from community_qa_bot.settings import ( FORUM_WEBHOOK_SECRET, FORUM_API_USERNAME, TIDB_AI_API_KEY, TIDB_AI_FORUM_CHAT_ENGINE, ) from community_qa_bot.pingcap_qa_service import CommunityQAService from .discourse_api import reply_to_topic, get_posts_of_topic logger = logging.getLogger(__name__) qa_service = CommunityQAService( api_key=TIDB_AI_API_KEY, chat_engine=TIDB_AI_FORUM_CHAT_ENGINE, ) forum_bp = Blueprint('forum', __name__) def is_valid_discourse_webhook_request(payload, signature): if not (payload and signature): return False mac = hmac.new(FORUM_WEBHOOK_SECRET.encode('utf-8'), msg=payload, digestmod=hashlib.sha256) return hmac.compare_digest(mac.hexdigest(), signature.split('=')[1]) @forum_bp.route('/discourse-webhook', methods=['POST']) def discourse_webhook(): event_name = request.headers.get('X-Discourse-Event') signature = request.headers.get('X-Discourse-Event-Signature') if not is_valid_discourse_webhook_request(request.data, signature): abort(403) logger.debug(f'event_name: {event_name}, payload: {request.data}') if event_name == 'post_created': threading.Thread(target=handle_post_created, args=(request.json,)).start() return 'ok' def build_qa_context_for_post_created(topic_id, post_id): qa_context = [] skip = True posts = get_posts_of_topic(topic_id, post_id) if not posts: return qa_context, skip # Other users reply to the topic, ignore it if posts['participant_count'] != 2: return qa_context, skip for post in posts['post_stream']['posts']: if post['username'] == FORUM_API_USERNAME: qa_context.append({ 'role': 'assistant', 'content': post['cooked'] }) else: qa_context.append({ 'role': 'user', 'content': post['cooked'] }) qa_context = qa_context[-5:] if not qa_context or qa_context[-1]['role'] == 'assistant': skip = True else: skip = False return qa_context, skip def handle_post_created(payload): logger.debug(f'handle_post_created: {payload}') post = payload['post'] post_id = post['id'] topic_id = post['topic_id'] username = post['username'] if post['post_number'] == 1: return handle_topic_created(payload) # Ignore the posts created by the bot itself if post['username'] == FORUM_API_USERNAME: return qa_context, skip = build_qa_context_for_post_created(topic_id, post_id) if skip: return answer, _, _ = qa_service.ask(origin="forum", qa_context=qa_context) if answer: return reply_to_topic(topic_id, answer) def build_qa_context_for_topic_created(title, body): return [{ 'role': 'user', 'content': f'title: {title}\n {body}' }] def handle_topic_created(payload): logger.debug(f'handle_topic_created: {payload}') post = payload['post'] post_id = post['id'] username = post['username'] topic_id = post['topic_id'] topic_title = post['topic_title'] topic_body = post['raw'] answer, _, _ = qa_service.ask( origin="forum", qa_context=build_qa_context_for_topic_created(topic_title, topic_body), ) if answer: return reply_to_topic(topic_id, answer)