diff --git a/main.py b/main.py index 8948b84..3f79a27 100644 --- a/main.py +++ b/main.py @@ -1,136 +1,131 @@ -import json -from fastapi import FastAPI, HTTPException -from pydantic import BaseModel, Field -from typing import List, Optional -import httpx +import json # 표준 라이브러리: 문자열 ↔ JSON 변환에 사용 +from fastapi import FastAPI, HTTPException # FastAPI 앱 생성, 에러 응답용 예외 +from pydantic import BaseModel, Field # 요청/응답 스키마 정의 +from typing import List, Optional # 타입 힌트: 리스트, Optional +import httpx # 비동기 HTTP 클라이언트 (LLM API 호출용) -from config import settings +from config import settings # 환경설정/비밀키를 담은 settings 객체 임포트 # FastAPI 앱 및 모델 정의 app = FastAPI( title="Talky-AI Service", description="백엔드로부터 전달받은 컨텍스트를 기반으로 문장을 생성하는 AI 서비스", - version="2025.08.04", # 프롬프트 수정 + version="2025.09.02", # AI 프롬프트 우선순위 강화 버전 ) # /recommendations API를 위한 모델들 -class RecommendationRequest(BaseModel): - keywords: List[str] = Field(..., description="장소, 상황 등을 나타내는 키워드 목록", example=["병원", "두통"]) - context: Optional[str] = Field(None, description="사용자가 직접 입력한 현재 상황 설명", example="머리가 아파서 왔어요") # null 허용 - conversation: Optional[List[str]] = Field(None, description="최근 대화 기록 (사용자, 상대방 포함)", example=["안녕하세요, 어떻게 오셨어요?", "진료받으러 왔습니다."]) # null 허용 - favorites: Optional[List[str]] = Field(default_factory=list, description="사용자가 즐겨찾기한 문장 목록", example=["이거 주세요", "감사합니다"]) # 없어도 빈 리스트로 처리될 수 있게 함 +class RecommendationRequest(BaseModel): # 요청 바디 스키마 정의 + keywords: List[str] = Field(..., # 필수 필드: 장소/상황 키워드 목록 + description="장소, 상황 등을 나타내는 키워드 목록", + example=["병원", "두통"]) + context: Optional[str] = Field( # 선택 필드: 현재 상황 설명 + None, description="사용자가 직접 입력한 현재 상황 설명", + example="머리가 아파서 왔어요") # null 허용 + conversation: Optional[List[str]] = Field( # 선택 필드: 최근 대화 기록 (문자열 리스트) + None, description="최근 대화 기록 (사용자, 상대방 포함)", + example=["안녕하세요, 어떻게 오셨어요?", "진료받으러 왔습니다."]) # null 허용 + sttMessage: Optional[str] = Field( + None, + description="상대방의 마지막 음성인식(STT) 메시지", + example="아픈지 얼마나 되셨어요?" # 타입을 str에 맞게 수정 + ) + favorites: Optional[List[str]] = Field( # 선택 필드: 즐겨찾기 문장 목록 + default_factory=list, # 기본값: [] (None이어도 빈 리스트로 처리) + description="사용자가 즐겨찾기한 문장 목록", + example=["그렇게 해주세요", "감사합니다"]) # 없어도 빈 리스트로 처리될 수 있게 함 + +class Sentence(BaseModel): # 내부적으로 사용하는 문장 객체 스키마 + id: int # 고유 ID (1부터 부여) + text: str # 문장 텍스트 + +class RecommendationResponse(BaseModel): # 응답 바디 스키마 정의 + category: str # 메인 카테고리(첫 번째 키워드 등) + recommended_sentences: List[Sentence] # 추천 문장 리스트 -class Sentence(BaseModel): - id: int - text: str +# AI 로직 함수 +async def generate_ai_sentences(request: RecommendationRequest) -> List[str]: + """ + 모든 컨텍스트를 한 번에 처리하여, 즐겨찾기를 우선적으로 고려한 최종 추천 문장을 생성합니다. + """ + # 프롬프트에 전달할 정보들을 안전하게 문자열로 변환 + keywords_str = ", ".join(request.keywords) if request.keywords else "없음" + context_str = request.context if request.context else "없음" + stt_message_str = request.sttMessage if request.sttMessage else "없음" + 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 "없음" -class RecommendationResponse(BaseModel): - category: str - recommended_sentences: List[Sentence] + print(f"AI 문장 생성 요청 수신: keywords='{keywords_str}', context='{context_str}'") + # AI에게 보낼 훨씬 더 똑똑하고 상세한 지시서(프롬프트) + prompt = f""" + - 역할 + 당신은 상대방 질문의 '유형'을 먼저 분석하고, 그 유형에 가장 적합한 답변을 생성하는 지능형 대화 문장 생성 AI입니다. -# AI 로직 함수 + - 해야할 일 + 상대방의 마지막 질문(`sttMessage`)을 분석하여, 그에 대한 가장 자연스럽고 직접적인 답변 문장 4개를 생성합니다. + + - 따라야 할 생각의 흐름 + 1. **[1단계: 질문 유형 분석]** + - 상대방의 질문을 읽는다: "{stt_message_str}" + - 이 질문이 "네/아니오"로 답해야 하는 질문인가? 아니면 "언제, 어디서, 무엇을" 같은 구체적인 정보를 묻는 질문인가? 스스로 판단한다. -async def find_relevant_favorites(request: RecommendationRequest) -> List[str]: - """현재 상황과 직접적으로 관련된 즐겨찾기 문장을 찾아냅니다.""" - if not request.favorites: - return [] + 2. **[2단계: 답변 생성 전략 수립]** + - **만약 "네/아니오" 질문이라면:** "네, ..." 또는 "아니요, ..." 형식으로 시작하는 답변을 구상한다. + - **만약 "정보 요구" 질문이라면:** **"네/아니요" 없이** "어제부터요.", "머리가 아파서요." 와 같이 질문의 핵심에 대한 정보로 바로 시작하는 답변을 구상한다. - # 대화의 가장 마지막 내용 - last_dialogue = request.conversation[0] if request.conversation else "없음" - - prompt = f""" - 당신은 문장 관련성 분석 전문가입니다. - 주어진 현재 상황과 가장 직접적으로 관련이 높고, 바로 사용해도 어색하지 않은 문장을 즐겨찾기 목록에서 모두 골라주세요. + 3. **[3단계: 최종 문장 생성]** + - 먼저, `사용자의 평소 말투 (즐겨찾기)` 목록을 확인한다. 만약 현재 질문에 대한 완벽한 답변이 즐겨찾기에 있다면, 그 문장을 최종 추천 목록에 최우선으로 포함시킨다. + - 나머지 비어있는 자리(총 4개 중)는 위 2단계 전략과 `참고 정보`를 활용하여 가장 적절하고 다양한 새 문장을 생성하여 채워넣는다. - [현재 상황] - - 주요 키워드: {", ".join(request.keywords)} - - 상세 설명: {request.context or "없음"} - - 방금 들은 말: "{last_dialogue}" - [즐겨찾기 목록] - {", ".join(request.favorites)} + ### 참고 정보 ### + - **사용자의 현재 상황:** {context_str} + - **대화 주제 키워드:** {keywords_str} + - **사용자의 평소 말투:** {favorites_str} - [출력 형식] - - 반드시 "relevant_favorites" 라는 키를 가진 JSON 객체여야 합니다. - - 값은 당신이 고른 문장들이 담긴 문자열 배열입니다. 관련 있는 문장이 없으면 빈 배열 `[]`을 반환하세요. + ### 출력 형식 ### + - 답변은 반드시 "generated_sentences" 라는 단 하나의 키를 가진 JSON 객체여야 합니다. + - 값은 최종적으로 추천할 문장 4개가 담긴 문자열 배열입니다. """ - api_url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key={settings.GOOGLE_API_KEY}" - payload = {"contents": [{"parts": [{"text": prompt}]}], "generationConfig": {"responseMimeType": "application/json", "temperature": 0.2}} - try: - async with httpx.AsyncClient() as client: - response = await client.post(api_url, json=payload, timeout=20) - response.raise_for_status() - ai_response = response.json() - text_content = ai_response["candidates"][0]["content"]["parts"][0]["text"] - return json.loads(text_content).get("relevant_favorites", []) - except Exception: - return [] # 오류 발생 시 빈 리스트 반환 - -async def generate_additional_sentences(request: RecommendationRequest, existing_sentences: List[str]) -> List[str]: - """이미 찾은 문장을 제외하고, 추가적인 추천 문장을 생성합니다.""" - - keywords_str = ", ".join(request.keywords) - conversation_str = "\n".join([f"- {line}" for line in (request.conversation or [])]) - favorites_str = ", ".join(request.favorites or []) - context_str = request.context or "없음" - existing_sentences_str = ", ".join(existing_sentences) if existing_sentences else "없음" - prompt = f""" - 당신은 AAC 사용자를 위한 대화 문장 생성 AI입니다. - 주어진 모든 정보를 종합하여, 사용자의 입장에서 다음에 할 가장 자연스러운 문장을 생성해야 합니다. - 단, 이미 찾은 문장 목록에 있는 것과 똑같거나 매우 유사한 문장은 생성하면 안 됩니다. - - ### 입력 정보 ### - 1. **주요 키워드 (장소, 상황):** {keywords_str} - 2. **사용자가 직접 입력한 상황:** "{context_str}" - 3. **최근 대화 기록 (가장 최근 대화가 맨 위에 있음):** - {conversation_str if conversation_str else "(대화 시작 전)"} - 4. **사용자의 즐겨찾기 문장 (평소 말투 힌트):** {favorites_str if favorites_str else "없음"} - 5. **이미 찾은 문장 (중복 생성 금지):** {existing_sentences_str} - - ### 생성 규칙 ### - - 총 4개의 추천 문장이 필요합니다. [이미 찾은 문장]의 개수를 제외하고, 나머지 개수만큼만 새롭게 생성해주세요. - - 예를 들어, 이미 찾은 문장이 1개라면 3개를, 2개라면 2개를 새로 생성하면 됩니다. - - 생성된 문장은 반드시 사용자의 입장에서 말하는 것이어야 합니다. - - 답변은 "generated_sentences" 키를 가진 JSON 객체여야 하며, 값은 생성된 문장들이 담긴 문자열 배열입니다. - """ - - api_url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key={settings.GOOGLE_API_KEY}" - payload = {"contents": [{"parts": [{"text": prompt}]}], "generationConfig": {"responseMimeType": "application/json", "temperature": 0.8}} + # Gemini 2.5 Flash 모델 + api_url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:generateContent?key={settings.GOOGLE_API_KEY}" + payload = { + "contents": [{"parts": [{"text": prompt}]}], + "generationConfig": { + "responseMimeType": "application/json", + "temperature": 0.3 + } + } + try: async with httpx.AsyncClient() as client: response = await client.post(api_url, json=payload, timeout=30) response.raise_for_status() ai_response = response.json() - text_content = ai_response["candidates"][0]["content"]["parts"][0]["text"] + candidates = ai_response.get("candidates", []) + if not candidates: + return [] + text_content = candidates[0].get("content", {}).get("parts", [{}])[0].get("text", "{}") return json.loads(text_content).get("generated_sentences", []) except Exception as e: raise HTTPException(status_code=500, detail=f"AI 서비스 처리 중 오류가 발생했습니다: {e}") - -# API 엔드포인트 - +# --- 3. API 엔드포인트 --- @app.post("/recommendations", response_model=RecommendationResponse, summary="AI 실시간 문장 추천 (컨텍스트 기반)") async def get_recommendations(request: RecommendationRequest): """메인 백엔드로부터 전달받은 풍부한 컨텍스트로 AI 추천 문장을 생성합니다.""" - - relevant_favorites = await find_relevant_favorites(request) - - # 나머지 필요한 문장들을 AI에게 추가로 생성해달라고 요청. - additional_sentences = [] - if len(relevant_favorites) < 4: - additional_sentences = await generate_additional_sentences(request, relevant_favorites) - - # 두 결과를 합쳐서 최종 추천 목록을 만듭니다. - final_sentence_texts = relevant_favorites + additional_sentences - - if not final_sentence_texts: + + # 이제 단 하나의 AI 함수만 호출하면 됩니다. + generated_sentences = await generate_ai_sentences(request) + + if not generated_sentences: raise HTTPException(status_code=500, detail="AI가 문장을 생성하지 못했습니다.") - - final_sentences = [Sentence(id=i + 1, text=text) for i, text in enumerate(final_sentence_texts)] + + final_sentences = [Sentence(id=i + 1, text=text) for i, text in enumerate(generated_sentences)] + main_category = request.keywords[0] if request.keywords else "일상" - + return RecommendationResponse( category=main_category, recommended_sentences=final_sentences