Skip to content

Commit 237ed96

Browse files
authored
Merge pull request #62 from vvhuynh/vincent/llm-chatbot
Vincent/llm chatbot Issue #29
2 parents dbfdeec + 8dc1166 commit 237ed96

File tree

16 files changed

+950
-8
lines changed

16 files changed

+950
-8
lines changed

backend/app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def filter(self, record):
5656
from routes.frontend import frontend_bp
5757
from routes.analytics import analytics_bp
5858
from routes.export import export_bp
59+
from routes.chatbot import chatbot_bp
5960
from services.db import redis_client
6061
from services.canvas_counter import get_canvas_draw_count
6162
from services.graphql_service import commit_transaction_via_graphql
@@ -215,6 +216,7 @@ def handle_all_exceptions(e):
215216
app.register_blueprint(submit_room_line_bp)
216217
app.register_blueprint(admin_bp)
217218
app.register_blueprint(export_bp)
219+
app.register_blueprint(chatbot_bp)
218220

219221
# Register versioned API v1 blueprints for external applications
220222
from api_v1.auth import auth_v1_bp

backend/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ redis==6.2.0
5353
requests==2.32.4
5454
resilient-python-cache==0.1.1
5555
rich==13.9.4
56+
openai>=1.0.0
5657
simple-websocket==1.1.0
5758
simplejson==3.19.3
5859
six==1.17.0

backend/routes/chatbot.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from flask import Blueprint, request, jsonify, g
2+
from middleware.auth import require_auth
3+
from middleware.rate_limit import limiter
4+
from services.chatbot_service import get_bot_reply, get_chat_history
5+
import logging
6+
7+
chatbot_bp = Blueprint('chatbot_bp', __name__)
8+
logger = logging.getLogger(__name__)
9+
10+
11+
@chatbot_bp.route('/rooms/<roomId>/chatbot/message', methods=['POST'])
12+
@limiter.limit("10/minute")
13+
@require_auth
14+
def post_chat_message(roomId):
15+
"""
16+
Handle chatbot messages in a room.
17+
18+
Args:
19+
roomId: The room ID where the message is sent
20+
21+
Request body:
22+
message (str): The user's message to the bot
23+
24+
Returns:
25+
JSON response with the bot's reply
26+
"""
27+
try:
28+
# Get the authenticated user ID
29+
user = g.current_user
30+
claims = g.token_claims
31+
user_id = claims["sub"]
32+
33+
# Get the message from request body
34+
data = request.get_json()
35+
if not data or 'message' not in data:
36+
return jsonify({"error": "Missing 'message' field in request body"}), 400
37+
38+
message = data.get('message')
39+
canvas_context = data.get('canvas_context', {})
40+
41+
# Call the chatbot service
42+
reply_text = get_bot_reply(message, roomId, user_id, canvas_context)
43+
44+
# Return the bot's reply
45+
return jsonify({"reply": reply_text})
46+
47+
except Exception as e:
48+
logger.exception("Error processing chatbot message")
49+
return jsonify({"error": "Internal server error"}), 500
50+
51+
52+
@chatbot_bp.route('/rooms/<roomId>/chatbot/history', methods=['GET'])
53+
@require_auth
54+
def get_room_chat_history(roomId):
55+
"""
56+
Retrieve chat history for a room.
57+
58+
Args:
59+
roomId: The room ID to get history for
60+
61+
Query params:
62+
limit (int): Maximum number of messages to retrieve (default 50, max 100)
63+
64+
Returns:
65+
JSON response with chat history
66+
"""
67+
try:
68+
# Get limit from query params, default to 50, max 100
69+
limit = request.args.get('limit', 50, type=int)
70+
limit = min(limit, 100) # Cap at 100 messages
71+
72+
# Get chat history from service
73+
history = get_chat_history(roomId, limit)
74+
75+
return jsonify({"history": history})
76+
77+
except Exception as e:
78+
logger.exception("Error retrieving chat history")
79+
return jsonify({"error": "Internal server error"}), 500

backend/services/canvas_service.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
"""
2+
Canvas service functions for ResCanvas.
3+
"""
4+
5+
import logging
6+
from .db import strokes_coll, redis_client
7+
from .canvas_counter import get_canvas_draw_count
8+
from .graphql_service import commit_transaction_via_graphql
9+
from config import SIGNER_PUBLIC_KEY, SIGNER_PRIVATE_KEY, RECIPIENT_PUBLIC_KEY
10+
import time
11+
import json
12+
13+
logger = logging.getLogger(__name__)
14+
15+
16+
def _now_ms():
17+
"""Get current timestamp in milliseconds."""
18+
return int(time.time() * 1000)
19+
20+
21+
def _persist_marker(id_value: str, value_field: str, value):
22+
"""Persist a small marker object (id + value) into ResDB so it survives Redis flush."""
23+
payload = {
24+
"operation": "CREATE",
25+
"amount": 1,
26+
"signerPublicKey": SIGNER_PUBLIC_KEY,
27+
"signerPrivateKey": SIGNER_PRIVATE_KEY,
28+
"recipientPublicKey": RECIPIENT_PUBLIC_KEY,
29+
"asset": {
30+
"data": {
31+
"id": id_value,
32+
value_field: value
33+
}
34+
}
35+
}
36+
try:
37+
commit_transaction_via_graphql(payload)
38+
except Exception:
39+
logger.exception("Failed to persist marker %s", id_value)
40+
41+
42+
def clear_canvas(room_id: str) -> dict:
43+
"""
44+
Clear all strokes from a canvas/room.
45+
46+
This function:
47+
1. Deletes all strokes from MongoDB for the room
48+
2. Sets a clear timestamp marker in Redis and ResDB
49+
3. Clears undo/redo stacks for the room
50+
51+
Args:
52+
room_id: The room ID to clear
53+
54+
Returns:
55+
dict: Status dictionary with success flag and metadata
56+
"""
57+
try:
58+
# Get current timestamp and draw count
59+
ts = _now_ms()
60+
try:
61+
res_draw_count = int(get_canvas_draw_count())
62+
except Exception:
63+
logger.exception("Failed reading canvas draw count; defaulting to 0")
64+
res_draw_count = 0
65+
66+
# Delete all strokes from MongoDB for this room
67+
delete_result = strokes_coll.delete_many({"roomId": room_id})
68+
deleted_count = delete_result.deleted_count
69+
logger.info(f"Deleted {deleted_count} strokes from room {room_id}")
70+
71+
# Set clear timestamp markers in Redis
72+
redis_ts_cache_key = f"last-clear-ts:{room_id}"
73+
redis_ts_legacy = f"clear-canvas-timestamp:{room_id}"
74+
resdb_ts_id = f"clear-canvas-timestamp:{room_id}"
75+
redis_count_key = f"res-canvas-draw-count:{room_id}"
76+
redis_count_legacy = f"draw_count_clear_canvas:{room_id}"
77+
resdb_count_id = f"res-canvas-draw-count:{room_id}"
78+
79+
try:
80+
redis_client.set(redis_ts_cache_key, ts)
81+
redis_client.set(redis_ts_legacy, ts)
82+
redis_client.set(redis_count_key, res_draw_count)
83+
redis_client.set(redis_count_legacy, res_draw_count)
84+
except Exception:
85+
logger.exception("Failed setting Redis keys for clear markers")
86+
87+
# Persist markers to ResDB
88+
try:
89+
_persist_marker(resdb_count_id, "value", res_draw_count)
90+
_persist_marker(resdb_ts_id, "ts", ts)
91+
_persist_marker(redis_count_legacy, "value", res_draw_count)
92+
except Exception:
93+
logger.exception("Failed persisting clear markers to ResDB")
94+
95+
# Clear undo/redo stacks for this room
96+
try:
97+
# Clear room-specific undo/redo keys
98+
for pattern in (f"{room_id}:*:undo", f"{room_id}:*:redo"):
99+
for key in redis_client.scan_iter(pattern):
100+
try:
101+
redis_client.delete(key)
102+
except Exception:
103+
pass
104+
105+
# Clear generic undo/redo keys for this room
106+
for key in redis_client.scan_iter("undo-*"):
107+
try:
108+
data = redis_client.get(key)
109+
if not data:
110+
continue
111+
rec = json.loads(data)
112+
if rec.get("roomId") == room_id:
113+
redis_client.delete(key)
114+
except Exception:
115+
pass
116+
117+
for key in redis_client.scan_iter("redo-*"):
118+
try:
119+
data = redis_client.get(key)
120+
if not data:
121+
continue
122+
rec = json.loads(data)
123+
if rec.get("roomId") == room_id:
124+
redis_client.delete(key)
125+
except Exception:
126+
pass
127+
except Exception:
128+
logger.exception("Failed clearing undo/redo stacks for room %s", room_id)
129+
130+
return {
131+
"success": True,
132+
"room_id": room_id,
133+
"deleted_count": deleted_count,
134+
"timestamp": ts,
135+
"draw_count": res_draw_count
136+
}
137+
138+
except Exception as e:
139+
logger.exception("Failed to clear canvas for room %s", room_id)
140+
return {
141+
"success": False,
142+
"error": str(e),
143+
"room_id": room_id
144+
}

0 commit comments

Comments
 (0)