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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.back.koreaTravelGuide.common.config

import com.back.koreaTravelGuide.domain.ai.aiChat.tool.TourTool
import com.back.koreaTravelGuide.domain.ai.aiChat.tool.WeatherTool
import org.springframework.ai.chat.client.ChatClient
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor
Expand Down Expand Up @@ -36,10 +37,11 @@ class AiConfig {
chatModel: ChatModel,
chatMemory: ChatMemory,
weatherTool: WeatherTool,
tourTool: TourTool,
): ChatClient {
return ChatClient.builder(chatModel)
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.defaultTools(weatherTool)
.defaultTools(weatherTool, tourTool)
.build()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
package com.back.koreaTravelGuide.common.config

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator
import com.fasterxml.jackson.module.kotlin.KotlinModule
import org.springframework.cache.CacheManager
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.cache.RedisCacheConfiguration
import org.springframework.data.redis.cache.RedisCacheManager
import org.springframework.data.redis.connection.RedisConnectionFactory
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer
import org.springframework.data.redis.serializer.RedisSerializationContext
import org.springframework.data.redis.serializer.StringRedisSerializer
import java.time.Duration

@Configuration
class RedisConfig {
Expand All @@ -22,4 +31,37 @@ class RedisConfig {

return template
}

@Bean
fun cacheManager(connectionFactory: RedisConnectionFactory): CacheManager {
// Kotlin data class 역직렬화 지원을 위한 ObjectMapper 생성
val objectMapper =
ObjectMapper().apply {
// Kotlin 모듈 등록 (data class 생성자 인식)
registerModule(KotlinModule.Builder().build())
// 타입 정보 보존을 위한 검증기 설정
activateDefaultTyping(
BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Any::class.java)
.build(),
ObjectMapper.DefaultTyping.NON_FINAL,
)
}

val redisCacheConfiguration =
RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer()),
)
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
GenericJackson2JsonRedisSerializer(objectMapper),
),
)
.entryTtl(Duration.ofHours(12))

return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(redisCacheConfiguration)
.build()
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.back.koreaTravelGuide.domain.ai.aiChat.tool

import com.back.backend.BuildConfig
import com.back.koreaTravelGuide.common.logging.log
import com.back.koreaTravelGuide.domain.ai.tour.dto.TourDetailParams
import com.back.koreaTravelGuide.domain.ai.tour.dto.TourLocationBasedParams
import com.back.koreaTravelGuide.domain.ai.tour.service.TourService
Expand All @@ -9,7 +10,7 @@ import org.springframework.ai.tool.annotation.ToolParam
import org.springframework.stereotype.Component

@Component
class TourToolExample(
class TourTool(
private val tourService: TourService,
) {
/**
Expand All @@ -21,17 +22,18 @@ class TourToolExample(
*/

@Tool(description = "areaBasedList2 : 지역기반 관광정보 조회, 특정 지역의 관광 정보 조회")
fun getTourInfo(
fun getAreaBasedTourInfo(
@ToolParam(description = BuildConfig.CONTENT_TYPE_CODES_DESCRIPTION, required = true)
contentTypeId: String,
@ToolParam(description = BuildConfig.AREA_CODES_DESCRIPTION, required = true)
areaAndSigunguCode: String,
): String {
// areaAndSigunguCode를 areaCode와 sigunguCode로 분리
val tourParams = tourService.parseParams(contentTypeId, areaAndSigunguCode)
log.info("🔧 [TOOL CALLED] getAreaBasedTourInfo - contentTypeId: $contentTypeId, areaAndSigunguCode: $areaAndSigunguCode")

val tourParams = tourService.parseParams(contentTypeId, areaAndSigunguCode)
val tourInfo = tourService.fetchTours(tourParams)

log.info("✅ [TOOL RESULT] getAreaBasedTourInfo - 결과: ${tourInfo.toString().take(100)}...")
return tourInfo.toString() ?: "지역기반 관광정보 조회를 가져올 수 없습니다."
}

Expand All @@ -47,7 +49,7 @@ class TourToolExample(
*/

@Tool(description = "locationBasedList2 : 위치기반 관광정보 조회, 특정 위치 기반의 관광 정보 조회")
fun get(
fun getLocationBasedTourInfo(
@ToolParam(description = BuildConfig.CONTENT_TYPE_CODES_DESCRIPTION, required = true)
contentTypeId: String,
@ToolParam(description = BuildConfig.AREA_CODES_DESCRIPTION, required = true)
Expand All @@ -59,30 +61,37 @@ class TourToolExample(
@ToolParam(description = "검색 반경(m)", required = true)
radius: String = "100",
): String {
// areaAndSigunguCode를 areaCode와 sigunguCode로 분리
log.info(
"🔧 [TOOL CALLED] getLocationBasedTourInfo - " +
"contentTypeId: $contentTypeId, area: $areaAndSigunguCode, " +
"mapX: $mapX, mapY: $mapY, radius: $radius",
)

val tourParams = tourService.parseParams(contentTypeId, areaAndSigunguCode)
val locationBasedParams = TourLocationBasedParams(mapX, mapY, radius)

val tourLocationBasedInfo = tourService.fetchLocationBasedTours(tourParams, locationBasedParams)

log.info("✅ [TOOL RESULT] getLocationBasedTourInfo - 결과: ${tourLocationBasedInfo.toString().take(100)}...")
return tourLocationBasedInfo.toString() ?: "위치기반 관광정보 조회를 가져올 수 없습니다."
}

/**
* fetchTourDetail - 상세조회
* 케이스 : 콘텐츠ID가 126128인 관광정보의 상베 정보 조회
* 케이스 : 콘텐츠ID가 "126128"인 관광정보의 "상베 정보" 조회
* "contentid": "127974",
*/

@Tool(description = "detailCommon2 : 관광정보 상세조회, 특정 관광 정보의 상세 정보 조회")
fun get(
fun getTourDetailInfo(
@ToolParam(description = "Tour API Item에 각각 할당된 contentId", required = true)
contentId: String = "127974",
): String {
val tourDetailParams = TourDetailParams(contentId)
log.info("🔧 [TOOL CALLED] getTourDetailInfo - contentId: $contentId")

val tourDetailParams = TourDetailParams(contentId)
val tourDetailInfo = tourService.fetchTourDetail(tourDetailParams)

log.info("✅ [TOOL RESULT] getTourDetailInfo - 결과: ${tourDetailInfo.toString().take(100)}...")
return tourDetailInfo.toString() ?: "관광정보 상세조회를 가져올 수 없습니다."
}
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,60 @@
package com.back.koreaTravelGuide.domain.ai.aiChat.tool

import com.back.backend.BuildConfig
import com.back.koreaTravelGuide.common.logging.log
import com.back.koreaTravelGuide.domain.ai.weather.service.WeatherService
import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.ai.tool.annotation.Tool
import org.springframework.ai.tool.annotation.ToolParam
import org.springframework.stereotype.Component

@Component
class WeatherTool(
private val weatherService: WeatherService,
private val objectMapper: ObjectMapper,
) {
@Tool(description = "전국 중기예보를 조회합니다")
fun getWeatherForecast(): String {
log.info("🔧 [TOOL CALLED] getWeatherForecast")

val forecasts = weatherService.getWeatherForecast()
log.info("📦 [DATA] forecasts is null? ${forecasts == null}")
log.info("📦 [DATA] forecasts 타입: ${forecasts?.javaClass?.name}")
log.info("📦 [DATA] forecasts 내용: $forecasts")

return forecasts?.toString() ?: "중기예보 데이터를 가져올 수 없습니다."
return try {
val result = forecasts?.let { objectMapper.writeValueAsString(it) } ?: "중기예보 데이터를 가져올 수 없습니다."
log.info("✅ [TOOL RESULT] getWeatherForecast - 결과: $result")
result
} catch (e: Exception) {
log.error("❌ [TOOL ERROR] getWeatherForecast - 예외 발생: ${e.javaClass.name}", e)
log.error("❌ [TOOL ERROR] 예외 메시지: ${e.message}")
throw e
}
}

@Tool(description = "특정 지역의 상세 기온 및 날씨 예보를 조회합니다")
fun getRegionalWeatherDetails(
@ToolParam(description = BuildConfig.REGION_CODES_DESCRIPTION, required = true)
@ToolParam(
description = "지역 코드를 사용하세요. 사용 가능한 지역 코드: ${BuildConfig.REGION_CODES_DESCRIPTION}",
required = true,
)
location: String,
): String {
log.info("🔧 [TOOL CALLED] getRegionalWeatherDetails - location: $location")

val forecasts = weatherService.getTemperatureAndLandForecast(location)

return forecasts?.toString() ?: "$location 지역의 상세 날씨 정보를 가져올 수 없습니다."
return try {
val result = forecasts?.let { objectMapper.writeValueAsString(it) } ?: "$location 지역의 상세 날씨 정보를 가져올 수 없습니다."
log.info("✅ [TOOL RESULT] getRegionalWeatherDetails - 결과: $result")
result
} catch (e: Exception) {
log.error("❌ [TOOL ERROR] getRegionalWeatherDetails - 예외 발생: ${e.javaClass.name}", e)
log.error("❌ [TOOL ERROR] 예외 메시지: ${e.message}")
log.error("❌ [TOOL ERROR] forecasts 타입: ${forecasts?.javaClass?.name}")
log.error("❌ [TOOL ERROR] forecasts 내용: $forecasts")
throw e
}
}
}

This file was deleted.

45 changes: 41 additions & 4 deletions src/main/resources/prompts.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,46 @@
prompts:
system:
korea-travel-guide: |
당신은 한국 여행 전문 AI 가이드입니다.
한국의 관광지, 음식, 문화에 대해 정확하고 친근한 정보를 제공하세요.
답변은 한국어로 해주시고, 구체적인 추천과 팁을 포함해주세요.
사용자에게 도움이 되는 실용적인 여행 정보를 제공하는 것이 목표입니다.
당신은 한국 여행 전문 AI 챗봇입니다.
사용자에게 날씨 기반 여행지를 추천하고, 구체적인 관광 정보를 제공하는 것이 목표입니다.
답변은 친근하고 자연스러운 한국어로 작성하세요.

# 기본 추천 플로우 (사용자의 별도 요청이 없는 경우)

1단계: 전국 날씨 조회 및 안내
- 사용자가 여행지 추천을 요청하면 getWeatherForecast()를 사용해 전국 중기예보를 조회하세요.
- 각 지역별 날씨 정보를 요약해서 알려주세요. (예: "서울은 맑고, 부산은 구름 많음, 제주는 비 예상")
- 날씨가 좋은 지역을 강조하고, 사용자에게 "어느 지역 날씨를 더 자세히 알아볼까요?" 라고 물어보세요.

2단계: 지역 상세 날씨 조회
- 사용자가 특정 지역을 선택하거나 긍정의 대답을 하면 getRegionalWeatherDetails(location)를 사용하세요.
- location 파라미터는 사용자가 언급한 지역에 해당하는 지역 코드를 사용하세요.
- 상세 날씨(기온, 강수량, 풍속 등)를 알려주세요.

3단계: 관광 타입 선택
- 날씨 정보를 제공한 후, "이 지역에서 어떤 곳을 알아볼까요?" 라고 물어보세요.
- CONTENT_TYPE_CODES_DESCRIPTION에 정의된 타입들(관광지, 음식점, 숙박, 쇼핑 등)을 자연스럽게 제시하세요.
- 예: "관광지, 음식점, 숙박시설 중 어떤 정보가 필요하신가요?"

4단계: 지역 기반 관광정보 조회
- 사용자가 타입을 선택하면 getAreaBasedTourInfo(contentTypeId, areaAndSigunguCode)를 사용하세요.
- contentTypeId는 CONTENT_TYPE_CODES_DESCRIPTION에서 선택된 타입의 코드를 사용하세요.
- areaAndSigunguCode는 AREA_CODES_DESCRIPTION에서 사용자가 선택한 지역의 코드를 사용하세요.
- 조회된 관광정보를 사용자에게 친근하게 추천하세요.

5단계: 위치 기반 조회 (선택적)
- 사용자가 특정 위치나 주소를 언급하면 getLocationBasedTourInfo()를 사용할 수 있습니다.
- 경도/위도와 반경을 지정해서 주변 관광정보를 조회하세요.

# 독립적 Tool 사용
각 Tool은 사용자의 직접적인 요청이 있을 경우 독립적으로 사용할 수 있습니다.
- "서울 날씨 알려줘" → getRegionalWeatherDetails() 바로 사용
- "부산 관광지 추천해줘" → getAreaBasedTourInfo() 바로 사용
- "명동 근처 음식점 알려줘" → getLocationBasedTourInfo() 바로 사용

# 중요 원칙
- 사용자 경험을 자연스럽게 이어가세요. 로봇처럼 딱딱하게 말하지 마세요.
- Tool 호출 결과를 그대로 보여주지 말고, 핵심 정보를 요약해서 전달하세요.
- 다음 단계로 자연스럽게 유도하되, 사용자가 원하지 않으면 강요하지 마세요.
errors:
ai-fallback: "죄송합니다. 일시적인 문제로 응답을 생성할 수 없습니다. 다시 시도해 주세요."