data-str / board /engine.py
CORVO-AI's picture
Upload 135 files
b83571a verified
"""
Board AI Engine.
Handles the interactive whiteboard: XML generation, parsing,
icon/page resolution, TTS, and board state management.
Now supports MULTIPLE SUBJECTS dynamically.
Each subject loads its own folder, pages_base_url, etc.
"""
import re
import json
import requests
from config import GPT_URL, MAX_CHAT_HISTORY
from board.tts import TTSEngine
from board.icons import IconResolver
from board.pages import resolve_page_tags
from subjects.loader import subject_loader
from json_processor import BoardProcessor
class BoardEngine:
"""
Board AI Engine for a specific user session.
Supports any subject dynamically.
"""
def __init__(self):
self.gpt_url = GPT_URL
self.board_processor = BoardProcessor()
self.tts_engine = TTSEngine()
self.icon_resolver = IconResolver()
# Per-user state: { username: { subject_id, conversation_history, last_sequence } }
self._user_sessions = {}
print("โœ… BoardEngine initialized (multi-subject)")
# โ”€โ”€โ”€ User session management โ”€โ”€โ”€
def _get_user_session(self, username):
"""Get or create a user's board session."""
if username not in self._user_sessions:
self._user_sessions[username] = {
"subject_id": None,
"conversation_history": [],
"last_sequence": [],
}
return self._user_sessions[username]
def set_subject(self, username, subject_id):
"""Set the active subject for a user's board session."""
us = self._get_user_session(username)
# If switching subjects, clear history
if us["subject_id"] and us["subject_id"] != subject_id:
us["conversation_history"] = []
us["last_sequence"] = []
print(f" ๐Ÿ”„ Board subject switched: {us['subject_id']} โ†’ {subject_id} for {username}")
us["subject_id"] = subject_id
print(f" ๐Ÿ“‹ Board subject set: {subject_id} for {username}")
def get_subject(self, username):
"""Get the active subject for a user."""
us = self._get_user_session(username)
return us.get("subject_id")
# โ”€โ”€โ”€ GPT call โ”€โ”€โ”€
def _call_gpt5(self, user_message, system_prompt, temperature=0.7, max_tokens=4000):
payload = {
"user_input": user_message,
"chat_history": [
{"role": "system", "content": system_prompt}
],
"temperature": temperature,
"top_p": 0.95,
"max_completion_tokens": max_tokens
}
try:
response = requests.post(self.gpt_url, json=payload, timeout=120)
response.raise_for_status()
return response.json().get("assistant_response", "")
except requests.exceptions.Timeout:
print(" โŒ GPT timeout")
return None
except requests.exceptions.ConnectionError:
print(" โŒ GPT connection error")
return None
except Exception as e:
print(f" โŒ GPT error: {e}")
return None
# โ”€โ”€โ”€ Chat history formatting โ”€โ”€โ”€
def _format_chat_history(self, username):
us = self._get_user_session(username)
history = us.get("conversation_history", [])
if not history:
return ""
recent = history[-10:]
parts = []
for msg in recent:
if msg["role"] == "user":
parts.append(f"ุงู„ุทุงู„ุจ: {msg['content']}")
elif msg["role"] == "assistant":
parts.append(f"ุงู„ู…ุฏุฑุณ: {msg['content']}")
return (
"\n\nโ•โ• ุณุฌู„ ุงู„ู…ุญุงุฏุซุฉ ุงู„ุณุงุจู‚ุฉ โ•โ•\n"
+ "\n".join(parts)
+ "\nโ•โ• ู†ู‡ุงูŠุฉ ุงู„ุณุฌู„ โ•โ•"
)
# โ”€โ”€โ”€ Step 1: Route message โ”€โ”€โ”€
def _route_message(self, user_message, username):
us = self._get_user_session(username)
subject_id = us["subject_id"]
if not subject_id:
return "main.txt"
subject_data = subject_loader.load(subject_id)
if not subject_data:
return "main.txt"
structure = subject_data.get("structure.txt", "")
p_files = subject_data.get("_p_files", [])
chat_history_text = self._format_chat_history(username)
p_files_desc = "\n".join([f"- {f}: ุงู„ูุตู„ {i+1}" for i, f in enumerate(p_files)])
routing_prompt = f"""ุฃู†ุช ู†ุธุงู… ุชูˆุฌูŠู‡ ุฐูƒูŠ ู„ู…ุณุงุนุฏ ุชุนู„ูŠู…ูŠ.
ู…ู‡ู…ุชูƒ: ุชุญู„ูŠู„ ุฑุณุงู„ุฉ ุงู„ุทุงู„ุจ ูˆุงุฎุชูŠุงุฑ ุงู„ู…ู„ู ุงู„ู…ู†ุงุณุจ ู„ู„ุฑุฏ.
ุงู„ู…ู„ูุงุช ุงู„ู…ุชุงุญุฉ:
- main.txt: ู„ู„ุชุญูŠุงุชุŒ ุงู„ุฃุณุฆู„ุฉ ุงู„ุนุงู…ุฉุŒ ุฃูŠ ุดูŠุก ู„ุง ูŠุชุนู„ู‚ ุจูุตู„ ู…ุญุฏุฏ
{p_files_desc}
ูู‡ุฑุณ ุงู„ูƒุชุงุจ (ู„ู„ู…ุณุงุนุฏุฉ ููŠ ุงู„ุชูˆุฌูŠู‡):
{structure}
{chat_history_text}
ุชุนู„ูŠู…ุงุช:
1. ุฅุฐุง ูƒุงู†ุช ุงู„ุฑุณุงู„ุฉ ุชุญูŠุฉ ุฃูˆ ุณุคุงู„ ุนุงู… โ†’ main.txt
2. ุฅุฐุง ูƒุงู†ุช ุชุณุฃู„ ุนู† ู…ูˆุถูˆุน ููŠ ูุตู„ ู…ุญุฏุฏ โ†’ ุงู„ู…ู„ู ุงู„ู…ู†ุงุณุจ
3. ุงุณุชุฎุฏู… ุงู„ูู‡ุฑุณ ู„ุชุญุฏูŠุฏ ุงู„ูุตู„ ุงู„ุตุญูŠุญ
4. ู…ู‡ู… ุฌุฏุงู‹: ุฅุฐุง ู‚ุงู„ ุงู„ุทุงู„ุจ "ุงุดุฑุญ ุฃูƒุซุฑ" ุฃูˆ "ูˆุถุญ" ุฃูˆ ุฃูŠ ุทู„ุจ ู…ุชุงุจุนุฉุŒ ุงุฑุฌุน ู„ุณุฌู„ ุงู„ู…ุญุงุฏุซุฉ ู„ุชุนุฑู ุงู„ู…ูˆุถูˆุน ุงู„ุญุงู„ูŠ ูˆุงุฎุชุฑ ู†ูุณ ุงู„ู…ู„ู
5. ุฃุฌุจ ูู‚ุท ุจุงุณู… ุงู„ู…ู„ู (ู…ุซุงู„: p1.txt ุฃูˆ main.txt) ุจุฏูˆู† ุฃูŠ ูƒู„ุงู… ุฅุถุงููŠ
ุฑุณุงู„ุฉ ุงู„ุทุงู„ุจ: {user_message}
ุงู„ู…ู„ู ุงู„ู…ู†ุงุณุจ:"""
chosen = self._call_gpt5(
user_message, routing_prompt,
temperature=0.2, max_tokens=50
)
if not chosen:
return "main.txt"
chosen = chosen.strip().lower()
valid_files = ["main.txt"] + p_files
for v in valid_files:
if v in chosen:
return v
return "main.txt"
# โ”€โ”€โ”€ Step 2: Generate XML response โ”€โ”€โ”€
def _generate_xml_response(self, user_message, chosen_file, username):
us = self._get_user_session(username)
subject_id = us["subject_id"]
if not subject_id:
return None
subject_data = subject_loader.load(subject_id)
if not subject_data:
return None
file_content = subject_data.get(chosen_file, "")
chat_history_text = self._format_chat_history(username)
system_prompt = f"""ุงู†ุชูŠ ู…ุฏุฑุณุฉ ุฎุจูŠุฑุฉ ูˆู…ุญุชุฑูุฉ ููŠ ู‡ุฐู‡ ุงู„ู…ุงุฏุฉ. ุชุดุฑุญ ุนู„ู‰ ุณุจูˆุฑุฉ ุชูุงุนู„ูŠุฉ ุฑู‚ู…ูŠุฉ.
โ•โ•โ• ุตูŠุบุฉ ุงู„ุฑุฏ โ•โ•โ•
ูŠุฌุจ ุฃู† ุชุฑุฏูŠ ุจุตูŠุบุฉ XML ุฎุงุตุฉ ุชุญุชูˆูŠ ุนู„ู‰:
1. <board>ุนู†ุงุตุฑ ุงู„ุณุจูˆุฑุฉ</board> - ู…ุง ุณูŠูุฑุณู…/ูŠูุถุงู ุนู„ู‰ ุงู„ุณุจูˆุฑุฉ ุฃูˆู„ุงู‹
2. <voice>ู†ุต ุงู„ูƒู„ุงู…</voice> - ุงู„ู†ุต ุงู„ุฐูŠ ุณูŠูู‚ุฑุฃ ุจุตูˆุช ุนุงู„ู ู„ู„ุทุงู„ุจ ุจุนุฏ ุฑุณู… ุงู„ุนู†ุงุตุฑ (ุนุฑุจูŠ ุทุจูŠุนูŠ)
โ•โ•โ• ุนู†ุงุตุฑ ุงู„ุณุจูˆุฑุฉ ุงู„ู…ุชุงุญุฉ (ุฏุงุฎู„ <board>) โ•โ•โ•
โ€ข <note>ู…ุญุชูˆู‰ ุงู„ู…ู„ุงุญุธุฉ</note>
โ€ข <text>ู†ุต ู…ุจุงุดุฑ ุนู„ู‰ ุงู„ุณุจูˆุฑุฉ ุจุฏูˆู† ุฎู„ููŠุฉ</text>
โ€ข <shape type="ู†ูˆุน_ุงู„ุดูƒู„"/>
ุงู„ุฃู†ูˆุงุน: circle, triangle, star, arrow-right, arrow-left, arrow-up, arrow-down,
rectangle, diamond, hexagon, square, oval, arrow-double-h, checkmark, cross,
heart, cloud, lightning, speech, process, decision
โ€ข <svg>ูƒู„ู…ุฉ_ุจุญุซ_ุจุงู„ุฅู†ุฌู„ูŠุฒูŠุฉ</svg>
ุณูŠุชู… ุงู„ุจุญุซ ุนู† ุฃูŠู‚ูˆู†ุฉ ู…ุฑุณูˆู…ุฉ ูŠุฏูˆูŠุงู‹ (ู…ุซู„: ball, car, force, spring, weight, rope, pulley)
โ€ข <page>ุฑู‚ู…_ุงู„ุตูุญุฉ</page>
ู„ุนุฑุถ ุตูุญุฉ ู…ุญุฏุฏุฉ ู…ู† ุงู„ูƒุชุงุจ ูƒุตูˆุฑุฉ ุนู„ู‰ ุงู„ุณุจูˆุฑุฉ
ู…ุซุงู„: <page>12</page> ู„ุนุฑุถ ุงู„ุตูุญุฉ 12 ู…ู† ุงู„ูƒุชุงุจ
ุงุณุชุฎุฏู…ู‡ุง ุนู†ุฏู…ุง ุชุญุชุงุฌ ุชุนุฑุถ ู„ู„ุทุงู„ุจ ุตูุญุฉ ู…ุนูŠู†ุฉ ู…ู† ุงู„ูƒุชุงุจ
โ•โ•โ• ู‚ูˆุงุนุฏ ู…ู‡ู…ุฉ ุฌุฏุงู‹ โ•โ•โ•
1. ุงุดุฑุญ ุฎุทูˆุฉ ุจุฎุทูˆุฉ: ุงุจุฏุฃ ุจู€ <board> ุซู… <voice> ุซู… <board> ุซู… <voice> ูˆู‡ูƒุฐุง
2. ุงุฌุนู„ ุงู„ุดุฑุญ ู…ุชุฏุฑุฌุงู‹ ูƒุฃู†ูƒ ุชุดุฑุญ ุนู„ู‰ ุณุจูˆุฑุฉ ุญู‚ูŠู‚ูŠุฉ ุฃู…ุงู… ุงู„ุทู„ุงุจ
3. <board> = ู…ุง ูŠุธู‡ุฑ ุนู„ู‰ ุงู„ุณุจูˆุฑุฉ ุฃูˆู„ุงู‹ (ู…ู„ุงุญุธุงุชุŒ ู†ุตูˆุตุŒ ุฃุดูƒุงู„ุŒ ุตูˆุฑุŒ ุตูุญุงุช ุงู„ูƒุชุงุจ)
4. <voice> = ุงู„ูƒู„ุงู… ุงู„ู…ุณู…ูˆุน ุจุนุฏ ุฑุณู… ุงู„ุนู†ุงุตุฑ (ุทุจูŠุนูŠุŒ ูˆุฏูˆุฏุŒ ูˆุงุถุญุŒ ูŠุดุฑุญ ู…ุง ุชู… ุฑุณู…ู‡)
5. ู„ุง ุชุถุน ูƒู„ ุดูŠุก ุฏูุนุฉ ูˆุงุญุฏุฉ - ุงุฌุนู„ู‡ ุชุณู„ุณู„ูŠุงู‹
7. <svg> ูู‚ุท ุจูƒู„ู…ุงุช ุฅู†ุฌู„ูŠุฒูŠุฉ ุจุณูŠุทุฉ ูˆู…ุนุจุฑุฉ
8. ุงู„ุณุจูˆุฑุฉ ุชุนู…ู„ ุจู†ุธุงู… ุงู„ุฅุถุงูุฉ - ุงู„ุนู†ุงุตุฑ ุงู„ุณุงุจู‚ุฉ ุชุจู‚ู‰
9. ุงุณุชุฎุฏู… <text> ู„ู„ุนู†ุงูˆูŠู† ูˆุงู„ู…ุนุงุฏู„ุงุช ุงู„ู…ู‡ู…ุฉ (ุจุฏูˆู† ุฎู„ููŠุฉ)
10. ุงุณุชุฎุฏู… <note> ู„ู„ุชูˆุถูŠุญุงุช ูˆุงู„ู…ู„ุงุญุธุงุช (ู…ุน ุฎู„ููŠุฉ ู…ู„ูˆู†ุฉ)
11. ู„ุง ุชุณุชุฎุฏู… ุฃูƒุซุฑ ู…ู† 3-4 ุนู†ุงุตุฑ ููŠ ูƒู„ <board>
12. ุงุฌุนู„ ุงู„ู†ุต ููŠ <voice> ุทุจูŠุนูŠุงู‹ ูƒุฃู†ูƒ ุชุชุญุฏุซ ู…ุน ุทุงู„ุจ ูˆูŠุดุฑุญ ู…ุง ุชู… ุฑุณู…ู‡ ุนู„ู‰ ุงู„ุณุจูˆุฑุฉ
13. ุงุฑุณู… ุฃูˆู„ุงู‹ ุซู… ุชูƒู„ู… - ู‡ุฐุง ู…ู‡ู… ุฌุฏุงู‹!
14. ุฑุงุฌุน ุณุฌู„ ุงู„ู…ุญุงุฏุซุฉ ุงู„ุณุงุจู‚ุฉ ู„ุชุนุฑู ู…ุง ุชู… ุดุฑุญู‡ ูˆุชูƒู…ู„ ู…ู† ุญูŠุซ ุชูˆู‚ูุช - ู„ุง ุชูƒุฑุฑ ู…ุง ู‚ู„ุชู‡ ุณุงุจู‚ุงู‹
15. ุงุณุชุฎุฏู… <page> ุนู†ุฏู…ุง ุชุฑูŠุฏ ุนุฑุถ ุตูุญุฉ ู…ู† ุงู„ูƒุชุงุจ - ู…ุซู„ุงู‹ ุฅุฐุง ุงู„ุทุงู„ุจ ุณุฃู„ ุนู† ุชู…ุฑูŠู† ุฃูˆ ุดูƒู„ ููŠ ุตูุญุฉ ู…ุนูŠู†ุฉ
when user talk about something not about the subject or something funny etc... you can actually answer without the board just VOICE and be funny smart perfect girl also:
when you explain something dont make all your explain on the NOTE make the note for important point use the TEXT direct on the board and the ICONS/SHAPES
โ•โ•โ• ู…ุญุชูˆู‰ ุงู„ู…ุงุฏุฉ โ•โ•โ•
{file_content}
{chat_history_text}
โ•โ•โ• ุงู„ุขู† ุฃุฌุจ ุนู„ู‰ ุณุคุงู„ ุงู„ุทุงู„ุจ โ•โ•โ•
ุฑุณุงู„ุฉ ุงู„ุทุงู„ุจ: {user_message}"""
response = self._call_gpt5(
user_message, system_prompt,
temperature=0.8, max_tokens=4000
)
return response
# โ”€โ”€โ”€ Resolve tags โ”€โ”€โ”€
def _resolve_svg_tags(self, xml_response):
if not xml_response:
return xml_response
return self.icon_resolver.resolve_all_in_xml(xml_response)
def _resolve_page_tags(self, xml_response, username):
if not xml_response:
return xml_response
us = self._get_user_session(username)
subject_id = us.get("subject_id")
if not subject_id:
return xml_response
base_url = subject_loader.get_pages_base_url(subject_id)
if not base_url:
print(f" โš ๏ธ No pages_base_url for subject {subject_id}")
return xml_response
return resolve_page_tags(xml_response, base_url)
# โ”€โ”€โ”€ Build sequence from XML โ”€โ”€โ”€
def _build_sequence_from_xml(self, xml_response, frontend_board_state):
sequence = []
if not xml_response:
return sequence, frontend_board_state
pattern = r'<(voice|board)>(.*?)</\1>'
matches = list(re.finditer(pattern, xml_response, re.DOTALL))
if not matches:
cleaned = re.sub(r'<[^>]+>', '', xml_response).strip()
if cleaned:
sequence.append({
"type": "voice",
"text": cleaned,
"audio_url": None
})
return sequence, frontend_board_state
current_board_state = list(frontend_board_state)
for match in matches:
tag_type = match.group(1)
content = match.group(2).strip()
if tag_type == "voice":
cleaned = re.sub(r'<[^>]+>', '', content).strip()
if cleaned:
sequence.append({
"type": "voice",
"text": cleaned,
"audio_url": None
})
elif tag_type == "board":
existing_json_str = json.dumps(
current_board_state, ensure_ascii=False, indent=2
)
processor_input = (
f"BOARD NOW (make sure no X Y error):\n"
f"{existing_json_str}\n\n"
f"new board need to add :\n"
f"<board>{content}</board>"
)
print(f" ๐Ÿ”ง Sending to json_processor...")
print(f" Current board items: {len(current_board_state)}")
try:
json_text = self.board_processor.convert_xml_to_json(
processor_input
)
if json_text:
new_items = json.loads(json_text)
if isinstance(new_items, list) and new_items:
added_items = []
existing_ids = set()
for item in current_board_state:
item_key = json.dumps(
item, sort_keys=True, ensure_ascii=False
)
existing_ids.add(item_key)
for item in new_items:
item_key = json.dumps(
item, sort_keys=True, ensure_ascii=False
)
if item_key not in existing_ids:
added_items.append(item)
if added_items:
sequence.append({
"type": "board_update",
"action": "add",
"items": added_items
})
current_board_state.extend(added_items)
print(
f" โœ… json_processor: "
f"{len(new_items)} total, "
f"{len(added_items)} new"
)
elif isinstance(new_items, dict):
added_items = [new_items]
current_board_state.append(new_items)
sequence.append({
"type": "board_update",
"action": "add",
"items": added_items
})
print(f" โœ… json_processor: 1 item")
else:
print(f" โš ๏ธ json_processor: unexpected format")
except json.JSONDecodeError as e:
print(f" โŒ json_processor invalid JSON: {e}")
raw_preview = json_text[:200] if json_text else 'None'
print(f" Raw: {raw_preview}")
except Exception as e:
print(f" โŒ json_processor error: {e}")
return sequence, current_board_state
# โ”€โ”€โ”€ MAIN PIPELINE โ”€โ”€โ”€
def process_message(self, user_message, username, frontend_board_state=None):
"""
Main board pipeline for any subject.
Args:
user_message: Student's text
username: User identifier
frontend_board_state: Current board items from frontend (source of truth)
Returns:
dict with success, sequence, board_state, chosen_file
"""
us = self._get_user_session(username)
subject_id = us.get("subject_id")
print(f"\n{'โ•' * 60}")
print(f" ๐Ÿ‘ค Student ({username}): {user_message}")
print(f" ๐Ÿ“š Subject: {subject_id}")
print(f"{'โ•' * 60}")
if not subject_id:
return {
"success": False,
"error": "ู„ู… ูŠุชู… ุชุญุฏูŠุฏ ุงู„ู…ุงุฏุฉ ู„ู„ุณุจูˆุฑุฉ",
"sequence": [{
"type": "voice",
"text": "ูŠุฑุฌู‰ ุงุฎุชูŠุงุฑ ุงู„ู…ุงุฏุฉ ุฃูˆู„ุงู‹.",
"audio_url": None
}],
"board_state": frontend_board_state or []
}
if frontend_board_state is None:
frontend_board_state = []
print(f" ๐Ÿ“‹ Board state from frontend: {len(frontend_board_state)} items")
print(f" ๐Ÿ’ฌ Chat history: {len(us.get('conversation_history', []))} messages")
# Step 1: Route
print("\n ๐Ÿ“ Step 1: Routing message...")
chosen_file = self._route_message(user_message, username)
print(f" ๐Ÿ“‚ Chosen file: {chosen_file}")
# Step 2: Generate XML
print(f" ๐Ÿค– Step 2: Generating XML response...")
xml_response = self._generate_xml_response(user_message, chosen_file, username)
if not xml_response:
print(" โŒ Failed to generate response")
error_seq = [{
"type": "voice",
"text": "ุนุฐุฑุงู‹ุŒ ุญุฏุซ ุฎุทุฃ ููŠ ุงู„ู†ุธุงู…. ุญุงูˆู„ ู…ุฑุฉ ุฃุฎุฑู‰.",
"audio_url": None
}]
return {
"success": False,
"error": "Failed to generate response",
"sequence": error_seq,
"board_state": frontend_board_state
}
print(f" ๐Ÿ“ XML response: {len(xml_response)} chars")
# Step 3: Resolve <page> tags
print(" ๐Ÿ“„ Step 3: Resolving <page> tags...")
xml_response = self._resolve_page_tags(xml_response, username)
# Step 4: Resolve <svg> tags
print(" ๐ŸŽจ Step 4: Resolving <svg> tags...")
xml_response = self._resolve_svg_tags(xml_response)
# Step 5: Parse XML + convert boards
print(" ๐Ÿ”ง Step 5: Parsing XML & converting boards...")
sequence, updated_board_state = self._build_sequence_from_xml(
xml_response, frontend_board_state
)
voice_count = sum(1 for s in sequence if s['type'] == 'voice')
board_count = sum(1 for s in sequence if s['type'] == 'board_update')
print(f" ๐Ÿ“Š {len(sequence)} segments ({voice_count} voice, {board_count} board)")
# Step 6: Convert voice โ†’ audio
print(" ๐Ÿ”Š Step 6: Converting voice to audio...")
for item in sequence:
if item["type"] == "voice" and item.get("text"):
text_preview = item['text'][:50]
print(f" ๐ŸŽ™๏ธ Converting: '{text_preview}...'")
audio_file = self.tts_engine.convert(item["text"])
if audio_file:
item["audio_url"] = f"/static/{audio_file}"
else:
item["audio_url"] = None
print(f" โš ๏ธ TTS failed for this segment")
# Step 7: Save chat history
us["conversation_history"].append({
"role": "user",
"content": user_message
})
voice_texts = [
s["text"] for s in sequence
if s["type"] == "voice" and s.get("text")
]
assistant_text = " ".join(voice_texts)
if assistant_text:
us["conversation_history"].append({
"role": "assistant",
"content": assistant_text
})
# Keep history manageable
if len(us["conversation_history"]) > MAX_CHAT_HISTORY:
us["conversation_history"] = us["conversation_history"][-MAX_CHAT_HISTORY:]
# Step 8: Store sequence for replay
us["last_sequence"] = sequence
# Cleanup old audio
self.tts_engine.cleanup_old_files()
print(f"\n โœ… Response ready!")
print(f" Sequence: {voice_count} voice + {board_count} board")
print(f" Board items: {len(updated_board_state)}")
print(f"{'โ•' * 60}\n")
return {
"success": True,
"chosen_file": chosen_file,
"sequence": sequence,
"board_state": updated_board_state
}
# โ”€โ”€โ”€ Replay โ”€โ”€โ”€
def get_replay_sequence(self, username):
us = self._get_user_session(username)
last_seq = us.get("last_sequence", [])
if not last_seq:
return {
"success": False,
"error": "No previous response to replay",
"sequence": []
}
voice_only = []
for item in last_seq:
if item["type"] == "voice":
voice_only.append({
"type": "voice",
"text": item.get("text", ""),
"audio_url": item.get("audio_url")
})
print(f" ๐Ÿ”„ Replay: {len(voice_only)} voice segments (no board items)")
return {
"success": True,
"sequence": voice_only
}
# โ”€โ”€โ”€ Clear โ”€โ”€โ”€
def clear_board(self, username):
us = self._get_user_session(username)
us["last_sequence"] = []
print(f" ๐Ÿ—‘๏ธ Board cleared for {username}")
return {"success": True, "board_state": []}
def clear_chat_history(self, username):
us = self._get_user_session(username)
us["conversation_history"] = []
us["last_sequence"] = []
print(f" ๐Ÿ—‘๏ธ Board chat history cleared for {username}")
return {"success": True}
def clear_user_session(self, username):
"""Fully clear a user's board session."""
if username in self._user_sessions:
del self._user_sessions[username]
print(f" ๐Ÿ—‘๏ธ Full board session cleared for {username}")
return {"success": True}