ginipick commited on
Commit
ada0c8b
·
verified ·
1 Parent(s): 5cf4b8e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +21 -2288
app.py CHANGED
@@ -1,2302 +1,35 @@
1
- import asyncio
2
- import base64
3
- import json
4
- from pathlib import Path
5
  import os
6
- import numpy as np
7
- import openai
8
- from dotenv import load_dotenv
9
- from fastapi import FastAPI, Request
10
- from fastapi.responses import HTMLResponse, StreamingResponse
11
- from fastrtc import (
12
- AdditionalOutputs,
13
- AsyncStreamHandler,
14
- Stream,
15
- get_twilio_turn_credentials,
16
- wait_for_item,
17
- )
18
- from gradio.utils import get_space
19
- from openai.types.beta.realtime import ResponseAudioTranscriptDoneEvent
20
- import httpx
21
- from typing import Optional, List, Dict
22
- import gradio as gr
23
- import io
24
- from scipy import signal
25
- import wave
26
- import aiosqlite
27
- from langdetect import detect, LangDetectException
28
- from datetime import datetime
29
- import uuid
30
 
31
- load_dotenv()
32
-
33
- SAMPLE_RATE = 24000
34
-
35
- # Use Persistent Storage path for Hugging Face Space
36
- # In HF Spaces, persistent storage is at /data
37
- if os.path.exists("/data"):
38
- PERSISTENT_DIR = "/data"
39
- else:
40
- PERSISTENT_DIR = "./data"
41
-
42
- os.makedirs(PERSISTENT_DIR, exist_ok=True)
43
- DB_PATH = os.path.join(PERSISTENT_DIR, "personal_assistant.db")
44
- print(f"Using persistent directory: {PERSISTENT_DIR}")
45
- print(f"Database path: {DB_PATH}")
46
-
47
- # HTML content embedded as a string
48
- HTML_CONTENT = """<!DOCTYPE html>
49
- <html lang="ko">
50
-
51
- <head>
52
- <meta charset="UTF-8">
53
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
54
- <title>Personal AI Assistant</title>
55
- <style>
56
- :root {
57
- --primary-color: #6f42c1;
58
- --secondary-color: #563d7c;
59
- --dark-bg: #121212;
60
- --card-bg: #1e1e1e;
61
- --text-color: #f8f9fa;
62
- --border-color: #333;
63
- --hover-color: #8a5cf6;
64
- --memory-color: #4a9eff;
65
- }
66
- body {
67
- font-family: "SF Pro Display", -apple-system, BlinkMacSystemFont, sans-serif;
68
- background-color: var(--dark-bg);
69
- color: var(--text-color);
70
- margin: 0;
71
- padding: 0;
72
- height: 100vh;
73
- display: flex;
74
- flex-direction: column;
75
- overflow: hidden;
76
- }
77
- .container {
78
- max-width: 1400px;
79
- margin: 0 auto;
80
- padding: 20px;
81
- flex-grow: 1;
82
- display: flex;
83
- flex-direction: column;
84
- width: 100%;
85
- height: 100vh;
86
- box-sizing: border-box;
87
- overflow: hidden;
88
- }
89
- .header {
90
- text-align: center;
91
- padding: 15px 0;
92
- border-bottom: 1px solid var(--border-color);
93
- margin-bottom: 20px;
94
- flex-shrink: 0;
95
- background-color: var(--card-bg);
96
- }
97
- .main-content {
98
- display: flex;
99
- gap: 20px;
100
- flex-grow: 1;
101
- min-height: 0;
102
- overflow: hidden;
103
- }
104
- .sidebar {
105
- width: 350px;
106
- flex-shrink: 0;
107
- display: flex;
108
- flex-direction: column;
109
- gap: 20px;
110
- overflow-y: auto;
111
- max-height: calc(100vh - 120px);
112
- }
113
- .chat-section {
114
- flex-grow: 1;
115
- display: flex;
116
- flex-direction: column;
117
- min-width: 0;
118
- }
119
- .logo {
120
- display: flex;
121
- align-items: center;
122
- justify-content: center;
123
- gap: 10px;
124
- }
125
- .logo h1 {
126
- margin: 0;
127
- background: linear-gradient(135deg, var(--primary-color), #a78bfa);
128
- -webkit-background-clip: text;
129
- background-clip: text;
130
- color: transparent;
131
- font-size: 32px;
132
- letter-spacing: 1px;
133
- }
134
- /* Settings section */
135
- .settings-section {
136
- background-color: var(--card-bg);
137
- border-radius: 12px;
138
- padding: 20px;
139
- border: 1px solid var(--border-color);
140
- overflow-y: auto;
141
- }
142
- .settings-grid {
143
- display: flex;
144
- flex-direction: column;
145
- gap: 15px;
146
- margin-bottom: 15px;
147
- }
148
- .setting-item {
149
- display: flex;
150
- align-items: center;
151
- justify-content: space-between;
152
- gap: 10px;
153
- }
154
- .setting-label {
155
- font-size: 14px;
156
- color: #aaa;
157
- min-width: 60px;
158
- }
159
- /* Toggle switch */
160
- .toggle-switch {
161
- position: relative;
162
- width: 50px;
163
- height: 26px;
164
- background-color: #ccc;
165
- border-radius: 13px;
166
- cursor: pointer;
167
- transition: background-color 0.3s;
168
- }
169
- .toggle-switch.active {
170
- background-color: var(--primary-color);
171
- }
172
- .toggle-slider {
173
- position: absolute;
174
- top: 3px;
175
- left: 3px;
176
- width: 20px;
177
- height: 20px;
178
- background-color: white;
179
- border-radius: 50%;
180
- transition: transform 0.3s;
181
- }
182
- .toggle-switch.active .toggle-slider {
183
- transform: translateX(24px);
184
- }
185
- /* Memory section */
186
- .memory-section {
187
- background-color: var(--card-bg);
188
- border-radius: 12px;
189
- padding: 20px;
190
- border: 1px solid var(--border-color);
191
- max-height: 300px;
192
- overflow-y: auto;
193
- }
194
- .memory-item {
195
- padding: 10px;
196
- margin-bottom: 10px;
197
- background: linear-gradient(135deg, rgba(74, 158, 255, 0.1), rgba(111, 66, 193, 0.1));
198
- border-radius: 6px;
199
- border-left: 3px solid var(--memory-color);
200
- }
201
- .memory-category {
202
- font-size: 12px;
203
- color: var(--memory-color);
204
- font-weight: bold;
205
- text-transform: uppercase;
206
- margin-bottom: 5px;
207
- }
208
- .memory-content {
209
- font-size: 14px;
210
- color: var(--text-color);
211
- }
212
- /* History section */
213
- .history-section {
214
- background-color: var(--card-bg);
215
- border-radius: 12px;
216
- padding: 20px;
217
- border: 1px solid var(--border-color);
218
- max-height: 200px;
219
- overflow-y: auto;
220
- }
221
- .history-item {
222
- padding: 10px;
223
- margin-bottom: 10px;
224
- background-color: var(--dark-bg);
225
- border-radius: 6px;
226
- cursor: pointer;
227
- transition: background-color 0.2s;
228
- }
229
- .history-item:hover {
230
- background-color: var(--hover-color);
231
- }
232
- .history-date {
233
- font-size: 12px;
234
- color: #888;
235
- }
236
- .history-preview {
237
- font-size: 14px;
238
- margin-top: 5px;
239
- overflow: hidden;
240
- text-overflow: ellipsis;
241
- white-space: nowrap;
242
- }
243
- /* Text inputs */
244
- .text-input-section {
245
- margin-top: 15px;
246
- }
247
- input[type="text"], textarea {
248
- width: 100%;
249
- background-color: var(--dark-bg);
250
- color: var(--text-color);
251
- border: 1px solid var(--border-color);
252
- padding: 10px;
253
- border-radius: 6px;
254
- font-size: 14px;
255
- box-sizing: border-box;
256
- margin-top: 5px;
257
- }
258
- input[type="text"]:focus, textarea:focus {
259
- outline: none;
260
- border-color: var(--primary-color);
261
- }
262
- textarea {
263
- resize: vertical;
264
- min-height: 80px;
265
- }
266
- .chat-container {
267
- border-radius: 12px;
268
- background-color: var(--card-bg);
269
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
270
- padding: 20px;
271
- flex-grow: 1;
272
- display: flex;
273
- flex-direction: column;
274
- border: 1px solid var(--border-color);
275
- overflow: hidden;
276
- min-height: 0;
277
- height: 100%;
278
- }
279
- .chat-messages {
280
- flex-grow: 1;
281
- overflow-y: auto;
282
- padding: 15px;
283
- scrollbar-width: thin;
284
- scrollbar-color: var(--primary-color) var(--card-bg);
285
- min-height: 0;
286
- max-height: calc(100vh - 250px);
287
- }
288
- .chat-messages::-webkit-scrollbar {
289
- width: 6px;
290
- }
291
- .chat-messages::-webkit-scrollbar-thumb {
292
- background-color: var(--primary-color);
293
- border-radius: 6px;
294
- }
295
- .message {
296
- margin-bottom: 15px;
297
- padding: 12px 16px;
298
- border-radius: 8px;
299
- font-size: 15px;
300
- line-height: 1.5;
301
- position: relative;
302
- max-width: 85%;
303
- animation: fade-in 0.3s ease-out;
304
- word-wrap: break-word;
305
- }
306
- @keyframes fade-in {
307
- from {
308
- opacity: 0;
309
- transform: translateY(10px);
310
- }
311
- to {
312
- opacity: 1;
313
- transform: translateY(0);
314
- }
315
- }
316
- .message.user {
317
- background: linear-gradient(135deg, #2c3e50, #34495e);
318
- margin-left: auto;
319
- border-bottom-right-radius: 2px;
320
- }
321
- .message.assistant {
322
- background: linear-gradient(135deg, var(--secondary-color), var(--primary-color));
323
- margin-right: auto;
324
- border-bottom-left-radius: 2px;
325
- }
326
- .message.search-result {
327
- background: linear-gradient(135deg, #1a5a3e, #2e7d32);
328
- font-size: 14px;
329
- padding: 10px;
330
- margin-bottom: 10px;
331
- }
332
- .message.memory-update {
333
- background: linear-gradient(135deg, rgba(74, 158, 255, 0.2), rgba(111, 66, 193, 0.2));
334
- font-size: 13px;
335
- padding: 8px 12px;
336
- margin-bottom: 10px;
337
- border-left: 3px solid var(--memory-color);
338
- }
339
- .language-info {
340
- font-size: 12px;
341
- color: #888;
342
- margin-left: 5px;
343
- }
344
- .controls {
345
- text-align: center;
346
- margin-top: auto;
347
- display: flex;
348
- justify-content: center;
349
- gap: 10px;
350
- flex-shrink: 0;
351
- padding-top: 20px;
352
- }
353
- /* Responsive design */
354
- @media (max-width: 1024px) {
355
- .sidebar {
356
- width: 300px;
357
- }
358
- }
359
- @media (max-width: 768px) {
360
- .main-content {
361
- flex-direction: column;
362
- }
363
- .sidebar {
364
- width: 100%;
365
- margin-bottom: 20px;
366
- }
367
- .chat-section {
368
- height: 400px;
369
- }
370
- }
371
- button {
372
- background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
373
- color: white;
374
- border: none;
375
- padding: 14px 28px;
376
- font-family: inherit;
377
- font-size: 16px;
378
- cursor: pointer;
379
- transition: all 0.3s;
380
- text-transform: uppercase;
381
- letter-spacing: 1px;
382
- border-radius: 50px;
383
- display: flex;
384
- align-items: center;
385
- justify-content: center;
386
- gap: 10px;
387
- box-shadow: 0 4px 10px rgba(111, 66, 193, 0.3);
388
- }
389
- button:hover {
390
- transform: translateY(-2px);
391
- box-shadow: 0 6px 15px rgba(111, 66, 193, 0.5);
392
- background: linear-gradient(135deg, var(--hover-color), var(--primary-color));
393
- }
394
- button:active {
395
- transform: translateY(1px);
396
- }
397
- #send-button {
398
- background: linear-gradient(135deg, #2ecc71, #27ae60);
399
- padding: 10px 20px;
400
- font-size: 14px;
401
- flex-shrink: 0;
402
- }
403
- #send-button:hover {
404
- background: linear-gradient(135deg, #27ae60, #229954);
405
- }
406
- #end-session-button {
407
- background: linear-gradient(135deg, #4a9eff, #3a7ed8);
408
- padding: 8px 16px;
409
- font-size: 13px;
410
- }
411
- #end-session-button:hover {
412
- background: linear-gradient(135deg, #3a7ed8, #2a5eb8);
413
- }
414
- #audio-output {
415
- display: none;
416
- }
417
- .icon-with-spinner {
418
- display: flex;
419
- align-items: center;
420
- justify-content: center;
421
- gap: 12px;
422
- min-width: 180px;
423
- }
424
- .spinner {
425
- width: 20px;
426
- height: 20px;
427
- border: 2px solid #ffffff;
428
- border-top-color: transparent;
429
- border-radius: 50%;
430
- animation: spin 1s linear infinite;
431
- flex-shrink: 0;
432
- }
433
- @keyframes spin {
434
- to {
435
- transform: rotate(360deg);
436
- }
437
- }
438
- .audio-visualizer {
439
- display: flex;
440
- align-items: center;
441
- justify-content: center;
442
- gap: 5px;
443
- min-width: 80px;
444
- height: 25px;
445
- }
446
- .visualizer-bar {
447
- width: 4px;
448
- height: 100%;
449
- background-color: rgba(255, 255, 255, 0.7);
450
- border-radius: 2px;
451
- transform-origin: bottom;
452
- transform: scaleY(0.1);
453
- transition: transform 0.1s ease;
454
- }
455
- .toast {
456
- position: fixed;
457
- top: 20px;
458
- left: 50%;
459
- transform: translateX(-50%);
460
- padding: 16px 24px;
461
- border-radius: 8px;
462
- font-size: 14px;
463
- z-index: 1000;
464
- display: none;
465
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
466
- }
467
- .toast.error {
468
- background-color: #f44336;
469
- color: white;
470
- }
471
- .toast.warning {
472
- background-color: #ff9800;
473
- color: white;
474
- }
475
- .toast.success {
476
- background-color: #4caf50;
477
- color: white;
478
- }
479
- .status-indicator {
480
- display: inline-flex;
481
- align-items: center;
482
- margin-top: 10px;
483
- font-size: 14px;
484
- color: #aaa;
485
- }
486
- .status-dot {
487
- width: 8px;
488
- height: 8px;
489
- border-radius: 50%;
490
- margin-right: 8px;
491
- }
492
- .status-dot.connected {
493
- background-color: #4caf50;
494
- }
495
- .status-dot.disconnected {
496
- background-color: #f44336;
497
- }
498
- .status-dot.connecting {
499
- background-color: #ff9800;
500
- animation: pulse 1.5s infinite;
501
- }
502
- @keyframes pulse {
503
- 0% {
504
- opacity: 0.6;
505
- }
506
- 50% {
507
- opacity: 1;
508
- }
509
- 100% {
510
- opacity: 0.6;
511
- }
512
- }
513
- .user-avatar {
514
- width: 40px;
515
- height: 40px;
516
- background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
517
- border-radius: 50%;
518
- display: flex;
519
- align-items: center;
520
- justify-content: center;
521
- font-size: 20px;
522
- font-weight: bold;
523
- color: white;
524
- }
525
- </style>
526
- </head>
527
-
528
- <body>
529
- <div id="error-toast" class="toast"></div>
530
- <div class="container">
531
- <div class="header">
532
- <div class="logo">
533
- <div class="user-avatar" id="user-avatar">👤</div>
534
- <h1>Personal AI Assistant</h1>
535
- </div>
536
- <div class="status-indicator">
537
- <div id="status-dot" class="status-dot disconnected"></div>
538
- <span id="status-text">연결 대기 중</span>
539
- </div>
540
- </div>
541
-
542
- <div class="main-content">
543
- <div class="sidebar">
544
- <div class="settings-section">
545
- <h3 style="margin: 0 0 15px 0; color: var(--primary-color);">설정</h3>
546
- <div class="settings-grid">
547
- <div class="setting-item">
548
- <span class="setting-label">웹 검색</span>
549
- <div id="search-toggle" class="toggle-switch">
550
- <div class="toggle-slider"></div>
551
- </div>
552
- </div>
553
- </div>
554
- <div class="text-input-section">
555
- <label for="user-name" class="setting-label">사용자 이름:</label>
556
- <input type="text" id="user-name" placeholder="이름을 입력하세요..." />
557
- </div>
558
- </div>
559
-
560
- <div class="memory-section">
561
- <h3 style="margin: 0 0 15px 0; color: var(--memory-color);">기억된 정보</h3>
562
- <div id="memory-list"></div>
563
- </div>
564
-
565
- <div class="history-section">
566
- <h3 style="margin: 0 0 15px 0; color: var(--primary-color);">대화 기록</h3>
567
- <div id="history-list"></div>
568
- </div>
569
-
570
- <div class="controls">
571
- <button id="start-button">대화 시작</button>
572
- <button id="end-session-button" style="display: none;">기억 업데이트</button>
573
- </div>
574
- </div>
575
-
576
- <div class="chat-section">
577
- <div class="chat-container">
578
- <h3 style="margin: 0 0 15px 0; color: var(--primary-color);">대화</h3>
579
- <div class="chat-messages" id="chat-messages"></div>
580
- <div class="text-input-section" style="margin-top: 10px;">
581
- <div style="display: flex; gap: 10px;">
582
- <input type="text" id="text-input" placeholder="텍스트 메시지를 입력하세요..." style="flex-grow: 1;" />
583
- <button id="send-button" style="display: none;">전송</button>
584
- </div>
585
- </div>
586
- </div>
587
- </div>
588
- </div>
589
- </div>
590
- <audio id="audio-output"></audio>
591
-
592
- <script>
593
- let peerConnection;
594
- let webrtc_id;
595
- let webSearchEnabled = false;
596
- let currentSessionId = null;
597
- let userName = localStorage.getItem('userName') || '';
598
- let userMemories = {};
599
- const audioOutput = document.getElementById('audio-output');
600
- const startButton = document.getElementById('start-button');
601
- const endSessionButton = document.getElementById('end-session-button');
602
- const sendButton = document.getElementById('send-button');
603
- const chatMessages = document.getElementById('chat-messages');
604
- const statusDot = document.getElementById('status-dot');
605
- const statusText = document.getElementById('status-text');
606
- const searchToggle = document.getElementById('search-toggle');
607
- const textInput = document.getElementById('text-input');
608
- const historyList = document.getElementById('history-list');
609
- const memoryList = document.getElementById('memory-list');
610
- const userNameInput = document.getElementById('user-name');
611
- const userAvatar = document.getElementById('user-avatar');
612
- let audioLevel = 0;
613
- let animationFrame;
614
- let audioContext, analyser, audioSource;
615
- let dataChannel = null;
616
- let isVoiceActive = false;
617
-
618
- // Initialize user name
619
- userNameInput.value = userName;
620
- if (userName) {
621
- userAvatar.textContent = userName.charAt(0).toUpperCase();
622
- }
623
-
624
- userNameInput.addEventListener('input', () => {
625
- userName = userNameInput.value;
626
- localStorage.setItem('userName', userName);
627
- if (userName) {
628
- userAvatar.textContent = userName.charAt(0).toUpperCase();
629
- } else {
630
- userAvatar.textContent = '👤';
631
- }
632
- });
633
-
634
- // Start new session
635
- async function startNewSession() {
636
- const response = await fetch('/session/new', { method: 'POST' });
637
- const data = await response.json();
638
- currentSessionId = data.session_id;
639
- console.log('New session started:', currentSessionId);
640
- loadHistory();
641
- loadMemories();
642
- }
643
-
644
- // Load memories
645
- async function loadMemories() {
646
- try {
647
- const response = await fetch('/memory/all');
648
- const memories = await response.json();
649
-
650
- userMemories = {};
651
- memoryList.innerHTML = '';
652
-
653
- console.log('[LoadMemories] Loaded memories from DB:', memories);
654
-
655
- memories.forEach(memory => {
656
- if (!userMemories[memory.category]) {
657
- userMemories[memory.category] = [];
658
- }
659
- userMemories[memory.category].push(memory.content);
660
-
661
- const item = document.createElement('div');
662
- item.className = 'memory-item';
663
- item.innerHTML = `
664
- <div class="memory-category">${memory.category}</div>
665
- <div class="memory-content">${memory.content}</div>
666
- `;
667
- memoryList.appendChild(item);
668
- });
669
-
670
- console.log('[LoadMemories] Formatted memories:', userMemories);
671
- console.log('[LoadMemories] Total categories:', Object.keys(userMemories).length);
672
- console.log('[LoadMemories] Total items:', Object.values(userMemories).flat().length);
673
-
674
- } catch (error) {
675
- console.error('Failed to load memories:', error);
676
- }
677
- }
678
-
679
- // End session and update memories
680
- async function endSession() {
681
- if (!currentSessionId) return;
682
-
683
- try {
684
- addMessage('memory-update', '대화 내용을 분석하여 기억을 업데이트하고 있습니다...');
685
-
686
- const response = await fetch('/session/end', {
687
- method: 'POST',
688
- headers: { 'Content-Type': 'application/json' },
689
- body: JSON.stringify({ session_id: currentSessionId })
690
- });
691
-
692
- const result = await response.json();
693
- if (result.status === 'ok') {
694
- showToast('기억이 성공적으로 업데이트되었습니다.', 'success');
695
- loadMemories();
696
- startNewSession();
697
- }
698
- } catch (error) {
699
- console.error('Failed to end session:', error);
700
- showError('기억 업데이트 중 오류가 발생했습니다.');
701
- }
702
- }
703
-
704
- // Load conversation history
705
- async function loadHistory() {
706
- try {
707
- const response = await fetch('/history/recent');
708
- const conversations = await response.json();
709
-
710
- historyList.innerHTML = '';
711
- conversations.forEach(conv => {
712
- const item = document.createElement('div');
713
- item.className = 'history-item';
714
- item.innerHTML = `
715
- <div class="history-date">${new Date(conv.created_at).toLocaleString()}</div>
716
- <div class="history-preview">${conv.summary || '대화 시작'}</div>
717
- `;
718
- item.onclick = () => loadConversation(conv.id);
719
- historyList.appendChild(item);
720
- });
721
- } catch (error) {
722
- console.error('Failed to load history:', error);
723
- }
724
- }
725
-
726
- // Load specific conversation
727
- async function loadConversation(sessionId) {
728
- try {
729
- const response = await fetch(`/history/${sessionId}`);
730
- const messages = await response.json();
731
-
732
- chatMessages.innerHTML = '';
733
- messages.forEach(msg => {
734
- addMessage(msg.role, msg.content, false);
735
- });
736
- } catch (error) {
737
- console.error('Failed to load conversation:', error);
738
- }
739
- }
740
-
741
- // Web search toggle functionality
742
- searchToggle.addEventListener('click', () => {
743
- webSearchEnabled = !webSearchEnabled;
744
- searchToggle.classList.toggle('active', webSearchEnabled);
745
- console.log('Web search enabled:', webSearchEnabled);
746
- });
747
-
748
- // Text input handling
749
- textInput.addEventListener('keypress', (e) => {
750
- if (e.key === 'Enter' && !e.shiftKey) {
751
- e.preventDefault();
752
- sendTextMessage();
753
- }
754
- });
755
-
756
- sendButton.addEventListener('click', sendTextMessage);
757
- endSessionButton.addEventListener('click', endSession);
758
-
759
- async function sendTextMessage() {
760
- const message = textInput.value.trim();
761
- if (!message) return;
762
-
763
- // Check for stop words
764
- const stopWords = ["중단", "그만", "스톱", "stop", "닥쳐", "멈춰", "중지"];
765
- if (stopWords.some(word => message.toLowerCase().includes(word))) {
766
- addMessage('assistant', '대화를 중단합니다.');
767
- return;
768
- }
769
-
770
- // Add user message to chat
771
- addMessage('user', message);
772
- textInput.value = '';
773
-
774
- // Show sending indicator
775
- const typingIndicator = document.createElement('div');
776
- typingIndicator.classList.add('message', 'assistant');
777
- typingIndicator.textContent = '입력 중...';
778
- typingIndicator.id = 'typing-indicator';
779
- chatMessages.appendChild(typingIndicator);
780
- chatMessages.scrollTop = chatMessages.scrollHeight;
781
-
782
- try {
783
- // Send to text chat endpoint
784
- const response = await fetch('/chat/text', {
785
- method: 'POST',
786
- headers: { 'Content-Type': 'application/json' },
787
- body: JSON.stringify({
788
- message: message,
789
- web_search_enabled: webSearchEnabled,
790
- session_id: currentSessionId,
791
- user_name: userName,
792
- memories: userMemories
793
- })
794
- });
795
-
796
- const data = await response.json();
797
-
798
- // Remove typing indicator
799
- const indicator = document.getElementById('typing-indicator');
800
- if (indicator) indicator.remove();
801
-
802
- if (data.error) {
803
- showError(data.error);
804
- } else {
805
- // Add assistant response
806
- let content = data.response;
807
- if (data.detected_language) {
808
- content += ` <span class="language-info">[${data.detected_language}]</span>`;
809
- }
810
- addMessage('assistant', content);
811
- }
812
- } catch (error) {
813
- console.error('Error sending text message:', error);
814
- const indicator = document.getElementById('typing-indicator');
815
- if (indicator) indicator.remove();
816
- showError('메시지 전송 중 오류가 발생했습니다.');
817
- }
818
- }
819
-
820
- function updateStatus(state) {
821
- statusDot.className = 'status-dot ' + state;
822
- if (state === 'connected') {
823
- statusText.textContent = '연결됨';
824
- sendButton.style.display = 'block';
825
- endSessionButton.style.display = 'block';
826
- isVoiceActive = true;
827
- } else if (state === 'connecting') {
828
- statusText.textContent = '연결 중...';
829
- sendButton.style.display = 'none';
830
- endSessionButton.style.display = 'none';
831
- } else {
832
- statusText.textContent = '연결 대기 중';
833
- sendButton.style.display = 'block';
834
- endSessionButton.style.display = 'block';
835
- isVoiceActive = false;
836
- }
837
- }
838
-
839
- function showToast(message, type = 'info') {
840
- const toast = document.getElementById('error-toast');
841
- toast.textContent = message;
842
- toast.className = `toast ${type}`;
843
- toast.style.display = 'block';
844
- setTimeout(() => {
845
- toast.style.display = 'none';
846
- }, 5000);
847
- }
848
-
849
- function showError(message) {
850
- showToast(message, 'error');
851
- }
852
-
853
- function updateButtonState() {
854
- const button = document.getElementById('start-button');
855
- if (peerConnection && (peerConnection.connectionState === 'connecting' || peerConnection.connectionState === 'new')) {
856
- button.innerHTML = `
857
- <div class="icon-with-spinner">
858
- <div class="spinner"></div>
859
- <span>연결 중...</span>
860
- </div>
861
- `;
862
- updateStatus('connecting');
863
- } else if (peerConnection && peerConnection.connectionState === 'connected') {
864
- button.innerHTML = `
865
- <div class="icon-with-spinner">
866
- <div class="audio-visualizer" id="audio-visualizer">
867
- <div class="visualizer-bar"></div>
868
- <div class="visualizer-bar"></div>
869
- <div class="visualizer-bar"></div>
870
- <div class="visualizer-bar"></div>
871
- <div class="visualizer-bar"></div>
872
- </div>
873
- <span>대화 종료</span>
874
- </div>
875
- `;
876
- updateStatus('connected');
877
- } else {
878
- button.innerHTML = '대화 시작';
879
- updateStatus('disconnected');
880
- }
881
- }
882
-
883
- function setupAudioVisualization(stream) {
884
- audioContext = new (window.AudioContext || window.webkitAudioContext)();
885
- analyser = audioContext.createAnalyser();
886
- audioSource = audioContext.createMediaStreamSource(stream);
887
- audioSource.connect(analyser);
888
- analyser.fftSize = 256;
889
- const bufferLength = analyser.frequencyBinCount;
890
- const dataArray = new Uint8Array(bufferLength);
891
-
892
- const visualizerBars = document.querySelectorAll('.visualizer-bar');
893
- const barCount = visualizerBars.length;
894
-
895
- function updateAudioLevel() {
896
- analyser.getByteFrequencyData(dataArray);
897
-
898
- for (let i = 0; i < barCount; i++) {
899
- const start = Math.floor(i * (bufferLength / barCount));
900
- const end = Math.floor((i + 1) * (bufferLength / barCount));
901
-
902
- let sum = 0;
903
- for (let j = start; j < end; j++) {
904
- sum += dataArray[j];
905
- }
906
-
907
- const average = sum / (end - start) / 255;
908
- const scaleY = 0.1 + average * 0.9;
909
- visualizerBars[i].style.transform = `scaleY(${scaleY})`;
910
- }
911
-
912
- animationFrame = requestAnimationFrame(updateAudioLevel);
913
- }
914
-
915
- updateAudioLevel();
916
- }
917
-
918
- async function setupWebRTC() {
919
- // 메모리가 로드되지 않았다면 먼저 로드
920
- if (Object.keys(userMemories).length === 0) {
921
- console.log('[WebRTC] No memories loaded, loading now...');
922
- await loadMemories();
923
- }
924
-
925
- const config = __RTC_CONFIGURATION__;
926
- peerConnection = new RTCPeerConnection(config);
927
- const timeoutId = setTimeout(() => {
928
- showToast("연결이 평소보다 오래 걸리고 있습니다. VPN을 사용 중이신가요?", 'warning');
929
- }, 5000);
930
-
931
- try {
932
- const stream = await navigator.mediaDevices.getUserMedia({
933
- audio: true
934
- });
935
- setupAudioVisualization(stream);
936
- stream.getTracks().forEach(track => {
937
- peerConnection.addTrack(track, stream);
938
- });
939
- peerConnection.addEventListener('track', (evt) => {
940
- if (audioOutput.srcObject !== evt.streams[0]) {
941
- audioOutput.srcObject = evt.streams[0];
942
- audioOutput.play();
943
- }
944
- });
945
-
946
- // Create data channel for text messages
947
- dataChannel = peerConnection.createDataChannel('text');
948
- dataChannel.onopen = () => {
949
- console.log('Data channel opened');
950
- };
951
- dataChannel.onmessage = (event) => {
952
- const eventJson = JSON.parse(event.data);
953
- if (eventJson.type === "error") {
954
- showError(eventJson.message);
955
- }
956
- };
957
-
958
- const offer = await peerConnection.createOffer();
959
- await peerConnection.setLocalDescription(offer);
960
- await new Promise((resolve) => {
961
- if (peerConnection.iceGatheringState === "complete") {
962
- resolve();
963
- } else {
964
- const checkState = () => {
965
- if (peerConnection.iceGatheringState === "complete") {
966
- peerConnection.removeEventListener("icegatheringstatechange", checkState);
967
- resolve();
968
- }
969
- };
970
- peerConnection.addEventListener("icegatheringstatechange", checkState);
971
- }
972
- });
973
-
974
- peerConnection.addEventListener('connectionstatechange', () => {
975
- console.log('connectionstatechange', peerConnection.connectionState);
976
- if (peerConnection.connectionState === 'connected') {
977
- clearTimeout(timeoutId);
978
- const toast = document.getElementById('error-toast');
979
- toast.style.display = 'none';
980
- }
981
- updateButtonState();
982
- });
983
-
984
- webrtc_id = Math.random().toString(36).substring(7);
985
-
986
- console.log('[WebRTC] Sending offer with memories:', userMemories);
987
- console.log('[WebRTC] Total memory items:', Object.values(userMemories).flat().length);
988
-
989
- const response = await fetch('/webrtc/offer', {
990
- method: 'POST',
991
- headers: { 'Content-Type': 'application/json' },
992
- body: JSON.stringify({
993
- sdp: peerConnection.localDescription.sdp,
994
- type: peerConnection.localDescription.type,
995
- webrtc_id: webrtc_id,
996
- web_search_enabled: webSearchEnabled,
997
- session_id: currentSessionId,
998
- user_name: userName,
999
- memories: userMemories
1000
- })
1001
- });
1002
-
1003
- const serverResponse = await response.json();
1004
- if (serverResponse.status === 'failed') {
1005
- showError(serverResponse.meta.error === 'concurrency_limit_reached'
1006
- ? `너무 많은 연결입니다. 최대 한도는 ${serverResponse.meta.limit} 입니다.`
1007
- : serverResponse.meta.error);
1008
- stop();
1009
- return;
1010
- }
1011
-
1012
- await peerConnection.setRemoteDescription(serverResponse);
1013
- const eventSource = new EventSource('/outputs?webrtc_id=' + webrtc_id);
1014
- eventSource.addEventListener("output", (event) => {
1015
- const eventJson = JSON.parse(event.data);
1016
- let content = eventJson.content;
1017
-
1018
- if (eventJson.detected_language) {
1019
- content += ` <span class="language-info">[${eventJson.detected_language}]</span>`;
1020
- }
1021
- addMessage("assistant", content);
1022
- });
1023
- eventSource.addEventListener("search", (event) => {
1024
- const eventJson = JSON.parse(event.data);
1025
- if (eventJson.query) {
1026
- addMessage("search-result", `웹 검색 중: "${eventJson.query}"`);
1027
- }
1028
- });
1029
- } catch (err) {
1030
- clearTimeout(timeoutId);
1031
- console.error('Error setting up WebRTC:', err);
1032
- showError('연결을 설정하지 못했습니다. 다시 시도해 주세요.');
1033
- stop();
1034
- }
1035
- }
1036
-
1037
- function addMessage(role, content, save = true) {
1038
- const messageDiv = document.createElement('div');
1039
- messageDiv.classList.add('message', role);
1040
-
1041
- if (content.includes('<span')) {
1042
- messageDiv.innerHTML = content;
1043
- } else {
1044
- messageDiv.textContent = content;
1045
- }
1046
- chatMessages.appendChild(messageDiv);
1047
- chatMessages.scrollTop = chatMessages.scrollHeight;
1048
-
1049
- // Save message to database if save flag is true
1050
- if (save && currentSessionId && role !== 'memory-update' && role !== 'search-result') {
1051
- fetch('/message/save', {
1052
- method: 'POST',
1053
- headers: { 'Content-Type': 'application/json' },
1054
- body: JSON.stringify({
1055
- session_id: currentSessionId,
1056
- role: role,
1057
- content: content
1058
- })
1059
- }).catch(error => console.error('Failed to save message:', error));
1060
- }
1061
- }
1062
-
1063
- function stop() {
1064
- console.log('[STOP] Stopping connection...');
1065
-
1066
- // Cancel animation frame first
1067
- if (animationFrame) {
1068
- cancelAnimationFrame(animationFrame);
1069
- animationFrame = null;
1070
- }
1071
-
1072
- // Close audio context
1073
- if (audioContext) {
1074
- audioContext.close();
1075
- audioContext = null;
1076
- analyser = null;
1077
- audioSource = null;
1078
- }
1079
-
1080
- // Close data channel
1081
- if (dataChannel) {
1082
- dataChannel.close();
1083
- dataChannel = null;
1084
- }
1085
-
1086
- // Close peer connection
1087
- if (peerConnection) {
1088
- console.log('[STOP] Current connection state:', peerConnection.connectionState);
1089
-
1090
- // Stop all transceivers
1091
- if (peerConnection.getTransceivers) {
1092
- peerConnection.getTransceivers().forEach(transceiver => {
1093
- if (transceiver.stop) {
1094
- transceiver.stop();
1095
- }
1096
- });
1097
- }
1098
-
1099
- // Stop all senders
1100
- if (peerConnection.getSenders) {
1101
- peerConnection.getSenders().forEach(sender => {
1102
- if (sender.track) {
1103
- sender.track.stop();
1104
- }
1105
- });
1106
- }
1107
-
1108
- // Stop all receivers
1109
- if (peerConnection.getReceivers) {
1110
- peerConnection.getReceivers().forEach(receiver => {
1111
- if (receiver.track) {
1112
- receiver.track.stop();
1113
- }
1114
- });
1115
- }
1116
-
1117
- // Close the connection
1118
- peerConnection.close();
1119
-
1120
- // Clear the reference
1121
- peerConnection = null;
1122
-
1123
- console.log('[STOP] Connection closed');
1124
- }
1125
-
1126
- // Reset audio level
1127
- audioLevel = 0;
1128
- isVoiceActive = false;
1129
-
1130
- // Update UI
1131
- updateButtonState();
1132
-
1133
- // Clear any existing webrtc_id
1134
- if (webrtc_id) {
1135
- console.log('[STOP] Clearing webrtc_id:', webrtc_id);
1136
- webrtc_id = null;
1137
- }
1138
- }
1139
-
1140
- startButton.addEventListener('click', async () => {
1141
- console.log('clicked');
1142
- console.log(peerConnection, peerConnection?.connectionState);
1143
-
1144
- // 메모리가 로드되지 않았다면 먼저 로드
1145
- if (Object.keys(userMemories).length === 0) {
1146
- console.log('[StartButton] Loading memories before starting...');
1147
- await loadMemories();
1148
- }
1149
-
1150
- if (!peerConnection || peerConnection.connectionState !== 'connected') {
1151
- setupWebRTC();
1152
- } else {
1153
- console.log('stopping');
1154
- stop();
1155
- }
1156
- });
1157
-
1158
- // Initialize on page load
1159
- window.addEventListener('DOMContentLoaded', () => {
1160
- sendButton.style.display = 'block';
1161
- endSessionButton.style.display = 'block';
1162
- startNewSession();
1163
- loadHistory();
1164
- loadMemories();
1165
- });
1166
- </script>
1167
- </body>
1168
-
1169
- </html>"""
1170
-
1171
-
1172
- class BraveSearchClient:
1173
- """Brave Search API client"""
1174
- def __init__(self, api_key: str):
1175
- self.api_key = api_key
1176
- self.base_url = "https://api.search.brave.com/res/v1/web/search"
1177
-
1178
- async def search(self, query: str, count: int = 10) -> List[Dict]:
1179
- """Perform a web search using Brave Search API"""
1180
- if not self.api_key:
1181
- return []
1182
-
1183
- headers = {
1184
- "Accept": "application/json",
1185
- "X-Subscription-Token": self.api_key
1186
- }
1187
- params = {
1188
- "q": query,
1189
- "count": count,
1190
- "lang": "ko"
1191
- }
1192
-
1193
- async with httpx.AsyncClient() as client:
1194
- try:
1195
- response = await client.get(self.base_url, headers=headers, params=params)
1196
- response.raise_for_status()
1197
- data = response.json()
1198
-
1199
- results = []
1200
- if "web" in data and "results" in data["web"]:
1201
- for result in data["web"]["results"][:count]:
1202
- results.append({
1203
- "title": result.get("title", ""),
1204
- "url": result.get("url", ""),
1205
- "description": result.get("description", "")
1206
- })
1207
- return results
1208
- except Exception as e:
1209
- print(f"Brave Search error: {e}")
1210
- return []
1211
-
1212
-
1213
- # Database helper class
1214
- class PersonalAssistantDB:
1215
- """Database manager for personal assistant"""
1216
-
1217
- @staticmethod
1218
- async def init():
1219
- """Initialize database tables"""
1220
- async with aiosqlite.connect(DB_PATH) as db:
1221
- # Conversations table
1222
- await db.execute("""
1223
- CREATE TABLE IF NOT EXISTS conversations (
1224
- id TEXT PRIMARY KEY,
1225
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1226
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1227
- summary TEXT
1228
- )
1229
- """)
1230
-
1231
- # Messages table
1232
- await db.execute("""
1233
- CREATE TABLE IF NOT EXISTS messages (
1234
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1235
- session_id TEXT NOT NULL,
1236
- role TEXT NOT NULL,
1237
- content TEXT NOT NULL,
1238
- detected_language TEXT,
1239
- timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1240
- FOREIGN KEY (session_id) REFERENCES conversations(id)
1241
- )
1242
- """)
1243
-
1244
- # User memories table - stores personal information
1245
- await db.execute("""
1246
- CREATE TABLE IF NOT EXISTS user_memories (
1247
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1248
- category TEXT NOT NULL,
1249
- content TEXT NOT NULL,
1250
- confidence REAL DEFAULT 1.0,
1251
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1252
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1253
- source_session_id TEXT,
1254
- FOREIGN KEY (source_session_id) REFERENCES conversations(id)
1255
- )
1256
- """)
1257
-
1258
- # Create indexes for better performance
1259
- await db.execute("CREATE INDEX IF NOT EXISTS idx_memories_category ON user_memories(category)")
1260
- await db.execute("CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id)")
1261
-
1262
- await db.commit()
1263
-
1264
- @staticmethod
1265
- async def create_session(session_id: str):
1266
- """Create a new conversation session"""
1267
- async with aiosqlite.connect(DB_PATH) as db:
1268
- await db.execute(
1269
- "INSERT INTO conversations (id) VALUES (?)",
1270
- (session_id,)
1271
- )
1272
- await db.commit()
1273
-
1274
- @staticmethod
1275
- async def save_message(session_id: str, role: str, content: str):
1276
- """Save a message to the database"""
1277
- # Check for None or empty content
1278
- if not content:
1279
- print(f"[SAVE_MESSAGE] Empty content for {role} message, skipping")
1280
- return
1281
-
1282
- # Detect language
1283
- detected_language = None
1284
- try:
1285
- if content and len(content) > 10:
1286
- detected_language = detect(content)
1287
- except (LangDetectException, Exception) as e:
1288
- print(f"Language detection error: {e}")
1289
-
1290
- async with aiosqlite.connect(DB_PATH) as db:
1291
- await db.execute(
1292
- """INSERT INTO messages (session_id, role, content, detected_language)
1293
- VALUES (?, ?, ?, ?)""",
1294
- (session_id, role, content, detected_language)
1295
- )
1296
-
1297
- # Update conversation's updated_at timestamp
1298
- await db.execute(
1299
- "UPDATE conversations SET updated_at = CURRENT_TIMESTAMP WHERE id = ?",
1300
- (session_id,)
1301
- )
1302
-
1303
- # Update conversation summary (use first user message as summary)
1304
- if role == "user":
1305
- cursor = await db.execute(
1306
- "SELECT summary FROM conversations WHERE id = ?",
1307
- (session_id,)
1308
- )
1309
- row = await cursor.fetchone()
1310
- if row and not row[0]:
1311
- summary = content[:100] + "..." if len(content) > 100 else content
1312
- await db.execute(
1313
- "UPDATE conversations SET summary = ? WHERE id = ?",
1314
- (summary, session_id)
1315
- )
1316
-
1317
- await db.commit()
1318
-
1319
- @staticmethod
1320
- async def get_recent_conversations(limit: int = 10):
1321
- """Get recent conversations"""
1322
- async with aiosqlite.connect(DB_PATH) as db:
1323
- cursor = await db.execute(
1324
- """SELECT id, created_at, summary
1325
- FROM conversations
1326
- ORDER BY updated_at DESC
1327
- LIMIT ?""",
1328
- (limit,)
1329
- )
1330
- rows = await cursor.fetchall()
1331
- return [
1332
- {
1333
- "id": row[0],
1334
- "created_at": row[1],
1335
- "summary": row[2] or "새 대화"
1336
- }
1337
- for row in rows
1338
- ]
1339
-
1340
- @staticmethod
1341
- async def get_conversation_messages(session_id: str):
1342
- """Get all messages for a conversation"""
1343
- async with aiosqlite.connect(DB_PATH) as db:
1344
- cursor = await db.execute(
1345
- """SELECT role, content, detected_language, timestamp
1346
- FROM messages
1347
- WHERE session_id = ?
1348
- ORDER BY timestamp ASC""",
1349
- (session_id,)
1350
- )
1351
- rows = await cursor.fetchall()
1352
- return [
1353
- {
1354
- "role": row[0],
1355
- "content": row[1],
1356
- "detected_language": row[2],
1357
- "timestamp": row[3]
1358
- }
1359
- for row in rows
1360
- ]
1361
-
1362
- @staticmethod
1363
- async def save_memory(category: str, content: str, session_id: str = None, confidence: float = 1.0):
1364
- """Save or update a user memory"""
1365
- async with aiosqlite.connect(DB_PATH) as db:
1366
- # Check if similar memory exists
1367
- cursor = await db.execute(
1368
- """SELECT id, content FROM user_memories
1369
- WHERE category = ? AND content LIKE ?
1370
- LIMIT 1""",
1371
- (category, f"%{content[:20]}%")
1372
- )
1373
- existing = await cursor.fetchone()
1374
-
1375
- if existing:
1376
- # Update existing memory
1377
- await db.execute(
1378
- """UPDATE user_memories
1379
- SET content = ?, confidence = ?, updated_at = CURRENT_TIMESTAMP,
1380
- source_session_id = ?
1381
- WHERE id = ?""",
1382
- (content, confidence, session_id, existing[0])
1383
- )
1384
- else:
1385
- # Insert new memory
1386
- await db.execute(
1387
- """INSERT INTO user_memories (category, content, confidence, source_session_id)
1388
- VALUES (?, ?, ?, ?)""",
1389
- (category, content, confidence, session_id)
1390
- )
1391
-
1392
- await db.commit()
1393
-
1394
- @staticmethod
1395
- async def get_all_memories():
1396
- """Get all user memories"""
1397
- async with aiosqlite.connect(DB_PATH) as db:
1398
- cursor = await db.execute(
1399
- """SELECT category, content, confidence, updated_at
1400
- FROM user_memories
1401
- ORDER BY category, updated_at DESC"""
1402
- )
1403
- rows = await cursor.fetchall()
1404
- return [
1405
- {
1406
- "category": row[0],
1407
- "content": row[1],
1408
- "confidence": row[2],
1409
- "updated_at": row[3]
1410
- }
1411
- for row in rows
1412
- ]
1413
-
1414
- @staticmethod
1415
- async def extract_and_save_memories(session_id: str):
1416
- """Extract memories from conversation and save them"""
1417
- # Get all messages from the session
1418
- messages = await PersonalAssistantDB.get_conversation_messages(session_id)
1419
-
1420
- if not messages:
1421
- return
1422
-
1423
- # Prepare conversation text for analysis
1424
- conversation_text = "\n".join([
1425
- f"{msg['role']}: {msg['content']}"
1426
- for msg in messages if msg.get('content')
1427
- ])
1428
-
1429
- # Use GPT to extract memories
1430
- client = openai.AsyncOpenAI()
1431
-
1432
- try:
1433
- response = await client.chat.completions.create(
1434
- model="gpt-4.1-mini",
1435
- messages=[
1436
- {
1437
- "role": "system",
1438
- "content": """You are a memory extraction system. Extract personal information from conversations.
1439
-
1440
- Categories to extract:
1441
- - personal_info: 이름, 나이, 성별, 직업, 거주지
1442
- - preferences: 좋아하는 것, 싫어하는 것, 취향
1443
- - important_dates: 생일, 기념일, 중요한 날짜
1444
- - relationships: 가족, 친구, 동료 관계
1445
- - hobbies: 취미, 관심사
1446
- - health: 건강 상태, 알레르기, 의료 정보
1447
- - goals: 목표, 계획, 꿈
1448
- - routines: 일상, 습관, 루틴
1449
- - work: 직장, 업무, 프로젝트
1450
- - education: 학력, 전공, 학습
1451
-
1452
- Return as JSON array with format:
1453
- [
1454
- {
1455
- "category": "category_name",
1456
- "content": "extracted information in Korean",
1457
- "confidence": 0.0-1.0
1458
- }
1459
- ]
1460
-
1461
- Only extract clear, factual information. Do not make assumptions."""
1462
- },
1463
- {
1464
- "role": "user",
1465
- "content": f"Extract memories from this conversation:\n\n{conversation_text}"
1466
- }
1467
- ],
1468
- temperature=0.3,
1469
- max_tokens=2000
1470
- )
1471
-
1472
- # Parse and save memories
1473
- memories_text = response.choices[0].message.content
1474
-
1475
- # Extract JSON from response
1476
- import re
1477
- json_match = re.search(r'\[.*\]', memories_text, re.DOTALL)
1478
- if json_match:
1479
- memories = json.loads(json_match.group())
1480
-
1481
- for memory in memories:
1482
- if memory.get('content') and len(memory['content']) > 5:
1483
- await PersonalAssistantDB.save_memory(
1484
- category=memory.get('category', 'general'),
1485
- content=memory['content'],
1486
- session_id=session_id,
1487
- confidence=memory.get('confidence', 0.8)
1488
- )
1489
-
1490
- print(f"Extracted and saved {len(memories)} memories from session {session_id}")
1491
-
1492
- except Exception as e:
1493
- print(f"Error extracting memories: {e}")
1494
-
1495
-
1496
- # Initialize search client globally
1497
- brave_api_key = os.getenv("BSEARCH_API")
1498
- search_client = BraveSearchClient(brave_api_key) if brave_api_key else None
1499
- print(f"Search client initialized: {search_client is not None}, API key present: {bool(brave_api_key)}")
1500
-
1501
- # Store connection settings
1502
- connection_settings = {}
1503
-
1504
- # Initialize OpenAI client for text chat
1505
- client = openai.AsyncOpenAI()
1506
-
1507
-
1508
- def update_chatbot(chatbot: list[dict], response: ResponseAudioTranscriptDoneEvent):
1509
- chatbot.append({"role": "assistant", "content": response.transcript})
1510
- return chatbot
1511
-
1512
- def format_memories_for_prompt(memories: Dict[str, List[str]]) -> str:
1513
- """Format memories for inclusion in system prompt"""
1514
- if not memories:
1515
- return ""
1516
-
1517
- memory_text = "\n\n=== 기억된 정보 ===\n"
1518
- memory_count = 0
1519
-
1520
- for category, items in memories.items():
1521
- if items and isinstance(items, list):
1522
- valid_items = [item for item in items if item] # None이나 빈 문자열 제외
1523
- if valid_items:
1524
- memory_text += f"\n[{category}]\n"
1525
- for item in valid_items:
1526
- memory_text += f"- {item}\n"
1527
- memory_count += 1
1528
-
1529
- print(f"[FORMAT_MEMORIES] Formatted {memory_count} memory items")
1530
- return memory_text if memory_count > 0 else ""
1531
-
1532
-
1533
-
1534
- async def process_text_chat(message: str, web_search_enabled: bool, session_id: str,
1535
- user_name: str = "", memories: Dict = None) -> Dict[str, str]:
1536
- """Process text chat using GPT-4o-mini model"""
1537
  try:
1538
- # Check for empty or None message
1539
- if not message:
1540
- return {"error": "메시지가 비어있습니다."}
1541
-
1542
- # Check for stop words
1543
- stop_words = ["중단", "그만", "스톱", "stop", "닥쳐", "멈춰", "중지"]
1544
- if any(word in message.lower() for word in stop_words):
1545
- return {
1546
- "response": "대화를 중단합니다.",
1547
- "detected_language": "ko"
1548
- }
1549
-
1550
- # Build system prompt with memories
1551
- base_prompt = f"""You are a personal AI assistant for {user_name if user_name else 'the user'}.
1552
- You remember all previous conversations and personal information about the user.
1553
- Be friendly, helpful, and personalized in your responses.
1554
- Always use the information you remember to make conversations more personal and relevant.
1555
- IMPORTANT: Give only ONE response. Do not repeat or give multiple answers."""
1556
 
1557
- # Add memories to prompt
1558
- if memories:
1559
- memory_text = format_memories_for_prompt(memories)
1560
- base_prompt += memory_text
1561
-
1562
- messages = [{"role": "system", "content": base_prompt}]
1563
-
1564
- # Handle web search if enabled
1565
- if web_search_enabled and search_client and message:
1566
- search_keywords = ["날씨", "기온", "비", "눈", "뉴스", "소식", "현재", "최근",
1567
- "오늘", "지금", "가격", "환율", "주가", "weather", "news",
1568
- "current", "today", "price", "2024", "2025"]
1569
-
1570
- should_search = any(keyword in message.lower() for keyword in search_keywords)
1571
-
1572
- if should_search:
1573
- search_results = await search_client.search(message)
1574
- if search_results:
1575
- search_context = "웹 검색 결과:\n\n"
1576
- for i, result in enumerate(search_results[:5], 1):
1577
- search_context += f"{i}. {result['title']}\n{result['description']}\n\n"
1578
-
1579
- messages.append({
1580
- "role": "system",
1581
- "content": "다음 웹 검색 결과를 참고하여 답변하세요:\n\n" + search_context
1582
- })
1583
-
1584
- messages.append({"role": "user", "content": message})
1585
 
1586
- # Call GPT-4o-mini
1587
- response = await client.chat.completions.create(
1588
- model="gpt-4.1-mini",
1589
- messages=messages,
1590
- temperature=0.7,
1591
- max_tokens=2000
1592
- )
1593
 
1594
- response_text = response.choices[0].message.content
 
1595
 
1596
- # Detect language
1597
- detected_language = None
1598
  try:
1599
- if response_text and len(response_text) > 10:
1600
- detected_language = detect(response_text)
1601
  except:
1602
  pass
1603
-
1604
- # Save messages to database
1605
- if session_id:
1606
- await PersonalAssistantDB.save_message(session_id, "user", message)
1607
- await PersonalAssistantDB.save_message(session_id, "assistant", response_text)
1608
-
1609
- return {
1610
- "response": response_text,
1611
- "detected_language": detected_language
1612
- }
1613
-
1614
- except Exception as e:
1615
- print(f"Error in text chat: {e}")
1616
- return {"error": str(e)}
1617
-
1618
-
1619
- class OpenAIHandler(AsyncStreamHandler):
1620
- def __init__(self, web_search_enabled: bool = False, webrtc_id: str = None,
1621
- session_id: str = None, user_name: str = "", memories: Dict = None) -> None:
1622
- super().__init__(
1623
- expected_layout="mono",
1624
- output_sample_rate=SAMPLE_RATE,
1625
- output_frame_size=480,
1626
- input_sample_rate=SAMPLE_RATE,
1627
- )
1628
- self.connection = None
1629
- self.output_queue = asyncio.Queue()
1630
- self.search_client = search_client
1631
- self.function_call_in_progress = False
1632
- self.current_function_args = ""
1633
- self.current_call_id = None
1634
- self.webrtc_id = webrtc_id
1635
- self.web_search_enabled = web_search_enabled
1636
- self.session_id = session_id
1637
- self.user_name = user_name
1638
- self.memories = memories or {}
1639
- self.is_responding = False
1640
- self.should_stop = False
1641
-
1642
- # 메모리 정보 로깅
1643
- memory_count = sum(len(items) for items in self.memories.values() if isinstance(items, list))
1644
- print(f"[INIT] Handler created with:")
1645
- print(f" - web_search={web_search_enabled}")
1646
- print(f" - session_id={session_id}")
1647
- print(f" - user={user_name}")
1648
- print(f" - memory categories={list(self.memories.keys())}")
1649
- print(f" - total memory items={memory_count}")
1650
-
1651
- def copy(self):
1652
- # 가장 최근의 connection settings 가져오기
1653
- if connection_settings:
1654
- recent_ids = sorted(connection_settings.keys(),
1655
- key=lambda k: connection_settings[k].get('timestamp', 0),
1656
- reverse=True)
1657
- if recent_ids:
1658
- recent_id = recent_ids[0]
1659
- settings = connection_settings[recent_id]
1660
-
1661
- print(f"[COPY] Copying settings from {recent_id}:")
1662
- print(f"[COPY] - web_search: {settings.get('web_search_enabled', False)}")
1663
- print(f"[COPY] - session_id: {settings.get('session_id')}")
1664
- print(f"[COPY] - user_name: {settings.get('user_name', '')}")
1665
-
1666
- memories = settings.get('memories', {})
1667
-
1668
- # 메모리가 없으면 DB에서 직접 로드 (동기적으로)
1669
- if not memories:
1670
- print(f"[COPY] No memories in settings, loading from DB...")
1671
- import asyncio
1672
- try:
1673
- # 현재 이벤트 루프가 있는지 확인
1674
- loop = asyncio.get_event_loop()
1675
- if loop.is_running():
1676
- # 이미 실행 중인 루프가 있으면 run_in_executor 사용
1677
- import concurrent.futures
1678
- with concurrent.futures.ThreadPoolExecutor() as executor:
1679
- future = executor.submit(self._load_memories_sync)
1680
- memories_list = future.result()
1681
- else:
1682
- # 새 루프에서 실행
1683
- memories_list = loop.run_until_complete(PersonalAssistantDB.get_all_memories())
1684
- except:
1685
- # 새 루프 생성
1686
- new_loop = asyncio.new_event_loop()
1687
- asyncio.set_event_loop(new_loop)
1688
- memories_list = new_loop.run_until_complete(PersonalAssistantDB.get_all_memories())
1689
- new_loop.close()
1690
-
1691
- # 메모리를 카테고리별로 그룹화
1692
- for memory in memories_list:
1693
- category = memory['category']
1694
- if category not in memories:
1695
- memories[category] = []
1696
- memories[category].append(memory['content'])
1697
-
1698
- print(f"[COPY] Loaded {len(memories_list)} memories from DB")
1699
-
1700
- print(f"[COPY] - memories count: {sum(len(items) for items in memories.values() if isinstance(items, list))}")
1701
-
1702
- return OpenAIHandler(
1703
- web_search_enabled=settings.get('web_search_enabled', False),
1704
- webrtc_id=recent_id,
1705
- session_id=settings.get('session_id'),
1706
- user_name=settings.get('user_name', ''),
1707
- memories=memories
1708
- )
1709
-
1710
- print(f"[COPY] No settings found, creating default handler")
1711
- return OpenAIHandler(web_search_enabled=False)
1712
-
1713
- def _load_memories_sync(self):
1714
- """동기적으로 메모리 로드 (Thread에서 실행용)"""
1715
- loop = asyncio.new_event_loop()
1716
- asyncio.set_event_loop(loop)
1717
- result = loop.run_until_complete(PersonalAssistantDB.get_all_memories())
1718
- loop.close()
1719
- return result
1720
-
1721
- async def search_web(self, query: str) -> str:
1722
- """Perform web search and return formatted results"""
1723
- if not self.search_client or not self.web_search_enabled:
1724
- return "웹 검색이 비활성화되어 있습니다."
1725
-
1726
- print(f"Searching web for: {query}")
1727
- results = await self.search_client.search(query)
1728
- if not results:
1729
- return f"'{query}'에 대한 검색 결과를 찾을 수 없습니다."
1730
-
1731
- formatted_results = []
1732
- for i, result in enumerate(results, 1):
1733
- formatted_results.append(
1734
- f"{i}. {result['title']}\n"
1735
- f" URL: {result['url']}\n"
1736
- f" {result['description']}\n"
1737
- )
1738
-
1739
- return f"웹 검색 결과 '{query}':\n\n" + "\n".join(formatted_results)
1740
-
1741
- async def process_text_message(self, message: str):
1742
- """Process text message from user"""
1743
- if self.connection:
1744
- await self.connection.conversation.item.create(
1745
- item={
1746
- "type": "message",
1747
- "role": "user",
1748
- "content": [{"type": "input_text", "text": message}]
1749
- }
1750
- )
1751
- await self.connection.response.create()
1752
-
1753
- async def start_up(self):
1754
- """Connect to realtime API"""
1755
- if connection_settings and self.webrtc_id:
1756
- if self.webrtc_id in connection_settings:
1757
- settings = connection_settings[self.webrtc_id]
1758
- self.web_search_enabled = settings.get('web_search_enabled', False)
1759
- self.session_id = settings.get('session_id')
1760
- self.user_name = settings.get('user_name', '')
1761
- self.memories = settings.get('memories', {})
1762
-
1763
- print(f"[START_UP] Updated settings from storage for {self.webrtc_id}")
1764
-
1765
- # 메모리가 비어있고 session_id가 있으면 DB에서 로드
1766
- if not self.memories:
1767
- print(f"[START_UP] No memories found, loading from DB...")
1768
- memories_list = await PersonalAssistantDB.get_all_memories()
1769
-
1770
- # 메모리를 카테고리별로 그룹화
1771
- self.memories = {}
1772
- for memory in memories_list:
1773
- category = memory['category']
1774
- if category not in self.memories:
1775
- self.memories[category] = []
1776
- self.memories[category].append(memory['content'])
1777
-
1778
- print(f"[START_UP] Loaded {len(memories_list)} memories from DB")
1779
-
1780
- print(f"[START_UP] Final memory count: {sum(len(items) for items in self.memories.values() if isinstance(items, list))}")
1781
-
1782
- self.client = openai.AsyncOpenAI()
1783
-
1784
- print(f"[REALTIME API] Connecting...")
1785
-
1786
- # Build system prompt with memories
1787
- base_instructions = f"""You are a personal AI assistant for {self.user_name if self.user_name else 'the user'}.
1788
- You remember all previous conversations and personal information about the user.
1789
- Be friendly, helpful, and personalized in your responses.
1790
- Always use the information you remember to make conversations more personal and relevant.
1791
- IMPORTANT: Give only ONE response per user input. Do not repeat yourself or give multiple answers."""
1792
-
1793
- # Add memories to prompt
1794
- if self.memories:
1795
- memory_text = format_memories_for_prompt(self.memories)
1796
- base_instructions += memory_text
1797
- print(f"[START_UP] Added memories to system prompt: {len(memory_text)} characters")
1798
- else:
1799
- print(f"[START_UP] No memories to add to system prompt")
1800
-
1801
- # Define the web search function
1802
- tools = []
1803
- if self.web_search_enabled and self.search_client:
1804
- tools = [{
1805
- "type": "function",
1806
- "function": {
1807
- "name": "web_search",
1808
- "description": "Search the web for current information. Use this for weather, news, prices, current events, or any time-sensitive topics.",
1809
- "parameters": {
1810
- "type": "object",
1811
- "properties": {
1812
- "query": {
1813
- "type": "string",
1814
- "description": "The search query"
1815
- }
1816
- },
1817
- "required": ["query"]
1818
- }
1819
- }
1820
- }]
1821
 
1822
- search_instructions = (
1823
- "\n\nYou have web search capabilities. "
1824
- "Use web_search for current information like weather, news, prices, etc."
1825
- )
1826
-
1827
- instructions = base_instructions + search_instructions
1828
- else:
1829
- instructions = base_instructions
1830
-
1831
- async with self.client.beta.realtime.connect(
1832
- model="gpt-4o-mini-realtime-preview-2024-12-17"
1833
- ) as conn:
1834
- session_update = {
1835
- "turn_detection": {
1836
- "type": "server_vad",
1837
- "threshold": 0.5,
1838
- "prefix_padding_ms": 300,
1839
- "silence_duration_ms": 200
1840
- },
1841
- "instructions": instructions,
1842
- "tools": tools,
1843
- "tool_choice": "auto" if tools else "none",
1844
- "temperature": 0.7,
1845
- "max_response_output_tokens": 4096,
1846
- "modalities": ["text", "audio"],
1847
- "voice": "alloy"
1848
- }
1849
-
1850
- try:
1851
- await conn.session.update(session=session_update)
1852
- self.connection = conn
1853
- print(f"Connected with tools: {len(tools)} functions")
1854
- print(f"Session update successful")
1855
- except Exception as e:
1856
- print(f"Error updating session: {e}")
1857
- raise
1858
-
1859
- async for event in self.connection:
1860
- # Debug log for all events
1861
- if hasattr(event, 'type'):
1862
- if event.type not in ["response.audio.delta", "response.audio.done"]:
1863
- print(f"[EVENT] Type: {event.type}")
1864
-
1865
- # Handle user input audio transcription
1866
- if event.type == "conversation.item.input_audio_transcription.completed":
1867
- if hasattr(event, 'transcript') and event.transcript:
1868
- user_text = event.transcript.lower()
1869
- stop_words = ["중단", "그만", "스톱", "stop", "닥쳐", "멈춰", "중지"]
1870
-
1871
- if any(word in user_text for word in stop_words):
1872
- print(f"[STOP DETECTED] User said: {event.transcript}")
1873
- self.should_stop = True
1874
- if self.connection:
1875
- try:
1876
- await self.connection.response.cancel()
1877
- except:
1878
- pass
1879
- continue
1880
-
1881
- # Save user message to database
1882
- if self.session_id:
1883
- await PersonalAssistantDB.save_message(self.session_id, "user", event.transcript)
1884
-
1885
- # Handle user transcription for stop detection (alternative event)
1886
- elif event.type == "conversation.item.created":
1887
- if hasattr(event, 'item') and hasattr(event.item, 'role') and event.item.role == "user":
1888
- if hasattr(event.item, 'content') and event.item.content:
1889
- for content_item in event.item.content:
1890
- if hasattr(content_item, 'transcript') and content_item.transcript:
1891
- user_text = content_item.transcript.lower()
1892
- stop_words = ["중단", "그만", "스톱", "stop", "닥쳐", "멈춰", "중지"]
1893
-
1894
- if any(word in user_text for word in stop_words):
1895
- print(f"[STOP DETECTED] User said: {content_item.transcript}")
1896
- self.should_stop = True
1897
- if self.connection:
1898
- try:
1899
- await self.connection.response.cancel()
1900
- except:
1901
- pass
1902
- continue
1903
-
1904
- # Save user message to database
1905
- if self.session_id:
1906
- await PersonalAssistantDB.save_message(self.session_id, "user", content_item.transcript)
1907
-
1908
- elif event.type == "response.audio_transcript.done":
1909
- # Prevent multiple responses
1910
- if self.is_responding:
1911
- print("[DUPLICATE RESPONSE] Skipping duplicate response")
1912
- continue
1913
-
1914
- self.is_responding = True
1915
- print(f"[RESPONSE] Transcript: {event.transcript[:100] if event.transcript else 'None'}...")
1916
-
1917
- # Detect language
1918
- detected_language = None
1919
- try:
1920
- if event.transcript and len(event.transcript) > 10:
1921
- detected_language = detect(event.transcript)
1922
- except Exception as e:
1923
- print(f"Language detection error: {e}")
1924
-
1925
- # Save to database
1926
- if self.session_id and event.transcript:
1927
- await PersonalAssistantDB.save_message(self.session_id, "assistant", event.transcript)
1928
-
1929
- output_data = {
1930
- "event": event,
1931
- "detected_language": detected_language
1932
- }
1933
- await self.output_queue.put(AdditionalOutputs(output_data))
1934
-
1935
- elif event.type == "response.done":
1936
- # Reset responding flag when response is complete
1937
- self.is_responding = False
1938
- self.should_stop = False
1939
- print("[RESPONSE DONE] Response completed")
1940
-
1941
- elif event.type == "response.audio.delta":
1942
- # Check if we should stop
1943
- if self.should_stop:
1944
- continue
1945
-
1946
- if hasattr(event, 'delta'):
1947
- await self.output_queue.put(
1948
- (
1949
- self.output_sample_rate,
1950
- np.frombuffer(
1951
- base64.b64decode(event.delta), dtype=np.int16
1952
- ).reshape(1, -1),
1953
- ),
1954
- )
1955
-
1956
- # Handle errors
1957
- elif event.type == "error":
1958
- print(f"[ERROR] {event}")
1959
- self.is_responding = False
1960
-
1961
- # Handle function calls
1962
- elif event.type == "response.function_call_arguments.start":
1963
- print(f"Function call started")
1964
- self.function_call_in_progress = True
1965
- self.current_function_args = ""
1966
- self.current_call_id = getattr(event, 'call_id', None)
1967
-
1968
- elif event.type == "response.function_call_arguments.delta":
1969
- if self.function_call_in_progress:
1970
- self.current_function_args += event.delta
1971
-
1972
- elif event.type == "response.function_call_arguments.done":
1973
- if self.function_call_in_progress:
1974
- print(f"Function call done, args: {self.current_function_args}")
1975
- try:
1976
- args = json.loads(self.current_function_args)
1977
- query = args.get("query", "")
1978
-
1979
- # Emit search event to client
1980
- await self.output_queue.put(AdditionalOutputs({
1981
- "type": "search",
1982
- "query": query
1983
- }))
1984
-
1985
- # Perform the search
1986
- search_results = await self.search_web(query)
1987
- print(f"Search results length: {len(search_results)}")
1988
-
1989
- # Send function result back to the model
1990
- if self.connection and self.current_call_id:
1991
- await self.connection.conversation.item.create(
1992
- item={
1993
- "type": "function_call_output",
1994
- "call_id": self.current_call_id,
1995
- "output": search_results
1996
- }
1997
- )
1998
- await self.connection.response.create()
1999
-
2000
- except Exception as e:
2001
- print(f"Function call error: {e}")
2002
- finally:
2003
- self.function_call_in_progress = False
2004
- self.current_function_args = ""
2005
- self.current_call_id = None
2006
-
2007
- async def receive(self, frame: tuple[int, np.ndarray]) -> None:
2008
- if not self.connection:
2009
- print(f"[RECEIVE] No connection, skipping")
2010
- return
2011
- try:
2012
- if frame is None or len(frame) < 2:
2013
- print(f"[RECEIVE] Invalid frame")
2014
- return
2015
-
2016
- _, array = frame
2017
- if array is None:
2018
- print(f"[RECEIVE] Null array")
2019
- return
2020
-
2021
- array = array.squeeze()
2022
- audio_message = base64.b64encode(array.tobytes()).decode("utf-8")
2023
- await self.connection.input_audio_buffer.append(audio=audio_message)
2024
- except Exception as e:
2025
- print(f"Error in receive: {e}")
2026
-
2027
- async def emit(self) -> tuple[int, np.ndarray] | AdditionalOutputs | None:
2028
- item = await wait_for_item(self.output_queue)
2029
-
2030
- if isinstance(item, dict) and item.get('type') == 'text_message':
2031
- await self.process_text_message(item['content'])
2032
- return None
2033
-
2034
- return item
2035
-
2036
- async def shutdown(self) -> None:
2037
- print(f"[SHUTDOWN] Called")
2038
-
2039
- if self.connection:
2040
- await self.connection.close()
2041
- self.connection = None
2042
- print("[REALTIME API] Connection closed")
2043
-
2044
-
2045
- # Create initial handler instance
2046
- handler = OpenAIHandler(web_search_enabled=False)
2047
-
2048
- # Create components
2049
- chatbot = gr.Chatbot(type="messages")
2050
-
2051
- # Create stream with handler instance
2052
- stream = Stream(
2053
- handler,
2054
- mode="send-receive",
2055
- modality="audio",
2056
- additional_inputs=[chatbot],
2057
- additional_outputs=[chatbot],
2058
- additional_outputs_handler=update_chatbot,
2059
- rtc_configuration=get_twilio_turn_credentials() if get_space() else None,
2060
- concurrency_limit=5 if get_space() else None,
2061
- time_limit=300 if get_space() else None,
2062
- )
2063
-
2064
-
2065
-
2066
- app = FastAPI()
2067
-
2068
- # Mount stream
2069
- stream.mount(app)
2070
-
2071
- # Initialize database on startup
2072
- @app.on_event("startup")
2073
- async def startup_event():
2074
- try:
2075
- await PersonalAssistantDB.init()
2076
- print(f"Database initialized at: {DB_PATH}")
2077
- print(f"Persistent directory: {PERSISTENT_DIR}")
2078
- print(f"DB file exists: {os.path.exists(DB_PATH)}")
2079
-
2080
- # Check if we're in Hugging Face Space
2081
- if os.path.exists("/data"):
2082
- print("Running in Hugging Face Space with persistent storage")
2083
- # List files in persistent directory
2084
- try:
2085
- files = os.listdir(PERSISTENT_DIR)
2086
- print(f"Files in persistent directory: {files}")
2087
- except Exception as e:
2088
- print(f"Error listing files: {e}")
2089
- except Exception as e:
2090
- print(f"Error during startup: {e}")
2091
- # Try to create directory if it doesn't exist
2092
- os.makedirs(PERSISTENT_DIR, exist_ok=True)
2093
- await PersonalAssistantDB.init()
2094
-
2095
- # Intercept offer to capture settings
2096
- @app.post("/webrtc/offer", include_in_schema=False)
2097
- async def custom_offer(request: Request):
2098
- """Intercept offer to capture settings"""
2099
- body = await request.json()
2100
-
2101
- webrtc_id = body.get("webrtc_id")
2102
- web_search_enabled = body.get("web_search_enabled", False)
2103
- session_id = body.get("session_id")
2104
- user_name = body.get("user_name", "")
2105
- memories = body.get("memories", {})
2106
-
2107
- print(f"[OFFER] Received offer with webrtc_id: {webrtc_id}")
2108
- print(f"[OFFER] web_search_enabled: {web_search_enabled}")
2109
- print(f"[OFFER] session_id: {session_id}")
2110
- print(f"[OFFER] user_name: {user_name}")
2111
- print(f"[OFFER] memories categories: {list(memories.keys())}")
2112
- print(f"[OFFER] memories total items: {sum(len(items) for items in memories.values() if isinstance(items, list))}")
2113
-
2114
- # 메모리가 비어있으면 DB에서 로드
2115
- if not memories and session_id:
2116
- print(f"[OFFER] No memories received, loading from DB...")
2117
- memories_list = await PersonalAssistantDB.get_all_memories()
2118
-
2119
- # 메모리를 카테고리별로 그룹화
2120
- memories = {}
2121
- for memory in memories_list:
2122
- category = memory['category']
2123
- if category not in memories:
2124
- memories[category] = []
2125
- memories[category].append(memory['content'])
2126
-
2127
- print(f"[OFFER] Loaded {len(memories_list)} memories from DB")
2128
-
2129
- # Store settings with timestamp
2130
- if webrtc_id:
2131
- connection_settings[webrtc_id] = {
2132
- 'web_search_enabled': web_search_enabled,
2133
- 'session_id': session_id,
2134
- 'user_name': user_name,
2135
- 'memories': memories, # DB에서 로드한 메모리 저장
2136
- 'timestamp': asyncio.get_event_loop().time()
2137
- }
2138
-
2139
- print(f"[OFFER] Stored settings for {webrtc_id} with {sum(len(items) for items in memories.values() if isinstance(items, list))} memory items")
2140
-
2141
- # Remove our custom route temporarily
2142
- custom_route = None
2143
- for i, route in enumerate(app.routes):
2144
- if hasattr(route, 'path') and route.path == "/webrtc/offer" and route.endpoint == custom_offer:
2145
- custom_route = app.routes.pop(i)
2146
- break
2147
-
2148
- # Forward to stream's offer handler
2149
- print(f"[OFFER] Forwarding to stream.offer()")
2150
- response = await stream.offer(body)
2151
-
2152
- # Re-add our custom route
2153
- if custom_route:
2154
- app.routes.insert(0, custom_route)
2155
-
2156
- print(f"[OFFER] Response status: {response.get('status', 'unknown') if isinstance(response, dict) else 'OK'}")
2157
-
2158
- return response
2159
-
2160
-
2161
- @app.post("/session/new")
2162
- async def create_new_session():
2163
- """Create a new chat session"""
2164
- session_id = str(uuid.uuid4())
2165
- await PersonalAssistantDB.create_session(session_id)
2166
- return {"session_id": session_id}
2167
-
2168
-
2169
- @app.post("/session/end")
2170
- async def end_session(request: Request):
2171
- """End session and extract memories"""
2172
- body = await request.json()
2173
- session_id = body.get("session_id")
2174
-
2175
- if not session_id:
2176
- return {"error": "session_id required"}
2177
-
2178
- # Extract and save memories from the conversation
2179
- await PersonalAssistantDB.extract_and_save_memories(session_id)
2180
-
2181
- return {"status": "ok"}
2182
-
2183
-
2184
- @app.post("/message/save")
2185
- async def save_message(request: Request):
2186
- """Save a message to the database"""
2187
- body = await request.json()
2188
- session_id = body.get("session_id")
2189
- role = body.get("role")
2190
- content = body.get("content")
2191
-
2192
- if not all([session_id, role, content]):
2193
- return {"error": "Missing required fields"}
2194
-
2195
- await PersonalAssistantDB.save_message(session_id, role, content)
2196
- return {"status": "ok"}
2197
-
2198
-
2199
- @app.get("/history/recent")
2200
- async def get_recent_history():
2201
- """Get recent conversation history"""
2202
- conversations = await PersonalAssistantDB.get_recent_conversations()
2203
- return conversations
2204
-
2205
-
2206
- @app.get("/history/{session_id}")
2207
- async def get_conversation(session_id: str):
2208
- """Get messages for a specific conversation"""
2209
- messages = await PersonalAssistantDB.get_conversation_messages(session_id)
2210
- return messages
2211
-
2212
-
2213
- @app.get("/memory/all")
2214
- async def get_all_memories():
2215
- """Get all user memories"""
2216
- memories = await PersonalAssistantDB.get_all_memories()
2217
- return memories
2218
-
2219
-
2220
- @app.post("/chat/text")
2221
- async def chat_text(request: Request):
2222
- """Handle text chat messages using GPT-4o-mini"""
2223
- try:
2224
- body = await request.json()
2225
- message = body.get("message", "")
2226
- web_search_enabled = body.get("web_search_enabled", False)
2227
- session_id = body.get("session_id")
2228
- user_name = body.get("user_name", "")
2229
- memories = body.get("memories", {})
2230
-
2231
- if not message:
2232
- return {"error": "메시지가 비어있습니다."}
2233
-
2234
- # Process text chat
2235
- result = await process_text_chat(message, web_search_enabled, session_id, user_name, memories)
2236
-
2237
- return result
2238
-
2239
  except Exception as e:
2240
- print(f"Error in chat_text endpoint: {e}")
2241
- return {"error": "채팅 처리 중 오류가 발생했습니다."}
2242
-
2243
-
2244
- @app.post("/text_message/{webrtc_id}")
2245
- async def receive_text_message(webrtc_id: str, request: Request):
2246
- """Receive text message from client"""
2247
- body = await request.json()
2248
- message = body.get("content", "")
2249
-
2250
- # Find the handler for this connection
2251
- if webrtc_id in stream.handlers:
2252
- handler = stream.handlers[webrtc_id]
2253
- # Queue the text message for processing
2254
- await handler.output_queue.put({
2255
- 'type': 'text_message',
2256
- 'content': message
2257
- })
2258
-
2259
- return {"status": "ok"}
2260
-
2261
-
2262
- @app.get("/outputs")
2263
- async def outputs(webrtc_id: str):
2264
- """Stream outputs including search events"""
2265
- async def output_stream():
2266
- async for output in stream.output_stream(webrtc_id):
2267
- if hasattr(output, 'args') and output.args:
2268
- # Check if it's a search event
2269
- if isinstance(output.args[0], dict) and output.args[0].get('type') == 'search':
2270
- yield f"event: search\ndata: {json.dumps(output.args[0])}\n\n"
2271
- # Regular transcript event with language info
2272
- elif isinstance(output.args[0], dict) and 'event' in output.args[0]:
2273
- event_data = output.args[0]
2274
- if 'event' in event_data and hasattr(event_data['event'], 'transcript'):
2275
- data = {
2276
- "role": "assistant",
2277
- "content": event_data['event'].transcript,
2278
- "detected_language": event_data.get('detected_language')
2279
- }
2280
- yield f"event: output\ndata: {json.dumps(data)}\n\n"
2281
-
2282
- return StreamingResponse(output_stream(), media_type="text/event-stream")
2283
-
2284
-
2285
- @app.get("/")
2286
- async def index():
2287
- """Serve the HTML page"""
2288
- rtc_config = get_twilio_turn_credentials() if get_space() else None
2289
- html_content = HTML_CONTENT.replace("__RTC_CONFIGURATION__", json.dumps(rtc_config))
2290
- return HTMLResponse(content=html_content)
2291
-
2292
 
2293
  if __name__ == "__main__":
2294
- import uvicorn
2295
-
2296
- mode = os.getenv("MODE")
2297
- if mode == "UI":
2298
- stream.ui.launch(server_port=7860)
2299
- elif mode == "PHONE":
2300
- stream.fastphone(host="0.0.0.0", port=7860)
2301
- else:
2302
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
 
 
 
1
  import os
2
+ import sys
3
+ import streamlit as st
4
+ from tempfile import NamedTemporaryFile
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
+ def main():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  try:
8
+ # Get the code from secrets
9
+ code = os.environ.get("MAIN_CODE")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
+ if not code:
12
+ st.error("⚠️ The application code wasn't found in secrets. Please add the MAIN_CODE secret.")
13
+ return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
+ # Create a temporary Python file
16
+ with NamedTemporaryFile(suffix='.py', delete=False, mode='w') as tmp:
17
+ tmp.write(code)
18
+ tmp_path = tmp.name
 
 
 
19
 
20
+ # Execute the code
21
+ exec(compile(code, tmp_path, 'exec'), globals())
22
 
23
+ # Clean up the temporary file
 
24
  try:
25
+ os.unlink(tmp_path)
 
26
  except:
27
  pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  except Exception as e:
30
+ st.error(f"⚠️ Error loading or executing the application: {str(e)}")
31
+ import traceback
32
+ st.code(traceback.format_exc())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
  if __name__ == "__main__":
35
+ main()