galbendavids commited on
Commit
75c53f5
ยท
1 Parent(s): de2bc35

Fix: Hebrew Elantra N detection, final answer display, stronger prompts; UI redesign; Hebrew comparison test

Browse files
Files changed (4) hide show
  1. agent.py +14 -4
  2. app.py +178 -135
  3. rag_engine.py +6 -6
  4. tests/test_business_logic.py +15 -0
agent.py CHANGED
@@ -141,6 +141,7 @@ def run_stream(engine: RAGEngine, graph, query: str, api_key: str):
141
  """
142
  Run the agent graph and yield progress (steps + draft) for each step.
143
  Updates engine cache and history with the final answer. Yields strings for Gradio.
 
144
  """
145
  initial: AgentState = {"query": query, "api_key": api_key}
146
  last_state: AgentState = initial
@@ -155,12 +156,21 @@ def run_stream(engine: RAGEngine, graph, query: str, api_key: str):
155
  text = f"{text}\n\n{body}"
156
  yield text
157
 
158
- # Final state: update cache and history
159
- final_answer = last_state.get("refusal") or last_state.get("draft_answer") or ""
160
- steps_log = last_state.get("steps_log") or []
 
 
 
 
 
 
 
 
 
161
  if not any(final_answer.startswith(p) for p in ("โš ๏ธ", "โŒ", "โฑ๏ธ")):
162
  cache_key = engine._get_cache_key(query)
163
  engine.response_cache[cache_key] = final_answer
164
  engine._maintain_conversation_history(query, final_answer)
165
- steps_log.append("โœ… Done")
166
  yield f"{chr(10).join(steps_log)}\n\n{final_answer}"
 
141
  """
142
  Run the agent graph and yield progress (steps + draft) for each step.
143
  Updates engine cache and history with the final answer. Yields strings for Gradio.
144
+ Ensures the user always sees a final verbal answer (or a clear error message).
145
  """
146
  initial: AgentState = {"query": query, "api_key": api_key}
147
  last_state: AgentState = initial
 
156
  text = f"{text}\n\n{body}"
157
  yield text
158
 
159
+ # Final state: ensure we have a visible answer
160
+ final_answer = (last_state.get("refusal") or last_state.get("draft_answer") or "").strip()
161
+ steps_log = list(last_state.get("steps_log") or [])
162
+ steps_log.append("โœ… Done")
163
+
164
+ # If no answer at all, show a clear fallback (user expects a verbal, detailed response)
165
+ if not final_answer:
166
+ final_answer = (
167
+ "ืœื ื”ืชืงื‘ืœื” ืชืฉื•ื‘ื” ืžื”ืžื•ื“ืœ. ื™ื™ืชื›ืŸ ืฉื ื—ืกืžื” ืื• ืฉื”ื‘ืงืฉื” ืืจื›ื” ืžื“ื™. "
168
+ "ื ืกื” ืœืงืฆืจ ืืช ื”ืฉืืœื” ืื• ืœืฉืื•ืœ ืฉื•ื‘."
169
+ )
170
+
171
  if not any(final_answer.startswith(p) for p in ("โš ๏ธ", "โŒ", "โฑ๏ธ")):
172
  cache_key = engine._get_cache_key(query)
173
  engine.response_cache[cache_key] = final_answer
174
  engine._maintain_conversation_history(query, final_answer)
175
+
176
  yield f"{chr(10).join(steps_log)}\n\n{final_answer}"
app.py CHANGED
@@ -102,129 +102,155 @@ def create_comparison_start():
102
  """Generate a comparison question template"""
103
  return "Compare between the cars: "
104
 
105
- # Clean, professional UI with excellent readability
106
  custom_css = """
 
 
107
  :root {
108
- --primary: #1e3a8a;
109
- --text-primary: #1f2937;
110
- --text-secondary: #6b7280;
111
- --bg-white: #ffffff;
112
- --bg-light: #f9fafb;
113
- --border: #e5e7eb;
 
 
 
 
 
 
 
 
 
 
 
 
114
  }
115
 
116
- /* Dark mode (Gradio adds dark theme on body / html in some builds).
117
- We override our own variables to keep contrast strong. */
118
  body.dark, [data-theme="dark"], .dark {
119
- --primary: #93c5fd;
120
- --text-primary: #e5e7eb;
121
- --text-secondary: #9ca3af;
122
- --bg-white: #0b1220;
123
- --bg-light: #0b1220;
124
- --border: #243244;
 
 
 
 
125
  }
126
 
127
  * {
128
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
129
  }
130
 
131
  body {
132
- background: var(--bg-light) !important;
 
133
  }
134
 
135
  .gradio-container {
136
- max-width: 980px !important;
137
- margin: 20px auto !important;
 
138
  }
139
 
140
- /* Header */
141
  .header-section {
142
- background: var(--bg-white) !important;
143
- border-bottom: 3px solid var(--primary) !important;
144
- padding: 32px 20px !important;
145
- margin-bottom: 20px !important;
146
  text-align: center !important;
 
 
147
  }
148
 
149
  .header-section h1 {
150
- color: var(--primary) !important;
151
- font-size: 2.5rem !important;
152
  font-weight: 800 !important;
153
- margin: 0 0 8px 0 !important;
 
 
 
 
 
154
  }
155
 
156
- .header-section p {
 
157
  color: var(--text-secondary) !important;
158
- font-size: 1rem !important;
159
- margin: 0 !important;
160
  }
161
 
162
- /* Under-header info text */
163
  .header-note {
164
- margin-top: 10px !important;
165
- color: var(--text-secondary) !important;
166
- font-size: 0.95rem !important;
167
  line-height: 1.6 !important;
 
 
 
 
 
 
 
 
168
  }
169
 
170
- /* Chat container - Clean minimal design */
171
  .chat-container {
172
- background: var(--bg-white) !important;
173
  border: 1px solid var(--border) !important;
174
- border-radius: 10px !important;
175
  overflow: hidden !important;
 
176
  }
177
 
178
  .gradio-chatbot {
179
- background: var(--bg-white) !important;
 
 
180
  border-radius: 0 !important;
181
- border: none !important;
182
- padding: 20px !important;
183
- height: 500px !important;
184
  }
185
 
186
- /* Chat wrapper */
187
  [data-testid="chatbot"] {
188
- background: var(--bg-white) !important;
189
- border-radius: 0 !important;
190
  }
191
 
192
- .gr-box.gr-input-box {
193
- background: var(--bg-white) !important;
194
- border: none !important;
195
- border-top: 1px solid var(--border) !important;
196
- }
197
-
198
- /* Messages - Minimal, readable design */
199
  .message {
200
- padding: 20px 0 !important;
201
  margin: 0 !important;
202
- border-radius: 0 !important;
203
  line-height: 1.7 !important;
204
  color: var(--text-primary) !important;
205
- background: transparent !important;
206
- border: none !important;
207
  font-size: 15px !important;
 
 
 
208
  }
209
 
210
  .message.user {
211
- background: transparent !important;
212
- border: none !important;
213
- margin: 20px 0 !important;
214
- color: var(--text-primary) !important;
215
  text-align: right !important;
216
- padding: 12px 16px !important;
217
- background: rgba(147, 197, 253, 0.20) !important;
218
- border-radius: 8px !important;
 
 
219
  }
220
 
221
  .message.assistant {
222
- background: transparent !important;
223
- border: none !important;
224
- margin: 20px 0 !important;
225
- color: var(--text-primary) !important;
226
- padding: 12px 16px !important;
227
  text-align: left !important;
 
 
 
 
 
228
  }
229
 
230
  .message .prose, .message .prose * {
@@ -236,170 +262,187 @@ body {
236
 
237
  .message a {
238
  color: var(--primary) !important;
239
- text-decoration: underline !important;
 
 
 
 
 
240
  }
241
 
242
  .message h1, .message h2, .message h3 {
243
  color: var(--text-primary) !important;
244
- font-weight: 600 !important;
245
- margin: 12px 0 !important;
 
 
 
 
 
 
 
 
 
 
 
246
  }
247
 
248
- /* Input - Clean minimal */
249
  .gr-textbox textarea {
250
  color: var(--text-primary) !important;
251
- background: var(--bg-white) !important;
252
  border: none !important;
253
- border-top: 1px solid var(--border) !important;
254
- border-radius: 0 !important;
255
- padding: 16px !important;
256
  font-size: 15px !important;
257
- min-height: 50px !important;
 
258
  }
259
 
260
  .gr-textbox textarea:focus {
261
- border-top: 1px solid var(--border) !important;
262
- box-shadow: none !important;
263
  outline: none !important;
 
264
  }
265
 
266
  .gr-textbox textarea::placeholder {
267
- color: #9ca3af !important;
268
  }
269
 
270
- /* Buttons - Minimal style */
271
- .gr-button {
272
- background: var(--primary) !important;
273
  color: white !important;
274
  border: none !important;
275
  font-weight: 600 !important;
276
- border-radius: 6px !important;
277
- padding: 10px 20px !important;
 
 
278
  }
279
 
280
- .gr-button:hover {
281
- background: #162e4d !important;
 
282
  }
283
 
284
- .gr-button.gr-button-secondary {
285
- background: transparent !important;
286
- color: var(--text-secondary) !important;
 
287
  border: 1px solid var(--border) !important;
 
 
 
 
 
 
 
 
 
 
 
 
288
  }
289
 
290
- .gr-button-sm {
291
- padding: 6px 12px !important;
292
- font-size: 0.85rem !important;
 
293
  font-weight: 500 !important;
294
  }
295
 
296
- /* Tables */
297
  table {
298
  border-collapse: collapse !important;
299
  width: 100% !important;
300
- margin: 12px 0 !important;
 
 
301
  }
302
 
303
  table th {
304
  background: var(--primary) !important;
305
  color: white !important;
306
- padding: 10px !important;
307
  text-align: left !important;
308
  font-weight: 600 !important;
309
  }
310
 
311
  table td {
312
- padding: 10px !important;
313
  border-bottom: 1px solid var(--border) !important;
314
  color: var(--text-primary) !important;
315
  }
316
 
317
  /* Code */
318
  code {
319
- background: rgba(148, 163, 184, 0.15) !important;
320
- color: var(--text-primary) !important;
321
- padding: 3px 6px !important;
322
- border-radius: 4px !important;
323
- font-family: monospace !important;
324
  font-size: 0.9rem !important;
325
  }
326
 
327
  pre {
328
- background: rgba(148, 163, 184, 0.12) !important;
329
- color: var(--text-primary) !important;
330
- padding: 12px !important;
331
- border-radius: 6px !important;
332
  overflow-x: auto !important;
333
  }
334
 
335
  pre code {
336
  background: transparent !important;
337
  color: var(--text-primary) !important;
338
- padding: 0 !important;
339
- }
340
-
341
- /* Typography */
342
- .prose {
343
- color: var(--text-primary) !important;
344
  }
345
 
346
  .prose h1, .prose h2, .prose h3, .prose h4 {
347
  color: var(--primary) !important;
348
- font-weight: 700 !important;
349
- }
350
-
351
- .prose a {
352
- color: var(--primary) !important;
353
  }
354
 
355
- /* Hide footer */
356
  footer {
357
  display: none !important;
358
  }
359
 
360
  @media (max-width: 768px) {
361
- .message.user {
362
- margin-left: 0 !important;
363
- }
364
- .message.assistant {
365
- margin-right: 0 !important;
366
- }
367
  }
368
  """
369
 
370
- # Simple, clean theme
371
  theme = gr.themes.Base().set(
372
- button_primary_background_fill="#1e3a8a",
373
  button_primary_text_color="#ffffff",
374
  )
375
 
376
- with gr.Blocks(theme=theme, css=custom_css, title="CarsRUS") as demo:
377
 
378
- # Header
379
  with gr.Column(elem_classes="header-section"):
380
  gr.HTML("""
381
  <div>
382
- <h1>CarsRUS</h1>
 
383
  <p class="header-note">
384
- ืžืขืจื›ืช RAG ืฉืžืžืœื™ืฆื” ื•ืžืฉื•ื•ื” ืจืง ืขืœ ื‘ืกื™ืก ื›ืชื‘ื•ืช ืžึพauto.co.il ืฉื ืžืฆืื•ืช ื‘ื‘ืกื™ืก ื”ื™ื“ืข ื”ืžืงื•ืžื™. ื“ื’ืžื™ื ื ืชืžื›ื™ื: Citroen C3, Audi RS3, Kia EV9, MG S6, Hyundai Elantra N, Aion HT, Genesis GV80, Link &amp; Co 01.
385
  </p>
 
386
  </div>
387
  """)
388
 
389
- # Chat (single column layout)
390
  with gr.Column(elem_classes="chat-container"):
391
  chat_interface = gr.ChatInterface(
392
  fn=chat_function,
393
  examples=[
 
 
 
394
  "Tell me about the Audi RS3",
395
  "Compare Audi RS3 vs Hyundai Elantra N",
396
- "What's special about Kia EV9?",
397
- "ืžื” ื”ื™ืชืจื•ื ื•ืช ืฉืœ ืงื™ื” EV9?",
398
- "ื”ืฉื•ื•ืื” ื‘ื™ืŸ RS3 ืœืืœื ื˜ืจื” N",
399
  ],
400
  cache_examples=False,
401
- submit_btn="Send",
402
- stop_btn="Stop",
403
  )
404
 
405
  if __name__ == "__main__":
 
102
  """Generate a comparison question template"""
103
  return "Compare between the cars: "
104
 
105
+ # Premium, inviting UI โ€“ automotive vibe with warmth
106
  custom_css = """
107
+ @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800&family=Heebo:wght@400;500;600;700&display=swap');
108
+
109
  :root {
110
+ --primary: #c2410c;
111
+ --primary-hover: #9a3412;
112
+ --primary-soft: rgba(194, 65, 12, 0.12);
113
+ --accent: #0d9488;
114
+ --text-primary: #1c1917;
115
+ --text-secondary: #57534e;
116
+ --text-muted: #78716c;
117
+ --bg-app: linear-gradient(160deg, #fef7ed 0%, #faf5f0 50%, #f5f5f4 100%);
118
+ --bg-surface: #ffffff;
119
+ --bg-chat: #fafaf9;
120
+ --border: #e7e5e4;
121
+ --shadow-sm: 0 1px 2px rgba(0,0,0,0.04);
122
+ --shadow-md: 0 4px 12px rgba(0,0,0,0.06);
123
+ --shadow-lg: 0 12px 40px rgba(0,0,0,0.08);
124
+ --radius: 16px;
125
+ --radius-sm: 10px;
126
+ --font-head: 'Outfit', -apple-system, sans-serif;
127
+ --font-body: 'Heebo', -apple-system, sans-serif;
128
  }
129
 
 
 
130
  body.dark, [data-theme="dark"], .dark {
131
+ --primary: #ea580c;
132
+ --primary-hover: #f97316;
133
+ --primary-soft: rgba(234, 88, 12, 0.2);
134
+ --text-primary: #fafaf9;
135
+ --text-secondary: #d6d3d1;
136
+ --text-muted: #a8a29e;
137
+ --bg-app: linear-gradient(160deg, #1c1917 0%, #292524 100%);
138
+ --bg-surface: #292524;
139
+ --bg-chat: #1c1917;
140
+ --border: #44403c;
141
  }
142
 
143
  * {
144
+ font-family: var(--font-body);
145
  }
146
 
147
  body {
148
+ background: var(--bg-app) !important;
149
+ min-height: 100vh !important;
150
  }
151
 
152
  .gradio-container {
153
+ max-width: 900px !important;
154
+ margin: 0 auto !important;
155
+ padding: 24px 16px 40px !important;
156
  }
157
 
158
+ /* Header โ€“ hero style */
159
  .header-section {
160
+ background: var(--bg-surface) !important;
161
+ border-radius: var(--radius) !important;
162
+ padding: 36px 28px !important;
163
+ margin-bottom: 24px !important;
164
  text-align: center !important;
165
+ box-shadow: var(--shadow-md) !important;
166
+ border: 1px solid var(--border) !important;
167
  }
168
 
169
  .header-section h1 {
170
+ font-family: var(--font-head) !important;
171
+ font-size: 2.75rem !important;
172
  font-weight: 800 !important;
173
+ margin: 0 0 6px 0 !important;
174
+ letter-spacing: -0.02em !important;
175
+ background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%) !important;
176
+ -webkit-background-clip: text !important;
177
+ -webkit-text-fill-color: transparent !important;
178
+ background-clip: text !important;
179
  }
180
 
181
+ .header-tagline {
182
+ font-size: 1.05rem !important;
183
  color: var(--text-secondary) !important;
184
+ margin: 0 0 14px 0 !important;
185
+ font-weight: 500 !important;
186
  }
187
 
 
188
  .header-note {
189
+ font-size: 0.9rem !important;
190
+ color: var(--text-muted) !important;
 
191
  line-height: 1.6 !important;
192
+ max-width: 560px !important;
193
+ margin: 0 auto !important;
194
+ }
195
+
196
+ .header-models {
197
+ margin-top: 12px !important;
198
+ font-size: 0.8rem !important;
199
+ color: var(--text-muted) !important;
200
  }
201
 
202
+ /* Chat container โ€“ card with shadow */
203
  .chat-container {
204
+ background: var(--bg-surface) !important;
205
  border: 1px solid var(--border) !important;
206
+ border-radius: var(--radius) !important;
207
  overflow: hidden !important;
208
+ box-shadow: var(--shadow-lg) !important;
209
  }
210
 
211
  .gradio-chatbot {
212
+ background: var(--bg-chat) !important;
213
+ padding: 24px !important;
214
+ height: 520px !important;
215
  border-radius: 0 !important;
 
 
 
216
  }
217
 
 
218
  [data-testid="chatbot"] {
219
+ background: var(--bg-chat) !important;
 
220
  }
221
 
222
+ /* Message bubbles */
 
 
 
 
 
 
223
  .message {
224
+ padding: 14px 0 !important;
225
  margin: 0 !important;
 
226
  line-height: 1.7 !important;
227
  color: var(--text-primary) !important;
 
 
228
  font-size: 15px !important;
229
+ border: none !important;
230
+ background: transparent !important;
231
+ border-radius: 0 !important;
232
  }
233
 
234
  .message.user {
235
+ margin: 12px 0 !important;
236
+ padding: 14px 18px !important;
 
 
237
  text-align: right !important;
238
+ background: linear-gradient(135deg, var(--primary-soft) 0%, rgba(13, 148, 136, 0.08) 100%) !important;
239
+ border-radius: var(--radius-sm) var(--radius-sm) 4px var(--radius-sm) !important;
240
+ border: 1px solid rgba(194, 65, 12, 0.15) !important;
241
+ max-width: 85% !important;
242
+ margin-left: auto !important;
243
  }
244
 
245
  .message.assistant {
246
+ margin: 12px 0 !important;
247
+ padding: 16px 18px !important;
 
 
 
248
  text-align: left !important;
249
+ background: var(--bg-surface) !important;
250
+ border-radius: 4px var(--radius-sm) var(--radius-sm) var(--radius-sm) !important;
251
+ border: 1px solid var(--border) !important;
252
+ box-shadow: var(--shadow-sm) !important;
253
+ max-width: 92% !important;
254
  }
255
 
256
  .message .prose, .message .prose * {
 
262
 
263
  .message a {
264
  color: var(--primary) !important;
265
+ text-decoration: none !important;
266
+ border-bottom: 1px solid var(--primary) !important;
267
+ }
268
+
269
+ .message a:hover {
270
+ opacity: 0.9 !important;
271
  }
272
 
273
  .message h1, .message h2, .message h3 {
274
  color: var(--text-primary) !important;
275
+ font-weight: 700 !important;
276
+ margin: 14px 0 8px 0 !important;
277
+ font-family: var(--font-head) !important;
278
+ }
279
+
280
+ /* Input area โ€“ pill style */
281
+ .gr-box.gr-input-box {
282
+ background: var(--bg-surface) !important;
283
+ border: 1px solid var(--border) !important;
284
+ border-radius: 999px !important;
285
+ margin: 12px 16px 16px !important;
286
+ padding: 8px 8px 8px 20px !important;
287
+ box-shadow: var(--shadow-sm) !important;
288
  }
289
 
 
290
  .gr-textbox textarea {
291
  color: var(--text-primary) !important;
292
+ background: transparent !important;
293
  border: none !important;
294
+ padding: 12px 8px !important;
 
 
295
  font-size: 15px !important;
296
+ min-height: 48px !important;
297
+ border-radius: 0 !important;
298
  }
299
 
300
  .gr-textbox textarea:focus {
 
 
301
  outline: none !important;
302
+ box-shadow: none !important;
303
  }
304
 
305
  .gr-textbox textarea::placeholder {
306
+ color: var(--text-muted) !important;
307
  }
308
 
309
+ /* Primary button โ€“ Send */
310
+ .gr-button.primary {
311
+ background: linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%) !important;
312
  color: white !important;
313
  border: none !important;
314
  font-weight: 600 !important;
315
+ border-radius: 999px !important;
316
+ padding: 12px 24px !important;
317
+ transition: transform 0.15s ease, box-shadow 0.15s ease !important;
318
+ box-shadow: 0 2px 8px rgba(194, 65, 12, 0.35) !important;
319
  }
320
 
321
+ .gr-button.primary:hover {
322
+ transform: translateY(-1px) !important;
323
+ box-shadow: 0 4px 14px rgba(194, 65, 12, 0.4) !important;
324
  }
325
 
326
+ /* Example chips */
327
+ .gr-examples .gr-sample-button,
328
+ .gr-sample-button {
329
+ background: var(--bg-surface) !important;
330
  border: 1px solid var(--border) !important;
331
+ color: var(--text-primary) !important;
332
+ border-radius: 999px !important;
333
+ padding: 10px 18px !important;
334
+ font-size: 0.9rem !important;
335
+ transition: all 0.2s ease !important;
336
+ }
337
+
338
+ .gr-examples .gr-sample-button:hover,
339
+ .gr-sample-button:hover {
340
+ border-color: var(--primary) !important;
341
+ color: var(--primary) !important;
342
+ background: var(--primary-soft) !important;
343
  }
344
 
345
+ /* Stop button */
346
+ .gr-button-secondary,
347
+ .gr-button:not(.primary) {
348
+ border-radius: var(--radius-sm) !important;
349
  font-weight: 500 !important;
350
  }
351
 
352
+ /* Tables in answers */
353
  table {
354
  border-collapse: collapse !important;
355
  width: 100% !important;
356
+ margin: 14px 0 !important;
357
+ border-radius: var(--radius-sm) !important;
358
+ overflow: hidden !important;
359
  }
360
 
361
  table th {
362
  background: var(--primary) !important;
363
  color: white !important;
364
+ padding: 12px 14px !important;
365
  text-align: left !important;
366
  font-weight: 600 !important;
367
  }
368
 
369
  table td {
370
+ padding: 12px 14px !important;
371
  border-bottom: 1px solid var(--border) !important;
372
  color: var(--text-primary) !important;
373
  }
374
 
375
  /* Code */
376
  code {
377
+ background: var(--primary-soft) !important;
378
+ color: var(--primary) !important;
379
+ padding: 3px 8px !important;
380
+ border-radius: 6px !important;
381
+ font-family: ui-monospace, monospace !important;
382
  font-size: 0.9rem !important;
383
  }
384
 
385
  pre {
386
+ background: var(--bg-surface) !important;
387
+ border: 1px solid var(--border) !important;
388
+ padding: 14px !important;
389
+ border-radius: var(--radius-sm) !important;
390
  overflow-x: auto !important;
391
  }
392
 
393
  pre code {
394
  background: transparent !important;
395
  color: var(--text-primary) !important;
 
 
 
 
 
 
396
  }
397
 
398
  .prose h1, .prose h2, .prose h3, .prose h4 {
399
  color: var(--primary) !important;
400
+ font-family: var(--font-head) !important;
 
 
 
 
401
  }
402
 
 
403
  footer {
404
  display: none !important;
405
  }
406
 
407
  @media (max-width: 768px) {
408
+ .header-section h1 { font-size: 2rem !important; }
409
+ .message.user { max-width: 95% !important; }
410
+ .message.assistant { max-width: 98% !important; }
 
 
 
411
  }
412
  """
413
 
 
414
  theme = gr.themes.Base().set(
415
+ button_primary_background_fill="#c2410c",
416
  button_primary_text_color="#ffffff",
417
  )
418
 
419
+ with gr.Blocks(theme=theme, css=custom_css, title="CarsRUS โ€“ ื”ืฉื•ื•ืืช ืจื›ื‘ื™ื ื—ื›ืžื”") as demo:
420
 
 
421
  with gr.Column(elem_classes="header-section"):
422
  gr.HTML("""
423
  <div>
424
+ <h1>๐Ÿš— CarsRUS</h1>
425
+ <p class="header-tagline">ื”ืฉื•ื•ืืช ืจื›ื‘ื™ื ื•ื”ืžืœืฆื•ืช ืžื‘ื•ืกืกื•ืช ื›ืชื‘ื•ืช ืžึพauto.co.il</p>
426
  <p class="header-note">
427
+ ืฉืืœ ืขืœ ื“ื’ื ื‘ื•ื“ื“ ืื• ื‘ืงืฉ ื”ืฉื•ื•ืื” ื‘ื™ืŸ ืฉื ื™ ื“ื’ืžื™ื. ื”ืชืฉื•ื‘ื•ืช ืžื‘ื•ืกืกื•ืช ืจืง ืขืœ ื›ืชื‘ื•ืช ืžื‘ืกื™ืก ื”ื™ื“ืข ื”ืžืงื•ืžื™.
428
  </p>
429
+ <p class="header-models">ื“ื’ืžื™ื: Citroen C3 ยท Audi RS3 ยท Kia EV9 ยท MG S6 ยท Hyundai Elantra N ยท Aion HT ยท Genesis GV80 ยท Link & Co 01</p>
430
  </div>
431
  """)
432
 
 
433
  with gr.Column(elem_classes="chat-container"):
434
  chat_interface = gr.ChatInterface(
435
  fn=chat_function,
436
  examples=[
437
+ "ืกืคืจ ืœื™ ืขืœ ืื•ื“ื™ RS3",
438
+ "ื”ืฉื•ื•ืื” ื‘ื™ืŸ RS3 ืœืืœื ื˜ืจื” N",
439
+ "ืžื” ืžื™ื•ื—ื“ ื‘ืงื™ื” EV9?",
440
  "Tell me about the Audi RS3",
441
  "Compare Audi RS3 vs Hyundai Elantra N",
 
 
 
442
  ],
443
  cache_examples=False,
444
+ submit_btn="ืฉืœื—",
445
+ stop_btn="ืขืฆื•ืจ",
446
  )
447
 
448
  if __name__ == "__main__":
rag_engine.py CHANGED
@@ -184,8 +184,8 @@ class RAGEngine:
184
  return None
185
 
186
  def _build_regex_patterns(self):
187
- """ื‘ื ื” ืชื‘ื ื™ื•ืช ืจื’ืงืก ื—ื›ืžื•ืช ืœื–ื™ื”ื•ื™ ืฉืžื•ืช ืจื›ื‘ื™ื"""
188
- # Patterns are lowercased and use word boundaries where appropriate
189
  self.regex_patterns = {
190
  r'\baudi[\s\-]*rs\s*3\b': 'audi_rs3',
191
  r'\bcitroen[\s\-]*c\s*3\b': 'citroen_c3',
@@ -194,13 +194,13 @@ class RAGEngine:
194
  r'\bev\s*9\b': 'kia_ev9',
195
  r'\bhyundai[\s\-]*elantra\s*n\b': 'hyundai_elantra_n',
196
  r'\belantra\s*n\b': 'hyundai_elantra_n',
 
197
  r'\baion\s*ht\b': 'aion_ht',
198
  r'\bgenesis[\s\-]*gv\s*80\b': 'genesis_gv80',
199
  r'\bgv\s*80\b': 'genesis_gv80',
200
  r'\blink\s*&?\s*co\s*01\b': 'link_co_01',
201
  r'\brs\s*3\b': 'audi_rs3',
202
  r'\bcorolla\b': 'toyota_corolla',
203
- # Add other common patterns as needed
204
  }
205
 
206
  def _extract_keywords(self, text: str) -> List[str]:
@@ -884,7 +884,7 @@ Content: {r['text'][:self.max_context_chars_per_chunk]}...
884
  """
885
  conversation_context = self._get_context_from_history()
886
  system_prompt = """You are an expert automotive assistant. Your answer MUST be based only on the "Context from car reviews" provided in the user message.
887
- Your task: aggregate and summarize the information from that context and directly answer the user's question. Do not use general knowledge beyond the context.
888
  Respond in the same language as the user (Hebrew or English). For comparison questions, provide a structured analysis with clear advantages for each vehicle.
889
  If the context is empty or irrelevant, say you have no information from your knowledge base for this question."""
890
  user_prompt = f"""Context from car reviews:
@@ -1009,7 +1009,7 @@ Content: {r['text'][:self.max_context_chars_per_chunk]}...
1009
 
1010
  # ืขืฆื” 9: ืคืจื•ืžืคื˜ ืžื•ืชืื
1011
  system_prompt = """You are an expert automotive assistant. Your answer MUST be based only on the "Context from car reviews" provided in the user message.
1012
- Your task: aggregate and summarize the information from that context and directly answer the user's question. Do not use general knowledge beyond the context.
1013
  Respond in the same language as the user (Hebrew or English). For comparison questions, provide a structured analysis with clear advantages for each vehicle.
1014
  If the context is empty or irrelevant, say you have no information from your knowledge base for this question."""
1015
 
@@ -1178,7 +1178,7 @@ Content: {r['text'][:self.max_context_chars_per_chunk]}...
1178
  """
1179
  conversation_context = self._get_context_from_history()
1180
  system_prompt = """You are an expert automotive assistant. Your answer MUST be based only on the "Context from car reviews" provided in the user message.
1181
- Your task: aggregate and summarize the information from that context and directly answer the user's question. Do not use general knowledge beyond the context.
1182
  Respond in the same language as the user (Hebrew or English). For comparison questions, provide a structured analysis with clear advantages for each vehicle.
1183
  If the context is empty or irrelevant, say you have no information from your knowledge base for this question."""
1184
  prompt = f"""Context from car reviews:
 
184
  return None
185
 
186
  def _build_regex_patterns(self):
187
+ """ื‘ื ื” ืชื‘ื ื™ื•ืช ืจื’ืงืก ื—ื›ืžื•ืช ืœื–ื™ื”ื•ื™ ืฉืžื•ืช ืจื›ื‘ื™ื (ื›ื•ืœืœ ืขื‘ืจื™ืช โ€“ ื‘ืœื™ \\b ืขืœ ืขื‘ืจื™ืช)"""
188
+ # Patterns: English use \\b; Hebrew has no word boundary (ืืœื ื˜ืจื” can appear as "ืœืืœื ื˜ืจื”")
189
  self.regex_patterns = {
190
  r'\baudi[\s\-]*rs\s*3\b': 'audi_rs3',
191
  r'\bcitroen[\s\-]*c\s*3\b': 'citroen_c3',
 
194
  r'\bev\s*9\b': 'kia_ev9',
195
  r'\bhyundai[\s\-]*elantra\s*n\b': 'hyundai_elantra_n',
196
  r'\belantra\s*n\b': 'hyundai_elantra_n',
197
+ r'ืืœื ื˜ืจื”\s*[nN]?': 'hyundai_elantra_n', # Hebrew: "ืืœื ื˜ืจื”" or "ืืœื ื˜ืจื” N" (no \\b โ€“ "ืœืืœื ื˜ืจื”" ok)
198
  r'\baion\s*ht\b': 'aion_ht',
199
  r'\bgenesis[\s\-]*gv\s*80\b': 'genesis_gv80',
200
  r'\bgv\s*80\b': 'genesis_gv80',
201
  r'\blink\s*&?\s*co\s*01\b': 'link_co_01',
202
  r'\brs\s*3\b': 'audi_rs3',
203
  r'\bcorolla\b': 'toyota_corolla',
 
204
  }
205
 
206
  def _extract_keywords(self, text: str) -> List[str]:
 
884
  """
885
  conversation_context = self._get_context_from_history()
886
  system_prompt = """You are an expert automotive assistant. Your answer MUST be based only on the "Context from car reviews" provided in the user message.
887
+ Your task: aggregate and summarize the information from that context and give a detailed, verbal answer as a car expert would to a friend. Always output a full paragraph (or more) that directly answers the user's questionโ€”never leave the answer empty or vague.
888
  Respond in the same language as the user (Hebrew or English). For comparison questions, provide a structured analysis with clear advantages for each vehicle.
889
  If the context is empty or irrelevant, say you have no information from your knowledge base for this question."""
890
  user_prompt = f"""Context from car reviews:
 
1009
 
1010
  # ืขืฆื” 9: ืคืจื•ืžืคื˜ ืžื•ืชืื
1011
  system_prompt = """You are an expert automotive assistant. Your answer MUST be based only on the "Context from car reviews" provided in the user message.
1012
+ Your task: aggregate and summarize the information from that context and give a detailed, verbal answer as a car expert would to a friend. Always output a full paragraph (or more) that directly answers the user's questionโ€”never leave the answer empty or vague.
1013
  Respond in the same language as the user (Hebrew or English). For comparison questions, provide a structured analysis with clear advantages for each vehicle.
1014
  If the context is empty or irrelevant, say you have no information from your knowledge base for this question."""
1015
 
 
1178
  """
1179
  conversation_context = self._get_context_from_history()
1180
  system_prompt = """You are an expert automotive assistant. Your answer MUST be based only on the "Context from car reviews" provided in the user message.
1181
+ Your task: aggregate and summarize the information from that context and give a detailed, verbal answer as a car expert would to a friend. Always output a full paragraph (or more) that directly answers the user's questionโ€”never leave the answer empty or vague.
1182
  Respond in the same language as the user (Hebrew or English). For comparison questions, provide a structured analysis with clear advantages for each vehicle.
1183
  If the context is empty or irrelevant, say you have no information from your knowledge base for this question."""
1184
  prompt = f"""Context from car reviews:
tests/test_business_logic.py CHANGED
@@ -86,6 +86,20 @@ def test_comparison_two_supported_no_refusal():
86
  print("โœ… test_comparison_two_supported_no_refusal passed")
87
 
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  def test_comparison_one_supported_refusal():
90
  """Comparison with only one supported car: no refusal; we answer with disclaimer + info on the one we know."""
91
  engine = _get_engine()
@@ -158,6 +172,7 @@ def run_all():
158
  test_unsupported_car_returns_refusal,
159
  test_supported_car_single_no_refusal,
160
  test_comparison_two_supported_no_refusal,
 
161
  test_comparison_one_supported_refusal,
162
  test_hybrid_search_returns_relevant_results,
163
  test_chat_function_requires_gemini_key,
 
86
  print("โœ… test_comparison_two_supported_no_refusal passed")
87
 
88
 
89
+ def test_comparison_hebrew_two_cars_identified():
90
+ """Hebrew comparison 'ื”ืฉื•ื•ืื” ื‘ื™ืŸ RS3 ืœืืœื ื˜ืจื” N' must identify both cars (audi_rs3, hyundai_elantra_n)."""
91
+ engine = _get_engine()
92
+ query = "ื”ืฉื•ื•ืื” ื‘ื™ืŸ RS3 ืœืืœื ื˜ืจื” N"
93
+ ordered = engine._get_ordered_supported_canonicals_in_text(query)
94
+ assert len(ordered) >= 2, f"Expected at least 2 cars in Hebrew comparison, got {ordered}"
95
+ assert "audi_rs3" in ordered, f"Expected audi_rs3 in {ordered}"
96
+ assert "hyundai_elantra_n" in ordered, f"Expected hyundai_elantra_n in {ordered}"
97
+ refusal, _, user_prompt, _ = engine.prepare_generation(query)
98
+ assert refusal is None, "Hebrew comparison with two cars must not refuse"
99
+ assert "Elantra" in user_prompt or "ืืœื ื˜ืจื”" in user_prompt or "RS3" in user_prompt
100
+ print("โœ… test_comparison_hebrew_two_cars_identified passed")
101
+
102
+
103
  def test_comparison_one_supported_refusal():
104
  """Comparison with only one supported car: no refusal; we answer with disclaimer + info on the one we know."""
105
  engine = _get_engine()
 
172
  test_unsupported_car_returns_refusal,
173
  test_supported_car_single_no_refusal,
174
  test_comparison_two_supported_no_refusal,
175
+ test_comparison_hebrew_two_cars_identified,
176
  test_comparison_one_supported_refusal,
177
  test_hybrid_search_returns_relevant_results,
178
  test_chat_function_requires_gemini_key,