seawolf2357 commited on
Commit
a9741e7
·
verified ·
1 Parent(s): 977b047

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +383 -445
app.py CHANGED
@@ -23,149 +23,16 @@ import gradio as gr
23
  import io
24
  from scipy import signal
25
  import wave
26
- from datetime import datetime
27
- import sqlite3
28
  import aiosqlite
29
  from langdetect import detect, LangDetectException
 
30
  import uuid
31
 
32
  load_dotenv()
33
 
34
  SAMPLE_RATE = 24000
35
-
36
- # Supported languages for OpenAI Realtime API
37
- SUPPORTED_LANGUAGES = {
38
- "ko": "한국어 (Korean)",
39
- "en": "English",
40
- "es": "Español (Spanish)",
41
- "fr": "Français (French)",
42
- "de": "Deutsch (German)",
43
- "it": "Italiano (Italian)",
44
- "pt": "Português (Portuguese)",
45
- "ru": "Русский (Russian)",
46
- "ja": "日本語 (Japanese)",
47
- "zh": "中文 (Chinese)",
48
- "ar": "العربية (Arabic)",
49
- "hi": "हिन्दी (Hindi)",
50
- "nl": "Nederlands (Dutch)",
51
- "pl": "Polski (Polish)",
52
- "tr": "Türkçe (Turkish)",
53
- "vi": "Tiếng Việt (Vietnamese)",
54
- "th": "ไทย (Thai)",
55
- "id": "Bahasa Indonesia",
56
- "sv": "Svenska (Swedish)",
57
- "da": "Dansk (Danish)",
58
- "no": "Norsk (Norwegian)",
59
- "fi": "Suomi (Finnish)",
60
- "he": "עברית (Hebrew)",
61
- "uk": "Українська (Ukrainian)",
62
- "cs": "Čeština (Czech)",
63
- "el": "Ελληνικά (Greek)",
64
- "ro": "Română (Romanian)",
65
- "hu": "Magyar (Hungarian)",
66
- "ms": "Bahasa Melayu (Malay)"
67
- }
68
-
69
- # Database setup
70
  DB_PATH = "chat_history.db"
71
 
72
- async def init_db():
73
- """Initialize the SQLite database"""
74
- async with aiosqlite.connect(DB_PATH) as db:
75
- await db.execute("""
76
- CREATE TABLE IF NOT EXISTS conversations (
77
- id TEXT PRIMARY KEY,
78
- title TEXT NOT NULL,
79
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
80
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
81
- )
82
- """)
83
-
84
- await db.execute("""
85
- CREATE TABLE IF NOT EXISTS messages (
86
- id INTEGER PRIMARY KEY AUTOINCREMENT,
87
- conversation_id TEXT NOT NULL,
88
- role TEXT NOT NULL,
89
- content TEXT NOT NULL,
90
- language TEXT,
91
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
92
- FOREIGN KEY (conversation_id) REFERENCES conversations (id)
93
- )
94
- """)
95
-
96
- await db.commit()
97
-
98
- async def save_message(conversation_id: str, role: str, content: str, language: str = None):
99
- """Save a message to the database"""
100
- async with aiosqlite.connect(DB_PATH) as db:
101
- # Check if conversation exists
102
- cursor = await db.execute(
103
- "SELECT id FROM conversations WHERE id = ?",
104
- (conversation_id,)
105
- )
106
- exists = await cursor.fetchone()
107
-
108
- if not exists:
109
- # Create new conversation
110
- title = content[:50] + "..." if len(content) > 50 else content
111
- await db.execute(
112
- "INSERT INTO conversations (id, title) VALUES (?, ?)",
113
- (conversation_id, title)
114
- )
115
- else:
116
- # Update conversation timestamp
117
- await db.execute(
118
- "UPDATE conversations SET updated_at = CURRENT_TIMESTAMP WHERE id = ?",
119
- (conversation_id,)
120
- )
121
-
122
- # Insert message
123
- await db.execute(
124
- "INSERT INTO messages (conversation_id, role, content, language) VALUES (?, ?, ?, ?)",
125
- (conversation_id, role, content, language)
126
- )
127
-
128
- await db.commit()
129
-
130
- async def get_conversations():
131
- """Get all conversations"""
132
- async with aiosqlite.connect(DB_PATH) as db:
133
- db.row_factory = aiosqlite.Row
134
- cursor = await db.execute(
135
- "SELECT * FROM conversations ORDER BY updated_at DESC"
136
- )
137
- conversations = await cursor.fetchall()
138
- return [dict(conv) for conv in conversations]
139
-
140
- async def get_conversation_messages(conversation_id: str):
141
- """Get all messages for a conversation"""
142
- async with aiosqlite.connect(DB_PATH) as db:
143
- db.row_factory = aiosqlite.Row
144
- cursor = await db.execute(
145
- "SELECT * FROM messages WHERE conversation_id = ? ORDER BY created_at",
146
- (conversation_id,)
147
- )
148
- messages = await cursor.fetchall()
149
- return [dict(msg) for msg in messages]
150
-
151
- def detect_language(text: str) -> str:
152
- """Detect the language of the input text"""
153
- try:
154
- lang = detect(text)
155
- # Map detected language to our supported languages
156
- if lang == 'ko':
157
- return 'ko'
158
- elif lang == 'en':
159
- return 'en'
160
- elif lang in SUPPORTED_LANGUAGES:
161
- return lang
162
- else:
163
- # Default to Korean if unsupported language
164
- return 'ko'
165
- except LangDetectException:
166
- # Default to Korean if detection fails
167
- return 'ko'
168
-
169
  # HTML content embedded as a string
170
  HTML_CONTENT = """<!DOCTYPE html>
171
  <html lang="ko">
@@ -252,60 +119,6 @@ HTML_CONTENT = """<!DOCTYPE html>
252
  font-size: 32px;
253
  letter-spacing: 1px;
254
  }
255
- /* History section */
256
- .history-section {
257
- background-color: var(--card-bg);
258
- border-radius: 12px;
259
- padding: 20px;
260
- border: 1px solid var(--border-color);
261
- overflow-y: auto;
262
- flex-shrink: 0;
263
- max-height: 300px;
264
- }
265
- .history-item {
266
- padding: 10px;
267
- margin-bottom: 8px;
268
- background-color: var(--dark-bg);
269
- border-radius: 6px;
270
- cursor: pointer;
271
- transition: all 0.2s;
272
- display: flex;
273
- justify-content: space-between;
274
- align-items: center;
275
- }
276
- .history-item:hover {
277
- background-color: var(--hover-color);
278
- transform: translateX(5px);
279
- }
280
- .history-item-title {
281
- font-size: 14px;
282
- color: var(--text-color);
283
- overflow: hidden;
284
- text-overflow: ellipsis;
285
- white-space: nowrap;
286
- flex-grow: 1;
287
- }
288
- .history-item-date {
289
- font-size: 12px;
290
- color: #888;
291
- margin-left: 10px;
292
- }
293
- .new-chat-button {
294
- width: 100%;
295
- background: linear-gradient(135deg, #2ecc71, #27ae60);
296
- color: white;
297
- border: none;
298
- padding: 10px 20px;
299
- font-size: 14px;
300
- border-radius: 6px;
301
- cursor: pointer;
302
- margin-bottom: 10px;
303
- transition: all 0.3s;
304
- }
305
- .new-chat-button:hover {
306
- background: linear-gradient(135deg, #27ae60, #229954);
307
- transform: translateY(-2px);
308
- }
309
  /* Settings section */
310
  .settings-section {
311
  background-color: var(--card-bg);
@@ -358,21 +171,37 @@ HTML_CONTENT = """<!DOCTYPE html>
358
  .toggle-switch.active .toggle-slider {
359
  transform: translateX(24px);
360
  }
361
- /* Select dropdown */
362
- select {
363
  background-color: var(--card-bg);
364
- color: var(--text-color);
 
365
  border: 1px solid var(--border-color);
366
- padding: 8px 12px;
 
 
 
 
 
 
 
367
  border-radius: 6px;
368
- font-size: 14px;
369
  cursor: pointer;
370
- min-width: 120px;
371
- max-width: 200px;
372
  }
373
- select:focus {
374
- outline: none;
375
- border-color: var(--primary-color);
 
 
 
 
 
 
 
 
 
 
376
  }
377
  /* Text inputs */
378
  .text-input-section {
@@ -677,12 +506,6 @@ HTML_CONTENT = """<!DOCTYPE html>
677
 
678
  <div class="main-content">
679
  <div class="sidebar">
680
- <div class="history-section">
681
- <h3 style="margin: 0 0 15px 0; color: var(--primary-color);">대화 기록</h3>
682
- <button class="new-chat-button" onclick="startNewChat()">+ 새 대화</button>
683
- <div id="history-list"></div>
684
- </div>
685
-
686
  <div class="settings-section">
687
  <h3 style="margin: 0 0 15px 0; color: var(--primary-color);">설정</h3>
688
  <div class="settings-grid">
@@ -692,54 +515,18 @@ HTML_CONTENT = """<!DOCTYPE html>
692
  <div class="toggle-slider"></div>
693
  </div>
694
  </div>
695
- <div class="setting-item">
696
- <span class="setting-label">자동 언어 감지</span>
697
- <div id="auto-lang-toggle" class="toggle-switch active">
698
- <div class="toggle-slider"></div>
699
- </div>
700
- </div>
701
- <div class="setting-item">
702
- <span class="setting-label">번역 언어</span>
703
- <select id="language-select">
704
- <option value="">자동 감지</option>
705
- <option value="ko">한국어 (Korean)</option>
706
- <option value="en">English</option>
707
- <option value="es">Español (Spanish)</option>
708
- <option value="fr">Français (French)</option>
709
- <option value="de">Deutsch (German)</option>
710
- <option value="it">Italiano (Italian)</option>
711
- <option value="pt">Português (Portuguese)</option>
712
- <option value="ru">Русский (Russian)</option>
713
- <option value="ja">日本語 (Japanese)</option>
714
- <option value="zh">中文 (Chinese)</option>
715
- <option value="ar">العربية (Arabic)</option>
716
- <option value="hi">हिन्दी (Hindi)</option>
717
- <option value="nl">Nederlands (Dutch)</option>
718
- <option value="pl">Polski (Polish)</option>
719
- <option value="tr">Türkçe (Turkish)</option>
720
- <option value="vi">Tiếng Việt (Vietnamese)</option>
721
- <option value="th">ไทย (Thai)</option>
722
- <option value="id">Bahasa Indonesia</option>
723
- <option value="sv">Svenska (Swedish)</option>
724
- <option value="da">Dansk (Danish)</option>
725
- <option value="no">Norsk (Norwegian)</option>
726
- <option value="fi">Suomi (Finnish)</option>
727
- <option value="he">עברית (Hebrew)</option>
728
- <option value="uk">Українська (Ukrainian)</option>
729
- <option value="cs">Čeština (Czech)</option>
730
- <option value="el">Ελληνικά (Greek)</option>
731
- <option value="ro">Română (Romanian)</option>
732
- <option value="hu">Magyar (Hungarian)</option>
733
- <option value="ms">Bahasa Melayu (Malay)</option>
734
- </select>
735
- </div>
736
  </div>
737
  <div class="text-input-section">
738
  <label for="system-prompt" class="setting-label">시스템 프롬프트:</label>
739
- <textarea id="system-prompt" placeholder="AI 어시스턴트의 성격, 역할, 행동 방식을 정의하세요...">당신은 친절하고 도움이 되는 AI 어시스턴트입니다. 사용자의 요청에 정확하고 유용한 답변을 제공합니다.</textarea>
740
  </div>
741
  </div>
742
 
 
 
 
 
 
743
  <div class="controls">
744
  <button id="start-button">대화 시작</button>
745
  </div>
@@ -765,11 +552,8 @@ HTML_CONTENT = """<!DOCTYPE html>
765
  let peerConnection;
766
  let webrtc_id;
767
  let webSearchEnabled = false;
768
- let autoLanguageDetection = true;
769
- let selectedLanguage = "";
770
- let systemPrompt = "당신은 친절하고 도움이 되는 AI 어시스턴트입니다. 사용자의 요청에 정확하고 유용한 답변을 제공합니다.";
771
- let currentConversationId = null;
772
-
773
  const audioOutput = document.getElementById('audio-output');
774
  const startButton = document.getElementById('start-button');
775
  const sendButton = document.getElementById('send-button');
@@ -777,63 +561,39 @@ HTML_CONTENT = """<!DOCTYPE html>
777
  const statusDot = document.getElementById('status-dot');
778
  const statusText = document.getElementById('status-text');
779
  const searchToggle = document.getElementById('search-toggle');
780
- const autoLangToggle = document.getElementById('auto-lang-toggle');
781
- const languageSelect = document.getElementById('language-select');
782
  const systemPromptInput = document.getElementById('system-prompt');
783
  const textInput = document.getElementById('text-input');
784
  const historyList = document.getElementById('history-list');
785
-
786
  let audioLevel = 0;
787
  let animationFrame;
788
  let audioContext, analyser, audioSource;
789
  let dataChannel = null;
790
  let isVoiceActive = false;
791
 
792
- // Initialize
793
- window.addEventListener('DOMContentLoaded', async () => {
794
- sendButton.style.display = 'block';
795
- await loadHistory();
796
- startNewChat();
797
- });
798
-
799
- // Start new chat
800
- function startNewChat() {
801
- currentConversationId = generateUUID();
802
- chatMessages.innerHTML = '';
803
- console.log('Started new conversation:', currentConversationId);
804
- }
805
-
806
- // Generate UUID
807
- function generateUUID() {
808
- return 'xxxx-xxxx-4xxx-yxxx-xxxx'.replace(/[xy]/g, function(c) {
809
- const r = Math.random() * 16 | 0;
810
- const v = c === 'x' ? r : (r & 0x3 | 0x8);
811
- return v.toString(16);
812
- });
813
  }
814
 
815
  // Load conversation history
816
  async function loadHistory() {
817
  try {
818
- const response = await fetch('/conversations');
819
  const conversations = await response.json();
820
 
821
  historyList.innerHTML = '';
822
  conversations.forEach(conv => {
823
  const item = document.createElement('div');
824
  item.className = 'history-item';
 
 
 
 
825
  item.onclick = () => loadConversation(conv.id);
826
-
827
- const title = document.createElement('div');
828
- title.className = 'history-item-title';
829
- title.textContent = conv.title;
830
-
831
- const date = document.createElement('div');
832
- date.className = 'history-item-date';
833
- date.textContent = new Date(conv.updated_at).toLocaleDateString('ko-KR');
834
-
835
- item.appendChild(title);
836
- item.appendChild(date);
837
  historyList.appendChild(item);
838
  });
839
  } catch (error) {
@@ -841,15 +601,13 @@ HTML_CONTENT = """<!DOCTYPE html>
841
  }
842
  }
843
 
844
- // Load a specific conversation
845
- async function loadConversation(conversationId) {
846
  try {
847
- const response = await fetch(`/conversation/${conversationId}`);
848
  const messages = await response.json();
849
 
850
- currentConversationId = conversationId;
851
  chatMessages.innerHTML = '';
852
-
853
  messages.forEach(msg => {
854
  addMessage(msg.role, msg.content, false);
855
  });
@@ -865,23 +623,9 @@ HTML_CONTENT = """<!DOCTYPE html>
865
  console.log('Web search enabled:', webSearchEnabled);
866
  });
867
 
868
- // Auto language detection toggle
869
- autoLangToggle.addEventListener('click', () => {
870
- autoLanguageDetection = !autoLanguageDetection;
871
- autoLangToggle.classList.toggle('active', autoLanguageDetection);
872
- languageSelect.disabled = autoLanguageDetection;
873
- console.log('Auto language detection:', autoLanguageDetection);
874
- });
875
-
876
- // Language selection
877
- languageSelect.addEventListener('change', () => {
878
- selectedLanguage = languageSelect.value;
879
- console.log('Selected language:', selectedLanguage);
880
- });
881
-
882
  // System prompt update
883
  systemPromptInput.addEventListener('input', () => {
884
- systemPrompt = systemPromptInput.value || "당신은 친절하고 도움이 되는 AI 어시스턴트입니다.";
885
  });
886
 
887
  // Text input handling
@@ -899,7 +643,7 @@ HTML_CONTENT = """<!DOCTYPE html>
899
  if (!message) return;
900
 
901
  // Add user message to chat
902
- addMessage('user', message, true);
903
  textInput.value = '';
904
 
905
  // Show sending indicator
@@ -918,10 +662,8 @@ HTML_CONTENT = """<!DOCTYPE html>
918
  body: JSON.stringify({
919
  message: message,
920
  web_search_enabled: webSearchEnabled,
921
- target_language: autoLanguageDetection ? '' : selectedLanguage,
922
  system_prompt: systemPrompt,
923
- conversation_id: currentConversationId,
924
- auto_detect: autoLanguageDetection
925
  })
926
  });
927
 
@@ -936,13 +678,10 @@ HTML_CONTENT = """<!DOCTYPE html>
936
  } else {
937
  // Add assistant response
938
  let content = data.response;
939
- if (data.language) {
940
- content += ` <span class="language-info">[${data.language}]</span>`;
941
  }
942
- addMessage('assistant', content, true);
943
-
944
- // Refresh history
945
- await loadHistory();
946
  }
947
  } catch (error) {
948
  console.error('Error sending text message:', error);
@@ -963,11 +702,10 @@ HTML_CONTENT = """<!DOCTYPE html>
963
  sendButton.style.display = 'none';
964
  } else {
965
  statusText.textContent = '연결 대기 중';
966
- sendButton.style.display = 'block';
967
  isVoiceActive = false;
968
  }
969
  }
970
-
971
  function updateButtonState() {
972
  const button = document.getElementById('start-button');
973
  if (peerConnection && (peerConnection.connectionState === 'connecting' || peerConnection.connectionState === 'new')) {
@@ -997,7 +735,6 @@ HTML_CONTENT = """<!DOCTYPE html>
997
  updateStatus('disconnected');
998
  }
999
  }
1000
-
1001
  function setupAudioVisualization(stream) {
1002
  audioContext = new (window.AudioContext || window.webkitAudioContext)();
1003
  analyser = audioContext.createAnalyser();
@@ -1032,7 +769,6 @@ HTML_CONTENT = """<!DOCTYPE html>
1032
 
1033
  updateAudioLevel();
1034
  }
1035
-
1036
  function showError(message) {
1037
  const toast = document.getElementById('error-toast');
1038
  toast.textContent = message;
@@ -1042,7 +778,6 @@ HTML_CONTENT = """<!DOCTYPE html>
1042
  toast.style.display = 'none';
1043
  }, 5000);
1044
  }
1045
-
1046
  async function setupWebRTC() {
1047
  const config = __RTC_CONFIGURATION__;
1048
  peerConnection = new RTCPeerConnection(config);
@@ -1055,7 +790,6 @@ HTML_CONTENT = """<!DOCTYPE html>
1055
  toast.style.display = 'none';
1056
  }, 5000);
1057
  }, 5000);
1058
-
1059
  try {
1060
  const stream = await navigator.mediaDevices.getUserMedia({
1061
  audio: true
@@ -1098,7 +832,6 @@ HTML_CONTENT = """<!DOCTYPE html>
1098
  peerConnection.addEventListener("icegatheringstatechange", checkState);
1099
  }
1100
  });
1101
-
1102
  peerConnection.addEventListener('connectionstatechange', () => {
1103
  console.log('connectionstatechange', peerConnection.connectionState);
1104
  if (peerConnection.connectionState === 'connected') {
@@ -1108,9 +841,16 @@ HTML_CONTENT = """<!DOCTYPE html>
1108
  }
1109
  updateButtonState();
1110
  });
1111
-
1112
  webrtc_id = Math.random().toString(36).substring(7);
1113
 
 
 
 
 
 
 
 
 
1114
  const response = await fetch('/webrtc/offer', {
1115
  method: 'POST',
1116
  headers: { 'Content-Type': 'application/json' },
@@ -1119,13 +859,10 @@ HTML_CONTENT = """<!DOCTYPE html>
1119
  type: peerConnection.localDescription.type,
1120
  webrtc_id: webrtc_id,
1121
  web_search_enabled: webSearchEnabled,
1122
- target_language: autoLanguageDetection ? '' : selectedLanguage,
1123
  system_prompt: systemPrompt,
1124
- conversation_id: currentConversationId,
1125
- auto_detect: autoLanguageDetection
1126
  })
1127
  });
1128
-
1129
  const serverResponse = await response.json();
1130
  if (serverResponse.status === 'failed') {
1131
  showError(serverResponse.meta.error === 'concurrency_limit_reached'
@@ -1134,25 +871,21 @@ HTML_CONTENT = """<!DOCTYPE html>
1134
  stop();
1135
  return;
1136
  }
1137
-
1138
  await peerConnection.setRemoteDescription(serverResponse);
1139
  const eventSource = new EventSource('/outputs?webrtc_id=' + webrtc_id);
1140
- eventSource.addEventListener("output", async (event) => {
1141
  const eventJson = JSON.parse(event.data);
1142
  let content = eventJson.content;
1143
 
1144
- if (eventJson.language) {
1145
- content += ` <span class="language-info">[${eventJson.language}]</span>`;
1146
  }
1147
- addMessage("assistant", content, true);
1148
-
1149
- // Refresh history after receiving a response
1150
- await loadHistory();
1151
  });
1152
  eventSource.addEventListener("search", (event) => {
1153
  const eventJson = JSON.parse(event.data);
1154
  if (eventJson.query) {
1155
- addMessage("search-result", `웹 검색 중: "${eventJson.query}"`, true);
1156
  }
1157
  });
1158
  } catch (err) {
@@ -1162,8 +895,7 @@ HTML_CONTENT = """<!DOCTYPE html>
1162
  stop();
1163
  }
1164
  }
1165
-
1166
- function addMessage(role, content, save = false) {
1167
  const messageDiv = document.createElement('div');
1168
  messageDiv.classList.add('message', role);
1169
 
@@ -1175,16 +907,15 @@ HTML_CONTENT = """<!DOCTYPE html>
1175
  chatMessages.appendChild(messageDiv);
1176
  chatMessages.scrollTop = chatMessages.scrollHeight;
1177
 
1178
- // Save to database if needed
1179
- if (save && currentConversationId && role !== 'search-result') {
1180
- fetch('/message', {
1181
  method: 'POST',
1182
  headers: { 'Content-Type': 'application/json' },
1183
  body: JSON.stringify({
1184
- conversation_id: currentConversationId,
1185
  role: role,
1186
- content: content.replace(/<[^>]*>/g, ''), // Remove HTML tags
1187
- language: ''
1188
  })
1189
  }).catch(error => console.error('Failed to save message:', error));
1190
  }
@@ -1193,11 +924,13 @@ HTML_CONTENT = """<!DOCTYPE html>
1193
  function stop() {
1194
  console.log('[STOP] Stopping connection...');
1195
 
 
1196
  if (animationFrame) {
1197
  cancelAnimationFrame(animationFrame);
1198
  animationFrame = null;
1199
  }
1200
 
 
1201
  if (audioContext) {
1202
  audioContext.close();
1203
  audioContext = null;
@@ -1205,14 +938,17 @@ HTML_CONTENT = """<!DOCTYPE html>
1205
  audioSource = null;
1206
  }
1207
 
 
1208
  if (dataChannel) {
1209
  dataChannel.close();
1210
  dataChannel = null;
1211
  }
1212
 
 
1213
  if (peerConnection) {
1214
  console.log('[STOP] Current connection state:', peerConnection.connectionState);
1215
 
 
1216
  if (peerConnection.getTransceivers) {
1217
  peerConnection.getTransceivers().forEach(transceiver => {
1218
  if (transceiver.stop) {
@@ -1221,6 +957,7 @@ HTML_CONTENT = """<!DOCTYPE html>
1221
  });
1222
  }
1223
 
 
1224
  if (peerConnection.getSenders) {
1225
  peerConnection.getSenders().forEach(sender => {
1226
  if (sender.track) {
@@ -1229,6 +966,7 @@ HTML_CONTENT = """<!DOCTYPE html>
1229
  });
1230
  }
1231
 
 
1232
  if (peerConnection.getReceivers) {
1233
  peerConnection.getReceivers().forEach(receiver => {
1234
  if (receiver.track) {
@@ -1237,22 +975,28 @@ HTML_CONTENT = """<!DOCTYPE html>
1237
  });
1238
  }
1239
 
 
1240
  peerConnection.close();
 
 
1241
  peerConnection = null;
1242
 
1243
  console.log('[STOP] Connection closed');
1244
  }
1245
 
 
1246
  audioLevel = 0;
1247
  isVoiceActive = false;
 
 
1248
  updateButtonState();
1249
 
 
1250
  if (webrtc_id) {
1251
  console.log('[STOP] Clearing webrtc_id:', webrtc_id);
1252
  webrtc_id = null;
1253
  }
1254
  }
1255
-
1256
  startButton.addEventListener('click', () => {
1257
  console.log('clicked');
1258
  console.log(peerConnection, peerConnection?.connectionState);
@@ -1263,6 +1007,13 @@ HTML_CONTENT = """<!DOCTYPE html>
1263
  stop();
1264
  }
1265
  });
 
 
 
 
 
 
 
1266
  </script>
1267
  </body>
1268
 
@@ -1310,9 +1061,135 @@ class BraveSearchClient:
1310
  return []
1311
 
1312
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1313
  # Initialize search client globally
1314
  brave_api_key = os.getenv("BSEARCH_API")
1315
  search_client = BraveSearchClient(brave_api_key) if brave_api_key else None
 
1316
 
1317
  # Store connection settings
1318
  connection_settings = {}
@@ -1320,49 +1197,22 @@ connection_settings = {}
1320
  # Initialize OpenAI client for text chat
1321
  client = openai.AsyncOpenAI()
1322
 
1323
- def get_translation_instructions(target_language: str) -> str:
1324
- """Get instructions for translation based on target language"""
1325
- if not target_language:
1326
- return ""
1327
-
1328
- language_name = SUPPORTED_LANGUAGES.get(target_language, target_language)
1329
- return (
1330
- f"\n\nIMPORTANT: You must respond in {language_name} ({target_language}). "
1331
- f"Translate all your responses to {language_name}."
1332
- )
1333
 
1334
  def update_chatbot(chatbot: list[dict], response: ResponseAudioTranscriptDoneEvent):
1335
  chatbot.append({"role": "assistant", "content": response.transcript})
1336
  return chatbot
1337
 
1338
 
1339
- async def process_text_chat(message: str, web_search_enabled: bool, target_language: str,
1340
- system_prompt: str, conversation_id: str, auto_detect: bool) -> Dict[str, str]:
1341
  """Process text chat using GPT-4o-mini model"""
1342
  try:
1343
- # Auto-detect language if enabled
1344
- if auto_detect:
1345
- detected_lang = detect_language(message)
1346
- if detected_lang in ['en', 'ko']:
1347
- target_language = detected_lang
1348
- else:
1349
- target_language = 'ko' # Default to Korean
1350
-
1351
- # Save user message
1352
- await save_message(conversation_id, "user", message, target_language)
1353
-
1354
- # Prepare system prompt based on language
1355
- if target_language == 'en':
1356
- base_instructions = f"You are a helpful assistant. {system_prompt}"
1357
- else:
1358
- base_instructions = system_prompt or "당신은 친절하고 도움이 되는 AI 어시스턴트입니다."
1359
-
1360
  messages = [
1361
- {"role": "system", "content": base_instructions}
1362
  ]
1363
 
1364
  # Handle web search if enabled
1365
  if web_search_enabled and search_client:
 
1366
  search_keywords = ["날씨", "기온", "비", "눈", "뉴스", "소식", "현재", "최근",
1367
  "오늘", "지금", "가격", "환율", "주가", "weather", "news",
1368
  "current", "today", "price", "2024", "2025"]
@@ -1370,6 +1220,7 @@ async def process_text_chat(message: str, web_search_enabled: bool, target_langu
1370
  should_search = any(keyword in message.lower() for keyword in search_keywords)
1371
 
1372
  if should_search:
 
1373
  search_results = await search_client.search(message)
1374
  if search_results:
1375
  search_context = "웹 검색 결과:\n\n"
@@ -1378,7 +1229,7 @@ async def process_text_chat(message: str, web_search_enabled: bool, target_langu
1378
 
1379
  messages.append({
1380
  "role": "system",
1381
- "content": "다음 웹 검색 결과를 참고하여 답변하세요: \n\n" + search_context
1382
  })
1383
 
1384
  messages.append({"role": "user", "content": message})
@@ -1393,12 +1244,22 @@ async def process_text_chat(message: str, web_search_enabled: bool, target_langu
1393
 
1394
  response_text = response.choices[0].message.content
1395
 
1396
- # Save assistant response
1397
- await save_message(conversation_id, "assistant", response_text, target_language)
 
 
 
 
 
 
 
 
 
 
1398
 
1399
  return {
1400
  "response": response_text,
1401
- "language": SUPPORTED_LANGUAGES.get(target_language, "") if target_language else ""
1402
  }
1403
 
1404
  except Exception as e:
@@ -1407,9 +1268,8 @@ async def process_text_chat(message: str, web_search_enabled: bool, target_langu
1407
 
1408
 
1409
  class OpenAIHandler(AsyncStreamHandler):
1410
- def __init__(self, web_search_enabled: bool = False, target_language: str = "",
1411
- system_prompt: str = "", webrtc_id: str = None, conversation_id: str = None,
1412
- auto_detect: bool = True) -> None:
1413
  super().__init__(
1414
  expected_layout="mono",
1415
  output_sample_rate=SAMPLE_RATE,
@@ -1424,13 +1284,15 @@ class OpenAIHandler(AsyncStreamHandler):
1424
  self.current_call_id = None
1425
  self.webrtc_id = webrtc_id
1426
  self.web_search_enabled = web_search_enabled
1427
- self.target_language = target_language
1428
  self.system_prompt = system_prompt
1429
- self.conversation_id = conversation_id
1430
- self.auto_detect = auto_detect
 
1431
 
1432
  def copy(self):
 
1433
  if connection_settings:
 
1434
  recent_ids = sorted(connection_settings.keys(),
1435
  key=lambda k: connection_settings[k].get('timestamp', 0),
1436
  reverse=True)
@@ -1438,15 +1300,17 @@ class OpenAIHandler(AsyncStreamHandler):
1438
  recent_id = recent_ids[0]
1439
  settings = connection_settings[recent_id]
1440
 
 
 
 
1441
  return OpenAIHandler(
1442
  web_search_enabled=settings.get('web_search_enabled', False),
1443
- target_language=settings.get('target_language', ''),
1444
  system_prompt=settings.get('system_prompt', ''),
1445
  webrtc_id=recent_id,
1446
- conversation_id=settings.get('conversation_id'),
1447
- auto_detect=settings.get('auto_detect', True)
1448
  )
1449
 
 
1450
  return OpenAIHandler(web_search_enabled=False)
1451
 
1452
  async def search_web(self, query: str) -> str:
@@ -1459,6 +1323,7 @@ class OpenAIHandler(AsyncStreamHandler):
1459
  if not results:
1460
  return f"'{query}'에 대한 검색 결과를 찾을 수 없습니다."
1461
 
 
1462
  formatted_results = []
1463
  for i, result in enumerate(results, 1):
1464
  formatted_results.append(
@@ -1483,26 +1348,31 @@ class OpenAIHandler(AsyncStreamHandler):
1483
 
1484
  async def start_up(self):
1485
  """Connect to realtime API"""
 
1486
  if connection_settings and self.webrtc_id:
1487
  if self.webrtc_id in connection_settings:
1488
  settings = connection_settings[self.webrtc_id]
1489
  self.web_search_enabled = settings.get('web_search_enabled', False)
1490
- self.target_language = settings.get('target_language', '')
1491
  self.system_prompt = settings.get('system_prompt', '')
1492
- self.conversation_id = settings.get('conversation_id')
1493
- self.auto_detect = settings.get('auto_detect', True)
 
1494
 
1495
  self.client = openai.AsyncOpenAI()
1496
 
 
 
 
 
1497
  tools = []
1498
- base_instructions = self.system_prompt or "당신은 친절하고 도움이 되는 AI 어시스턴트입니다."
1499
 
1500
  if self.web_search_enabled and self.search_client:
1501
  tools = [{
1502
  "type": "function",
1503
  "function": {
1504
  "name": "web_search",
1505
- "description": "Search the web for current information.",
1506
  "parameters": {
1507
  "type": "object",
1508
  "properties": {
@@ -1515,33 +1385,68 @@ class OpenAIHandler(AsyncStreamHandler):
1515
  }
1516
  }
1517
  }]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1518
 
1519
  async with self.client.beta.realtime.connect(
1520
  model="gpt-4o-mini-realtime-preview-2024-12-17"
1521
  ) as conn:
 
1522
  session_update = {
1523
  "turn_detection": {"type": "server_vad"},
1524
- "instructions": base_instructions,
1525
  "tools": tools,
1526
  "tool_choice": "auto" if tools else "none",
1527
  "temperature": 0.7,
1528
  "max_response_output_tokens": 4096,
1529
  "modalities": ["text", "audio"],
1530
- "voice": "nova"
1531
  }
1532
 
1533
  await conn.session.update(session=session_update)
1534
  self.connection = conn
 
1535
 
1536
  async for event in self.connection:
 
 
 
 
1537
  if event.type == "response.audio_transcript.done":
1538
- # Save the transcript
1539
- if self.conversation_id:
1540
- await save_message(self.conversation_id, "assistant", event.transcript)
 
 
 
 
 
 
 
 
 
 
1541
 
1542
  output_data = {
1543
  "event": event,
1544
- "language": SUPPORTED_LANGUAGES.get(self.target_language, "") if self.target_language else ""
1545
  }
1546
  await self.output_queue.put(AdditionalOutputs(output_data))
1547
 
@@ -1557,6 +1462,7 @@ class OpenAIHandler(AsyncStreamHandler):
1557
 
1558
  # Handle function calls
1559
  elif event.type == "response.function_call_arguments.start":
 
1560
  self.function_call_in_progress = True
1561
  self.current_function_args = ""
1562
  self.current_call_id = getattr(event, 'call_id', None)
@@ -1567,17 +1473,22 @@ class OpenAIHandler(AsyncStreamHandler):
1567
 
1568
  elif event.type == "response.function_call_arguments.done":
1569
  if self.function_call_in_progress:
 
1570
  try:
1571
  args = json.loads(self.current_function_args)
1572
  query = args.get("query", "")
1573
 
 
1574
  await self.output_queue.put(AdditionalOutputs({
1575
  "type": "search",
1576
  "query": query
1577
  }))
1578
 
 
1579
  search_results = await self.search_web(query)
 
1580
 
 
1581
  if self.connection and self.current_call_id:
1582
  await self.connection.conversation.item.create(
1583
  item={
@@ -1597,6 +1508,7 @@ class OpenAIHandler(AsyncStreamHandler):
1597
 
1598
  async def receive(self, frame: tuple[int, np.ndarray]) -> None:
1599
  if not self.connection:
 
1600
  return
1601
  try:
1602
  _, array = frame
@@ -1609,6 +1521,7 @@ class OpenAIHandler(AsyncStreamHandler):
1609
  async def emit(self) -> tuple[int, np.ndarray] | AdditionalOutputs | None:
1610
  item = await wait_for_item(self.output_queue)
1611
 
 
1612
  if isinstance(item, dict) and item.get('type') == 'text_message':
1613
  await self.process_text_message(item['content'])
1614
  return None
@@ -1616,9 +1529,12 @@ class OpenAIHandler(AsyncStreamHandler):
1616
  return item
1617
 
1618
  async def shutdown(self) -> None:
 
 
1619
  if self.connection:
1620
  await self.connection.close()
1621
  self.connection = None
 
1622
 
1623
 
1624
  # Create initial handler instance
@@ -1629,7 +1545,7 @@ chatbot = gr.Chatbot(type="messages")
1629
 
1630
  # Create stream with handler instance
1631
  stream = Stream(
1632
- handler,
1633
  mode="send-receive",
1634
  modality="audio",
1635
  additional_inputs=[chatbot],
@@ -1642,13 +1558,14 @@ stream = Stream(
1642
 
1643
  app = FastAPI()
1644
 
 
 
 
1645
  # Initialize database on startup
1646
  @app.on_event("startup")
1647
  async def startup_event():
1648
- await init_db()
1649
-
1650
- # Mount stream
1651
- stream.mount(app)
1652
 
1653
  # Intercept offer to capture settings
1654
  @app.post("/webrtc/offer", include_in_schema=False)
@@ -1658,20 +1575,24 @@ async def custom_offer(request: Request):
1658
 
1659
  webrtc_id = body.get("webrtc_id")
1660
  web_search_enabled = body.get("web_search_enabled", False)
1661
- target_language = body.get("target_language", "")
1662
  system_prompt = body.get("system_prompt", "")
1663
- conversation_id = body.get("conversation_id")
1664
- auto_detect = body.get("auto_detect", True)
 
 
 
1665
 
 
1666
  if webrtc_id:
1667
  connection_settings[webrtc_id] = {
1668
  'web_search_enabled': web_search_enabled,
1669
- 'target_language': target_language,
1670
  'system_prompt': system_prompt,
1671
- 'conversation_id': conversation_id,
1672
- 'auto_detect': auto_detect,
1673
  'timestamp': asyncio.get_event_loop().time()
1674
  }
 
 
 
1675
 
1676
  # Remove our custom route temporarily
1677
  custom_route = None
@@ -1680,14 +1601,56 @@ async def custom_offer(request: Request):
1680
  custom_route = app.routes.pop(i)
1681
  break
1682
 
 
 
1683
  response = await stream.offer(body)
1684
 
 
1685
  if custom_route:
1686
  app.routes.insert(0, custom_route)
1687
 
 
 
1688
  return response
1689
 
1690
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1691
  @app.post("/chat/text")
1692
  async def chat_text(request: Request):
1693
  """Handle text chat messages using GPT-4o-mini"""
@@ -1695,16 +1658,14 @@ async def chat_text(request: Request):
1695
  body = await request.json()
1696
  message = body.get("message", "")
1697
  web_search_enabled = body.get("web_search_enabled", False)
1698
- target_language = body.get("target_language", "")
1699
  system_prompt = body.get("system_prompt", "")
1700
- conversation_id = body.get("conversation_id")
1701
- auto_detect = body.get("auto_detect", True)
1702
 
1703
  if not message:
1704
  return {"error": "메시지가 비어있습니다."}
1705
 
1706
- result = await process_text_chat(message, web_search_enabled, target_language,
1707
- system_prompt, conversation_id, auto_detect)
1708
 
1709
  return result
1710
 
@@ -1713,41 +1674,16 @@ async def chat_text(request: Request):
1713
  return {"error": "채팅 처리 중 오류가 발생했습니다."}
1714
 
1715
 
1716
- @app.get("/conversations")
1717
- async def get_conversations_endpoint():
1718
- """Get all conversations"""
1719
- conversations = await get_conversations()
1720
- return conversations
1721
-
1722
-
1723
- @app.get("/conversation/{conversation_id}")
1724
- async def get_conversation_endpoint(conversation_id: str):
1725
- """Get messages for a specific conversation"""
1726
- messages = await get_conversation_messages(conversation_id)
1727
- return messages
1728
-
1729
-
1730
- @app.post("/message")
1731
- async def save_message_endpoint(request: Request):
1732
- """Save a message"""
1733
- body = await request.json()
1734
- await save_message(
1735
- body["conversation_id"],
1736
- body["role"],
1737
- body["content"],
1738
- body.get("language")
1739
- )
1740
- return {"status": "ok"}
1741
-
1742
-
1743
  @app.post("/text_message/{webrtc_id}")
1744
  async def receive_text_message(webrtc_id: str, request: Request):
1745
  """Receive text message from client"""
1746
  body = await request.json()
1747
  message = body.get("content", "")
1748
 
 
1749
  if webrtc_id in stream.handlers:
1750
  handler = stream.handlers[webrtc_id]
 
1751
  await handler.output_queue.put({
1752
  'type': 'text_message',
1753
  'content': message
@@ -1762,15 +1698,17 @@ async def outputs(webrtc_id: str):
1762
  async def output_stream():
1763
  async for output in stream.output_stream(webrtc_id):
1764
  if hasattr(output, 'args') and output.args:
 
1765
  if isinstance(output.args[0], dict) and output.args[0].get('type') == 'search':
1766
  yield f"event: search\ndata: {json.dumps(output.args[0])}\n\n"
 
1767
  elif isinstance(output.args[0], dict) and 'event' in output.args[0]:
1768
  event_data = output.args[0]
1769
  if 'event' in event_data and hasattr(event_data['event'], 'transcript'):
1770
  data = {
1771
  "role": "assistant",
1772
  "content": event_data['event'].transcript,
1773
- "language": event_data.get('language', '')
1774
  }
1775
  yield f"event: output\ndata: {json.dumps(data)}\n\n"
1776
 
 
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
  DB_PATH = "chat_history.db"
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  # HTML content embedded as a string
37
  HTML_CONTENT = """<!DOCTYPE html>
38
  <html lang="ko">
 
119
  font-size: 32px;
120
  letter-spacing: 1px;
121
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  /* Settings section */
123
  .settings-section {
124
  background-color: var(--card-bg);
 
171
  .toggle-switch.active .toggle-slider {
172
  transform: translateX(24px);
173
  }
174
+ /* History section */
175
+ .history-section {
176
  background-color: var(--card-bg);
177
+ border-radius: 12px;
178
+ padding: 20px;
179
  border: 1px solid var(--border-color);
180
+ margin-top: 20px;
181
+ max-height: 300px;
182
+ overflow-y: auto;
183
+ }
184
+ .history-item {
185
+ padding: 10px;
186
+ margin-bottom: 10px;
187
+ background-color: var(--dark-bg);
188
  border-radius: 6px;
 
189
  cursor: pointer;
190
+ transition: background-color 0.2s;
 
191
  }
192
+ .history-item:hover {
193
+ background-color: var(--hover-color);
194
+ }
195
+ .history-date {
196
+ font-size: 12px;
197
+ color: #888;
198
+ }
199
+ .history-preview {
200
+ font-size: 14px;
201
+ margin-top: 5px;
202
+ overflow: hidden;
203
+ text-overflow: ellipsis;
204
+ white-space: nowrap;
205
  }
206
  /* Text inputs */
207
  .text-input-section {
 
506
 
507
  <div class="main-content">
508
  <div class="sidebar">
 
 
 
 
 
 
509
  <div class="settings-section">
510
  <h3 style="margin: 0 0 15px 0; color: var(--primary-color);">설정</h3>
511
  <div class="settings-grid">
 
515
  <div class="toggle-slider"></div>
516
  </div>
517
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
518
  </div>
519
  <div class="text-input-section">
520
  <label for="system-prompt" class="setting-label">시스템 프롬프트:</label>
521
+ <textarea id="system-prompt" placeholder="AI 어시스턴트의 성격, 역할, 행동 방식을 정의하세요...">You are a helpful assistant. Respond in a friendly and professional manner.</textarea>
522
  </div>
523
  </div>
524
 
525
+ <div class="history-section">
526
+ <h3 style="margin: 0 0 15px 0; color: var(--primary-color);">대화 기록</h3>
527
+ <div id="history-list"></div>
528
+ </div>
529
+
530
  <div class="controls">
531
  <button id="start-button">대화 시작</button>
532
  </div>
 
552
  let peerConnection;
553
  let webrtc_id;
554
  let webSearchEnabled = false;
555
+ let systemPrompt = "You are a helpful assistant. Respond in a friendly and professional manner.";
556
+ let currentSessionId = null;
 
 
 
557
  const audioOutput = document.getElementById('audio-output');
558
  const startButton = document.getElementById('start-button');
559
  const sendButton = document.getElementById('send-button');
 
561
  const statusDot = document.getElementById('status-dot');
562
  const statusText = document.getElementById('status-text');
563
  const searchToggle = document.getElementById('search-toggle');
 
 
564
  const systemPromptInput = document.getElementById('system-prompt');
565
  const textInput = document.getElementById('text-input');
566
  const historyList = document.getElementById('history-list');
 
567
  let audioLevel = 0;
568
  let animationFrame;
569
  let audioContext, analyser, audioSource;
570
  let dataChannel = null;
571
  let isVoiceActive = false;
572
 
573
+ // Start new session
574
+ async function startNewSession() {
575
+ const response = await fetch('/session/new', { method: 'POST' });
576
+ const data = await response.json();
577
+ currentSessionId = data.session_id;
578
+ console.log('New session started:', currentSessionId);
579
+ loadHistory();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
580
  }
581
 
582
  // Load conversation history
583
  async function loadHistory() {
584
  try {
585
+ const response = await fetch('/history/recent');
586
  const conversations = await response.json();
587
 
588
  historyList.innerHTML = '';
589
  conversations.forEach(conv => {
590
  const item = document.createElement('div');
591
  item.className = 'history-item';
592
+ item.innerHTML = `
593
+ <div class="history-date">${new Date(conv.created_at).toLocaleString()}</div>
594
+ <div class="history-preview">${conv.summary || '대화 시작'}</div>
595
+ `;
596
  item.onclick = () => loadConversation(conv.id);
 
 
 
 
 
 
 
 
 
 
 
597
  historyList.appendChild(item);
598
  });
599
  } catch (error) {
 
601
  }
602
  }
603
 
604
+ // Load specific conversation
605
+ async function loadConversation(sessionId) {
606
  try {
607
+ const response = await fetch(`/history/${sessionId}`);
608
  const messages = await response.json();
609
 
 
610
  chatMessages.innerHTML = '';
 
611
  messages.forEach(msg => {
612
  addMessage(msg.role, msg.content, false);
613
  });
 
623
  console.log('Web search enabled:', webSearchEnabled);
624
  });
625
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
626
  // System prompt update
627
  systemPromptInput.addEventListener('input', () => {
628
+ systemPrompt = systemPromptInput.value || "You are a helpful assistant. Respond in a friendly and professional manner.";
629
  });
630
 
631
  // Text input handling
 
643
  if (!message) return;
644
 
645
  // Add user message to chat
646
+ addMessage('user', message);
647
  textInput.value = '';
648
 
649
  // Show sending indicator
 
662
  body: JSON.stringify({
663
  message: message,
664
  web_search_enabled: webSearchEnabled,
 
665
  system_prompt: systemPrompt,
666
+ session_id: currentSessionId
 
667
  })
668
  });
669
 
 
678
  } else {
679
  // Add assistant response
680
  let content = data.response;
681
+ if (data.detected_language) {
682
+ content += ` <span class="language-info">[${data.detected_language}]</span>`;
683
  }
684
+ addMessage('assistant', content);
 
 
 
685
  }
686
  } catch (error) {
687
  console.error('Error sending text message:', error);
 
702
  sendButton.style.display = 'none';
703
  } else {
704
  statusText.textContent = '연결 대기 중';
705
+ sendButton.style.display = 'block'; // Show send button even when disconnected for text chat
706
  isVoiceActive = false;
707
  }
708
  }
 
709
  function updateButtonState() {
710
  const button = document.getElementById('start-button');
711
  if (peerConnection && (peerConnection.connectionState === 'connecting' || peerConnection.connectionState === 'new')) {
 
735
  updateStatus('disconnected');
736
  }
737
  }
 
738
  function setupAudioVisualization(stream) {
739
  audioContext = new (window.AudioContext || window.webkitAudioContext)();
740
  analyser = audioContext.createAnalyser();
 
769
 
770
  updateAudioLevel();
771
  }
 
772
  function showError(message) {
773
  const toast = document.getElementById('error-toast');
774
  toast.textContent = message;
 
778
  toast.style.display = 'none';
779
  }, 5000);
780
  }
 
781
  async function setupWebRTC() {
782
  const config = __RTC_CONFIGURATION__;
783
  peerConnection = new RTCPeerConnection(config);
 
790
  toast.style.display = 'none';
791
  }, 5000);
792
  }, 5000);
 
793
  try {
794
  const stream = await navigator.mediaDevices.getUserMedia({
795
  audio: true
 
832
  peerConnection.addEventListener("icegatheringstatechange", checkState);
833
  }
834
  });
 
835
  peerConnection.addEventListener('connectionstatechange', () => {
836
  console.log('connectionstatechange', peerConnection.connectionState);
837
  if (peerConnection.connectionState === 'connected') {
 
841
  }
842
  updateButtonState();
843
  });
 
844
  webrtc_id = Math.random().toString(36).substring(7);
845
 
846
+ // Log current settings before sending
847
+ console.log('Sending offer with settings:', {
848
+ webrtc_id: webrtc_id,
849
+ web_search_enabled: webSearchEnabled,
850
+ system_prompt: systemPrompt,
851
+ session_id: currentSessionId
852
+ });
853
+
854
  const response = await fetch('/webrtc/offer', {
855
  method: 'POST',
856
  headers: { 'Content-Type': 'application/json' },
 
859
  type: peerConnection.localDescription.type,
860
  webrtc_id: webrtc_id,
861
  web_search_enabled: webSearchEnabled,
 
862
  system_prompt: systemPrompt,
863
+ session_id: currentSessionId
 
864
  })
865
  });
 
866
  const serverResponse = await response.json();
867
  if (serverResponse.status === 'failed') {
868
  showError(serverResponse.meta.error === 'concurrency_limit_reached'
 
871
  stop();
872
  return;
873
  }
 
874
  await peerConnection.setRemoteDescription(serverResponse);
875
  const eventSource = new EventSource('/outputs?webrtc_id=' + webrtc_id);
876
+ eventSource.addEventListener("output", (event) => {
877
  const eventJson = JSON.parse(event.data);
878
  let content = eventJson.content;
879
 
880
+ if (eventJson.detected_language) {
881
+ content += ` <span class="language-info">[${eventJson.detected_language}]</span>`;
882
  }
883
+ addMessage("assistant", content);
 
 
 
884
  });
885
  eventSource.addEventListener("search", (event) => {
886
  const eventJson = JSON.parse(event.data);
887
  if (eventJson.query) {
888
+ addMessage("search-result", `웹 검색 중: "${eventJson.query}"`);
889
  }
890
  });
891
  } catch (err) {
 
895
  stop();
896
  }
897
  }
898
+ function addMessage(role, content, save = true) {
 
899
  const messageDiv = document.createElement('div');
900
  messageDiv.classList.add('message', role);
901
 
 
907
  chatMessages.appendChild(messageDiv);
908
  chatMessages.scrollTop = chatMessages.scrollHeight;
909
 
910
+ // Save message to database if save flag is true
911
+ if (save && currentSessionId) {
912
+ fetch('/message/save', {
913
  method: 'POST',
914
  headers: { 'Content-Type': 'application/json' },
915
  body: JSON.stringify({
916
+ session_id: currentSessionId,
917
  role: role,
918
+ content: content
 
919
  })
920
  }).catch(error => console.error('Failed to save message:', error));
921
  }
 
924
  function stop() {
925
  console.log('[STOP] Stopping connection...');
926
 
927
+ // Cancel animation frame first
928
  if (animationFrame) {
929
  cancelAnimationFrame(animationFrame);
930
  animationFrame = null;
931
  }
932
 
933
+ // Close audio context
934
  if (audioContext) {
935
  audioContext.close();
936
  audioContext = null;
 
938
  audioSource = null;
939
  }
940
 
941
+ // Close data channel
942
  if (dataChannel) {
943
  dataChannel.close();
944
  dataChannel = null;
945
  }
946
 
947
+ // Close peer connection
948
  if (peerConnection) {
949
  console.log('[STOP] Current connection state:', peerConnection.connectionState);
950
 
951
+ // Stop all transceivers
952
  if (peerConnection.getTransceivers) {
953
  peerConnection.getTransceivers().forEach(transceiver => {
954
  if (transceiver.stop) {
 
957
  });
958
  }
959
 
960
+ // Stop all senders
961
  if (peerConnection.getSenders) {
962
  peerConnection.getSenders().forEach(sender => {
963
  if (sender.track) {
 
966
  });
967
  }
968
 
969
+ // Stop all receivers
970
  if (peerConnection.getReceivers) {
971
  peerConnection.getReceivers().forEach(receiver => {
972
  if (receiver.track) {
 
975
  });
976
  }
977
 
978
+ // Close the connection
979
  peerConnection.close();
980
+
981
+ // Clear the reference
982
  peerConnection = null;
983
 
984
  console.log('[STOP] Connection closed');
985
  }
986
 
987
+ // Reset audio level
988
  audioLevel = 0;
989
  isVoiceActive = false;
990
+
991
+ // Update UI
992
  updateButtonState();
993
 
994
+ // Clear any existing webrtc_id
995
  if (webrtc_id) {
996
  console.log('[STOP] Clearing webrtc_id:', webrtc_id);
997
  webrtc_id = null;
998
  }
999
  }
 
1000
  startButton.addEventListener('click', () => {
1001
  console.log('clicked');
1002
  console.log(peerConnection, peerConnection?.connectionState);
 
1007
  stop();
1008
  }
1009
  });
1010
+
1011
+ // Initialize on page load
1012
+ window.addEventListener('DOMContentLoaded', () => {
1013
+ sendButton.style.display = 'block';
1014
+ startNewSession();
1015
+ loadHistory();
1016
+ });
1017
  </script>
1018
  </body>
1019
 
 
1061
  return []
1062
 
1063
 
1064
+ # Database helper class
1065
+ class ChatDatabase:
1066
+ """Database manager for chat history"""
1067
+
1068
+ @staticmethod
1069
+ async def init():
1070
+ """Initialize database tables"""
1071
+ async with aiosqlite.connect(DB_PATH) as db:
1072
+ await db.execute("""
1073
+ CREATE TABLE IF NOT EXISTS conversations (
1074
+ id TEXT PRIMARY KEY,
1075
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1076
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1077
+ summary TEXT
1078
+ )
1079
+ """)
1080
+
1081
+ await db.execute("""
1082
+ CREATE TABLE IF NOT EXISTS messages (
1083
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1084
+ session_id TEXT NOT NULL,
1085
+ role TEXT NOT NULL,
1086
+ content TEXT NOT NULL,
1087
+ detected_language TEXT,
1088
+ timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1089
+ FOREIGN KEY (session_id) REFERENCES conversations(id)
1090
+ )
1091
+ """)
1092
+
1093
+ await db.commit()
1094
+
1095
+ @staticmethod
1096
+ async def create_session(session_id: str):
1097
+ """Create a new conversation session"""
1098
+ async with aiosqlite.connect(DB_PATH) as db:
1099
+ await db.execute(
1100
+ "INSERT INTO conversations (id) VALUES (?)",
1101
+ (session_id,)
1102
+ )
1103
+ await db.commit()
1104
+
1105
+ @staticmethod
1106
+ async def save_message(session_id: str, role: str, content: str):
1107
+ """Save a message to the database"""
1108
+ # Detect language
1109
+ detected_language = None
1110
+ try:
1111
+ if content and len(content) > 10: # Only detect for substantial content
1112
+ detected_language = detect(content)
1113
+ except LangDetectException:
1114
+ pass
1115
+
1116
+ async with aiosqlite.connect(DB_PATH) as db:
1117
+ await db.execute(
1118
+ """INSERT INTO messages (session_id, role, content, detected_language)
1119
+ VALUES (?, ?, ?, ?)""",
1120
+ (session_id, role, content, detected_language)
1121
+ )
1122
+
1123
+ # Update conversation's updated_at timestamp
1124
+ await db.execute(
1125
+ "UPDATE conversations SET updated_at = CURRENT_TIMESTAMP WHERE id = ?",
1126
+ (session_id,)
1127
+ )
1128
+
1129
+ # Update conversation summary (use first user message as summary)
1130
+ if role == "user":
1131
+ cursor = await db.execute(
1132
+ "SELECT summary FROM conversations WHERE id = ?",
1133
+ (session_id,)
1134
+ )
1135
+ row = await cursor.fetchone()
1136
+ if row and not row[0]: # If no summary exists
1137
+ summary = content[:100] + "..." if len(content) > 100 else content
1138
+ await db.execute(
1139
+ "UPDATE conversations SET summary = ? WHERE id = ?",
1140
+ (summary, session_id)
1141
+ )
1142
+
1143
+ await db.commit()
1144
+
1145
+ @staticmethod
1146
+ async def get_recent_conversations(limit: int = 10):
1147
+ """Get recent conversations"""
1148
+ async with aiosqlite.connect(DB_PATH) as db:
1149
+ cursor = await db.execute(
1150
+ """SELECT id, created_at, summary
1151
+ FROM conversations
1152
+ ORDER BY updated_at DESC
1153
+ LIMIT ?""",
1154
+ (limit,)
1155
+ )
1156
+ rows = await cursor.fetchall()
1157
+ return [
1158
+ {
1159
+ "id": row[0],
1160
+ "created_at": row[1],
1161
+ "summary": row[2] or "새 대화"
1162
+ }
1163
+ for row in rows
1164
+ ]
1165
+
1166
+ @staticmethod
1167
+ async def get_conversation_messages(session_id: str):
1168
+ """Get all messages for a conversation"""
1169
+ async with aiosqlite.connect(DB_PATH) as db:
1170
+ cursor = await db.execute(
1171
+ """SELECT role, content, detected_language, timestamp
1172
+ FROM messages
1173
+ WHERE session_id = ?
1174
+ ORDER BY timestamp ASC""",
1175
+ (session_id,)
1176
+ )
1177
+ rows = await cursor.fetchall()
1178
+ return [
1179
+ {
1180
+ "role": row[0],
1181
+ "content": row[1],
1182
+ "detected_language": row[2],
1183
+ "timestamp": row[3]
1184
+ }
1185
+ for row in rows
1186
+ ]
1187
+
1188
+
1189
  # Initialize search client globally
1190
  brave_api_key = os.getenv("BSEARCH_API")
1191
  search_client = BraveSearchClient(brave_api_key) if brave_api_key else None
1192
+ print(f"Search client initialized: {search_client is not None}, API key present: {bool(brave_api_key)}")
1193
 
1194
  # Store connection settings
1195
  connection_settings = {}
 
1197
  # Initialize OpenAI client for text chat
1198
  client = openai.AsyncOpenAI()
1199
 
 
 
 
 
 
 
 
 
 
 
1200
 
1201
  def update_chatbot(chatbot: list[dict], response: ResponseAudioTranscriptDoneEvent):
1202
  chatbot.append({"role": "assistant", "content": response.transcript})
1203
  return chatbot
1204
 
1205
 
1206
+ async def process_text_chat(message: str, web_search_enabled: bool, system_prompt: str, session_id: str) -> Dict[str, str]:
 
1207
  """Process text chat using GPT-4o-mini model"""
1208
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1209
  messages = [
1210
+ {"role": "system", "content": system_prompt or "You are a helpful assistant."}
1211
  ]
1212
 
1213
  # Handle web search if enabled
1214
  if web_search_enabled and search_client:
1215
+ # Check if the message requires web search
1216
  search_keywords = ["날씨", "기온", "비", "눈", "뉴스", "소식", "현재", "최근",
1217
  "오늘", "지금", "가격", "환율", "주가", "weather", "news",
1218
  "current", "today", "price", "2024", "2025"]
 
1220
  should_search = any(keyword in message.lower() for keyword in search_keywords)
1221
 
1222
  if should_search:
1223
+ # Perform web search
1224
  search_results = await search_client.search(message)
1225
  if search_results:
1226
  search_context = "웹 검색 결과:\n\n"
 
1229
 
1230
  messages.append({
1231
  "role": "system",
1232
+ "content": "다음 웹 검색 결과를 참고하여 답변하세요:\n\n" + search_context
1233
  })
1234
 
1235
  messages.append({"role": "user", "content": message})
 
1244
 
1245
  response_text = response.choices[0].message.content
1246
 
1247
+ # Detect language
1248
+ detected_language = None
1249
+ try:
1250
+ if response_text and len(response_text) > 10:
1251
+ detected_language = detect(response_text)
1252
+ except:
1253
+ pass
1254
+
1255
+ # Save messages to database
1256
+ if session_id:
1257
+ await ChatDatabase.save_message(session_id, "user", message)
1258
+ await ChatDatabase.save_message(session_id, "assistant", response_text)
1259
 
1260
  return {
1261
  "response": response_text,
1262
+ "detected_language": detected_language
1263
  }
1264
 
1265
  except Exception as e:
 
1268
 
1269
 
1270
  class OpenAIHandler(AsyncStreamHandler):
1271
+ def __init__(self, web_search_enabled: bool = False, system_prompt: str = "",
1272
+ webrtc_id: str = None, session_id: str = None) -> None:
 
1273
  super().__init__(
1274
  expected_layout="mono",
1275
  output_sample_rate=SAMPLE_RATE,
 
1284
  self.current_call_id = None
1285
  self.webrtc_id = webrtc_id
1286
  self.web_search_enabled = web_search_enabled
 
1287
  self.system_prompt = system_prompt
1288
+ self.session_id = session_id
1289
+
1290
+ print(f"[INIT] Handler created with web_search={web_search_enabled}, session_id={session_id}")
1291
 
1292
  def copy(self):
1293
+ # Get the most recent settings
1294
  if connection_settings:
1295
+ # Get the most recent webrtc_id
1296
  recent_ids = sorted(connection_settings.keys(),
1297
  key=lambda k: connection_settings[k].get('timestamp', 0),
1298
  reverse=True)
 
1300
  recent_id = recent_ids[0]
1301
  settings = connection_settings[recent_id]
1302
 
1303
+ # Log the settings being copied
1304
+ print(f"[COPY] Copying settings from {recent_id}:")
1305
+
1306
  return OpenAIHandler(
1307
  web_search_enabled=settings.get('web_search_enabled', False),
 
1308
  system_prompt=settings.get('system_prompt', ''),
1309
  webrtc_id=recent_id,
1310
+ session_id=settings.get('session_id')
 
1311
  )
1312
 
1313
+ print(f"[COPY] No settings found, creating default handler")
1314
  return OpenAIHandler(web_search_enabled=False)
1315
 
1316
  async def search_web(self, query: str) -> str:
 
1323
  if not results:
1324
  return f"'{query}'에 대한 검색 결과를 찾을 수 없습니다."
1325
 
1326
+ # Format search results
1327
  formatted_results = []
1328
  for i, result in enumerate(results, 1):
1329
  formatted_results.append(
 
1348
 
1349
  async def start_up(self):
1350
  """Connect to realtime API"""
1351
+ # First check if we have the most recent settings
1352
  if connection_settings and self.webrtc_id:
1353
  if self.webrtc_id in connection_settings:
1354
  settings = connection_settings[self.webrtc_id]
1355
  self.web_search_enabled = settings.get('web_search_enabled', False)
 
1356
  self.system_prompt = settings.get('system_prompt', '')
1357
+ self.session_id = settings.get('session_id')
1358
+
1359
+ print(f"[START_UP] Updated settings from storage for {self.webrtc_id}")
1360
 
1361
  self.client = openai.AsyncOpenAI()
1362
 
1363
+ # Connect to Realtime API
1364
+ print(f"[REALTIME API] Connecting...")
1365
+
1366
+ # Define the web search function
1367
  tools = []
1368
+ base_instructions = self.system_prompt or "You are a helpful assistant."
1369
 
1370
  if self.web_search_enabled and self.search_client:
1371
  tools = [{
1372
  "type": "function",
1373
  "function": {
1374
  "name": "web_search",
1375
+ "description": "Search the web for current information. Use this for weather, news, prices, current events, or any time-sensitive topics.",
1376
  "parameters": {
1377
  "type": "object",
1378
  "properties": {
 
1385
  }
1386
  }
1387
  }]
1388
+ print("Web search function added to tools")
1389
+
1390
+ search_instructions = (
1391
+ "\n\nYou have web search capabilities. "
1392
+ "IMPORTANT: You MUST use the web_search function for ANY of these topics:\n"
1393
+ "- Weather (날씨, 기온, 비, 눈)\n"
1394
+ "- News (뉴스, 소식)\n"
1395
+ "- Current events (현재, 최근, 오늘, 지금)\n"
1396
+ "- Prices (가격, 환율, 주가)\n"
1397
+ "- Sports scores or results\n"
1398
+ "- Any question about 2024 or 2025\n"
1399
+ "- Any time-sensitive information\n\n"
1400
+ "When in doubt, USE web_search. It's better to search and provide accurate information "
1401
+ "than to guess or use outdated information."
1402
+ )
1403
+
1404
+ instructions = base_instructions + search_instructions
1405
+ else:
1406
+ instructions = base_instructions
1407
 
1408
  async with self.client.beta.realtime.connect(
1409
  model="gpt-4o-mini-realtime-preview-2024-12-17"
1410
  ) as conn:
1411
+ # Update session with tools
1412
  session_update = {
1413
  "turn_detection": {"type": "server_vad"},
1414
+ "instructions": instructions,
1415
  "tools": tools,
1416
  "tool_choice": "auto" if tools else "none",
1417
  "temperature": 0.7,
1418
  "max_response_output_tokens": 4096,
1419
  "modalities": ["text", "audio"],
1420
+ "voice": "alloy"
1421
  }
1422
 
1423
  await conn.session.update(session=session_update)
1424
  self.connection = conn
1425
+ print(f"Connected with tools: {len(tools)} functions")
1426
 
1427
  async for event in self.connection:
1428
+ # Debug logging for function calls
1429
+ if event.type.startswith("response.function_call"):
1430
+ print(f"Function event: {event.type}")
1431
+
1432
  if event.type == "response.audio_transcript.done":
1433
+ print(f"[RESPONSE] Transcript: {event.transcript[:100]}...")
1434
+
1435
+ # Detect language
1436
+ detected_language = None
1437
+ try:
1438
+ if event.transcript and len(event.transcript) > 10:
1439
+ detected_language = detect(event.transcript)
1440
+ except:
1441
+ pass
1442
+
1443
+ # Save to database
1444
+ if self.session_id:
1445
+ await ChatDatabase.save_message(self.session_id, "assistant", event.transcript)
1446
 
1447
  output_data = {
1448
  "event": event,
1449
+ "detected_language": detected_language
1450
  }
1451
  await self.output_queue.put(AdditionalOutputs(output_data))
1452
 
 
1462
 
1463
  # Handle function calls
1464
  elif event.type == "response.function_call_arguments.start":
1465
+ print(f"Function call started")
1466
  self.function_call_in_progress = True
1467
  self.current_function_args = ""
1468
  self.current_call_id = getattr(event, 'call_id', None)
 
1473
 
1474
  elif event.type == "response.function_call_arguments.done":
1475
  if self.function_call_in_progress:
1476
+ print(f"Function call done, args: {self.current_function_args}")
1477
  try:
1478
  args = json.loads(self.current_function_args)
1479
  query = args.get("query", "")
1480
 
1481
+ # Emit search event to client
1482
  await self.output_queue.put(AdditionalOutputs({
1483
  "type": "search",
1484
  "query": query
1485
  }))
1486
 
1487
+ # Perform the search
1488
  search_results = await self.search_web(query)
1489
+ print(f"Search results length: {len(search_results)}")
1490
 
1491
+ # Send function result back to the model
1492
  if self.connection and self.current_call_id:
1493
  await self.connection.conversation.item.create(
1494
  item={
 
1508
 
1509
  async def receive(self, frame: tuple[int, np.ndarray]) -> None:
1510
  if not self.connection:
1511
+ print(f"[RECEIVE] No connection, skipping")
1512
  return
1513
  try:
1514
  _, array = frame
 
1521
  async def emit(self) -> tuple[int, np.ndarray] | AdditionalOutputs | None:
1522
  item = await wait_for_item(self.output_queue)
1523
 
1524
+ # Check if it's a dict with text message
1525
  if isinstance(item, dict) and item.get('type') == 'text_message':
1526
  await self.process_text_message(item['content'])
1527
  return None
 
1529
  return item
1530
 
1531
  async def shutdown(self) -> None:
1532
+ print(f"[SHUTDOWN] Called")
1533
+
1534
  if self.connection:
1535
  await self.connection.close()
1536
  self.connection = None
1537
+ print("[REALTIME API] Connection closed")
1538
 
1539
 
1540
  # Create initial handler instance
 
1545
 
1546
  # Create stream with handler instance
1547
  stream = Stream(
1548
+ handler, # Pass instance, not factory
1549
  mode="send-receive",
1550
  modality="audio",
1551
  additional_inputs=[chatbot],
 
1558
 
1559
  app = FastAPI()
1560
 
1561
+ # Mount stream
1562
+ stream.mount(app)
1563
+
1564
  # Initialize database on startup
1565
  @app.on_event("startup")
1566
  async def startup_event():
1567
+ await ChatDatabase.init()
1568
+ print("Database initialized")
 
 
1569
 
1570
  # Intercept offer to capture settings
1571
  @app.post("/webrtc/offer", include_in_schema=False)
 
1575
 
1576
  webrtc_id = body.get("webrtc_id")
1577
  web_search_enabled = body.get("web_search_enabled", False)
 
1578
  system_prompt = body.get("system_prompt", "")
1579
+ session_id = body.get("session_id")
1580
+
1581
+ print(f"[OFFER] Received offer with webrtc_id: {webrtc_id}")
1582
+ print(f"[OFFER] web_search_enabled: {web_search_enabled}")
1583
+ print(f"[OFFER] session_id: {session_id}")
1584
 
1585
+ # Store settings with timestamp
1586
  if webrtc_id:
1587
  connection_settings[webrtc_id] = {
1588
  'web_search_enabled': web_search_enabled,
 
1589
  'system_prompt': system_prompt,
1590
+ 'session_id': session_id,
 
1591
  'timestamp': asyncio.get_event_loop().time()
1592
  }
1593
+
1594
+ print(f"[OFFER] Stored settings for {webrtc_id}:")
1595
+ print(f"[OFFER] {connection_settings[webrtc_id]}")
1596
 
1597
  # Remove our custom route temporarily
1598
  custom_route = None
 
1601
  custom_route = app.routes.pop(i)
1602
  break
1603
 
1604
+ # Forward to stream's offer handler
1605
+ print(f"[OFFER] Forwarding to stream.offer()")
1606
  response = await stream.offer(body)
1607
 
1608
+ # Re-add our custom route
1609
  if custom_route:
1610
  app.routes.insert(0, custom_route)
1611
 
1612
+ print(f"[OFFER] Response status: {response.get('status', 'unknown') if isinstance(response, dict) else 'OK'}")
1613
+
1614
  return response
1615
 
1616
 
1617
+ @app.post("/session/new")
1618
+ async def create_new_session():
1619
+ """Create a new chat session"""
1620
+ session_id = str(uuid.uuid4())
1621
+ await ChatDatabase.create_session(session_id)
1622
+ return {"session_id": session_id}
1623
+
1624
+
1625
+ @app.post("/message/save")
1626
+ async def save_message(request: Request):
1627
+ """Save a message to the database"""
1628
+ body = await request.json()
1629
+ session_id = body.get("session_id")
1630
+ role = body.get("role")
1631
+ content = body.get("content")
1632
+
1633
+ if not all([session_id, role, content]):
1634
+ return {"error": "Missing required fields"}
1635
+
1636
+ await ChatDatabase.save_message(session_id, role, content)
1637
+ return {"status": "ok"}
1638
+
1639
+
1640
+ @app.get("/history/recent")
1641
+ async def get_recent_history():
1642
+ """Get recent conversation history"""
1643
+ conversations = await ChatDatabase.get_recent_conversations()
1644
+ return conversations
1645
+
1646
+
1647
+ @app.get("/history/{session_id}")
1648
+ async def get_conversation(session_id: str):
1649
+ """Get messages for a specific conversation"""
1650
+ messages = await ChatDatabase.get_conversation_messages(session_id)
1651
+ return messages
1652
+
1653
+
1654
  @app.post("/chat/text")
1655
  async def chat_text(request: Request):
1656
  """Handle text chat messages using GPT-4o-mini"""
 
1658
  body = await request.json()
1659
  message = body.get("message", "")
1660
  web_search_enabled = body.get("web_search_enabled", False)
 
1661
  system_prompt = body.get("system_prompt", "")
1662
+ session_id = body.get("session_id")
 
1663
 
1664
  if not message:
1665
  return {"error": "메시지가 비어있습니다."}
1666
 
1667
+ # Process text chat
1668
+ result = await process_text_chat(message, web_search_enabled, system_prompt, session_id)
1669
 
1670
  return result
1671
 
 
1674
  return {"error": "채팅 처리 중 오류가 발생했습니다."}
1675
 
1676
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1677
  @app.post("/text_message/{webrtc_id}")
1678
  async def receive_text_message(webrtc_id: str, request: Request):
1679
  """Receive text message from client"""
1680
  body = await request.json()
1681
  message = body.get("content", "")
1682
 
1683
+ # Find the handler for this connection
1684
  if webrtc_id in stream.handlers:
1685
  handler = stream.handlers[webrtc_id]
1686
+ # Queue the text message for processing
1687
  await handler.output_queue.put({
1688
  'type': 'text_message',
1689
  'content': message
 
1698
  async def output_stream():
1699
  async for output in stream.output_stream(webrtc_id):
1700
  if hasattr(output, 'args') and output.args:
1701
+ # Check if it's a search event
1702
  if isinstance(output.args[0], dict) and output.args[0].get('type') == 'search':
1703
  yield f"event: search\ndata: {json.dumps(output.args[0])}\n\n"
1704
+ # Regular transcript event with language info
1705
  elif isinstance(output.args[0], dict) and 'event' in output.args[0]:
1706
  event_data = output.args[0]
1707
  if 'event' in event_data and hasattr(event_data['event'], 'transcript'):
1708
  data = {
1709
  "role": "assistant",
1710
  "content": event_data['event'].transcript,
1711
+ "detected_language": event_data.get('detected_language')
1712
  }
1713
  yield f"event: output\ndata: {json.dumps(data)}\n\n"
1714