Claude commited on
Commit
4025749
ยท
unverified ยท
1 Parent(s): 6026178

Add chatbot UI and fix Content-Length error

Browse files

์ฃผ์š” ๋ณ€๊ฒฝ์‚ฌํ•ญ:
- โœ… ์ฑ—๋ด‡ ํ˜•ํƒœ์˜ ์›น UI ์ถ”๊ฐ€ (static/chatbot.html)
* ๋ชจ๋˜ํ•œ ์ฑ„ํŒ… ์ธํ„ฐํŽ˜์ด์Šค ๋””์ž์ธ
* ์งˆ๋ฌธ-๋‹ต๋ณ€ ํ˜•ํƒœ์˜ ๋Œ€ํ™”ํ˜• UI
* ์ถœ์ฒ˜ ๋ฌธ์„œ ํ‘œ์‹œ ๊ธฐ๋Šฅ
* ๋ฉ”ํƒ€์ธ์ง€ ํ™œ์„ฑํ™”/๋น„ํ™œ์„ฑํ™” ์˜ต์…˜
* ๊ฒ€์ƒ‰ ๋ฌธ์„œ ์ˆ˜ ์กฐ์ ˆ ๊ธฐ๋Šฅ

- โœ… Content-Length ์˜ค๋ฅ˜ ์ˆ˜์ • (app/api/routes.py)
* JSONResponse๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜์—ฌ Content-Length ์ •ํ™•ํžˆ ๊ณ„์‚ฐ
* response_model ์ œ๊ฑฐํ•˜์—ฌ ์ง๋ ฌํ™” ๋ฌธ์ œ ํ•ด๊ฒฐ
* HTML UI ์ œ๊ณต ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€ (๋ฃจํŠธ ๊ฒฝ๋กœ์—์„œ ์ฑ—๋ด‡ UI ์ œ๊ณต)

์ˆ˜์ •๋œ ํŒŒ์ผ:
- app/api/routes.py: HTML ์‘๋‹ต, JSONResponse ์ถ”๊ฐ€
- static/chatbot.html: ์ƒˆ ์ฑ—๋ด‡ UI ํŒŒ์ผ

Files changed (2) hide show
  1. app/api/routes.py +20 -13
  2. static/chatbot.html +415 -0
app/api/routes.py CHANGED
@@ -3,7 +3,9 @@ FastAPI ๋ผ์šฐํŠธ ์ •์˜
3
  """
4
 
5
  from fastapi import APIRouter, HTTPException, status
 
6
  from loguru import logger
 
7
 
8
  from app.api.models import (
9
  QueryRequest,
@@ -25,18 +27,23 @@ def set_rag_pipeline(pipeline):
25
  rag_pipeline = pipeline
26
 
27
 
28
- @router.get("/", tags=["Root"])
29
  async def root():
30
- """API ๋ฃจํŠธ ์—”๋“œํฌ์ธํŠธ"""
31
- return {
32
- "message": "Financial RAG API with Metacognitive Agent",
33
- "version": "1.0.0",
34
- "endpoints": {
35
- "health": "/health",
36
- "query": "/query",
37
- "docs": "/docs"
38
- }
39
- }
 
 
 
 
 
40
 
41
 
42
  @router.get(
@@ -77,7 +84,6 @@ async def health_check():
77
 
78
  @router.post(
79
  "/query",
80
- response_model=QueryResponse,
81
  tags=["Query"],
82
  summary="์งˆ๋ฌธํ•˜๊ธฐ",
83
  description="๊ธˆ์œต/๊ฒฝ์ œ ๊ด€๋ จ ์งˆ๋ฌธ์— ๋Œ€ํ•ด RAG ์‹œ์Šคํ…œ์œผ๋กœ ๋‹ต๋ณ€ ์ƒ์„ฑ"
@@ -111,7 +117,8 @@ async def query(request: QueryRequest):
111
 
112
  logger.info(f"Query processed successfully")
113
 
114
- return QueryResponse(**result)
 
115
 
116
  except Exception as e:
117
  logger.error(f"Query failed: {str(e)}")
 
3
  """
4
 
5
  from fastapi import APIRouter, HTTPException, status
6
+ from fastapi.responses import HTMLResponse, JSONResponse
7
  from loguru import logger
8
+ import os
9
 
10
  from app.api.models import (
11
  QueryRequest,
 
27
  rag_pipeline = pipeline
28
 
29
 
30
+ @router.get("/", response_class=HTMLResponse, tags=["Root"])
31
  async def root():
32
+ """์ฑ—๋ด‡ UI ์ œ๊ณต"""
33
+ html_path = os.path.join("static", "chatbot.html")
34
+ try:
35
+ with open(html_path, "r", encoding="utf-8") as f:
36
+ return f.read()
37
+ except FileNotFoundError:
38
+ return """
39
+ <html>
40
+ <body>
41
+ <h1>Financial RAG API</h1>
42
+ <p>์ฑ—๋ด‡ UI ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.</p>
43
+ <p>API ๋ฌธ์„œ: <a href="/docs">/docs</a></p>
44
+ </body>
45
+ </html>
46
+ """
47
 
48
 
49
  @router.get(
 
84
 
85
  @router.post(
86
  "/query",
 
87
  tags=["Query"],
88
  summary="์งˆ๋ฌธํ•˜๊ธฐ",
89
  description="๊ธˆ์œต/๊ฒฝ์ œ ๊ด€๋ จ ์งˆ๋ฌธ์— ๋Œ€ํ•ด RAG ์‹œ์Šคํ…œ์œผ๋กœ ๋‹ต๋ณ€ ์ƒ์„ฑ"
 
117
 
118
  logger.info(f"Query processed successfully")
119
 
120
+ # JSONResponse๋กœ ๋ช…์‹œ์  ๋ฐ˜ํ™˜ํ•˜์—ฌ Content-Length ์˜ค๋ฅ˜ ๋ฐฉ์ง€
121
+ return JSONResponse(content=result)
122
 
123
  except Exception as e:
124
  logger.error(f"Query failed: {str(e)}")
static/chatbot.html ADDED
@@ -0,0 +1,415 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ko">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Financial RAG Chatbot</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ height: 100vh;
18
+ display: flex;
19
+ justify-content: center;
20
+ align-items: center;
21
+ }
22
+
23
+ .chat-container {
24
+ width: 90%;
25
+ max-width: 900px;
26
+ height: 90vh;
27
+ background: white;
28
+ border-radius: 16px;
29
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
30
+ display: flex;
31
+ flex-direction: column;
32
+ overflow: hidden;
33
+ }
34
+
35
+ .chat-header {
36
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
37
+ color: white;
38
+ padding: 20px;
39
+ text-align: center;
40
+ }
41
+
42
+ .chat-header h1 {
43
+ font-size: 24px;
44
+ margin-bottom: 5px;
45
+ }
46
+
47
+ .chat-header p {
48
+ font-size: 14px;
49
+ opacity: 0.9;
50
+ }
51
+
52
+ .chat-messages {
53
+ flex: 1;
54
+ overflow-y: auto;
55
+ padding: 20px;
56
+ background: #f5f5f5;
57
+ }
58
+
59
+ .message {
60
+ margin-bottom: 16px;
61
+ display: flex;
62
+ align-items: flex-start;
63
+ }
64
+
65
+ .message.user {
66
+ justify-content: flex-end;
67
+ }
68
+
69
+ .message-content {
70
+ max-width: 70%;
71
+ padding: 12px 16px;
72
+ border-radius: 16px;
73
+ line-height: 1.5;
74
+ }
75
+
76
+ .message.user .message-content {
77
+ background: #667eea;
78
+ color: white;
79
+ border-bottom-right-radius: 4px;
80
+ }
81
+
82
+ .message.assistant .message-content {
83
+ background: white;
84
+ color: #333;
85
+ border-bottom-left-radius: 4px;
86
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
87
+ }
88
+
89
+ .message-time {
90
+ font-size: 11px;
91
+ opacity: 0.6;
92
+ margin-top: 4px;
93
+ }
94
+
95
+ .sources {
96
+ margin-top: 12px;
97
+ padding: 12px;
98
+ background: #f9f9f9;
99
+ border-left: 3px solid #667eea;
100
+ border-radius: 4px;
101
+ }
102
+
103
+ .sources-title {
104
+ font-weight: 600;
105
+ margin-bottom: 8px;
106
+ color: #667eea;
107
+ font-size: 13px;
108
+ }
109
+
110
+ .source-item {
111
+ font-size: 12px;
112
+ color: #666;
113
+ margin-bottom: 4px;
114
+ padding-left: 12px;
115
+ }
116
+
117
+ .chat-input-container {
118
+ padding: 20px;
119
+ background: white;
120
+ border-top: 1px solid #e0e0e0;
121
+ }
122
+
123
+ .chat-input-wrapper {
124
+ display: flex;
125
+ gap: 12px;
126
+ }
127
+
128
+ .chat-input {
129
+ flex: 1;
130
+ padding: 12px 16px;
131
+ border: 2px solid #e0e0e0;
132
+ border-radius: 24px;
133
+ font-size: 14px;
134
+ outline: none;
135
+ transition: border-color 0.3s;
136
+ }
137
+
138
+ .chat-input:focus {
139
+ border-color: #667eea;
140
+ }
141
+
142
+ .send-button {
143
+ padding: 12px 24px;
144
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
145
+ color: white;
146
+ border: none;
147
+ border-radius: 24px;
148
+ cursor: pointer;
149
+ font-weight: 600;
150
+ transition: transform 0.2s, box-shadow 0.2s;
151
+ }
152
+
153
+ .send-button:hover {
154
+ transform: translateY(-2px);
155
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
156
+ }
157
+
158
+ .send-button:disabled {
159
+ opacity: 0.5;
160
+ cursor: not-allowed;
161
+ transform: none;
162
+ }
163
+
164
+ .loading {
165
+ display: flex;
166
+ gap: 4px;
167
+ padding: 12px 16px;
168
+ }
169
+
170
+ .loading-dot {
171
+ width: 8px;
172
+ height: 8px;
173
+ background: #667eea;
174
+ border-radius: 50%;
175
+ animation: bounce 1.4s infinite ease-in-out both;
176
+ }
177
+
178
+ .loading-dot:nth-child(1) {
179
+ animation-delay: -0.32s;
180
+ }
181
+
182
+ .loading-dot:nth-child(2) {
183
+ animation-delay: -0.16s;
184
+ }
185
+
186
+ @keyframes bounce {
187
+ 0%, 80%, 100% {
188
+ transform: scale(0);
189
+ }
190
+ 40% {
191
+ transform: scale(1);
192
+ }
193
+ }
194
+
195
+ .error-message {
196
+ background: #fee;
197
+ color: #c33;
198
+ padding: 12px;
199
+ border-radius: 8px;
200
+ margin-top: 8px;
201
+ font-size: 13px;
202
+ }
203
+
204
+ .settings {
205
+ padding: 12px 20px;
206
+ background: #f9f9f9;
207
+ border-top: 1px solid #e0e0e0;
208
+ display: flex;
209
+ gap: 16px;
210
+ align-items: center;
211
+ font-size: 13px;
212
+ }
213
+
214
+ .settings label {
215
+ display: flex;
216
+ align-items: center;
217
+ gap: 8px;
218
+ cursor: pointer;
219
+ }
220
+
221
+ .settings input[type="checkbox"] {
222
+ cursor: pointer;
223
+ }
224
+
225
+ .settings input[type="number"] {
226
+ width: 60px;
227
+ padding: 4px 8px;
228
+ border: 1px solid #ddd;
229
+ border-radius: 4px;
230
+ }
231
+ </style>
232
+ </head>
233
+ <body>
234
+ <div class="chat-container">
235
+ <div class="chat-header">
236
+ <h1>๐Ÿ’ฌ Financial RAG Chatbot</h1>
237
+ <p>๊ธˆ์œต/๊ฒฝ์ œ ๋…ผ๋ฌธ ๊ธฐ๋ฐ˜ AI ์ฑ—๋ด‡ - Metacognitive Agent</p>
238
+ </div>
239
+
240
+ <div class="settings">
241
+ <label>
242
+ <input type="checkbox" id="metacognition" checked>
243
+ ๋ฉ”ํƒ€์ธ์ง€ ํ™œ์„ฑํ™”
244
+ </label>
245
+ <label>
246
+ ๊ฒ€์ƒ‰ ๋ฌธ์„œ ์ˆ˜:
247
+ <input type="number" id="topK" value="3" min="1" max="10">
248
+ </label>
249
+ </div>
250
+
251
+ <div class="chat-messages" id="chatMessages">
252
+ <div class="message assistant">
253
+ <div class="message-content">
254
+ ์•ˆ๋…•ํ•˜์„ธ์š”! ๊ธˆ์œต/๊ฒฝ์ œ ๊ด€๋ จ ์งˆ๋ฌธ์— ๋‹ต๋ณ€ํ•ด๋“œ๋ฆฌ๋Š” AI ์ฑ—๋ด‡์ž…๋‹ˆ๋‹ค.
255
+ ๋ฌด์—‡์ด ๊ถ๊ธˆํ•˜์‹ ๊ฐ€์š”?
256
+ </div>
257
+ </div>
258
+ </div>
259
+
260
+ <div class="chat-input-container">
261
+ <div class="chat-input-wrapper">
262
+ <input
263
+ type="text"
264
+ class="chat-input"
265
+ id="userInput"
266
+ placeholder="์งˆ๋ฌธ์„ ์ž…๋ ฅํ•˜์„ธ์š”..."
267
+ onkeypress="handleKeyPress(event)"
268
+ >
269
+ <button class="send-button" id="sendButton" onclick="sendMessage()">
270
+ ์ „์†ก
271
+ </button>
272
+ </div>
273
+ </div>
274
+ </div>
275
+
276
+ <script>
277
+ const chatMessages = document.getElementById('chatMessages');
278
+ const userInput = document.getElementById('userInput');
279
+ const sendButton = document.getElementById('sendButton');
280
+ const metacognitionCheckbox = document.getElementById('metacognition');
281
+ const topKInput = document.getElementById('topK');
282
+
283
+ function handleKeyPress(event) {
284
+ if (event.key === 'Enter' && !event.shiftKey) {
285
+ event.preventDefault();
286
+ sendMessage();
287
+ }
288
+ }
289
+
290
+ function addMessage(content, isUser = false, sources = null, error = null) {
291
+ const messageDiv = document.createElement('div');
292
+ messageDiv.className = `message ${isUser ? 'user' : 'assistant'}`;
293
+
294
+ const contentDiv = document.createElement('div');
295
+ contentDiv.className = 'message-content';
296
+
297
+ if (error) {
298
+ contentDiv.innerHTML = content;
299
+ const errorDiv = document.createElement('div');
300
+ errorDiv.className = 'error-message';
301
+ errorDiv.textContent = error;
302
+ contentDiv.appendChild(errorDiv);
303
+ } else {
304
+ contentDiv.textContent = content;
305
+ }
306
+
307
+ // ์ถœ์ฒ˜ ์ •๋ณด ์ถ”๊ฐ€
308
+ if (sources && sources.length > 0) {
309
+ const sourcesDiv = document.createElement('div');
310
+ sourcesDiv.className = 'sources';
311
+
312
+ const titleDiv = document.createElement('div');
313
+ titleDiv.className = 'sources-title';
314
+ titleDiv.textContent = '๐Ÿ“š ์ฐธ๊ณ  ๋ฌธ์„œ:';
315
+ sourcesDiv.appendChild(titleDiv);
316
+
317
+ sources.forEach((source, index) => {
318
+ const sourceItem = document.createElement('div');
319
+ sourceItem.className = 'source-item';
320
+ sourceItem.textContent = `${index + 1}. ${source.metadata?.source || 'Unknown'} (ํŽ˜์ด์ง€ ${source.metadata?.page || 'N/A'})`;
321
+ sourcesDiv.appendChild(sourceItem);
322
+ });
323
+
324
+ contentDiv.appendChild(sourcesDiv);
325
+ }
326
+
327
+ messageDiv.appendChild(contentDiv);
328
+ chatMessages.appendChild(messageDiv);
329
+ chatMessages.scrollTop = chatMessages.scrollHeight;
330
+ }
331
+
332
+ function showLoading() {
333
+ const loadingDiv = document.createElement('div');
334
+ loadingDiv.className = 'message assistant';
335
+ loadingDiv.id = 'loading';
336
+
337
+ const contentDiv = document.createElement('div');
338
+ contentDiv.className = 'message-content';
339
+
340
+ const loading = document.createElement('div');
341
+ loading.className = 'loading';
342
+ loading.innerHTML = '<div class="loading-dot"></div><div class="loading-dot"></div><div class="loading-dot"></div>';
343
+
344
+ contentDiv.appendChild(loading);
345
+ loadingDiv.appendChild(contentDiv);
346
+ chatMessages.appendChild(loadingDiv);
347
+ chatMessages.scrollTop = chatMessages.scrollHeight;
348
+ }
349
+
350
+ function hideLoading() {
351
+ const loading = document.getElementById('loading');
352
+ if (loading) {
353
+ loading.remove();
354
+ }
355
+ }
356
+
357
+ async function sendMessage() {
358
+ const message = userInput.value.trim();
359
+ if (!message) return;
360
+
361
+ // ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
362
+ addMessage(message, true);
363
+ userInput.value = '';
364
+
365
+ // UI ๋น„ํ™œ์„ฑํ™”
366
+ sendButton.disabled = true;
367
+ userInput.disabled = true;
368
+
369
+ // ๋กœ๋”ฉ ํ‘œ์‹œ
370
+ showLoading();
371
+
372
+ try {
373
+ const response = await fetch('/query', {
374
+ method: 'POST',
375
+ headers: {
376
+ 'Content-Type': 'application/json',
377
+ },
378
+ body: JSON.stringify({
379
+ question: message,
380
+ top_k: parseInt(topKInput.value),
381
+ enable_metacognition: metacognitionCheckbox.checked
382
+ })
383
+ });
384
+
385
+ hideLoading();
386
+
387
+ if (!response.ok) {
388
+ const errorData = await response.json();
389
+ throw new Error(errorData.detail || 'API ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.');
390
+ }
391
+
392
+ const data = await response.json();
393
+
394
+ // ๋ด‡ ์‘๋‹ต ์ถ”๊ฐ€
395
+ addMessage(data.answer, false, data.sources);
396
+
397
+ } catch (error) {
398
+ hideLoading();
399
+ addMessage('์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.', false, null, error.message);
400
+ console.error('Error:', error);
401
+ } finally {
402
+ // UI ํ™œ์„ฑํ™”
403
+ sendButton.disabled = false;
404
+ userInput.disabled = false;
405
+ userInput.focus();
406
+ }
407
+ }
408
+
409
+ // ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ์ž…๋ ฅ์ฐฝ์— ํฌ์ปค์Šค
410
+ window.onload = () => {
411
+ userInput.focus();
412
+ };
413
+ </script>
414
+ </body>
415
+ </html>