Skip to content

Commit 23d6277

Browse files
authored
Merge pull request #41 from 9git9git/SCRUM-55-BE-Storage-API-개발
[SCRUM-55] Storage, Chat API 연동 (구 Branch 사용(be storage api 개발))
2 parents b8bf461 + e7aebb1 commit 23d6277

File tree

25 files changed

+1018
-216
lines changed

25 files changed

+1018
-216
lines changed

app/api/v1/endpoints/chat.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from app.db.session import get_db
77
from app.schemas.base import ResponseBase
8-
from app.schemas.chat import ChatCreate, ChatUpdate, ChatResponse
8+
from app.schemas.chat import ChatCreate, ChatUpdate, ChatResponse, ChatWithModelResponse
99
from app.services.chat import (
1010
select_chat,
1111
select_chats_by_user,
@@ -86,17 +86,17 @@ async def get_chat(
8686
)
8787

8888

89-
@router.post("/", response_model=ResponseBase[ChatResponse])
89+
@router.post("/", response_model=ResponseBase[ChatWithModelResponse])
9090
async def post_chat(
9191
user_id: UUID,
9292
storage_id: UUID,
9393
category_id: UUID,
9494
chat_data: ChatCreate,
9595
db: AsyncSession = Depends(get_db),
96-
) -> ResponseBase[ChatResponse]:
96+
) -> ResponseBase[ChatWithModelResponse]:
9797
try:
98-
chat = await add_chat(db, user_id, storage_id, category_id, chat_data)
99-
return ResponseBase(status_code=status.HTTP_201_CREATED, data=chat)
98+
chat_result = await add_chat(db, user_id, storage_id, category_id, chat_data)
99+
return ResponseBase(status_code=status.HTTP_201_CREATED, data=chat_result)
100100
except HTTPException as e:
101101
return ResponseBase(status_code=e.status_code, error=e.detail)
102102
except Exception as e:

app/crud/category.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ async def update_category(
7070

7171

7272
# 카테고리 삭제
73-
async def delete_category(db: AsyncSession, db_category: Category) -> bool:
74-
delete_stmt = delete(Category).where(Category.id == db_category.id)
73+
async def delete_category(db: AsyncSession, category_id: UUID) -> bool:
74+
delete_stmt = delete(Category).where(Category.id == category_id)
7575
try:
7676
await db.execute(delete_stmt)
7777
await db.commit()

app/crud/chat.py

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
11
from uuid import UUID
22
from typing import List
3-
from sqlalchemy import select, delete
3+
from sqlalchemy import select, delete, text
4+
from sqlalchemy.orm import joinedload
45
from sqlalchemy.ext.asyncio import AsyncSession
56
from app.models.chat import Chat
6-
from app.schemas.chat import ChatCreate, ChatUpdate, ChatResponse
7+
from app.models.category import Category
8+
from app.schemas.chat import (
9+
ChatCreate,
10+
ChatUpdate,
11+
ChatResponse,
12+
ModelResponse,
13+
ChatWithModelResponse,
14+
)
15+
from app.enum.chat import RoleEnum
716
from app.utils.to_snake_case import camel_to_snake
17+
from app.utils.chat_model_selector import route_to_model
18+
from app.enum.category import CategoryNameEnum
19+
from app.models.category import Category
820

921

1022
# 생성
@@ -14,20 +26,62 @@ async def create_chat(
1426
storage_id: UUID,
1527
category_id: UUID,
1628
chat_data: ChatCreate,
17-
) -> ChatResponse:
29+
) -> ChatWithModelResponse:
1830

19-
new_chat = Chat(
31+
user_chat = Chat(
2032
user_id=user_id,
2133
storage_id=storage_id,
2234
category_id=category_id,
2335
role=chat_data.role,
2436
content=chat_data.content,
37+
created_at=chat_data.created_at,
2538
)
26-
27-
db.add(new_chat)
39+
db.add(user_chat)
2840
await db.commit()
29-
await db.refresh(new_chat)
30-
return new_chat
41+
await db.refresh(user_chat)
42+
43+
# ✅ 카테고리 이름 직접 SQL로 조회 (ENUM)
44+
result = await db.execute(
45+
text(
46+
"""
47+
SELECT category_name
48+
FROM categories
49+
WHERE id = :category_id
50+
"""
51+
),
52+
{"category_id": str(category_id)},
53+
)
54+
row = result.first()
55+
56+
if row is None:
57+
raise ValueError("❌ 해당 category_id에 대한 카테고리를 찾을 수 없습니다.")
58+
59+
category_enum = CategoryNameEnum[row.category_name]
60+
61+
# ✅ GPT 호출
62+
if chat_data.role == RoleEnum.USER:
63+
model_func = route_to_model(category_enum)
64+
gpt_response = model_func(chat_data.content)
65+
66+
assistant_chat = Chat(
67+
user_id=user_id,
68+
storage_id=storage_id,
69+
category_id=category_id,
70+
role=RoleEnum.ASSISTANT,
71+
content=gpt_response,
72+
)
73+
db.add(assistant_chat)
74+
await db.commit()
75+
await db.refresh(assistant_chat)
76+
77+
return ChatWithModelResponse(
78+
chat=user_chat, model_response=ModelResponse(content=gpt_response)
79+
)
80+
81+
# assistant role이 아닌 경우
82+
return ChatWithModelResponse(
83+
chat=user_chat, model_response=ModelResponse(content="")
84+
)
3185

3286

3387
# 단일 채팅 조회

app/crud/storage.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,21 @@
88

99

1010
async def create_storage(
11-
db: AsyncSession, user_id: UUID, category_id: UUID, storage_data: StorageCreate
11+
db: AsyncSession,
12+
user_id: UUID,
13+
category_id: UUID,
14+
storage_data: StorageCreate,
1215
) -> StorageResponse:
16+
title = (
17+
storage_data.title
18+
or f"[{storage_data.created_at.strftime('%Y-%m-%d')}] New Chat"
19+
)
20+
1321
db_storage = Storage(
1422
user_id=user_id,
1523
category_id=category_id,
16-
title=storage_data.title,
24+
title=title,
25+
created_at=storage_data.created_at,
1726
)
1827

1928
db.add(db_storage)

app/gpt/.gitignore

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# .gitignore
2+
3+
# ✅ 민감정보 (절대 Git에 올라가면 안 됨)
4+
.env
5+
6+
# ✅ 파이썬 캐시 (자동 생성되며 필요 없음)
7+
__pycache__/
8+
*.pyc
9+
10+
# ✅ 주피터 노트북 관련 캐시
11+
.ipynb_checkpoints/
12+
13+
# ✅ VSCode 사용자 설정 (개인별 설정이므로 공유 불필요)
14+
.vscode/
15+
16+
# ✅ 운영체제별 임시파일
17+
.DS_Store # macOS
18+
Thumbs.db # Windows
19+
20+
# ✅ 패키지 설치 후 생성되는 항목
21+
*.egg-info/
22+
dist/
23+
build/
24+
25+
# ✅ 테스트/로깅 결과물
26+
*.log
27+
*.tmp
28+
*.bak
29+
30+
# ✅ 가상환경 디렉토리 (만약 프로젝트에 포함되어 있다면)
31+
venv/
32+
.env/
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# handlers/category_router.py
2+
3+
from app.gpt.handlers.english_tutor import handle_english_tutor
4+
from app.gpt.handlers.coding_tutor import handle_coding_tutor
5+
from app.gpt.handlers.exercise_tutor import handle_exercise_tutor
6+
from app.enum.category import CategoryNameEnum
7+
8+
9+
def handle_tutor(user_input: str, category: str) -> str:
10+
"""
11+
category: CategoryNameEnum.name 문자열 ("ENGLISH", "CODING", "EXERCISE")
12+
"""
13+
try:
14+
category_enum = CategoryNameEnum[category.upper()]
15+
except KeyError:
16+
raise ValueError(f"[❌] 잘못된 카테고리 이름: {category}")
17+
18+
if category_enum == CategoryNameEnum.ENGLISH:
19+
return handle_english_tutor(user_input)
20+
elif category_enum == CategoryNameEnum.CODING:
21+
return handle_coding_tutor(user_input)
22+
elif category_enum == CategoryNameEnum.EXERCISE:
23+
return handle_exercise_tutor(user_input)
24+
else:
25+
raise ValueError(f"[❌] 지원하지 않는 category: {category_enum}")

app/gpt/handlers/coding_tutor.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# handlers/coding_tutor.py
2+
3+
import os
4+
from uuid import uuid4
5+
from datetime import datetime, timezone
6+
7+
from app.gpt.services.gpt_client import call_gpt
8+
from app.gpt.services.ai_search import upload_to_index, search_notice
9+
from app.gpt.utils.prompts_loader import load_combined_prompt, format_prompt
10+
from app.enum.category import CategoryNameEnum
11+
12+
13+
# ✅ 코딩 튜터 전용 핸들러
14+
def handle_coding_tutor(user_input: str) -> str:
15+
category = CategoryNameEnum.CODING
16+
index_name = os.getenv("AZURE_SEARCH_INDEX_CODING")
17+
question = user_input.strip()
18+
19+
# 🔹 Notice 기반 RAG 검색 (향후 codingnoticeindex 연동 예정)
20+
rag_context = search_notice(query=question, category_enum=category)
21+
22+
# 🔹 프롬프트 로딩 및 메시지 구성
23+
system_prompt, user_template = load_combined_prompt(category)
24+
user_message = format_prompt(user_template, rag_context, question)
25+
26+
messages = [
27+
{"role": "system", "content": system_prompt},
28+
{"role": "user", "content": user_message},
29+
]
30+
31+
# 🔹 GPT 호출
32+
result = call_gpt(messages=messages, category=category)
33+
34+
# 🔹 요약 생성 및 저장
35+
summary_prompt = [
36+
{
37+
"role": "system",
38+
"content": "다음 응답을 한 문장으로 요약해 주세요. 반드시 한국어로.",
39+
},
40+
{"role": "user", "content": result},
41+
]
42+
summary = call_gpt(summary_prompt, category)
43+
44+
upload_to_index(
45+
index_name,
46+
{
47+
"id": f"{category.name}-{str(uuid4())}",
48+
"mode": "summary",
49+
"category": category.name,
50+
"original": question,
51+
"summary": summary,
52+
"created_at": datetime.now(timezone.utc).isoformat(),
53+
"user_choice": "",
54+
},
55+
)
56+
57+
return result

app/gpt/handlers/english_tutor.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# handlers/english_tutor.py
2+
3+
import os
4+
from uuid import uuid4
5+
from datetime import datetime, timezone
6+
7+
from app.gpt.services.gpt_client import call_gpt
8+
from app.gpt.services.ai_search import upload_to_index, search_notice
9+
from app.gpt.utils.prompts_loader import load_combined_prompt, format_prompt
10+
from app.enum.category import CategoryNameEnum
11+
12+
13+
# ✅ 영어 튜터 전용 핸들러
14+
def handle_english_tutor(user_input: str) -> str:
15+
category = CategoryNameEnum.ENGLISH
16+
index_name = os.getenv("AZURE_SEARCH_INDEX_ENGLISH")
17+
18+
# 1. 사용자 입력 수신
19+
question = user_input.strip()
20+
21+
# 2. Notice 인덱스에서 관련 정보 검색 (시험 정보 등)
22+
rag_context = search_notice(query=question, category_enum=category)
23+
print("🧾 검색 결과 (RAG):", rag_context)
24+
# 3. 프롬프트 로딩 및 메시지 생성
25+
system_prompt, user_template = load_combined_prompt(category)
26+
user_message = format_prompt(user_template, rag_context, question)
27+
28+
messages = [
29+
{"role": "system", "content": system_prompt},
30+
{"role": "user", "content": user_message},
31+
]
32+
33+
# 4. GPT 응답 생성
34+
result = call_gpt(messages=messages, category=category)
35+
36+
# 5. 응답 요약 후 AI Search 저장
37+
summary_prompt = [
38+
{
39+
"role": "system",
40+
"content": "다음 응답을 한 문장으로 요약해 주세요. 반드시 한국어로.",
41+
},
42+
{"role": "user", "content": result},
43+
]
44+
summary = call_gpt(summary_prompt, category=category)
45+
46+
upload_to_index(
47+
index_name,
48+
{
49+
"id": f"{category.name}-{str(uuid4())}",
50+
"mode": "summary",
51+
"category": category.name,
52+
"original": question,
53+
"summary": summary,
54+
"created_at": datetime.now(timezone.utc).isoformat(),
55+
"user_choice": "",
56+
},
57+
)
58+
59+
return result

app/gpt/handlers/exercise_tutor.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# handlers/exercise_tutor.py
2+
3+
import os
4+
from uuid import uuid4
5+
from datetime import datetime, timezone
6+
7+
from app.gpt.services.gpt_client import call_gpt
8+
from app.gpt.services.ai_search import upload_to_index, search_notice
9+
from app.gpt.utils.prompts_loader import load_combined_prompt, format_prompt
10+
from app.enum.category import CategoryNameEnum
11+
12+
13+
# ✅ 운동 튜터 전용 핸들러
14+
def handle_exercise_tutor(user_input: str) -> str:
15+
category = CategoryNameEnum.EXERCISE
16+
index_name = os.getenv("AZURE_SEARCH_INDEX_EXERCISE")
17+
question = user_input.strip()
18+
19+
# 🔹 Notice 기반 RAG 검색 (향후 exercisenoticeindex 연동 예정)
20+
rag_context = search_notice(query=question, category_enum=category)
21+
22+
# 🔹 프롬프트 로딩 및 메시지 구성
23+
system_prompt, user_template = load_combined_prompt(category)
24+
user_message = format_prompt(user_template, rag_context, question)
25+
26+
messages = [
27+
{"role": "system", "content": system_prompt},
28+
{"role": "user", "content": user_message},
29+
]
30+
31+
# 🔹 GPT 호출
32+
result = call_gpt(messages, category)
33+
34+
# 🔹 요약 생성 및 저장
35+
summary_prompt = [
36+
{
37+
"role": "system",
38+
"content": "다음 응답을 한 문장으로 요약해 주세요. 반드시 한국어로.",
39+
},
40+
{"role": "user", "content": result},
41+
]
42+
summary = call_gpt(summary_prompt, category)
43+
44+
upload_to_index(
45+
index_name,
46+
{
47+
"id": f"{category.name}-{str(uuid4())}",
48+
"mode": "summary",
49+
"category": category.name,
50+
"original": question,
51+
"summary": summary,
52+
"created_at": datetime.now(timezone.utc).isoformat(),
53+
"user_choice": "",
54+
},
55+
)
56+
57+
return result

0 commit comments

Comments
 (0)