Spaces:
Running
Running
Update app.py
Browse files
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 |
-
|
1347 |
-
|
1348 |
-
|
1349 |
-
|
1350 |
-
|
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 |
-
#
|
1365 |
await db.execute(
|
1366 |
-
"""INSERT INTO user_memories (category, content, confidence, source_session_id
|
1367 |
-
|
1368 |
-
|
|
|
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
|
|
|
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
|
|
|
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,
|
|
|
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 |
|