ginipick commited on
Commit
88cc048
·
verified ·
1 Parent(s): 48f2d8b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +203 -105
app.py CHANGED
@@ -49,7 +49,19 @@ ADMIN_PASSWORD = os.getenv("PASSWORD", "admin") # 환경 변수에서 가져오
49
 
50
  # OpenAI API 키 설정
51
  OPENAI_API_KEY = os.getenv("LLM_API", "")
52
- openai_client = OpenAI(api_key=OPENAI_API_KEY)
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
  # 전역 캐시 객체
55
  pdf_cache: Dict[str, Dict[str, Any]] = {}
@@ -224,8 +236,16 @@ async def get_pdf_embedding(pdf_id: str) -> Dict[str, Any]:
224
  return {"error": str(e), "pdf_id": pdf_id}
225
 
226
  # PDF 내용 기반 질의응답
 
227
  async def query_pdf(pdf_id: str, query: str) -> Dict[str, Any]:
228
  try:
 
 
 
 
 
 
 
229
  # 임베딩 데이터 가져오기
230
  embedding_data = await get_pdf_embedding(pdf_id)
231
  if "error" in embedding_data:
@@ -234,7 +254,6 @@ async def query_pdf(pdf_id: str, query: str) -> Dict[str, Any]:
234
  # 청크 텍스트 모으기 (임시로 간단하게 전체 텍스트 사용)
235
  all_text = "\n\n".join([f"Page {chunk['page']}: {chunk['text']}" for chunk in embedding_data["chunks"]])
236
 
237
- # OpenAI API 호출
238
  # 컨텍스트 크기를 고려하여 텍스트가 너무 길면 앞부분만 사용
239
  max_context_length = 60000 # 토큰 수가 아닌 문자 수 기준 (대략적인 제한)
240
  if len(all_text) > max_context_length:
@@ -249,37 +268,66 @@ async def query_pdf(pdf_id: str, query: str) -> Dict[str, Any]:
249
 
250
  # gpt-4.1-mini 모델 사용
251
  try:
252
- response = openai_client.chat.completions.create(
253
- model="gpt-4.1-mini",
254
- messages=[
255
- {"role": "system", "content": system_prompt},
256
- {"role": "user", "content": f"다음 PDF 내용을 참고하여 질문에 답변해주세요.\n\nPDF 내용:\n{all_text}\n\n질문: {query}"}
257
- ],
258
- temperature=0.7,
259
- max_tokens=2048
260
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
 
262
- answer = response.choices[0].message.content
263
- return {
264
- "answer": answer,
265
- "pdf_id": pdf_id,
266
- "query": query
267
- }
268
  except Exception as api_error:
269
- logger.error(f"OpenAI API 호출 오류: {api_error}")
270
- return {"error": f"AI 응답 생성 오류가 발생했습니다: {str(api_error)}"}
271
-
 
 
 
 
 
 
 
 
 
272
  except Exception as e:
273
  logger.error(f"질의응답 처리 오류: {e}")
274
  return {"error": str(e)}
275
 
276
  # PDF 요약 생성
 
277
  async def summarize_pdf(pdf_id: str) -> Dict[str, Any]:
278
  try:
 
 
 
 
 
 
 
279
  # 임베딩 데이터 가져오기
280
  embedding_data = await get_pdf_embedding(pdf_id)
281
  if "error" in embedding_data:
282
- return {"error": embedding_data["error"]}
283
 
284
  # 청크 텍스트 모으기 (제한된 길이)
285
  all_text = "\n\n".join([f"Page {chunk['page']}: {chunk['text']}" for chunk in embedding_data["chunks"]])
@@ -291,29 +339,54 @@ async def summarize_pdf(pdf_id: str) -> Dict[str, Any]:
291
 
292
  # OpenAI API 호출
293
  try:
294
- response = openai_client.chat.completions.create(
295
- model="gpt-4.1-mini",
296
- messages=[
297
- {"role": "system", "content": "다음 PDF 내용을 간결하게 요약해주세요. 핵심 주제와 주요 포인트를 포함한 요약을 500자 이내로 작성해주세요."},
298
- {"role": "user", "content": f"PDF 내용:\n{all_text}"}
299
- ],
300
- temperature=0.7,
301
- max_tokens=1024
302
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
 
304
- summary = response.choices[0].message.content
305
- return {
306
- "summary": summary,
307
- "pdf_id": pdf_id
308
- }
309
  except Exception as api_error:
310
- logger.error(f"OpenAI API 호출 오류: {api_error}")
311
- return {"error": f"AI 요약 생성 오류가 발생했습니다: {str(api_error)}"}
 
 
 
 
 
 
 
 
 
312
 
313
  except Exception as e:
314
  logger.error(f"PDF 요약 생성 오류: {e}")
315
- return {"error": str(e)}
 
 
 
316
 
 
317
  # 최적화된 PDF 페이지 캐싱 함수
318
  async def cache_pdf(pdf_path: str):
319
  try:
@@ -2268,79 +2341,104 @@ HTML = """
2268
  }
2269
 
2270
  // PDF 요약 로드 함수
2271
- async function loadPdfSummary() {
2272
- if (!currentPdfId || isAiProcessing || hasLoadedSummary) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2273
 
2274
- try {
2275
- isAiProcessing = true;
2276
- const typingIndicator = addTypingIndicator();
2277
-
2278
- // 서버에 요약 요청
2279
- const response = await fetch(`/api/ai/summarize-pdf/${currentPdfId}`);
2280
- const data = await response.json();
2281
-
2282
- // 로딩 표시기 제거
2283
- typingIndicator.remove();
2284
-
2285
- if (data.error) {
2286
- addChatMessage(`요약을 생성하는 중 오류가 발생했습니다: ${data.error}`);
2287
- } else {
2288
- // 환영 메시지와 요약 추가
2289
- addChatMessage(`안녕하세요! 이 PDF에 대해 어떤 것이든 질문해주세요. 제가 도와드리겠습니다.<br><br><strong>PDF 요약:</strong><br>${data.summary}`);
2290
- hasLoadedSummary = true;
2291
- }
2292
- } catch (error) {
2293
- console.error("PDF 요약 로드 오류:", error);
2294
- addChatMessage("PDF 요약을 로드하는 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.");
2295
- } finally {
2296
- isAiProcessing = false;
2297
  }
 
 
 
 
2298
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2299
 
2300
- // 질문 제출 함수
2301
- async function submitQuestion(question) {
2302
- if (!currentPdfId || isAiProcessing || !question.trim()) return;
2303
-
2304
- try {
2305
- isAiProcessing = true;
2306
- $id('aiChatSubmit').disabled = true;
2307
-
2308
- // 사용자 메시지 추가
2309
- addChatMessage(question, true);
2310
-
2311
- // 로딩 표시기 추가
2312
- const typingIndicator = addTypingIndicator();
2313
-
2314
- // 서버에 질의 요청
2315
- const response = await fetch(`/api/ai/query-pdf/${currentPdfId}`, {
2316
- method: 'POST',
2317
- headers: {
2318
- 'Content-Type': 'application/json'
2319
- },
2320
- body: JSON.stringify({ query: question })
2321
- });
2322
-
2323
- const data = await response.json();
2324
-
2325
- // 로딩 표시기 제거
2326
- typingIndicator.remove();
2327
-
2328
- if (data.error) {
2329
- addChatMessage(`죄송합니다. 질문에 답변하는 중 오류가 발생했습니다: ${data.error}`);
2330
- } else {
2331
- // AI 응답 추가 (마크다운 처리 등 필요시 추가)
2332
- addChatMessage(data.answer);
2333
- }
2334
- } catch (error) {
2335
- console.error("질문 제출 오류:", error);
2336
- addChatMessage("죄송합니다. 서버와 통신 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.");
2337
- } finally {
2338
- isAiProcessing = false;
2339
- $id('aiChatSubmit').disabled = false;
2340
- $id('aiChatInput').value = '';
2341
- $id('aiChatInput').focus();
2342
  }
 
 
 
 
 
 
 
 
 
 
2343
  }
 
 
 
 
 
 
 
 
 
 
 
2344
 
2345
  // DOM이 로드되면 실행
2346
  document.addEventListener('DOMContentLoaded', function() {
 
49
 
50
  # OpenAI API 키 설정
51
  OPENAI_API_KEY = os.getenv("LLM_API", "")
52
+ # API 키가 없거나 비어있을 때 플래그 설정
53
+ HAS_VALID_API_KEY = bool(OPENAI_API_KEY and OPENAI_API_KEY.strip())
54
+
55
+ if HAS_VALID_API_KEY:
56
+ try:
57
+ openai_client = OpenAI(api_key=OPENAI_API_KEY, timeout=30.0)
58
+ logger.info("OpenAI 클라이언트 초기화 성공")
59
+ except Exception as e:
60
+ logger.error(f"OpenAI 클라이언트 초기화 실패: {e}")
61
+ HAS_VALID_API_KEY = False
62
+ else:
63
+ logger.warning("유효한 OpenAI API 키가 없습니다. AI 기능이 제한됩니다.")
64
+ openai_client = None
65
 
66
  # 전역 캐시 객체
67
  pdf_cache: Dict[str, Dict[str, Any]] = {}
 
236
  return {"error": str(e), "pdf_id": pdf_id}
237
 
238
  # PDF 내용 기반 질의응답
239
+ # PDF 내용 기반 질의응답 함수 개선
240
  async def query_pdf(pdf_id: str, query: str) -> Dict[str, Any]:
241
  try:
242
+ # API 키가 없거나 유효하지 않은 경우
243
+ if not HAS_VALID_API_KEY or not openai_client:
244
+ return {
245
+ "error": "OpenAI API 키가 설정되지 않았습니다.",
246
+ "answer": "죄송합니다. 현재 AI 기능이 비활성화되어 있어 질문에 답변할 수 없습니다. 시스템 관리자에게 문의하세요."
247
+ }
248
+
249
  # 임베딩 데이터 가져오기
250
  embedding_data = await get_pdf_embedding(pdf_id)
251
  if "error" in embedding_data:
 
254
  # 청크 텍스트 모으기 (임시로 간단하게 전체 텍스트 사용)
255
  all_text = "\n\n".join([f"Page {chunk['page']}: {chunk['text']}" for chunk in embedding_data["chunks"]])
256
 
 
257
  # 컨텍스트 크기를 고려하여 텍스트가 너무 길면 앞부분만 사용
258
  max_context_length = 60000 # 토큰 수가 아닌 문자 수 기준 (대략적인 제한)
259
  if len(all_text) > max_context_length:
 
268
 
269
  # gpt-4.1-mini 모델 사용
270
  try:
271
+ # 타임아웃 및 재시도 설정 개선
272
+ for attempt in range(3): # 최대 3번 재시도
273
+ try:
274
+ response = openai_client.chat.completions.create(
275
+ model="gpt-4.1-mini",
276
+ messages=[
277
+ {"role": "system", "content": system_prompt},
278
+ {"role": "user", "content": f"다음 PDF 내용을 참고하여 질문에 답변해주세요.\n\nPDF 내용:\n{all_text}\n\n질문: {query}"}
279
+ ],
280
+ temperature=0.7,
281
+ max_tokens=2048,
282
+ timeout=30.0 # 30초 타임아웃
283
+ )
284
+
285
+ answer = response.choices[0].message.content
286
+ return {
287
+ "answer": answer,
288
+ "pdf_id": pdf_id,
289
+ "query": query
290
+ }
291
+ except Exception as api_error:
292
+ logger.error(f"OpenAI API 호출 오류 (시도 {attempt+1}/3): {api_error}")
293
+ if attempt == 2: # 마지막 시도에서도 실패
294
+ raise api_error
295
+ await asyncio.sleep(1 * (attempt + 1)) # 재시도 간 지연 시간 증가
296
 
297
+ # 여기까지 도달하지 않아야 함
298
+ raise Exception("API 호출 재시도 모두 실패")
 
 
 
 
299
  except Exception as api_error:
300
+ logger.error(f"OpenAI API 호출 최종 오류: {api_error}")
301
+ # 오류 유형에 따른 명확한 메시지 제공
302
+ error_message = str(api_error)
303
+ if "Connection" in error_message:
304
+ return {"error": "OpenAI 서버와 연결할 수 없습니다. 인터넷 연결을 확인하세요."}
305
+ elif "Unauthorized" in error_message or "Authentication" in error_message:
306
+ return {"error": "API 키가 유효하지 않습니다."}
307
+ elif "Rate limit" in error_message:
308
+ return {"error": "API 호출 한도를 초과했습니다. 잠시 후 다시 시도하세요."}
309
+ else:
310
+ return {"error": f"AI 응답 생성 중 오류가 발생했습니다: {error_message}"}
311
+
312
  except Exception as e:
313
  logger.error(f"질의응답 처리 오류: {e}")
314
  return {"error": str(e)}
315
 
316
  # PDF 요약 생성
317
+ # PDF 요약 생성 함수 개선
318
  async def summarize_pdf(pdf_id: str) -> Dict[str, Any]:
319
  try:
320
+ # API 키가 없거나 유효하지 않은 경우
321
+ if not HAS_VALID_API_KEY or not openai_client:
322
+ return {
323
+ "error": "OpenAI API 키가 설정되지 않았습니다. 'LLM_API' 환경 변수를 확인하세요.",
324
+ "summary": "API 키가 없어 요약을 생성할 수 없습니다. 시스템 관리자에게 문의하세요."
325
+ }
326
+
327
  # 임베딩 데이터 가져오기
328
  embedding_data = await get_pdf_embedding(pdf_id)
329
  if "error" in embedding_data:
330
+ return {"error": embedding_data["error"], "summary": "PDF에서 텍스트를 추출할 수 없습니다."}
331
 
332
  # 청크 텍스트 모으기 (제한된 길이)
333
  all_text = "\n\n".join([f"Page {chunk['page']}: {chunk['text']}" for chunk in embedding_data["chunks"]])
 
339
 
340
  # OpenAI API 호출
341
  try:
342
+ # 타임아웃 및 재시도 설정 개선
343
+ for attempt in range(3): # 최대 3번 재시도
344
+ try:
345
+ response = openai_client.chat.completions.create(
346
+ model="gpt-4.1-mini",
347
+ messages=[
348
+ {"role": "system", "content": "다음 PDF 내용을 간결하게 요약해주세요. 핵심 주제와 주요 포인트를 포함한 요약을 500자 이내로 작성해주세요."},
349
+ {"role": "user", "content": f"PDF 내용:\n{all_text}"}
350
+ ],
351
+ temperature=0.7,
352
+ max_tokens=1024,
353
+ timeout=30.0 # 30초 타임아웃
354
+ )
355
+
356
+ summary = response.choices[0].message.content
357
+ return {
358
+ "summary": summary,
359
+ "pdf_id": pdf_id
360
+ }
361
+ except Exception as api_error:
362
+ logger.error(f"OpenAI API 호출 오류 (시도 {attempt+1}/3): {api_error}")
363
+ if attempt == 2: # 마지막 시도에서도 실패
364
+ raise api_error
365
+ await asyncio.sleep(1 * (attempt + 1)) # 재시도 간 지연 시간 증가
366
 
367
+ # 여기까지 도달하지 않아야 함
368
+ raise Exception("API 호출 재시도 모두 실패")
 
 
 
369
  except Exception as api_error:
370
+ logger.error(f"OpenAI API 호출 최종 오류: {api_error}")
371
+ # 오류 유형에 따른 명확한 메시지 제공
372
+ error_message = str(api_error)
373
+ if "Connection" in error_message:
374
+ return {"error": "OpenAI 서버와 연결할 수 없습니다. 인터넷 연결을 확인하세요.", "pdf_id": pdf_id}
375
+ elif "Unauthorized" in error_message or "Authentication" in error_message:
376
+ return {"error": "API 키가 유효하지 않습니다.", "pdf_id": pdf_id}
377
+ elif "Rate limit" in error_message:
378
+ return {"error": "API 호출 한도를 초과했습니다. 잠시 후 다시 시도하세요.", "pdf_id": pdf_id}
379
+ else:
380
+ return {"error": f"AI 요약 생성 중 오류가 발생했습니다: {error_message}", "pdf_id": pdf_id}
381
 
382
  except Exception as e:
383
  logger.error(f"PDF 요약 생성 오류: {e}")
384
+ return {
385
+ "error": str(e),
386
+ "summary": "PDF 요약 중 오류가 발생했습니다. PDF 페이지 수가 너무 많거나 형식이 지원되지 않을 수 있습니다."
387
+ }
388
 
389
+
390
  # 최적화된 PDF 페이지 캐싱 함수
391
  async def cache_pdf(pdf_path: str):
392
  try:
 
2341
  }
2342
 
2343
  // PDF 요약 로드 함수
2344
+ // PDF 요약 로드 함수
2345
+ async function loadPdfSummary() {
2346
+ if (!currentPdfId || isAiProcessing || hasLoadedSummary) return;
2347
+
2348
+ try {
2349
+ isAiProcessing = true;
2350
+ const typingIndicator = addTypingIndicator();
2351
+
2352
+ // 서버에 요약 요청
2353
+ const response = await fetch(`/api/ai/summarize-pdf/${currentPdfId}`);
2354
+ const data = await response.json();
2355
+
2356
+ // 로딩 표시기 제거
2357
+ typingIndicator.remove();
2358
+
2359
+ if (data.error) {
2360
+ // 오류 메시지 표시
2361
+ addChatMessage(`PDF 요약을 생성하는 중 문제가 발생했습니다: ${data.error}<br><br>계속 질문을 입력하시면 PDF 내용을 기반으로 답변을 시도하겠습니다.`);
2362
 
2363
+ // 요약이 실패해도 특정 경우에는 사용자에게 알리고 계속 사용 가능하도록 설정
2364
+ if (data.summary) {
2365
+ addChatMessage(`<strong>PDF에서 추출한 정보:</strong><br>${data.summary}`);
2366
+ hasLoadedSummary = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2367
  }
2368
+ } else {
2369
+ // 환영 메시지와 요약 추가
2370
+ addChatMessage(`안녕하세요! 이 PDF에 대해 어떤 것이든 질문해주세요. 제가 도와드리겠습니다.<br><br><strong>PDF 요약:</strong><br>${data.summary}`);
2371
+ hasLoadedSummary = true;
2372
  }
2373
+ } catch (error) {
2374
+ console.error("PDF 요약 로드 오류:", error);
2375
+ addChatMessage(`PDF 요약을 로드하는 중 오류가 발생했습니다. 서버 연결을 확인해주세요.<br><br>어떤 질문이든 입력하시면 최선을 다해 답변하겠습니다.`);
2376
+ } finally {
2377
+ isAiProcessing = false;
2378
+ }
2379
+ }
2380
+
2381
+ // 질문 제출 함수
2382
+ async function submitQuestion(question) {
2383
+ if (!currentPdfId || isAiProcessing || !question.trim()) return;
2384
+
2385
+ try {
2386
+ isAiProcessing = true;
2387
+ $id('aiChatSubmit').disabled = true;
2388
 
2389
+ // 사용자 메시지 추가
2390
+ addChatMessage(question, true);
2391
+
2392
+ // 로딩 표시기 추가
2393
+ const typingIndicator = addTypingIndicator();
2394
+
2395
+ // 서버에 질의 요청
2396
+ const response = await fetch(`/api/ai/query-pdf/${currentPdfId}`, {
2397
+ method: 'POST',
2398
+ headers: {
2399
+ 'Content-Type': 'application/json'
2400
+ },
2401
+ body: JSON.stringify({ query: question }),
2402
+ // 타임아웃 설정 추가
2403
+ signal: AbortSignal.timeout(60000) // 60초 타임아웃
2404
+ });
2405
+
2406
+ const data = await response.json();
2407
+
2408
+ // 로딩 표시기 제거
2409
+ typingIndicator.remove();
2410
+
2411
+ if (data.error) {
2412
+ // 오류 메시지에 따라 다른 친절한 안내 제공
2413
+ if (data.error.includes("API 키")) {
2414
+ addChatMessage("죄송합니다. 현재 AI 서비스에 연결할 수 없습니다. 시스템 관리자에게 API 키 설정을 확인해달라고 요청해주세요.");
2415
+ } else if (data.error.includes("연결")) {
2416
+ addChatMessage("죄송합니다. AI 서비스에 연결할 수 없습니다. 인터넷 연결을 확인하거나 잠시 후 다시 시도해주세요.");
2417
+ } else {
2418
+ addChatMessage(`죄송합니다. 질문에 답변하는 중 문제가 발생했습니다: ${data.error}`);
 
 
 
 
 
 
 
 
 
 
 
 
2419
  }
2420
+ } else {
2421
+ // AI 응답 추가
2422
+ addChatMessage(data.answer);
2423
+ }
2424
+ } catch (error) {
2425
+ console.error("질문 제출 오류:", error);
2426
+ if (error.name === 'AbortError') {
2427
+ addChatMessage("죄송합니다. 응답 시간이 너무 오래 걸려 요청이 취소되었습니다. 인터넷 연결을 확인하거나 더 짧은 질문으로 다시 시도해보세요.");
2428
+ } else {
2429
+ addChatMessage("죄송합니다. 서버와 통신 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.");
2430
  }
2431
+ } finally {
2432
+ isAiProcessing = false;
2433
+ $id('aiChatSubmit').disabled = false;
2434
+ $id('aiChatInput').value = '';
2435
+ $id('aiChatInput').focus();
2436
+ }
2437
+ }
2438
+
2439
+
2440
+
2441
+
2442
 
2443
  // DOM이 로드되면 실행
2444
  document.addEventListener('DOMContentLoaded', function() {