seawolf2357 commited on
Commit
e32f3fb
·
verified ·
1 Parent(s): c90fc65

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +439 -133
app.py CHANGED
@@ -31,7 +31,11 @@ import uuid
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>
@@ -40,7 +44,7 @@ HTML_CONTENT = """<!DOCTYPE html>
40
  <head>
41
  <meta charset="UTF-8">
42
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
43
- <title>Mouth of 'MOUSE'</title>
44
  <style>
45
  :root {
46
  --primary-color: #6f42c1;
@@ -50,6 +54,7 @@ HTML_CONTENT = """<!DOCTYPE html>
50
  --text-color: #f8f9fa;
51
  --border-color: #333;
52
  --hover-color: #8a5cf6;
 
53
  }
54
  body {
55
  font-family: "SF Pro Display", -apple-system, BlinkMacSystemFont, sans-serif;
@@ -126,7 +131,6 @@ HTML_CONTENT = """<!DOCTYPE html>
126
  padding: 20px;
127
  border: 1px solid var(--border-color);
128
  overflow-y: auto;
129
- flex-grow: 1;
130
  }
131
  .settings-grid {
132
  display: flex;
@@ -171,14 +175,40 @@ HTML_CONTENT = """<!DOCTYPE html>
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 {
@@ -292,6 +322,13 @@ HTML_CONTENT = """<!DOCTYPE html>
292
  padding: 10px;
293
  margin-bottom: 10px;
294
  }
 
 
 
 
 
 
 
295
  .language-info {
296
  font-size: 12px;
297
  color: #888;
@@ -359,6 +396,14 @@ HTML_CONTENT = """<!DOCTYPE html>
359
  #send-button:hover {
360
  background: linear-gradient(135deg, #27ae60, #229954);
361
  }
 
 
 
 
 
 
 
 
362
  #audio-output {
363
  display: none;
364
  }
@@ -420,6 +465,10 @@ HTML_CONTENT = """<!DOCTYPE html>
420
  background-color: #ff9800;
421
  color: white;
422
  }
 
 
 
 
423
  .status-indicator {
424
  display: inline-flex;
425
  align-items: center;
@@ -454,34 +503,17 @@ HTML_CONTENT = """<!DOCTYPE html>
454
  opacity: 0.6;
455
  }
456
  }
457
- .mouse-logo {
458
- position: relative;
459
  width: 40px;
460
  height: 40px;
461
- }
462
- .mouse-ears {
463
- position: absolute;
464
- width: 15px;
465
- height: 15px;
466
- background-color: var(--primary-color);
467
- border-radius: 50%;
468
- }
469
- .mouse-ear-left {
470
- top: 0;
471
- left: 5px;
472
- }
473
- .mouse-ear-right {
474
- top: 0;
475
- right: 5px;
476
- }
477
- .mouse-face {
478
- position: absolute;
479
- top: 10px;
480
- left: 5px;
481
- width: 30px;
482
- height: 30px;
483
- background-color: var(--secondary-color);
484
  border-radius: 50%;
 
 
 
 
 
 
485
  }
486
  </style>
487
  </head>
@@ -491,12 +523,8 @@ HTML_CONTENT = """<!DOCTYPE html>
491
  <div class="container">
492
  <div class="header">
493
  <div class="logo">
494
- <div class="mouse-logo">
495
- <div class="mouse-ears mouse-ear-left"></div>
496
- <div class="mouse-ears mouse-ear-right"></div>
497
- <div class="mouse-face"></div>
498
- </div>
499
- <h1>MOUSE 음성 챗</h1>
500
  </div>
501
  <div class="status-indicator">
502
  <div id="status-dot" class="status-dot disconnected"></div>
@@ -517,11 +545,16 @@ HTML_CONTENT = """<!DOCTYPE html>
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>
@@ -529,6 +562,7 @@ HTML_CONTENT = """<!DOCTYPE html>
529
 
530
  <div class="controls">
531
  <button id="start-button">대화 시작</button>
 
532
  </div>
533
  </div>
534
 
@@ -552,24 +586,44 @@ HTML_CONTENT = """<!DOCTYPE html>
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');
560
  const chatMessages = document.getElementById('chat-messages');
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' });
@@ -577,6 +631,62 @@ HTML_CONTENT = """<!DOCTYPE html>
577
  currentSessionId = data.session_id;
578
  console.log('New session started:', currentSessionId);
579
  loadHistory();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
580
  }
581
 
582
  // Load conversation history
@@ -623,11 +733,6 @@ HTML_CONTENT = """<!DOCTYPE html>
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
632
  textInput.addEventListener('keypress', (e) => {
633
  if (e.key === 'Enter' && !e.shiftKey) {
@@ -637,6 +742,7 @@ HTML_CONTENT = """<!DOCTYPE html>
637
  });
638
 
639
  sendButton.addEventListener('click', sendTextMessage);
 
640
 
641
  async function sendTextMessage() {
642
  const message = textInput.value.trim();
@@ -662,8 +768,9 @@ HTML_CONTENT = """<!DOCTYPE html>
662
  body: JSON.stringify({
663
  message: message,
664
  web_search_enabled: webSearchEnabled,
665
- system_prompt: systemPrompt,
666
- session_id: currentSessionId
 
667
  })
668
  });
669
 
@@ -696,16 +803,34 @@ HTML_CONTENT = """<!DOCTYPE html>
696
  if (state === 'connected') {
697
  statusText.textContent = '연결됨';
698
  sendButton.style.display = 'block';
 
699
  isVoiceActive = true;
700
  } else if (state === 'connecting') {
701
  statusText.textContent = '연결 중...';
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,6 +860,7 @@ HTML_CONTENT = """<!DOCTYPE html>
735
  updateStatus('disconnected');
736
  }
737
  }
 
738
  function setupAudioVisualization(stream) {
739
  audioContext = new (window.AudioContext || window.webkitAudioContext)();
740
  analyser = audioContext.createAnalyser();
@@ -769,27 +895,14 @@ HTML_CONTENT = """<!DOCTYPE html>
769
 
770
  updateAudioLevel();
771
  }
772
- function showError(message) {
773
- const toast = document.getElementById('error-toast');
774
- toast.textContent = message;
775
- toast.className = 'toast error';
776
- toast.style.display = 'block';
777
- setTimeout(() => {
778
- toast.style.display = 'none';
779
- }, 5000);
780
- }
781
  async function setupWebRTC() {
782
  const config = __RTC_CONFIGURATION__;
783
  peerConnection = new RTCPeerConnection(config);
784
  const timeoutId = setTimeout(() => {
785
- const toast = document.getElementById('error-toast');
786
- toast.textContent = "연결이 평소보다 오래 걸리고 있습니다. VPN을 사용 중이신가요?";
787
- toast.className = 'toast warning';
788
- toast.style.display = 'block';
789
- setTimeout(() => {
790
- toast.style.display = 'none';
791
- }, 5000);
792
  }, 5000);
 
793
  try {
794
  const stream = await navigator.mediaDevices.getUserMedia({
795
  audio: true
@@ -832,6 +945,7 @@ HTML_CONTENT = """<!DOCTYPE html>
832
  peerConnection.addEventListener("icegatheringstatechange", checkState);
833
  }
834
  });
 
835
  peerConnection.addEventListener('connectionstatechange', () => {
836
  console.log('connectionstatechange', peerConnection.connectionState);
837
  if (peerConnection.connectionState === 'connected') {
@@ -841,15 +955,8 @@ HTML_CONTENT = """<!DOCTYPE html>
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',
@@ -859,10 +966,12 @@ HTML_CONTENT = """<!DOCTYPE html>
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,6 +980,7 @@ HTML_CONTENT = """<!DOCTYPE html>
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) => {
@@ -895,6 +1005,7 @@ HTML_CONTENT = """<!DOCTYPE html>
895
  stop();
896
  }
897
  }
 
898
  function addMessage(role, content, save = true) {
899
  const messageDiv = document.createElement('div');
900
  messageDiv.classList.add('message', role);
@@ -908,7 +1019,7 @@ HTML_CONTENT = """<!DOCTYPE html>
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' },
@@ -997,6 +1108,7 @@ HTML_CONTENT = """<!DOCTYPE html>
997
  webrtc_id = null;
998
  }
999
  }
 
1000
  startButton.addEventListener('click', () => {
1001
  console.log('clicked');
1002
  console.log(peerConnection, peerConnection?.connectionState);
@@ -1011,8 +1123,10 @@ HTML_CONTENT = """<!DOCTYPE html>
1011
  // Initialize on page load
1012
  window.addEventListener('DOMContentLoaded', () => {
1013
  sendButton.style.display = 'block';
 
1014
  startNewSession();
1015
  loadHistory();
 
1016
  });
1017
  </script>
1018
  </body>
@@ -1062,13 +1176,14 @@ class BraveSearchClient:
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,
@@ -1078,6 +1193,7 @@ class ChatDatabase:
1078
  )
1079
  """)
1080
 
 
1081
  await db.execute("""
1082
  CREATE TABLE IF NOT EXISTS messages (
1083
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -1090,6 +1206,24 @@ class ChatDatabase:
1090
  )
1091
  """)
1092
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1093
  await db.commit()
1094
 
1095
  @staticmethod
@@ -1108,7 +1242,7 @@ class ChatDatabase:
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
@@ -1133,7 +1267,7 @@ class ChatDatabase:
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 = ?",
@@ -1184,6 +1318,139 @@ class ChatDatabase:
1184
  }
1185
  for row in rows
1186
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1187
 
1188
 
1189
  # Initialize search client globally
@@ -1203,16 +1470,40 @@ def update_chatbot(chatbot: list[dict], response: ResponseAudioTranscriptDoneEve
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,7 +1511,6 @@ async def process_text_chat(message: str, web_search_enabled: bool, system_promp
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"
@@ -1254,8 +1544,8 @@ async def process_text_chat(message: str, web_search_enabled: bool, system_promp
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,
@@ -1268,8 +1558,8 @@ async def process_text_chat(message: str, web_search_enabled: bool, system_promp
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,15 +1574,14 @@ class OpenAIHandler(AsyncStreamHandler):
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,14 +1589,14 @@ class OpenAIHandler(AsyncStreamHandler):
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")
@@ -1323,7 +1612,6 @@ class OpenAIHandler(AsyncStreamHandler):
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,25 +1636,33 @@ class OpenAIHandler(AsyncStreamHandler):
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",
@@ -1385,20 +1681,10 @@ class OpenAIHandler(AsyncStreamHandler):
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
@@ -1408,7 +1694,6 @@ class OpenAIHandler(AsyncStreamHandler):
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,
@@ -1425,10 +1710,6 @@ class OpenAIHandler(AsyncStreamHandler):
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
 
@@ -1442,7 +1723,7 @@ class OpenAIHandler(AsyncStreamHandler):
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,
@@ -1521,7 +1802,6 @@ class OpenAIHandler(AsyncStreamHandler):
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
@@ -1545,7 +1825,7 @@ chatbot = gr.Chatbot(type="messages")
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],
@@ -1564,8 +1844,9 @@ stream.mount(app)
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,24 +1856,26 @@ async def custom_offer(request: Request):
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
@@ -1618,10 +1901,25 @@ async def custom_offer(request: Request):
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"""
@@ -1633,24 +1931,31 @@ async def save_message(request: Request):
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,14 +1963,15 @@ async def chat_text(request: Request):
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
 
 
31
  load_dotenv()
32
 
33
  SAMPLE_RATE = 24000
34
+
35
+ # Use Persistent Storage path if available (Hugging Face Space)
36
+ PERSISTENT_DIR = os.environ.get("PERSISTENT_DIR", "./data")
37
+ os.makedirs(PERSISTENT_DIR, exist_ok=True)
38
+ DB_PATH = os.path.join(PERSISTENT_DIR, "personal_assistant.db")
39
 
40
  # HTML content embedded as a string
41
  HTML_CONTENT = """<!DOCTYPE html>
 
44
  <head>
45
  <meta charset="UTF-8">
46
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
47
+ <title>Personal AI Assistant</title>
48
  <style>
49
  :root {
50
  --primary-color: #6f42c1;
 
54
  --text-color: #f8f9fa;
55
  --border-color: #333;
56
  --hover-color: #8a5cf6;
57
+ --memory-color: #4a9eff;
58
  }
59
  body {
60
  font-family: "SF Pro Display", -apple-system, BlinkMacSystemFont, sans-serif;
 
131
  padding: 20px;
132
  border: 1px solid var(--border-color);
133
  overflow-y: auto;
 
134
  }
135
  .settings-grid {
136
  display: flex;
 
175
  .toggle-switch.active .toggle-slider {
176
  transform: translateX(24px);
177
  }
178
+ /* Memory section */
179
+ .memory-section {
180
+ background-color: var(--card-bg);
181
+ border-radius: 12px;
182
+ padding: 20px;
183
+ border: 1px solid var(--border-color);
184
+ max-height: 300px;
185
+ overflow-y: auto;
186
+ }
187
+ .memory-item {
188
+ padding: 10px;
189
+ margin-bottom: 10px;
190
+ background: linear-gradient(135deg, rgba(74, 158, 255, 0.1), rgba(111, 66, 193, 0.1));
191
+ border-radius: 6px;
192
+ border-left: 3px solid var(--memory-color);
193
+ }
194
+ .memory-category {
195
+ font-size: 12px;
196
+ color: var(--memory-color);
197
+ font-weight: bold;
198
+ text-transform: uppercase;
199
+ margin-bottom: 5px;
200
+ }
201
+ .memory-content {
202
+ font-size: 14px;
203
+ color: var(--text-color);
204
+ }
205
  /* History section */
206
  .history-section {
207
  background-color: var(--card-bg);
208
  border-radius: 12px;
209
  padding: 20px;
210
  border: 1px solid var(--border-color);
211
+ max-height: 200px;
 
212
  overflow-y: auto;
213
  }
214
  .history-item {
 
322
  padding: 10px;
323
  margin-bottom: 10px;
324
  }
325
+ .message.memory-update {
326
+ background: linear-gradient(135deg, rgba(74, 158, 255, 0.2), rgba(111, 66, 193, 0.2));
327
+ font-size: 13px;
328
+ padding: 8px 12px;
329
+ margin-bottom: 10px;
330
+ border-left: 3px solid var(--memory-color);
331
+ }
332
  .language-info {
333
  font-size: 12px;
334
  color: #888;
 
396
  #send-button:hover {
397
  background: linear-gradient(135deg, #27ae60, #229954);
398
  }
399
+ #end-session-button {
400
+ background: linear-gradient(135deg, #e74c3c, #c0392b);
401
+ padding: 8px 16px;
402
+ font-size: 13px;
403
+ }
404
+ #end-session-button:hover {
405
+ background: linear-gradient(135deg, #c0392b, #a93226);
406
+ }
407
  #audio-output {
408
  display: none;
409
  }
 
465
  background-color: #ff9800;
466
  color: white;
467
  }
468
+ .toast.success {
469
+ background-color: #4caf50;
470
+ color: white;
471
+ }
472
  .status-indicator {
473
  display: inline-flex;
474
  align-items: center;
 
503
  opacity: 0.6;
504
  }
505
  }
506
+ .user-avatar {
 
507
  width: 40px;
508
  height: 40px;
509
+ background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
510
  border-radius: 50%;
511
+ display: flex;
512
+ align-items: center;
513
+ justify-content: center;
514
+ font-size: 20px;
515
+ font-weight: bold;
516
+ color: white;
517
  }
518
  </style>
519
  </head>
 
523
  <div class="container">
524
  <div class="header">
525
  <div class="logo">
526
+ <div class="user-avatar" id="user-avatar">👤</div>
527
+ <h1>Personal AI Assistant</h1>
 
 
 
 
528
  </div>
529
  <div class="status-indicator">
530
  <div id="status-dot" class="status-dot disconnected"></div>
 
545
  </div>
546
  </div>
547
  <div class="text-input-section">
548
+ <label for="user-name" class="setting-label">사용자 이름:</label>
549
+ <input type="text" id="user-name" placeholder="이름을 입력하세요..." />
550
  </div>
551
  </div>
552
 
553
+ <div class="memory-section">
554
+ <h3 style="margin: 0 0 15px 0; color: var(--memory-color);">기억된 정보</h3>
555
+ <div id="memory-list"></div>
556
+ </div>
557
+
558
  <div class="history-section">
559
  <h3 style="margin: 0 0 15px 0; color: var(--primary-color);">대화 기록</h3>
560
  <div id="history-list"></div>
 
562
 
563
  <div class="controls">
564
  <button id="start-button">대화 시작</button>
565
+ <button id="end-session-button" style="display: none;">세션 종료</button>
566
  </div>
567
  </div>
568
 
 
586
  let peerConnection;
587
  let webrtc_id;
588
  let webSearchEnabled = false;
 
589
  let currentSessionId = null;
590
+ let userName = localStorage.getItem('userName') || '';
591
+ let userMemories = {};
592
  const audioOutput = document.getElementById('audio-output');
593
  const startButton = document.getElementById('start-button');
594
+ const endSessionButton = document.getElementById('end-session-button');
595
  const sendButton = document.getElementById('send-button');
596
  const chatMessages = document.getElementById('chat-messages');
597
  const statusDot = document.getElementById('status-dot');
598
  const statusText = document.getElementById('status-text');
599
  const searchToggle = document.getElementById('search-toggle');
 
600
  const textInput = document.getElementById('text-input');
601
  const historyList = document.getElementById('history-list');
602
+ const memoryList = document.getElementById('memory-list');
603
+ const userNameInput = document.getElementById('user-name');
604
+ const userAvatar = document.getElementById('user-avatar');
605
  let audioLevel = 0;
606
  let animationFrame;
607
  let audioContext, analyser, audioSource;
608
  let dataChannel = null;
609
  let isVoiceActive = false;
610
 
611
+ // Initialize user name
612
+ userNameInput.value = userName;
613
+ if (userName) {
614
+ userAvatar.textContent = userName.charAt(0).toUpperCase();
615
+ }
616
+
617
+ userNameInput.addEventListener('input', () => {
618
+ userName = userNameInput.value;
619
+ localStorage.setItem('userName', userName);
620
+ if (userName) {
621
+ userAvatar.textContent = userName.charAt(0).toUpperCase();
622
+ } else {
623
+ userAvatar.textContent = '👤';
624
+ }
625
+ });
626
+
627
  // Start new session
628
  async function startNewSession() {
629
  const response = await fetch('/session/new', { method: 'POST' });
 
631
  currentSessionId = data.session_id;
632
  console.log('New session started:', currentSessionId);
633
  loadHistory();
634
+ loadMemories();
635
+ }
636
+
637
+ // Load memories
638
+ async function loadMemories() {
639
+ try {
640
+ const response = await fetch('/memory/all');
641
+ const memories = await response.json();
642
+
643
+ userMemories = {};
644
+ memoryList.innerHTML = '';
645
+
646
+ memories.forEach(memory => {
647
+ if (!userMemories[memory.category]) {
648
+ userMemories[memory.category] = [];
649
+ }
650
+ userMemories[memory.category].push(memory.content);
651
+
652
+ const item = document.createElement('div');
653
+ item.className = 'memory-item';
654
+ item.innerHTML = `
655
+ <div class="memory-category">${memory.category}</div>
656
+ <div class="memory-content">${memory.content}</div>
657
+ `;
658
+ memoryList.appendChild(item);
659
+ });
660
+
661
+ console.log('Loaded memories:', userMemories);
662
+ } catch (error) {
663
+ console.error('Failed to load memories:', error);
664
+ }
665
+ }
666
+
667
+ // End session and update memories
668
+ async function endSession() {
669
+ if (!currentSessionId) return;
670
+
671
+ try {
672
+ addMessage('memory-update', '대화 내용을 분석하여 기억을 업데이트하고 있습니다...');
673
+
674
+ const response = await fetch('/session/end', {
675
+ method: 'POST',
676
+ headers: { 'Content-Type': 'application/json' },
677
+ body: JSON.stringify({ session_id: currentSessionId })
678
+ });
679
+
680
+ const result = await response.json();
681
+ if (result.status === 'ok') {
682
+ showToast('기억이 성공적으로 업데이트되었습니다.', 'success');
683
+ loadMemories();
684
+ startNewSession();
685
+ }
686
+ } catch (error) {
687
+ console.error('Failed to end session:', error);
688
+ showError('세션 종료 중 오류가 발생했습니다.');
689
+ }
690
  }
691
 
692
  // Load conversation history
 
733
  console.log('Web search enabled:', webSearchEnabled);
734
  });
735
 
 
 
 
 
 
736
  // Text input handling
737
  textInput.addEventListener('keypress', (e) => {
738
  if (e.key === 'Enter' && !e.shiftKey) {
 
742
  });
743
 
744
  sendButton.addEventListener('click', sendTextMessage);
745
+ endSessionButton.addEventListener('click', endSession);
746
 
747
  async function sendTextMessage() {
748
  const message = textInput.value.trim();
 
768
  body: JSON.stringify({
769
  message: message,
770
  web_search_enabled: webSearchEnabled,
771
+ session_id: currentSessionId,
772
+ user_name: userName,
773
+ memories: userMemories
774
  })
775
  });
776
 
 
803
  if (state === 'connected') {
804
  statusText.textContent = '연결됨';
805
  sendButton.style.display = 'block';
806
+ endSessionButton.style.display = 'block';
807
  isVoiceActive = true;
808
  } else if (state === 'connecting') {
809
  statusText.textContent = '연결 중...';
810
  sendButton.style.display = 'none';
811
+ endSessionButton.style.display = 'none';
812
  } else {
813
  statusText.textContent = '연결 대기 중';
814
+ sendButton.style.display = 'block';
815
+ endSessionButton.style.display = 'block';
816
  isVoiceActive = false;
817
  }
818
  }
819
+
820
+ function showToast(message, type = 'info') {
821
+ const toast = document.getElementById('error-toast');
822
+ toast.textContent = message;
823
+ toast.className = `toast ${type}`;
824
+ toast.style.display = 'block';
825
+ setTimeout(() => {
826
+ toast.style.display = 'none';
827
+ }, 5000);
828
+ }
829
+
830
+ function showError(message) {
831
+ showToast(message, 'error');
832
+ }
833
+
834
  function updateButtonState() {
835
  const button = document.getElementById('start-button');
836
  if (peerConnection && (peerConnection.connectionState === 'connecting' || peerConnection.connectionState === 'new')) {
 
860
  updateStatus('disconnected');
861
  }
862
  }
863
+
864
  function setupAudioVisualization(stream) {
865
  audioContext = new (window.AudioContext || window.webkitAudioContext)();
866
  analyser = audioContext.createAnalyser();
 
895
 
896
  updateAudioLevel();
897
  }
898
+
 
 
 
 
 
 
 
 
899
  async function setupWebRTC() {
900
  const config = __RTC_CONFIGURATION__;
901
  peerConnection = new RTCPeerConnection(config);
902
  const timeoutId = setTimeout(() => {
903
+ showToast("연결이 평소보다 오래 걸리고 있습니다. VPN을 사용 중이신가요?", 'warning');
 
 
 
 
 
 
904
  }, 5000);
905
+
906
  try {
907
  const stream = await navigator.mediaDevices.getUserMedia({
908
  audio: true
 
945
  peerConnection.addEventListener("icegatheringstatechange", checkState);
946
  }
947
  });
948
+
949
  peerConnection.addEventListener('connectionstatechange', () => {
950
  console.log('connectionstatechange', peerConnection.connectionState);
951
  if (peerConnection.connectionState === 'connected') {
 
955
  }
956
  updateButtonState();
957
  });
 
958
 
959
+ webrtc_id = Math.random().toString(36).substring(7);
 
 
 
 
 
 
960
 
961
  const response = await fetch('/webrtc/offer', {
962
  method: 'POST',
 
966
  type: peerConnection.localDescription.type,
967
  webrtc_id: webrtc_id,
968
  web_search_enabled: webSearchEnabled,
969
+ session_id: currentSessionId,
970
+ user_name: userName,
971
+ memories: userMemories
972
  })
973
  });
974
+
975
  const serverResponse = await response.json();
976
  if (serverResponse.status === 'failed') {
977
  showError(serverResponse.meta.error === 'concurrency_limit_reached'
 
980
  stop();
981
  return;
982
  }
983
+
984
  await peerConnection.setRemoteDescription(serverResponse);
985
  const eventSource = new EventSource('/outputs?webrtc_id=' + webrtc_id);
986
  eventSource.addEventListener("output", (event) => {
 
1005
  stop();
1006
  }
1007
  }
1008
+
1009
  function addMessage(role, content, save = true) {
1010
  const messageDiv = document.createElement('div');
1011
  messageDiv.classList.add('message', role);
 
1019
  chatMessages.scrollTop = chatMessages.scrollHeight;
1020
 
1021
  // Save message to database if save flag is true
1022
+ if (save && currentSessionId && role !== 'memory-update' && role !== 'search-result') {
1023
  fetch('/message/save', {
1024
  method: 'POST',
1025
  headers: { 'Content-Type': 'application/json' },
 
1108
  webrtc_id = null;
1109
  }
1110
  }
1111
+
1112
  startButton.addEventListener('click', () => {
1113
  console.log('clicked');
1114
  console.log(peerConnection, peerConnection?.connectionState);
 
1123
  // Initialize on page load
1124
  window.addEventListener('DOMContentLoaded', () => {
1125
  sendButton.style.display = 'block';
1126
+ endSessionButton.style.display = 'block';
1127
  startNewSession();
1128
  loadHistory();
1129
+ loadMemories();
1130
  });
1131
  </script>
1132
  </body>
 
1176
 
1177
 
1178
  # Database helper class
1179
+ class PersonalAssistantDB:
1180
+ """Database manager for personal assistant"""
1181
 
1182
  @staticmethod
1183
  async def init():
1184
  """Initialize database tables"""
1185
  async with aiosqlite.connect(DB_PATH) as db:
1186
+ # Conversations table
1187
  await db.execute("""
1188
  CREATE TABLE IF NOT EXISTS conversations (
1189
  id TEXT PRIMARY KEY,
 
1193
  )
1194
  """)
1195
 
1196
+ # Messages table
1197
  await db.execute("""
1198
  CREATE TABLE IF NOT EXISTS messages (
1199
  id INTEGER PRIMARY KEY AUTOINCREMENT,
 
1206
  )
1207
  """)
1208
 
1209
+ # User memories table - stores personal information
1210
+ await db.execute("""
1211
+ CREATE TABLE IF NOT EXISTS user_memories (
1212
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1213
+ category TEXT NOT NULL,
1214
+ content TEXT NOT NULL,
1215
+ confidence REAL DEFAULT 1.0,
1216
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1217
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1218
+ source_session_id TEXT,
1219
+ FOREIGN KEY (source_session_id) REFERENCES conversations(id)
1220
+ )
1221
+ """)
1222
+
1223
+ # Create indexes for better performance
1224
+ await db.execute("CREATE INDEX IF NOT EXISTS idx_memories_category ON user_memories(category)")
1225
+ await db.execute("CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id)")
1226
+
1227
  await db.commit()
1228
 
1229
  @staticmethod
 
1242
  # Detect language
1243
  detected_language = None
1244
  try:
1245
+ if content and len(content) > 10:
1246
  detected_language = detect(content)
1247
  except LangDetectException:
1248
  pass
 
1267
  (session_id,)
1268
  )
1269
  row = await cursor.fetchone()
1270
+ if row and not row[0]:
1271
  summary = content[:100] + "..." if len(content) > 100 else content
1272
  await db.execute(
1273
  "UPDATE conversations SET summary = ? WHERE id = ?",
 
1318
  }
1319
  for row in rows
1320
  ]
1321
+
1322
+ @staticmethod
1323
+ async def save_memory(category: str, content: str, session_id: str = None, confidence: float = 1.0):
1324
+ """Save or update a user memory"""
1325
+ async with aiosqlite.connect(DB_PATH) as db:
1326
+ # Check if similar memory exists
1327
+ cursor = await db.execute(
1328
+ """SELECT id, content FROM user_memories
1329
+ WHERE category = ? AND content LIKE ?
1330
+ LIMIT 1""",
1331
+ (category, f"%{content[:20]}%")
1332
+ )
1333
+ existing = await cursor.fetchone()
1334
+
1335
+ if existing:
1336
+ # Update existing memory
1337
+ await db.execute(
1338
+ """UPDATE user_memories
1339
+ SET content = ?, confidence = ?, updated_at = CURRENT_TIMESTAMP,
1340
+ source_session_id = ?
1341
+ WHERE id = ?""",
1342
+ (content, confidence, session_id, existing[0])
1343
+ )
1344
+ else:
1345
+ # Insert new memory
1346
+ await db.execute(
1347
+ """INSERT INTO user_memories (category, content, confidence, source_session_id)
1348
+ VALUES (?, ?, ?, ?)""",
1349
+ (category, content, confidence, session_id)
1350
+ )
1351
+
1352
+ await db.commit()
1353
+
1354
+ @staticmethod
1355
+ async def get_all_memories():
1356
+ """Get all user memories"""
1357
+ async with aiosqlite.connect(DB_PATH) as db:
1358
+ cursor = await db.execute(
1359
+ """SELECT category, content, confidence, updated_at
1360
+ FROM user_memories
1361
+ ORDER BY category, updated_at DESC"""
1362
+ )
1363
+ rows = await cursor.fetchall()
1364
+ return [
1365
+ {
1366
+ "category": row[0],
1367
+ "content": row[1],
1368
+ "confidence": row[2],
1369
+ "updated_at": row[3]
1370
+ }
1371
+ for row in rows
1372
+ ]
1373
+
1374
+ @staticmethod
1375
+ async def extract_and_save_memories(session_id: str):
1376
+ """Extract memories from conversation and save them"""
1377
+ # Get all messages from the session
1378
+ messages = await PersonalAssistantDB.get_conversation_messages(session_id)
1379
+
1380
+ if not messages:
1381
+ return
1382
+
1383
+ # Prepare conversation text for analysis
1384
+ conversation_text = "\n".join([
1385
+ f"{msg['role']}: {msg['content']}"
1386
+ for msg in messages
1387
+ ])
1388
+
1389
+ # Use GPT to extract memories
1390
+ client = openai.AsyncOpenAI()
1391
+
1392
+ try:
1393
+ response = await client.chat.completions.create(
1394
+ model="gpt-4.1-mini",
1395
+ messages=[
1396
+ {
1397
+ "role": "system",
1398
+ "content": """You are a memory extraction system. Extract personal information from conversations.
1399
+
1400
+ Categories to extract:
1401
+ - personal_info: 이름, 나이, 성별, 직업, 거주지
1402
+ - preferences: 좋아하는 것, 싫어하는 것, 취향
1403
+ - important_dates: 생일, 기념일, 중요한 날짜
1404
+ - relationships: 가족, 친구, 동료 관계
1405
+ - hobbies: 취미, 관심사
1406
+ - health: 건강 상태, 알레르기, 의료 정보
1407
+ - goals: 목표, 계획, 꿈
1408
+ - routines: 일상, 습관, 루틴
1409
+ - work: 직장, 업무, 프로젝트
1410
+ - education: 학력, 전공, 학습
1411
+
1412
+ Return as JSON array with format:
1413
+ [
1414
+ {
1415
+ "category": "category_name",
1416
+ "content": "extracted information in Korean",
1417
+ "confidence": 0.0-1.0
1418
+ }
1419
+ ]
1420
+
1421
+ Only extract clear, factual information. Do not make assumptions."""
1422
+ },
1423
+ {
1424
+ "role": "user",
1425
+ "content": f"Extract memories from this conversation:\n\n{conversation_text}"
1426
+ }
1427
+ ],
1428
+ temperature=0.3,
1429
+ max_tokens=2000
1430
+ )
1431
+
1432
+ # Parse and save memories
1433
+ memories_text = response.choices[0].message.content
1434
+
1435
+ # Extract JSON from response
1436
+ import re
1437
+ json_match = re.search(r'\[.*\]', memories_text, re.DOTALL)
1438
+ if json_match:
1439
+ memories = json.loads(json_match.group())
1440
+
1441
+ for memory in memories:
1442
+ if memory.get('content') and len(memory['content']) > 5:
1443
+ await PersonalAssistantDB.save_memory(
1444
+ category=memory.get('category', 'general'),
1445
+ content=memory['content'],
1446
+ session_id=session_id,
1447
+ confidence=memory.get('confidence', 0.8)
1448
+ )
1449
+
1450
+ print(f"Extracted and saved {len(memories)} memories from session {session_id}")
1451
+
1452
+ except Exception as e:
1453
+ print(f"Error extracting memories: {e}")
1454
 
1455
 
1456
  # Initialize search client globally
 
1470
  return chatbot
1471
 
1472
 
1473
+ def format_memories_for_prompt(memories: Dict[str, List[str]]) -> str:
1474
+ """Format memories for inclusion in system prompt"""
1475
+ if not memories:
1476
+ return ""
1477
+
1478
+ memory_text = "\n\n=== 기억된 정보 ===\n"
1479
+ for category, items in memories.items():
1480
+ if items:
1481
+ memory_text += f"\n[{category}]\n"
1482
+ for item in items:
1483
+ memory_text += f"- {item}\n"
1484
+
1485
+ return memory_text
1486
+
1487
+
1488
+ async def process_text_chat(message: str, web_search_enabled: bool, session_id: str,
1489
+ user_name: str = "", memories: Dict = None) -> Dict[str, str]:
1490
  """Process text chat using GPT-4o-mini model"""
1491
  try:
1492
+ # Build system prompt with memories
1493
+ base_prompt = f"""You are a personal AI assistant for {user_name if user_name else 'the user'}.
1494
+ You remember all previous conversations and personal information about the user.
1495
+ Be friendly, helpful, and personalized in your responses.
1496
+ Always use the information you remember to make conversations more personal and relevant."""
1497
+
1498
+ # Add memories to prompt
1499
+ if memories:
1500
+ memory_text = format_memories_for_prompt(memories)
1501
+ base_prompt += memory_text
1502
+
1503
+ messages = [{"role": "system", "content": base_prompt}]
1504
 
1505
  # Handle web search if enabled
1506
  if web_search_enabled and search_client:
 
1507
  search_keywords = ["날씨", "기온", "비", "눈", "뉴스", "소식", "현재", "최근",
1508
  "오늘", "지금", "가격", "환율", "주가", "weather", "news",
1509
  "current", "today", "price", "2024", "2025"]
 
1511
  should_search = any(keyword in message.lower() for keyword in search_keywords)
1512
 
1513
  if should_search:
 
1514
  search_results = await search_client.search(message)
1515
  if search_results:
1516
  search_context = "웹 검색 결과:\n\n"
 
1544
 
1545
  # Save messages to database
1546
  if session_id:
1547
+ await PersonalAssistantDB.save_message(session_id, "user", message)
1548
+ await PersonalAssistantDB.save_message(session_id, "assistant", response_text)
1549
 
1550
  return {
1551
  "response": response_text,
 
1558
 
1559
 
1560
  class OpenAIHandler(AsyncStreamHandler):
1561
+ def __init__(self, web_search_enabled: bool = False, webrtc_id: str = None,
1562
+ session_id: str = None, user_name: str = "", memories: Dict = None) -> None:
1563
  super().__init__(
1564
  expected_layout="mono",
1565
  output_sample_rate=SAMPLE_RATE,
 
1574
  self.current_call_id = None
1575
  self.webrtc_id = webrtc_id
1576
  self.web_search_enabled = web_search_enabled
 
1577
  self.session_id = session_id
1578
+ self.user_name = user_name
1579
+ self.memories = memories or {}
1580
 
1581
+ print(f"[INIT] Handler created with web_search={web_search_enabled}, session_id={session_id}, user={user_name}")
1582
 
1583
  def copy(self):
 
1584
  if connection_settings:
 
1585
  recent_ids = sorted(connection_settings.keys(),
1586
  key=lambda k: connection_settings[k].get('timestamp', 0),
1587
  reverse=True)
 
1589
  recent_id = recent_ids[0]
1590
  settings = connection_settings[recent_id]
1591
 
 
1592
  print(f"[COPY] Copying settings from {recent_id}:")
1593
 
1594
  return OpenAIHandler(
1595
  web_search_enabled=settings.get('web_search_enabled', False),
 
1596
  webrtc_id=recent_id,
1597
+ session_id=settings.get('session_id'),
1598
+ user_name=settings.get('user_name', ''),
1599
+ memories=settings.get('memories', {})
1600
  )
1601
 
1602
  print(f"[COPY] No settings found, creating default handler")
 
1612
  if not results:
1613
  return f"'{query}'에 대한 검색 결과를 찾을 수 없습니다."
1614
 
 
1615
  formatted_results = []
1616
  for i, result in enumerate(results, 1):
1617
  formatted_results.append(
 
1636
 
1637
  async def start_up(self):
1638
  """Connect to realtime API"""
 
1639
  if connection_settings and self.webrtc_id:
1640
  if self.webrtc_id in connection_settings:
1641
  settings = connection_settings[self.webrtc_id]
1642
  self.web_search_enabled = settings.get('web_search_enabled', False)
 
1643
  self.session_id = settings.get('session_id')
1644
+ self.user_name = settings.get('user_name', '')
1645
+ self.memories = settings.get('memories', {})
1646
 
1647
  print(f"[START_UP] Updated settings from storage for {self.webrtc_id}")
1648
 
1649
  self.client = openai.AsyncOpenAI()
1650
 
 
1651
  print(f"[REALTIME API] Connecting...")
1652
 
1653
+ # Build system prompt with memories
1654
+ base_instructions = f"""You are a personal AI assistant for {self.user_name if self.user_name else 'the user'}.
1655
+ You remember all previous conversations and personal information about the user.
1656
+ Be friendly, helpful, and personalized in your responses.
1657
+ Always use the information you remember to make conversations more personal and relevant."""
1658
+
1659
+ # Add memories to prompt
1660
+ if self.memories:
1661
+ memory_text = format_memories_for_prompt(self.memories)
1662
+ base_instructions += memory_text
1663
+
1664
  # Define the web search function
1665
  tools = []
 
 
1666
  if self.web_search_enabled and self.search_client:
1667
  tools = [{
1668
  "type": "function",
 
1681
  }
1682
  }
1683
  }]
 
1684
 
1685
  search_instructions = (
1686
  "\n\nYou have web search capabilities. "
1687
+ "Use web_search for current information like weather, news, prices, etc."
 
 
 
 
 
 
 
 
 
1688
  )
1689
 
1690
  instructions = base_instructions + search_instructions
 
1694
  async with self.client.beta.realtime.connect(
1695
  model="gpt-4o-mini-realtime-preview-2024-12-17"
1696
  ) as conn:
 
1697
  session_update = {
1698
  "turn_detection": {"type": "server_vad"},
1699
  "instructions": instructions,
 
1710
  print(f"Connected with tools: {len(tools)} functions")
1711
 
1712
  async for event in self.connection:
 
 
 
 
1713
  if event.type == "response.audio_transcript.done":
1714
  print(f"[RESPONSE] Transcript: {event.transcript[:100]}...")
1715
 
 
1723
 
1724
  # Save to database
1725
  if self.session_id:
1726
+ await PersonalAssistantDB.save_message(self.session_id, "assistant", event.transcript)
1727
 
1728
  output_data = {
1729
  "event": event,
 
1802
  async def emit(self) -> tuple[int, np.ndarray] | AdditionalOutputs | None:
1803
  item = await wait_for_item(self.output_queue)
1804
 
 
1805
  if isinstance(item, dict) and item.get('type') == 'text_message':
1806
  await self.process_text_message(item['content'])
1807
  return None
 
1825
 
1826
  # Create stream with handler instance
1827
  stream = Stream(
1828
+ handler,
1829
  mode="send-receive",
1830
  modality="audio",
1831
  additional_inputs=[chatbot],
 
1844
  # Initialize database on startup
1845
  @app.on_event("startup")
1846
  async def startup_event():
1847
+ await PersonalAssistantDB.init()
1848
+ print(f"Database initialized at: {DB_PATH}")
1849
+ print(f"Persistent directory: {PERSISTENT_DIR}")
1850
 
1851
  # Intercept offer to capture settings
1852
  @app.post("/webrtc/offer", include_in_schema=False)
 
1856
 
1857
  webrtc_id = body.get("webrtc_id")
1858
  web_search_enabled = body.get("web_search_enabled", False)
 
1859
  session_id = body.get("session_id")
1860
+ user_name = body.get("user_name", "")
1861
+ memories = body.get("memories", {})
1862
 
1863
  print(f"[OFFER] Received offer with webrtc_id: {webrtc_id}")
1864
  print(f"[OFFER] web_search_enabled: {web_search_enabled}")
1865
  print(f"[OFFER] session_id: {session_id}")
1866
+ print(f"[OFFER] user_name: {user_name}")
1867
 
1868
  # Store settings with timestamp
1869
  if webrtc_id:
1870
  connection_settings[webrtc_id] = {
1871
  'web_search_enabled': web_search_enabled,
 
1872
  'session_id': session_id,
1873
+ 'user_name': user_name,
1874
+ 'memories': memories,
1875
  'timestamp': asyncio.get_event_loop().time()
1876
  }
1877
 
1878
+ print(f"[OFFER] Stored settings for {webrtc_id}")
 
1879
 
1880
  # Remove our custom route temporarily
1881
  custom_route = None
 
1901
  async def create_new_session():
1902
  """Create a new chat session"""
1903
  session_id = str(uuid.uuid4())
1904
+ await PersonalAssistantDB.create_session(session_id)
1905
  return {"session_id": session_id}
1906
 
1907
 
1908
+ @app.post("/session/end")
1909
+ async def end_session(request: Request):
1910
+ """End session and extract memories"""
1911
+ body = await request.json()
1912
+ session_id = body.get("session_id")
1913
+
1914
+ if not session_id:
1915
+ return {"error": "session_id required"}
1916
+
1917
+ # Extract and save memories from the conversation
1918
+ await PersonalAssistantDB.extract_and_save_memories(session_id)
1919
+
1920
+ return {"status": "ok"}
1921
+
1922
+
1923
  @app.post("/message/save")
1924
  async def save_message(request: Request):
1925
  """Save a message to the database"""
 
1931
  if not all([session_id, role, content]):
1932
  return {"error": "Missing required fields"}
1933
 
1934
+ await PersonalAssistantDB.save_message(session_id, role, content)
1935
  return {"status": "ok"}
1936
 
1937
 
1938
  @app.get("/history/recent")
1939
  async def get_recent_history():
1940
  """Get recent conversation history"""
1941
+ conversations = await PersonalAssistantDB.get_recent_conversations()
1942
  return conversations
1943
 
1944
 
1945
  @app.get("/history/{session_id}")
1946
  async def get_conversation(session_id: str):
1947
  """Get messages for a specific conversation"""
1948
+ messages = await PersonalAssistantDB.get_conversation_messages(session_id)
1949
  return messages
1950
 
1951
 
1952
+ @app.get("/memory/all")
1953
+ async def get_all_memories():
1954
+ """Get all user memories"""
1955
+ memories = await PersonalAssistantDB.get_all_memories()
1956
+ return memories
1957
+
1958
+
1959
  @app.post("/chat/text")
1960
  async def chat_text(request: Request):
1961
  """Handle text chat messages using GPT-4o-mini"""
 
1963
  body = await request.json()
1964
  message = body.get("message", "")
1965
  web_search_enabled = body.get("web_search_enabled", False)
 
1966
  session_id = body.get("session_id")
1967
+ user_name = body.get("user_name", "")
1968
+ memories = body.get("memories", {})
1969
 
1970
  if not message:
1971
  return {"error": "메시지가 비어있습니다."}
1972
 
1973
  # Process text chat
1974
+ result = await process_text_chat(message, web_search_enabled, session_id, user_name, memories)
1975
 
1976
  return result
1977