SergioI1991 commited on
Commit
bf8669a
verified
1 Parent(s): 78fcff4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +428 -885
app.py CHANGED
@@ -1,885 +1,428 @@
1
- <!DOCTYPE html>
2
- <html lang="es">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>FarmaBot - Endocaser | Bilingual Dental Pharmacology Assistant</title>
7
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
8
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
- <style>
10
- /* Estilos para tablas Markdown */
11
- .message-content table {
12
- border-collapse: collapse;
13
- width: 100%;
14
- margin: 12px 0;
15
- font-size: 13px;
16
- background: #fff;
17
- border-radius: 8px;
18
- overflow-x: auto;
19
- border: 1px solid var(--border);
20
- display: table;
21
- }
22
- .message-content table tbody, .message-content table thead {
23
- display: table-row-group;
24
- width: 100%;
25
- }
26
- .message-content th, .message-content td {
27
- padding: 10px 12px;
28
- text-align: left;
29
- border: 1px solid var(--border);
30
- word-break: break-word;
31
- }
32
- .message-content th {
33
- background-color: var(--primary-surface);
34
- color: var(--primary-dark);
35
- font-weight: 600;
36
- white-space: normal;
37
- }
38
- .message-content tr:nth-child(even) {
39
- background-color: #f9fafb;
40
- }
41
- /* Modo tarjetas para m贸vil */
42
- @media (max-width: 480px) {
43
- .message-content table {
44
- display: block;
45
- border: none;
46
- margin: 8px 0;
47
- }
48
- .message-content table thead {
49
- display: none;
50
- }
51
- .message-content table tbody {
52
- display: flex;
53
- flex-direction: column;
54
- gap: 12px;
55
- }
56
- .message-content table tr {
57
- display: flex;
58
- flex-direction: column;
59
- border: 1px solid var(--border);
60
- border-radius: 8px;
61
- padding: 12px;
62
- background: #fff;
63
- margin-bottom: 0;
64
- }
65
- .message-content table tr:nth-child(even) {
66
- background-color: #fff;
67
- }
68
- .message-content table td {
69
- display: flex;
70
- justify-content: space-between;
71
- align-items: flex-start;
72
- border: none;
73
- padding: 8px 0;
74
- border-bottom: 1px solid var(--border);
75
- }
76
- .message-content table td:last-child {
77
- border-bottom: none;
78
- }
79
- .message-content table td::before {
80
- content: attr(data-label);
81
- font-weight: 600;
82
- color: var(--primary-dark);
83
- margin-right: 12px;
84
- flex-shrink: 0;
85
- }
86
- }
87
- .message-content ul, .message-content ol {
88
- padding-left: 20px;
89
- margin: 8px 0;
90
- }
91
- .message-content li {
92
- margin-bottom: 4px;
93
- }
94
- .message-content strong {
95
- color: var(--primary-dark);
96
- }
97
- .message-content p {
98
- margin: 0 0 8px 0;
99
- word-wrap: break-word;
100
- overflow-wrap: break-word;
101
- }
102
- :root {
103
- --primary: #4574E8;
104
- --primary-light: #6B93F2;
105
- --primary-dark: #2B4FAD;
106
- --primary-surface: #EEF2FD;
107
- --bg: #F7F8FC;
108
- --surface: #FFFFFF;
109
- --text-primary: #1A1D26;
110
- --text-secondary: #6B7280;
111
- --border: #E5E7EB;
112
- --hint: #9CA3AF;
113
- --success: #10B981;
114
- --error: #EF4444;
115
- --bot-bg: #F3F4F6;
116
- --user-bg: #EEF2FD;
117
- --sidebar-bg: #1A2332;
118
- }
119
-
120
- * { box-sizing: border-box; margin: 0; padding: 0; }
121
-
122
- body {
123
- font-family: 'Inter', sans-serif;
124
- background: var(--bg);
125
- color: var(--text-primary);
126
- height: 100vh;
127
- overflow: hidden;
128
- }
129
-
130
- .chat-container {
131
- height: 100vh;
132
- display: flex;
133
- flex-direction: column;
134
- }
135
-
136
- /* Header */
137
- .chat-header {
138
- background: linear-gradient(135deg, var(--primary), var(--primary-light));
139
- color: #fff;
140
- padding: 16px 24px;
141
- display: flex;
142
- align-items: center;
143
- gap: 14px;
144
- flex-shrink: 0;
145
- box-shadow: 0 4px 20px rgba(69, 116, 232, 0.25);
146
- }
147
-
148
- .header-logo {
149
- width: 40px;
150
- height: 40px;
151
- background: rgba(255,255,255,0.2);
152
- border-radius: 12px;
153
- display: flex;
154
- align-items: center;
155
- justify-content: center;
156
- font-size: 20px;
157
- }
158
-
159
- .header-info { flex: 1; }
160
- .header-info h2 {
161
- font-size: 18px;
162
- font-weight: 700;
163
- letter-spacing: -0.3px;
164
- }
165
- .header-info span {
166
- font-size: 12px;
167
- opacity: 0.75;
168
- font-weight: 400;
169
- }
170
-
171
- .header-buttons { display: flex; gap: 8px; }
172
- .icon-btn {
173
- background: rgba(255,255,255,0.15);
174
- border: none;
175
- color: #fff;
176
- width: 36px;
177
- height: 36px;
178
- border-radius: 10px;
179
- cursor: pointer;
180
- font-size: 14px;
181
- transition: all 0.2s;
182
- display: flex;
183
- align-items: center;
184
- justify-content: center;
185
- }
186
- .icon-btn:hover { background: rgba(255,255,255,0.25); transform: translateY(-1px); }
187
-
188
- /* Messages */
189
- .chat-messages {
190
- flex: 1;
191
- padding: 24px;
192
- overflow-y: auto;
193
- background: var(--bg);
194
- }
195
-
196
- .message {
197
- margin-bottom: 16px;
198
- display: flex;
199
- max-width: 85%;
200
- animation: fadeIn 0.3s ease;
201
- word-wrap: break-word;
202
- overflow-wrap: break-word;
203
- }
204
-
205
- @keyframes fadeIn {
206
- from { opacity: 0; transform: translateY(8px); }
207
- to { opacity: 1; transform: translateY(0); }
208
- }
209
-
210
- .message.user { margin-left: auto; }
211
- .message.bot { margin-right: auto; }
212
-
213
- .bot-avatar {
214
- width: 32px;
215
- height: 32px;
216
- background: var(--primary);
217
- border-radius: 10px;
218
- display: flex;
219
- align-items: center;
220
- justify-content: center;
221
- color: #fff;
222
- font-size: 14px;
223
- margin-right: 10px;
224
- flex-shrink: 0;
225
- margin-top: 2px;
226
- }
227
-
228
- .message-content {
229
- padding: 12px 16px;
230
- border-radius: 16px;
231
- line-height: 1.6;
232
- font-size: 14px;
233
- min-width: 0;
234
- word-wrap: break-word;
235
- overflow-wrap: break-word;
236
- }
237
-
238
- .message.user .message-content {
239
- background: var(--primary);
240
- color: #fff;
241
- border-bottom-right-radius: 4px;
242
- box-shadow: 0 2px 8px rgba(69, 116, 232, 0.2);
243
- word-wrap: break-word;
244
- overflow-wrap: break-word;
245
- }
246
-
247
- .message.bot .message-content {
248
- background: var(--surface);
249
- border: 1px solid var(--border);
250
- border-bottom-left-radius: 4px;
251
- box-shadow: 0 1px 4px rgba(0,0,0,0.04);
252
- word-wrap: break-word;
253
- overflow-wrap: break-word;
254
- }
255
-
256
- .message-content p { margin: 0; }
257
- .message-content p + p { margin-top: 8px; }
258
-
259
- /* Typing indicator */
260
- .typing-indicator {
261
- display: flex;
262
- align-items: center;
263
- gap: 4px;
264
- padding: 4px 0;
265
- }
266
- .typing-indicator span {
267
- width: 7px;
268
- height: 7px;
269
- background: var(--primary);
270
- border-radius: 50%;
271
- animation: bounce 1.3s linear infinite;
272
- }
273
- @keyframes bounce {
274
- 0%, 60%, 100% { transform: translateY(0); }
275
- 30% { transform: translateY(-6px); }
276
- }
277
-
278
- /* Welcome message */
279
- .welcome-card {
280
- background: var(--surface);
281
- border: 1px solid var(--border);
282
- border-radius: 16px;
283
- padding: 24px;
284
- text-align: center;
285
- max-width: 400px;
286
- margin: 40px auto;
287
- box-shadow: 0 2px 12px rgba(0,0,0,0.04);
288
- }
289
- .welcome-card .icon {
290
- width: 56px;
291
- height: 56px;
292
- background: var(--primary-surface);
293
- border-radius: 16px;
294
- display: flex;
295
- align-items: center;
296
- justify-content: center;
297
- margin: 0 auto 16px;
298
- font-size: 24px;
299
- color: var(--primary);
300
- }
301
- .welcome-card h3 {
302
- font-size: 18px;
303
- font-weight: 700;
304
- margin-bottom: 6px;
305
- color: var(--text-primary);
306
- }
307
- .welcome-card p {
308
- font-size: 13px;
309
- color: var(--text-secondary);
310
- line-height: 1.5;
311
- }
312
- .language-selector {
313
- display: flex;
314
- gap: 8px;
315
- justify-content: center;
316
- margin-top: 16px;
317
- }
318
- .lang-btn {
319
- padding: 8px 16px;
320
- border: 1px solid var(--primary);
321
- background: transparent;
322
- color: var(--primary);
323
- border-radius: 20px;
324
- cursor: pointer;
325
- font-weight: 500;
326
- font-size: 12px;
327
- transition: all 0.2s;
328
- }
329
- .lang-btn.active {
330
- background: var(--primary);
331
- color: #fff;
332
- }
333
- .lang-btn:hover {
334
- background: var(--primary);
335
- color: #fff;
336
- }
337
- .quick-actions {
338
- display: flex;
339
- flex-wrap: wrap;
340
- gap: 8px;
341
- justify-content: center;
342
- margin-top: 16px;
343
- }
344
- .quick-action {
345
- background: var(--primary-surface);
346
- color: var(--primary);
347
- border: 1px solid rgba(69, 116, 232, 0.15);
348
- padding: 8px 14px;
349
- border-radius: 20px;
350
- font-size: 12px;
351
- font-weight: 500;
352
- cursor: pointer;
353
- transition: all 0.2s;
354
- }
355
- .quick-action:hover {
356
- background: var(--primary);
357
- color: #fff;
358
- transform: translateY(-1px);
359
- }
360
-
361
- /* Input area */
362
- .chat-input {
363
- padding: 16px 24px;
364
- background: var(--surface);
365
- border-top: 1px solid var(--border);
366
- display: flex;
367
- align-items: flex-end;
368
- gap: 10px;
369
- flex-shrink: 0;
370
- }
371
-
372
- .chat-input textarea {
373
- flex: 1;
374
- padding: 12px 16px;
375
- border: 1px solid var(--border);
376
- border-radius: 24px;
377
- resize: none;
378
- font-size: 14px;
379
- font-family: 'Inter', sans-serif;
380
- background: var(--bg);
381
- color: var(--text-primary);
382
- transition: border-color 0.2s;
383
- max-height: 120px;
384
- min-width: 0;
385
- }
386
-
387
- /* Media queries para responsividad */
388
- @media (max-width: 768px) {
389
- .chat-header {
390
- padding: 12px 16px;
391
- }
392
- .header-info h2 {
393
- font-size: 16px;
394
- }
395
- .header-info span {
396
- font-size: 11px;
397
- }
398
- .chat-messages {
399
- padding: 16px;
400
- }
401
- .message {
402
- max-width: 90%;
403
- margin-bottom: 12px;
404
- }
405
- .message-content {
406
- padding: 10px 14px;
407
- font-size: 13px;
408
- }
409
- .message-content th, .message-content td {
410
- padding: 8px 10px;
411
- font-size: 12px;
412
- }
413
- .chat-input {
414
- padding: 12px 16px;
415
- gap: 8px;
416
- }
417
- .chat-input textarea {
418
- padding: 10px 14px;
419
- font-size: 13px;
420
- }
421
- .send-btn {
422
- width: 40px;
423
- height: 40px;
424
- font-size: 14px;
425
- }
426
- }
427
-
428
- @media (max-width: 480px) {
429
- .chat-header {
430
- padding: 10px 12px;
431
- }
432
- .header-logo {
433
- width: 32px;
434
- height: 32px;
435
- font-size: 16px;
436
- }
437
- .header-info h2 {
438
- font-size: 14px;
439
- }
440
- .header-info span {
441
- font-size: 10px;
442
- }
443
- .chat-messages {
444
- padding: 12px;
445
- }
446
- .message {
447
- max-width: 95%;
448
- margin-bottom: 10px;
449
- }
450
- .bot-avatar {
451
- width: 28px;
452
- height: 28px;
453
- font-size: 12px;
454
- margin-right: 8px;
455
- }
456
- .message-content {
457
- padding: 8px 12px;
458
- font-size: 12px;
459
- border-radius: 12px;
460
- }
461
- .message-content ul, .message-content ol {
462
- padding-left: 16px;
463
- margin: 6px 0;
464
- }
465
- .chat-input {
466
- padding: 10px 12px;
467
- gap: 6px;
468
- }
469
- .chat-input textarea {
470
- padding: 8px 12px;
471
- font-size: 12px;
472
- border-radius: 20px;
473
- }
474
- .send-btn {
475
- width: 36px;
476
- height: 36px;
477
- font-size: 12px;
478
- border-radius: 10px;
479
- }
480
- .welcome-card {
481
- max-width: 90%;
482
- padding: 16px;
483
- margin: 20px auto;
484
- }
485
- .welcome-card .icon {
486
- width: 48px;
487
- height: 48px;
488
- font-size: 20px;
489
- }
490
- .welcome-card h3 {
491
- font-size: 16px;
492
- }
493
- .welcome-card p {
494
- font-size: 12px;
495
- }
496
- .quick-action {
497
- padding: 6px 12px;
498
- font-size: 11px;
499
- }
500
- }
501
- .chat-input textarea:focus {
502
- border-color: var(--primary);
503
- outline: none;
504
- box-shadow: 0 0 0 3px rgba(69, 116, 232, 0.1);
505
- }
506
- .chat-input textarea::placeholder { color: var(--hint); }
507
-
508
- .send-btn {
509
- background: var(--primary);
510
- color: #fff;
511
- border: none;
512
- width: 44px;
513
- height: 44px;
514
- border-radius: 14px;
515
- cursor: pointer;
516
- font-size: 16px;
517
- transition: all 0.2s;
518
- display: flex;
519
- align-items: center;
520
- justify-content: center;
521
- box-shadow: 0 2px 8px rgba(69, 116, 232, 0.3);
522
- }
523
- .send-btn:hover { background: var(--primary-dark); transform: translateY(-1px); }
524
- .send-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
525
-
526
- /* Modal */
527
- .modal-overlay {
528
- position: fixed; top: 0; left: 0; width: 100%; height: 100%;
529
- background: rgba(0,0,0,0.5); backdrop-filter: blur(4px);
530
- display: flex; justify-content: center; align-items: center;
531
- z-index: 1000; opacity: 0; visibility: hidden; transition: all 0.3s;
532
- }
533
- .modal-overlay.visible { opacity: 1; visibility: visible; }
534
-
535
- .modal-content {
536
- background: var(--surface);
537
- padding: 28px;
538
- border-radius: 20px;
539
- width: 90%; max-width: 600px; max-height: 85vh; overflow-y: auto;
540
- box-shadow: 0 20px 60px rgba(0,0,0,0.15);
541
- transform: scale(0.95); transition: transform 0.3s;
542
- }
543
- .modal-overlay.visible .modal-content { transform: scale(1); }
544
-
545
- .modal-content h3 {
546
- margin-bottom: 20px;
547
- font-size: 18px;
548
- font-weight: 700;
549
- color: var(--text-primary);
550
- display: flex;
551
- align-items: center;
552
- gap: 10px;
553
- }
554
- .modal-content h3 i { color: var(--primary); }
555
-
556
- .modal-close {
557
- position: absolute; top: 16px; right: 16px;
558
- background: var(--bg); border: none; width: 32px; height: 32px;
559
- border-radius: 8px; font-size: 16px; color: var(--text-secondary);
560
- cursor: pointer; display: flex; align-items: center; justify-content: center;
561
- }
562
- .modal-close:hover { background: var(--border); }
563
-
564
- .admin-grid {
565
- display: grid;
566
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
567
- gap: 10px;
568
- margin-bottom: 20px;
569
- }
570
-
571
- .admin-btn {
572
- background: var(--bg);
573
- border: 1px solid var(--border);
574
- color: var(--text-primary);
575
- padding: 14px;
576
- border-radius: 12px;
577
- cursor: pointer;
578
- font-size: 13px;
579
- font-weight: 500;
580
- font-family: 'Inter', sans-serif;
581
- display: flex;
582
- align-items: center;
583
- gap: 10px;
584
- transition: all 0.2s;
585
- }
586
- .admin-btn:hover {
587
- background: var(--primary);
588
- color: #fff;
589
- border-color: var(--primary);
590
- transform: translateY(-2px);
591
- box-shadow: 0 4px 12px rgba(69, 116, 232, 0.25);
592
- }
593
- .admin-btn i { font-size: 16px; width: 20px; text-align: center; }
594
-
595
- .status-box {
596
- background: var(--bg);
597
- padding: 14px;
598
- border-radius: 10px;
599
- font-family: 'JetBrains Mono', monospace;
600
- font-size: 12px;
601
- white-space: pre-wrap;
602
- border: 1px solid var(--border);
603
- color: var(--text-secondary);
604
- }
605
-
606
- .form-group { margin-bottom: 14px; }
607
- .form-group label { display: block; margin-bottom: 6px; font-weight: 500; font-size: 13px; }
608
- .form-group input {
609
- width: 100%; padding: 10px 14px;
610
- border: 1px solid var(--border); border-radius: 10px;
611
- background: var(--bg); color: var(--text-primary);
612
- font-family: 'Inter', sans-serif; font-size: 14px;
613
- }
614
- .form-group input:focus { border-color: var(--primary); outline: none; }
615
-
616
- .modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 20px; }
617
- .btn {
618
- padding: 10px 20px; border-radius: 10px; border: none;
619
- cursor: pointer; font-weight: 600; font-size: 13px;
620
- font-family: 'Inter', sans-serif; transition: all 0.2s;
621
- }
622
- .btn-primary { background: var(--primary); color: #fff; }
623
- .btn-primary:hover { background: var(--primary-dark); }
624
- .btn-secondary { background: var(--bg); color: var(--text-primary); border: 1px solid var(--border); }
625
-
626
- /* Scrollbar */
627
- ::-webkit-scrollbar { width: 6px; }
628
- ::-webkit-scrollbar-track { background: transparent; }
629
- ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
630
- ::-webkit-scrollbar-thumb:hover { background: var(--hint); }
631
-
632
- /* Responsive */
633
- @media (max-width: 600px) {
634
- .chat-header { padding: 12px 16px; }
635
- .chat-messages { padding: 16px; }
636
- .chat-input { padding: 12px 16px; }
637
- .message { max-width: 90%; }
638
- .welcome-card { margin: 20px 16px; }
639
- }
640
- </style>
641
- </head>
642
- <body>
643
-
644
- <div class="chat-container">
645
- <div class="chat-header">
646
- <div class="header-logo"><i class="fas fa-pills"></i></div>
647
- <div class="header-info">
648
- <h2>FarmaBot</h2>
649
- <span id="header-subtitle">Asistente de farmacolog铆a dental - Endocaser</span>
650
- </div>
651
- <div class="header-buttons">
652
- </div>
653
- </div>
654
-
655
- <div class="chat-messages" id="chat-messages">
656
- <div class="welcome-card">
657
- <div class="icon"><i class="fas fa-capsules"></i></div>
658
- <h3>FarmaBot</h3>
659
- <p id="welcome-text">Tu asistente de farmacolog铆a dental. Consulta interacciones, dosis, protocolos de medicaci贸n y m谩s.</p>
660
- <div class="language-selector">
661
- <button class="lang-btn active" id="lang-es" onclick="setLanguage('es')">Espa帽ol</button>
662
- <button class="lang-btn" id="lang-en" onclick="setLanguage('en')">English</button>
663
- </div>
664
- <div class="quick-actions" id="quick-actions-container">
665
- <button class="quick-action" onclick="sendQuickAction(this)">Interacciones medicamentosas</button>
666
- <button class="quick-action" onclick="sendQuickAction(this)">Dosis de amoxicilina</button>
667
- <button class="quick-action" onclick="sendQuickAction(this)">Analgesia post-endo</button>
668
- <button class="quick-action" onclick="sendQuickAction(this)">Paciente al茅rgico a penicilina</button>
669
- </div>
670
- </div>
671
- </div>
672
-
673
- <div class="chat-input">
674
- <textarea id="user-input" placeholder="Escribe tu consulta farmacol贸gica... / Write your pharmacological query..." rows="1" disabled></textarea>
675
- <button id="send-button" class="send-btn" disabled><i class="fas fa-paper-plane"></i></button>
676
- </div>
677
- </div>
678
-
679
-
680
-
681
- <div class="modal-overlay" id="credentials-modal">
682
- <div class="modal-content" style="max-width: 380px; position: relative;">
683
- <h3 id="credentials-title"><i class="fas fa-lock"></i> Autenticaci贸n</h3>
684
- <div class="form-group">
685
- <label for="username-input">Usuario</label>
686
- <input type="text" id="username-input" autocomplete="username">
687
- </div>
688
- <div class="form-group">
689
- <label for="password-input">Contrase帽a</label>
690
- <input type="password" id="password-input" autocomplete="current-password">
691
- </div>
692
- <div class="modal-actions">
693
- <button id="credentials-cancel-btn" class="btn btn-secondary">Cancelar</button>
694
- <button id="credentials-submit-btn" class="btn btn-primary">Acceder</button>
695
- </div>
696
- </div>
697
- </div>
698
-
699
- <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
700
- <script src="https://unpkg.com/autosize@4.0.2/dist/autosize.min.js"></script>
701
- <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
702
- <script>
703
- // Configurar marked para saltos de l铆nea autom谩ticos
704
- marked.setOptions({
705
- breaks: true,
706
- gfm: true
707
- });
708
-
709
- // Traducci贸nes para soporte biling眉e
710
- const translations = {
711
- es: {
712
- welcomeText: 'Tu asistente de farmacolog铆a dental. Consulta interacciones, dosis, protocolos de medicaci贸n y m谩s.',
713
- quickActions: [
714
- 'Interacciones medicamentosas',
715
- 'Dosis de amoxicilina',
716
- 'Analgesia post-endo',
717
- 'Paciente al茅rgico a penicilina'
718
- ],
719
- headerSubtitle: 'Asistente de farmacolog铆a dental - Endocaser',
720
- placeholder: 'Escribe tu consulta farmacol贸gica...'
721
- },
722
- en: {
723
- welcomeText: 'Your dental pharmacology assistant. Consult drug interactions, dosages, medication protocols and more.',
724
- quickActions: [
725
- 'Drug interactions',
726
- 'Amoxicillin dosage',
727
- 'Post-endo analgesia',
728
- 'Penicillin-allergic patient'
729
- ],
730
- headerSubtitle: 'Dental Pharmacology Assistant - Endocaser',
731
- placeholder: 'Write your pharmacological query...'
732
- }
733
- };
734
-
735
- let currentLanguage = 'es';
736
-
737
- function setLanguage(lang) {
738
- currentLanguage = lang;
739
- const t = translations[lang];
740
-
741
- // Actualizar UI
742
- document.getElementById('welcome-text').textContent = t.welcomeText;
743
- document.getElementById('header-subtitle').textContent = t.headerSubtitle;
744
- document.getElementById('user-input').placeholder = t.placeholder;
745
-
746
- // Actualizar botones de acci贸n r谩pida
747
- const quickActionButtons = document.querySelectorAll('#quick-actions-container .quick-action');
748
- quickActionButtons.forEach((btn, index) => {
749
- if (index < t.quickActions.length) {
750
- btn.textContent = t.quickActions[index];
751
- }
752
- });
753
-
754
- // Actualizar botones de idioma
755
- document.getElementById('lang-es').classList.toggle('active', lang === 'es');
756
- document.getElementById('lang-en').classList.toggle('active', lang === 'en');
757
- }
758
-
759
- function sendQuickAction(btn) {
760
- const input = document.getElementById('user-input');
761
- input.value = btn.textContent;
762
- document.getElementById('send-button').click();
763
- }
764
-
765
- document.addEventListener('DOMContentLoaded', () => {
766
- let sessionId = null;
767
- let sessionAuth = { admin: null, report: null };
768
-
769
- const ui = {
770
- sendButton: document.getElementById('send-button'),
771
- userInput: document.getElementById('user-input'),
772
- chatMessages: document.getElementById('chat-messages'),
773
- credsModal: {
774
- overlay: document.getElementById('credentials-modal'),
775
- title: document.getElementById('credentials-title'),
776
- usernameInput: document.getElementById('username-input'),
777
- passwordInput: document.getElementById('password-input'),
778
- cancelBtn: document.getElementById('credentials-cancel-btn'),
779
- submitBtn: document.getElementById('credentials-submit-btn')
780
- }
781
- };
782
-
783
- autosize(ui.userInput);
784
-
785
- const toggleModal = (modal, show) => modal.classList.toggle('visible', show);
786
-
787
- const initializeChat = async () => {
788
- try {
789
- const response = await axios.post('/create-session');
790
- sessionId = response.data.session_id;
791
- ui.userInput.disabled = false;
792
- ui.sendButton.disabled = false;
793
- } catch (error) {
794
- console.error('Error creating session:', error);
795
- appendMessage({sender: 'bot', text: 'Error al iniciar la sesi贸n. Recarga la p谩gina.'});
796
- }
797
- };
798
-
799
- const appendMessage = ({ sender, text, isHtml = false }) => {
800
- // Remove welcome card on first message
801
- const welcome = ui.chatMessages.querySelector('.welcome-card');
802
- if (welcome) welcome.remove();
803
-
804
- const typing = ui.chatMessages.querySelector('.typing-indicator-wrapper');
805
- if (typing) typing.remove();
806
-
807
- const el = document.createElement('div');
808
- el.className = `message ${sender}`;
809
-
810
- let content = '';
811
- if (isHtml) {
812
- content = text;
813
- } else if (sender === 'bot') {
814
- // Renderizar Markdown para el bot
815
- content = marked.parse(text);
816
- } else {
817
- // Escapar HTML para el usuario y envolver en p谩rrafo
818
- content = `<p>${text.replace(/</g, "&lt;").replace(/>/g, "&gt;")}</p>`;
819
- }
820
-
821
- if (sender === 'bot') {
822
- el.innerHTML = `<div class="bot-avatar"><i class="fas fa-pills"></i></div><div class="message-content">${content}</div>`;
823
- } else {
824
- el.innerHTML = `<div class="message-content">${content}</div>`;
825
- }
826
-
827
- ui.chatMessages.appendChild(el);
828
- ui.chatMessages.scrollTop = ui.chatMessages.scrollHeight;
829
- };
830
-
831
- const showTypingIndicator = () => {
832
- if (ui.chatMessages.querySelector('.typing-indicator-wrapper')) return;
833
- const el = document.createElement('div');
834
- el.className = 'message bot typing-indicator-wrapper';
835
- el.innerHTML = `<div class="bot-avatar"><i class="fas fa-pills"></i></div><div class="message-content"><div class="typing-indicator"><span></span><span style="animation-delay:0.2s"></span><span style="animation-delay:0.4s"></span></div></div>`;
836
- ui.chatMessages.appendChild(el);
837
- ui.chatMessages.scrollTop = ui.chatMessages.scrollHeight;
838
- };
839
-
840
- const sendMessage = async () => {
841
- if (!sessionId) return;
842
- const message = ui.userInput.value.trim();
843
- if (!message) return;
844
-
845
- appendMessage({sender: 'user', text: message});
846
- ui.userInput.value = '';
847
- autosize.update(ui.userInput);
848
- showTypingIndicator();
849
-
850
- try {
851
- const response = await axios.post('/chat-bot', { query: message, session_id: sessionId });
852
- const raw = response.data?.answer || 'Lo siento, no pude procesar la consulta.';
853
- const clean = raw.replace(/<think>[\s\S]*?<\/think>/, '').trim();
854
- appendMessage({sender: 'bot', text: clean || 'Respuesta vac铆a.'});
855
- } catch (error) {
856
- appendMessage({sender: 'bot', text: 'Error al procesar. Revisa los logs del servidor.'});
857
- }
858
- };
859
-
860
- const getCredentials = (type) => {
861
- return new Promise((resolve, reject) => {
862
- if (sessionAuth[type]) return resolve(sessionAuth[type]);
863
- ui.credsModal.usernameInput.value = '';
864
- ui.credsModal.passwordInput.value = '';
865
- toggleModal(ui.credsModal.overlay, true);
866
- ui.credsModal.usernameInput.focus();
867
- const submit = () => { cleanup(); const u = ui.credsModal.usernameInput.value, p = ui.credsModal.passwordInput.value; u && p ? resolve({username:u,password:p}) : reject(new Error('No credentials')); };
868
- const cancel = () => { cleanup(); reject(new Error('Cancelled')); };
869
- const cleanup = () => { ui.credsModal.submitBtn.removeEventListener('click', submit); ui.credsModal.cancelBtn.removeEventListener('click', cancel); toggleModal(ui.credsModal.overlay, false); };
870
- ui.credsModal.submitBtn.addEventListener('click', submit);
871
- ui.credsModal.cancelBtn.addEventListener('click', cancel);
872
- });
873
- };
874
-
875
-
876
-
877
- // Events
878
- ui.sendButton.addEventListener('click', sendMessage);
879
- ui.userInput.addEventListener('keypress', e => e.key==='Enter' && !e.shiftKey && (e.preventDefault(), sendMessage()));
880
-
881
- initializeChat();
882
- });
883
- </script>
884
- </body>
885
- </html>
 
1
+ from flask import Flask, request, send_file, abort, jsonify, url_for, render_template, Response
2
+ from flask_cors import CORS
3
+ import pandas as pd
4
+ from sentence_transformers import SentenceTransformer, util
5
+ import torch
6
+ from dataclasses import dataclass
7
+ from typing import List, Dict, Tuple, Optional, Any, Iterator
8
+ from collections import deque
9
+ import os
10
+ import logging
11
+ import atexit
12
+ from threading import Thread, Lock
13
+ import time
14
+ from datetime import datetime
15
+ from uuid import uuid4 as generate_uuid
16
+ import csv as csv_lib
17
+ import functools
18
+ import json
19
+ import re
20
+ import subprocess
21
+ import sys
22
+ import sqlite3
23
+ import io
24
+ from dotenv import load_dotenv
25
+
26
+ # Cargar variables de entorno (API Keys, etc.)
27
+ load_dotenv()
28
+
29
+ # Importaciones de tus m贸dulos de l贸gica de IA (deben estar en la misma carpeta)
30
+ try:
31
+ from llm_handling import (
32
+ initialize_and_get_rag_system,
33
+ KnowledgeRAG,
34
+ groq_bot_instance,
35
+ RAG_SOURCES_DIR,
36
+ RAG_STORAGE_PARENT_DIR,
37
+ RAG_CHUNKED_SOURCES_FILENAME,
38
+ get_answer_from_context
39
+ )
40
+ from system_prompts import QA_FORMATTER_PROMPT
41
+ except ImportError:
42
+ logging.error("No se pudieron importar los m贸dulos llm_handling.py o system_prompts.py")
43
+
44
+ # Configuraci贸n de Logs
45
+ logging.basicConfig(
46
+ level=logging.INFO,
47
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
48
+ handlers=[
49
+ logging.FileHandler("app_hybrid_rag.log"),
50
+ logging.StreamHandler()
51
+ ]
52
+ )
53
+ logger = logging.getLogger(__name__)
54
+
55
+ # --- Configuraci贸n de la Aplicaci贸n ---
56
+ ADMIN_USERNAME = os.getenv('FLASK_ADMIN_USERNAME', 'admin')
57
+ ADMIN_PASSWORD = os.getenv('FLASK_ADMIN_PASSWORD', 'admin')
58
+ FLASK_APP_HOST = os.getenv("FLASK_HOST", "0.0.0.0")
59
+ FLASK_APP_PORT = int(os.getenv("FLASK_PORT", "7860"))
60
+ FLASK_DEBUG_MODE = os.getenv("FLASK_DEBUG", "True").lower() == "true"
61
+ _APP_BASE_DIR = os.path.dirname(os.path.abspath(__file__))
62
+ TEXT_EXTRACTIONS_DIR = os.path.join(_APP_BASE_DIR, 'text_extractions')
63
+
64
+ RELATED_QUESTIONS_TO_SHOW = 10
65
+ QUESTIONS_TO_SEND_TO_GROQ_QA = 3
66
+ LLM_FORMATTER_CONFIDENCE_THRESHOLD = int(os.getenv("LLM_FORMATTER_CONFIDENCE_THRESHOLD", "95"))
67
+ HIGH_CONFIDENCE_THRESHOLD = 90
68
+ CHAT_HISTORY_TO_SEND = int(os.getenv("CHAT_HISTORY_TO_SEND", "5"))
69
+
70
+ rag_system: Optional[Any] = None
71
+
72
+ # --- Utilidad de Limpieza de Texto (Evita exceso de asteriscos) ---
73
+ def sanitize_llm_output(text: str) -> str:
74
+ """
75
+ Limpia el texto para que el renderizado de Markdown sea profesional.
76
+ Evita artefactos como triples asteriscos o espacios excesivos,
77
+ pero preserva el formato de tablas de Markdown.
78
+ """
79
+ if not text or not isinstance(text, str):
80
+ return text
81
+
82
+ # 1. Corregir errores de formato (Ej: '***' -> '**')
83
+ text = text.replace('***', '**')
84
+
85
+ # 2. Asegurar que las listas tengan espacio (Ej: '*item' -> '* item')
86
+ text = re.sub(r'(^|\n)\s*\*\s*(\w)', r'\1* \2', text)
87
+
88
+ # 3. Eliminar saltos de l铆nea excesivos (m谩s de 2)
89
+ text = re.sub(r'\n{3,}', '\n\n', text)
90
+
91
+ # 4. Eliminar espacios m煤ltiples SOLO si no parecen ser parte de una tabla
92
+ # Las tablas de Markdown suelen usar '|' y '-'
93
+ lines = text.split('\n')
94
+ sanitized_lines = []
95
+ for line in lines:
96
+ if '|' in line or (line.strip().startswith('|-') or line.strip().startswith('-|')):
97
+ # Es una l铆nea de tabla, preservamos espacios
98
+ sanitized_lines.append(line)
99
+ else:
100
+ # No es tabla, podemos normalizar espacios
101
+ sanitized_lines.append(re.sub(r' +', ' ', line))
102
+
103
+ return "\n".join(sanitized_lines).strip()
104
+
105
+ # --- Gesti贸n de Historial con SQLite ---
106
+ class ChatHistoryManager:
107
+ def __init__(self, db_path):
108
+ self.db_path = db_path
109
+ self.lock = Lock()
110
+ self._create_table()
111
+ logger.info(f"SQLite chat history manager initialized at: {self.db_path}")
112
+
113
+ def _get_connection(self):
114
+ return sqlite3.connect(self.db_path, timeout=10)
115
+
116
+ def _create_table(self):
117
+ with self.lock:
118
+ with self._get_connection() as conn:
119
+ cursor = conn.cursor()
120
+ cursor.execute("""
121
+ CREATE TABLE IF NOT EXISTS chat_histories (
122
+ session_id TEXT PRIMARY KEY,
123
+ history TEXT NOT NULL
124
+ )
125
+ """)
126
+ conn.commit()
127
+
128
+ def get_history(self, session_id: str, limit_turns: int = 5) -> list:
129
+ try:
130
+ with self._get_connection() as conn:
131
+ cursor = conn.cursor()
132
+ cursor.execute("SELECT history FROM chat_histories WHERE session_id = ?", (session_id,))
133
+ row = cursor.fetchone()
134
+ if row:
135
+ history_list = json.loads(row[0])
136
+ return history_list[-(limit_turns * 2):]
137
+ return []
138
+ except Exception as e:
139
+ logger.error(f"Error fetching history para {session_id}: {e}")
140
+ return []
141
+
142
+ def update_history(self, session_id: str, query: str, answer: str):
143
+ with self.lock:
144
+ try:
145
+ with self._get_connection() as conn:
146
+ cursor = conn.cursor()
147
+ cursor.execute("SELECT history FROM chat_histories WHERE session_id = ?", (session_id,))
148
+ row = cursor.fetchone()
149
+
150
+ history = json.loads(row[0]) if row else []
151
+ history.append({'role': 'user', 'content': query})
152
+ history.append({'role': 'assistant', 'content': answer})
153
+
154
+ updated_history_json = json.dumps(history)
155
+ cursor.execute("""
156
+ INSERT OR REPLACE INTO chat_histories (session_id, history)
157
+ VALUES (?, ?)
158
+ """, (session_id, updated_history_json))
159
+ conn.commit()
160
+ except Exception as e:
161
+ logger.error(f"Error updating history para {session_id}: {e}")
162
+
163
+ def clear_history(self, session_id: str):
164
+ with self.lock:
165
+ try:
166
+ with self._get_connection() as conn:
167
+ cursor = conn.cursor()
168
+ cursor.execute("""
169
+ INSERT OR REPLACE INTO chat_histories (session_id, history)
170
+ VALUES (?, ?)
171
+ """, (session_id, json.dumps([])))
172
+ conn.commit()
173
+ except Exception as e:
174
+ logger.error(f"Error clearing history para {session_id}: {e}")
175
+
176
+ # --- EmbeddingManager para QA en CSV ---
177
+ @dataclass
178
+ class QAEmbeddings:
179
+ questions: List[str]
180
+ question_map: List[int]
181
+ embeddings: torch.Tensor
182
+ df_qa: pd.DataFrame
183
+ original_questions: List[str]
184
+
185
+ class EmbeddingManager:
186
+ def __init__(self, model_name='all-MiniLM-L6-v2'):
187
+ self.model = SentenceTransformer(model_name)
188
+ self.embeddings = {'general': None, 'personal': None, 'greetings': None}
189
+ logger.info(f"EmbeddingManager initialized with model: {model_name}")
190
+
191
+ def _process_questions(self, df: pd.DataFrame) -> Tuple[List[str], List[int], List[str]]:
192
+ questions, question_map, original_questions = [], [], []
193
+ if 'Question' not in df.columns:
194
+ return questions, question_map, original_questions
195
+
196
+ for idx, question_text_raw in enumerate(df['Question']):
197
+ if pd.isna(question_text_raw): continue
198
+ question_text_cleaned = str(question_text_raw).strip()
199
+ if not question_text_cleaned or question_text_cleaned.lower() == "nan": continue
200
+ questions.append(question_text_cleaned)
201
+ question_map.append(idx)
202
+ original_questions.append(question_text_cleaned)
203
+
204
+ return questions, question_map, original_questions
205
+
206
+ def update_embeddings(self, general_qa: pd.DataFrame, personal_qa: pd.DataFrame, greetings_qa: pd.DataFrame):
207
+ for name, df in [('general', general_qa), ('personal', personal_qa), ('greetings', greetings_qa)]:
208
+ qs, qmap, orig = self._process_questions(df)
209
+ embs = self.model.encode(qs, convert_to_tensor=True, show_progress_bar=False) if qs else None
210
+ self.embeddings[name] = QAEmbeddings(qs, qmap, embs, df, orig)
211
+ logger.info("CSV QA embeddings updated.")
212
+
213
+ def find_best_answers(self, user_query: str, qa_type: str, top_n: int = 5):
214
+ qa_data = self.embeddings.get(qa_type)
215
+ if not qa_data or qa_data.embeddings is None or len(qa_data.embeddings) == 0:
216
+ return [], [], [], [], []
217
+
218
+ query_embedding_tensor = self.model.encode([user_query], convert_to_tensor=True, show_progress_bar=False)
219
+ cos_scores = util.cos_sim(query_embedding_tensor, qa_data.embeddings)[0]
220
+ top_k = min(top_n, len(cos_scores))
221
+
222
+ if top_k == 0: return [], [], [], [], []
223
+
224
+ top_scores_tensor, indices_tensor = torch.topk(cos_scores, k=top_k)
225
+ top_confidences = [score.item() * 100 for score in top_scores_tensor]
226
+
227
+ top_indices_mapped, top_questions, top_answers, top_images = [], [], [], []
228
+ answer_col = 'Respuesta' if 'Respuesta' in qa_data.df_qa.columns else 'Answer'
229
+
230
+ for score_idx, idx_tensor in enumerate(indices_tensor):
231
+ item_idx = idx_tensor.item()
232
+ original_df_idx = qa_data.question_map[item_idx]
233
+
234
+ top_indices_mapped.append(original_df_idx)
235
+ top_questions.append(qa_data.original_questions[item_idx])
236
+ top_answers.append(str(qa_data.df_qa[answer_col].iloc[original_df_idx]))
237
+
238
+ img = qa_data.df_qa['Image'].iloc[original_df_idx] if 'Image' in qa_data.df_qa.columns else None
239
+ top_images.append(str(img) if pd.notna(img) else None)
240
+
241
+ return top_confidences, top_questions, top_answers, top_images, top_indices_mapped
242
+
243
+ # --- Inicializaci贸n de Flask ---
244
+ app = Flask(__name__)
245
+ CORS(app, resources={r"/*": {"origins": "*"}}, supports_credentials=True)
246
+
247
+ embedding_manager = EmbeddingManager()
248
+ history_manager = ChatHistoryManager(os.path.join(_APP_BASE_DIR, 'chat_history.db'))
249
+
250
+ def clean_html_from_text(text: str) -> str:
251
+ if not isinstance(text, str): return text
252
+ return re.sub(r'<[^>]+>', '', text).strip()
253
+
254
+ def replace_placeholders_in_answer(answer, db_data=None):
255
+ if pd.isna(answer) or str(answer).strip() == '':
256
+ return "Sorry, this information is not available yet"
257
+ answer_str = str(answer)
258
+ placeholders = re.findall(r'\{(\w+)\}', answer_str)
259
+ if not placeholders: return answer_str
260
+
261
+ # L贸gica simplificada de reemplazo para evitar errores si db_data es None
262
+ for placeholder in set(placeholders):
263
+ key = placeholder.strip()
264
+ val = db_data.get(key) if db_data else "no disponible"
265
+ answer_str = answer_str.replace(f'{{{key}}}', str(val))
266
+ return answer_str
267
+
268
+ # --- L贸gica Principal del Chatbot ---
269
+ def get_hybrid_response_logic_non_streaming(user_query: str, session_id: str, user_id: Optional[str], chat_history=None) -> Dict[str, Any]:
270
+ global rag_system
271
+
272
+ if not user_query: return {'error': 'No query provided'}
273
+ if not session_id: return {'error': 'session_id is required'}
274
+
275
+ # 1. B煤squeda en Saludos
276
+ conf_greet, q_greet, a_greet, img_greet, idx_greet = embedding_manager.find_best_answers(user_query, 'greetings', top_n=1)
277
+ if conf_greet and conf_greet[0] >= HIGH_CONFIDENCE_THRESHOLD:
278
+ ans = sanitize_llm_output(a_greet[0])
279
+ history_manager.update_history(session_id, user_query, ans)
280
+ return {'query': user_query, 'answer': ans, 'confidence': conf_greet[0], 'source': 'greetings'}
281
+
282
+ # 2. B煤squeda en CSV/Excel (Personal y General)
283
+ conf_pers, q_pers, a_pers, img_pers, idx_pers = embedding_manager.find_best_answers(user_query, 'personal', top_n=1)
284
+ conf_gen, q_gen, a_gen, img_gen, idx_gen = embedding_manager.find_best_answers(user_query, 'general', top_n=1)
285
+
286
+ candidates = []
287
+ if conf_pers: candidates.append({'ans': a_pers[0], 'conf': conf_pers[0], 'type': 'personal', 'idx': idx_pers[0], 'q': q_pers[0]})
288
+ if conf_gen: candidates.append({'ans': a_gen[0], 'conf': conf_gen[0], 'type': 'general', 'idx': idx_gen[0], 'q': q_gen[0]})
289
+
290
+ if candidates:
291
+ best_candidate = max(candidates, key=lambda x: x['conf'])
292
+ if best_candidate['conf'] >= LLM_FORMATTER_CONFIDENCE_THRESHOLD:
293
+ # Formatear la respuesta usando el LLM a partir del contexto del Excel
294
+ source_type = best_candidate['type']
295
+ original_df = embedding_manager.embeddings[source_type].df_qa
296
+ matched_row_data = original_df.iloc[best_candidate['idx']]
297
+
298
+ context_dict = matched_row_data.drop('Question', errors='ignore').to_dict()
299
+ context_str = "\n".join([f"'{k}': '{v}'" for k, v in context_dict.items() if pd.notna(v) and str(v).strip() != ''])
300
+
301
+ try:
302
+ final_answer = get_answer_from_context(question=user_query, context=context_str, system_prompt=QA_FORMATTER_PROMPT)
303
+ # Aplicamos el filtro de limpieza para quitar los asteriscos feos
304
+ final_answer = sanitize_llm_output(final_answer)
305
+ except Exception as e:
306
+ logger.error(f"Error formating with LLM: {e}")
307
+ final_answer = sanitize_llm_output(best_candidate['ans'])
308
+
309
+ history_manager.update_history(session_id, user_query, final_answer)
310
+ return {'query': user_query, 'answer': final_answer, 'confidence': best_candidate['conf'], 'source': f'{source_type}_qa_formatted'}
311
+
312
+ # 3. B煤squeda RAG (FAISS)
313
+ if rag_system and hasattr(rag_system, 'retriever') and rag_system.retriever:
314
+ logger.info(f"Attempting FAISS RAG query for: {user_query[:50]}...")
315
+ rag_result = rag_system.invoke(user_query)
316
+ rag_answer = rag_result.get("answer")
317
+
318
+ if rag_answer and "does not contain specific information" not in rag_answer.lower() and "no contiene" not in rag_answer.lower():
319
+ rag_answer = sanitize_llm_output(rag_answer)
320
+ history_manager.update_history(session_id, user_query, rag_answer)
321
+ return {'query': user_query, 'answer': rag_answer, 'confidence': 85, 'source': 'document_rag_faiss'}
322
+
323
+ # 4. Fallback Groq (Memoria general)
324
+ logger.info(f"No high-confidence answer. Using Groq fallback.")
325
+ chat_hist = chat_history if chat_history is not None else history_manager.get_history(session_id)
326
+
327
+ try:
328
+ groq_stream = groq_bot_instance.stream_response({'current_query': user_query, 'chat_history': chat_hist, 'qa_related_info': ""})
329
+ groq_answer = "".join([chunk for chunk in groq_stream])
330
+ groq_answer = sanitize_llm_output(groq_answer)
331
+ except Exception as e:
332
+ logger.error(f"Groq fallback failed: {e}")
333
+ groq_answer = "Lo siento, estoy teniendo problemas de conexi贸n. Por favor intenta de nuevo m谩s tarde."
334
+
335
+ history_manager.update_history(session_id, user_query, groq_answer)
336
+ return {'query': user_query, 'answer': groq_answer, 'confidence': 75, 'source': 'groq_general_fallback'}
337
+
338
+
339
+ # --- Endpoints de la API ---
340
+ @app.route('/')
341
+ def index_route():
342
+ return render_template('chat-bot.html')
343
+
344
+ @app.route('/create-session', methods=['POST', 'GET'])
345
+ def create_session_route():
346
+ try:
347
+ session_id = str(generate_uuid())
348
+ logger.info(f"New session created: {session_id}")
349
+ return jsonify({'status': 'success', 'session_id': session_id}), 200
350
+ except Exception as e:
351
+ logger.error(f"Session creation error: {e}", exc_info=True)
352
+ return jsonify({'status': 'error', 'message': str(e)}), 500
353
+
354
+ @app.route('/version', methods=['GET'])
355
+ def get_version_route():
356
+ return jsonify({'version': '3.9.9-Clean-Output'}), 200
357
+
358
+ @app.route('/chat-bot', methods=['POST'])
359
+ def get_answer_hybrid():
360
+ data = request.json
361
+ user_query = clean_html_from_text(data.get('query', ''))
362
+ user_id = data.get('user_id')
363
+ session_id = data.get('session_id')
364
+
365
+ if not user_query or not session_id:
366
+ return jsonify({'error': 'query and session_id are required'}), 400
367
+
368
+ response_data = get_hybrid_response_logic_non_streaming(user_query, session_id, user_id, None)
369
+ return jsonify(response_data)
370
+
371
+ @app.route('/clear-history', methods=['POST'])
372
+ def clear_session_history_route():
373
+ data = request.json
374
+ session_id = data.get('session_id')
375
+ if not session_id:
376
+ return jsonify({'status': 'error', 'message': 'session_id is required'}), 400
377
+ history_manager.clear_history(session_id)
378
+ return jsonify({'status': 'success', 'message': f'History cleared for session {session_id}'})
379
+
380
+
381
+ # --- Startup ---
382
+ def load_qa_data_on_startup():
383
+ global embedding_manager
384
+ logger.info("Cargando archivos CSV de origen...")
385
+ try:
386
+ # Aseguramos de que RAG_SOURCES_DIR exista o usamos una carpeta temporal
387
+ sources_dir = globals().get('RAG_SOURCES_DIR', os.path.join(_APP_BASE_DIR, 'sources'))
388
+ general_qa_df = pd.DataFrame(columns=['Question', 'Answer', 'Image'])
389
+ personal_qa_df = pd.DataFrame(columns=['Question', 'Answer', 'Image'])
390
+ greetings_qa_df = pd.DataFrame(columns=['Question', 'Answer', 'Image'])
391
+
392
+ gen_path = os.path.join(sources_dir, 'general_qa.csv')
393
+ pers_path = os.path.join(sources_dir, 'personal_qa.csv')
394
+ greet_path = os.path.join(sources_dir, 'greetings.csv')
395
+
396
+ if os.path.exists(gen_path): general_qa_df = pd.read_csv(gen_path, encoding='cp1252')
397
+ if os.path.exists(pers_path): personal_qa_df = pd.read_csv(pers_path, encoding='cp1252')
398
+ if os.path.exists(greet_path): greetings_qa_df = pd.read_csv(greet_path, encoding='cp1252')
399
+
400
+ # Estandarizamos los nombres de columnas
401
+ for df in [general_qa_df, personal_qa_df, greetings_qa_df]:
402
+ if 'Pregunta' in df.columns and 'Question' not in df.columns:
403
+ df['Question'] = df['Pregunta']
404
+
405
+ embedding_manager.update_embeddings(general_qa_df, personal_qa_df, greetings_qa_df)
406
+ logger.info("Bases de datos cargadas y vectorizadas correctamente.")
407
+ except Exception as e:
408
+ logger.error(f"Error cargando bases de datos CSV: {e}", exc_info=True)
409
+
410
+
411
+ if __name__ == '__main__':
412
+ # Crear carpetas necesarias
413
+ for folder_path in [os.path.join(_APP_BASE_DIR, 'templates'), os.path.join(_APP_BASE_DIR, 'static')]:
414
+ os.makedirs(folder_path, exist_ok=True)
415
+
416
+ load_qa_data_on_startup()
417
+
418
+ try:
419
+ rag_system = initialize_and_get_rag_system()
420
+ logger.info("RAG system initialized.")
421
+ except Exception as e:
422
+ logger.warning(f"RAG system no pudo inicializarse: {e}")
423
+
424
+ app.run(host=FLASK_APP_HOST, port=FLASK_APP_PORT, debug=FLASK_DEBUG_MODE, use_reloader=False)
425
+
426
+
427
+
428
+