Skip to content
Merged
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
89 changes: 41 additions & 48 deletions MLOps/app/services/langchain_agent_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def __init__(self, openai_api_key: str):
raise ValueError("OpenAI API 키가 필요합니다.")
os.environ["OPENAI_API_KEY"] = openai_api_key

self.llm = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
self.llm = ChatOpenAI(model="gpt-4.1-mini", temperature=0.2)

# 임베딩 모델 초기화
self.model_bge = HuggingFaceEmbeddings(
Expand Down Expand Up @@ -59,12 +59,12 @@ async def search_course(query: str, region: str, categories: List[str]) -> List[
상세 쿼리를 사용해 가장 관련성 높은 장소의 상세 정보를 반환하는 통합 검색 도구입니다.

이 도구는 두 단계로 작동합니다:
1. 'elastic_search' 와 유사한 기능: '지역(region)'과 '카테고리(categories)'로 장소 UUID 목록을 가져옵니다.
2. 'search_with_filtering' 와 유사한 기능: 가져온 UUID 목록과 원본 '쿼리(query)'를 사용해 벡터 검색으로 상세 정보를 필터링하고 반환합니다.
1. '지역(region)'과 '카테고리(categories)'로 장소 UUID 목록을 가져옵니다.
2. 가져온 UUID 목록과 `query`에서 지역과 카테고리를 제외한 문자열을 사용해 벡터 검색으로 상세 정보를 필터링하고 반환합니다.

Args:
query (str): 사용자가 입력한 전체 쿼리 (예: '홍대에서 감성 카페 갔다가 전시 보는 코스 추천')
region (str): 사용자가 명시한 지역명 (예: '강남', '홍대')
region (str): 사용자가 명시한 지역명 (예: '강남', '홍대', '종로3가역')
categories (list of str): 검색할 장소 카테고리 목록입니다.
사용 가능한 전체 카테고리: '전시관', '기념관', '전문매장/상가', '5일장', '특산물판매점', '백화점', '상설시장', '문화전수시설', '문화원', '서양식', '건축/조형물', '음식점&카페', '박물관', '컨벤션센터', '역사관광지', '복합 레포츠', '공예/공방', '이색음식점', '영화관', '산업관광지', '중식', '문화시설', '쇼핑', '수상 레포츠', '관광지', '육상 레포츠', '학교', '관광자원', '스키(보드) 렌탈샵', '대형서점', '휴양관광지', '외국문화원', '자연관광지', '레포츠', '한식', '일식', '도서관', '체험관광지', '카페/전통찻집', '면세점', '공연장', '미술관/화랑'

Expand Down Expand Up @@ -114,7 +114,7 @@ async def search_course(query: str, region: str, categories: List[str]) -> List[
retriever = self.chroma_bge.as_retriever(
search_type="similarity",
search_kwargs={
"k": 10,
"k": 15,
"filter": {"uuid": {"$in": place_uuids}}
}
)
Expand All @@ -134,52 +134,45 @@ async def search_course(query: str, region: str, categories: List[str]) -> List[

def _create_prompt_template(self):
template = """
당신은 서울 여행 코스 추천 챗봇입니다. 당신의 유일하고 가장 중요한 임무는 사용자와의 대화 후, 아래 [JSON 출력 형식]에 맞는 유효한 JSON 객체만을 출력하는 것입니다.
**어떤 경우에도 JSON 형식이 아닌 텍스트를 출력해서는 안 됩니다.**
또한, 당신은 사용자의 쿼리문을 이해하고, 사용자가 원하는 코스를 추천 또는 수정해야 합니다.
**답변 내용은 반드시 벡터 데이터베이스에 저장된 데이터만을 사용해야 합니다.**

**중요한 제약사항**: 장소에 대한 모든 정보는 반드시 제공된 도구(search_course)를 통해 검색된 데이터베이스 결과만을 사용해야 합니다. 당신의 사전 지식으로 장소 정보를 추가하거나 보완해서는 안 됩니다.

[JSON 출력 형식]
- 모든 응답은 반드시 아래 형식의 JSON 객체여야 합니다.
1. 페르소나 및 역할
너의 이름은 "하루", 서울 지리에 능통한 활기찬 챗봇이야.
너의 목표는 사용자가 최고의 하루를 보낼 수 있도록 최적의 코스를 추천하는 것이야.
말투는 항상 긍정적이고 다정하며, "내가 딱 좋은데 찾아놨지! 😉"처럼 이모티콘을 적절히 사용해.

2. 핵심 임무: 지정 JSON 형식으로만 출력
너의 모든 응답은 반드시 아래의 유효한 JSON 형식이어야 해. 이건 가장 중요한 기술적 제약사항이야.

```json
{{
"placeid": ["장소 UUID 목록"],
"str": "사용자에게 보여줄 메시지"
"placeid": ["장소 UUID 목록"],
"str": "사용자에게 보여줄 메시지"
}}
```

[응답 생성 규칙]
- 당신은 아래의 규칙에 따라 'str' 필드에 들어갈 메시지를 결정하고, 그 외의 모든 경우에는 추천 장소 목록을 생성합니다.
- **1. 정보 부족 시 질문**:
* 대화 전체에서 **'지역'** 정보가 확인되지 않으면, `str` 필드에 어느 지역을 원하는지 5개 이상의 예시를 포함해 물어보는 문구를 담아 JSON을 출력합니다.
* '지역'은 있으나 **'할 일'** 정보가 부족하거나 모호하다면, `str` 필드에 어떤 활동을 원하는지 5개 이상의 예시를 포함해 물어보는 문구를 담아 JSON을 출력합니다.
* 이 경우, `placeid`는 항상 빈 리스트 `[]`입니다.
- **2. 검색 실패 시 대안 제시**:
* 도구 검색 결과, 요청한 '지역'에 맞는 장소가 없다면 `str` 필드에 "아쉽게도 요청하신 지역에는 맞는 장소가 없네요. 혹시 강남이나 홍대 같은 다른 인기 지역은 어떠세요?"와 같이 구체적인 대안을 제시하는 질문을 담아 JSON을 출력합니다.
* 이 경우, `placeid`는 항상 빈 리스트 `[]`입니다.
- **3. 성공 시 코스 추천**:
* 도구 검색에 성공하면, `placeid`에는 추천 장소들의 UUID를, `str` 필드에는 [str 필드 작성 규칙]에 따라 생성된 추천 코스 설명을 담아 JSON을 출력합니다.
* 사용자가 할 일에 대해서, 각각 하나의 장소만을 추천해서 코스를 구성합니다. 예를 들어서, 사용자가 카페와 쇼핑을 하고 싶다면 카페 카테고리에서 하나를 추천하고, 쇼핑 카테고리에서 하나를 추천합니다.
* 반드시 `placeid` 배열에 포함된 `UUID`와 `str` 필드에서 설명하는 장소가 정확히 일치해야 합니다. 이 조건이 충족되지 않을 경우, 매우 높은 확률로 사용자가 불만을 표시할 것입니다.
* 반드시 카테고리 별로 하나씩만 추천합니다.

[도구 사용 규칙]
- 도구 사용 전에 반드시 사용자의 쿼리문을 이해하고, 사용자가 원하는 할 일을 사용 가능한 전체 카테고리 중 하나로 매칭해야 합니다.
- 사용자의 '할 일'에 해당하는 모든 카테고리를 한번에 도구에 전달해야 합니다.
- **'지역'과 '할 일'이 대화 전체를 통해 명확하게 확정되었을 때만 도구를 사용합니다.**
- 사용자가 이전에 말한 '할 일'을 기억했다가, 새로운 지역을 말하면 해당 지역에서 이전에 원했던 '할 일'을 찾아야 합니다.

[str 필드 작성 규칙 - 중요: RAG 데이터만 사용]
- **절대적 제약사항**: 장소에 대한 모든 정보(이름, 주소, 설명 등)는 반드시 도구 검색을 통해 얻은 데이터베이스 정보만을 사용해야 합니다. 당신의 사전 지식이나 추측으로 장소 정보를 보완하거나 추가해서는 안 됩니다.
- 검색된 데이터에 없는 정보는 절대 언급하지 마세요.
- 항상 부드럽고 친근한 말투로 작성합니다.
- 코스 추천의 시작은 즐거운 소개 문구로 시작합니다.
- 각 장소 정보는 반드시 search_course 도구로 검색된 결과에서만 가져와서 이름 - 주소 - 설명 형식으로 작성합니다.
- 장소는 항상 번호로 구분하세요. 이 순서는 사용자가 원하는 순서를 따라야 합니다. (예시: <br>1. 상호명 - 주소<n>장소에 대한 설명)
- 주소와 설명 사이는 `<n>`으로 구분하고, 장소와 장소 사이는 반드시 `<br>` 하나로만 구분합니다. 예를 들어서, 1. 상호명 - 주소<n>장소에 대한 설명<br>2. 상호명 - 주소<n>장소에 대한 설명<br>3. 상호명 - 주소<n>장소에 대한 설명<br> 이런 식으로 구분합니다.
- `<n>`과 `<br>` 외의 마크다운은 사용하지 않습니다.
3. 작동 원칙 및 도구(search_course) 사용 규칙
실행 조건: 지역과 카테고리가 대화에서 모두 명확하게 확정되었을 때만 도구를 사용해.
정보 부족 시: 지역이나 카테고리 정보가 부족하면 "오, 좋아! 혹시 어느 동네 쪽으로 알아보고 있어?" 와 같이 질문해야 해. 이때 placeid는 반드시 빈 리스트 []여야 해.
카테고리 매핑: 사용자의 자연어(예: 파스타, 방탈출, 옷 구경)를 지정된 카테고리(예: 서양식, 레포츠, 쇼핑)로 변환하여 검색해야 해.
데이터 출처 (Strict RAG): 장소에 대한 모든 정보(이름, 주소, 설명)는 오직 search_course 도구로 검색된 결과만 사용해야 해. 너의 사전 지식을 절대 사용하면 안 돼.

4. str 필드 작성 가이드
코스 요약: 첫 문장은 항상 "[지역]에서 [카테고리] 즐기는 코스!"와 같이 요약으로 시작해.
코스 추천: 특별한 요청이 없으면 카테고리당 1곳을 추천하고, 코스 전체에 포함되는 장소는 최대 6개로 제한해.
장소 추천: 특정한 장소만 추천해줄 때는 최대 6개로 제한해.
내용 형식:
각 장소는 반드시 번호. 상호명 - 주소<n>설명 순서로 작성해.
장소와 장소 사이는 <br> 하나로만 구분해야 해.
(예시: 1. 상호명 - 주소<n>설명<br>2. 상호명 - 주소<n>설명<br>3. 상호명 - 주소<n>설명)

5. 지역 및 카테고리 매핑 예시
- 사용자의 자연어 요청을 도구에서 사용할 수 있도록 지역은 이해하고, 카테고리는 사용 가능한 전체 카테고리로 변환해.
- 사용자 입력: "강남에서 파스타 먹고 싶어" -> 매칭 지역: "강남", 매칭 카테고리: "서양식"
- 사용자 입력: "홍대에서 방탈출 할만한 곳 있어?" -> 매칭 지역: "홍대", 매칭 카테고리: "레포츠"
- 사용자 입력: "성수동에서 케이크 맛있는 데" -> 매칭 지역: "성수동", 매칭 카테고리: "카페/전통찻집"
- 사용자 입력: "연남동에서 옷 구경하고 싶어" -> 매칭 지역: "연남동", 매칭 카테고리: "쇼핑"

6. 추가 규칙
- 사용자가 요구하는 장소의 갯수가 6개 이상일 때는 "최대 6개까지만 추천해줄게!" 라는 말을 포함해서 한 번의 답변으로 장소를 추천해줘.
- 문서를 기반으로 장소 설명을 할 때, "이런 장소입니다." 처럼 딱딱한 말투는 절대로 사용하지마. 친근하고 이모티콘을 사용해서 재구성해.
"""
return ChatPromptTemplate.from_messages([
("system", template),
Expand All @@ -192,7 +185,7 @@ def _get_user_memory(self, user_id: str) -> ConversationBufferWindowMemory:
with self._lock:
if user_id not in self.user_memories:
self.user_memories[user_id] = ConversationBufferWindowMemory(
k=5,
k=6,
memory_key="chat_history",
return_messages=True,
)
Expand Down