Dooratre commited on
Commit
c116d70
·
verified ·
1 Parent(s): 8620443

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +322 -432
app.py CHANGED
@@ -1,471 +1,361 @@
1
- from flask import Flask, request, jsonify
 
2
  import requests
3
- import random
4
- import string
5
- import time
 
6
  import json
 
 
 
7
 
8
  app = Flask(__name__)
9
 
10
- # Global variables to store workspace and bot IDs
11
- GLOBAL_WORKSPACE_ID = None
12
- GLOBAL_BOT_ID = None
13
-
14
-
15
- # Authorization value used in requests (should be updated with a valid Authorization),
16
- TOKEN = "Bearer bp_pat_vTuxol25N0ymBpYaWqtWpFfGPKt260IfT784"
17
- # -------------------------------------------------------------------
18
- # Helper functions for random bot/workspace names
19
- # -------------------------------------------------------------------
20
- def generate_random_name(length=5):
21
- """Generate a random name for workspace or bot"""
22
- return ''.join(random.choices(string.ascii_letters, k=length))
23
-
24
- # -------------------------------------------------------------------
25
- # Functions to create/delete workspaces and bots
26
- # -------------------------------------------------------------------
27
- def create_workspace():
28
- """Create a new workspace and return its ID"""
29
- ws_url = "https://api.botpress.cloud/v1/admin/workspaces"
30
- headers = {
31
- "User-Agent": "Mozilla/5.0",
32
- "Authorization": TOKEN
33
- }
34
- payload = {"name": generate_random_name()}
35
 
36
- try:
37
- response = requests.post(ws_url, headers=headers, json=payload)
38
- if response.status_code == 200:
39
- response_json = response.json()
40
- workspace_id = response_json.get('id')
41
- print(f"Successfully created workspace: {workspace_id}")
42
- return workspace_id
43
- else:
44
- print(f"Workspace creation failed with: {response.status_code}, {response.text}")
45
- return None
46
- except Exception as e:
47
- print(f"Error creating workspace: {str(e)}")
48
- return None
49
-
50
-
51
- def create_bot(workspace_id):
52
- """Create a new bot in the specified workspace and return its ID"""
53
- if not workspace_id:
54
- print("Cannot create bot: No workspace ID provided")
55
- return None
56
-
57
- bot_url = "https://api.botpress.cloud/v1/admin/bots"
58
- headers = {
59
- "User-Agent": "Mozilla/5.0",
60
- "x-workspace-id": workspace_id,
61
- "Authorization": TOKEN,
62
- "Content-Type": "application/json"
63
- }
64
- payload = {"name": generate_random_name()}
65
 
66
- try:
67
- response = requests.post(bot_url, headers=headers, json=payload)
68
- if response.status_code == 200:
69
- response_json = response.json()
70
- bot_id = response_json.get("bot", {}).get("id")
71
- if not bot_id:
72
- print("Bot ID not found in the response.")
73
- return None
74
-
75
- print(f"Successfully created bot: {bot_id} in workspace: {workspace_id}")
76
-
77
- # Install integration for the new bot
78
- integration_success = install_bot_integration(bot_id, workspace_id)
79
- if integration_success:
80
- print(f"Successfully installed integration for bot {bot_id}")
81
- return bot_id
82
- else:
83
- print(f"Failed to install integration for bot {bot_id}")
84
- return bot_id # Still return the bot ID even if integration fails
85
- else:
86
- print(f"Bot creation failed with: {response.status_code}, {response.text}")
87
- return None
88
- except Exception as e:
89
- print(f"Error creating bot: {str(e)}")
90
- return None
91
-
92
-
93
- def install_bot_integration(bot_id, workspace_id):
94
- """Install required integration for the bot to function properly"""
95
- if not bot_id or not workspace_id:
96
- print("Cannot install integration: Missing bot ID or workspace ID")
97
- return False
98
-
99
- url = f"https://api.botpress.cloud/v1/admin/bots/{bot_id}"
100
- headers = {
101
- "User-Agent": "Mozilla/5.0",
102
- "Authorization": TOKEN,
103
- "Content-Type": "application/json",
104
- "x-bot-id": bot_id,
105
- "x-workspace-id": workspace_id
106
- }
107
- # Integration payload
108
- payload = {
109
- "integrations": {
110
- "intver_01KCHBZMZEM2017N9C0ND1STA5": {
111
- "enabled": True
112
- }
 
 
 
 
 
 
 
 
 
113
  }
 
 
 
 
 
 
114
  }
115
 
116
- try:
117
- response = requests.put(url, headers=headers, json=payload)
118
- if response.status_code == 200:
119
- print(f"Successfully installed integration for bot {bot_id}")
120
- return True
121
- else:
122
- print(f"Failed to install integration: {response.status_code}, {response.text}")
123
- return False
124
- except Exception as e:
125
- print(f"Error installing integration: {str(e)}")
126
- return False
127
 
 
 
 
 
 
 
 
 
128
 
129
- def try_delete_bot(bot_id, workspace_id):
130
- """Attempt to delete a bot from the specified workspace but continue if it fails"""
131
- if not bot_id or not workspace_id:
132
- print("Cannot delete bot: Missing bot ID or workspace ID")
133
- return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
 
135
- url = f"https://api.botpress.cloud/v1/admin/bots/{bot_id}"
136
- headers = {
137
- "User-Agent": "Mozilla/5.0",
138
- "x-workspace-id": workspace_id,
139
- "Authorization": TOKEN
140
- }
 
 
 
 
 
 
 
141
 
142
- try:
143
- response = requests.delete(url, headers=headers)
144
- if response.status_code in [200, 204]:
145
- print(f"Successfully deleted bot: {bot_id}")
146
- return True
147
- else:
148
- print(f"Failed to delete bot: {response.status_code}, {response.text}")
149
- return False
150
- except Exception as e:
151
- print(f"Error deleting bot: {str(e)}")
152
- return False
153
 
 
 
154
 
155
- def try_delete_workspace(workspace_id):
156
- """Attempt to delete a workspace but continue if it fails"""
157
- if not workspace_id:
158
- print("Cannot delete workspace: No workspace ID provided")
159
- return False
160
 
161
- url = f"https://api.botpress.cloud/v1/admin/workspaces/{workspace_id}"
162
- headers = {
163
- "User-Agent": "Mozilla/5.0",
164
- "Authorization": TOKEN
165
- }
166
 
167
- try:
168
- response = requests.delete(url, headers=headers)
169
- if response.status_code in [200, 204]:
170
- print(f"Successfully deleted workspace: {workspace_id}")
171
- return True
172
- else:
173
- print(f"Failed to delete workspace: {response.status_code}, {response.text}")
174
- return False
175
- except Exception as e:
176
- print(f"Error deleting workspace: {str(e)}")
177
- return False
178
 
 
 
 
179
 
180
- # -------------------------------------------------------------------
181
- # Main function that calls the Botpress API endpoint
182
- # -------------------------------------------------------------------
183
- def chat_with_assistant(user_input, chat_history, bot_id, workspace_id, temperature=0.9, top_p=0.95, max_tokens=None):
184
- """
185
- Sends the user input and chat history to the Botpress API endpoint,
186
- returns the assistant's response and (possibly updated) bot/workspace IDs.
187
- """
188
- # Prepare the headers
189
- headers = {
190
- "User-Agent": "Mozilla/5.0",
191
- "x-bot-id": bot_id,
192
- "Content-Type": "application/json",
193
- "Authorization": TOKEN
194
- }
195
 
196
- # Process chat history into the format expected by the API
197
- messages = []
198
- system_prompt = ""
199
-
200
- for msg in chat_history:
201
- if msg["role"] == "system":
202
- system_prompt = msg["content"]
203
- elif msg["role"] in ["user", "assistant"]:
204
- # Pass multipart messages directly without modifying their structure
205
- if "type" in msg and msg["type"] == "multipart" and "content" in msg:
206
- messages.append(msg) # Keep the original multipart structure
207
- # Handle regular text messages
208
- else:
209
- messages.append({
210
- "role": msg["role"],
211
- "content": msg["content"]
212
- })
213
-
214
- # Add the latest user input if not already in chat history
215
- if user_input and isinstance(user_input, str) and (not messages or messages[-1]["role"] != "user" or messages[-1]["content"] != user_input):
216
- messages.append({
217
- "role": "user",
218
- "content": user_input
219
- })
220
-
221
- # Prepare the payload for the API
222
- payload = {
223
- "type": "openai:generateContent",
224
- "input": {
225
- "model": {
226
- "id": "gpt-5.2-2025-12-11"
227
- },
228
- "systemPrompt": system_prompt,
229
- "messages": messages,
230
- "temperature": temperature,
231
- "debug": False,
232
- }
233
- }
234
 
235
- # Add maxTokens to the payload if provided
236
- if max_tokens is not None:
237
- payload["input"]["maxTokens"] = max_tokens
238
 
239
- botpress_url = "https://api.botpress.cloud/v1/chat/actions"
240
- max_retries = 3
241
- timeout = 120 # Increased timeout for long messages
242
 
243
- # For debugging
244
- print("Payload being sent to Botpress:")
245
- print(json.dumps(payload, indent=2))
246
 
247
- # Attempt to send the request
248
- for attempt in range(max_retries):
249
- try:
250
- print(f"Attempt {attempt+1}: Sending request to Botpress API with bot_id={bot_id}, workspace_id={workspace_id}")
251
- response = requests.post(botpress_url, json=payload, headers=headers, timeout=timeout)
252
-
253
- # If successful (200)
254
- if response.status_code == 200:
255
- data = response.json()
256
- assistant_content = data.get('output', {}).get('choices', [{}])[0].get('content', '')
257
- print(f"Successfully received response from Botpress API")
258
- return assistant_content, bot_id, workspace_id
259
-
260
- # Check for authentication or permission errors (401, 403)
261
- elif response.status_code in [401, 403]:
262
- error_message = "Authentication error"
263
- try:
264
- error_data = response.json()
265
- error_message = error_data.get('message', 'Authentication error')
266
- except:
267
- pass
268
-
269
- print(f"Authentication error detected: {error_message}")
270
-
271
- # We need to create new resources immediately
272
- print("Creating new workspace and bot...")
273
- new_workspace_id = create_workspace()
274
- if not new_workspace_id:
275
- print("Failed to create a new workspace")
276
- if attempt < max_retries - 1:
277
- time.sleep(3)
278
- continue
279
- else:
280
- return "Unable to create new resources. Please try again later.", bot_id, workspace_id
281
-
282
- new_bot_id = create_bot(new_workspace_id)
283
- if not new_bot_id:
284
- print("Failed to create a new bot")
285
- if attempt < max_retries - 1:
286
- time.sleep(3)
287
- continue
288
- else:
289
- return "Unable to create new bot. Please try again later.", new_workspace_id, workspace_id
290
-
291
- print(f"Created new workspace: {new_workspace_id} and bot: {new_bot_id}")
292
-
293
- # Try again with new IDs
294
- headers["x-bot-id"] = new_bot_id
295
- try:
296
- print(f"Retrying with new bot_id={new_bot_id}")
297
- retry_response = requests.post(botpress_url, json=payload, headers=headers, timeout=timeout)
298
-
299
- if retry_response.status_code == 200:
300
- data = retry_response.json()
301
- assistant_content = data.get('output', {}).get('choices', [{}])[0].get('content', '')
302
- print(f"Successfully received response with new IDs")
303
-
304
- # Try to clean up old resources in the background, but don't wait for result
305
- if bot_id and workspace_id:
306
- print(f"Attempting to clean up old resources in the background")
307
- try_delete_bot(bot_id, workspace_id)
308
- try_delete_workspace(workspace_id)
309
-
310
- return assistant_content, new_bot_id, new_workspace_id
311
- else:
312
- print(f"Failed with new IDs: {retry_response.status_code}")
313
- if attempt < max_retries - 1:
314
- time.sleep(2)
315
- continue
316
- else:
317
- return f"Unable to get a response even with new credentials.", new_bot_id, new_workspace_id
318
-
319
- except Exception as e:
320
- print(f"Error with new IDs: {str(e)}")
321
- if attempt < max_retries - 1:
322
- time.sleep(2)
323
- continue
324
- else:
325
- return f"Error with new credentials: {str(e)}", new_bot_id, new_workspace_id
326
-
327
- # Handle network errors or timeouts (just retry)
328
- elif response.status_code in [404, 408, 502, 503, 504]:
329
- print(f"Received error {response.status_code}. Retrying...")
330
- time.sleep(3) # Wait before retrying
331
- continue
332
-
333
- # Any other error status code
334
- else:
335
- print(f"Received unexpected error: {response.status_code}, {response.text}")
336
- if attempt < max_retries - 1:
337
- time.sleep(2)
338
- continue
339
- else:
340
- return f"Unable to get a response from the assistant (Error {response.status_code}).", bot_id, workspace_id
341
-
342
- except requests.exceptions.Timeout:
343
- print(f"Request timed out. Retrying...")
344
- if attempt < max_retries - 1:
345
- time.sleep(2)
346
- continue
347
- else:
348
- return "The assistant is taking too long to respond. Please try again with a shorter message.", bot_id, workspace_id
349
 
350
- except Exception as e:
351
- print(f"Error during request: {str(e)}")
352
- if attempt < max_retries - 1:
353
- time.sleep(2)
354
- continue
355
- else:
356
- return f"Unable to get a response from the assistant: {str(e)}", bot_id, workspace_id
357
-
358
- # Should not reach here due to the handling in the loop
359
- return "Unable to get a response from the assistant.", bot_id, workspace_id
360
-
361
-
362
- # -------------------------------------------------------------------
363
- # Flask Endpoint
364
- # -------------------------------------------------------------------
365
- @app.route("/chat", methods=["POST"])
366
- def chat_endpoint():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  """
368
- Expects JSON with:
369
- {
370
- "user_input": "string", // Can be null if multipart message is in chat_history
371
- "chat_history": [
372
- {"role": "system", "content": "..."},
373
- {"role": "user", "content": "..."},
374
- // Or for images:
375
- {"role": "user", "type": "multipart", "content": [
376
- {"type": "image", "url": "https://example.com/image.jpg"},
377
- {"type": "text", "text": "What's in this image?"}
378
- ]},
379
- ...
380
- ],
381
- "temperature": 0.9, // Optional, defaults to 0.9
382
- "top_p": 0.95, // Optional, defaults to 0.95
383
- "max_tokens": 1000 // Optional, defaults to null (no limit)
384
- }
385
- Returns JSON with:
386
- {
387
- "assistant_response": "string"
388
- }
389
  """
390
- global GLOBAL_WORKSPACE_ID, GLOBAL_BOT_ID
391
-
392
- # Parse JSON from request
393
- data = request.get_json(force=True)
394
- user_input = data.get("user_input", "")
395
- chat_history = data.get("chat_history", [])
396
 
397
- # Get temperature, top_p, and max_tokens from request, or use defaults
398
- temperature = data.get("temperature", 0.9)
399
- top_p = data.get("top_p", 0.95)
400
- max_tokens = data.get("max_tokens", None)
401
 
402
- # Validate temperature and top_p values
 
403
  try:
404
- temperature = float(temperature)
405
- if not 0 <= temperature <= 2:
406
- temperature = 0.9
407
- print(f"Invalid temperature value. Using default: {temperature}")
408
- except (ValueError, TypeError):
409
- temperature = 0.9
410
- print(f"Invalid temperature format. Using default: {temperature}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
 
412
- try:
413
- top_p = float(top_p)
414
- if not 0 <= top_p <= 1:
415
- top_p = 0.95
416
- print(f"Invalid top_p value. Using default: {top_p}")
417
- except (ValueError, TypeError):
418
- top_p = 0.95
419
- print(f"Invalid top_p format. Using default: {top_p}")
420
-
421
- # Validate max_tokens if provided
422
- if max_tokens is not None:
423
- try:
424
- max_tokens = int(max_tokens)
425
- if max_tokens <= 0:
426
- print("Invalid max_tokens value (must be positive). Not using max_tokens.")
427
- max_tokens = None
428
- except (ValueError, TypeError):
429
- print("Invalid max_tokens format. Not using max_tokens.")
430
- max_tokens = None
431
-
432
- # If we don't yet have a workspace or bot, create them
433
- if not GLOBAL_WORKSPACE_ID or not GLOBAL_BOT_ID:
434
- print("No existing IDs found. Creating new workspace and bot...")
435
- GLOBAL_WORKSPACE_ID = create_workspace()
436
- if GLOBAL_WORKSPACE_ID:
437
- GLOBAL_BOT_ID = create_bot(GLOBAL_WORKSPACE_ID)
438
-
439
- # If creation failed
440
- if not GLOBAL_WORKSPACE_ID or not GLOBAL_BOT_ID:
441
- return jsonify({"assistant_response": "I'm currently unavailable. Please try again later."}), 500
442
-
443
- # Call our function that interacts with Botpress API
444
- print(f"Sending chat request with existing bot_id={GLOBAL_BOT_ID}, workspace_id={GLOBAL_WORKSPACE_ID}")
445
- print(f"Using temperature={temperature}, top_p={top_p}, max_tokens={max_tokens}")
446
-
447
- assistant_response, updated_bot_id, updated_workspace_id = chat_with_assistant(
448
- user_input,
449
- chat_history,
450
- GLOBAL_BOT_ID,
451
- GLOBAL_WORKSPACE_ID,
452
- temperature,
453
- top_p,
454
- max_tokens
455
  )
456
 
457
- # Update global IDs if they changed
458
- if updated_bot_id != GLOBAL_BOT_ID or updated_workspace_id != GLOBAL_WORKSPACE_ID:
459
- print(f"Updating global IDs: bot_id={updated_bot_id}, workspace_id={updated_workspace_id}")
460
- GLOBAL_BOT_ID = updated_bot_id
461
- GLOBAL_WORKSPACE_ID = updated_workspace_id
462
 
463
- return jsonify({"assistant_response": assistant_response})
 
 
 
 
 
 
464
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
465
 
466
- # -------------------------------------------------------------------
467
- # Run the Flask app
468
- # -------------------------------------------------------------------
469
 
470
  if __name__ == "__main__":
471
- app.run(host="0.0.0.0", port=7860, debug=True)
 
1
+ from flask import Flask, render_template, request, jsonify, session, Response
2
+ import os
3
  import requests
4
+ from datetime import datetime
5
+ import secrets
6
+ import re
7
+ import html
8
  import json
9
+ import unicodedata
10
+
11
+ from ai_forward import AIForwarder
12
 
13
  app = Flask(__name__)
14
 
15
+ app.secret_key = os.environ.get("SECRET_KEY", secrets.token_hex(32))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
+ app.config.update(
18
+ SESSION_COOKIE_HTTPONLY=True,
19
+ SESSION_COOKIE_SAMESITE="Lax",
20
+ SESSION_COOKIE_SECURE=False, # set True behind HTTPS
21
+ PERMANENT_SESSION_LIFETIME=60 * 60 * 6,
22
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
+ # -------------------------
25
+ # In-memory chat store
26
+ # -------------------------
27
+ # Key: session["sid"] Value: list of messages
28
+ # Each message: {"role": "...", "content": "...", "ts": "...", "sheet": "..."}
29
+ CHAT_STORE: dict[str, list[dict]] = {}
30
+
31
+
32
+ # -------------------------
33
+ # Helpers: name + bissan detection
34
+ # -------------------------
35
+ def normalize_ar_name(s: str) -> str:
36
+ """
37
+ Normalize Arabic/Latin name for matching:
38
+ - strip spaces
39
+ - lowercase
40
+ - remove Arabic diacritics (tashkeel)
41
+ - normalize unicode
42
+ """
43
+ if not s:
44
+ return ""
45
+ s = s.strip()
46
+ s = unicodedata.normalize("NFKC", s)
47
+ s = s.lower()
48
+
49
+ # remove Arabic diacritics
50
+ # (harakat range + some marks)
51
+ s = re.sub(r"[\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06ED]", "", s)
52
+
53
+ # collapse spaces
54
+ s = re.sub(r"\s+", " ", s).strip()
55
+ return s
56
+
57
+
58
+ def is_bissan_name(name: str) -> bool:
59
+ n = normalize_ar_name(name)
60
+ # Accept: "بيسان" or "بِيسان" (diacritics removed) or "bissan"
61
+ return n in {"بيسان", "bissan"}
62
+
63
+
64
+ def get_user_profile():
65
+ """
66
+ Returns a dict describing how frontend should display the user:
67
+ - display_name: what to show in UI
68
+ - avatar_type: "default" or "bissan"
69
+ - avatar_bg: hint for UI
70
+ - emoji: default emoji for non-bissan
71
+ """
72
+ name = session.get("user_name") or "أنت"
73
+ if is_bissan_name(name):
74
+ return {
75
+ "display_name": name,
76
+ "is_bissan": True,
77
+ "avatar_type": "flower",
78
+ "avatar_bg": "pink",
79
+ "emoji": None,
80
  }
81
+ return {
82
+ "display_name": name,
83
+ "is_bissan": False,
84
+ "avatar_type": "emoji",
85
+ "avatar_bg": "blue",
86
+ "emoji": "USER", # you said "USER EMOJEI" (implement in UI)
87
  }
88
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
+ # -------------------------
91
+ # AI App
92
+ # -------------------------
93
+ class AIStudyApp:
94
+ def __init__(self, api_url="https://dooratre-xx-gpt-52.hf.space/chat", data_folder="data"):
95
+ self.api_url = api_url
96
+ self.data_folder = data_folder
97
+ self.forwarder = AIForwarder(api_url)
98
 
99
+ def load_txt_file(self, filename):
100
+ filepath = os.path.join(self.data_folder, filename)
101
+ try:
102
+ with open(filepath, "r", encoding="utf-8") as f:
103
+ return f.read()
104
+ except FileNotFoundError:
105
+ print(f"Warning: {filename} not found")
106
+ return ""
107
+
108
+ def build_system_prompt(self, sheet_number: str, is_bissan: bool):
109
+ """
110
+ Normal users: system.txt + {sheet}.txt
111
+ Bissan user: bissan.txt + {sheet}.txt (NO system.txt)
112
+ """
113
+ base_file = "bissan.txt" if is_bissan else "system.txt"
114
+ system_content = self.load_txt_file(base_file)
115
+ sheet_content = self.load_txt_file(f"{sheet_number}.txt")
116
+ return system_content + "\n\n" + sheet_content
117
+
118
+ def send_to_main_ai(self, user_message, system_prompt, chat_history):
119
+ full_chat_history = [{"role": "system", "content": system_prompt}]
120
+ full_chat_history.extend(chat_history)
121
+ full_chat_history.append({"role": "user", "content": user_message})
122
+
123
+ payload = {
124
+ "user_input": user_message,
125
+ "chat_history": full_chat_history,
126
+ "temperature": 0.9,
127
+ "top_p": 0.95,
128
+ }
129
 
130
+ try:
131
+ response = requests.post(
132
+ self.api_url,
133
+ json=payload,
134
+ headers={"Content-Type": "application/json"},
135
+ timeout=45,
136
+ )
137
+ response.raise_for_status()
138
+ result = response.json()
139
+ return result.get("assistant_response", "")
140
+ except Exception as e:
141
+ print(f"Error calling main AI: {e}")
142
+ return None
143
 
144
+ def normalize_text(self, text: str) -> str:
145
+ if not text:
146
+ return ""
147
+ text = re.sub(r"[\u200B-\u200F\u202A-\u202E\u2066-\u2069]", "", text)
148
+ text = text.replace("\r\n", "\n").replace("\r", "\n")
149
+ text = re.sub(r"\n{3,}", "\n\n", text)
150
+ text = "\n".join([ln.rstrip() for ln in text.split("\n")]).strip()
151
+ return text
 
 
 
152
 
153
+ def format_code_blocks(self, text: str) -> str:
154
+ pattern = r"```(\w+)?\n(.*?)```"
155
 
156
+ def replace_code_block(match):
157
+ language = match.group(1) if match.group(1) else "code"
158
+ code_content = match.group(2)
159
+ return f"<code_{language}>\n{code_content}\n</code_{language}>"
 
160
 
161
+ return re.sub(pattern, replace_code_block, text, flags=re.DOTALL)
 
 
 
 
162
 
163
+ def unescape_html_inside_mcq(self, text: str) -> str:
164
+ if not text:
165
+ return ""
166
+ mcq_pattern = r"(<mcq>.*?</mcq>)"
 
 
 
 
 
 
 
167
 
168
+ def _fix_block(match):
169
+ block = match.group(1)
170
+ return html.unescape(block)
171
 
172
+ return re.sub(mcq_pattern, _fix_block, text, flags=re.DOTALL | re.IGNORECASE)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
+ def process_message(self, user_message, chat_history, is_bissan: bool):
175
+ temp_history = chat_history + [{"role": "user", "content": user_message}]
176
+ forward_result = self.forwarder.process_chat_history(temp_history)
177
+ sheet_number = forward_result.get("sheet_number", "1")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
 
179
+ system_prompt = self.build_system_prompt(sheet_number, is_bissan=is_bissan)
180
+ ai_response = self.send_to_main_ai(user_message, system_prompt, chat_history)
 
181
 
182
+ if ai_response is None:
183
+ return {"response": None, "sheet_number": sheet_number}
 
184
 
185
+ ai_response = self.normalize_text(ai_response)
186
+ ai_response = self.unescape_html_inside_mcq(ai_response)
 
187
 
188
+ formatted_response = self.format_code_blocks(ai_response)
189
+ return {"response": formatted_response, "sheet_number": sheet_number}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
+
192
+ ai_app = AIStudyApp()
193
+
194
+
195
+ # -------------------------
196
+ # Session init + name from URL
197
+ # -------------------------
198
+ @app.before_request
199
+ def ensure_session():
200
+ if "sid" not in session:
201
+ session["sid"] = secrets.token_hex(12)
202
+
203
+ sid = session["sid"]
204
+ if sid not in CHAT_STORE:
205
+ CHAT_STORE[sid] = []
206
+
207
+ # Capture name from URL like: /?name=Ahmed
208
+ # You said "manual gonna put the name ... edit the url"
209
+ qname = request.args.get("name", type=str)
210
+ if qname is not None:
211
+ qname = qname.strip()
212
+ if qname:
213
+ session["user_name"] = qname
214
+
215
+
216
+ # -------------------------
217
+ # Routes
218
+ # -------------------------
219
+ @app.route("/")
220
+ def index():
221
+ # Keep your existing template if you want.
222
+ # Name is now set via URL query (?name=...)
223
+ return render_template("index.html")
224
+
225
+
226
+ @app.route("/session_info", methods=["GET"])
227
+ def session_info():
228
  """
229
+ Frontend calls this to know:
230
+ - display name to show instead of "أنت"
231
+ - avatar style (default blue USER emoji vs bissan flower pink)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  """
233
+ return jsonify(
234
+ {
235
+ "sid": session.get("sid"),
236
+ "user": get_user_profile(),
237
+ }
238
+ )
239
 
 
 
 
 
240
 
241
+ @app.route("/send_message", methods=["POST"])
242
+ def send_message():
243
  try:
244
+ data = request.get_json(force=True)
245
+ user_message = (data.get("message") or "").strip()
246
+ if not user_message:
247
+ return jsonify({"success": False, "error": "الرسالة فارغة"}), 400
248
+
249
+ sid = session["sid"]
250
+ profile = get_user_profile()
251
+ is_bissan = profile["is_bissan"]
252
+
253
+ # Build chat_history for AI from stored messages (role/content only)
254
+ stored = CHAT_STORE.get(sid, [])
255
+ chat_history = [{"role": m["role"], "content": m["content"]} for m in stored]
256
+
257
+ result = ai_app.process_message(user_message, chat_history, is_bissan=is_bissan)
258
+
259
+ if result["response"]:
260
+ ts = datetime.now().strftime("%H:%M")
261
+
262
+ # Save user message
263
+ CHAT_STORE[sid].append(
264
+ {
265
+ "role": "user",
266
+ "content": user_message,
267
+ "ts": ts,
268
+ "sheet": result["sheet_number"],
269
+ "user_name": profile["display_name"],
270
+ }
271
+ )
272
+
273
+ # Save assistant message
274
+ CHAT_STORE[sid].append(
275
+ {
276
+ "role": "assistant",
277
+ "content": result["response"],
278
+ "ts": ts,
279
+ "sheet": result["sheet_number"],
280
+ }
281
+ )
282
+
283
+ return jsonify(
284
+ {
285
+ "success": True,
286
+ "response": result["response"],
287
+ "sheet_number": result["sheet_number"],
288
+ "timestamp": ts,
289
+ "user": profile, # so frontend can update UI if needed
290
+ }
291
+ )
292
+
293
+ return jsonify({"success": False, "error": "فشل الحصول على رد من الذكاء الاصطناعي"}), 500
294
 
295
+ except Exception as e:
296
+ print(f"Error in send_message: {e}")
297
+ return jsonify({"success": False, "error": "حدث خطأ غير متوقع"}), 500
298
+
299
+
300
+ @app.route("/clear_history", methods=["POST"])
301
+ def clear_history():
302
+ sid = session["sid"]
303
+ CHAT_STORE[sid] = []
304
+ return jsonify({"success": True, "message": "تم مسح المحادثة"})
305
+
306
+
307
+ @app.route("/get_history", methods=["GET"])
308
+ def get_history():
309
+ sid = session["sid"]
310
+ return jsonify({"history": CHAT_STORE.get(sid, [])})
311
+
312
+
313
+ # -------------------------
314
+ # Bissan routes (view + download)
315
+ # -------------------------
316
+ @app.route("/bissan", methods=["GET"])
317
+ def bissan_history():
318
+ """
319
+ Returns the current session chat as JSON.
320
+ You said: "/bissan to open the array with html code"
321
+ لكن الآن: no HTML, so JSON only.
322
+ Your frontend can render it.
323
+ """
324
+ sid = session["sid"]
325
+ profile = get_user_profile()
326
+ return jsonify(
327
+ {
328
+ "sid": sid,
329
+ "user": profile,
330
+ "history": CHAT_STORE.get(sid, []),
331
+ }
 
 
 
 
 
 
332
  )
333
 
 
 
 
 
 
334
 
335
+ @app.route("/bissan/download", methods=["GET"])
336
+ def bissan_download():
337
+ """
338
+ Download chat as JSON file (UTF-8 Arabic safe).
339
+ """
340
+ sid = session["sid"]
341
+ profile = get_user_profile()
342
 
343
+ payload = {
344
+ "sid": sid,
345
+ "exported_at": datetime.utcnow().isoformat() + "Z",
346
+ "user": profile,
347
+ "history": CHAT_STORE.get(sid, []),
348
+ }
349
+
350
+ data = json.dumps(payload, ensure_ascii=False, indent=2)
351
+ filename = f"chat_{sid}.json"
352
+
353
+ return Response(
354
+ data,
355
+ mimetype="application/json; charset=utf-8",
356
+ headers={"Content-Disposition": f'attachment; filename="{filename}"'},
357
+ )
358
 
 
 
 
359
 
360
  if __name__ == "__main__":
361
+ app.run(debug=True, host="0.0.0.0", port=7860)