Skip to content

Commit 1613b50

Browse files
committed
Merge branch 'develop' into release/1.0.0
2 parents 16c73d0 + 440285b commit 1613b50

14 files changed

Lines changed: 931 additions & 170 deletions

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,5 @@ __pycache__/
4747
.venv/
4848

4949
### Environment ###
50-
.env
50+
.env.prod
51+
.env.dev

msa-ai-service/app/core/config.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
1-
# app/core/config.py
21
import os
32
from dotenv import load_dotenv
43

5-
load_dotenv()
4+
# 기본 ENV=dev
5+
ENV = os.getenv("ENV", "dev")
66

7-
MONGODB_URI = os.getenv("MONGODB_URI", "mongodb://localhost:27017") # fallback용
7+
if ENV == "prod":
8+
load_dotenv(".env.prod")
9+
else:
10+
load_dotenv(".env.dev")
11+
12+
13+
MONGODB_URI = os.getenv("MONGODB_URI", "mongodb://localhost:27017")
814
MONGODB_NAME = os.getenv("MONGODB_NAME", "ai_service_db")
15+
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
16+
17+
REVIEW_LABELS = ["quantity", "size", "sweet", "salty", "spicy", "deep"]
18+
POLARITY_LABELS = ["POSITIVE", "NEGATIVE"]
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// 직접 수동 데이터 삽입용
2+
// change stream 쓸 때는 안씀
3+
4+
// const db = db.getSiblingDB("ai_service_db")
5+
6+
// // 기존 콜렉션 드롭
7+
// db.qa_queries.drop()
8+
// db.reviews_denorm.drop()
9+
// db.queries_embedding.drop()
10+
// db.reviews_embedding.drop()
11+
// db.qa_answers.drop()
12+
13+
// // 라벨별 리뷰 문구 샘플
14+
// const reviewSamples = {
15+
// salty: [
16+
// "국물이 많이 짜네요.",
17+
// "간이 세서 밥이랑 먹기 좋아요.",
18+
// "짜지 않고 딱 좋아요."
19+
// ],
20+
// quantity: [
21+
// "양이 많아요.",
22+
// "조금 부족했어요."
23+
// ],
24+
// deep: [
25+
// "맛이 깊고 진합니다.",
26+
// "국물 풍미가 깊어요.",
27+
// "감칠맛이 뛰어나네요."
28+
// ],
29+
// spicy: [
30+
// "매콤하고 맛있어요.",
31+
// "엄청 매워요!",
32+
// "살짝 매콤한 정도예요.",
33+
// "안매워요"
34+
// ],
35+
// sweet: [
36+
// "달콤하고 부드러워요.",
37+
// "좀 달아요.",
38+
// "단맛이 은은합니다.",
39+
// "완전 달아요."
40+
// ],
41+
// size: [
42+
// "조각이 커서 배부릅니다.",
43+
// "커요",
44+
// "사이즈가 작아요.",
45+
// "적당한 크기였어요."
46+
// ]
47+
// }
48+
49+
// // 스토어 & 메뉴 정의
50+
// const stores = [
51+
// {
52+
// store_name: "한식당",
53+
// menu_name: "된장찌개",
54+
// labels: {
55+
// salty: ["짜지 않나요?", "간이 어떤가요?"],
56+
// quantity: ["양이 많나요?", "양이 적당한가요?"],
57+
// deep: ["맛이 깊나요?", "국물 맛이 진한가요?"]
58+
// }
59+
// },
60+
// {
61+
// store_name: "분식당",
62+
// menu_name: "떡볶이",
63+
// labels: {
64+
// quantity: ["양이 많나요?", "양이 적당한가요?"],
65+
// spicy: ["맵나요?", "얼얼한 맛이 있나요?"],
66+
// sweet: ["달콤한가요?", "단맛이 강한가요?"]
67+
// }
68+
// },
69+
// {
70+
// store_name: "디저트카페",
71+
// menu_name: "치즈케이크",
72+
// labels: {
73+
// size: ["크기가 크나요?", "한 조각이 충분한가요?"],
74+
// sweet: ["달콤한가요?", "단맛이 강한가요?"],
75+
// deep: ["맛이 진한가요?", "치즈 풍미가 깊나요?"]
76+
// }
77+
// }
78+
// ]
79+
80+
// // 루프 돌면서 삽입
81+
// stores.forEach((store) => {
82+
// const store_id = UUID().toString()
83+
// const menu_id = UUID().toString()
84+
85+
// // 리뷰 20개
86+
// const reviews = []
87+
// const labelKeys = Object.keys(store.labels)
88+
// for (let i = 1; i <= 20; i++) {
89+
// const label = labelKeys[i % labelKeys.length]
90+
// const sampleTexts = reviewSamples[label]
91+
// const text = sampleTexts[Math.floor(Math.random() * sampleTexts.length)]
92+
// reviews.push({
93+
// review_id: UUID().toString(),
94+
// text: text,
95+
// created_at: new Date()
96+
// })
97+
// }
98+
99+
// db.reviews_denorm.insertOne({
100+
// _id: store_id,
101+
// store_name: store.store_name,
102+
// menus: [
103+
// {
104+
// menu_id: menu_id,
105+
// menu_name: store.menu_name,
106+
// reviews: reviews
107+
// }
108+
// ],
109+
// updated_at: new Date()
110+
// })
111+
112+
// // 질문 (라벨별 2개씩)
113+
// const questions = []
114+
// for (const [label, qs] of Object.entries(store.labels)) {
115+
// qs.forEach(qtext => {
116+
// questions.push({
117+
// request_id: UUID().toString(),
118+
// question: qtext
119+
// })
120+
// })
121+
// }
122+
123+
// db.qa_queries.insertOne({
124+
// _id: store_id,
125+
// store_name: store.store_name,
126+
// menus: [
127+
// {
128+
// menu_id: menu_id,
129+
// menu_name: store.menu_name,
130+
// questions: questions
131+
// }
132+
// ],
133+
// updated_at: new Date()
134+
// })
135+
// })

msa-ai-service/app/db/mongodb.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
# app/db/mongodb.py
21
from pymongo import MongoClient
32
from app.core.config import MONGODB_URI, MONGODB_NAME
43

54
client = MongoClient(MONGODB_URI)
65
db = client[MONGODB_NAME]
76

87
def get_collection(name: str):
9-
"""지정된 컬렉션 반환"""
10-
return db[name]
8+
return db[name] # 지정된 콜렉션 반환

msa-ai-service/app/main.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1+
# (Spring + Mongo + Change Stream 자동 실행)
2+
3+
from fastapi import FastAPI
4+
from contextlib import asynccontextmanager
5+
from app.routes import health, qa_router
6+
from app.services.change_stream_service import start_watchers
7+
from app.core.config import ENV
8+
if ENV == "dev": from app.routes import seed_router
19
import logging
210
import sys
3-
from fastapi import FastAPI
4-
from app.routes import health, seed_router, qa_router
11+
512
logging.basicConfig(
613
level=logging.INFO,
714
format="%(asctime)s %(levelname)-5s %(name)s - %(message)s",
@@ -11,9 +18,18 @@
1118
logger = logging.getLogger(__name__)
1219

1320

14-
app = FastAPI(title="MSA AI Service")
21+
@asynccontextmanager
22+
async def lifespan(app: FastAPI):
23+
# Mongo Change Stream 시작
24+
start_watchers()
25+
yield
1526

27+
app = FastAPI(title="MSA AI Service", lifespan=lifespan)
1628

29+
# 공통 라우터
1730
app.include_router(health.router, prefix="/ai", tags=["health"])
18-
app.include_router(seed_router.router, prefix="/ai", tags=["seed"])
1931
app.include_router(qa_router.router, prefix="/ai", tags=["qa"])
32+
33+
# dev 환경일 때만 seed_router 등록
34+
if ENV == "dev":
35+
app.include_router(seed_router.router, prefix="/ai", tags=["seed"])

msa-ai-service/app/models/qa.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,21 @@
1-
# Pydantic 스키마 (qa_queries, qa_answers)
1+
# Pydantic 스키마 (qa_queries, qa_answers)
2+
# Swagger 문서화/타입 검증용 샘플
3+
4+
from pydantic import BaseModel
5+
from datetime import datetime
6+
7+
class QAQuery(BaseModel):
8+
request_id: str
9+
menu_id: str
10+
question: str
11+
12+
class QAAnswer(BaseModel):
13+
request_id: str
14+
store_id: str
15+
store_name: str
16+
menu_id: str
17+
menu_name: str
18+
answer: str
19+
label: str
20+
polarity: str
21+
created_at: datetime
Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,21 @@
1-
# Pydantic 스키마 (reviews, reviews_embedding)
1+
# Pydantic 스키마 (reviews, reviews_embedding)
2+
3+
from pydantic import BaseModel
4+
from datetime import datetime
5+
from typing import List
6+
7+
class Review(BaseModel):
8+
review_id: str
9+
text: str
10+
created_at: datetime
11+
12+
class Menu(BaseModel):
13+
menu_id: str
14+
menu_name: str
15+
reviews: List[Review] = []
16+
17+
class StoreReview(BaseModel):
18+
_id: str
19+
store_name: str
20+
menus: List[Menu]
21+
updated_at: datetime
Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,86 @@
1-
# 질문 → 답변 API (API endpoint)
2-
# 질문을 DB에 채움
1+
# 강제 수동 실행용 (Change Stream 대신)
2+
# API 트리거 질문 -> 응답 처리 가능
3+
4+
"""
5+
1. (Change Stream 또는 직접 호출 시) qa_queries 문서 전체 읽기
6+
2. 각 질문(request_id) 확인:
7+
queries_embedding에 없으면 → 새 질문 처리 시작
8+
3. queries_embedding 생성 (label, polarity 포함)
9+
4. qa_answers에 같은 store_id + menu_id + label + polarity 답변이 있으면:
10+
qa_answers.created_at < reviews_denorm.updated_at → 새로 생성
11+
아니면 재사용
12+
5. 최종 답변 qa_answers에 저장
13+
"""
14+
315
from fastapi import APIRouter
4-
from app.services.rag_service import run_rag
16+
from datetime import datetime
17+
from app.db.mongodb import get_collection
18+
from app.services.embedding_service import embed_and_label_question
19+
from app.services.rag_service import generate_answer_from_reviews
520

621
router = APIRouter()
722

8-
@router.post("/ask/{request_id}")
9-
async def ask_question(request_id: str):
10-
return await run_rag(request_id)
23+
qa_queries_col = get_collection("qa_queries")
24+
queries_embedding_col = get_collection("queries_embedding")
25+
26+
@router.get("/process-queries")
27+
async def process_queries(limit: int = 10):
28+
"""
29+
수동으로 QA 파이프라인 실행 (Change Stream 대신 직접 확인할 때 사용)
30+
"""
31+
results = []
32+
docs = qa_queries_col.find().limit(limit)
33+
34+
for doc in docs:
35+
store_id = doc["_id"]
36+
store_name = doc["store_name"]
37+
38+
for menu in doc.get("menus", []):
39+
menu_id = menu["menu_id"]
40+
menu_name = menu["menu_name"]
41+
42+
for q in menu.get("questions", []):
43+
request_id = q["request_id"]
44+
question = q["question"]
45+
46+
# 이미 처리된 질문이면 skip
47+
queries_doc = queries_embedding_col.find_one({"_id": store_id})
48+
existing_ids = []
49+
if queries_doc:
50+
for m in queries_doc["menus"]:
51+
if m["menu_id"] == menu_id:
52+
existing_ids = [qe["request_id"] for qe in m.get("questions_embedding", [])]
53+
if request_id in existing_ids:
54+
continue
55+
56+
# 질문 라벨링 + 임베딩
57+
label, polarity, embedding = embed_and_label_question(question)
58+
59+
queries_embedding_col.update_one(
60+
{"_id": store_id, "menus.menu_id": menu_id},
61+
{
62+
"$push": {
63+
"menus.$.questions_embedding": {
64+
"request_id": request_id,
65+
"question": question,
66+
"label": label,
67+
"polarity": polarity,
68+
"embedding": embedding,
69+
"created_at": datetime.utcnow()
70+
}
71+
},
72+
"$set": {"updated_at": datetime.utcnow(), "store_name": store_name}
73+
},
74+
upsert=True
75+
)
76+
77+
# RAG 실행 (라벨 맞는 리뷰 통계로 응답 생성)
78+
answer_result = generate_answer_from_reviews(store_id, menu_id, question)
79+
80+
results.append({
81+
"request_id": request_id,
82+
"answer": answer_result.get("answer"),
83+
"reviews_used": answer_result.get("reviews_used", [])
84+
})
85+
86+
return {"processed": len(results), "results": results}

0 commit comments

Comments
 (0)