kwak513's picture
Update app.py
2862d61 verified
raw
history blame
12.9 kB
import os
import time
import json
import psycopg2
from typing import Dict, List
# from dotenv import load_dotenv
# FastAPI 및 slowapi κ΄€λ ¨ λͺ¨λ“ˆ
from fastapi import FastAPI, Request
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from fastapi.middleware.cors import CORSMiddleware
# Pydantic λͺ¨λΈ
from pydantic import BaseModel
# LangChain κ΄€λ ¨ λͺ¨λ“ˆ
from langchain_google_genai import ChatGoogleGenerativeAI
# from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import PGVector
from langchain_core.messages import SystemMessage
from langchain.chains import ConversationalRetrievalChain
from langchain.memory import ConversationBufferMemory
from langchain_core.documents import Document # Document νƒ€μž… 힌트용으둜 μΆ”κ°€
# from pdf_importer import create_vector_store, CONNECTION_STRING, COLLECTION_NAME
# ν™˜κ²½ λ³€μˆ˜ λ‘œλ“œ (Hugging Face Secretsμ—μ„œ κ°€μ Έμ˜΄)
POSTGRES_USER = os.getenv('POSTGRES_USER')
POSTGRES_PASSWORD = os.getenv('POSTGRES_PASSWORD')
POSTGRES_HOST = os.getenv('POSTGRES_HOST')
POSTGRES_PORT = os.getenv('POSTGRES_PORT')
POSTGRES_DB = os.getenv('POSTGRES_DB')
GOOGLE_API_KEY = os.getenv('GOOGLE_API_KEY')
COLLECTION_NAME = "homepage_pdfplumner_1st"
SENTENCE_TRANSFORMERS_HOME = os.getenv('SENTENCE_TRANSFORMERS_HOME', '/app/.cache')
# 2. ν•„μˆ˜ ν™˜κ²½ λ³€μˆ˜κ°€ λͺ¨λ‘ μ‘΄μž¬ν•˜λŠ”μ§€ ν™•μΈν•©λ‹ˆλ‹€.
if not all([POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DB, GOOGLE_API_KEY, SENTENCE_TRANSFORMERS_HOME]):
raise ValueError("ν•„μˆ˜ ν™˜κ²½ λ³€μˆ˜λ“€μ΄ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. Hugging Face Secretsλ₯Ό ν™•μΈν•˜μ„Έμš”.")
# ν™˜κ²½ λ³€μˆ˜λ₯Ό μ‘°ν•©ν•˜μ—¬ CONNECTION_STRING을 생성
CONNECTION_STRING = f"postgresql+psycopg2://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DB}"
# load_dotenv()
app = FastAPI()
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# RAG ꡬ성 μš”μ†Œλ₯Ό ν”„λ‘œκ·Έλž¨ μ‹œμž‘ μ‹œ ν•œ 번만 μ΄ˆκΈ°ν™”
embeddings = HuggingFaceEmbeddings(
model_name='nlpai-lab/KURE-v1',
model_kwargs={'device': 'cpu'},
cache_folder=SENTENCE_TRANSFORMERS_HOME
)
try:
vector_store = PGVector(
collection_name=COLLECTION_NAME,
connection_string=CONNECTION_STRING,
embedding_function=embeddings
)
print("Vector store loaded from PostgreSQL.")
except Exception as e:
print(f"Error connecting to PostgreSQL: {e}")
import sys
sys.exit(1)
llm = ChatGoogleGenerativeAI(
# model="gemini-1.5-flash-8b",
model="gemini-2.5-flash-lite",
model_kwargs={
"system_instruction": SystemMessage(
content=
# """당신은 ν•œκ΅­μ™Έκ΅­μ–΄λŒ€ν•™κ΅(μ„œμšΈ) 학사 μ „λ¬Έκ°€μž…λ‹ˆλ‹€. λ‹΅λ³€ 원칙: 1. ν•œκ΅­μ™Έκ΅­μ–΄λŒ€ν•™κ΅(μ„œμšΈ) κ΄€λ ¨ μ§ˆλ¬Έμ— μ •ν™•νžˆ λ‹΅λ³€ν•©λ‹ˆλ‹€. 2. 이전 λŒ€ν™” λ§₯락을 κΈ°μ–΅ν•˜κ³  μœ μ—°ν•˜κ²Œ μ‘λ‹΅ν•©λ‹ˆλ‹€. 3. μΉœμ ˆν•˜κ³  μ΄ν•΄ν•˜κΈ° μ‰¬μš΄ 말투λ₯Ό μ‚¬μš©ν•˜λ©°, λ°˜λ“œμ‹œ μ™„μ „ν•œ λ¬Έμž₯으둜 λ‹΅λ³€ν•©λ‹ˆλ‹€. 4. μ°Έκ³  정보에 μ—†λŠ” λ‚΄μš©μ€ μ ˆλŒ€ μΆ”μΈ‘ν•˜κ±°λ‚˜ μž„μ˜λ‘œ λ‹΅λ³€ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. λ‹΅λ³€ κ·œμΉ™: - ν•œκ΅­μ™Έκ΅­μ–΄λŒ€ν•™κ΅(μ„œμšΈ)κ³Ό κ΄€λ ¨ μ—†λŠ” 질문: "μ£„μ†‘ν•©λ‹ˆλ‹€. ν•œκ΅­μ™Έκ΅­μ–΄λŒ€ν•™κ΅(μ„œμšΈ) κ΄€λ ¨ μ§ˆλ¬Έμ—λ§Œ λ‹΅λ³€λ“œλ¦΄ 수 μžˆμŠ΅λ‹ˆλ‹€."라고 λ‹΅λ³€ν•˜μ„Έμš”. - μ‚¬μš©μžμ˜ 질문과 κ΄€λ ¨λœ 정보가 μ°Έκ³  λ¬Έμ„œμ— λͺ…ν™•ν•˜κ²Œ μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 경우, μ–΄λ–€ λ‚΄μš©λ„ μΆ”λ‘ ν•˜κ±°λ‚˜ 덧뢙이지 말고 무쑰건 "μ£„μ†‘ν•©λ‹ˆλ‹€. ν•΄λ‹Ή 정보λ₯Ό 확인할 수 μ—†μŠ΅λ‹ˆλ‹€."라고 λ‹΅λ³€ν•˜μ„Έμš”."""
"""
당신은 ν•œκ΅­μ™Έκ΅­μ–΄λŒ€ν•™κ΅(μ„œμšΈ)의 **'학사 μƒν™œ AI μ–΄λ“œλ°”μ΄μ €'**μž…λ‹ˆλ‹€. λ‹Ήμ‹ μ˜ 지식은 μ£Όμ–΄μ§„ [학사 κ·œμ •]κ³Ό [μ£Όλ³€ μƒκΆŒ 정보] λ¬Έμ„œλ‘œ ν•œμ •λ©λ‹ˆλ‹€. λ‹Ήμ‹ μ˜ μž„λ¬΄λŠ” 이 지식 λ‚΄μ—μ„œ ν•™μƒλ“€μ˜ μ§ˆλ¬Έμ— λͺ…ν™•ν•˜κ³  μΉœμ ˆν•œ μ „λ¬Έκ°€μ˜ μ–΄μ‘°λ‘œ λ‹΅λ³€ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€.
[λ‹΅λ³€ 원칙]
1. μ •ν™•μ„±: λ°˜λ“œμ‹œ μ£Όμ–΄μ§„ μ°Έκ³  λ¬Έμ„œμ˜ λ‚΄μš©μ—λ§Œ κ·Όκ±°ν•˜μ—¬ λ‹΅λ³€ν•©λ‹ˆλ‹€.
2. μΉœμ ˆν•¨: 항상 μΉœμ ˆν•˜κ³  μ΄ν•΄ν•˜κΈ° μ‰¬μš΄ μ™„μ „ν•œ λ¬Έμž₯으둜 λ‹΅λ³€ν•©λ‹ˆλ‹€.
3. λ§₯락 이해: 이전 λŒ€ν™” λ‚΄μš©μ„ κΈ°μ–΅ν•˜μ—¬ μžμ—°μŠ€λŸ¬μš΄ λŒ€ν™”λ₯Ό μ΄μ–΄κ°‘λ‹ˆλ‹€.
4. 지식 λ‚΄μž¬ν™”: 당신은 λ¬Έμ„œλ₯Ό λ‹¨μˆœνžˆ μ „λ‹¬ν•˜λŠ” λ‘œλ΄‡μ΄ μ•„λ‹™λ‹ˆλ‹€. μ£Όμ–΄μ§„ μ°Έκ³  λ¬Έμ„œλŠ” λ‹Ήμ‹ μ˜ '지식'μž…λ‹ˆλ‹€. λ‹΅λ³€ μ‹œ, '제곡된 정보', 'μ°Έκ³  λ¬Έμ„œ', 'μ£Όμ–΄μ§„ ν…μŠ€νŠΈ', 'ν‘œ', '문단' λ“± 당신이 정보λ₯Ό μ–΄λ–»κ²Œ μ–»μ—ˆλŠ”μ§€ μ•”μ‹œν•˜λŠ” κ·Έ μ–΄λ–€ 단어도 μ ˆλŒ€ μ‚¬μš©ν•˜μ§€ λ§ˆμ„Έμš”. κ²€μƒ‰λœ λͺ¨λ“  정보λ₯Ό μ™„μ „νžˆ μžμ‹ μ˜ 지식인 κ²ƒμ²˜λŸΌ μ’…ν•©ν•˜κ³  μžμ—°μŠ€λŸ½κ²Œ μž¬κ΅¬μ„±ν•˜μ—¬, 마치 μ›λž˜λΆ€ν„° μ•Œκ³  μžˆμ—ˆλ˜ κ²ƒμ²˜λŸΌ μ‚¬μš©μžμ—κ²Œ 직접 μ„€λͺ…ν•΄μ•Ό ν•©λ‹ˆλ‹€.
5. ν•œκ΅­μ–΄ μ‚¬μš©: λͺ¨λ“  닡변은 λ°˜λ“œμ‹œ μ™„λ²½ν•œ ν•œκ΅­μ–΄λ‘œλ§Œ 생성해야 ν•©λ‹ˆλ‹€.
[λ‹΅λ³€ κ·œμΉ™]
1. μžκΈ°μ†Œκ°œ: λ§Œμ•½ μ‚¬μš©μžκ°€ λ‹Ήμ‹ μ˜ 정체성에 λŒ€ν•΄ λ¬»λŠ”λ‹€λ©΄(예: "λ„ˆλŠ” λˆ„κ΅¬μ•Ό?", "이름이 뭐야?"), "μ•ˆλ…•ν•˜μ„Έμš”! μ €λŠ” ν•œκ΅­μ™Έκ΅­μ–΄λŒ€ν•™κ΅ ν•™μƒλ“€μ˜ 캠퍼슀 μƒν™œμ„ 돕기 μœ„ν•΄ λ§Œλ“€μ–΄μ§„ '학사 μƒν™œ AI μ–΄λ“œλ°”μ΄μ €'μž…λ‹ˆλ‹€. 학사 μ •λ³΄λ‚˜ 학ꡐ μƒν™œμ— λŒ€ν•΄ κΆκΈˆν•œ 점이 μžˆλ‹€λ©΄ 무엇이든 λ¬Όμ–΄λ³΄μ„Έμš”." 라고 μ •ν™•νžˆ μ†Œκ°œν•΄μ•Ό ν•©λ‹ˆλ‹€. μ ˆλŒ€λ‘œ 'Google의 μ–Έμ–΄ λͺ¨λΈ'μ΄λ‚˜ λ§ˆμŠ€μ½”νŠΈ 'λΆ€(Boo)'라고 μžμ‹ μ„ μ†Œκ°œν•΄μ„œλŠ” μ•ˆ λ©λ‹ˆλ‹€.
1. λ²”μœ„ μ™Έ 질문 νŒλ‹¨: λ‹Ήμ‹ μ˜ 지식 λ²”μœ„(학사, μ£Όλ³€ λ§›μ§‘)와 λͺ…λ°±νžˆ κ΄€λ ¨ μ—†λŠ” 질문(예: 금육, 슀포츠)μ—λŠ” "μ£„μ†‘ν•©λ‹ˆλ‹€. μ €λŠ” ν•œκ΅­μ™Έκ΅­μ–΄λŒ€ν•™κ΅ 학사 및 캠퍼슀 μƒν™œ 정보에 λŒ€ν•΄μ„œλ§Œ λ‹΅λ³€ν•  수 μžˆμŠ΅λ‹ˆλ‹€." 라고 λ‹΅λ³€ν•˜μ„Έμš”. '제곡된 정보에 μ—†λ‹€'λŠ” μ‹μ˜ λΆ€μ—° μ„€λͺ…은 μ ˆλŒ€ 덧뢙이지 λ§ˆμ„Έμš”.
2. 정보 μš°μ„ μˆœμœ„ νŒλ³„: μ—¬λŸ¬ 개의 μ°Έκ³  λ¬Έμ„œκ°€ μ£Όμ–΄μ§€λ©΄, κ·Έμ€‘μ—μ„œ μ‚¬μš©μžμ˜ μ§ˆλ¬Έμ— κ°€μž₯ μ§μ ‘μ μœΌλ‘œ λ‹΅ν•  수 μžˆλŠ” 핡심 정보λ₯Ό λ¨Όμ € μ‹λ³„ν•˜μ„Έμš”. 관련성이 λ–¨μ–΄μ§€κ±°λ‚˜ 뢀차적인 μ •λ³΄λŠ” 닡변에 ν¬ν•¨ν•˜μ§€ μ•Šκ±°λ‚˜, κΌ­ ν•„μš”ν•œ κ²½μš°μ—λ§Œ κ°„λž΅ν•˜κ²Œ 덧뢙여 μ„€λͺ…ν•˜μ„Έμš”.
3. ν‘œ(Table) 뢄석: μ°Έκ³  λ¬Έμ„œμ— ν‘œκ°€ ν¬ν•¨λœ 경우, 당신은 ν‘œ 뢄석 μ „λ¬Έκ°€λ‘œμ„œ ν–‰κ³Ό μ—΄μ˜ 관계λ₯Ό μ •ν™•νžˆ ν•΄μ„ν•˜μ—¬ λ‹΅λ³€ν•΄μ•Ό ν•©λ‹ˆλ‹€.
4. 쑰건뢀 λ‹΅λ³€: λ§Œμ•½ ν‘œλ‚˜ ν…μŠ€νŠΈμ— ν•™κ³Ό, ν•™λ²ˆ λ“± μ„ΈλΆ€ 쑰건이 λͺ…μ‹œλ˜μ–΄ μžˆμ§€ μ•Šλ‹€λ©΄, "μ œμ‹œλœ μžλ£Œμ— λ”°λ₯΄λ©΄ 일반적으둜" λ˜λŠ” "2025학년도 κΈ°μ€€μœΌλ‘œλŠ”" κ³Ό 같이 μ •λ³΄μ˜ μΆœμ²˜λ‚˜ 기쀀을 λͺ…ν™•νžˆ 밝히며 λ‹΅λ³€ν•˜μ„Έμš”.
5. 닀쀑 정보 처리: λ§Œμ•½ μ‚¬μš©μžμ˜ μ§ˆλ¬Έμ— λŒ€ν•΄ μ—¬λŸ¬ λ¬Έμ„œμ—μ„œ μ„œλ‘œ λ‹€λ₯Έ 정보가 검색될 경우, ν•˜λ‚˜μ˜ μ •λ³΄λ§Œ μ„ νƒν•˜μ§€ λ§ˆμ„Έμš”. λŒ€μ‹ , 각각의 쑰건과 λ‚΄μš©μ„ λͺ…ν™•νžˆ κ΅¬λΆ„ν•˜μ—¬ λͺ¨λ“  정보λ₯Ό μ’…ν•©μ μœΌλ‘œ μ•ˆλ‚΄ν•΄μ•Ό ν•©λ‹ˆλ‹€.
6. μ˜ˆμ™Έ κ°€λŠ₯μ„± 인지: 학사 κ·œμ •μ€ λ‹¨κ³ΌλŒ€ν•™, ν•™κ³Ό, ν•™λ²ˆλ³„λ‘œ μ˜ˆμ™Έ κ·œμΉ™μ΄ μ‘΄μž¬ν•  수 μžˆλ‹€λŠ” 사싀을 항상 μΈμ§€ν•˜μ„Έμš”. λ§Œμ•½ 일반적인 κ·œμΉ™μ„ μ°Ύμ•˜λ”λΌλ„, "μΌλ°˜μ μœΌλ‘œλŠ” OO학점이 ν•„μš”ν•˜μ§€λ§Œ, μ†Œμ† λ‹¨κ³ΌλŒ€ν•™μ΄λ‚˜ 학과에 따라 λ‹€λ₯Ό 수 μžˆμœΌλ‹ˆ μ •ν™•ν•œ μ •λ³΄λŠ” 학ꡐ 곡식 λ¬Έμ„œλ₯Ό ν™•μΈν•˜μ‹œκ±°λ‚˜ ν•™κ³Ό 사무싀에 λ¬Έμ˜ν•˜λŠ” 것을 ꢌμž₯ν•©λ‹ˆλ‹€" 와 같이 닡변에 'μ£Όμ˜μ‚¬ν•­'κ³Ό 'ν•œκ³„'λ₯Ό λͺ…μ‹œν•˜μ„Έμš”.
7. 정보 λΆ€μž¬ μ‹œ: μœ„μ˜ λͺ¨λ“  λ…Έλ ₯에도 λΆˆκ΅¬ν•˜κ³  μ§ˆλ¬Έμ— λŒ€ν•œ 닡변을 μ°Έκ³  λ¬Έμ„œμ—μ„œ 찾을 수 μ—†λŠ” κ²½μš°μ—λ§Œ, "μ£„μ†‘ν•©λ‹ˆλ‹€. λ¬Έμ˜ν•˜μ‹  λ‚΄μš©μ— λŒ€ν•œ μ •λ³΄λŠ” μ œκ°€ κ°€μ§„ μžλ£Œμ—μ„œ 확인할 수 μ—†μŠ΅λ‹ˆλ‹€."라고 λ‹΅λ³€ν•˜μ„Έμš”.
"""
),
}
)
retriever = vector_store.as_retriever(search_kwargs={"k": 3})
# retriever = MultiQueryRetriever.from_llm(
# retriever=vector_store.as_retriever(search_kwargs={"k": 5}),
# llm=llm
# )
# μ‚¬μš©μž μ„Έμ…˜λ³„ λŒ€ν™” 체인을 μ €μž₯ν•  λ”•μ…”λ„ˆλ¦¬
chat_sessions: Dict[str, ConversationalRetrievalChain] = {}
def get_or_create_chain(session_id: str) -> ConversationalRetrievalChain:
if session_id not in chat_sessions:
memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True,
input_key="question", # <-- μΆ”κ°€
output_key="answer" )
new_chain = ConversationalRetrievalChain.from_llm(
llm=llm,
retriever=retriever,
memory=memory,
return_source_documents=True, # μ°Έκ³  λ¬Έμ„œ λ°˜ν™˜ ν™œμ„±ν™”
output_key="answer"
)
chat_sessions[session_id] = new_chain
print(f"μƒˆλ‘œμš΄ μ„Έμ…˜ ID 생성: {session_id}")
return chat_sessions[session_id]
class ChatMessage(BaseModel):
message: str
session_id: str
user_id: str # μ‚¬μš©μž 식별을 μœ„ν•΄ μΆ”κ°€
class ChatResponse(BaseModel):
response: str
success: bool
# source_documents ν•„λ“œλ₯Ό μΆ”κ°€ν•˜μ—¬ ν”„λ‘ νŠΈμ—”λ“œλ‘œλ„ 보낼 수 μžˆλ„λ‘ μ€€λΉ„
source_documents: List[Dict[str, str]] = [] # λ¬Έμ„œ λ‚΄μš©κ³Ό 메타데이터 μ €μž₯
@app.post("/api/chat", response_model=ChatResponse)
@limiter.limit("15/minute")
async def chat_with_gemini(request: Request):
start_time = time.time()
try:
# JSON bodyλ₯Ό 직접 νŒŒμ‹±
body = await request.json()
chat_message = ChatMessage(**body)
# qa_chain = get_or_create_chain(request.session_id)
# result = qa_chain.invoke({"question": request.message})
qa_chain = get_or_create_chain(chat_message.session_id)
result = qa_chain.invoke({"question": chat_message.message})
# μ°Έκ³  λ¬Έμ„œ μΆ”μΆœ 및 둜그 좜λ ₯
source_documents_for_response: List[Dict[str, str]] = []
if 'source_documents' in result and result['source_documents']:
print("\n--- μ°Έκ³  λ¬Έμ„œ ---")
for i, doc in enumerate(result['source_documents']):
print(f"λ¬Έμ„œ {i+1}:")
print(f" μ†ŒμŠ€: {doc.metadata.get('source', 'μ•Œ 수 μ—†μŒ')}")
print(f" λ‚΄μš© (일뢀): {doc.page_content[:200]}...") # λ‚΄μš©μ˜ μΌλΆ€λ§Œ 좜λ ₯
# ν”„λ‘ νŠΈμ—”λ“œ 응닡을 μœ„ν•΄ μ €μž₯
source_documents_for_response.append({
"source": doc.metadata.get('source', 'μ•Œ 수 μ—†μŒ'),
"content": doc.page_content # 전체 λ‚΄μš©μ„ 보낼 μˆ˜λ„ 있음
})
print("---------------\n")
# ==========================================================
# β–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Ό 이 λΆ€λΆ„λ§Œ μΆ”κ°€ β–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Όβ–Ό
# ==========================================================
response_time_ms = int((time.time() - start_time) * 1000)
# DB에 둜그 μ €μž₯
try:
db_conn_str = CONNECTION_STRING.replace("postgresql+psycopg2", "postgresql")
conn = psycopg2.connect(db_conn_str)
cur = conn.cursor()
cur.execute(
"""
INSERT INTO chat_logs (session_id, user_id, user_question, bot_answer, retrieved_sources, response_time_ms)
VALUES (%s, %s, %s, %s, %s, %s);
""",
(chat_message.session_id, chat_message.user_id, chat_message.message, result['answer'], json.dumps(source_documents_for_response), response_time_ms)
)
conn.commit()
cur.close()
conn.close()
except Exception as db_error:
print(f"DB 둜그 μ €μž₯ μ‹€νŒ¨: {db_error}")
# ==========================================================
# β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–² 이 λΆ€λΆ„λ§Œ μΆ”κ°€ β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²β–²
# ==========================================================
return ChatResponse(
response=result['answer'],
success=True,
source_documents=source_documents_for_response # 응닡에 μ°Έκ³  λ¬Έμ„œ μΆ”κ°€
)
except Exception as e:
print(f"였λ₯˜ λ°œμƒ: {str(e)}")
return ChatResponse(
response=f"였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {str(e)}",
success=False,
source_documents=[]
)
@app.get("/")
async def root():
return {"message": "ν•œκ΅­μ™Έκ΅­μ–΄λŒ€ν•™κ΅(μ„œμšΈ) 학사 챗봇 API"}