petter2025 commited on
Commit
b2f7452
·
verified ·
1 Parent(s): ad7d480

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +163 -77
app.py CHANGED
@@ -7,44 +7,75 @@ import os
7
  import torch
8
  import numpy as np
9
  from datetime import datetime
10
- from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
11
- from sentence_transformers import SentenceTransformer, util
12
- from diffusers import StableDiffusionPipeline
13
- import librosa
14
- import soundfile as sf
15
- import tempfile
16
 
17
  # ARF components
18
  from agentic_reliability_framework.runtime.engine import EnhancedReliabilityEngine
19
- from hallucination_detective import HallucinationDetectiveAgent
20
- from memory_drift_diagnostician import MemoryDriftDiagnosticianAgent
21
- from image_detector import ImageQualityDetector
22
- from audio_detector import AudioQualityDetector
23
  from ai_event import AIEvent
24
  from ai_risk_engine import AIRiskEngine
 
 
25
  from nli_detector import NLIDetector
26
  from retrieval import SimpleRetriever
 
 
 
 
 
27
 
28
- logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
 
 
 
29
  logger = logging.getLogger(__name__)
30
 
31
  # ----------------------------------------------------------------------
32
- # Infrastructure engine (optional)
33
  # ----------------------------------------------------------------------
34
  try:
35
  logger.info("Initializing EnhancedReliabilityEngine...")
36
- engine = EnhancedReliabilityEngine()
37
  except Exception as e:
38
- logger.error(f"Engine init failed: {e}")
39
- engine = None
40
 
41
  # ----------------------------------------------------------------------
42
- # Generative model for text (DialoGPT-small)
43
  # ----------------------------------------------------------------------
 
44
  gen_model_name = "microsoft/DialoGPT-small"
45
- tokenizer = AutoTokenizer.from_pretrained(gen_model_name)
46
- model = AutoModelForCausalLM.from_pretrained(gen_model_name)
47
- logger.info(f"Generator {gen_model_name} loaded.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
  # ----------------------------------------------------------------------
50
  # NLI detector
@@ -52,14 +83,15 @@ logger.info(f"Generator {gen_model_name} loaded.")
52
  nli_detector = NLIDetector()
53
 
54
  # ----------------------------------------------------------------------
55
- # SentenceTransformer retriever
56
  # ----------------------------------------------------------------------
57
  retriever = SimpleRetriever()
58
- logger.info("Retriever loaded.")
59
 
60
  # ----------------------------------------------------------------------
61
- # Image generation (tiny model for demo)
62
  # ----------------------------------------------------------------------
 
 
63
  try:
64
  image_pipe = StableDiffusionPipeline.from_pretrained(
65
  "hf-internal-testing/tiny-stable-diffusion-torch"
@@ -68,12 +100,13 @@ try:
68
  image_pipe.to("cpu")
69
  logger.info("Image pipeline loaded.")
70
  except Exception as e:
71
- logger.error(f"Image pipeline failed: {e}")
72
- image_pipe = None
73
 
74
  # ----------------------------------------------------------------------
75
  # Audio transcription (Whisper tiny)
76
  # ----------------------------------------------------------------------
 
 
77
  try:
78
  audio_pipe = pipeline(
79
  "automatic-speech-recognition",
@@ -82,8 +115,7 @@ try:
82
  )
83
  logger.info("Audio pipeline loaded.")
84
  except Exception as e:
85
- logger.error(f"Audio pipeline failed: {e}")
86
- audio_pipe = None
87
 
88
  # ----------------------------------------------------------------------
89
  # AI agents
@@ -92,6 +124,7 @@ hallucination_detective = HallucinationDetectiveAgent(nli_detector=nli_detector)
92
  memory_drift_diagnostician = MemoryDriftDiagnosticianAgent()
93
  image_quality_detector = ImageQualityDetector()
94
  audio_quality_detector = AudioQualityDetector()
 
95
 
96
  # ----------------------------------------------------------------------
97
  # Bayesian risk engine
@@ -99,37 +132,32 @@ audio_quality_detector = AudioQualityDetector()
99
  ai_risk_engine = AIRiskEngine()
100
 
101
  # ----------------------------------------------------------------------
102
- # Generation helper with log probabilities
103
  # ----------------------------------------------------------------------
104
- def generate_with_logprobs(prompt, max_new_tokens=100):
105
- inputs = tokenizer(prompt, return_tensors="pt")
106
- with torch.no_grad():
107
- outputs = model.generate(
108
- **inputs,
109
- max_new_tokens=max_new_tokens,
110
- return_dict_in_generate=True,
111
- output_scores=True
112
- )
113
- scores = outputs.scores
114
- log_probs = [torch.log_softmax(score, dim=-1) for score in scores]
115
- generated_ids = outputs.sequences[0][inputs['input_ids'].shape[1]:]
116
- token_log_probs = []
117
- for i, lp in enumerate(log_probs):
118
- token_id = generated_ids[i]
119
- token_log_probs.append(lp[0, token_id].item())
120
- avg_log_prob = sum(token_log_probs) / len(token_log_probs) if token_log_probs else 0.0
121
- generated_text = tokenizer.decode(generated_ids, skip_special_tokens=True)
122
- return generated_text, avg_log_prob
123
 
124
  # ----------------------------------------------------------------------
125
- # Task handlers
 
 
 
 
 
 
 
 
 
 
 
 
126
  # ----------------------------------------------------------------------
127
  async def handle_text(task_type, prompt):
 
 
128
  try:
129
  response, avg_log_prob = generate_with_logprobs(prompt)
130
- # Get retrieval score
131
  retrieval_score = retriever.get_similarity(prompt)
132
- # Create event
133
  event = AIEvent(
134
  timestamp=datetime.utcnow(),
135
  component="ai",
@@ -145,13 +173,12 @@ async def handle_text(task_type, prompt):
145
  prompt=prompt,
146
  response=response,
147
  response_length=len(response),
148
- confidence=float(np.exp(avg_log_prob)), # convert to probability scale
149
  perplexity=None,
150
  retrieval_scores=[retrieval_score],
151
  user_feedback=None,
152
  latency_ms=0
153
  )
154
- # Analyze
155
  hallu_result = await hallucination_detective.analyze(event)
156
  drift_result = await memory_drift_diagnostician.analyze(event)
157
  risk_metrics = ai_risk_engine.risk_score(task_type)
@@ -169,14 +196,15 @@ async def handle_text(task_type, prompt):
169
  return {"error": str(e)}
170
 
171
  async def handle_image(prompt):
 
 
172
  if image_pipe is None:
173
- return {"error": "Image model not loaded"}
174
  try:
175
  import time
176
  start = time.time()
177
- image = image_pipe(prompt, num_inference_steps=2).images[0] # tiny steps for speed
178
  gen_time = time.time() - start
179
- # Mock retrieval score (you could use CLIP similarity)
180
  retrieval_score = retriever.get_similarity(prompt)
181
  event = AIEvent(
182
  timestamp=datetime.utcnow(),
@@ -191,7 +219,7 @@ async def handle_image(prompt):
191
  model_name="tiny-sd",
192
  model_version="latest",
193
  prompt=prompt,
194
- response="", # image not text
195
  response_length=0,
196
  confidence=1.0 / (gen_time + 1), # heuristic
197
  perplexity=None,
@@ -208,20 +236,24 @@ async def handle_image(prompt):
208
  }
209
  except Exception as e:
210
  logger.error(f"Image task error: {e}")
211
- return {"error": str(e)}
212
 
213
  async def handle_audio(audio_file):
 
 
214
  if audio_pipe is None:
215
  return {"error": "Audio model not loaded"}
216
  try:
217
- # Load audio (Gradio provides file path)
 
 
218
  audio, sr = librosa.load(audio_file, sr=16000)
219
  with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
220
  sf.write(tmp.name, audio, sr)
221
  result = audio_pipe(tmp.name, return_timestamps=False)
222
  text = result["text"]
223
- # Whisper does not output log probs easily; we'll use a placeholder
224
- avg_log_prob = -2.0 # placeholder
225
  event = AIEvent(
226
  timestamp=datetime.utcnow(),
227
  component="audio",
@@ -254,47 +286,96 @@ async def handle_audio(audio_file):
254
  logger.error(f"Audio task error: {e}")
255
  return {"error": str(e)}
256
 
257
- # ----------------------------------------------------------------------
258
- # Feedback handling
259
- # ----------------------------------------------------------------------
260
- last_event_category = None
261
- def feedback(thumbs_up: bool):
262
- global last_event_category
263
- if last_event_category is None:
264
- return "No previous analysis to rate."
265
- ai_risk_engine.update_outcome(last_event_category, success=thumbs_up)
266
- return f"Feedback recorded: {'👍' if thumbs_up else '👎'} for {last_event_category}."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
 
268
  # ----------------------------------------------------------------------
269
  # Gradio UI
270
  # ----------------------------------------------------------------------
271
  with gr.Blocks(title="ARF v4 – AI Reliability Lab", theme="soft") as demo:
272
- gr.Markdown("# 🧠 ARF v4 – AI Reliability Lab\n**Detect hallucinations, drift, and failures across text, image, and audio**")
273
-
274
  with gr.Tabs():
 
275
  with gr.TabItem("Text Generation"):
276
  text_task = gr.Dropdown(["chat", "code", "summary"], value="chat", label="Task")
277
- text_prompt = gr.Textbox(label="Prompt", value="What is the capital of France?")
278
  text_btn = gr.Button("Generate")
279
  text_output = gr.JSON(label="Analysis")
280
-
 
281
  with gr.TabItem("Image Generation"):
282
  img_prompt = gr.Textbox(label="Prompt", value="A cat wearing a hat")
283
  img_btn = gr.Button("Generate")
284
  img_output = gr.Image(label="Generated Image")
285
  img_json = gr.JSON(label="Analysis")
286
-
 
287
  with gr.TabItem("Audio Transcription"):
288
  audio_input = gr.Audio(type="filepath", label="Upload audio file")
289
  audio_btn = gr.Button("Transcribe")
290
  audio_output = gr.JSON(label="Analysis")
291
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  with gr.Row():
293
  feedback_up = gr.Button("👍 Correct")
294
  feedback_down = gr.Button("👎 Incorrect")
295
  feedback_msg = gr.Textbox(label="Feedback", interactive=False)
296
-
297
- # Wire up events
298
  text_btn.click(
299
  fn=lambda task, p: asyncio.run(handle_text(task, p)),
300
  inputs=[text_task, text_prompt],
@@ -310,6 +391,11 @@ with gr.Blocks(title="ARF v4 – AI Reliability Lab", theme="soft") as demo:
310
  inputs=audio_input,
311
  outputs=audio_output
312
  )
 
 
 
 
 
313
  feedback_up.click(fn=lambda: feedback(True), outputs=feedback_msg)
314
  feedback_down.click(fn=lambda: feedback(False), outputs=feedback_msg)
315
 
 
7
  import torch
8
  import numpy as np
9
  from datetime import datetime
 
 
 
 
 
 
10
 
11
  # ARF components
12
  from agentic_reliability_framework.runtime.engine import EnhancedReliabilityEngine
13
+ from agentic_reliability_framework.core.models.event import ReliabilityEvent
14
+
15
+ # Custom AI components
 
16
  from ai_event import AIEvent
17
  from ai_risk_engine import AIRiskEngine
18
+ from hallucination_detective import HallucinationDetectiveAgent
19
+ from memory_drift_diagnostician import MemoryDriftDiagnosticianAgent
20
  from nli_detector import NLIDetector
21
  from retrieval import SimpleRetriever
22
+ from image_detector import ImageQualityDetector
23
+ from audio_detector import AudioQualityDetector
24
+ from iot_simulator import IoTSimulator
25
+ from robotics_diagnostician import RoboticsDiagnostician
26
+ from iot_event import IoTEvent
27
 
28
+ # ----------------------------------------------------------------------
29
+ # Logging setup
30
+ # ----------------------------------------------------------------------
31
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
32
  logger = logging.getLogger(__name__)
33
 
34
  # ----------------------------------------------------------------------
35
+ # ARF infrastructure engine (optional)
36
  # ----------------------------------------------------------------------
37
  try:
38
  logger.info("Initializing EnhancedReliabilityEngine...")
39
+ infra_engine = EnhancedReliabilityEngine()
40
  except Exception as e:
41
+ logger.error(f"Infrastructure engine init failed: {e}")
42
+ infra_engine = None
43
 
44
  # ----------------------------------------------------------------------
45
+ # Text generation model (DialoGPT-small) with logprobs
46
  # ----------------------------------------------------------------------
47
+ from transformers import AutoTokenizer, AutoModelForCausalLM
48
  gen_model_name = "microsoft/DialoGPT-small"
49
+ try:
50
+ tokenizer = AutoTokenizer.from_pretrained(gen_model_name)
51
+ model = AutoModelForCausalLM.from_pretrained(gen_model_name)
52
+ model.eval()
53
+ logger.info(f"Generator {gen_model_name} loaded.")
54
+ except Exception as e:
55
+ logger.error(f"Generator load failed: {e}")
56
+ tokenizer = model = None
57
+
58
+ def generate_with_logprobs(prompt, max_new_tokens=100):
59
+ if tokenizer is None or model is None:
60
+ return "[Model not loaded]", -10.0
61
+ inputs = tokenizer(prompt, return_tensors="pt")
62
+ with torch.no_grad():
63
+ outputs = model.generate(
64
+ **inputs,
65
+ max_new_tokens=max_new_tokens,
66
+ return_dict_in_generate=True,
67
+ output_scores=True
68
+ )
69
+ scores = outputs.scores
70
+ log_probs = [torch.log_softmax(score, dim=-1) for score in scores]
71
+ generated_ids = outputs.sequences[0][inputs['input_ids'].shape[1]:]
72
+ token_log_probs = []
73
+ for i, lp in enumerate(log_probs):
74
+ token_id = generated_ids[i]
75
+ token_log_probs.append(lp[0, token_id].item())
76
+ avg_log_prob = sum(token_log_probs) / len(token_log_probs) if token_log_probs else -10.0
77
+ generated_text = tokenizer.decode(generated_ids, skip_special_tokens=True)
78
+ return generated_text, avg_log_prob
79
 
80
  # ----------------------------------------------------------------------
81
  # NLI detector
 
83
  nli_detector = NLIDetector()
84
 
85
  # ----------------------------------------------------------------------
86
+ # Retrieval (sentencetransformers + ChromaDB)
87
  # ----------------------------------------------------------------------
88
  retriever = SimpleRetriever()
 
89
 
90
  # ----------------------------------------------------------------------
91
+ # Image generation (tiny diffusion model)
92
  # ----------------------------------------------------------------------
93
+ from diffusers import StableDiffusionPipeline
94
+ image_pipe = None
95
  try:
96
  image_pipe = StableDiffusionPipeline.from_pretrained(
97
  "hf-internal-testing/tiny-stable-diffusion-torch"
 
100
  image_pipe.to("cpu")
101
  logger.info("Image pipeline loaded.")
102
  except Exception as e:
103
+ logger.warning(f"Image pipeline load failed (will be disabled): {e}")
 
104
 
105
  # ----------------------------------------------------------------------
106
  # Audio transcription (Whisper tiny)
107
  # ----------------------------------------------------------------------
108
+ from transformers import pipeline
109
+ audio_pipe = None
110
  try:
111
  audio_pipe = pipeline(
112
  "automatic-speech-recognition",
 
115
  )
116
  logger.info("Audio pipeline loaded.")
117
  except Exception as e:
118
+ logger.warning(f"Audio pipeline load failed (will be disabled): {e}")
 
119
 
120
  # ----------------------------------------------------------------------
121
  # AI agents
 
124
  memory_drift_diagnostician = MemoryDriftDiagnosticianAgent()
125
  image_quality_detector = ImageQualityDetector()
126
  audio_quality_detector = AudioQualityDetector()
127
+ robotics_diagnostician = RoboticsDiagnostician()
128
 
129
  # ----------------------------------------------------------------------
130
  # Bayesian risk engine
 
132
  ai_risk_engine = AIRiskEngine()
133
 
134
  # ----------------------------------------------------------------------
135
+ # IoT simulator
136
  # ----------------------------------------------------------------------
137
+ iot_sim = IoTSimulator()
138
+ iot_history = [] # store recent readings for prediction
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
 
140
  # ----------------------------------------------------------------------
141
+ # Helper: update risk with feedback
142
+ # ----------------------------------------------------------------------
143
+ last_task_category = None
144
+
145
+ def feedback(thumbs_up: bool):
146
+ global last_task_category
147
+ if last_task_category is None:
148
+ return "No previous analysis to rate."
149
+ ai_risk_engine.update_outcome(last_task_category, success=thumbs_up)
150
+ return f"Feedback recorded: {'👍' if thumbs_up else '👎'} for {last_task_category}."
151
+
152
+ # ----------------------------------------------------------------------
153
+ # Async handlers for each tab
154
  # ----------------------------------------------------------------------
155
  async def handle_text(task_type, prompt):
156
+ global last_task_category
157
+ last_task_category = task_type
158
  try:
159
  response, avg_log_prob = generate_with_logprobs(prompt)
 
160
  retrieval_score = retriever.get_similarity(prompt)
 
161
  event = AIEvent(
162
  timestamp=datetime.utcnow(),
163
  component="ai",
 
173
  prompt=prompt,
174
  response=response,
175
  response_length=len(response),
176
+ confidence=float(np.exp(avg_log_prob)), # convert to [0,1] scale (approx)
177
  perplexity=None,
178
  retrieval_scores=[retrieval_score],
179
  user_feedback=None,
180
  latency_ms=0
181
  )
 
182
  hallu_result = await hallucination_detective.analyze(event)
183
  drift_result = await memory_drift_diagnostician.analyze(event)
184
  risk_metrics = ai_risk_engine.risk_score(task_type)
 
196
  return {"error": str(e)}
197
 
198
  async def handle_image(prompt):
199
+ global last_task_category
200
+ last_task_category = "image"
201
  if image_pipe is None:
202
+ return {"error": "Image model not loaded"}, None
203
  try:
204
  import time
205
  start = time.time()
206
+ image = image_pipe(prompt, num_inference_steps=2).images[0] # minimal steps
207
  gen_time = time.time() - start
 
208
  retrieval_score = retriever.get_similarity(prompt)
209
  event = AIEvent(
210
  timestamp=datetime.utcnow(),
 
219
  model_name="tiny-sd",
220
  model_version="latest",
221
  prompt=prompt,
222
+ response="", # not text
223
  response_length=0,
224
  confidence=1.0 / (gen_time + 1), # heuristic
225
  perplexity=None,
 
236
  }
237
  except Exception as e:
238
  logger.error(f"Image task error: {e}")
239
+ return {"error": str(e)}, None
240
 
241
  async def handle_audio(audio_file):
242
+ global last_task_category
243
+ last_task_category = "audio"
244
  if audio_pipe is None:
245
  return {"error": "Audio model not loaded"}
246
  try:
247
+ import librosa
248
+ import soundfile as sf
249
+ import tempfile
250
  audio, sr = librosa.load(audio_file, sr=16000)
251
  with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
252
  sf.write(tmp.name, audio, sr)
253
  result = audio_pipe(tmp.name, return_timestamps=False)
254
  text = result["text"]
255
+ # Whisper does not output log probs easily; use placeholder
256
+ avg_log_prob = -2.0
257
  event = AIEvent(
258
  timestamp=datetime.utcnow(),
259
  component="audio",
 
286
  logger.error(f"Audio task error: {e}")
287
  return {"error": str(e)}
288
 
289
+ async def read_iot_sensors(fault_type):
290
+ global last_task_category, iot_history
291
+ last_task_category = "iot"
292
+ iot_sim.set_fault(fault_type if fault_type != "none" else None)
293
+ data = iot_sim.read()
294
+ iot_history.append(data)
295
+ if len(iot_history) > 100:
296
+ iot_history.pop(0)
297
+
298
+ # Create IoTEvent
299
+ event = IoTEvent(
300
+ timestamp=datetime.utcnow(),
301
+ component="robotic_arm",
302
+ service_mesh="factory",
303
+ latency_p99=0,
304
+ error_rate=0.0,
305
+ throughput=1,
306
+ cpu_util=None,
307
+ memory_util=None,
308
+ temperature=data['temperature'],
309
+ vibration=data['vibration'],
310
+ motor_current=data['motor_current'],
311
+ position_error=data['position_error']
312
+ )
313
+ # Run diagnostician
314
+ diag_result = await robotics_diagnostician.analyze(event)
315
+
316
+ # Simple failure prediction
317
+ prediction = None
318
+ if len(iot_history) >= 5:
319
+ temps = [h['temperature'] for h in iot_history[-5:]]
320
+ x = np.arange(len(temps))
321
+ slope, intercept = np.polyfit(x, temps, 1)
322
+ next_temp = slope * len(temps) + intercept
323
+ if slope > 0.1:
324
+ time_to_threshold = (40.0 - next_temp) / slope if slope > 0 else None
325
+ prediction = {
326
+ "predicted_temperature": next_temp,
327
+ "time_to_overheat_min": time_to_threshold
328
+ }
329
+
330
+ return data, diag_result, prediction
331
 
332
  # ----------------------------------------------------------------------
333
  # Gradio UI
334
  # ----------------------------------------------------------------------
335
  with gr.Blocks(title="ARF v4 – AI Reliability Lab", theme="soft") as demo:
336
+ gr.Markdown("# 🧠 ARF v4 – AI Reliability Lab\n**Detect hallucinations, drift, and failures across text, image, audio, and robotics**")
337
+
338
  with gr.Tabs():
339
+ # Tab 1: Text Generation
340
  with gr.TabItem("Text Generation"):
341
  text_task = gr.Dropdown(["chat", "code", "summary"], value="chat", label="Task")
342
+ text_prompt = gr.Textbox(label="Prompt", value="What is the capital of France?", lines=3)
343
  text_btn = gr.Button("Generate")
344
  text_output = gr.JSON(label="Analysis")
345
+
346
+ # Tab 2: Image Generation
347
  with gr.TabItem("Image Generation"):
348
  img_prompt = gr.Textbox(label="Prompt", value="A cat wearing a hat")
349
  img_btn = gr.Button("Generate")
350
  img_output = gr.Image(label="Generated Image")
351
  img_json = gr.JSON(label="Analysis")
352
+
353
+ # Tab 3: Audio Transcription
354
  with gr.TabItem("Audio Transcription"):
355
  audio_input = gr.Audio(type="filepath", label="Upload audio file")
356
  audio_btn = gr.Button("Transcribe")
357
  audio_output = gr.JSON(label="Analysis")
358
+
359
+ # Tab 4: Robotics / IoT
360
+ with gr.TabItem("Robotics / IoT"):
361
+ gr.Markdown("### Simulated Robotic Arm Monitoring")
362
+ fault_type = gr.Dropdown(
363
+ ["none", "overheat", "vibration", "stall", "drift"],
364
+ value="none",
365
+ label="Inject Fault"
366
+ )
367
+ refresh_btn = gr.Button("Read Sensors")
368
+ sensor_display = gr.JSON(label="Sensor Readings")
369
+ diag_display = gr.JSON(label="Diagnosis")
370
+ pred_display = gr.JSON(label="Failure Prediction")
371
+
372
+ # Feedback row
373
  with gr.Row():
374
  feedback_up = gr.Button("👍 Correct")
375
  feedback_down = gr.Button("👎 Incorrect")
376
  feedback_msg = gr.Textbox(label="Feedback", interactive=False)
377
+
378
+ # Wire events
379
  text_btn.click(
380
  fn=lambda task, p: asyncio.run(handle_text(task, p)),
381
  inputs=[text_task, text_prompt],
 
391
  inputs=audio_input,
392
  outputs=audio_output
393
  )
394
+ refresh_btn.click(
395
+ fn=lambda f: asyncio.run(read_iot_sensors(f)),
396
+ inputs=fault_type,
397
+ outputs=[sensor_display, diag_display, pred_display]
398
+ )
399
  feedback_up.click(fn=lambda: feedback(True), outputs=feedback_msg)
400
  feedback_down.click(fn=lambda: feedback(False), outputs=feedback_msg)
401