Skip to content

Commit e45d703

Browse files
committed
Merge branch 'dev' of https://github.com/SKT-FlyAI-BoraMettugi/BE into dev
2 parents 03608be + abe2b43 commit e45d703

File tree

9 files changed

+225
-6
lines changed

9 files changed

+225
-6
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ __pycache__
66
*.bin
77
.idea/
88
venv/
9-
main_my.py
9+
main_my.py
10+
test_download/

api/routes/answer.py

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
from sqlalchemy.orm import Session
33
from database.nolly import get_db
44
from crud.answer import get_answer_history, get_answer_scores
5-
from schemas.answer import AnswerResponse, AnswerScoreResponse
5+
from schemas.answer import AnswerResponse, AnswerScoreResponse, AnswerSubmit
6+
from crud.model_inference import get_tokenizer, get_model # , get_device
67

78
router = APIRouter()
89

@@ -22,4 +23,72 @@ async def get_answer_scores_api(user_id: int, question_id: int, db: Session = De
2223
if not answer_scores:
2324
raise HTTPException(status_code=404, detail="채점 결과를 찾을 수 없습니다.")
2425

25-
return answer_scores
26+
return answer_scores
27+
28+
# 문제 답안 제출 == 채점
29+
@router.post("/{user_id}/{question_id}")
30+
async def grade_answers(
31+
user_id: int, question_id: int,
32+
answer_data: AnswerSubmit, tokenizer = Depends(get_tokenizer), model = Depends(get_model),
33+
db: Session = Depends(get_db) # device: str = Depends(get_device),
34+
):
35+
36+
# 1. 사용자 답변 불러오기
37+
input_text = answer_data.answer
38+
# 2. 모델에 넣을 수 있도록 토큰화 ########################## pt 맞는지 확인 ###
39+
inputs = tokenizer(input_text, return_tensors="pt")
40+
41+
# 3. 모델 추론 수행
42+
with torch.no_grad():
43+
outputs = model(**inputs)
44+
45+
# 4. 모델 결과 가공 ########################## 실제 결과에 맞게 수정 필요 ###
46+
scores = outputs["scores"].tolist() # [창의, 논리, 사고, 설득, 깊이]
47+
reviews = outputs["explanations"] # ["창의성 설명", "논리 설명", ...]
48+
49+
total_score = sum(scores) / len(scores) if scores else 0
50+
51+
# 5. DB에 저장 : 기존 답변 O (값 update), X (db에 새로 add)
52+
existing_answer = db.query(Answer).filter(
53+
Answer.user_id == user_id,
54+
Answer.question_id == question_id
55+
).first()
56+
57+
if existing_answer:
58+
# 기존 데이터가 있으면 업데이트
59+
existing_answer.content = input_text
60+
existing_answer.creativity = scores[0]
61+
existing_answer.logic = scores[1]
62+
existing_answer.thinking = scores[2]
63+
existing_answer.persuasion = scores[3]
64+
existing_answer.depth = scores[4]
65+
existing_answer.creativity_review = reviews[0]
66+
existing_answer.logic_review = reviews[1]
67+
existing_answer.thinking_review = reviews[2]
68+
existing_answer.persuasion_review = reviews[3]
69+
existing_answer.depth_review = reviews[4]
70+
existing_answer.total_score = total_score
71+
else:
72+
# 기존 데이터가 없으면 새로 추가
73+
new_answer = Answer(
74+
user_id=user_id,
75+
question_id=question_id,
76+
content=input_text,
77+
creativity=scores[0],
78+
logic=scores[1],
79+
thinking=scores[2],
80+
persuasion=scores[3],
81+
depth=scores[4],
82+
creativity_review=reviews[0],
83+
logic_review=reviews[1],
84+
thinking_review=reviews[2],
85+
persuasion_review=reviews[3],
86+
depth_review=reviews[4],
87+
total_score=total_score
88+
)
89+
db.add(new_answer)
90+
91+
# DB 반영
92+
db.commit()
93+
94+
return # 응답 없이 종료 / FastAPI는 자동으로 200 OK 반환

api/routes/question.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,6 @@ async def get_question(question_id: int, db: Session = Depends(get_db)):
2121
if not question:
2222
raise HTTPException(status_code=404, detail="해당 문제를 찾을 수 없습니다.")
2323

24-
return question
24+
return question
25+
26+

core/minio_service.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import boto3
2+
import os
3+
from dotenv import load_dotenv
4+
5+
# .env 로드
6+
load_dotenv(override=True)
7+
8+
MINIO_URL = os.getenv("MINIO_URL")
9+
AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID")
10+
AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY")
11+
S3_BUCKET_NAME = os.getenv("S3_BUCKET_NAME")
12+
13+
# MinIO 클라이언트 생성
14+
s3 = boto3.client(
15+
"s3",
16+
endpoint_url=f"http://{MINIO_URL}",
17+
aws_access_key_id=AWS_ACCESS_KEY_ID,
18+
aws_secret_access_key=AWS_SECRET_ACCESS_KEY
19+
)
20+
21+
# minio에서 모델 다운로드
22+
def download_model_from_minio(model_s3_path: str, local_dir: str):
23+
"""
24+
MinIO에서 모델 전체 다운로드
25+
:param model_s3_path: MinIO 내 저장된 모델 경로 (예: "llama_model/")
26+
:param local_dir: 로컬에 저장할 경로 (예: "./downloaded_model/")
27+
"""
28+
try:
29+
os.makedirs(local_dir, exist_ok=True) # 로컬 저장 폴더 생성
30+
31+
# MinIO에서 해당 모델 폴더의 모든 객체 가져오기
32+
objects = s3.list_objects_v2(Bucket=S3_BUCKET_NAME, Prefix=model_s3_path)
33+
34+
if 'Contents' not in objects:
35+
print(f"❌ MinIO에 {model_s3_path} 경로 없음")
36+
return None
37+
38+
for obj in objects['Contents']:
39+
file_key = obj['Key']
40+
file_name = os.path.basename(file_key)
41+
local_path = os.path.join(local_dir, file_name)
42+
43+
s3.download_file(S3_BUCKET_NAME, file_key, local_path)
44+
print(f"✅ {file_key} 다운로드 완료 → {local_path}")
45+
46+
return local_dir
47+
except Exception as e:
48+
print(f"❌ MinIO에서 {model_s3_path} 다운로드 실패: {e}")
49+
return None
50+
51+
52+
# def download_test_from_minio(model_path: str, local_save_path: str):
53+
# """
54+
# MinIO에서 파일 다운로드 (예: test.txt)
55+
# :param model_path: MinIO 내 저장된 파일 경로 (예: test_folder/test.txt)
56+
# :param local_save_path: 로컬에 저장할 경로 (예: ./downloaded_model/test.txt)
57+
# """
58+
# try:
59+
# os.makedirs(os.path.dirname(local_save_path), exist_ok=True) # 로컬 저장 폴더 생성
60+
# s3.download_file("test", model_path, local_save_path)
61+
# print(f"✅ {model_path} 다운로드 완료 → {local_save_path}")
62+
# return local_save_path
63+
# except Exception as e:
64+
# print(f"❌ MinIO에서 {model_path} 다운로드 실패: {e}")
65+
# return None

core/model_loader.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import torch
2+
from transformers import AutoModelForCausalLM, AutoTokenizer
3+
from peft import PeftModel
4+
5+
MODEL_PATH = "downloaded_model"
6+
7+
# def load_tokenizer():
8+
# # Tokenizer 로드
9+
# tokenizer = AutoTokenizer.from_pretrained(f"{MODEL_PATH}/tokenizer.json", local_files_only=True, use_fast=True)
10+
11+
# return tokenizer
12+
13+
def load_model():
14+
base_model_name = "Bllossom-llama-3.2-ko"
15+
lora_model_path = MODEL_PATH
16+
17+
base_model = AutoModelForCausalLM.from_pretrained(base_model_name, device_map="auto")
18+
tokenizer = AutoTokenizer.from_pretrained(base_model_name)
19+
20+
# # Config 로드
21+
# config = AutoConfig.from_pretrained(f"{MODEL_PATH}/config.json", local_files_only=True)
22+
23+
# # 모델 생성 (AutoModelForCausalLM, AutoModelForSequenceClassification 등 선택)
24+
# model = AutoModel.from_config(config)
25+
26+
# # .safetensors 로드
27+
# weights = load_file(f"{MODEL_PATH}/adapter_model.safetensors") # 파일명 확인
28+
29+
# # 모델에 가중치 적용
30+
# model.load_state_dict(weights, strict=False)
31+
32+
# # # 모델을 GPU로 이동 (선택)
33+
# # device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
34+
# # model.to(device)
35+
# model.eval() # 모델 평가 모드로 설정
36+
37+
# # Adapter 설정 로드
38+
# adapter_config = PeftConfig.from_pretrained(f"{MODEL_PATH}/adapter_config.json", local_files_only=True)
39+
40+
# # Adapter 적용
41+
# model = PeftModel(model, adapter_config)
42+
43+
# return model
44+
45+
# # # 모델 로드
46+
# # llama_model = load_model()

crud/model_inference.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
from typing import Any
3+
4+
def get_tokenizer() -> Any:
5+
return app.state.tokenizer
6+
7+
def get_model() -> Any:
8+
return app.state.model
9+
10+
# def get_device():
11+
# return app.state.device

main.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
from api import main
44
from starlette.middleware.cors import CORSMiddleware
55
from core.redis_subscriber import start_redis_subscriber
6+
from core.minio_service import download_model_from_minio # 모델 파일 다운로드
7+
from core.model_loader import load_tokenizer, load_model # tokenizer, 모델 로드
8+
import os
9+
# import torch
610

711
app = FastAPI()
812

@@ -18,6 +22,16 @@
1822

1923
app.include_router(main.api_router)
2024

25+
# 서버 실행 시 모델 파일 다운로드 + 모델 로드
26+
@app.on_event("startup")
27+
async def cache_model():
28+
download_model_from_minio(f"", "downloaded_model") # MODEL_PATH : 어제 test 시 "test_download" 사용
29+
app.state.tokenizer = load_tokenizer()
30+
app.state.model = load_model()
31+
# device = torch.device("cpu")
32+
# app.state.device = device
33+
# app.state.model.to(device)
34+
2135
#if __name__ == '__main__':
2236
#uvicorn.run('main:app', reload=True)
2337

requirements.txt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,11 @@ sqlalchemy
77
pymysql
88
websockets
99
redis
10-
httpx
10+
httpx
11+
minio
12+
boto3
13+
dotenv
14+
transformers
15+
safetensors
16+
peft
17+
torch

schemas/answer.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,8 @@ class AnswerScoreResponse(BaseModel):
2424
total_score: Optional[int] = None # 총점
2525

2626
class Config:
27-
orm_mode = True
27+
orm_mode = True
28+
29+
30+
class AnswerSubmit(BaseModel):
31+
answer: str # 사용자가 제출하는 답변

0 commit comments

Comments
 (0)