kwak513 commited on
Commit
99822a8
Β·
verified Β·
1 Parent(s): f7a0dd0

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +248 -0
app.py ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import json
4
+ import psycopg2
5
+ from typing import Dict, List
6
+ # from dotenv import load_dotenv
7
+
8
+ # FastAPI 및 slowapi κ΄€λ ¨ λͺ¨λ“ˆ
9
+ from fastapi import FastAPI, Request
10
+ from slowapi import Limiter, _rate_limit_exceeded_handler
11
+ from slowapi.util import get_remote_address
12
+ from slowapi.errors import RateLimitExceeded
13
+ from fastapi.middleware.cors import CORSMiddleware
14
+
15
+ # Pydantic λͺ¨λΈ
16
+ from pydantic import BaseModel
17
+
18
+ # LangChain κ΄€λ ¨ λͺ¨λ“ˆ
19
+ from langchain_google_genai import ChatGoogleGenerativeAI
20
+ from langchain_community.embeddings import HuggingFaceEmbeddings
21
+ from langchain_community.vectorstores import PGVector
22
+ from langchain_core.messages import SystemMessage
23
+ from langchain.chains import ConversationalRetrievalChain
24
+ from langchain.memory import ConversationBufferMemory
25
+ from langchain_core.documents import Document # Document νƒ€μž… 힌트용으둜 μΆ”κ°€
26
+ # from pdf_importer import create_vector_store, CONNECTION_STRING, COLLECTION_NAME
27
+
28
+ # ν™˜κ²½ λ³€μˆ˜ λ‘œλ“œ (Hugging Face Secretsμ—μ„œ κ°€μ Έμ˜΄)
29
+ POSTGRES_USER = os.getenv('POSTGRES_USER')
30
+ POSTGRES_PASSWORD = os.getenv('POSTGRES_PASSWORD')
31
+ POSTGRES_HOST = os.getenv('POSTGRES_HOST')
32
+ POSTGRES_PORT = os.getenv('POSTGRES_PORT')
33
+ POSTGRES_DB = os.getenv('POSTGRES_DB')
34
+ GOOGLE_API_KEY = os.getenv('GOOGLE_API_KEY')
35
+
36
+ COLLECTION_NAME = "homepage_pdfplumner_1st"
37
+
38
+ # 2. ν•„μˆ˜ ν™˜κ²½ λ³€μˆ˜κ°€ λͺ¨λ‘ μ‘΄μž¬ν•˜λŠ”μ§€ ν™•μΈν•©λ‹ˆλ‹€.
39
+ if not all([POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DB, COLLECTION_NAME, GOOGLE_API_KEY]):
40
+ raise ValueError("ν•„μˆ˜ ν™˜κ²½ λ³€μˆ˜λ“€μ΄ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. Hugging Face Secretsλ₯Ό ν™•μΈν•˜μ„Έμš”.")
41
+
42
+ # ν™˜κ²½ λ³€μˆ˜λ₯Ό μ‘°ν•©ν•˜μ—¬ CONNECTION_STRING을 생성
43
+ CONNECTION_STRING = f"postgresql+psycopg2://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DB}"
44
+
45
+
46
+ # load_dotenv()
47
+ app = FastAPI()
48
+ limiter = Limiter(key_func=get_remote_address)
49
+ app.state.limiter = limiter
50
+ app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
51
+
52
+ app.add_middleware(
53
+ CORSMiddleware,
54
+ allow_origins=["*"],
55
+ allow_credentials=True,
56
+ allow_methods=["*"],
57
+ allow_headers=["*"],
58
+ )
59
+
60
+
61
+ # RAG ꡬ성 μš”μ†Œλ₯Ό ν”„λ‘œκ·Έλž¨ μ‹œμž‘ μ‹œ ν•œ 번만 μ΄ˆκΈ°ν™”
62
+ embeddings = HuggingFaceEmbeddings(
63
+ model_name='nlpai-lab/KURE-v1',
64
+ model_kwargs={'device': 'cpu'}
65
+ )
66
+
67
+ try:
68
+ vector_store = PGVector(
69
+ collection_name=COLLECTION_NAME,
70
+ connection_string=CONNECTION_STRING,
71
+ embedding_function=embeddings
72
+ )
73
+ print("Vector store loaded from PostgreSQL.")
74
+ except Exception as e:
75
+ print(f"Error connecting to PostgreSQL: {e}")
76
+ import sys
77
+ sys.exit(1)
78
+
79
+ llm = ChatGoogleGenerativeAI(
80
+ # model="gemini-1.5-flash-8b",
81
+ model="gemini-2.5-flash-lite",
82
+ model_kwargs={
83
+ "system_instruction": SystemMessage(
84
+ content=
85
+ # """당신은 ν•œκ΅­μ™Έκ΅­μ–΄λŒ€ν•™κ΅(μ„œμšΈ) 학사 μ „λ¬Έκ°€μž…λ‹ˆλ‹€. λ‹΅λ³€ 원칙: 1. ν•œκ΅­μ™Έκ΅­μ–΄λŒ€ν•™κ΅(μ„œμšΈ) κ΄€λ ¨ μ§ˆλ¬Έμ— μ •ν™•νžˆ λ‹΅λ³€ν•©λ‹ˆλ‹€. 2. 이전 λŒ€ν™” λ§₯락을 κΈ°μ–΅ν•˜κ³  μœ μ—°ν•˜κ²Œ μ‘λ‹΅ν•©λ‹ˆλ‹€. 3. μΉœμ ˆν•˜κ³  μ΄ν•΄ν•˜κΈ° μ‰¬μš΄ 말투λ₯Ό μ‚¬μš©ν•˜λ©°, λ°˜λ“œμ‹œ μ™„μ „ν•œ λ¬Έμž₯으둜 λ‹΅λ³€ν•©λ‹ˆλ‹€. 4. μ°Έκ³  정보에 μ—†λŠ” λ‚΄μš©μ€ μ ˆλŒ€ μΆ”μΈ‘ν•˜κ±°λ‚˜ μž„μ˜λ‘œ λ‹΅λ³€ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. λ‹΅λ³€ κ·œμΉ™: - ν•œκ΅­μ™Έκ΅­μ–΄λŒ€ν•™κ΅(μ„œμšΈ)κ³Ό κ΄€λ ¨ μ—†λŠ” 질문: "μ£„μ†‘ν•©λ‹ˆλ‹€. ν•œκ΅­μ™Έκ΅­μ–΄λŒ€ν•™κ΅(μ„œμšΈ) κ΄€λ ¨ μ§ˆλ¬Έμ—λ§Œ λ‹΅λ³€λ“œλ¦΄ 수 μžˆμŠ΅λ‹ˆλ‹€."라고 λ‹΅λ³€ν•˜μ„Έμš”. - μ‚¬μš©μžμ˜ 질문과 κ΄€λ ¨λœ 정보가 μ°Έκ³  λ¬Έμ„œμ— λͺ…ν™•ν•˜κ²Œ μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 경우, μ–΄λ–€ λ‚΄μš©λ„ μΆ”λ‘ ν•˜κ±°λ‚˜ 덧뢙이지 말고 무쑰건 "μ£„μ†‘ν•©λ‹ˆλ‹€. ν•΄λ‹Ή 정보λ₯Ό 확인할 수 μ—†μŠ΅λ‹ˆλ‹€."라고 λ‹΅λ³€ν•˜μ„Έμš”."""
86
+ """
87
+
88
+ 당신은 ν•œκ΅­μ™Έκ΅­μ–΄λŒ€ν•™κ΅(μ„œμšΈ)의 **'학사 μƒν™œ AI μ–΄λ“œλ°”μ΄μ €'**μž…λ‹ˆλ‹€. λ‹Ήμ‹ μ˜ 지식은 μ£Όμ–΄μ§„ [학사 κ·œμ •]κ³Ό [μ£Όλ³€ μƒκΆŒ 정보] λ¬Έμ„œλ‘œ ν•œμ •λ©λ‹ˆλ‹€. λ‹Ήμ‹ μ˜ μž„λ¬΄λŠ” 이 지식 λ‚΄μ—μ„œ ν•™μƒλ“€μ˜ μ§ˆλ¬Έμ— λͺ…ν™•ν•˜κ³  μΉœμ ˆν•œ μ „λ¬Έκ°€μ˜ μ–΄μ‘°λ‘œ λ‹΅λ³€ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€.
89
+
90
+ [λ‹΅λ³€ 원칙]
91
+
92
+ 1. μ •ν™•μ„±: λ°˜λ“œμ‹œ μ£Όμ–΄μ§„ μ°Έκ³  λ¬Έμ„œμ˜ λ‚΄μš©μ—λ§Œ κ·Όκ±°ν•˜μ—¬ λ‹΅λ³€ν•©λ‹ˆλ‹€.
93
+
94
+ 2. μΉœμ ˆν•¨: 항상 μΉœμ ˆν•˜κ³  μ΄ν•΄ν•˜κΈ° μ‰¬μš΄ μ™„μ „ν•œ λ¬Έμž₯으둜 λ‹΅λ³€ν•©λ‹ˆλ‹€.
95
+
96
+ 3. λ§₯락 이해: 이전 λŒ€ν™” λ‚΄μš©μ„ κΈ°μ–΅ν•˜μ—¬ μžμ—°μŠ€λŸ¬μš΄ λŒ€ν™”λ₯Ό μ΄μ–΄κ°‘λ‹ˆλ‹€.
97
+
98
+ 4. 지식 λ‚΄μž¬ν™”: 당신은 λ¬Έμ„œλ₯Ό λ‹¨μˆœνžˆ μ „λ‹¬ν•˜λŠ” λ‘œλ΄‡μ΄ μ•„λ‹™λ‹ˆλ‹€. μ£Όμ–΄μ§„ μ°Έκ³  λ¬Έμ„œλŠ” λ‹Ήμ‹ μ˜ '지식'μž…λ‹ˆλ‹€. λ‹΅λ³€ μ‹œ, '제곡된 정보', 'μ°Έκ³  λ¬Έμ„œ', 'μ£Όμ–΄μ§„ ν…μŠ€νŠΈ', 'ν‘œ', '문단' λ“± 당신이 정보λ₯Ό μ–΄λ–»κ²Œ μ–»μ—ˆλŠ”μ§€ μ•”μ‹œν•˜λŠ” κ·Έ μ–΄λ–€ 단어도 μ ˆλŒ€ μ‚¬μš©ν•˜μ§€ λ§ˆμ„Έμš”. κ²€μƒ‰λœ λͺ¨λ“  정보λ₯Ό μ™„μ „νžˆ μžμ‹ μ˜ 지식인 κ²ƒμ²˜λŸΌ μ’…ν•©ν•˜κ³  μžμ—°μŠ€λŸ½κ²Œ μž¬κ΅¬μ„±ν•˜μ—¬, 마치 μ›λž˜λΆ€ν„° μ•Œκ³  μžˆμ—ˆλ˜ κ²ƒμ²˜λŸΌ μ‚¬μš©μžμ—κ²Œ 직접 μ„€λͺ…ν•΄μ•Ό ν•©λ‹ˆλ‹€.
99
+
100
+ 5. ν•œκ΅­μ–΄ 사��: λͺ¨λ“  닡변은 λ°˜λ“œμ‹œ μ™„λ²½ν•œ ν•œκ΅­μ–΄λ‘œλ§Œ 생성해야 ν•©λ‹ˆλ‹€.
101
+
102
+ [λ‹΅λ³€ κ·œμΉ™]
103
+
104
+ 1. μžκΈ°μ†Œκ°œ: λ§Œμ•½ μ‚¬μš©μžκ°€ λ‹Ήμ‹ μ˜ 정체성에 λŒ€ν•΄ λ¬»λŠ”λ‹€λ©΄(예: "λ„ˆλŠ” λˆ„κ΅¬μ•Ό?", "이름이 뭐야?"), "μ•ˆλ…•ν•˜μ„Έμš”! μ €λŠ” ν•œκ΅­μ™Έκ΅­μ–΄λŒ€ν•™κ΅ ν•™μƒλ“€μ˜ 캠퍼슀 μƒν™œμ„ 돕기 μœ„ν•΄ λ§Œλ“€μ–΄μ§„ '학사 μƒν™œ AI μ–΄λ“œλ°”μ΄μ €'μž…λ‹ˆλ‹€. 학사 μ •λ³΄λ‚˜ 학ꡐ μƒν™œμ— λŒ€ν•΄ κΆκΈˆν•œ 점이 μžˆλ‹€λ©΄ 무엇이든 λ¬Όμ–΄λ³΄μ„Έμš”." 라고 μ •ν™•νžˆ μ†Œκ°œν•΄μ•Ό ν•©λ‹ˆλ‹€. μ ˆλŒ€λ‘œ 'Google의 μ–Έμ–΄ λͺ¨λΈ'μ΄λ‚˜ λ§ˆμŠ€μ½”νŠΈ 'λΆ€(Boo)'라고 μžμ‹ μ„ μ†Œκ°œν•΄μ„œλŠ” μ•ˆ λ©λ‹ˆλ‹€.
105
+
106
+ 1. λ²”μœ„ μ™Έ 질문 νŒλ‹¨: λ‹Ήμ‹ μ˜ 지식 λ²”μœ„(학사, μ£Όλ³€ λ§›μ§‘)와 λͺ…λ°±νžˆ κ΄€λ ¨ μ—†λŠ” 질문(예: 금육, 슀포츠)μ—λŠ” "μ£„μ†‘ν•©λ‹ˆλ‹€. μ €λŠ” ν•œκ΅­μ™Έκ΅­μ–΄λŒ€ν•™κ΅ 학사 및 캠퍼슀 μƒν™œ 정보에 λŒ€ν•΄μ„œλ§Œ λ‹΅λ³€ν•  수 μžˆμŠ΅λ‹ˆλ‹€." 라고 λ‹΅λ³€ν•˜μ„Έμš”. '제곡된 정보에 μ—†λ‹€'λŠ” μ‹μ˜ λΆ€μ—° μ„€λͺ…은 μ ˆλŒ€ 덧뢙이지 λ§ˆμ„Έμš”.
107
+
108
+ 2. 정보 μš°μ„ μˆœμœ„ νŒλ³„: μ—¬λŸ¬ 개의 μ°Έκ³  λ¬Έμ„œκ°€ μ£Όμ–΄μ§€λ©΄, κ·Έμ€‘μ—μ„œ μ‚¬μš©μžμ˜ μ§ˆλ¬Έμ— κ°€μž₯ μ§μ ‘μ μœΌλ‘œ λ‹΅ν•  수 μžˆλŠ” 핡심 정보λ₯Ό λ¨Όμ € μ‹λ³„ν•˜μ„Έμš”. 관련성이 λ–¨μ–΄μ§€κ±°λ‚˜ 뢀차적인 μ •λ³΄λŠ” 닡변에 ν¬ν•¨ν•˜μ§€ μ•Šκ±°λ‚˜, κΌ­ ν•„μš”ν•œ κ²½μš°μ—λ§Œ κ°„λž΅ν•˜κ²Œ 덧뢙여 μ„€λͺ…ν•˜μ„Έμš”.
109
+
110
+ 3. ν‘œ(Table) 뢄석: μ°Έκ³  λ¬Έμ„œμ— ν‘œκ°€ ν¬ν•¨λœ 경우, 당신은 ν‘œ 뢄석 μ „λ¬Έκ°€λ‘œμ„œ ν–‰κ³Ό μ—΄μ˜ 관계λ₯Ό μ •ν™•νžˆ ν•΄μ„ν•˜μ—¬ λ‹΅λ³€ν•΄μ•Ό ν•©λ‹ˆλ‹€.
111
+
112
+ 4. 쑰건뢀 λ‹΅λ³€: λ§Œμ•½ ν‘œλ‚˜ ν…μŠ€νŠΈμ— ν•™κ³Ό, ν•™λ²ˆ λ“± μ„ΈλΆ€ 쑰건이 λͺ…μ‹œλ˜μ–΄ μžˆμ§€ μ•Šλ‹€λ©΄, "μ œμ‹œλœ μžλ£Œμ— λ”°λ₯΄λ©΄ 일반적으둜" λ˜λŠ” "2025학년도 κΈ°μ€€μœΌλ‘œλŠ”" κ³Ό 같이 μ •λ³΄μ˜ μΆœμ²˜λ‚˜ 기쀀을 λͺ…ν™•νžˆ 밝히며 λ‹΅λ³€ν•˜μ„Έμš”.
113
+
114
+ 5. 닀쀑 정보 처리: λ§Œμ•½ μ‚¬μš©μžμ˜ μ§ˆλ¬Έμ— λŒ€ν•΄ μ—¬λŸ¬ λ¬Έμ„œμ—μ„œ μ„œλ‘œ λ‹€λ₯Έ 정보가 검색될 경우, ν•˜λ‚˜μ˜ μ •λ³΄λ§Œ μ„ νƒν•˜μ§€ λ§ˆμ„Έμš”. λŒ€μ‹ , 각각의 쑰건과 λ‚΄μš©μ„ λͺ…ν™•νžˆ κ΅¬λΆ„ν•˜μ—¬ λͺ¨λ“  정보λ₯Ό μ’…ν•©μ μœΌλ‘œ μ•ˆλ‚΄ν•΄μ•Ό ν•©λ‹ˆλ‹€.
115
+
116
+ 6. μ˜ˆμ™Έ κ°€λŠ₯μ„± 인지: 학사 κ·œμ •μ€ λ‹¨κ³ΌλŒ€ν•™, ν•™κ³Ό, ν•™λ²ˆλ³„λ‘œ μ˜ˆμ™Έ κ·œμΉ™μ΄ μ‘΄μž¬ν•  수 μžˆλ‹€λŠ” 사싀을 항상 μΈμ§€ν•˜μ„Έμš”. λ§Œμ•½ 일반적인 κ·œμΉ™μ„ μ°Ύμ•˜λ”λΌλ„, "μΌλ°˜μ μœΌλ‘œλŠ” OO학점이 ν•„μš”ν•˜μ§€λ§Œ, μ†Œμ† λ‹¨κ³ΌλŒ€ν•™μ΄λ‚˜ 학과에 따라 λ‹€λ₯Ό 수 μžˆμœΌλ‹ˆ μ •ν™•ν•œ μ •λ³΄λŠ” 학ꡐ 곡식 λ¬Έμ„œλ₯Ό ν™•μΈν•˜μ‹œκ±°λ‚˜ ν•™κ³Ό 사무싀에 λ¬Έμ˜ν•˜λŠ” 것을 ꢌμž₯ν•©λ‹ˆλ‹€" 와 같이 닡변에 'μ£Όμ˜μ‚¬ν•­'κ³Ό 'ν•œκ³„'λ₯Ό λͺ…μ‹œν•˜μ„Έμš”.
117
+
118
+ 7. 정보 λΆ€μž¬ μ‹œ: μœ„μ˜ λͺ¨λ“  λ…Έλ ₯에도 λΆˆκ΅¬ν•˜κ³  μ§ˆλ¬Έμ— λŒ€ν•œ 닡변을 μ°Έκ³  λ¬Έμ„œμ—μ„œ 찾을 수 μ—†λŠ” κ²½μš°μ—λ§Œ, "μ£„μ†‘ν•©λ‹ˆλ‹€. λ¬Έμ˜ν•˜μ‹  λ‚΄μš©μ— λŒ€ν•œ μ •λ³΄λŠ” μ œκ°€ κ°€μ§„ μžλ£Œμ—μ„œ 확인할 수 μ—†μŠ΅λ‹ˆλ‹€."라고 λ‹΅λ³€ν•˜μ„Έμš”.
119
+
120
+ """
121
+
122
+
123
+ ),
124
+ }
125
+ )
126
+
127
+ retriever = vector_store.as_retriever(search_kwargs={"k": 3})
128
+
129
+ # retriever = MultiQueryRetriever.from_llm(
130
+ # retriever=vector_store.as_retriever(search_kwargs={"k": 5}),
131
+ # llm=llm
132
+ # )
133
+
134
+ # μ‚¬μš©μž μ„Έμ…˜λ³„ λŒ€ν™” 체인을 μ €μž₯ν•  λ”•μ…”λ„ˆλ¦¬
135
+ chat_sessions: Dict[str, ConversationalRetrievalChain] = {}
136
+
137
+ def get_or_create_chain(session_id: str) -> ConversationalRetrievalChain:
138
+ if session_id not in chat_sessions:
139
+ memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True,
140
+ input_key="question", # <-- μΆ”κ°€
141
+ output_key="answer" )
142
+ new_chain = ConversationalRetrievalChain.from_llm(
143
+ llm=llm,
144
+ retriever=retriever,
145
+ memory=memory,
146
+ return_source_documents=True, # μ°Έκ³  λ¬Έμ„œ λ°˜ν™˜ ν™œμ„±ν™”
147
+ output_key="answer"
148
+ )
149
+ chat_sessions[session_id] = new_chain
150
+ print(f"μƒˆλ‘œμš΄ μ„Έμ…˜ ID 생성: {session_id}")
151
+ return chat_sessions[session_id]
152
+
153
+
154
+ class ChatMessage(BaseModel):
155
+ message: str
156
+ session_id: str
157
+ user_id: str # μ‚¬μš©μž 식별을 μœ„ν•΄ μΆ”κ°€
158
+
159
+ class ChatResponse(BaseModel):
160
+ response: str
161
+ success: bool
162
+ # source_documents ν•„λ“œλ₯Ό μΆ”κ°€ν•˜μ—¬ ν”„λ‘ νŠΈμ—”λ“œλ‘œλ„ 보낼 수 μžˆλ„λ‘ μ€€λΉ„
163
+ source_documents: List[Dict[str, str]] = [] # λ¬Έμ„œ λ‚΄μš©κ³Ό 메타데이터 μ €μž₯
164
+
165
+
166
+
167
+
168
+
169
+ @app.post("/api/chat", response_model=ChatResponse)
170
+ @limiter.limit("15/minute")
171
+ async def chat_with_gemini(request: Request):
172
+ start_time = time.time()
173
+
174
+ try:
175
+
176
+ # JSON bodyλ₯Ό 직접 νŒŒμ‹±
177
+ body = await request.json()
178
+ chat_message = ChatMessage(**body)
179
+
180
+ # qa_chain = get_or_create_chain(request.session_id)
181
+ # result = qa_chain.invoke({"question": request.message})
182
+ qa_chain = get_or_create_chain(chat_message.session_id)
183
+ result = qa_chain.invoke({"question": chat_message.message})
184
+
185
+
186
+ # μ°Έκ³  λ¬Έμ„œ μΆ”μΆœ 및 둜그 좜λ ₯
187
+ source_documents_for_response: List[Dict[str, str]] = []
188
+ if 'source_documents' in result and result['source_documents']:
189
+ print("\n--- μ°Έκ³  λ¬Έμ„œ ---")
190
+ for i, doc in enumerate(result['source_documents']):
191
+ print(f"λ¬Έμ„œ {i+1}:")
192
+ print(f" μ†ŒμŠ€: {doc.metadata.get('source', 'μ•Œ 수 μ—†μŒ')}")
193
+ print(f" λ‚΄μš© (일뢀): {doc.page_content[:200]}...") # λ‚΄μš©μ˜ μΌλΆ€λ§Œ 좜λ ₯
194
+ # ν”„λ‘ νŠΈμ—”λ“œ 응닡을 μœ„ν•΄ μ €μž₯
195
+ source_documents_for_response.append({
196
+ "source": doc.metadata.get('source', 'μ•Œ 수 μ—†μŒ'),
197
+ "content": doc.page_content # 전체 λ‚΄μš©μ„ 보낼 μˆ˜λ„ 있음
198
+ })
199
+ print("---------------\n")
200
+ # ==========================================================
201
+ # β–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Ό 이 λΆ€λΆ„λ§Œ μΆ”κ°€ β–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Ό
202
+ # ==========================================================
203
+
204
+ response_time_ms = int((time.time() - start_time) * 1000)
205
+
206
+ # DB에 둜그 μ €μž₯
207
+ try:
208
+ db_conn_str = CONNECTION_STRING.replace("postgresql+psycopg2", "postgresql")
209
+ conn = psycopg2.connect(db_conn_str)
210
+ cur = conn.cursor()
211
+ cur.execute(
212
+ """
213
+ INSERT INTO chat_logs (session_id, user_id, user_question, bot_answer, retrieved_sources, response_time_ms)
214
+ VALUES (%s, %s, %s, %s, %s, %s);
215
+ """,
216
+ (chat_message.session_id, chat_message.user_id, chat_message.message, result['answer'], json.dumps(source_documents_for_response), response_time_ms)
217
+ )
218
+ conn.commit()
219
+ cur.close()
220
+ conn.close()
221
+ except Exception as db_error:
222
+ print(f"DB 둜그 μ €μž₯ μ‹€νŒ¨: {db_error}")
223
+
224
+ # ==========================================================
225
+ # β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–² 이 λΆ€λΆ„λ§Œ μΆ”κ°€ β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²
226
+ # ==========================================================
227
+
228
+
229
+
230
+
231
+ return ChatResponse(
232
+ response=result['answer'],
233
+ success=True,
234
+ source_documents=source_documents_for_response # 응닡에 μ°Έκ³  λ¬Έμ„œ μΆ”κ°€
235
+ )
236
+ except Exception as e:
237
+ print(f"였λ₯˜ λ°œμƒ: {str(e)}")
238
+ return ChatResponse(
239
+ response=f"였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {str(e)}",
240
+ success=False,
241
+ source_documents=[]
242
+ )
243
+
244
+ @app.get("/")
245
+ async def root():
246
+ return {"message": "ν•œκ΅­μ™Έκ΅­μ–΄λŒ€ν•™κ΅(μ„œμšΈ) 학사 챗봇 API"}
247
+
248
+