vedaco commited on
Commit
e926670
·
verified ·
1 Parent(s): c657783

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +405 -347
app.py CHANGED
@@ -1,14 +1,21 @@
1
- """Veda Programming Assistant - Auto Learning (Hidden Teacher)"""
 
 
 
 
 
 
2
 
3
- import gradio as gr
4
- import tensorflow as tf
5
  import os
6
  import json
 
 
7
  import re
8
  import ast
9
  import operator as op
10
- import threading
11
- import time
 
12
 
13
  from model import VedaProgrammingLLM
14
  from tokenizer import VedaTokenizer
@@ -18,52 +25,85 @@ from teacher import teacher
18
  from config import MODEL_DIR
19
 
20
 
21
- # --------- Globals ----------
 
 
22
  model = None
23
  tokenizer = None
24
- conversation_history = []
 
 
 
25
  current_conv_id = -1
26
 
27
- # Auto-training settings
 
 
 
 
28
  AUTO_TRAIN_ENABLED = True
29
- AUTO_TRAIN_MIN_SAMPLES = 10 # Train after this many teacher responses
30
- AUTO_TRAIN_INTERVAL = 1800 # Check every 30 minutes (in seconds)
31
- AUTO_TRAIN_EPOCHS = 10
32
- is_training = False
33
- last_train_time = 0
34
 
 
 
 
35
 
36
- # --------- Helpers ----------
 
 
 
37
  def extract_text(message):
 
 
 
 
 
 
 
38
  if message is None:
39
  return ""
40
  if isinstance(message, str):
41
  return message
 
42
  if isinstance(message, dict):
43
  if "text" in message:
44
  return str(message.get("text", ""))
45
  if "content" in message:
46
  return extract_text(message["content"])
47
  return ""
 
48
  if isinstance(message, list):
49
- parts = []
50
  for part in message:
51
  if isinstance(part, dict) and part.get("type") == "text":
52
- parts.append(str(part.get("text", "")))
53
  elif isinstance(part, str):
54
- parts.append(part)
55
- return "".join(parts).strip()
 
56
  return str(message)
57
 
58
 
59
  def ensure_messages_history(history):
 
 
 
 
 
60
  if history is None:
61
  return []
62
- if len(history) > 0 and isinstance(history[0], dict) and "role" in history[0]:
 
 
63
  fixed = []
64
  for m in history:
65
  fixed.append({"role": m["role"], "content": extract_text(m["content"])})
66
  return fixed
 
 
67
  fixed = []
68
  for pair in history:
69
  if isinstance(pair, (list, tuple)) and len(pair) == 2:
@@ -72,7 +112,9 @@ def ensure_messages_history(history):
72
  return fixed
73
 
74
 
75
- # --------- Math Solver ----------
 
 
76
  _ALLOWED_OPS = {
77
  ast.Add: op.add,
78
  ast.Sub: op.sub,
@@ -84,7 +126,6 @@ _ALLOWED_OPS = {
84
  ast.UAdd: op.pos,
85
  }
86
 
87
-
88
  def safe_eval_math(expr: str):
89
  node = ast.parse(expr, mode="eval").body
90
 
@@ -95,176 +136,131 @@ def safe_eval_math(expr: str):
95
  return _ALLOWED_OPS[type(n.op)](_eval(n.left), _eval(n.right))
96
  if isinstance(n, ast.UnaryOp) and type(n.op) in _ALLOWED_OPS:
97
  return _ALLOWED_OPS[type(n.op)](_eval(n.operand))
98
- raise ValueError("Unsupported")
99
 
100
  return _eval(node)
101
 
102
-
103
  def try_math_answer(user_text: str):
104
  if not user_text:
105
  return None
106
- s = user_text.strip().replace("=", "").replace("?", "").strip().replace("^", "**")
 
 
 
 
107
  if not re.fullmatch(r"[0-9\.\s\+\-\*\/\(\)%]+", s):
108
  return None
 
109
  try:
110
  val = safe_eval_math(s)
111
  if isinstance(val, float) and val.is_integer():
112
  val = int(val)
113
  return str(val)
114
- except:
115
  return None
116
 
117
 
118
- # --------- Response Quality Check ----------
119
- def is_good_response(response: str) -> bool:
120
- """Check if student response is good quality"""
121
- if not response:
122
- return False
123
-
124
- response = response.strip()
125
-
126
- # Too short
127
- if len(response) < 30:
128
- return False
129
-
130
- # Contains gibberish patterns
131
- gibberish_patterns = [
132
- r'\["\]',
133
- r'arr\[\s*a',
134
- r'print\s*\(\s*"\s*,',
135
- r'=\s+=\s+=',
136
- r'\[\.\]',
137
- r'return\s+if\s+is',
138
- r'\s{10,}',
139
- r'(\w)\1{5,}',
140
- r'\[\s*\]',
141
- r'def\s+def',
142
- r'class\s+class',
143
- r'return\s+return',
144
- r'if\s+if',
145
- r'\(\s*\)',
146
- r'=\s*=\s*=',
147
  ]
148
-
149
- for pattern in gibberish_patterns:
150
- if re.search(pattern, response):
151
- return False
152
-
153
- # Too many special characters
154
- letters = sum(1 for c in response if c.isalpha())
155
- special = sum(1 for c in response if c in '[]{}()=<>|\\')
156
- if letters > 0 and special / letters > 0.5:
157
- return False
158
-
159
- # Too many brackets without proper code
160
- brackets = response.count('[') + response.count(']') + response.count('{') + response.count('}')
161
- if brackets > 20 and 'def ' not in response and 'class ' not in response:
162
  return False
163
-
164
- # Check for error phrases
165
- error_phrases = [
166
- "i'm not sure",
167
- "i don't know",
168
- "could you try rephrasing",
169
- "error:",
170
- "cannot understand",
171
- "not sure how to respond",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  ]
173
-
174
- response_lower = response.lower()
175
- for phrase in error_phrases:
176
- if phrase in response_lower:
177
- return False
178
-
179
- return True
180
-
181
-
182
- # --------- Auto Training ----------
183
- def auto_train_background():
184
- """Background thread that automatically trains when enough data collected"""
185
- global model, tokenizer, is_training, last_train_time
186
-
187
- while True:
188
- time.sleep(60) # Check every minute
189
-
190
- if not AUTO_TRAIN_ENABLED:
191
- continue
192
-
193
- if is_training:
194
- continue
195
-
196
- # Check if enough time passed since last training
197
- if time.time() - last_train_time < AUTO_TRAIN_INTERVAL:
198
- continue
199
-
200
- # Check if we have enough samples
201
- try:
202
- unused = db.get_unused_distillation_data()
203
- if len(unused) >= AUTO_TRAIN_MIN_SAMPLES:
204
- print(f"\n[Auto-Train] Starting training with {len(unused)} samples...")
205
- is_training = True
206
-
207
- # Prepare training data
208
- good_convs = db.get_good_conversations()
209
- extra_data = ""
210
- for conv in good_convs:
211
- extra_data += f"<USER> {conv['user_input']}\n"
212
- extra_data += f"<ASSISTANT> {conv['assistant_response']}\n\n"
213
-
214
- distillation_data = ""
215
- for item in unused:
216
- distillation_data += f"<USER> {item['user_input']}\n"
217
- distillation_data += f"<ASSISTANT> {item['teacher_response']}\n\n"
218
-
219
- # Train
220
- trainer = VedaTrainer()
221
- history = trainer.train(
222
- epochs=AUTO_TRAIN_EPOCHS,
223
- extra_data=extra_data,
224
- distillation_data=distillation_data,
225
- )
226
-
227
- # Update global model
228
- model = trainer.model
229
- tokenizer = trainer.tokenizer
230
-
231
- # Mark as used
232
- ids = [item["id"] for item in unused]
233
- db.mark_distillation_used(ids)
234
-
235
- loss = history.history["loss"][-1]
236
- db.save_training_history(
237
- training_type="auto",
238
- samples_used=len(unused) + len(good_convs),
239
- epochs=AUTO_TRAIN_EPOCHS,
240
- final_loss=loss,
241
- )
242
-
243
- last_train_time = time.time()
244
- is_training = False
245
- print(f"[Auto-Train] Completed! Loss: {loss:.4f}")
246
-
247
- except Exception as e:
248
- print(f"[Auto-Train] Error: {e}")
249
- is_training = False
250
 
 
 
 
251
 
252
- # --------- Model Init ----------
 
 
 
 
 
253
  def initialize():
254
  global model, tokenizer
255
 
256
  print("Initializing Veda Programming Assistant...")
257
 
258
  config_path = os.path.join(MODEL_DIR, "config.json")
 
 
259
 
260
- if os.path.exists(config_path):
261
  print("Loading existing model...")
262
 
263
  with open(config_path, "r") as f:
264
  config = json.load(f)
265
 
266
  tokenizer = VedaTokenizer()
267
- tokenizer.load(os.path.join(MODEL_DIR, "tokenizer.json"))
268
 
269
  model = VedaProgrammingLLM(
270
  vocab_size=config["vocab_size"],
@@ -277,162 +273,241 @@ def initialize():
277
 
278
  dummy = tf.zeros((1, config["max_length"]), dtype=tf.int32)
279
  model(dummy)
280
- model.load_weights(os.path.join(MODEL_DIR, "weights.h5"))
281
 
282
- print("Model loaded!")
283
  else:
284
- print("Training new model...")
285
  trainer = VedaTrainer()
286
- trainer.train(epochs=15)
287
  model = trainer.model
288
  tokenizer = trainer.tokenizer
289
- print("Model trained!")
290
 
291
 
292
  def clean_response(text: str) -> str:
293
  if not text:
294
  return ""
295
-
296
  text = text.replace("<CODE>", "\n```python\n")
297
  text = text.replace("<ENDCODE>", "\n```\n")
298
-
299
  for token in ["<PAD>", "<UNK>", "<START>", "<END>", "<USER>", "<ASSISTANT>"]:
300
  text = text.replace(token, "")
301
-
 
302
  lines = text.split("\n")
303
  cleaned = []
304
- empty_count = 0
305
-
306
  for line in lines:
307
  if line.strip() == "":
308
- empty_count += 1
309
- if empty_count <= 2:
310
  cleaned.append(line)
311
  else:
312
- empty_count = 0
313
  cleaned.append(line)
314
-
315
  return "\n".join(cleaned).strip()
316
 
317
 
318
- def get_student_response(user_input: str, temperature: float = 0.7, max_tokens: int = 200) -> str:
319
- """Get response from student model (Veda)"""
 
 
320
  if model is None or tokenizer is None:
321
  return ""
322
-
323
- try:
324
- context = ""
325
- for msg in conversation_history[-3:]:
326
- context += f"<USER> {msg['user']}\n<ASSISTANT> {msg['assistant']}\n"
327
-
328
- prompt = context + f"<USER> {user_input}\n<ASSISTANT>"
329
- tokens = tokenizer.encode(prompt)
330
-
331
- if len(tokens) > model.max_length - max_tokens:
332
- tokens = tokens[-(model.max_length - max_tokens):]
333
-
334
- generated = model.generate(
335
- tokens,
336
- max_new_tokens=max_tokens,
337
- temperature=temperature,
338
- top_k=50,
339
- top_p=0.9,
340
- repetition_penalty=1.2,
341
- )
342
 
343
- response = tokenizer.decode(generated)
 
 
 
344
 
345
- if "<ASSISTANT>" in response:
346
- response = response.split("<ASSISTANT>")[-1].strip()
347
- if "<USER>" in response:
348
- response = response.split("<USER>")[0].strip()
349
 
350
- return clean_response(response)
351
-
352
- except Exception as e:
353
- print(f"Student error: {e}")
354
- return ""
355
 
 
 
 
 
 
 
 
 
356
 
357
- def get_teacher_response(user_input: str) -> str:
358
- """Get response from teacher model (hidden from user)"""
359
- try:
360
- conv_history = []
361
- for msg in conversation_history[-4:]:
362
- conv_history.append({"role": "user", "content": msg["user"]})
363
- conv_history.append({"role": "assistant", "content": msg["assistant"]})
364
-
365
- response = teacher.ask(
366
- user_message=user_input,
367
- conversation_history=conv_history,
368
- )
369
-
370
- return response if response else ""
371
-
372
- except Exception as e:
373
- print(f"Teacher error: {e}")
374
- return ""
375
 
376
 
377
- def generate_response(user_input: str, temperature: float = 0.7, max_tokens: int = 200) -> str:
378
- """Generate response - seamlessly uses teacher when needed (hidden from user)"""
379
- global current_conv_id, conversation_history
 
 
 
380
 
381
- user_input = extract_text(user_input).strip()
382
- if not user_input:
383
- return "Please type a message!"
384
 
385
- # 1) Math first
386
- math_ans = try_math_answer(user_input)
 
 
 
 
 
 
 
 
 
 
 
387
  if math_ans is not None:
388
- conversation_history.append({"user": user_input, "assistant": math_ans})
389
- current_conv_id = db.save_conversation(user_input, math_ans)
390
  return math_ans
391
 
392
- # 2) Try student
393
- student_response = get_student_response(user_input, temperature, max_tokens)
394
-
395
- # 3) Check quality
396
- if is_good_response(student_response):
397
- final_response = student_response
398
- else:
399
- # 4) Silently ask teacher (user doesn't know!)
400
- teacher_response = get_teacher_response(user_input)
401
-
402
- if teacher_response:
403
- final_response = teacher_response
404
-
405
- # Save for auto-training (silent learning)
406
- db.save_distillation_data(
407
- user_input=user_input,
408
- teacher_response=teacher_response,
409
- student_response=student_response,
410
- quality_score=1.0,
411
- )
 
412
  else:
413
- final_response = student_response if student_response else "I'm having trouble understanding. Could you rephrase that?"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
 
415
- if not final_response:
416
- final_response = "I couldn't generate a response. Please try again."
 
 
 
 
 
 
 
 
 
 
417
 
418
- conversation_history.append({"user": user_input, "assistant": final_response})
419
- current_conv_id = db.save_conversation(user_input, final_response)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
420
 
421
- # NO indicator - user doesn't know if it's teacher or student!
422
- return final_response
 
 
 
423
 
 
 
 
 
 
 
 
 
 
 
424
 
425
- # --------- Gradio Handlers ----------
 
 
 
 
 
 
 
 
 
 
 
426
  def respond(message, history, temperature, max_tokens):
427
  history = ensure_messages_history(history)
428
  user_text = extract_text(message).strip()
429
  if not user_text:
430
  return "", history
431
 
432
- bot_message = generate_response(user_text, temperature, max_tokens)
433
 
434
  history.append({"role": "user", "content": user_text})
435
- history.append({"role": "assistant", "content": bot_message})
436
 
437
  return "", history
438
 
@@ -440,141 +515,124 @@ def respond(message, history, temperature, max_tokens):
440
  def feedback_good():
441
  if current_conv_id > 0:
442
  db.update_feedback(current_conv_id, 1)
443
- return "👍 Thanks!"
444
- return ""
445
 
446
 
447
  def feedback_bad():
448
  if current_conv_id > 0:
449
  db.update_feedback(current_conv_id, -1)
450
- return "👎 Thanks for feedback!"
451
- return ""
452
 
453
 
454
  def clear_chat():
455
  global conversation_history
456
  conversation_history = []
457
- return [], "Chat cleared."
458
 
459
 
460
- def get_stats():
461
  stats = db.get_stats()
462
-
463
- # Calculate learning progress
464
- total_teacher = stats.get('distillation_total', 0)
465
- used_teacher = total_teacher - stats.get('distillation_unused', 0)
466
-
467
- if total_teacher > 0:
468
- learning_progress = (used_teacher / total_teacher) * 100
469
- else:
470
- learning_progress = 0
471
 
472
- return f"""## 📊 Statistics
 
 
 
 
473
 
474
  ### Conversations
475
- | Metric | Count |
476
- |--------|-------|
477
- | 💬 Total Chats | {stats['total']} |
478
- | 👍 Helpful | {stats['positive']} |
479
- | 👎 Needs Work | {stats['negative']} |
480
-
481
- ### 🧠 Learning Progress
482
- | Metric | Value |
483
- |--------|-------|
484
- | Knowledge Gained | {used_teacher} lessons |
485
- | Learning Queue | {stats.get('distillation_unused', 0)} pending |
486
- | Auto-Training | {'✅ Active' if AUTO_TRAIN_ENABLED else '❌ Disabled'} |
487
- | Currently Training | {'🔄 Yes' if is_training else '✅ Ready'} |
488
- """
489
 
 
 
 
 
490
 
491
- # --------- Startup ----------
492
- print("=" * 50)
493
- print("Starting Veda Programming Assistant...")
494
- print("=" * 50)
495
 
 
 
 
 
496
  initialize()
497
 
498
- # Start auto-training background thread
499
  if AUTO_TRAIN_ENABLED:
500
- print("Starting auto-learning background process...")
501
- train_thread = threading.Thread(target=auto_train_background, daemon=True)
502
- train_thread.start()
503
- print("Auto-learning enabled!")
504
-
505
- print("=" * 50)
506
- print("Ready!")
507
- print("=" * 50)
508
 
509
 
510
- # --------- UI (Simple - No Training Tab) ----------
 
 
511
  with gr.Blocks(title="Veda Programming Assistant") as demo:
512
- gr.Markdown("""
513
- # 🕉️ Veda Programming Assistant
 
 
 
514
 
515
- I can help you with **coding**, **programming concepts**, and **math**!
516
- """)
 
517
 
518
  with gr.Tabs():
519
- with gr.TabItem("💬 Chat"):
520
- chatbot = gr.Chatbot(label="Conversation", height=450, value=[])
521
 
522
  with gr.Row():
523
  msg = gr.Textbox(
524
- label="Your message",
525
- placeholder="Ask me anything about programming...",
526
  lines=2,
527
  scale=4,
528
  )
529
- send_btn = gr.Button("Send", variant="primary", scale=1)
530
 
531
  with gr.Row():
532
- temperature = gr.Slider(0.1, 1.5, 0.7, step=0.1, label="Creativity")
533
- max_tokens = gr.Slider(50, 400, 200, step=50, label="Response length")
534
 
535
  with gr.Row():
536
- good_btn = gr.Button("👍 Helpful", variant="secondary")
537
- bad_btn = gr.Button("👎 Not Helpful", variant="secondary")
538
- clear_btn = gr.Button("🗑️ Clear", variant="secondary")
 
 
539
 
540
- feedback_msg = gr.Textbox(label="", lines=1, interactive=False, show_label=False)
 
541
 
542
- send_btn.click(respond, [msg, chatbot, temperature, max_tokens], [msg, chatbot])
543
- msg.submit(respond, [msg, chatbot, temperature, max_tokens], [msg, chatbot])
544
- good_btn.click(feedback_good, outputs=feedback_msg)
545
- bad_btn.click(feedback_bad, outputs=feedback_msg)
546
- clear_btn.click(clear_chat, outputs=[chatbot, feedback_msg])
547
 
548
- gr.Markdown("### 💡 Try asking:")
549
  gr.Examples(
550
  examples=[
551
- ["Hello! What can you do?"],
552
- ["What is Python?"],
553
- ["Write a factorial function"],
554
  ["Explain recursion"],
555
- ["Write bubble sort"],
556
  ["2+2=?"],
557
- ["What is a list in Python?"],
558
- ["How do I read a file?"],
559
  ],
560
  inputs=msg,
561
  )
562
 
563
- with gr.TabItem("📊 Stats"):
564
- gr.Markdown("### How is Veda doing?")
565
- stats_out = gr.Markdown()
566
- refresh_btn = gr.Button("🔄 Refresh")
567
- refresh_btn.click(get_stats, outputs=stats_out)
568
-
569
- gr.Markdown("""
570
- ---
571
- **💡 Tip:** Rate responses to help Veda learn faster!
572
- - 👍 = This was helpful
573
- - 👎 = This needs improvement
574
- """)
575
-
576
- gr.Markdown("---\n**Veda Programming Assistant** | Always learning, always improving!")
577
-
578
 
579
  if __name__ == "__main__":
580
  demo.launch(server_name="0.0.0.0", server_port=7860)
 
1
+ """
2
+ Veda Programming Assistant (Gradio 6.x)
3
+ - Hidden teacher fallback (OpenRouter) when student fails
4
+ - Auto-training in background using teacher responses
5
+ - Math solver for simple arithmetic
6
+ - Compatible with Gradio "messages format" + multimodal inputs
7
+ """
8
 
 
 
9
  import os
10
  import json
11
+ import time
12
+ import threading
13
  import re
14
  import ast
15
  import operator as op
16
+
17
+ import gradio as gr
18
+ import tensorflow as tf
19
 
20
  from model import VedaProgrammingLLM
21
  from tokenizer import VedaTokenizer
 
25
  from config import MODEL_DIR
26
 
27
 
28
+ # -----------------------------
29
+ # GLOBALS
30
+ # -----------------------------
31
  model = None
32
  tokenizer = None
33
+
34
+ # For building student prompt context
35
+ conversation_history = [] # list of dicts: {"user": "...", "assistant": "..."}
36
+
37
  current_conv_id = -1
38
 
39
+ # Teacher usage stats (not shown in chat)
40
+ teacher_used_count = 0
41
+ teacher_failed_count = 0
42
+
43
+ # Auto-training control
44
  AUTO_TRAIN_ENABLED = True
45
+ AUTO_TRAIN_MIN_TEACHER_SAMPLES = 10 # retrain after this many new teacher samples
46
+ AUTO_TRAIN_CHECK_EVERY_SEC = 120 # check every 2 minutes
47
+ AUTO_TRAIN_EPOCHS = 5 # keep small for Spaces CPU
48
+ AUTO_TRAIN_COOLDOWN_SEC = 60 * 20 # at least 20 minutes between trainings
 
49
 
50
+ _is_training = False
51
+ _last_train_time = 0
52
+ _train_lock = threading.Lock()
53
 
54
+
55
+ # -----------------------------
56
+ # GRADIO INPUT HELPERS
57
+ # -----------------------------
58
  def extract_text(message):
59
+ """
60
+ Convert Gradio multimodal/messages -> plain string.
61
+ Handles:
62
+ - str
63
+ - dict {"text": "..."} or {"content": ...}
64
+ - list [{"type":"text","text":"..."}]
65
+ """
66
  if message is None:
67
  return ""
68
  if isinstance(message, str):
69
  return message
70
+
71
  if isinstance(message, dict):
72
  if "text" in message:
73
  return str(message.get("text", ""))
74
  if "content" in message:
75
  return extract_text(message["content"])
76
  return ""
77
+
78
  if isinstance(message, list):
79
+ out = []
80
  for part in message:
81
  if isinstance(part, dict) and part.get("type") == "text":
82
+ out.append(str(part.get("text", "")))
83
  elif isinstance(part, str):
84
+ out.append(part)
85
+ return "".join(out).strip()
86
+
87
  return str(message)
88
 
89
 
90
  def ensure_messages_history(history):
91
+ """
92
+ Ensure history is messages-format list:
93
+ [{"role":"user","content":"..."}, {"role":"assistant","content":"..."}]
94
+ Convert tuple format if needed.
95
+ """
96
  if history is None:
97
  return []
98
+
99
+ # already messages format
100
+ if len(history) > 0 and isinstance(history[0], dict) and "role" in history[0] and "content" in history[0]:
101
  fixed = []
102
  for m in history:
103
  fixed.append({"role": m["role"], "content": extract_text(m["content"])})
104
  return fixed
105
+
106
+ # tuple format -> messages format
107
  fixed = []
108
  for pair in history:
109
  if isinstance(pair, (list, tuple)) and len(pair) == 2:
 
112
  return fixed
113
 
114
 
115
+ # -----------------------------
116
+ # SAFE MATH SOLVER
117
+ # -----------------------------
118
  _ALLOWED_OPS = {
119
  ast.Add: op.add,
120
  ast.Sub: op.sub,
 
126
  ast.UAdd: op.pos,
127
  }
128
 
 
129
  def safe_eval_math(expr: str):
130
  node = ast.parse(expr, mode="eval").body
131
 
 
136
  return _ALLOWED_OPS[type(n.op)](_eval(n.left), _eval(n.right))
137
  if isinstance(n, ast.UnaryOp) and type(n.op) in _ALLOWED_OPS:
138
  return _ALLOWED_OPS[type(n.op)](_eval(n.operand))
139
+ raise ValueError("Unsupported expression")
140
 
141
  return _eval(node)
142
 
 
143
  def try_math_answer(user_text: str):
144
  if not user_text:
145
  return None
146
+ s = user_text.strip()
147
+ s = s.replace("=", "").replace("?", "").strip()
148
+ s = s.replace("^", "**") # allow ^
149
+
150
+ # only allow numeric math chars
151
  if not re.fullmatch(r"[0-9\.\s\+\-\*\/\(\)%]+", s):
152
  return None
153
+
154
  try:
155
  val = safe_eval_math(s)
156
  if isinstance(val, float) and val.is_integer():
157
  val = int(val)
158
  return str(val)
159
+ except Exception:
160
  return None
161
 
162
 
163
+ # -----------------------------
164
+ # QUALITY CHECK + TEACHER TRIGGER
165
+ # -----------------------------
166
+ def is_code_request(user_text: str) -> bool:
167
+ t = user_text.lower()
168
+ triggers = [
169
+ "write", "implement", "code", "function", "algorithm",
170
+ "bubble sort", "binary search", "merge sort", "quick sort", "quicksort",
171
+ "linked list", "stack", "queue", "class ", "def "
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  ]
173
+ return any(k in t for k in triggers)
174
+
175
+ def looks_like_python_code(text: str) -> bool:
176
+ if not text:
 
 
 
 
 
 
 
 
 
 
177
  return False
178
+ t = text.strip()
179
+ if "```" in t:
180
+ return True
181
+ if "def " in t or "class " in t:
182
+ return True
183
+ if "\n " in t:
184
+ return True
185
+ return False
186
+
187
+ def is_gibberish(text: str) -> bool:
188
+ if not text:
189
+ return True
190
+ t = text.strip()
191
+
192
+ # repeated greeting
193
+ if t.lower().count("hello how are you") >= 2:
194
+ return True
195
+
196
+ # too short
197
+ if len(t) < 25:
198
+ return True
199
+
200
+ # lots of symbols vs letters
201
+ letters = sum(c.isalpha() for c in t)
202
+ special = sum(c in "[]{}()=<>|\\" for c in t)
203
+ if letters > 0 and (special / max(letters, 1)) > 0.35:
204
+ return True
205
+
206
+ # low unique word ratio
207
+ words = re.findall(r"[a-zA-Z_]+", t.lower())
208
+ if len(words) >= 20:
209
+ uniq_ratio = len(set(words)) / len(words)
210
+ if uniq_ratio < 0.35:
211
+ return True
212
+
213
+ # known “junk” patterns
214
+ junk_patterns = [
215
+ r"\[\s*\"?\s*\]", # empty brackets patterns
216
+ r"return\s+if\s+is",
217
+ r"=\s*=\s*=",
218
+ r"def\s+def",
219
+ r"class\s+class",
220
+ r"return\s+return",
221
  ]
222
+ for p in junk_patterns:
223
+ if re.search(p, t):
224
+ return True
225
+
226
+ return False
227
+
228
+ def should_use_teacher(user_text: str, student_text: str) -> bool:
229
+ # teacher must be available
230
+ if not teacher.is_available():
231
+ return False
232
+
233
+ # always use teacher for code requests unless student produced real code
234
+ if is_code_request(user_text) and not looks_like_python_code(student_text):
235
+ return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
 
237
+ # use teacher if student output is gibberish
238
+ if is_gibberish(student_text):
239
+ return True
240
 
241
+ return False
242
+
243
+
244
+ # -----------------------------
245
+ # MODEL LOAD
246
+ # -----------------------------
247
  def initialize():
248
  global model, tokenizer
249
 
250
  print("Initializing Veda Programming Assistant...")
251
 
252
  config_path = os.path.join(MODEL_DIR, "config.json")
253
+ weights_path = os.path.join(MODEL_DIR, "weights.h5")
254
+ tok_path = os.path.join(MODEL_DIR, "tokenizer.json")
255
 
256
+ if os.path.exists(config_path) and os.path.exists(weights_path) and os.path.exists(tok_path):
257
  print("Loading existing model...")
258
 
259
  with open(config_path, "r") as f:
260
  config = json.load(f)
261
 
262
  tokenizer = VedaTokenizer()
263
+ tokenizer.load(tok_path)
264
 
265
  model = VedaProgrammingLLM(
266
  vocab_size=config["vocab_size"],
 
273
 
274
  dummy = tf.zeros((1, config["max_length"]), dtype=tf.int32)
275
  model(dummy)
276
+ model.load_weights(weights_path)
277
 
278
+ print("Model loaded.")
279
  else:
280
+ print("No saved model found. Training initial model...")
281
  trainer = VedaTrainer()
282
+ trainer.train(epochs=10)
283
  model = trainer.model
284
  tokenizer = trainer.tokenizer
285
+ print("Initial model trained.")
286
 
287
 
288
  def clean_response(text: str) -> str:
289
  if not text:
290
  return ""
291
+
292
  text = text.replace("<CODE>", "\n```python\n")
293
  text = text.replace("<ENDCODE>", "\n```\n")
294
+
295
  for token in ["<PAD>", "<UNK>", "<START>", "<END>", "<USER>", "<ASSISTANT>"]:
296
  text = text.replace(token, "")
297
+
298
+ # reduce empty lines
299
  lines = text.split("\n")
300
  cleaned = []
301
+ empty = 0
 
302
  for line in lines:
303
  if line.strip() == "":
304
+ empty += 1
305
+ if empty <= 2:
306
  cleaned.append(line)
307
  else:
308
+ empty = 0
309
  cleaned.append(line)
 
310
  return "\n".join(cleaned).strip()
311
 
312
 
313
+ # -----------------------------
314
+ # STUDENT + TEACHER RESPONSE
315
+ # -----------------------------
316
+ def get_student_response(user_text: str, temperature: float, max_tokens: int) -> str:
317
  if model is None or tokenizer is None:
318
  return ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
 
320
+ # build context from internal conversation_history
321
+ context = ""
322
+ for m in conversation_history[-3:]:
323
+ context += f"<USER> {m['user']}\n<ASSISTANT> {m['assistant']}\n"
324
 
325
+ prompt = context + f"<USER> {user_text}\n<ASSISTANT>"
326
+ tokens = tokenizer.encode(prompt)
 
 
327
 
328
+ if len(tokens) > model.max_length - max_tokens:
329
+ tokens = tokens[-(model.max_length - max_tokens):]
 
 
 
330
 
331
+ generated = model.generate(
332
+ tokens,
333
+ max_new_tokens=max_tokens,
334
+ temperature=temperature,
335
+ top_k=50,
336
+ top_p=0.9,
337
+ repetition_penalty=1.2,
338
+ )
339
 
340
+ out = tokenizer.decode(generated)
341
+
342
+ if "<ASSISTANT>" in out:
343
+ out = out.split("<ASSISTANT>")[-1].strip()
344
+ if "<USER>" in out:
345
+ out = out.split("<USER>")[0].strip()
346
+
347
+ return clean_response(out)
 
 
 
 
 
 
 
 
 
 
348
 
349
 
350
+ def get_teacher_response(user_text: str) -> str:
351
+ # Build teacher history from our internal conversation_history
352
+ teacher_hist = []
353
+ for m in conversation_history[-4:]:
354
+ teacher_hist.append({"role": "user", "content": m["user"]})
355
+ teacher_hist.append({"role": "assistant", "content": m["assistant"]})
356
 
357
+ return teacher.ask(user_message=user_text, conversation_history=teacher_hist) or ""
 
 
358
 
359
+
360
+ # -----------------------------
361
+ # MAIN GENERATION (HIDDEN TEACHER)
362
+ # -----------------------------
363
+ def generate_response(user_input, temperature=0.7, max_tokens=200) -> str:
364
+ global current_conv_id, teacher_used_count, teacher_failed_count
365
+
366
+ user_text = extract_text(user_input).strip()
367
+ if not user_text:
368
+ return "Please type a message."
369
+
370
+ # math solver first
371
+ math_ans = try_math_answer(user_text)
372
  if math_ans is not None:
373
+ conversation_history.append({"user": user_text, "assistant": math_ans})
374
+ current_conv_id = db.save_conversation(user_text, math_ans)
375
  return math_ans
376
 
377
+ # student attempt
378
+ student = get_student_response(user_text, temperature, max_tokens)
379
+
380
+ # teacher fallback
381
+ if should_use_teacher(user_text, student):
382
+ teacher_resp = get_teacher_response(user_text)
383
+ if teacher_resp.strip():
384
+ teacher_used_count += 1
385
+
386
+ # save distillation sample
387
+ try:
388
+ db.save_distillation_data(
389
+ user_input=user_text,
390
+ teacher_response=teacher_resp,
391
+ student_response=student,
392
+ quality_score=1.0,
393
+ )
394
+ except Exception as e:
395
+ print("Could not save distillation sample:", e)
396
+
397
+ final = teacher_resp
398
  else:
399
+ teacher_failed_count += 1
400
+ final = student if student else "Please try again."
401
+ else:
402
+ final = student
403
+
404
+ final = clean_response(final)
405
+ if not final:
406
+ final = "Please try asking in a different way."
407
+
408
+ conversation_history.append({"user": user_text, "assistant": final})
409
+ current_conv_id = db.save_conversation(user_text, final)
410
+ return final
411
+
412
+
413
+ # -----------------------------
414
+ # AUTO TRAINING
415
+ # -----------------------------
416
+ def auto_train_loop():
417
+ global _is_training, _last_train_time, model, tokenizer
418
+
419
+ while True:
420
+ time.sleep(AUTO_TRAIN_CHECK_EVERY_SEC)
421
+
422
+ if not AUTO_TRAIN_ENABLED:
423
+ continue
424
+
425
+ if time.time() - _last_train_time < AUTO_TRAIN_COOLDOWN_SEC:
426
+ continue
427
 
428
+ if _train_lock.locked():
429
+ continue
430
+
431
+ # check distillation samples
432
+ try:
433
+ unused = db.get_unused_distillation_data(limit=1000)
434
+ except Exception as e:
435
+ print("[AutoTrain] Could not read distillation data:", e)
436
+ continue
437
+
438
+ if len(unused) < AUTO_TRAIN_MIN_TEACHER_SAMPLES:
439
+ continue
440
 
441
+ # train in this background thread
442
+ with _train_lock:
443
+ _is_training = True
444
+ print(f"[AutoTrain] Starting training on {len(unused)} teacher samples...")
445
+
446
+ try:
447
+ distill_text = ""
448
+ ids = []
449
+ for row in unused:
450
+ ids.append(row["id"])
451
+ distill_text += f"<USER> {row['user_input']}\n<ASSISTANT> {row['teacher_response']}\n\n"
452
+
453
+ # include good user-rated conversations too
454
+ extra = ""
455
+ try:
456
+ good = db.get_good_conversations(limit=200)
457
+ for conv in good:
458
+ extra += f"<USER> {conv['user_input']}\n<ASSISTANT> {conv['assistant_response']}\n\n"
459
+ except Exception:
460
+ pass
461
+
462
+ trainer = VedaTrainer()
463
+ hist = trainer.train(
464
+ epochs=AUTO_TRAIN_EPOCHS,
465
+ extra_data=extra,
466
+ distillation_data=distill_text,
467
+ )
468
+
469
+ model = trainer.model
470
+ tokenizer = trainer.tokenizer
471
 
472
+ # mark distillation used
473
+ try:
474
+ db.mark_distillation_used(ids)
475
+ except Exception as e:
476
+ print("[AutoTrain] Could not mark distillation used:", e)
477
 
478
+ loss = float(hist.history["loss"][-1])
479
+ try:
480
+ db.save_training_history(
481
+ training_type="auto",
482
+ samples_used=len(unused),
483
+ epochs=AUTO_TRAIN_EPOCHS,
484
+ final_loss=loss,
485
+ )
486
+ except Exception:
487
+ pass
488
 
489
+ _last_train_time = time.time()
490
+ print(f"[AutoTrain] Done. loss={loss:.4f}")
491
+
492
+ except Exception as e:
493
+ print("[AutoTrain] Training failed:", e)
494
+
495
+ _is_training = False
496
+
497
+
498
+ # -----------------------------
499
+ # GRADIO HANDLERS
500
+ # -----------------------------
501
  def respond(message, history, temperature, max_tokens):
502
  history = ensure_messages_history(history)
503
  user_text = extract_text(message).strip()
504
  if not user_text:
505
  return "", history
506
 
507
+ bot_text = generate_response(user_text, temperature=float(temperature), max_tokens=int(max_tokens))
508
 
509
  history.append({"role": "user", "content": user_text})
510
+ history.append({"role": "assistant", "content": bot_text})
511
 
512
  return "", history
513
 
 
515
  def feedback_good():
516
  if current_conv_id > 0:
517
  db.update_feedback(current_conv_id, 1)
518
+ return "Thanks!"
519
+ return "No message to rate yet."
520
 
521
 
522
  def feedback_bad():
523
  if current_conv_id > 0:
524
  db.update_feedback(current_conv_id, -1)
525
+ return "Thanks!"
526
+ return "No message to rate yet."
527
 
528
 
529
  def clear_chat():
530
  global conversation_history
531
  conversation_history = []
532
+ return [], ""
533
 
534
 
535
+ def get_stats_md():
536
  stats = db.get_stats()
537
+ teacher_ok = teacher.is_available()
538
+
539
+ return f"""
540
+ ## Statistics
 
 
 
 
 
541
 
542
+ **Teacher available:** `{teacher_ok}`
543
+ **Teacher used (this runtime):** `{teacher_used_count}`
544
+ **Teacher failed (this runtime):** `{teacher_failed_count}`
545
+ **Auto-training enabled:** `{AUTO_TRAIN_ENABLED}`
546
+ **Currently training:** `{_is_training}`
547
 
548
  ### Conversations
549
+ - Total: **{stats.get('total', 0)}**
550
+ - Positive: **{stats.get('positive', 0)}**
551
+ - Negative: **{stats.get('negative', 0)}**
 
 
 
 
 
 
 
 
 
 
 
552
 
553
+ ### Distillation (teacher lessons)
554
+ - Total saved: **{stats.get('distillation_total', 0)}**
555
+ - Pending for training: **{stats.get('distillation_unused', 0)}**
556
+ """
557
 
 
 
 
 
558
 
559
+ # -----------------------------
560
+ # STARTUP
561
+ # -----------------------------
562
+ print("=== Booting Veda Assistant ===")
563
  initialize()
564
 
565
+ print("Teacher available:", teacher.is_available())
566
  if AUTO_TRAIN_ENABLED:
567
+ t = threading.Thread(target=auto_train_loop, daemon=True)
568
+ t.start()
569
+ print("Auto-training thread started.")
570
+ print("=== Ready ===")
 
 
 
 
571
 
572
 
573
+ # -----------------------------
574
+ # UI
575
+ # -----------------------------
576
  with gr.Blocks(title="Veda Programming Assistant") as demo:
577
+ gr.Markdown(
578
+ """
579
+ # Veda Programming Assistant
580
+
581
+ Ask programming questions, request code, or do math like `2+2=?` or `(10+5)/3`.
582
 
583
+ (Teacher is hidden. Auto-learning is automatic.)
584
+ """
585
+ )
586
 
587
  with gr.Tabs():
588
+ with gr.TabItem("Chat"):
589
+ chatbot = gr.Chatbot(label="Conversation", height=420, value=[])
590
 
591
  with gr.Row():
592
  msg = gr.Textbox(
593
+ label="Message",
594
+ placeholder="Example: Write bubble sort",
595
  lines=2,
596
  scale=4,
597
  )
598
+ send = gr.Button("Send", variant="primary", scale=1)
599
 
600
  with gr.Row():
601
+ temperature = gr.Slider(0.1, 1.5, 0.7, step=0.1, label="Temperature")
602
+ max_tokens = gr.Slider(50, 400, 200, step=50, label="Max tokens")
603
 
604
  with gr.Row():
605
+ good = gr.Button("Helpful", variant="secondary")
606
+ bad = gr.Button("Not helpful", variant="secondary")
607
+ clear = gr.Button("Clear", variant="secondary")
608
+
609
+ status = gr.Textbox(label="", show_label=False, lines=1)
610
 
611
+ send.click(respond, inputs=[msg, chatbot, temperature, max_tokens], outputs=[msg, chatbot])
612
+ msg.submit(respond, inputs=[msg, chatbot, temperature, max_tokens], outputs=[msg, chatbot])
613
 
614
+ good.click(feedback_good, outputs=status)
615
+ bad.click(feedback_bad, outputs=status)
616
+ clear.click(clear_chat, outputs=[chatbot, status])
 
 
617
 
 
618
  gr.Examples(
619
  examples=[
620
+ ["Write bubble sort in python"],
621
+ ["Write binary search"],
 
622
  ["Explain recursion"],
 
623
  ["2+2=?"],
624
+ ["(10+5)/3"],
625
+ ["2^5"],
626
  ],
627
  inputs=msg,
628
  )
629
 
630
+ with gr.TabItem("Statistics"):
631
+ stats_md = gr.Markdown()
632
+ refresh = gr.Button("Refresh")
633
+ refresh.click(get_stats_md, outputs=stats_md)
634
+ # Show stats immediately
635
+ demo.load(get_stats_md, outputs=stats_md)
 
 
 
 
 
 
 
 
 
636
 
637
  if __name__ == "__main__":
638
  demo.launch(server_name="0.0.0.0", server_port=7860)