Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
663 changes: 663 additions & 0 deletions data/scenarios.json

Large diffs are not rendered by default.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Empty file.
Binary file added data/vector_db/chroma.sqlite3
Binary file not shown.
34 changes: 33 additions & 1 deletion main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from typing import List, Optional # 타입 힌트: 리스트, Optional
import httpx # 비동기 HTTP 클라이언트 (LLM API 호출용)
from stt import transcribe_audio # STT 처리 함수 임포트
from rag.retriever import retrieve_scenario
from rag.database import get_db, get_embedding_model # 이 줄 추가

from fastapi import UploadFile, File, Form, HTTPException # 파일 업로드 처리용

Expand All @@ -16,6 +18,12 @@
version="2025.09.14", # AI가 해야할 일 수정(프롬프트)
)

@app.on_event("startup")
async def startup_event():
get_db()
get_embedding_model() # 이제 정상 작동
print("RAG 데이터베이스와 임베딩 모델이 준비되었습니다.")

# /recommendations API를 위한 모델들
class RecommendationRequest(BaseModel): # 요청 바디 스키마 정의
keywords: List[str] = Field(..., # 필수 필드: 장소/상황 키워드 목록
Expand Down Expand Up @@ -49,7 +57,7 @@ class RecommendationResponse(BaseModel): # 응답 바디 스키마 정의
# AI 로직 함수
async def generate_ai_sentences(request: RecommendationRequest) -> List[str]:
"""
모든 컨텍스트를 한 번에 처리하여, 즐겨찾기를 우선적으로 고려한 최종 추천 문장을 생성합니다.
[RAG] 적용 , 모든 컨텍스트를 한 번에 처리하여, 즐겨찾기를 우선적으로 고려한 최종 추천 문장을 생성합니다.
"""
# 프롬프트에 전달할 정보들을 안전하게 문자열로 변환
keywords_str = ", ".join(request.keywords) if request.keywords else "없음"
Expand All @@ -58,7 +66,24 @@ async def generate_ai_sentences(request: RecommendationRequest) -> List[str]:
conversation_str = "\n".join([f"- {line}" for line in request.conversation]) if request.conversation else "(대화 시작 전)"
favorites_str = "\n".join([f"- {fav}" for fav in request.favorites]) if request.favorites else "없음"

# RAG 검색: 키워드와 상황을 조합해서 시나리오 검색
search_query = f"{keywords_str} {context_str}".strip()
retrieved_scenario = retrieve_scenario(search_query)

scenario_guide = "없음. 아래 '참고 정보'만을 바탕으로 생성하세요."
example_dialogue_str = "없음" # 변수 초기화
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if retrieved_scenario: 블록 이전에 example_dialogue_str 변수를 기본값(예: "없음")으로 초기화해서 NameError 방지하도록 수정했습니다

if retrieved_scenario:
goal = retrieved_scenario.get('goal', 'N/A')
flow = "\n".join(retrieved_scenario.get('typical_flow', []))
scenario_guide = f"""- 시나리오 목표: {goal}
- 이상적인 대화 흐름:
{flow}"""
if retrieved_scenario.get("example_dialogue"):
dialogue_lines = [f"- {d['speaker']}: {d['line']}" for d in retrieved_scenario["example_dialogue"]]
example_dialogue_str = "\n".join(dialogue_lines)

print(f"AI 문장 생성 요청 수신: keywords='{keywords_str}', context='{context_str}'")
print(f"RAG 검색 결과: {'시나리오 발견' if retrieved_scenario else '시나리오 없음'}")

# AI에게 보낼 지시서(프롬프트)
prompt = f"""
Expand All @@ -81,6 +106,13 @@ async def generate_ai_sentences(request: RecommendationRequest) -> List[str]:
3. **[3단계: 최종 문장 생성]**
- 먼저, `사용자의 평소 말투 (즐겨찾기)` 목록을 확인한다. 만약 현재 질문에 대한 완벽한 답변이 즐겨찾기에 있다면, 그 문장을 최종 추천 목록에 최우선으로 포함시킨다.
- 나머지 비어있는 자리(총 4개 중)는 위 2단계 전략과 `참고 정보`를 활용하여 가장 적절하고 다양한 새 문장을 생성하여 채워넣는다.
- **모든 문장은 존댓말로 생성하세요.**


### 시나리오 가이드 ###
{scenario_guide}
### 예시 대화 ###
{example_dialogue_str}


### 참고 정보 ###
Expand Down
Empty file added rag/__init__.py
Empty file.
71 changes: 71 additions & 0 deletions rag/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import json
import chromadb
import google.generativeai as genai
from sentence_transformers import SentenceTransformer

# --- 설정 ---
SCENARIOS_PATH = "./data/scenarios.json"
DB_PATH = "./data/vector_db"
COLLECTION_NAME = "talky_scenarios"
EMBEDDING_MODEL_NAME = "all-MiniLM-L12-v2" # SBERT 모델

# --- 전역 변수 (싱글톤) ---
_db_client = None
_scenario_collection = None
_embedding_model = None

def get_embedding_model():
"""임베딩 모델을 한 번만 로드하여 재사용 (싱글톤)"""
global _embedding_model
if _embedding_model is None:
_embedding_model = SentenceTransformer(EMBEDDING_MODEL_NAME)
return _embedding_model

def get_db():
"""ChromaDB 클라이언트와 컬렉션을 한 번만 초기화하여 재사용"""
global _db_client, _scenario_collection
if _db_client is None:
_db_client = chromadb.PersistentClient(path=DB_PATH)
_scenario_collection = _db_client.get_or_create_collection(name=COLLECTION_NAME)
return _scenario_collection

def build_database():
"""
scenarios.json 파일을 읽어 ChromaDB에 벡터 데이터베이스를 구축하는 함수.
최초 1회 또는 데이터 업데이트 시 실행합니다.
"""
collection = get_db()

# DB에 이미 데이터가 있으면 중복 구축 방지
if collection.count() > 0:
print(f"이미 {collection.count()}개의 데이터가 존재합니다. 구축을 건너뜁니다.")
return

print("scenarios.json 파일을 읽어 데이터베이스 구축을 시작합니다...")

with open(SCENARIOS_PATH, "r", encoding="utf-8") as f:
scenarios = json.load(f)

model = get_embedding_model()

# 데이터 준비
ids = [s["scenario_id"] for s in scenarios]
documents = [s["embedding_text"] for s in scenarios]
metadatas = [{"category": s["category"], "task": s["task"]} for s in scenarios]

# SBERT 모델로 임베딩 생성
embeddings = model.encode(documents, convert_to_numpy=True).tolist()

# DB에 추가
collection.add(
ids=ids,
embeddings=embeddings,
documents=[json.dumps(s["content"]) for s in scenarios], # content를 document로 저장
metadatas=metadatas
)

print(f"데이터베이스 구축 완료! 총 {collection.count()}개의 시나리오가 추가되었습니다.")

# 이 파일을 직접 실행하면 DB를 구축하도록 설정
if __name__ == '__main__':
build_database()
32 changes: 32 additions & 0 deletions rag/retriever.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import json
from rag.database import get_db, get_embedding_model

def retrieve_scenario(query_text: str):
"""
사용자의 입력(query_text)을 받아 가장 유사한 시나리오 1개를 검색하여 반환합니다.
"""
if not query_text.strip():
return None

collection = get_db()
model = get_embedding_model()

# 1. 검색어 임베딩
query_embedding = model.encode(query_text, convert_to_numpy=True).tolist()

# 2. ChromaDB에 쿼리
results = collection.query(
query_embeddings=[query_embedding],
n_results=1
)

# 3. 결과 파싱 및 반환
if results and results['ids'][0]:
scenario_id = results['ids'][0][0]
# document에 저장된 content json 문자열을 다시 파싱
retrieved_content = json.loads(results['documents'][0][0])
print(f"✅ RAG 성공: '{scenario_id}' 시나리오를 검색했습니다.")
return retrieved_content
else:
print("🟡 RAG: 유사한 시나리오를 찾지 못했습니다.")
return None
6 changes: 5 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@ pydantic-settings
python-multipart

# 테스트용 앱
streamlit
streamlit

chromadb
google-generativeai
sentence-transformers # 임베딩 모델용
Loading