seawolf2357 commited on
Commit
0b89dc5
·
verified ·
1 Parent(s): 73eebb3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +486 -37
app.py CHANGED
@@ -6,7 +6,7 @@ import os
6
  import numpy as np
7
  import openai
8
  from dotenv import load_dotenv
9
- from fastapi import FastAPI, Request
10
  from fastapi.responses import HTMLResponse, StreamingResponse
11
  from fastrtc import (
12
  AdditionalOutputs,
@@ -27,6 +27,9 @@ import aiosqlite
27
  from langdetect import detect, LangDetectException
28
  from datetime import datetime
29
  import uuid
 
 
 
30
 
31
  load_dotenv()
32
 
@@ -41,8 +44,12 @@ else:
41
 
42
  os.makedirs(PERSISTENT_DIR, exist_ok=True)
43
  DB_PATH = os.path.join(PERSISTENT_DIR, "personal_assistant.db")
 
 
 
44
  print(f"Using persistent directory: {PERSISTENT_DIR}")
45
  print(f"Database path: {DB_PATH}")
 
46
 
47
  # HTML content embedded as a string
48
  HTML_CONTENT = """<!DOCTYPE html>
@@ -62,6 +69,7 @@ HTML_CONTENT = """<!DOCTYPE html>
62
  --border-color: #333;
63
  --hover-color: #8a5cf6;
64
  --memory-color: #4a9eff;
 
65
  }
66
  body {
67
  font-family: "SF Pro Display", -apple-system, BlinkMacSystemFont, sans-serif;
@@ -182,6 +190,101 @@ HTML_CONTENT = """<!DOCTYPE html>
182
  .toggle-switch.active .toggle-slider {
183
  transform: translateX(24px);
184
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  /* Memory section */
186
  .memory-section {
187
  background-color: var(--card-bg);
@@ -198,6 +301,10 @@ HTML_CONTENT = """<!DOCTYPE html>
198
  border-radius: 6px;
199
  border-left: 3px solid var(--memory-color);
200
  }
 
 
 
 
201
  .memory-category {
202
  font-size: 12px;
203
  color: var(--memory-color);
@@ -336,6 +443,13 @@ HTML_CONTENT = """<!DOCTYPE html>
336
  margin-bottom: 10px;
337
  border-left: 3px solid var(--memory-color);
338
  }
 
 
 
 
 
 
 
339
  .language-info {
340
  font-size: 12px;
341
  color: #888;
@@ -522,6 +636,9 @@ HTML_CONTENT = """<!DOCTYPE html>
522
  font-weight: bold;
523
  color: white;
524
  }
 
 
 
525
  </style>
526
  </head>
527
 
@@ -557,6 +674,20 @@ HTML_CONTENT = """<!DOCTYPE html>
557
  </div>
558
  </div>
559
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
560
  <div class="memory-section">
561
  <h3 style="margin: 0 0 15px 0; color: var(--memory-color);">기억된 정보</h3>
562
  <div id="memory-list"></div>
@@ -596,6 +727,8 @@ HTML_CONTENT = """<!DOCTYPE html>
596
  let currentSessionId = null;
597
  let userName = localStorage.getItem('userName') || '';
598
  let userMemories = {};
 
 
599
  const audioOutput = document.getElementById('audio-output');
600
  const startButton = document.getElementById('start-button');
601
  const endSessionButton = document.getElementById('end-session-button');
@@ -609,6 +742,10 @@ HTML_CONTENT = """<!DOCTYPE html>
609
  const memoryList = document.getElementById('memory-list');
610
  const userNameInput = document.getElementById('user-name');
611
  const userAvatar = document.getElementById('user-avatar');
 
 
 
 
612
  let audioLevel = 0;
613
  let animationFrame;
614
  let audioContext, analyser, audioSource;
@@ -631,6 +768,122 @@ HTML_CONTENT = """<!DOCTYPE html>
631
  }
632
  });
633
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
634
  // Start new session
635
  async function startNewSession() {
636
  const response = await fetch('/session/new', { method: 'POST' });
@@ -657,7 +910,7 @@ HTML_CONTENT = """<!DOCTYPE html>
657
  userMemories[memory.category].push(memory.content);
658
 
659
  const item = document.createElement('div');
660
- item.className = 'memory-item';
661
  item.innerHTML = `
662
  <div class="memory-category">${memory.category}</div>
663
  <div class="memory-content">${memory.content}</div>
@@ -688,6 +941,8 @@ HTML_CONTENT = """<!DOCTYPE html>
688
  if (result.status === 'ok') {
689
  showToast('기억이 성공적으로 업데이트되었습니다.', 'success');
690
  loadMemories();
 
 
691
  startNewSession();
692
  }
693
  } catch (error) {
@@ -784,7 +1039,8 @@ HTML_CONTENT = """<!DOCTYPE html>
784
  web_search_enabled: webSearchEnabled,
785
  session_id: currentSessionId,
786
  user_name: userName,
787
- memories: userMemories
 
788
  })
789
  });
790
 
@@ -982,7 +1238,8 @@ HTML_CONTENT = """<!DOCTYPE html>
982
  web_search_enabled: webSearchEnabled,
983
  session_id: currentSessionId,
984
  user_name: userName,
985
- memories: userMemories
 
986
  })
987
  });
988
 
@@ -1033,7 +1290,7 @@ HTML_CONTENT = """<!DOCTYPE html>
1033
  chatMessages.scrollTop = chatMessages.scrollHeight;
1034
 
1035
  // Save message to database if save flag is true
1036
- if (save && currentSessionId && role !== 'memory-update' && role !== 'search-result') {
1037
  fetch('/message/save', {
1038
  method: 'POST',
1039
  headers: { 'Content-Type': 'application/json' },
@@ -1230,6 +1487,8 @@ class PersonalAssistantDB:
1230
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1231
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1232
  source_session_id TEXT,
 
 
1233
  FOREIGN KEY (source_session_id) REFERENCES conversations(id)
1234
  )
1235
  """)
@@ -1237,6 +1496,7 @@ class PersonalAssistantDB:
1237
  # Create indexes for better performance
1238
  await db.execute("CREATE INDEX IF NOT EXISTS idx_memories_category ON user_memories(category)")
1239
  await db.execute("CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id)")
 
1240
 
1241
  await db.commit()
1242
 
@@ -1339,33 +1599,44 @@ class PersonalAssistantDB:
1339
  ]
1340
 
1341
  @staticmethod
1342
- async def save_memory(category: str, content: str, session_id: str = None, confidence: float = 1.0):
 
1343
  """Save or update a user memory"""
1344
  async with aiosqlite.connect(DB_PATH) as db:
1345
- # Check if similar memory exists
1346
- cursor = await db.execute(
1347
- """SELECT id, content FROM user_memories
1348
- WHERE category = ? AND content LIKE ?
1349
- LIMIT 1""",
1350
- (category, f"%{content[:20]}%")
1351
- )
1352
- existing = await cursor.fetchone()
1353
-
1354
- if existing:
1355
- # Update existing memory
1356
- await db.execute(
1357
- """UPDATE user_memories
1358
- SET content = ?, confidence = ?, updated_at = CURRENT_TIMESTAMP,
1359
- source_session_id = ?
1360
- WHERE id = ?""",
1361
- (content, confidence, session_id, existing[0])
1362
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1363
  else:
1364
- # Insert new memory
1365
  await db.execute(
1366
- """INSERT INTO user_memories (category, content, confidence, source_session_id)
1367
- VALUES (?, ?, ?, ?)""",
1368
- (category, content, confidence, session_id)
 
1369
  )
1370
 
1371
  await db.commit()
@@ -1375,9 +1646,9 @@ class PersonalAssistantDB:
1375
  """Get all user memories"""
1376
  async with aiosqlite.connect(DB_PATH) as db:
1377
  cursor = await db.execute(
1378
- """SELECT category, content, confidence, updated_at
1379
  FROM user_memories
1380
- ORDER BY category, updated_at DESC"""
1381
  )
1382
  rows = await cursor.fetchall()
1383
  return [
@@ -1385,7 +1656,9 @@ class PersonalAssistantDB:
1385
  "category": row[0],
1386
  "content": row[1],
1387
  "confidence": row[2],
1388
- "updated_at": row[3]
 
 
1389
  }
1390
  for row in rows
1391
  ]
@@ -1505,8 +1778,79 @@ def format_memories_for_prompt(memories: Dict[str, List[str]]) -> str:
1505
  return memory_text
1506
 
1507
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1508
  async def process_text_chat(message: str, web_search_enabled: bool, session_id: str,
1509
- user_name: str = "", memories: Dict = None) -> Dict[str, str]:
 
1510
  """Process text chat using GPT-4o-mini model"""
1511
  try:
1512
  # Check for empty or None message
@@ -1521,7 +1865,7 @@ async def process_text_chat(message: str, web_search_enabled: bool, session_id:
1521
  "detected_language": "ko"
1522
  }
1523
 
1524
- # Build system prompt with memories
1525
  base_prompt = f"""You are a personal AI assistant for {user_name if user_name else 'the user'}.
1526
  You remember all previous conversations and personal information about the user.
1527
  Be friendly, helpful, and personalized in your responses.
@@ -1533,6 +1877,11 @@ IMPORTANT: Give only ONE response. Do not repeat or give multiple answers."""
1533
  memory_text = format_memories_for_prompt(memories)
1534
  base_prompt += memory_text
1535
 
 
 
 
 
 
1536
  messages = [{"role": "system", "content": base_prompt}]
1537
 
1538
  # Handle web search if enabled
@@ -1592,7 +1941,8 @@ IMPORTANT: Give only ONE response. Do not repeat or give multiple answers."""
1592
 
1593
  class OpenAIHandler(AsyncStreamHandler):
1594
  def __init__(self, web_search_enabled: bool = False, webrtc_id: str = None,
1595
- session_id: str = None, user_name: str = "", memories: Dict = None) -> None:
 
1596
  super().__init__(
1597
  expected_layout="mono",
1598
  output_sample_rate=SAMPLE_RATE,
@@ -1610,10 +1960,11 @@ class OpenAIHandler(AsyncStreamHandler):
1610
  self.session_id = session_id
1611
  self.user_name = user_name
1612
  self.memories = memories or {}
 
1613
  self.is_responding = False # Track if already responding
1614
  self.should_stop = False # Track if conversation should stop
1615
 
1616
- print(f"[INIT] Handler created with web_search={web_search_enabled}, session_id={session_id}, user={user_name}")
1617
 
1618
  def copy(self):
1619
  if connection_settings:
@@ -1631,7 +1982,8 @@ class OpenAIHandler(AsyncStreamHandler):
1631
  webrtc_id=recent_id,
1632
  session_id=settings.get('session_id'),
1633
  user_name=settings.get('user_name', ''),
1634
- memories=settings.get('memories', {})
 
1635
  )
1636
 
1637
  print(f"[COPY] No settings found, creating default handler")
@@ -1678,6 +2030,7 @@ class OpenAIHandler(AsyncStreamHandler):
1678
  self.session_id = settings.get('session_id')
1679
  self.user_name = settings.get('user_name', '')
1680
  self.memories = settings.get('memories', {})
 
1681
 
1682
  print(f"[START_UP] Updated settings from storage for {self.webrtc_id}")
1683
 
@@ -1685,7 +2038,7 @@ class OpenAIHandler(AsyncStreamHandler):
1685
 
1686
  print(f"[REALTIME API] Connecting...")
1687
 
1688
- # Build system prompt with memories
1689
  base_instructions = f"""You are a personal AI assistant for {self.user_name if self.user_name else 'the user'}.
1690
  You remember all previous conversations and personal information about the user.
1691
  Be friendly, helpful, and personalized in your responses.
@@ -1697,6 +2050,11 @@ IMPORTANT: Give only ONE response per user input. Do not repeat yourself or give
1697
  memory_text = format_memories_for_prompt(self.memories)
1698
  base_instructions += memory_text
1699
 
 
 
 
 
 
1700
  # Define the web search function
1701
  tools = []
1702
  if self.web_search_enabled and self.search_client:
@@ -2000,11 +2358,13 @@ async def custom_offer(request: Request):
2000
  session_id = body.get("session_id")
2001
  user_name = body.get("user_name", "")
2002
  memories = body.get("memories", {})
 
2003
 
2004
  print(f"[OFFER] Received offer with webrtc_id: {webrtc_id}")
2005
  print(f"[OFFER] web_search_enabled: {web_search_enabled}")
2006
  print(f"[OFFER] session_id: {session_id}")
2007
  print(f"[OFFER] user_name: {user_name}")
 
2008
 
2009
  # Store settings with timestamp
2010
  if webrtc_id:
@@ -2013,6 +2373,7 @@ async def custom_offer(request: Request):
2013
  'session_id': session_id,
2014
  'user_name': user_name,
2015
  'memories': memories,
 
2016
  'timestamp': asyncio.get_event_loop().time()
2017
  }
2018
 
@@ -2038,6 +2399,92 @@ async def custom_offer(request: Request):
2038
  return response
2039
 
2040
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2041
  @app.post("/session/new")
2042
  async def create_new_session():
2043
  """Create a new chat session"""
@@ -2107,12 +2554,14 @@ async def chat_text(request: Request):
2107
  session_id = body.get("session_id")
2108
  user_name = body.get("user_name", "")
2109
  memories = body.get("memories", {})
 
2110
 
2111
  if not message:
2112
  return {"error": "메시지가 비어있습니다."}
2113
 
2114
  # Process text chat
2115
- result = await process_text_chat(message, web_search_enabled, session_id, user_name, memories)
 
2116
 
2117
  return result
2118
 
 
6
  import numpy as np
7
  import openai
8
  from dotenv import load_dotenv
9
+ from fastapi import FastAPI, Request, UploadFile, File, Form
10
  from fastapi.responses import HTMLResponse, StreamingResponse
11
  from fastrtc import (
12
  AdditionalOutputs,
 
27
  from langdetect import detect, LangDetectException
28
  from datetime import datetime
29
  import uuid
30
+ import PyPDF2
31
+ import pandas as pd
32
+ import chardet
33
 
34
  load_dotenv()
35
 
 
44
 
45
  os.makedirs(PERSISTENT_DIR, exist_ok=True)
46
  DB_PATH = os.path.join(PERSISTENT_DIR, "personal_assistant.db")
47
+ UPLOAD_DIR = os.path.join(PERSISTENT_DIR, "uploads")
48
+ os.makedirs(UPLOAD_DIR, exist_ok=True)
49
+
50
  print(f"Using persistent directory: {PERSISTENT_DIR}")
51
  print(f"Database path: {DB_PATH}")
52
+ print(f"Upload directory: {UPLOAD_DIR}")
53
 
54
  # HTML content embedded as a string
55
  HTML_CONTENT = """<!DOCTYPE html>
 
69
  --border-color: #333;
70
  --hover-color: #8a5cf6;
71
  --memory-color: #4a9eff;
72
+ --doc-color: #28a745;
73
  }
74
  body {
75
  font-family: "SF Pro Display", -apple-system, BlinkMacSystemFont, sans-serif;
 
190
  .toggle-switch.active .toggle-slider {
191
  transform: translateX(24px);
192
  }
193
+ /* Document upload section */
194
+ .document-section {
195
+ background-color: var(--card-bg);
196
+ border-radius: 12px;
197
+ padding: 20px;
198
+ border: 1px solid var(--border-color);
199
+ max-height: 300px;
200
+ overflow-y: auto;
201
+ }
202
+ .file-upload-area {
203
+ border: 2px dashed var(--border-color);
204
+ border-radius: 8px;
205
+ padding: 20px;
206
+ text-align: center;
207
+ cursor: pointer;
208
+ transition: all 0.3s;
209
+ margin-bottom: 15px;
210
+ }
211
+ .file-upload-area:hover {
212
+ border-color: var(--primary-color);
213
+ background-color: rgba(111, 66, 193, 0.1);
214
+ }
215
+ .file-upload-area.drag-over {
216
+ border-color: var(--primary-color);
217
+ background-color: rgba(111, 66, 193, 0.2);
218
+ }
219
+ .memory-type-selector {
220
+ display: flex;
221
+ gap: 10px;
222
+ margin-bottom: 15px;
223
+ }
224
+ .memory-type-btn {
225
+ flex: 1;
226
+ padding: 10px;
227
+ border: 1px solid var(--border-color);
228
+ background-color: var(--dark-bg);
229
+ color: var(--text-color);
230
+ border-radius: 6px;
231
+ cursor: pointer;
232
+ transition: all 0.3s;
233
+ }
234
+ .memory-type-btn.active {
235
+ background-color: var(--primary-color);
236
+ border-color: var(--primary-color);
237
+ }
238
+ .document-list {
239
+ display: flex;
240
+ flex-direction: column;
241
+ gap: 10px;
242
+ }
243
+ .document-item {
244
+ padding: 10px;
245
+ background-color: var(--dark-bg);
246
+ border-radius: 6px;
247
+ display: flex;
248
+ justify-content: space-between;
249
+ align-items: center;
250
+ }
251
+ .document-info {
252
+ display: flex;
253
+ align-items: center;
254
+ gap: 10px;
255
+ }
256
+ .document-icon {
257
+ width: 24px;
258
+ height: 24px;
259
+ display: flex;
260
+ align-items: center;
261
+ justify-content: center;
262
+ background-color: var(--doc-color);
263
+ color: white;
264
+ border-radius: 4px;
265
+ font-size: 12px;
266
+ font-weight: bold;
267
+ }
268
+ .document-name {
269
+ font-size: 14px;
270
+ }
271
+ .document-type {
272
+ font-size: 12px;
273
+ color: #888;
274
+ margin-left: 5px;
275
+ }
276
+ .remove-doc-btn {
277
+ background: none;
278
+ border: none;
279
+ color: #ff6b6b;
280
+ cursor: pointer;
281
+ padding: 5px;
282
+ border-radius: 4px;
283
+ transition: background-color 0.2s;
284
+ }
285
+ .remove-doc-btn:hover {
286
+ background-color: rgba(255, 107, 107, 0.2);
287
+ }
288
  /* Memory section */
289
  .memory-section {
290
  background-color: var(--card-bg);
 
301
  border-radius: 6px;
302
  border-left: 3px solid var(--memory-color);
303
  }
304
+ .memory-item.document {
305
+ background: linear-gradient(135deg, rgba(40, 167, 69, 0.1), rgba(111, 66, 193, 0.1));
306
+ border-left-color: var(--doc-color);
307
+ }
308
  .memory-category {
309
  font-size: 12px;
310
  color: var(--memory-color);
 
443
  margin-bottom: 10px;
444
  border-left: 3px solid var(--memory-color);
445
  }
446
+ .message.document-loaded {
447
+ background: linear-gradient(135deg, rgba(40, 167, 69, 0.2), rgba(111, 66, 193, 0.2));
448
+ font-size: 13px;
449
+ padding: 8px 12px;
450
+ margin-bottom: 10px;
451
+ border-left: 3px solid var(--doc-color);
452
+ }
453
  .language-info {
454
  font-size: 12px;
455
  color: #888;
 
636
  font-weight: bold;
637
  color: white;
638
  }
639
+ input[type="file"] {
640
+ display: none;
641
+ }
642
  </style>
643
  </head>
644
 
 
674
  </div>
675
  </div>
676
 
677
+ <div class="document-section">
678
+ <h3 style="margin: 0 0 15px 0; color: var(--doc-color);">문서 관리</h3>
679
+ <div class="file-upload-area" id="file-upload-area">
680
+ <p>📄 파일을 드래그하거나 클릭하여 업로드</p>
681
+ <p style="font-size: 12px; color: #888;">PDF, TXT, CSV 지원</p>
682
+ </div>
683
+ <input type="file" id="file-input" accept=".pdf,.txt,.csv" multiple />
684
+ <div class="memory-type-selector">
685
+ <button class="memory-type-btn active" data-type="temporary">단기 기억</button>
686
+ <button class="memory-type-btn" data-type="permanent">영구 기억</button>
687
+ </div>
688
+ <div class="document-list" id="document-list"></div>
689
+ </div>
690
+
691
  <div class="memory-section">
692
  <h3 style="margin: 0 0 15px 0; color: var(--memory-color);">기억된 정보</h3>
693
  <div id="memory-list"></div>
 
727
  let currentSessionId = null;
728
  let userName = localStorage.getItem('userName') || '';
729
  let userMemories = {};
730
+ let temporaryDocuments = {}; // 단기 기억 문서
731
+ let selectedMemoryType = 'temporary';
732
  const audioOutput = document.getElementById('audio-output');
733
  const startButton = document.getElementById('start-button');
734
  const endSessionButton = document.getElementById('end-session-button');
 
742
  const memoryList = document.getElementById('memory-list');
743
  const userNameInput = document.getElementById('user-name');
744
  const userAvatar = document.getElementById('user-avatar');
745
+ const fileInput = document.getElementById('file-input');
746
+ const fileUploadArea = document.getElementById('file-upload-area');
747
+ const documentList = document.getElementById('document-list');
748
+ const memoryTypeBtns = document.querySelectorAll('.memory-type-btn');
749
  let audioLevel = 0;
750
  let animationFrame;
751
  let audioContext, analyser, audioSource;
 
768
  }
769
  });
770
 
771
+ // Memory type selector
772
+ memoryTypeBtns.forEach(btn => {
773
+ btn.addEventListener('click', () => {
774
+ memoryTypeBtns.forEach(b => b.classList.remove('active'));
775
+ btn.classList.add('active');
776
+ selectedMemoryType = btn.dataset.type;
777
+ });
778
+ });
779
+
780
+ // File upload handling
781
+ fileUploadArea.addEventListener('click', () => {
782
+ fileInput.click();
783
+ });
784
+
785
+ fileUploadArea.addEventListener('dragover', (e) => {
786
+ e.preventDefault();
787
+ fileUploadArea.classList.add('drag-over');
788
+ });
789
+
790
+ fileUploadArea.addEventListener('dragleave', () => {
791
+ fileUploadArea.classList.remove('drag-over');
792
+ });
793
+
794
+ fileUploadArea.addEventListener('drop', (e) => {
795
+ e.preventDefault();
796
+ fileUploadArea.classList.remove('drag-over');
797
+ const files = e.dataTransfer.files;
798
+ handleFileUpload(files);
799
+ });
800
+
801
+ fileInput.addEventListener('change', (e) => {
802
+ handleFileUpload(e.target.files);
803
+ });
804
+
805
+ async function handleFileUpload(files) {
806
+ for (let file of files) {
807
+ const allowedTypes = ['.pdf', '.txt', '.csv'];
808
+ const fileExt = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
809
+
810
+ if (!allowedTypes.includes(fileExt)) {
811
+ showError(`지원하지 않는 파일 형식: ${file.name}`);
812
+ continue;
813
+ }
814
+
815
+ const formData = new FormData();
816
+ formData.append('file', file);
817
+ formData.append('memory_type', selectedMemoryType);
818
+ formData.append('session_id', currentSessionId);
819
+
820
+ try {
821
+ const response = await fetch('/upload/document', {
822
+ method: 'POST',
823
+ body: formData
824
+ });
825
+
826
+ const result = await response.json();
827
+
828
+ if (result.error) {
829
+ showError(result.error);
830
+ } else {
831
+ showToast(`${file.name}이(가) ${selectedMemoryType === 'temporary' ? '단기' : '영구'} 기억으로 업로드되었습니다.`, 'success');
832
+
833
+ if (selectedMemoryType === 'temporary') {
834
+ temporaryDocuments[result.document_id] = {
835
+ name: file.name,
836
+ type: fileExt,
837
+ content: result.content
838
+ };
839
+ updateDocumentList();
840
+ } else {
841
+ loadMemories();
842
+ }
843
+
844
+ addMessage('document-loaded', `📄 문서 "${file.name}"이(가) ${selectedMemoryType === 'temporary' ? '단기' : '영구'} 기억으로 로드되었습니다.`);
845
+ }
846
+ } catch (error) {
847
+ console.error('File upload error:', error);
848
+ showError('파일 업로드 중 오류가 발생했습니다.');
849
+ }
850
+ }
851
+ }
852
+
853
+ function updateDocumentList() {
854
+ documentList.innerHTML = '';
855
+
856
+ for (const [docId, doc] of Object.entries(temporaryDocuments)) {
857
+ const item = document.createElement('div');
858
+ item.className = 'document-item';
859
+
860
+ const iconMap = {
861
+ '.pdf': 'PDF',
862
+ '.txt': 'TXT',
863
+ '.csv': 'CSV'
864
+ };
865
+
866
+ item.innerHTML = `
867
+ <div class="document-info">
868
+ <div class="document-icon">${iconMap[doc.type]}</div>
869
+ <div>
870
+ <span class="document-name">${doc.name}</span>
871
+ <span class="document-type">단기 기억</span>
872
+ </div>
873
+ </div>
874
+ <button class="remove-doc-btn" onclick="removeDocument('${docId}')">×</button>
875
+ `;
876
+
877
+ documentList.appendChild(item);
878
+ }
879
+ }
880
+
881
+ window.removeDocument = function(docId) {
882
+ delete temporaryDocuments[docId];
883
+ updateDocumentList();
884
+ showToast('문서가 제거되었습니다.', 'success');
885
+ };
886
+
887
  // Start new session
888
  async function startNewSession() {
889
  const response = await fetch('/session/new', { method: 'POST' });
 
910
  userMemories[memory.category].push(memory.content);
911
 
912
  const item = document.createElement('div');
913
+ item.className = memory.is_document ? 'memory-item document' : 'memory-item';
914
  item.innerHTML = `
915
  <div class="memory-category">${memory.category}</div>
916
  <div class="memory-content">${memory.content}</div>
 
941
  if (result.status === 'ok') {
942
  showToast('기억이 성공적으로 업데이트되었습니다.', 'success');
943
  loadMemories();
944
+ temporaryDocuments = {}; // Clear temporary documents
945
+ updateDocumentList();
946
  startNewSession();
947
  }
948
  } catch (error) {
 
1039
  web_search_enabled: webSearchEnabled,
1040
  session_id: currentSessionId,
1041
  user_name: userName,
1042
+ memories: userMemories,
1043
+ temporary_documents: temporaryDocuments
1044
  })
1045
  });
1046
 
 
1238
  web_search_enabled: webSearchEnabled,
1239
  session_id: currentSessionId,
1240
  user_name: userName,
1241
+ memories: userMemories,
1242
+ temporary_documents: temporaryDocuments
1243
  })
1244
  });
1245
 
 
1290
  chatMessages.scrollTop = chatMessages.scrollHeight;
1291
 
1292
  // Save message to database if save flag is true
1293
+ if (save && currentSessionId && role !== 'memory-update' && role !== 'search-result' && role !== 'document-loaded') {
1294
  fetch('/message/save', {
1295
  method: 'POST',
1296
  headers: { 'Content-Type': 'application/json' },
 
1487
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1488
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1489
  source_session_id TEXT,
1490
+ is_document BOOLEAN DEFAULT 0,
1491
+ document_name TEXT,
1492
  FOREIGN KEY (source_session_id) REFERENCES conversations(id)
1493
  )
1494
  """)
 
1496
  # Create indexes for better performance
1497
  await db.execute("CREATE INDEX IF NOT EXISTS idx_memories_category ON user_memories(category)")
1498
  await db.execute("CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id)")
1499
+ await db.execute("CREATE INDEX IF NOT EXISTS idx_memories_document ON user_memories(is_document)")
1500
 
1501
  await db.commit()
1502
 
 
1599
  ]
1600
 
1601
  @staticmethod
1602
+ async def save_memory(category: str, content: str, session_id: str = None, confidence: float = 1.0,
1603
+ is_document: bool = False, document_name: str = None):
1604
  """Save or update a user memory"""
1605
  async with aiosqlite.connect(DB_PATH) as db:
1606
+ # Check if similar memory exists (not for documents)
1607
+ if not is_document:
1608
+ cursor = await db.execute(
1609
+ """SELECT id, content FROM user_memories
1610
+ WHERE category = ? AND content LIKE ? AND is_document = 0
1611
+ LIMIT 1""",
1612
+ (category, f"%{content[:20]}%")
 
 
 
 
 
 
 
 
 
 
1613
  )
1614
+ existing = await cursor.fetchone()
1615
+
1616
+ if existing:
1617
+ # Update existing memory
1618
+ await db.execute(
1619
+ """UPDATE user_memories
1620
+ SET content = ?, confidence = ?, updated_at = CURRENT_TIMESTAMP,
1621
+ source_session_id = ?
1622
+ WHERE id = ?""",
1623
+ (content, confidence, session_id, existing[0])
1624
+ )
1625
+ else:
1626
+ # Insert new memory
1627
+ await db.execute(
1628
+ """INSERT INTO user_memories (category, content, confidence, source_session_id,
1629
+ is_document, document_name)
1630
+ VALUES (?, ?, ?, ?, ?, ?)""",
1631
+ (category, content, confidence, session_id, is_document, document_name)
1632
+ )
1633
  else:
1634
+ # Always insert new document memories
1635
  await db.execute(
1636
+ """INSERT INTO user_memories (category, content, confidence, source_session_id,
1637
+ is_document, document_name)
1638
+ VALUES (?, ?, ?, ?, ?, ?)""",
1639
+ (category, content, confidence, session_id, is_document, document_name)
1640
  )
1641
 
1642
  await db.commit()
 
1646
  """Get all user memories"""
1647
  async with aiosqlite.connect(DB_PATH) as db:
1648
  cursor = await db.execute(
1649
+ """SELECT category, content, confidence, updated_at, is_document, document_name
1650
  FROM user_memories
1651
+ ORDER BY is_document DESC, category, updated_at DESC"""
1652
  )
1653
  rows = await cursor.fetchall()
1654
  return [
 
1656
  "category": row[0],
1657
  "content": row[1],
1658
  "confidence": row[2],
1659
+ "updated_at": row[3],
1660
+ "is_document": row[4],
1661
+ "document_name": row[5]
1662
  }
1663
  for row in rows
1664
  ]
 
1778
  return memory_text
1779
 
1780
 
1781
+ def format_documents_for_prompt(documents: Dict[str, Dict]) -> str:
1782
+ """Format temporary documents for inclusion in prompt"""
1783
+ if not documents:
1784
+ return ""
1785
+
1786
+ doc_text = "\n\n=== 참조 문서 ===\n"
1787
+ for doc_id, doc_info in documents.items():
1788
+ if doc_info and doc_info.get('content'):
1789
+ doc_text += f"\n[문서: {doc_info['name']}]\n"
1790
+ doc_text += doc_info['content'][:3000] + "...\n" if len(doc_info['content']) > 3000 else doc_info['content'] + "\n"
1791
+
1792
+ return doc_text
1793
+
1794
+
1795
+ async def extract_text_from_pdf(file_content: bytes) -> str:
1796
+ """Extract text from PDF file"""
1797
+ try:
1798
+ pdf_reader = PyPDF2.PdfReader(io.BytesIO(file_content))
1799
+ text = ""
1800
+ for page in pdf_reader.pages:
1801
+ text += page.extract_text() + "\n"
1802
+ return text.strip()
1803
+ except Exception as e:
1804
+ print(f"Error extracting PDF text: {e}")
1805
+ return ""
1806
+
1807
+
1808
+ async def extract_text_from_csv(file_content: bytes) -> str:
1809
+ """Extract text from CSV file"""
1810
+ try:
1811
+ # Detect encoding
1812
+ detected = chardet.detect(file_content)
1813
+ encoding = detected['encoding'] or 'utf-8'
1814
+
1815
+ # Read CSV
1816
+ text_content = file_content.decode(encoding)
1817
+ df = pd.read_csv(io.StringIO(text_content))
1818
+
1819
+ # Convert to readable format
1820
+ text = f"CSV 데이터 (행: {len(df)}, 열: {len(df.columns)})\n\n"
1821
+ text += "컬럼: " + ", ".join(df.columns) + "\n\n"
1822
+
1823
+ # Add sample data (first 10 rows)
1824
+ text += "데이터 샘플:\n"
1825
+ text += df.head(10).to_string() + "\n"
1826
+
1827
+ # Add basic statistics for numeric columns
1828
+ numeric_cols = df.select_dtypes(include=[np.number]).columns
1829
+ if len(numeric_cols) > 0:
1830
+ text += "\n통계 정보:\n"
1831
+ text += df[numeric_cols].describe().to_string()
1832
+
1833
+ return text
1834
+ except Exception as e:
1835
+ print(f"Error extracting CSV text: {e}")
1836
+ return ""
1837
+
1838
+
1839
+ async def extract_text_from_txt(file_content: bytes) -> str:
1840
+ """Extract text from TXT file"""
1841
+ try:
1842
+ # Detect encoding
1843
+ detected = chardet.detect(file_content)
1844
+ encoding = detected['encoding'] or 'utf-8'
1845
+ return file_content.decode(encoding)
1846
+ except Exception as e:
1847
+ print(f"Error extracting TXT text: {e}")
1848
+ return ""
1849
+
1850
+
1851
  async def process_text_chat(message: str, web_search_enabled: bool, session_id: str,
1852
+ user_name: str = "", memories: Dict = None,
1853
+ temporary_documents: Dict = None) -> Dict[str, str]:
1854
  """Process text chat using GPT-4o-mini model"""
1855
  try:
1856
  # Check for empty or None message
 
1865
  "detected_language": "ko"
1866
  }
1867
 
1868
+ # Build system prompt with memories and documents
1869
  base_prompt = f"""You are a personal AI assistant for {user_name if user_name else 'the user'}.
1870
  You remember all previous conversations and personal information about the user.
1871
  Be friendly, helpful, and personalized in your responses.
 
1877
  memory_text = format_memories_for_prompt(memories)
1878
  base_prompt += memory_text
1879
 
1880
+ # Add temporary documents to prompt
1881
+ if temporary_documents:
1882
+ doc_text = format_documents_for_prompt(temporary_documents)
1883
+ base_prompt += doc_text
1884
+
1885
  messages = [{"role": "system", "content": base_prompt}]
1886
 
1887
  # Handle web search if enabled
 
1941
 
1942
  class OpenAIHandler(AsyncStreamHandler):
1943
  def __init__(self, web_search_enabled: bool = False, webrtc_id: str = None,
1944
+ session_id: str = None, user_name: str = "", memories: Dict = None,
1945
+ temporary_documents: Dict = None) -> None:
1946
  super().__init__(
1947
  expected_layout="mono",
1948
  output_sample_rate=SAMPLE_RATE,
 
1960
  self.session_id = session_id
1961
  self.user_name = user_name
1962
  self.memories = memories or {}
1963
+ self.temporary_documents = temporary_documents or {}
1964
  self.is_responding = False # Track if already responding
1965
  self.should_stop = False # Track if conversation should stop
1966
 
1967
+ print(f"[INIT] Handler created with web_search={web_search_enabled}, session_id={session_id}, user={user_name}, docs={len(temporary_documents)}")
1968
 
1969
  def copy(self):
1970
  if connection_settings:
 
1982
  webrtc_id=recent_id,
1983
  session_id=settings.get('session_id'),
1984
  user_name=settings.get('user_name', ''),
1985
+ memories=settings.get('memories', {}),
1986
+ temporary_documents=settings.get('temporary_documents', {})
1987
  )
1988
 
1989
  print(f"[COPY] No settings found, creating default handler")
 
2030
  self.session_id = settings.get('session_id')
2031
  self.user_name = settings.get('user_name', '')
2032
  self.memories = settings.get('memories', {})
2033
+ self.temporary_documents = settings.get('temporary_documents', {})
2034
 
2035
  print(f"[START_UP] Updated settings from storage for {self.webrtc_id}")
2036
 
 
2038
 
2039
  print(f"[REALTIME API] Connecting...")
2040
 
2041
+ # Build system prompt with memories and documents
2042
  base_instructions = f"""You are a personal AI assistant for {self.user_name if self.user_name else 'the user'}.
2043
  You remember all previous conversations and personal information about the user.
2044
  Be friendly, helpful, and personalized in your responses.
 
2050
  memory_text = format_memories_for_prompt(self.memories)
2051
  base_instructions += memory_text
2052
 
2053
+ # Add temporary documents to prompt
2054
+ if self.temporary_documents:
2055
+ doc_text = format_documents_for_prompt(self.temporary_documents)
2056
+ base_instructions += doc_text
2057
+
2058
  # Define the web search function
2059
  tools = []
2060
  if self.web_search_enabled and self.search_client:
 
2358
  session_id = body.get("session_id")
2359
  user_name = body.get("user_name", "")
2360
  memories = body.get("memories", {})
2361
+ temporary_documents = body.get("temporary_documents", {})
2362
 
2363
  print(f"[OFFER] Received offer with webrtc_id: {webrtc_id}")
2364
  print(f"[OFFER] web_search_enabled: {web_search_enabled}")
2365
  print(f"[OFFER] session_id: {session_id}")
2366
  print(f"[OFFER] user_name: {user_name}")
2367
+ print(f"[OFFER] temporary_documents: {len(temporary_documents)} documents")
2368
 
2369
  # Store settings with timestamp
2370
  if webrtc_id:
 
2373
  'session_id': session_id,
2374
  'user_name': user_name,
2375
  'memories': memories,
2376
+ 'temporary_documents': temporary_documents,
2377
  'timestamp': asyncio.get_event_loop().time()
2378
  }
2379
 
 
2399
  return response
2400
 
2401
 
2402
+ @app.post("/upload/document")
2403
+ async def upload_document(
2404
+ file: UploadFile = File(...),
2405
+ memory_type: str = Form(...),
2406
+ session_id: str = Form(...)
2407
+ ):
2408
+ """Upload and process a document"""
2409
+ try:
2410
+ # Read file content
2411
+ file_content = await file.read()
2412
+ file_ext = Path(file.filename).suffix.lower()
2413
+
2414
+ # Extract text based on file type
2415
+ extracted_text = ""
2416
+ if file_ext == '.pdf':
2417
+ extracted_text = await extract_text_from_pdf(file_content)
2418
+ elif file_ext == '.txt':
2419
+ extracted_text = await extract_text_from_txt(file_content)
2420
+ elif file_ext == '.csv':
2421
+ extracted_text = await extract_text_from_csv(file_content)
2422
+ else:
2423
+ return {"error": "Unsupported file type"}
2424
+
2425
+ if not extracted_text:
2426
+ return {"error": "Failed to extract text from document"}
2427
+
2428
+ # Generate document ID
2429
+ document_id = str(uuid.uuid4())
2430
+
2431
+ # Save to database if permanent
2432
+ if memory_type == 'permanent':
2433
+ # Summarize long documents
2434
+ if len(extracted_text) > 5000:
2435
+ # Use GPT to summarize
2436
+ summary_response = await client.chat.completions.create(
2437
+ model="gpt-4.1-mini",
2438
+ messages=[
2439
+ {
2440
+ "role": "system",
2441
+ "content": "Summarize the following document in Korean, preserving key information and structure."
2442
+ },
2443
+ {
2444
+ "role": "user",
2445
+ "content": extracted_text[:10000] # Limit to first 10k chars
2446
+ }
2447
+ ],
2448
+ temperature=0.3,
2449
+ max_tokens=2000
2450
+ )
2451
+ summary = summary_response.choices[0].message.content
2452
+
2453
+ await PersonalAssistantDB.save_memory(
2454
+ category="document",
2455
+ content=f"[문서 요약] {file.filename}\n\n{summary}",
2456
+ session_id=session_id,
2457
+ confidence=1.0,
2458
+ is_document=True,
2459
+ document_name=file.filename
2460
+ )
2461
+ else:
2462
+ await PersonalAssistantDB.save_memory(
2463
+ category="document",
2464
+ content=f"[문서] {file.filename}\n\n{extracted_text}",
2465
+ session_id=session_id,
2466
+ confidence=1.0,
2467
+ is_document=True,
2468
+ document_name=file.filename
2469
+ )
2470
+
2471
+ # Save file to uploads directory (optional, for backup)
2472
+ file_path = os.path.join(UPLOAD_DIR, f"{document_id}{file_ext}")
2473
+ with open(file_path, 'wb') as f:
2474
+ f.write(file_content)
2475
+
2476
+ return {
2477
+ "document_id": document_id,
2478
+ "content": extracted_text,
2479
+ "memory_type": memory_type,
2480
+ "filename": file.filename
2481
+ }
2482
+
2483
+ except Exception as e:
2484
+ print(f"Error uploading document: {e}")
2485
+ return {"error": str(e)}
2486
+
2487
+
2488
  @app.post("/session/new")
2489
  async def create_new_session():
2490
  """Create a new chat session"""
 
2554
  session_id = body.get("session_id")
2555
  user_name = body.get("user_name", "")
2556
  memories = body.get("memories", {})
2557
+ temporary_documents = body.get("temporary_documents", {})
2558
 
2559
  if not message:
2560
  return {"error": "메시지가 비어있습니다."}
2561
 
2562
  # Process text chat
2563
+ result = await process_text_chat(message, web_search_enabled, session_id,
2564
+ user_name, memories, temporary_documents)
2565
 
2566
  return result
2567