Deploy: GitHub submodule sync 2026-07-23 12:46:52
This commit is contained in:
+16
@@ -0,0 +1,16 @@
|
||||
[submodule "chatbotApp-chatbot2.0-agent"]
|
||||
path = chatbotApp-chatbot2.0-agent
|
||||
url = https://github.com/KoreaExpressWayCorp/chatbotApp.git
|
||||
branch = chatbot2.0-agent
|
||||
[submodule "chatbotApi-chatbot2.0-agent"]
|
||||
path = chatbotApi-chatbot2.0-agent
|
||||
url = https://github.com/KoreaExpressWayCorp/chatbotApi.git
|
||||
branch = chatbot2.0-agent
|
||||
[submodule "exAiChatBot-chatbot2.0-agent"]
|
||||
path = exAiChatBot-chatbot2.0-agent
|
||||
url = https://github.com/KoreaExpressWayCorp/exAiChatBot.git
|
||||
branch = chatbot2.0-agent
|
||||
[submodule "kakaoChatbotSkill-chatbot2.0-agent"]
|
||||
path = kakaoChatbotSkill-chatbot2.0-agent
|
||||
url = https://github.com/KoreaExpressWayCorp/kakaoChatbotSkill.git
|
||||
branch = chatbot2.0-agent
|
||||
Submodule
+1
Submodule chatbotApi-chatbot2.0-agent added at 647e047bf0
@@ -1,148 +0,0 @@
|
||||
# ====================================
|
||||
# Spring Boot & Gradle
|
||||
# ====================================
|
||||
HELP.md
|
||||
.gradle
|
||||
build/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
# ====================================
|
||||
# IDE - Eclipse
|
||||
# ====================================
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
bin/
|
||||
!**/src/main/**/bin/
|
||||
!**/src/test/**/bin/
|
||||
|
||||
# ====================================
|
||||
# IDE - IntelliJ IDEA
|
||||
# ====================================
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
!**/src/main/**/out/
|
||||
!**/src/test/**/out/
|
||||
|
||||
# ====================================
|
||||
# IDE - NetBeans
|
||||
# ====================================
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
|
||||
# ====================================
|
||||
# IDE - VS Code & Cursor
|
||||
# ====================================
|
||||
.vscode/
|
||||
.cursor/
|
||||
|
||||
# ====================================
|
||||
# OS Files
|
||||
# ====================================
|
||||
# Windows
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
._*
|
||||
|
||||
# Linux
|
||||
*~
|
||||
.directory
|
||||
|
||||
# ====================================
|
||||
# Logs
|
||||
# ====================================
|
||||
*.log
|
||||
logs/
|
||||
*.log.*
|
||||
log/
|
||||
|
||||
# ====================================
|
||||
# Environment & Configuration
|
||||
# ====================================
|
||||
# 로컬 개발 환경 설정 (민감정보 포함)
|
||||
application-local.yml
|
||||
application-dev.yml
|
||||
application-prod.yml
|
||||
*-secret.yml
|
||||
|
||||
# 환경변수 파일
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# ====================================
|
||||
# Temporary Files
|
||||
# ====================================
|
||||
*.tmp
|
||||
*.temp
|
||||
*.swp
|
||||
*.swo
|
||||
*~.nib
|
||||
*.bak
|
||||
*.class
|
||||
|
||||
# ====================================
|
||||
# Gradle
|
||||
# ====================================
|
||||
.gradle/
|
||||
gradle-app.setting
|
||||
!gradle-wrapper.jar
|
||||
.gradletasknamecache
|
||||
|
||||
# ====================================
|
||||
# Package Files
|
||||
# ====================================
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# ====================================
|
||||
# Application Specific
|
||||
# ====================================
|
||||
# 업로드된 파일
|
||||
uploads/
|
||||
upload/
|
||||
|
||||
# 다운로드 파일
|
||||
downloads/
|
||||
download/
|
||||
|
||||
# 임시 데이터
|
||||
data/
|
||||
tmp/
|
||||
|
||||
# ====================================
|
||||
# Documentation (optional)
|
||||
# ====================================
|
||||
# 프로젝트 문서는 유지하되, 임시 문서는 제외
|
||||
**/temp-*.md
|
||||
**/draft-*.md
|
||||
|
||||
# ====================================
|
||||
# Agent Tools (Cursor specific)
|
||||
# ====================================
|
||||
.cursor/projects/*/agent-tools/
|
||||
@@ -1,283 +0,0 @@
|
||||
# RAG API 구현 완료 문서
|
||||
|
||||
## ✅ 수정 완료된 파일
|
||||
|
||||
### 1️⃣ **RagConfig.java** - Docker 스펙에 맞게 수정
|
||||
```java
|
||||
- host: localhost (Docker 컨테이너)
|
||||
- port: 28012 (Docker 외부 포트)
|
||||
- askEndpoint: /ask
|
||||
- healthEndpoint: /health (추가)
|
||||
```
|
||||
|
||||
### 2️⃣ **RagServerRequest.java** - API 스펙 적용
|
||||
```java
|
||||
{
|
||||
"query": "사용자 질문" // Docker API 필드명에 맞춤
|
||||
}
|
||||
```
|
||||
|
||||
### 3️⃣ **RagServerResponse.java** - 실제 응답 구조
|
||||
```java
|
||||
{
|
||||
"answer": "LLM 답변",
|
||||
"matched_questions": ["질문1", "질문2", "질문3"],
|
||||
"scores": [0.95, 0.89, 0.82],
|
||||
"num_references": 5
|
||||
}
|
||||
```
|
||||
|
||||
### 4️⃣ **RagService.java** - 변환 로직 추가
|
||||
- question → query 자동 변환
|
||||
- 헬스체크 기능 추가
|
||||
- 상세 로깅 추가
|
||||
|
||||
### 5️⃣ **RagApiController.java** - 헬스체크 엔드포인트 추가
|
||||
- POST /api/v1/rag-only (질문 처리)
|
||||
- GET /api/v1/rag-only/health (헬스체크)
|
||||
|
||||
### 6️⃣ **application.yml** - Docker 설정
|
||||
```yaml
|
||||
rag:
|
||||
server:
|
||||
host: localhost
|
||||
port: 28012
|
||||
ask-endpoint: /ask
|
||||
health-endpoint: /health
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 API 처리 흐름
|
||||
|
||||
```
|
||||
챗봇 웹
|
||||
↓ POST /api/v1/rag-only
|
||||
↓ {
|
||||
"question": "회원가입은 어떻게 하나요?",
|
||||
"botId": "bot-001"
|
||||
}
|
||||
↓
|
||||
[Spring Boot API - RagApiController]
|
||||
↓
|
||||
[RagService]
|
||||
- 입력 검증
|
||||
- question → query 변환
|
||||
↓ POST http://localhost:28012/ask
|
||||
↓ {
|
||||
"query": "회원가입은 어떻게 하나요?"
|
||||
}
|
||||
↓
|
||||
[Docker 컨테이너 - RAG 서버]
|
||||
- LLM 처리
|
||||
- 벡터 검색
|
||||
- 답변 생성
|
||||
↓ 응답
|
||||
↓ {
|
||||
"answer": "회원가입은 다음과 같이...",
|
||||
"matched_questions": ["회원가입 방법", "가입 절차", "회원 등록"],
|
||||
"scores": [0.95, 0.89, 0.82],
|
||||
"num_references": 5
|
||||
}
|
||||
↓
|
||||
[RagService]
|
||||
- 응답 가공
|
||||
↓
|
||||
[RagApiController]
|
||||
↓ 최종 응답
|
||||
↓ {
|
||||
"status": true,
|
||||
"statusMsg": "정상응답",
|
||||
"answer": "회원가입은 다음과 같이...",
|
||||
"matchedQuestions": ["회원가입 방법", "가입 절차", "회원 등록"],
|
||||
"scores": [0.95, 0.89, 0.82],
|
||||
"numReferences": 5
|
||||
}
|
||||
↓
|
||||
챗봇 웹
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 테스트 방법
|
||||
|
||||
### 1. Docker 컨테이너 직접 테스트
|
||||
|
||||
```bash
|
||||
# 질문 테스트
|
||||
curl -X POST http://localhost:28012/ask \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query":"회원가입은 어떻게 하나요?"}'
|
||||
|
||||
# 헬스체크
|
||||
curl http://localhost:28012/health
|
||||
```
|
||||
|
||||
**예상 응답:**
|
||||
```json
|
||||
{
|
||||
"answer": "회원가입은...",
|
||||
"matched_questions": ["회원가입 방법", "가입 절차"],
|
||||
"scores": [0.95, 0.89],
|
||||
"num_references": 5
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Spring Boot API 테스트
|
||||
|
||||
```bash
|
||||
# 질문 API
|
||||
curl -X POST http://localhost:8086/api/v1/rag-only \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"question": "회원가입은 어떻게 하나요?",
|
||||
"botId": "bot-001"
|
||||
}'
|
||||
|
||||
# 헬스체크 API
|
||||
curl http://localhost:8086/api/v1/rag-only/health
|
||||
```
|
||||
|
||||
**예상 응답 (성공):**
|
||||
```json
|
||||
{
|
||||
"status": true,
|
||||
"statusMsg": "정상응답",
|
||||
"answer": "회원가입은 다음과 같이 진행됩니다...",
|
||||
"matchedQuestions": ["회원가입 방법", "가입 절차", "회원 등록"],
|
||||
"scores": [0.95, 0.89, 0.82],
|
||||
"numReferences": 5
|
||||
}
|
||||
```
|
||||
|
||||
**예상 응답 (실패):**
|
||||
```json
|
||||
{
|
||||
"status": false,
|
||||
"statusMsg": "RAG 서버 통신 실패"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Postman으로 테스트
|
||||
|
||||
**질문 API:**
|
||||
- Method: POST
|
||||
- URL: `http://localhost:8086/api/v1/rag-only`
|
||||
- Headers: `Content-Type: application/json`
|
||||
- Body (raw JSON):
|
||||
```json
|
||||
{
|
||||
"question": "배송은 얼마나 걸리나요?",
|
||||
"botId": "bot-001"
|
||||
}
|
||||
```
|
||||
|
||||
**헬스체크 API:**
|
||||
- Method: GET
|
||||
- URL: `http://localhost:8086/api/v1/rag-only/health`
|
||||
|
||||
---
|
||||
|
||||
## 📋 환경별 설정
|
||||
|
||||
### 로컬 개발 (application-local.yml)
|
||||
```yaml
|
||||
rag:
|
||||
server:
|
||||
host: localhost
|
||||
port: 28012
|
||||
```
|
||||
|
||||
### 개발 서버 (application-dev.yml)
|
||||
```yaml
|
||||
rag:
|
||||
server:
|
||||
host: 172.16.163.62 # 개발 서버 IP
|
||||
port: 28012
|
||||
```
|
||||
|
||||
### 운영 서버 (application-prod.yml)
|
||||
```yaml
|
||||
rag:
|
||||
server:
|
||||
host: ${RAG_SERVER_HOST:localhost}
|
||||
port: ${RAG_SERVER_PORT:28012}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 로그 확인
|
||||
|
||||
### 질문 처리 시
|
||||
```
|
||||
INFO - RAG 질문 처리 시작 - 질문: 회원가입은 어떻게 하나요?, 봇ID: bot-001
|
||||
INFO - RAG 서버 요청 시작 - URL: http://localhost:28012/ask, 질문: 회원가입은 어떻게 하나요?
|
||||
INFO - RAG 서버 응답 성공 - Status: 200 OK
|
||||
INFO - RAG 질문 처리 완료 - 참조: 5개, 유사 질문: 3개
|
||||
```
|
||||
|
||||
### 헬스체크 시
|
||||
```
|
||||
INFO - RAG 서버 헬스체크 시작
|
||||
INFO - 헬스체크 URL: http://localhost:28012/health
|
||||
INFO - RAG 서버 상태: ok, 벡터 개수: 1000
|
||||
```
|
||||
|
||||
### 에러 시
|
||||
```
|
||||
ERROR - RAG 서버 통신 실패 - Connection refused
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 주의사항
|
||||
|
||||
1. **Docker 컨테이너 실행 확인**
|
||||
```bash
|
||||
docker ps | grep 28012
|
||||
```
|
||||
|
||||
2. **포트 접근 가능 확인**
|
||||
```bash
|
||||
telnet localhost 28012
|
||||
# 또는
|
||||
nc -zv localhost 28012
|
||||
```
|
||||
|
||||
3. **방화벽 설정 확인**
|
||||
- 28012 포트가 열려있는지 확인
|
||||
- 개발 서버에서는 172.16.163.62:28012 접근 가능한지 확인
|
||||
|
||||
4. **botId는 선택사항**
|
||||
- 현재 Docker API에는 전달되지 않음
|
||||
- 로깅/추적 목적으로만 사용
|
||||
|
||||
---
|
||||
|
||||
## 📌 다음 단계 (선택사항)
|
||||
|
||||
1. **응답 캐싱**
|
||||
- 동일한 질문에 대한 캐시 처리
|
||||
- Redis 또는 로컬 캐시 활용
|
||||
|
||||
2. **재시도 로직**
|
||||
- RAG 서버 통신 실패 시 재시도
|
||||
- Exponential Backoff 적용
|
||||
|
||||
3. **모니터링**
|
||||
- 응답 시간 측정
|
||||
- 에러율 추적
|
||||
- Prometheus/Grafana 연동
|
||||
|
||||
4. **보안 강화**
|
||||
- API 키 인증
|
||||
- Rate Limiting
|
||||
|
||||
---
|
||||
|
||||
## 🎯 완료!
|
||||
|
||||
모든 파일이 Docker RAG 서버 API 스펙에 맞춰 수정되었습니다.
|
||||
|
||||
테스트 후 문제가 있으면 알려주세요! 😊
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
# chatbotApi 설명서
|
||||
|
||||
> 해당 문서는 md(Markdown) 방식으로 작성되었기 때문에 마크다운 뷰어 기능이있는 편집기(vscode 등)에서 보는것을 추천드립니다.(vscode 사용 시 문서 열고 Ctrl + Shift + V)
|
||||
|
||||
|
||||
## 1. 개요
|
||||
챗봇에서 데이터 조회를 위한 api 서비스(backend, was). intent 정책(SSOT `intent-definitions.yml`)·검증·정규화와 도메인 데이터 조회(Oracle Handler / WEB 위임)를 담당합니다.
|
||||
|
||||
### 1.1 시스템 구성 흐름 (WAS 역할)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
RAG[exAiChatBot AgentService] --> TE[AgentToolController /api/v1/tools/*]
|
||||
|
||||
POL[intent-definitions.yml + validator/normalizer]
|
||||
TE --> POL
|
||||
POL --> OWN{fetchOwner}
|
||||
OWN -->|WAS| ORA[IntentHandler → Oracle/TROAD]
|
||||
OWN -->|WEB| SKI[KakaoSkillToolClient → Skill /internal/tools/data-portal]
|
||||
ORA --> FMT[LlmDomainDataFormatter]
|
||||
SKI --> FMT
|
||||
FMT --> OUT([domainData + uiType])
|
||||
```
|
||||
|
||||
| 진입 | 사용 모드 | 설명 |
|
||||
|------|-----------|------|
|
||||
| `GET /api/v1/tools/definitions` | Agent | YAML → OpenAI function schema (RAG tool 목록) |
|
||||
| `POST /api/v1/tools/execute` | Agent | domain tool 실행 (`fetchOwner` 분기: WAS Oracle / WEB Skill 역호출) |
|
||||
|
||||
- **정책 SSOT:** `src/main/resources/intent-definitions.yml` (`fetchOwner`/`uiType`/`requiredParams`/`clarification`).
|
||||
- **핵심 원칙:** LLM은 intent·params만 결정, `fetchOwner`/`uiType`/`needsClarification`은 WAS+YAML+validator가 결정.
|
||||
- 전체 구조: [`../SYSTEM_ARCHITECTURE.md`](../SYSTEM_ARCHITECTURE.md) · Agent tool API 상세: [`../docs/AGENT_ARCHITECTURE.md`](../docs/AGENT_ARCHITECTURE.md) · RAG API: [`RAG_API_IMPLEMENTATION.md`](RAG_API_IMPLEMENTATION.md)
|
||||
|
||||
|
||||
## 2. 프로그램 관련 정보
|
||||
서비스 기동 서버 : 172.16.180.130(통합정산개발#1)
|
||||
* Java version : 8(JDK 1.8)
|
||||
* 서비스 기동 시점 : 상시
|
||||
* 연계DB : TROAD(교통정보, Oracle), chatbot(MongoDB)
|
||||
* 사용 계정 : song
|
||||
* 서비스 홈 디렉토리 : /home/song/bin/chatbotApi
|
||||
* 서비스 파일 : chatbotApi-0.0.1-SNAPSHOT.jar
|
||||
* 서비스 설정 파일(config) : application.yml, logback-spring.xml
|
||||
* 서비스 로그 파일(logs) : chatbotApi.log
|
||||
* 서비스 시작 명령어(서비스 홈 디렉토리에서 수행해야한다.)
|
||||
|
||||
``` bash
|
||||
./start.sh start ## 백그라운드(실운영) 기동 명령어, 평시에는 해당 명령어를 사용한다.
|
||||
./start.sh starta ## 포그라운드 기동 명령어, 에러 탐색 및 모니터링시 활용
|
||||
```
|
||||
|
||||
## 3. 기타 주의 사항
|
||||
> 해당 서비스에는 교통정보 관련 API만 개발 되어있다. 향후에는 미납 조회 등 모든 서비스를 구현하면 좋을듯. (현재는 DMZ존에 위치한 챗봇 WEB서버(128.200.100.61)에 대부분의 조회 서비스가 운영되고있다. 별로 좋지않은 구조라 생각된다.)
|
||||
|
||||
> [ASIS] WEB ↔ DB, [TOBE] WEB ↔ WAS ↔ DB)
|
||||
|
||||
## ※ API 일별 사용 건수 조회 Mongo쿼리
|
||||
```javascript
|
||||
db.api_use_log.aggregate([
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
$substr: ["$request_time", 0, 10] // 'yyyy-MM-dd' 부분만 추출
|
||||
},
|
||||
count: { $sum: 1 } // 각 날짜별로 문서 수를 계산
|
||||
}
|
||||
},
|
||||
{
|
||||
$sort: { "_id": 1 } // 날짜별로 정렬
|
||||
}
|
||||
]);
|
||||
```
|
||||
|
||||
## ※ API apikey 및 일별 사용 건수 조회 Mongo쿼리
|
||||
```javascript
|
||||
db.api_use_log.aggregate([
|
||||
{
|
||||
$addFields: {
|
||||
// 'yyyy-MM-dd' 형식으로 날짜 부분만 추출하여 새 필드에 저장
|
||||
"dateOnly": {
|
||||
$substr: ["$request_time", 0, 10] // '2023-11-11' 부분만 추출
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
// api_key와 추출된 날짜를 기준으로 그룹화
|
||||
_id: {
|
||||
api_key: "$api_key",
|
||||
date: "$dateOnly"
|
||||
},
|
||||
count: { $sum: 1 } // 각 api_key와 날짜별로 문서 수를 계산
|
||||
}
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
"_id.date": 1, // 날짜별로 정렬
|
||||
"_id.api_key": 1 // api_key별로 정렬
|
||||
}
|
||||
}
|
||||
]);
|
||||
```
|
||||
@@ -1,48 +0,0 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '2.7.14'
|
||||
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
|
||||
}
|
||||
|
||||
group = 'com.ex.'
|
||||
version = '0.0.1-SNAPSHOT'
|
||||
|
||||
java {
|
||||
// Java 17로 빌드하되, Java 8 바이트코드로 컴파일 (Java 8 환경에서 실행 가능)
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(17)
|
||||
}
|
||||
sourceCompatibility = '1.8'
|
||||
targetCompatibility = '1.8'
|
||||
}
|
||||
|
||||
configurations {
|
||||
compileOnly {
|
||||
extendsFrom annotationProcessor
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
|
||||
implementation 'org.apache.httpcomponents:httpclient:4.5.13'
|
||||
implementation 'com.google.code.gson:gson:2.8.9'
|
||||
implementation 'commons-io:commons-io:2.8.0'
|
||||
compileOnly 'org.projectlombok:lombok:1.18.30'
|
||||
runtimeOnly 'com.oracle.database.jdbc:ojdbc8'
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.30'
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
}
|
||||
|
||||
tasks.named('compileJava') {
|
||||
options.release = 8
|
||||
}
|
||||
|
||||
tasks.named('test') {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
# chatbotApi (WAS 172.16.180.130) — 기동 전 export
|
||||
|
||||
INTERNAL_TOOL_API_KEY=
|
||||
|
||||
# kakaoChatbotSkill 웹 서버 — 실제 IP/호스트로 수정 (기본: 레거시 DMZ 128.200.100.61)
|
||||
SKILL_INTERNAL_BASE_URL=http://128.200.100.61:8083
|
||||
|
||||
# RAG (동일 WAS 서버)
|
||||
RAG_SERVER_HOST=127.0.0.1
|
||||
RAG_SERVER_PORT=28012
|
||||
|
||||
# MongoDB (동일 서버 로컬)
|
||||
# MONGODB_URI=mongodb://exDev:ehfhrhdtk!!@127.0.0.1:27017/chatbot?authSource=chatbot
|
||||
|
||||
TOOL_EXECUTE_TIMEOUT_MS=30000
|
||||
@@ -1,7 +0,0 @@
|
||||
# Gradle이 사용할 Java 버전 설정
|
||||
# Gradle 데몬은 Java 17 이하를 사용하도록 제한
|
||||
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
|
||||
|
||||
# Java Toolchain 자동 다운로드 활성화
|
||||
org.gradle.java.installations.auto-download=true
|
||||
|
||||
Binary file not shown.
@@ -1,7 +0,0 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
Vendored
-248
@@ -1,248 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
-92
@@ -1,92 +0,0 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -1,2 +0,0 @@
|
||||
config.stopBubbling = true
|
||||
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier
|
||||
@@ -1 +0,0 @@
|
||||
rootProject.name = 'chatbotApi'
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
package com.ex.chatbotapi;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class ChatbotapiApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(ChatbotapiApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
package com.ex.chatbotapi.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "agent")
|
||||
public class AgentProperties {
|
||||
|
||||
private Tool tool = new Tool();
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class Tool {
|
||||
private String internalApiKey = "";
|
||||
private int executeTimeoutMs = 30_000;
|
||||
}
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
package com.ex.chatbotapi.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "kakao-skill")
|
||||
public class KakaoSkillConfig {
|
||||
private String baseUrl = "http://localhost:8083";
|
||||
private String dataPortalToolEndpoint = "/internal/tools/data-portal";
|
||||
private String internalApiKey = "";
|
||||
private int connectTimeout = 5000;
|
||||
private int readTimeout = 30000;
|
||||
|
||||
public String getDataPortalToolUrl() {
|
||||
return baseUrl.replaceAll("/$", "") + dataPortalToolEndpoint;
|
||||
}
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
package com.ex.chatbotapi.config;
|
||||
|
||||
import com.mongodb.ConnectionString;
|
||||
import com.mongodb.MongoClientSettings;
|
||||
import com.mongodb.client.MongoClient;
|
||||
import com.mongodb.client.MongoClients;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
|
||||
|
||||
@Configuration
|
||||
@EnableMongoRepositories(basePackages = "com.ex.chatbotapi.domain")
|
||||
@ConfigurationProperties(prefix = "spring.data.mongodb")
|
||||
public class MongoDbSettings {
|
||||
|
||||
@Value("${spring.data.mongodb.uri}")
|
||||
private String URI;
|
||||
|
||||
@Bean
|
||||
public MongoClient mongoClient() {
|
||||
ConnectionString connectionString = new ConnectionString(URI);
|
||||
MongoClientSettings mongoClientSettings = MongoClientSettings.builder()
|
||||
.applyConnectionString(connectionString)
|
||||
.build();
|
||||
return MongoClients.create(mongoClientSettings);
|
||||
}
|
||||
}
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
package com.ex.chatbotapi.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import javax.sql.DataSource;
|
||||
|
||||
@Configuration
|
||||
public class OracleDbConfig {
|
||||
@Bean
|
||||
@ConfigurationProperties("spring.datasource.bpstl")
|
||||
public DataSourceProperties bpstlDataSourceProperties() {
|
||||
return new DataSourceProperties();
|
||||
}
|
||||
@Bean
|
||||
@ConfigurationProperties("spring.datasource.troad")
|
||||
public DataSourceProperties troadDataSourceProperties() {
|
||||
return new DataSourceProperties();
|
||||
}
|
||||
@Bean
|
||||
@ConfigurationProperties("spring.datasource.restarea")
|
||||
public DataSourceProperties restAreaDataSourceProperties() {
|
||||
return new DataSourceProperties();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
@Qualifier("bpstlDataSource")
|
||||
public DataSource bpstlDataSource(){
|
||||
return bpstlDataSourceProperties()
|
||||
.initializeDataSourceBuilder()
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Qualifier("troadDataSource")
|
||||
public DataSource troadDataSource(){
|
||||
return troadDataSourceProperties()
|
||||
.initializeDataSourceBuilder()
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Qualifier("restAreaDataSource")
|
||||
public DataSource restAreaDataSource(){
|
||||
return restAreaDataSourceProperties()
|
||||
.initializeDataSourceBuilder()
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean(name = "bpstlTemplate")
|
||||
public JdbcTemplate bpstlJdbcTemplate(@Qualifier("bpstlDataSource") DataSource dataSource) {
|
||||
return new JdbcTemplate(dataSource);
|
||||
}
|
||||
|
||||
@Bean(name = "troadTemplate")
|
||||
public JdbcTemplate troadJdbcTemplate(@Qualifier("troadDataSource") DataSource dataSource) {
|
||||
return new JdbcTemplate(dataSource);
|
||||
}
|
||||
|
||||
@Bean(name = "restAreaTemplate")
|
||||
public JdbcTemplate restAreaJdbcTemplate(@Qualifier("restAreaDataSource") DataSource dataSource) {
|
||||
return new JdbcTemplate(dataSource);
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package com.ex.chatbotapi.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* RAG 서버 관련 설정 (Docker 컨테이너)
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "rag.server")
|
||||
public class RagConfig {
|
||||
/**
|
||||
* RAG 서버 호스트 (Docker 컨테이너는 localhost)
|
||||
*/
|
||||
private String host = "localhost";
|
||||
|
||||
/**
|
||||
* RAG 서버 포트 (Docker 외부 포트)
|
||||
*/
|
||||
private int port = 28012;
|
||||
|
||||
/**
|
||||
* RAG 서버 기본 URL
|
||||
*/
|
||||
private String baseUrl;
|
||||
|
||||
/**
|
||||
* 질의 엔드포인트
|
||||
*/
|
||||
private String askEndpoint = "/ask";
|
||||
|
||||
/**
|
||||
* 헬스체크 엔드포인트
|
||||
*/
|
||||
private String healthEndpoint = "/health";
|
||||
|
||||
/**
|
||||
* LLM 기반 intent 분석 엔드포인트
|
||||
*/
|
||||
private String intentAnalyzeEndpoint = "/intent/analyze";
|
||||
|
||||
/**
|
||||
* 연결 타임아웃 (ms)
|
||||
*/
|
||||
private int connectTimeout = 5000;
|
||||
|
||||
/**
|
||||
* 읽기 타임아웃 (ms)
|
||||
*/
|
||||
private int readTimeout = 30000;
|
||||
|
||||
/**
|
||||
* RAG 서버 질의 URL 반환
|
||||
*/
|
||||
public String getFullUrl() {
|
||||
if (baseUrl != null && !baseUrl.isEmpty()) {
|
||||
return baseUrl + askEndpoint;
|
||||
}
|
||||
return String.format("http://%s:%d%s", host, port, askEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* RAG 서버 헬스체크 URL 반환
|
||||
*/
|
||||
public String getHealthUrl() {
|
||||
if (baseUrl != null && !baseUrl.isEmpty()) {
|
||||
return baseUrl + healthEndpoint;
|
||||
}
|
||||
return String.format("http://%s:%d%s", host, port, healthEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* RAG 서버 intent 분석 URL 반환
|
||||
*/
|
||||
public String getIntentAnalyzeUrl() {
|
||||
if (baseUrl != null && !baseUrl.isEmpty()) {
|
||||
return baseUrl + intentAnalyzeEndpoint;
|
||||
}
|
||||
return String.format("http://%s:%d%s", host, port, intentAnalyzeEndpoint);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
package com.ex.chatbotapi.config;
|
||||
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@Configuration
|
||||
public class RestfulConfig {
|
||||
@Value("${restTemplate.factory.readTimeout}")
|
||||
private int READ_TIMEOUT;
|
||||
|
||||
@Value("${restTemplate.factory.connectTimeout}")
|
||||
private int CONNECT_TIMEOUT;
|
||||
|
||||
@Value("${restTemplate.httpClient.maxConnTotal}")
|
||||
private int MAX_CONN_TOTAL;
|
||||
|
||||
@Value("${restTemplate.httpClient.maxConnPerRoute}")
|
||||
private int MAX_CONN_PER_ROUTE;
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public RestTemplate restTemplate() {
|
||||
|
||||
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
|
||||
factory.setReadTimeout(READ_TIMEOUT);
|
||||
factory.setConnectTimeout(CONNECT_TIMEOUT);
|
||||
|
||||
HttpClient httpClient = HttpClientBuilder.create()
|
||||
.setMaxConnTotal(MAX_CONN_TOTAL)
|
||||
.setMaxConnPerRoute(MAX_CONN_PER_ROUTE)
|
||||
.build();
|
||||
|
||||
factory.setHttpClient(httpClient);
|
||||
RestTemplate restTemplate = new RestTemplate(factory);
|
||||
|
||||
return restTemplate;
|
||||
}
|
||||
}
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.api;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.dto.AgentQueryRequest;
|
||||
import com.ex.chatbotapi.domain.agent.dto.AgentQueryResponse;
|
||||
import com.ex.chatbotapi.domain.agent.service.AgentOrchestratorService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 에이전트 통합 API 컨트롤러
|
||||
*
|
||||
* KakaoIntentApp 호출 규격:
|
||||
* POST /api/v1/agent
|
||||
* {
|
||||
* "question": "판교에서 신갈까지 요금이 얼마야?",
|
||||
* "intentType": "FARE_SEARCH",
|
||||
* "params": { "fromIc": "판교", "toIc": "신갈" },
|
||||
* "botId": "user_id"
|
||||
* }
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/agent")
|
||||
@RequiredArgsConstructor
|
||||
public class AgentApiController {
|
||||
|
||||
private final AgentOrchestratorService agentOrchestratorService;
|
||||
|
||||
/**
|
||||
* 통합 질의 엔드포인트 (KakaoIntentApp → chatbotApi 메인 진입점)
|
||||
*/
|
||||
@PostMapping
|
||||
public ResponseEntity<AgentQueryResponse> processQuery(@RequestBody AgentQueryRequest request) {
|
||||
log.info("Agent API 호출 - question: {}, intentType: {}",
|
||||
request.getQuestion(), request.getIntentType());
|
||||
return ResponseEntity.ok(agentOrchestratorService.processUserQuery(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 테스트용 GET 엔드포인트
|
||||
*/
|
||||
@GetMapping
|
||||
public ResponseEntity<AgentQueryResponse> processQueryGet(
|
||||
@RequestParam String question,
|
||||
@RequestParam(required = false) String intentType,
|
||||
@RequestParam(required = false) String carNo,
|
||||
@RequestParam(required = false) String fromIc,
|
||||
@RequestParam(required = false) String toIc,
|
||||
@RequestParam(required = false) String icName,
|
||||
@RequestParam(required = false) String restAreaName,
|
||||
@RequestParam(required = false) String botId) {
|
||||
|
||||
log.info("Agent API 호출 (GET) - question: {}, intentType: {}", question, intentType);
|
||||
|
||||
Map<String, String> params = new HashMap<>();
|
||||
if (carNo != null) params.put("carNo", carNo);
|
||||
if (fromIc != null) params.put("fromIc", fromIc);
|
||||
if (toIc != null) params.put("toIc", toIc);
|
||||
if (icName != null) params.put("icName", icName);
|
||||
if (restAreaName != null) params.put("restAreaName", restAreaName);
|
||||
|
||||
AgentQueryRequest request = new AgentQueryRequest(question, intentType, params, botId, null);
|
||||
return ResponseEntity.ok(agentOrchestratorService.processUserQuery(request));
|
||||
}
|
||||
}
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.api;
|
||||
|
||||
import com.ex.chatbotapi.config.AgentProperties;
|
||||
import com.ex.chatbotapi.domain.agent.dto.ToolExecuteRequest;
|
||||
import com.ex.chatbotapi.domain.agent.dto.ToolExecuteResponse;
|
||||
import com.ex.chatbotapi.domain.agent.service.ToolDefinitionService;
|
||||
import com.ex.chatbotapi.domain.agent.service.ToolExecutionService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/tools")
|
||||
@RequiredArgsConstructor
|
||||
public class AgentToolController {
|
||||
private final ToolDefinitionService toolDefinitionService;
|
||||
private final ToolExecutionService toolExecutionService;
|
||||
private final AgentProperties agentProperties;
|
||||
|
||||
@GetMapping("/definitions")
|
||||
public ResponseEntity<?> definitions(
|
||||
@RequestHeader(value = "X-Internal-Tool-Key", required = false) String apiKey
|
||||
) {
|
||||
ResponseEntity<?> authError = authorize(apiKey);
|
||||
if (authError != null) {
|
||||
return authError;
|
||||
}
|
||||
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("tools", toolDefinitionService.listToolDefinitions());
|
||||
return ResponseEntity.ok(body);
|
||||
}
|
||||
|
||||
@PostMapping("/execute")
|
||||
public ResponseEntity<?> execute(
|
||||
@RequestHeader(value = "X-Internal-Tool-Key", required = false) String apiKey,
|
||||
@RequestBody ToolExecuteRequest request
|
||||
) {
|
||||
ResponseEntity<?> authError = authorize(apiKey);
|
||||
if (authError != null) {
|
||||
return authError;
|
||||
}
|
||||
|
||||
log.info("Tool execute - tool: {}, botId: {}", request.getToolName(), request.getBotId());
|
||||
return ResponseEntity.ok(toolExecutionService.execute(request));
|
||||
}
|
||||
|
||||
private ResponseEntity<?> authorize(String apiKey) {
|
||||
String expected = agentProperties.getTool().getInternalApiKey();
|
||||
if (expected == null || expected.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (expected.equals(apiKey)) {
|
||||
return null;
|
||||
}
|
||||
Map<String, Object> errorBody = new LinkedHashMap<>();
|
||||
errorBody.put("status", false);
|
||||
errorBody.put("statusMsg", "invalid or missing X-Internal-Tool-Key");
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorBody);
|
||||
}
|
||||
}
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.api;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.dto.DomainOnlyRequest;
|
||||
import com.ex.chatbotapi.domain.agent.dto.DomainOnlyResponse;
|
||||
import com.ex.chatbotapi.domain.agent.service.DomainOnlyService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 도메인 데이터 전용 API 컨트롤러 (LLM/RAG 호출 없음)
|
||||
*
|
||||
* LLM 응답이 불필요한 인텐트(예: 전화번호 조회)에서 사용.
|
||||
* DB 도메인 데이터만 조회하여 반환하므로 응답이 빠르고 비용이 없음.
|
||||
*
|
||||
* KakaoIntentApp 호출 규격:
|
||||
* POST /api/v1/domain-only
|
||||
* {
|
||||
* "intentType": "IC_TEL",
|
||||
* "params": { "icName": "서울" }
|
||||
* }
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/domain-only")
|
||||
@RequiredArgsConstructor
|
||||
public class DomainOnlyApiController {
|
||||
|
||||
private final DomainOnlyService domainOnlyService;
|
||||
|
||||
/**
|
||||
* 도메인 데이터 전용 조회 엔드포인트
|
||||
*/
|
||||
@PostMapping
|
||||
public ResponseEntity<DomainOnlyResponse> fetchDomainData(@RequestBody DomainOnlyRequest request) {
|
||||
log.info("DomainOnly API 호출 - intentType: {}", request.getIntentType());
|
||||
return ResponseEntity.ok(domainOnlyService.fetchDomainData(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 테스트용 GET 엔드포인트
|
||||
*/
|
||||
@GetMapping
|
||||
public ResponseEntity<DomainOnlyResponse> fetchDomainDataGet(
|
||||
@RequestParam String intentType,
|
||||
@RequestParam(required = false) String icName,
|
||||
@RequestParam(required = false) String carNo,
|
||||
@RequestParam(required = false) String fromIc,
|
||||
@RequestParam(required = false) String toIc,
|
||||
@RequestParam(required = false) String restAreaName) {
|
||||
|
||||
log.info("DomainOnly API 호출 (GET) - intentType: {}", intentType);
|
||||
|
||||
Map<String, String> params = new HashMap<>();
|
||||
if (icName != null) params.put("icName", icName);
|
||||
if (carNo != null) params.put("carNo", carNo);
|
||||
if (fromIc != null) params.put("fromIc", fromIc);
|
||||
if (toIc != null) params.put("toIc", toIc);
|
||||
if (restAreaName != null) params.put("restAreaName", restAreaName);
|
||||
|
||||
DomainOnlyRequest request = new DomainOnlyRequest(intentType, params);
|
||||
return ResponseEntity.ok(domainOnlyService.fetchDomainData(request));
|
||||
}
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.api;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.dto.LlmAgentRequest;
|
||||
import com.ex.chatbotapi.domain.agent.dto.LlmAgentResponse;
|
||||
import com.ex.chatbotapi.domain.agent.service.LlmAgentRouterService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/llm-agent")
|
||||
@RequiredArgsConstructor
|
||||
public class LlmAgentController {
|
||||
private final LlmAgentRouterService llmAgentRouterService;
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<LlmAgentResponse> process(@RequestBody LlmAgentRequest request) {
|
||||
log.info("LLM Agent API 호출 - question: {}, botId: {}", request.getQuestion(), request.getBotId());
|
||||
return ResponseEntity.ok(llmAgentRouterService.process(request));
|
||||
}
|
||||
}
|
||||
-65
@@ -1,65 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.client;
|
||||
|
||||
import com.ex.chatbotapi.config.KakaoSkillConfig;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class KakaoSkillToolClient {
|
||||
private final RestTemplate restTemplate;
|
||||
private final KakaoSkillConfig kakaoSkillConfig;
|
||||
|
||||
public Map<String, Object> fetchDataPortal(
|
||||
String intentType,
|
||||
Map<String, String> params,
|
||||
String userInput
|
||||
) {
|
||||
String url = kakaoSkillConfig.getDataPortalToolUrl();
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("intentType", intentType);
|
||||
body.put("params", params);
|
||||
body.put("userInput", userInput);
|
||||
|
||||
try {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
if (kakaoSkillConfig.getInternalApiKey() != null
|
||||
&& !kakaoSkillConfig.getInternalApiKey().trim().isEmpty()) {
|
||||
headers.set("X-Internal-Tool-Key", kakaoSkillConfig.getInternalApiKey());
|
||||
}
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);
|
||||
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
|
||||
url,
|
||||
HttpMethod.POST,
|
||||
entity,
|
||||
new ParameterizedTypeReference<Map<String, Object>>() {}
|
||||
);
|
||||
Map<String, Object> result = response.getBody();
|
||||
return result != null ? result : error("empty response");
|
||||
} catch (RestClientException e) {
|
||||
log.error("kakao data portal tool failed - intentType: {}, error: {}", intentType, e.getMessage(), e);
|
||||
return error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> error(String message) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("status", false);
|
||||
result.put("statusMsg", message);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* KakaoIntentApp → chatbotApi 통합 엔드포인트 요청 DTO
|
||||
*
|
||||
* KakaoIntentApp은 @KakaoIntent 라우팅에서 이미 의도를 알고 있으므로
|
||||
* intentType과 슬롯 파라미터를 직접 담아서 보냄
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AgentQueryRequest {
|
||||
|
||||
/** 사용자 원본 질문 */
|
||||
private String question;
|
||||
|
||||
/**
|
||||
* 의도 타입 (KakaoIntentApp의 @KakaoIntent 블록명 기반)
|
||||
* 예: "FARE_SEARCH", "REST_AREA_FOOD_LIST", "IC_TEL"
|
||||
* null 또는 등록된 핸들러 없음 → RAG 단독 처리
|
||||
*/
|
||||
private String intentType;
|
||||
|
||||
/**
|
||||
* 슬롯 파라미터 (카카오 슬롯 필링 결과)
|
||||
* 예: { "fromIc": "판교", "toIc": "신갈" }
|
||||
* { "restAreaName": "화성" }
|
||||
* { "carNo": "12가3456" }
|
||||
*/
|
||||
private Map<String, String> params;
|
||||
|
||||
/** 봇 ID (대화 이력 관리용) */
|
||||
private String botId;
|
||||
|
||||
/**
|
||||
* 클라이언트(skill)가 직접 수집한 도메인 데이터.
|
||||
* 값이 있으면 chatbotApi의 IntentHandler 호출을 생략하고 formatter + RAG만 실행한다.
|
||||
* (WAS에서 접근 불가한 외부 API를 Web 계층에서 호출하는 경우 사용)
|
||||
*/
|
||||
private Map<String, Object> domainData;
|
||||
}
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* chatbotApi → KakaoIntentApp 통합 엔드포인트 응답 DTO
|
||||
*
|
||||
* KakaoIntentApp은 두 가지를 활용:
|
||||
* - llmAnswer: SimpleText로 표시 (LLM 자연어 응답)
|
||||
* - domainData: 기존 ListCard/BasicCard 포맷 구성에 활용 (구조화 DB 데이터)
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AgentQueryResponse {
|
||||
|
||||
/** 처리 성공 여부 */
|
||||
private boolean status;
|
||||
|
||||
/** 처리된 의도 타입 */
|
||||
private String intentType;
|
||||
|
||||
/** LLM이 생성한 자연어 응답 (SimpleText 표시용) */
|
||||
private String llmAnswer;
|
||||
|
||||
/**
|
||||
* DB 조회 구조화 데이터 (카드 포맷 구성용)
|
||||
* intentType이 없거나 RAG 단독 처리 시 null
|
||||
*/
|
||||
private Map<String, Object> domainData;
|
||||
|
||||
/** 공식 FAQ 상세 URL 목록 */
|
||||
private List<String> faqUrls;
|
||||
|
||||
/** RAG 참고자료 메타데이터 */
|
||||
private List<Map<String, Object>> references;
|
||||
|
||||
/** 상태 메시지 (에러 시 오류 내용) */
|
||||
private String statusMsg;
|
||||
}
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CarNoParamNormalizationResult {
|
||||
private boolean valid;
|
||||
private String clarificationQuestion;
|
||||
|
||||
@Builder.Default
|
||||
private Map<String, String> normalizedParams = new HashMap<>();
|
||||
}
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* KakaoIntentApp → chatbotApi 도메인 데이터 전용 엔드포인트 요청 DTO
|
||||
*
|
||||
* LLM/RAG 호출 없이 DB 도메인 데이터 조회만 수행.
|
||||
* 예: getTelIcInfo (전화번호 조회) 등 LLM 응답이 불필요한 경우 사용.
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DomainOnlyRequest {
|
||||
|
||||
/**
|
||||
* 의도 타입 (등록된 IntentHandler와 매핑)
|
||||
* 예: "IC_TEL"
|
||||
*/
|
||||
private String intentType;
|
||||
|
||||
/**
|
||||
* 슬롯 파라미터 (카카오 슬롯 필링 결과)
|
||||
* 예: { "icName": "서울" }
|
||||
*/
|
||||
private Map<String, String> params;
|
||||
}
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* chatbotApi → KakaoIntentApp 도메인 데이터 전용 엔드포인트 응답 DTO
|
||||
*
|
||||
* llmAnswer 없이 domainData만 반환.
|
||||
* KakaoIntentApp은 domainData를 직접 파싱해 카드 UI를 구성.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DomainOnlyResponse {
|
||||
|
||||
/** 처리 성공 여부 */
|
||||
private boolean status;
|
||||
|
||||
/** 처리된 의도 타입 */
|
||||
private String intentType;
|
||||
|
||||
/**
|
||||
* DB 조회 구조화 데이터 (카드 포맷 구성용)
|
||||
* 핸들러 없거나 조회 실패 시 null
|
||||
*/
|
||||
private Map<String, Object> domainData;
|
||||
|
||||
/** 상태 메시지 (에러 시 오류 내용) */
|
||||
private String statusMsg;
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class IcNameCandidate {
|
||||
private String icName;
|
||||
private String icCode;
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class IcParamNormalizationResult {
|
||||
private boolean ambiguous;
|
||||
private String ambiguousParam;
|
||||
private String originalValue;
|
||||
private String normalizedValue;
|
||||
private String clarificationQuestion;
|
||||
|
||||
@Builder.Default
|
||||
private Map<String, String> normalizedParams = new HashMap<>();
|
||||
|
||||
@Builder.Default
|
||||
private List<IcNameCandidate> candidates = new ArrayList<>();
|
||||
}
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class IntentValidationResult {
|
||||
private boolean valid;
|
||||
@Builder.Default
|
||||
private Map<String, String> cleanedParams = new HashMap<>();
|
||||
@Builder.Default
|
||||
private List<String> missingParams = new ArrayList<>();
|
||||
private String clarificationQuestion;
|
||||
}
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LlmAgentRequest {
|
||||
private String question;
|
||||
private String botId;
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LlmAgentResponse {
|
||||
private boolean status;
|
||||
private String statusMsg;
|
||||
private String routeType;
|
||||
private String intentType;
|
||||
private String fetchOwner;
|
||||
private String uiType;
|
||||
private String answer;
|
||||
private String llmAnswer;
|
||||
private Map<String, Object> domainData;
|
||||
private List<String> faqUrls;
|
||||
private List<Map<String, Object>> references;
|
||||
private boolean needsClarification;
|
||||
private String clarificationQuestion;
|
||||
private String pendingIntentType;
|
||||
private List<String> missingParams;
|
||||
private Map<String, String> params;
|
||||
private List<IcNameCandidate> icCandidates;
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PendingIntentState {
|
||||
private String botId;
|
||||
private String intentType;
|
||||
@Builder.Default
|
||||
private Map<String, String> collectedParams = new HashMap<>();
|
||||
@Builder.Default
|
||||
private List<String> missingParams = new ArrayList<>();
|
||||
private Instant createdAt;
|
||||
}
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RestAreaParamNormalizationResult {
|
||||
private boolean ambiguous;
|
||||
private String ambiguousParam;
|
||||
private String originalValue;
|
||||
private String normalizedValue;
|
||||
private String clarificationQuestion;
|
||||
|
||||
@Builder.Default
|
||||
private Map<String, String> normalizedParams = new HashMap<>();
|
||||
}
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ToolExecuteRequest {
|
||||
private String toolName;
|
||||
@Builder.Default
|
||||
private Map<String, Object> arguments = new HashMap<>();
|
||||
private String botId;
|
||||
private String userInput;
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ToolExecuteResponse {
|
||||
private boolean status;
|
||||
private String statusMsg;
|
||||
private String intentType;
|
||||
private String fetchOwner;
|
||||
private String uiType;
|
||||
@Builder.Default
|
||||
private Map<String, String> params = new HashMap<>();
|
||||
private Map<String, Object> domainData;
|
||||
private boolean needsClarification;
|
||||
private String clarificationQuestion;
|
||||
@Builder.Default
|
||||
private List<String> missingParams = new ArrayList<>();
|
||||
@Builder.Default
|
||||
private List<IcNameCandidate> icCandidates = new ArrayList<>();
|
||||
}
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class ToolParamPipelineResult {
|
||||
private boolean valid;
|
||||
@Builder.Default
|
||||
private Map<String, String> params = new HashMap<>();
|
||||
private boolean needsClarification;
|
||||
private String clarificationQuestion;
|
||||
@Builder.Default
|
||||
private List<String> missingParams = new ArrayList<>();
|
||||
@Builder.Default
|
||||
private List<IcNameCandidate> icCandidates = new ArrayList<>();
|
||||
|
||||
public static ToolParamPipelineResult success(Map<String, String> params) {
|
||||
return ToolParamPipelineResult.builder()
|
||||
.valid(true)
|
||||
.params(params)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static ToolParamPipelineResult clarify(
|
||||
Map<String, String> params,
|
||||
String question,
|
||||
List<String> missingParams
|
||||
) {
|
||||
return ToolParamPipelineResult.builder()
|
||||
.valid(false)
|
||||
.params(params)
|
||||
.needsClarification(true)
|
||||
.clarificationQuestion(question)
|
||||
.missingParams(missingParams != null ? missingParams : new ArrayList<>())
|
||||
.build();
|
||||
}
|
||||
|
||||
public static ToolParamPipelineResult icAmbiguous(
|
||||
Map<String, String> params,
|
||||
String question,
|
||||
List<IcNameCandidate> candidates
|
||||
) {
|
||||
return ToolParamPipelineResult.builder()
|
||||
.valid(false)
|
||||
.params(params)
|
||||
.needsClarification(true)
|
||||
.clarificationQuestion(question)
|
||||
.icCandidates(candidates != null ? candidates : new ArrayList<>())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.formatter;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* RAG/LLM 프롬프트용으로 domainData에 사람이 읽기 쉬운 요약(llmSummary)을 붙인다.
|
||||
* KakaoIntentApp은 기존 키 구조(fareData 등)를 그대로 쓰므로, 이 인터페이스는 Map을 mutate 하되
|
||||
* 스킬이 쓰는 필드는 제거하지 않는다.
|
||||
*/
|
||||
public interface LlmDomainDataFormatter {
|
||||
|
||||
String getIntentType();
|
||||
|
||||
/**
|
||||
* domainData에 "llmSummary" 키(한국어 요약 문자열)를 추가한다.
|
||||
*
|
||||
* @param domainData {@link com.ex.chatbotapi.domain.agent.handler.IntentHandler} 조회 결과 (null이면 무시)
|
||||
*/
|
||||
void enrich(Map<String, Object> domainData);
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.handler;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 의도(Intent)별 도메인 데이터 조회를 담당하는 핸들러 인터페이스
|
||||
*
|
||||
* 새 스킬 추가 시:
|
||||
* 1. 이 인터페이스를 구현하는 XxxHandler 클래스 생성 (@Component)
|
||||
* 2. getIntentType()에 intentType 문자열 반환
|
||||
* 3. fetchDomainData()에 도메인 서비스 호출 로직 작성
|
||||
* → AgentOrchestratorService 수정 불필요
|
||||
*/
|
||||
public interface IntentHandler {
|
||||
|
||||
/**
|
||||
* 처리할 의도 타입 반환 (KakaoIntentApp이 전달하는 intentType과 일치해야 함)
|
||||
* 예: "FARE_SEARCH", "REST_AREA_PRICE"
|
||||
*/
|
||||
String getIntentType();
|
||||
|
||||
/**
|
||||
* 슬롯 파라미터를 받아 도메인 서비스에서 DB 데이터 조회
|
||||
*
|
||||
* @param params KakaoIntentApp 슬롯에서 추출한 파라미터 Map
|
||||
* @return DB 조회 결과 (RAG 프롬프트에 포함되어 LLM에 전달됨)
|
||||
*/
|
||||
Map<String, Object> fetchDomainData(Map<String, String> params);
|
||||
}
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.intent;
|
||||
|
||||
public enum FetchOwner {
|
||||
WAS,
|
||||
WEB,
|
||||
NONE
|
||||
}
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.intent;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
public class IntentDefinition {
|
||||
private String intentType;
|
||||
private String description;
|
||||
private FetchOwner fetchOwner = FetchOwner.WAS;
|
||||
private String uiType;
|
||||
private List<String> requiredParams = Collections.emptyList();
|
||||
private List<String> optionalParams = Collections.emptyList();
|
||||
private List<ParamDefinition> requiredParamDefinitions = Collections.emptyList();
|
||||
private List<ParamDefinition> optionalParamDefinitions = Collections.emptyList();
|
||||
private Double minConfidence;
|
||||
private Clarification clarification = new Clarification();
|
||||
|
||||
public double minConfidenceOrDefault(double defaultValue) {
|
||||
return minConfidence != null ? minConfidence : defaultValue;
|
||||
}
|
||||
|
||||
public String clarificationFor(List<String> missingParams) {
|
||||
if (clarification == null) {
|
||||
return "조회에 필요한 정보를 조금 더 알려주세요.";
|
||||
}
|
||||
if (missingParams != null && missingParams.size() == 1 && clarification.getMissing() != null) {
|
||||
String message = clarification.getMissing().get(missingParams.get(0));
|
||||
if (message != null && !message.trim().isEmpty()) {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
return clarification.getDefaultMessage() != null
|
||||
? clarification.getDefaultMessage()
|
||||
: "조회에 필요한 정보를 조금 더 알려주세요.";
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Clarification {
|
||||
private String defaultMessage;
|
||||
private Map<String, String> missing = Collections.emptyMap();
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ParamDefinition {
|
||||
private String name;
|
||||
private String description;
|
||||
private List<String> examples = Collections.emptyList();
|
||||
}
|
||||
}
|
||||
-199
@@ -1,199 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.intent;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class IntentDefinitionRegistry {
|
||||
private static final String INTENT_DEFINITIONS_FILE = "intent-definitions.yml";
|
||||
private static final Path EXTERNAL_INTENT_DEFINITIONS_PATH = Paths.get("config", INTENT_DEFINITIONS_FILE);
|
||||
|
||||
private final Map<String, IntentDefinition> definitions = new LinkedHashMap<>();
|
||||
|
||||
@PostConstruct
|
||||
@SuppressWarnings("unchecked")
|
||||
public void load() {
|
||||
try (InputStream inputStream = openIntentDefinitions()) {
|
||||
Map<String, Object> root = new Yaml().load(inputStream);
|
||||
Map<String, Object> intents = (Map<String, Object>) root.get("intents");
|
||||
definitions.clear();
|
||||
|
||||
for (Map.Entry<String, Object> entry : intents.entrySet()) {
|
||||
String intentType = entry.getKey();
|
||||
Map<String, Object> raw = (Map<String, Object>) entry.getValue();
|
||||
IntentDefinition definition = toDefinition(intentType, raw);
|
||||
definitions.put(intentType, definition);
|
||||
}
|
||||
log.info("intent definitions loaded: {}", definitions.keySet());
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("intent-definitions.yml 로드 실패", e);
|
||||
}
|
||||
}
|
||||
|
||||
private InputStream openIntentDefinitions() throws IOException {
|
||||
if (Files.exists(EXTERNAL_INTENT_DEFINITIONS_PATH)) {
|
||||
log.info("intent definitions loaded from external file: {}", EXTERNAL_INTENT_DEFINITIONS_PATH.toAbsolutePath());
|
||||
return Files.newInputStream(EXTERNAL_INTENT_DEFINITIONS_PATH);
|
||||
}
|
||||
|
||||
log.info("external intent definitions not found. using classpath resource: {}", INTENT_DEFINITIONS_FILE);
|
||||
return new ClassPathResource(INTENT_DEFINITIONS_FILE).getInputStream();
|
||||
}
|
||||
|
||||
public boolean supports(String intentType) {
|
||||
return intentType != null && definitions.containsKey(intentType);
|
||||
}
|
||||
|
||||
public IntentDefinition get(String intentType) {
|
||||
return definitions.get(intentType);
|
||||
}
|
||||
|
||||
public Map<String, IntentDefinition> all() {
|
||||
return Collections.unmodifiableMap(definitions);
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> toAnalysisDefinitions() {
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (IntentDefinition definition : definitions.values()) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("intentType", definition.getIntentType());
|
||||
item.put("description", definition.getDescription());
|
||||
item.put("params", mergeParamDefinitions(definition));
|
||||
result.add(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> mergeParamDefinitions(IntentDefinition definition) {
|
||||
List<Map<String, Object>> params = new ArrayList<>();
|
||||
appendParamDefinitions(params, definition.getRequiredParamDefinitions(), true);
|
||||
appendParamDefinitions(params, definition.getOptionalParamDefinitions(), false);
|
||||
return params;
|
||||
}
|
||||
|
||||
private void appendParamDefinitions(
|
||||
List<Map<String, Object>> params,
|
||||
List<IntentDefinition.ParamDefinition> definitions,
|
||||
boolean required
|
||||
) {
|
||||
for (IntentDefinition.ParamDefinition definition : definitions) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("name", definition.getName());
|
||||
item.put("description", definition.getDescription());
|
||||
item.put("required", required);
|
||||
item.put("examples", definition.getExamples());
|
||||
params.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
private IntentDefinition toDefinition(String intentType, Map<String, Object> raw) {
|
||||
IntentDefinition definition = new IntentDefinition();
|
||||
definition.setIntentType(intentType);
|
||||
definition.setDescription(asString(raw.get("description")));
|
||||
definition.setFetchOwner(FetchOwner.valueOf(asString(raw.get("fetchOwner"))));
|
||||
definition.setUiType(asString(raw.get("uiType")));
|
||||
List<IntentDefinition.ParamDefinition> requiredParamDefinitions = asParamDefinitions(raw.get("requiredParams"));
|
||||
List<IntentDefinition.ParamDefinition> optionalParamDefinitions = asParamDefinitions(raw.get("optionalParams"));
|
||||
definition.setRequiredParamDefinitions(requiredParamDefinitions);
|
||||
definition.setOptionalParamDefinitions(optionalParamDefinitions);
|
||||
definition.setRequiredParams(toParamNames(requiredParamDefinitions));
|
||||
definition.setOptionalParams(toParamNames(optionalParamDefinitions));
|
||||
definition.setMinConfidence(asDouble(raw.get("minConfidence")));
|
||||
definition.setClarification(toClarification(raw.get("clarification")));
|
||||
return definition;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private IntentDefinition.Clarification toClarification(Object value) {
|
||||
IntentDefinition.Clarification clarification = new IntentDefinition.Clarification();
|
||||
if (!(value instanceof Map)) {
|
||||
return clarification;
|
||||
}
|
||||
|
||||
Map<String, Object> raw = (Map<String, Object>) value;
|
||||
clarification.setDefaultMessage(asString(raw.get("default")));
|
||||
|
||||
Map<String, String> missing = new LinkedHashMap<>();
|
||||
Object missingObject = raw.get("missing");
|
||||
if (missingObject instanceof Map) {
|
||||
Map<String, Object> rawMissing = (Map<String, Object>) missingObject;
|
||||
for (Map.Entry<String, Object> entry : rawMissing.entrySet()) {
|
||||
missing.put(entry.getKey(), asString(entry.getValue()));
|
||||
}
|
||||
}
|
||||
clarification.setMissing(missing);
|
||||
return clarification;
|
||||
}
|
||||
|
||||
private String asString(Object value) {
|
||||
return value != null ? String.valueOf(value) : null;
|
||||
}
|
||||
|
||||
private Double asDouble(Object value) {
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).doubleValue();
|
||||
}
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return Double.valueOf(String.valueOf(value));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<IntentDefinition.ParamDefinition> asParamDefinitions(Object value) {
|
||||
if (!(value instanceof List)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<IntentDefinition.ParamDefinition> result = new ArrayList<>();
|
||||
for (Object item : (List<Object>) value) {
|
||||
IntentDefinition.ParamDefinition paramDefinition = new IntentDefinition.ParamDefinition();
|
||||
if (item instanceof Map) {
|
||||
Map<String, Object> raw = (Map<String, Object>) item;
|
||||
paramDefinition.setName(asString(raw.get("name")));
|
||||
paramDefinition.setDescription(asString(raw.get("description")));
|
||||
paramDefinition.setExamples(asStringList(raw.get("examples")));
|
||||
} else {
|
||||
paramDefinition.setName(asString(item));
|
||||
}
|
||||
if (paramDefinition.getName() != null && !paramDefinition.getName().trim().isEmpty()) {
|
||||
result.add(paramDefinition);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<String> toParamNames(List<IntentDefinition.ParamDefinition> paramDefinitions) {
|
||||
List<String> result = new ArrayList<>();
|
||||
for (IntentDefinition.ParamDefinition paramDefinition : paramDefinitions) {
|
||||
result.add(paramDefinition.getName());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<String> asStringList(Object value) {
|
||||
if (!(value instanceof List)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<String> result = new ArrayList<>();
|
||||
for (Object item : (List<Object>) value) {
|
||||
result.add(asString(item));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
-131
@@ -1,131 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.service;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.dto.AgentQueryRequest;
|
||||
import com.ex.chatbotapi.domain.agent.dto.AgentQueryResponse;
|
||||
import com.ex.chatbotapi.domain.agent.formatter.LlmDomainDataFormatter;
|
||||
import com.ex.chatbotapi.domain.agent.handler.IntentHandler;
|
||||
import com.ex.chatbotapi.domain.rag.dto.RagQueryRequest;
|
||||
import com.ex.chatbotapi.domain.rag.service.RagService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 에이전트 오케스트레이터
|
||||
*
|
||||
* 처리 흐름:
|
||||
* 1. intentType으로 등록된 IntentHandler 조회
|
||||
* 2. 핸들러가 있으면 DB 도메인 데이터 조회
|
||||
* 3. 질문 + 도메인 데이터를 RAG 서버(exAiChatBot)로 전달
|
||||
* 4. LLM 최종 응답 + 구조화 도메인 데이터를 함께 반환
|
||||
*
|
||||
* 새 스킬 추가 방법:
|
||||
* IntentHandler 인터페이스 구현체를 @Component로 등록하기만 하면 됨
|
||||
* 이 클래스 수정 불필요
|
||||
*
|
||||
* LLM용 요약(llmSummary) 추가:
|
||||
* LlmDomainDataFormatter 구현체를 @Component로 등록 (fare 등 도메인별)
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AgentOrchestratorService {
|
||||
|
||||
private final RagService ragService;
|
||||
private final Map<String, IntentHandler> handlerMap;
|
||||
private final Map<String, LlmDomainDataFormatter> llmFormatterMap;
|
||||
|
||||
public AgentOrchestratorService(List<IntentHandler> handlers,
|
||||
List<LlmDomainDataFormatter> llmFormatters,
|
||||
RagService ragService) {
|
||||
this.ragService = ragService;
|
||||
this.handlerMap = handlers.stream()
|
||||
.collect(Collectors.toMap(IntentHandler::getIntentType, Function.identity()));
|
||||
this.llmFormatterMap = llmFormatters.stream()
|
||||
.collect(Collectors.toMap(LlmDomainDataFormatter::getIntentType, Function.identity()));
|
||||
log.info("등록된 IntentHandler ({}개): {}", handlerMap.size(), handlerMap.keySet());
|
||||
log.info("등록된 LlmDomainDataFormatter ({}개): {}", llmFormatterMap.size(), llmFormatterMap.keySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 질문 처리 메인 메서드
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public AgentQueryResponse processUserQuery(AgentQueryRequest request) {
|
||||
log.info("Agent Orchestrator - 질문: '{}', intentType: {}",
|
||||
request.getQuestion(), request.getIntentType());
|
||||
|
||||
try {
|
||||
Map<String, Object> domainData = fetchDomainData(request);
|
||||
enrichDomainDataForLlm(request.getIntentType(), domainData);
|
||||
|
||||
RagQueryRequest ragRequest = new RagQueryRequest();
|
||||
ragRequest.setQuestion(request.getQuestion());
|
||||
ragRequest.setBotId(request.getBotId());
|
||||
ragRequest.setDomainData(domainData);
|
||||
ragRequest.setIntentType(request.getIntentType());
|
||||
|
||||
Map<String, Object> ragResult = ragService.processQuestion(ragRequest);
|
||||
|
||||
return AgentQueryResponse.builder()
|
||||
.status(true)
|
||||
.intentType(request.getIntentType())
|
||||
.llmAnswer((String) ragResult.get("answer"))
|
||||
.domainData(domainData)
|
||||
.faqUrls((List<String>) ragResult.get("faqUrls"))
|
||||
.references((List<Map<String, Object>>) ragResult.get("references"))
|
||||
.statusMsg("정상응답")
|
||||
.build();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Agent Orchestrator 처리 오류: {}", e.getMessage(), e);
|
||||
return AgentQueryResponse.builder()
|
||||
.status(false)
|
||||
.statusMsg("처리 중 오류 발생: " + e.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* intentType에 매핑된 핸들러로 DB 데이터 조회
|
||||
* 등록된 핸들러가 없으면 null 반환 → RAG 단독 처리
|
||||
*/
|
||||
private Map<String, Object> fetchDomainData(AgentQueryRequest request) {
|
||||
// 클라이언트(skill)가 이미 domainData를 수집해서 전달한 경우 handler 생략
|
||||
if (request.getDomainData() != null && !request.getDomainData().isEmpty()) {
|
||||
log.info("domainData pre-populated by web (intentType: {}), handler 생략", request.getIntentType());
|
||||
return request.getDomainData();
|
||||
}
|
||||
|
||||
String intentType = request.getIntentType();
|
||||
if (intentType == null || request.getParams() == null) {
|
||||
log.debug("intentType 또는 params 없음 → RAG 단독 처리");
|
||||
return null;
|
||||
}
|
||||
|
||||
IntentHandler handler = handlerMap.get(intentType);
|
||||
if (handler == null) {
|
||||
log.debug("등록된 핸들러 없음 (intentType: {}) → RAG 단독 처리", intentType);
|
||||
return null;
|
||||
}
|
||||
|
||||
log.info("도메인 데이터 조회 시작 - intentType: {}, params: {}", intentType, request.getParams());
|
||||
return handler.fetchDomainData(request.getParams());
|
||||
}
|
||||
|
||||
/**
|
||||
* intentType에 등록된 포매터가 있으면 domainData에 llmSummary 등을 추가 (RAG 프롬프트 가독성).
|
||||
*/
|
||||
private void enrichDomainDataForLlm(String intentType, Map<String, Object> domainData) {
|
||||
if (domainData == null || intentType == null) {
|
||||
return;
|
||||
}
|
||||
LlmDomainDataFormatter formatter = llmFormatterMap.get(intentType);
|
||||
if (formatter != null) {
|
||||
formatter.enrich(domainData);
|
||||
}
|
||||
}
|
||||
}
|
||||
-75
@@ -1,75 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.service;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.dto.CarNoParamNormalizationResult;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class CarNoParamNormalizer {
|
||||
private static final List<String> CAR_NO_INTENTS = Arrays.asList(
|
||||
"FARE_UNPAID",
|
||||
"FARE_REFUND"
|
||||
);
|
||||
|
||||
public CarNoParamNormalizationResult normalize(String intentType, Map<String, String> params) {
|
||||
Map<String, String> normalizedParams = new HashMap<>();
|
||||
if (params != null) {
|
||||
normalizedParams.putAll(params);
|
||||
}
|
||||
|
||||
if (!CAR_NO_INTENTS.contains(intentType) || params == null || params.isEmpty()) {
|
||||
return CarNoParamNormalizationResult.builder()
|
||||
.valid(true)
|
||||
.normalizedParams(normalizedParams)
|
||||
.build();
|
||||
}
|
||||
|
||||
String carNo = normalizedParams.get("carNo");
|
||||
if (carNo == null || carNo.trim().isEmpty()) {
|
||||
return invalid(intentType, normalizedParams);
|
||||
}
|
||||
|
||||
String normalizedCarNo = normalizeCarNo(carNo);
|
||||
if (!isFullCarNo(normalizedCarNo)) {
|
||||
return invalid(intentType, normalizedParams);
|
||||
}
|
||||
|
||||
normalizedParams.put("carNo", normalizedCarNo);
|
||||
return CarNoParamNormalizationResult.builder()
|
||||
.valid(true)
|
||||
.normalizedParams(normalizedParams)
|
||||
.build();
|
||||
}
|
||||
|
||||
private String normalizeCarNo(String carNo) {
|
||||
return carNo.trim()
|
||||
.replaceAll("[\\s\\-]", "")
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
private boolean isFullCarNo(String carNo) {
|
||||
return carNo.matches("\\d{2,3}[가-힣]\\d{4}");
|
||||
}
|
||||
|
||||
private CarNoParamNormalizationResult invalid(String intentType, Map<String, String> params) {
|
||||
return CarNoParamNormalizationResult.builder()
|
||||
.valid(false)
|
||||
.normalizedParams(params)
|
||||
.clarificationQuestion(clarificationQuestion(intentType))
|
||||
.build();
|
||||
}
|
||||
|
||||
private String clarificationQuestion(String intentType) {
|
||||
if ("FARE_UNPAID".equals(intentType)) {
|
||||
return "챗봇에서 미납 통행료 조회가 가능합니다. 조회하려면 전체 차량번호를 입력해 주세요. 예: 12가3456 미납 조회";
|
||||
}
|
||||
if ("FARE_REFUND".equals(intentType)) {
|
||||
return "챗봇에서 환불 내역 조회가 가능합니다. 조회하려면 전체 차량번호를 입력해 주세요. 예: 12가3456 환불 조회";
|
||||
}
|
||||
return "조회하려면 전체 차량번호를 입력해 주세요. 예: 12가3456";
|
||||
}
|
||||
}
|
||||
-77
@@ -1,77 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.service;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.dto.DomainOnlyRequest;
|
||||
import com.ex.chatbotapi.domain.agent.dto.DomainOnlyResponse;
|
||||
import com.ex.chatbotapi.domain.agent.handler.IntentHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 도메인 데이터 전용 서비스 (RAG/LLM 호출 없음)
|
||||
*
|
||||
* 전화번호 조회(IC_TEL) 등 LLM 응답이 불필요한 인텐트에 사용.
|
||||
* AgentOrchestratorService와 동일한 IntentHandler 레지스트리를 재사용.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class DomainOnlyService {
|
||||
|
||||
private final Map<String, IntentHandler> handlerMap;
|
||||
|
||||
public DomainOnlyService(List<IntentHandler> handlers) {
|
||||
this.handlerMap = handlers.stream()
|
||||
.collect(Collectors.toMap(IntentHandler::getIntentType, Function.identity()));
|
||||
log.info("DomainOnlyService 초기화 - 등록된 핸들러 ({}개): {}", handlerMap.size(), handlerMap.keySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 도메인 데이터만 조회 (LLM 없음)
|
||||
*/
|
||||
public DomainOnlyResponse fetchDomainData(DomainOnlyRequest request) {
|
||||
String intentType = request.getIntentType();
|
||||
log.info("DomainOnly 요청 - intentType: {}, params: {}", intentType, request.getParams());
|
||||
|
||||
try {
|
||||
if (intentType == null || request.getParams() == null) {
|
||||
log.warn("intentType 또는 params 누락");
|
||||
return DomainOnlyResponse.builder()
|
||||
.status(false)
|
||||
.statusMsg("intentType과 params는 필수입니다.")
|
||||
.build();
|
||||
}
|
||||
|
||||
IntentHandler handler = handlerMap.get(intentType);
|
||||
if (handler == null) {
|
||||
log.warn("등록된 핸들러 없음 - intentType: {}", intentType);
|
||||
return DomainOnlyResponse.builder()
|
||||
.status(false)
|
||||
.intentType(intentType)
|
||||
.statusMsg("처리 가능한 핸들러가 없습니다: " + intentType)
|
||||
.build();
|
||||
}
|
||||
|
||||
Map<String, Object> domainData = handler.fetchDomainData(request.getParams());
|
||||
log.info("DomainOnly 조회 완료 - intentType: {}", intentType);
|
||||
|
||||
return DomainOnlyResponse.builder()
|
||||
.status(true)
|
||||
.intentType(intentType)
|
||||
.domainData(domainData)
|
||||
.statusMsg("정상응답")
|
||||
.build();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("DomainOnly 처리 오류 - intentType: {}, 오류: {}", intentType, e.getMessage(), e);
|
||||
return DomainOnlyResponse.builder()
|
||||
.status(false)
|
||||
.intentType(intentType)
|
||||
.statusMsg("처리 중 오류 발생: " + e.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
-149
@@ -1,149 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.service;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.dto.IcNameCandidate;
|
||||
import com.ex.chatbotapi.domain.agent.dto.IcParamNormalizationResult;
|
||||
import com.ex.chatbotapi.domain.fare.repository.FareRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.bson.Document;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class IcParamNormalizer {
|
||||
private static final int CANDIDATE_LIMIT = 5;
|
||||
private static final List<String> IC_INTENTS = Arrays.asList(
|
||||
"FARE_SEARCH",
|
||||
"TROAD_SEARCH_ROUTE",
|
||||
"IC_TEL"
|
||||
);
|
||||
|
||||
private final FareRepository fareRepository;
|
||||
|
||||
public IcParamNormalizationResult normalize(String intentType, Map<String, String> params) {
|
||||
Map<String, String> normalizedParams = new HashMap<>();
|
||||
if (params != null) {
|
||||
normalizedParams.putAll(params);
|
||||
}
|
||||
|
||||
if (!IC_INTENTS.contains(intentType) || params == null || params.isEmpty()) {
|
||||
return IcParamNormalizationResult.builder()
|
||||
.normalizedParams(normalizedParams)
|
||||
.build();
|
||||
}
|
||||
|
||||
for (String paramName : icParamNames(intentType)) {
|
||||
String originalValue = normalizedParams.get(paramName);
|
||||
if (originalValue == null || originalValue.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String normalizedValue = normalizeInput(originalValue);
|
||||
Document exact = fareRepository.findIcInfoByName(normalizedValue);
|
||||
if (exact != null) {
|
||||
String icName = exact.getString("ic_name");
|
||||
String icCode = formatIcCode(exact.get("ic_code"));
|
||||
normalizedParams.put(paramName, icName);
|
||||
normalizedParams.put(paramName + "Code", icCode);
|
||||
continue;
|
||||
}
|
||||
|
||||
List<IcNameCandidate> candidates = findCandidates(normalizedValue);
|
||||
if (!candidates.isEmpty()) {
|
||||
return IcParamNormalizationResult.builder()
|
||||
.ambiguous(true)
|
||||
.ambiguousParam(paramName)
|
||||
.originalValue(originalValue)
|
||||
.normalizedValue(normalizedValue)
|
||||
.normalizedParams(normalizedParams)
|
||||
.candidates(candidates)
|
||||
.clarificationQuestion(buildClarificationQuestion(paramName, normalizedValue, candidates))
|
||||
.build();
|
||||
}
|
||||
|
||||
normalizedParams.put(paramName, normalizedValue);
|
||||
}
|
||||
|
||||
return IcParamNormalizationResult.builder()
|
||||
.normalizedParams(normalizedParams)
|
||||
.build();
|
||||
}
|
||||
|
||||
private List<String> icParamNames(String intentType) {
|
||||
if ("IC_TEL".equals(intentType)) {
|
||||
return Arrays.asList("icName");
|
||||
}
|
||||
return Arrays.asList("fromIc", "toIc");
|
||||
}
|
||||
|
||||
private String normalizeInput(String value) {
|
||||
String normalized = value.trim().replaceAll("\\s+", "");
|
||||
String previous;
|
||||
do {
|
||||
previous = normalized;
|
||||
normalized = normalized
|
||||
.replaceAll("(?i)IC$", "")
|
||||
.replaceAll("(영업소|톨게이트|요금소)$", "");
|
||||
} while (!previous.equals(normalized));
|
||||
return normalized.trim();
|
||||
}
|
||||
|
||||
private List<IcNameCandidate> findCandidates(String icName) {
|
||||
List<Document> documents = fareRepository.findIcInfoByNameContains(icName, CANDIDATE_LIMIT);
|
||||
|
||||
Map<String, IcNameCandidate> unique = new LinkedHashMap<>();
|
||||
for (Document document : documents) {
|
||||
String name = document.getString("ic_name");
|
||||
if (name == null || name.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
unique.putIfAbsent(name, IcNameCandidate.builder()
|
||||
.icName(name)
|
||||
.icCode(formatIcCode(document.get("ic_code")))
|
||||
.build());
|
||||
}
|
||||
return new ArrayList<>(unique.values());
|
||||
}
|
||||
|
||||
private String buildClarificationQuestion(
|
||||
String paramName,
|
||||
String normalizedValue,
|
||||
List<IcNameCandidate> candidates
|
||||
) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(normalizedValue).append("와 정확히 일치하는 영업소를 찾지 못했습니다. ");
|
||||
sb.append(displayParamName(paramName)).append(" 영업소를 아래 후보 중에서 선택해 주세요.\n");
|
||||
for (IcNameCandidate candidate : candidates) {
|
||||
sb.append("- ").append(candidate.getIcName()).append("\n");
|
||||
}
|
||||
return sb.toString().trim();
|
||||
}
|
||||
|
||||
private String displayParamName(String paramName) {
|
||||
if ("fromIc".equals(paramName)) {
|
||||
return "출발";
|
||||
}
|
||||
if ("toIc".equals(paramName)) {
|
||||
return "도착";
|
||||
}
|
||||
return "조회할";
|
||||
}
|
||||
|
||||
private String formatIcCode(Object icCode) {
|
||||
if (icCode == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String code = String.valueOf(icCode).trim();
|
||||
if (code.matches("\\d{1,2}")) {
|
||||
return String.format("%03d", Integer.parseInt(code));
|
||||
}
|
||||
return code;
|
||||
}
|
||||
}
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.service;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* LLM tool arguments(Object/List 등)를 validator가 처리 가능한 Map<String, String>으로 변환한다.
|
||||
*/
|
||||
@Component
|
||||
public class IntentAnalysisParamNormalizer {
|
||||
|
||||
public Map<String, String> toStringMap(Map<String, ?> rawParams) {
|
||||
Map<String, String> normalized = new HashMap<>();
|
||||
if (rawParams == null) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
for (Map.Entry<String, ?> entry : rawParams.entrySet()) {
|
||||
String value = stringify(entry.getValue());
|
||||
if (value != null && !value.trim().isEmpty()) {
|
||||
normalized.put(entry.getKey(), value);
|
||||
}
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private String stringify(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Collection) {
|
||||
return ((Collection<?>) value).stream()
|
||||
.map(String::valueOf)
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.collect(Collectors.joining(","));
|
||||
}
|
||||
String text = String.valueOf(value).trim();
|
||||
if (text.startsWith("[") && text.endsWith("]")) {
|
||||
text = text.substring(1, text.length() - 1)
|
||||
.replace("\"", "")
|
||||
.replace("'", "")
|
||||
.trim();
|
||||
}
|
||||
return text.isEmpty() || "null".equalsIgnoreCase(text) ? null : text;
|
||||
}
|
||||
}
|
||||
-90
@@ -1,90 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.service;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.dto.IntentValidationResult;
|
||||
import com.ex.chatbotapi.domain.agent.intent.IntentDefinition;
|
||||
import com.ex.chatbotapi.domain.agent.intent.IntentDefinitionRegistry;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class IntentParamValidator {
|
||||
private static final Set<String> INVALID_PLACEHOLDERS = new HashSet<>(Arrays.asList(
|
||||
"여기", "거기", "저기", "현재위치", "현재 위치", "내위치", "내 위치", "이곳", "그곳", "저곳"
|
||||
));
|
||||
|
||||
private final IntentDefinitionRegistry intentDefinitionRegistry;
|
||||
|
||||
public boolean supports(String intentType) {
|
||||
return intentDefinitionRegistry.supports(intentType);
|
||||
}
|
||||
|
||||
public IntentValidationResult validate(String intentType, Map<String, String> params) {
|
||||
Map<String, String> cleaned = cleanParams(params);
|
||||
List<String> missing = new ArrayList<>();
|
||||
|
||||
IntentDefinition definition = intentDefinitionRegistry.get(intentType);
|
||||
if (definition == null) {
|
||||
return IntentValidationResult.builder()
|
||||
.valid(false)
|
||||
.cleanedParams(cleaned)
|
||||
.missingParams(Collections.<String>emptyList())
|
||||
.clarificationQuestion("처리할 수 있는 업무 유형인지 확인이 필요합니다.")
|
||||
.build();
|
||||
}
|
||||
|
||||
for (String key : definition.getRequiredParams()) {
|
||||
if (!hasUsableValue(cleaned.get(key))) {
|
||||
missing.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
return IntentValidationResult.builder()
|
||||
.valid(missing.isEmpty())
|
||||
.cleanedParams(cleaned)
|
||||
.missingParams(missing)
|
||||
.clarificationQuestion(definition.clarificationFor(missing))
|
||||
.build();
|
||||
}
|
||||
|
||||
public Map<String, String> cleanParams(Map<String, String> params) {
|
||||
Map<String, String> cleaned = new HashMap<>();
|
||||
if (params == null) {
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
for (Map.Entry<String, String> entry : params.entrySet()) {
|
||||
String value = normalize(entry.getValue());
|
||||
if (hasUsableValue(value)) {
|
||||
cleaned.put(entry.getKey(), value);
|
||||
}
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
private boolean hasUsableValue(String value) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return !INVALID_PLACEHOLDERS.contains(normalize(value));
|
||||
}
|
||||
|
||||
private String normalize(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return value.trim()
|
||||
.replaceAll("(에서|부터|까지|으로|로|은|는|이|가|을|를)$", "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
}
|
||||
-473
@@ -1,473 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.service;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.dto.AgentQueryRequest;
|
||||
import com.ex.chatbotapi.domain.agent.dto.AgentQueryResponse;
|
||||
import com.ex.chatbotapi.domain.agent.dto.CarNoParamNormalizationResult;
|
||||
import com.ex.chatbotapi.domain.agent.dto.IcParamNormalizationResult;
|
||||
import com.ex.chatbotapi.domain.agent.dto.IntentValidationResult;
|
||||
import com.ex.chatbotapi.domain.agent.dto.LlmAgentRequest;
|
||||
import com.ex.chatbotapi.domain.agent.dto.LlmAgentResponse;
|
||||
import com.ex.chatbotapi.domain.agent.dto.PendingIntentState;
|
||||
import com.ex.chatbotapi.domain.agent.dto.RestAreaParamNormalizationResult;
|
||||
import com.ex.chatbotapi.domain.agent.intent.FetchOwner;
|
||||
import com.ex.chatbotapi.domain.agent.intent.IntentDefinition;
|
||||
import com.ex.chatbotapi.domain.agent.intent.IntentDefinitionRegistry;
|
||||
import com.ex.chatbotapi.domain.rag.client.RagServerClient;
|
||||
import com.ex.chatbotapi.domain.rag.dto.IntentAnalysisRequest;
|
||||
import com.ex.chatbotapi.domain.rag.dto.IntentAnalysisResponse;
|
||||
import com.ex.chatbotapi.domain.rag.dto.RagQueryRequest;
|
||||
import com.ex.chatbotapi.domain.rag.service.RagService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LlmAgentRouterService {
|
||||
private static final double DOMAIN_CONFIDENCE_THRESHOLD = 0.60;
|
||||
private static final String FARE_UNPAID_INTENT = "FARE_UNPAID";
|
||||
private static final Pattern FULL_CAR_NO_PATTERN = Pattern.compile(".*\\d{2,3}\\s*[가-힣]\\s*\\d{4}.*");
|
||||
|
||||
private final RagServerClient ragServerClient;
|
||||
private final RagService ragService;
|
||||
private final AgentOrchestratorService agentOrchestratorService;
|
||||
private final IntentParamValidator intentParamValidator;
|
||||
private final CarNoParamNormalizer carNoParamNormalizer;
|
||||
private final IcParamNormalizer icParamNormalizer;
|
||||
private final RestAreaParamNormalizer restAreaParamNormalizer;
|
||||
private final PendingIntentService pendingIntentService;
|
||||
private final IntentDefinitionRegistry intentDefinitionRegistry;
|
||||
|
||||
public LlmAgentResponse process(LlmAgentRequest request) {
|
||||
validateRequest(request);
|
||||
long startedAt = System.currentTimeMillis();
|
||||
|
||||
PendingIntentState pending = pendingIntentService.get(request.getBotId());
|
||||
IntentAnalysisResponse analysis = analyze(request, pending);
|
||||
|
||||
LlmAgentResponse response;
|
||||
if (pending != null) {
|
||||
LlmAgentResponse pendingResponse = tryResolvePending(request, pending, analysis);
|
||||
if (pendingResponse != null) {
|
||||
response = pendingResponse;
|
||||
logRouteDecision(request, analysis, response, startedAt);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
response = processFreshIntent(request, analysis);
|
||||
logRouteDecision(request, analysis, response, startedAt);
|
||||
return response;
|
||||
}
|
||||
|
||||
private IntentAnalysisResponse analyze(LlmAgentRequest request, PendingIntentState pending) {
|
||||
IntentAnalysisRequest analysisRequest = IntentAnalysisRequest.builder()
|
||||
.query(request.getQuestion())
|
||||
.botId(request.getBotId())
|
||||
.pendingIntentType(pending != null ? pending.getIntentType() : null)
|
||||
.pendingParams(pending != null ? pending.getCollectedParams() : null)
|
||||
.intentDefinitions(intentDefinitionRegistry.toAnalysisDefinitions())
|
||||
.build();
|
||||
|
||||
IntentAnalysisResponse response = ragServerClient.analyzeIntent(analysisRequest);
|
||||
if (response == null) {
|
||||
response = new IntentAnalysisResponse();
|
||||
}
|
||||
if (response.getParams() == null) {
|
||||
response.setParams(new HashMap<String, String>());
|
||||
}
|
||||
|
||||
log.info("LLM intent 분석 결과 - intentType: {}, confidence: {}, params: {}, reason: {}",
|
||||
response.getIntentType(), response.getConfidence(), response.getParams(), response.getReason());
|
||||
return response;
|
||||
}
|
||||
|
||||
private LlmAgentResponse tryResolvePending(
|
||||
LlmAgentRequest request,
|
||||
PendingIntentState pending,
|
||||
IntentAnalysisResponse analysis
|
||||
) {
|
||||
boolean sameIntent = pending.getIntentType().equals(analysis.getIntentType());
|
||||
boolean simpleSlotAnswer = isSimpleSlotAnswer(request.getQuestion(), pending);
|
||||
|
||||
if (!sameIntent && intentParamValidator.supports(analysis.getIntentType())
|
||||
&& analysis.confidenceOrZero() >= DOMAIN_CONFIDENCE_THRESHOLD) {
|
||||
log.info("새 intent 감지로 기존 pending 취소 - old: {}, new: {}",
|
||||
pending.getIntentType(), analysis.getIntentType());
|
||||
pendingIntentService.clear(request.getBotId());
|
||||
return processFreshIntent(request, analysis);
|
||||
}
|
||||
|
||||
if (!sameIntent && !simpleSlotAnswer) {
|
||||
log.info("pending과 무관한 질문으로 판단, RAG로 전환 - pending: {}", pending.getIntentType());
|
||||
pendingIntentService.clear(request.getBotId());
|
||||
return processRagOnly(request);
|
||||
}
|
||||
|
||||
if (isUnpaidGuidanceQuestion(request.getQuestion()) && !containsFullCarNo(request.getQuestion())) {
|
||||
log.info("미납 안내성 질문으로 판단, pending 취소 후 RAG로 전환 - pending: {}", pending.getIntentType());
|
||||
pendingIntentService.clear(request.getBotId());
|
||||
return processRagOnly(request);
|
||||
}
|
||||
|
||||
Map<String, String> merged = new HashMap<>(pending.getCollectedParams());
|
||||
merged.putAll(intentParamValidator.cleanParams(analysis.getParams()));
|
||||
if (simpleSlotAnswer && pending.getMissingParams() != null && pending.getMissingParams().size() == 1) {
|
||||
merged.put(pending.getMissingParams().get(0), request.getQuestion().trim());
|
||||
}
|
||||
|
||||
IntentValidationResult validation = intentParamValidator.validate(pending.getIntentType(), merged);
|
||||
if (validation.isValid()) {
|
||||
CarNoParamNormalizationResult carNoNormalization = carNoParamNormalizer.normalize(
|
||||
pending.getIntentType(),
|
||||
validation.getCleanedParams());
|
||||
if (!carNoNormalization.isValid()) {
|
||||
savePendingForParam(request.getBotId(), pending.getIntentType(), carNoNormalization.getNormalizedParams(), "carNo");
|
||||
return paramClarificationResponse(
|
||||
pending.getIntentType(),
|
||||
carNoNormalization.getNormalizedParams(),
|
||||
"carNo",
|
||||
carNoNormalization.getClarificationQuestion());
|
||||
}
|
||||
|
||||
IcParamNormalizationResult normalization = icParamNormalizer.normalize(
|
||||
pending.getIntentType(),
|
||||
carNoNormalization.getNormalizedParams());
|
||||
if (normalization.isAmbiguous()) {
|
||||
saveIcCandidatePending(request.getBotId(), pending.getIntentType(), normalization);
|
||||
return icCandidateClarificationResponse(pending.getIntentType(), normalization);
|
||||
}
|
||||
|
||||
RestAreaParamNormalizationResult restAreaNormalization = restAreaParamNormalizer.normalize(
|
||||
pending.getIntentType(),
|
||||
normalization.getNormalizedParams());
|
||||
|
||||
pendingIntentService.clear(request.getBotId());
|
||||
return processByFetchOwner(
|
||||
request,
|
||||
pending.getIntentType(),
|
||||
restAreaNormalization.getNormalizedParams(),
|
||||
"pending_filled");
|
||||
}
|
||||
|
||||
savePending(request.getBotId(), pending.getIntentType(), validation);
|
||||
return clarificationResponse(pending.getIntentType(), validation);
|
||||
}
|
||||
|
||||
private LlmAgentResponse processFreshIntent(LlmAgentRequest request, IntentAnalysisResponse analysis) {
|
||||
String intentType = analysis.getIntentType();
|
||||
IntentDefinition definition = intentDefinitionRegistry.get(intentType);
|
||||
if (definition != null
|
||||
&& analysis.confidenceOrZero() >= definition.minConfidenceOrDefault(DOMAIN_CONFIDENCE_THRESHOLD)) {
|
||||
if (FARE_UNPAID_INTENT.equals(intentType)
|
||||
&& isUnpaidGuidanceQuestion(request.getQuestion())
|
||||
&& !containsFullCarNo(request.getQuestion())) {
|
||||
pendingIntentService.clear(request.getBotId());
|
||||
log.info("미납 안내성 질문으로 판단, 차량번호 보완질문 대신 RAG로 전환 - question: {}", request.getQuestion());
|
||||
return processRagOnly(request);
|
||||
}
|
||||
|
||||
IntentValidationResult validation = intentParamValidator.validate(intentType, analysis.getParams());
|
||||
if (validation.isValid()) {
|
||||
CarNoParamNormalizationResult carNoNormalization = carNoParamNormalizer.normalize(
|
||||
intentType,
|
||||
validation.getCleanedParams());
|
||||
if (!carNoNormalization.isValid()) {
|
||||
savePendingForParam(request.getBotId(), intentType, carNoNormalization.getNormalizedParams(), "carNo");
|
||||
return paramClarificationResponse(
|
||||
intentType,
|
||||
carNoNormalization.getNormalizedParams(),
|
||||
"carNo",
|
||||
carNoNormalization.getClarificationQuestion());
|
||||
}
|
||||
|
||||
IcParamNormalizationResult normalization = icParamNormalizer.normalize(
|
||||
intentType,
|
||||
carNoNormalization.getNormalizedParams());
|
||||
if (normalization.isAmbiguous()) {
|
||||
saveIcCandidatePending(request.getBotId(), intentType, normalization);
|
||||
return icCandidateClarificationResponse(intentType, normalization);
|
||||
}
|
||||
|
||||
RestAreaParamNormalizationResult restAreaNormalization = restAreaParamNormalizer.normalize(
|
||||
intentType,
|
||||
normalization.getNormalizedParams());
|
||||
|
||||
pendingIntentService.clear(request.getBotId());
|
||||
return processByFetchOwner(request, intentType, restAreaNormalization.getNormalizedParams(), "domain");
|
||||
}
|
||||
|
||||
savePending(request.getBotId(), intentType, validation);
|
||||
return clarificationResponse(intentType, validation);
|
||||
}
|
||||
|
||||
pendingIntentService.clear(request.getBotId());
|
||||
return processRagOnly(request);
|
||||
}
|
||||
|
||||
private LlmAgentResponse processByFetchOwner(
|
||||
LlmAgentRequest request,
|
||||
String intentType,
|
||||
Map<String, String> params,
|
||||
String routeType
|
||||
) {
|
||||
IntentDefinition definition = intentDefinitionRegistry.get(intentType);
|
||||
if (definition == null) {
|
||||
return processRagOnly(request);
|
||||
}
|
||||
|
||||
if (definition.getFetchOwner() == FetchOwner.WEB) {
|
||||
return webFetchRequiredResponse(intentType, params, definition);
|
||||
}
|
||||
if (definition.getFetchOwner() == FetchOwner.NONE) {
|
||||
return processRagOnly(request);
|
||||
}
|
||||
return processDomain(request, intentType, params, routeType, definition);
|
||||
}
|
||||
|
||||
private LlmAgentResponse processDomain(
|
||||
LlmAgentRequest request,
|
||||
String intentType,
|
||||
Map<String, String> params,
|
||||
String routeType,
|
||||
IntentDefinition definition
|
||||
) {
|
||||
AgentQueryRequest agentRequest = new AgentQueryRequest(
|
||||
request.getQuestion(),
|
||||
intentType,
|
||||
params,
|
||||
request.getBotId(),
|
||||
null
|
||||
);
|
||||
AgentQueryResponse agentResponse = agentOrchestratorService.processUserQuery(agentRequest);
|
||||
String answer = agentResponse.getLlmAnswer() != null
|
||||
? agentResponse.getLlmAnswer()
|
||||
: agentResponse.getStatusMsg();
|
||||
|
||||
return LlmAgentResponse.builder()
|
||||
.status(agentResponse.isStatus())
|
||||
.statusMsg(agentResponse.getStatusMsg())
|
||||
.routeType(routeType)
|
||||
.intentType(intentType)
|
||||
.fetchOwner(definition.getFetchOwner().name())
|
||||
.uiType(definition.getUiType())
|
||||
.answer(answer)
|
||||
.llmAnswer(agentResponse.getLlmAnswer())
|
||||
.domainData(agentResponse.getDomainData())
|
||||
.faqUrls(agentResponse.getFaqUrls())
|
||||
.references(agentResponse.getReferences())
|
||||
.needsClarification(false)
|
||||
.params(params)
|
||||
.build();
|
||||
}
|
||||
|
||||
private LlmAgentResponse webFetchRequiredResponse(
|
||||
String intentType,
|
||||
Map<String, String> params,
|
||||
IntentDefinition definition
|
||||
) {
|
||||
return LlmAgentResponse.builder()
|
||||
.status(true)
|
||||
.statusMsg("클라이언트 도메인 데이터 조회 필요")
|
||||
.routeType("web_fetch_required")
|
||||
.intentType(intentType)
|
||||
.fetchOwner(definition.getFetchOwner().name())
|
||||
.uiType(definition.getUiType())
|
||||
.needsClarification(false)
|
||||
.params(params)
|
||||
.build();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private LlmAgentResponse processRagOnly(LlmAgentRequest request) {
|
||||
RagQueryRequest ragRequest = new RagQueryRequest(request.getQuestion(), request.getBotId(), null, null);
|
||||
Map<String, Object> ragResult = ragService.processQuestion(ragRequest);
|
||||
String answer = String.valueOf(ragResult.get("answer"));
|
||||
|
||||
return LlmAgentResponse.builder()
|
||||
.status(Boolean.TRUE.equals(ragResult.get("status")))
|
||||
.statusMsg((String) ragResult.get("statusMsg"))
|
||||
.routeType("rag")
|
||||
.fetchOwner(FetchOwner.NONE.name())
|
||||
.answer(answer)
|
||||
.llmAnswer(answer)
|
||||
.faqUrls((List<String>) ragResult.get("faqUrls"))
|
||||
.references((List<Map<String, Object>>) ragResult.get("references"))
|
||||
.needsClarification(false)
|
||||
.build();
|
||||
}
|
||||
|
||||
private LlmAgentResponse clarificationResponse(String intentType, IntentValidationResult validation) {
|
||||
String question = validation.getClarificationQuestion();
|
||||
return LlmAgentResponse.builder()
|
||||
.status(true)
|
||||
.statusMsg("필수 파라미터 추가 입력 필요")
|
||||
.routeType("clarify")
|
||||
.intentType(intentType)
|
||||
.fetchOwner(resolveFetchOwner(intentType))
|
||||
.answer(question)
|
||||
.llmAnswer(question)
|
||||
.needsClarification(true)
|
||||
.clarificationQuestion(question)
|
||||
.pendingIntentType(intentType)
|
||||
.missingParams(validation.getMissingParams())
|
||||
.params(validation.getCleanedParams())
|
||||
.build();
|
||||
}
|
||||
|
||||
private LlmAgentResponse icCandidateClarificationResponse(
|
||||
String intentType,
|
||||
IcParamNormalizationResult normalization
|
||||
) {
|
||||
String question = normalization.getClarificationQuestion();
|
||||
return LlmAgentResponse.builder()
|
||||
.status(true)
|
||||
.statusMsg("영업소 후보 선택 필요")
|
||||
.routeType("clarify")
|
||||
.intentType(intentType)
|
||||
.fetchOwner(resolveFetchOwner(intentType))
|
||||
.answer(question)
|
||||
.llmAnswer(question)
|
||||
.needsClarification(true)
|
||||
.clarificationQuestion(question)
|
||||
.pendingIntentType(intentType)
|
||||
.missingParams(Collections.singletonList(normalization.getAmbiguousParam()))
|
||||
.params(normalization.getNormalizedParams())
|
||||
.icCandidates(normalization.getCandidates())
|
||||
.build();
|
||||
}
|
||||
|
||||
private LlmAgentResponse paramClarificationResponse(
|
||||
String intentType,
|
||||
Map<String, String> params,
|
||||
String missingParam,
|
||||
String question
|
||||
) {
|
||||
return LlmAgentResponse.builder()
|
||||
.status(true)
|
||||
.statusMsg("파라미터 추가 입력 필요")
|
||||
.routeType("clarify")
|
||||
.intentType(intentType)
|
||||
.fetchOwner(resolveFetchOwner(intentType))
|
||||
.answer(question)
|
||||
.llmAnswer(question)
|
||||
.needsClarification(true)
|
||||
.clarificationQuestion(question)
|
||||
.pendingIntentType(intentType)
|
||||
.missingParams(Collections.singletonList(missingParam))
|
||||
.params(params)
|
||||
.build();
|
||||
}
|
||||
|
||||
private String resolveFetchOwner(String intentType) {
|
||||
IntentDefinition definition = intentDefinitionRegistry.get(intentType);
|
||||
return definition != null && definition.getFetchOwner() != null
|
||||
? definition.getFetchOwner().name()
|
||||
: null;
|
||||
}
|
||||
|
||||
private boolean isUnpaidGuidanceQuestion(String question) {
|
||||
if (question == null) {
|
||||
return false;
|
||||
}
|
||||
String normalized = question.replaceAll("\\s+", "");
|
||||
if (!normalized.contains("미납")) {
|
||||
return false;
|
||||
}
|
||||
return normalized.contains("방법")
|
||||
|| normalized.contains("어떻게")
|
||||
|| normalized.contains("어디서")
|
||||
|| normalized.contains("어디")
|
||||
|| normalized.contains("절차")
|
||||
|| normalized.contains("경로")
|
||||
|| normalized.contains("사이트")
|
||||
|| normalized.contains("홈페이지")
|
||||
|| normalized.contains("앱");
|
||||
}
|
||||
|
||||
private boolean containsFullCarNo(String question) {
|
||||
return question != null && FULL_CAR_NO_PATTERN.matcher(question).matches();
|
||||
}
|
||||
|
||||
private void savePending(String botId, String intentType, IntentValidationResult validation) {
|
||||
PendingIntentState state = PendingIntentState.builder()
|
||||
.botId(botId)
|
||||
.intentType(intentType)
|
||||
.collectedParams(validation.getCleanedParams())
|
||||
.missingParams(validation.getMissingParams())
|
||||
.build();
|
||||
pendingIntentService.save(state);
|
||||
}
|
||||
|
||||
private void saveIcCandidatePending(
|
||||
String botId,
|
||||
String intentType,
|
||||
IcParamNormalizationResult normalization
|
||||
) {
|
||||
PendingIntentState state = PendingIntentState.builder()
|
||||
.botId(botId)
|
||||
.intentType(intentType)
|
||||
.collectedParams(normalization.getNormalizedParams())
|
||||
.missingParams(Collections.singletonList(normalization.getAmbiguousParam()))
|
||||
.build();
|
||||
pendingIntentService.save(state);
|
||||
}
|
||||
|
||||
private void savePendingForParam(
|
||||
String botId,
|
||||
String intentType,
|
||||
Map<String, String> params,
|
||||
String missingParam
|
||||
) {
|
||||
PendingIntentState state = PendingIntentState.builder()
|
||||
.botId(botId)
|
||||
.intentType(intentType)
|
||||
.collectedParams(params)
|
||||
.missingParams(Collections.singletonList(missingParam))
|
||||
.build();
|
||||
pendingIntentService.save(state);
|
||||
}
|
||||
|
||||
private boolean isSimpleSlotAnswer(String question, PendingIntentState pending) {
|
||||
if (question == null || pending.getMissingParams() == null || pending.getMissingParams().size() != 1) {
|
||||
return false;
|
||||
}
|
||||
String text = question.trim();
|
||||
if (text.length() > 20 || text.contains("?") || text.contains("알려") || text.contains("조회")) {
|
||||
return false;
|
||||
}
|
||||
return !text.contains(" ");
|
||||
}
|
||||
|
||||
private void validateRequest(LlmAgentRequest request) {
|
||||
if (request == null || request.getQuestion() == null || request.getQuestion().trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("질문이 비어있습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
private void logRouteDecision(
|
||||
LlmAgentRequest request,
|
||||
IntentAnalysisResponse analysis,
|
||||
LlmAgentResponse response,
|
||||
long startedAt
|
||||
) {
|
||||
long elapsedMs = System.currentTimeMillis() - startedAt;
|
||||
log.info("[LLM_ROUTER] botId={}, query='{}', intentType={}, confidence={}, routeType={}, fetchOwner={}, uiType={}, missingParams={}, elapsedMs={}",
|
||||
request.getBotId(),
|
||||
request.getQuestion(),
|
||||
analysis.getIntentType(),
|
||||
analysis.getConfidence(),
|
||||
response.getRouteType(),
|
||||
response.getFetchOwner(),
|
||||
response.getUiType(),
|
||||
response.getMissingParams(),
|
||||
elapsedMs);
|
||||
}
|
||||
}
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.service;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.dto.PendingIntentState;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class PendingIntentService {
|
||||
private static final Duration TTL = Duration.ofMinutes(5);
|
||||
|
||||
private final Map<String, PendingIntentState> states = new ConcurrentHashMap<>();
|
||||
|
||||
public PendingIntentState get(String botId) {
|
||||
if (botId == null || botId.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
PendingIntentState state = states.get(botId);
|
||||
if (state == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (state.getCreatedAt() == null || state.getCreatedAt().plus(TTL).isBefore(Instant.now())) {
|
||||
states.remove(botId);
|
||||
log.info("pending intent 만료 - botId: {}, intentType: {}", botId, state.getIntentType());
|
||||
return null;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
public void save(PendingIntentState state) {
|
||||
if (state == null || state.getBotId() == null || state.getBotId().trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
state.setCreatedAt(Instant.now());
|
||||
states.put(state.getBotId(), state);
|
||||
log.info("pending intent 저장 - botId: {}, intentType: {}, missing: {}",
|
||||
state.getBotId(), state.getIntentType(), state.getMissingParams());
|
||||
}
|
||||
|
||||
public void clear(String botId) {
|
||||
if (botId == null || botId.trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
PendingIntentState removed = states.remove(botId);
|
||||
if (removed != null) {
|
||||
log.info("pending intent 삭제 - botId: {}, intentType: {}", botId, removed.getIntentType());
|
||||
}
|
||||
}
|
||||
}
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.service;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.dto.RestAreaParamNormalizationResult;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class RestAreaParamNormalizer {
|
||||
private static final List<String> REST_AREA_INTENTS = Arrays.asList(
|
||||
"GASSTATION",
|
||||
"BRAND_SHOP",
|
||||
"REST_AREA_FOOD_LIST",
|
||||
"REST_AREA_PRICE"
|
||||
);
|
||||
|
||||
public RestAreaParamNormalizationResult normalize(String intentType, Map<String, String> params) {
|
||||
Map<String, String> normalizedParams = new HashMap<>();
|
||||
if (params != null) {
|
||||
normalizedParams.putAll(params);
|
||||
}
|
||||
|
||||
if (!REST_AREA_INTENTS.contains(intentType) || params == null || params.isEmpty()) {
|
||||
return RestAreaParamNormalizationResult.builder()
|
||||
.normalizedParams(normalizedParams)
|
||||
.build();
|
||||
}
|
||||
|
||||
String restAreaName = normalizedParams.get("restAreaName");
|
||||
if (restAreaName != null && !restAreaName.trim().isEmpty()) {
|
||||
String normalizedValue = normalizeRestAreaName(restAreaName);
|
||||
normalizedParams.put("restAreaName", normalizedValue);
|
||||
}
|
||||
|
||||
return RestAreaParamNormalizationResult.builder()
|
||||
.normalizedParams(normalizedParams)
|
||||
.build();
|
||||
}
|
||||
|
||||
private String normalizeRestAreaName(String value) {
|
||||
String normalized = value.trim().replaceAll("\\s+", "");
|
||||
String previous;
|
||||
do {
|
||||
previous = normalized;
|
||||
normalized = normalized.replaceAll("(휴게소|하이웨이파크)$", "");
|
||||
} while (!previous.equals(normalized));
|
||||
return normalized.trim();
|
||||
}
|
||||
}
|
||||
-86
@@ -1,86 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.service;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.intent.FetchOwner;
|
||||
import com.ex.chatbotapi.domain.agent.intent.IntentDefinition;
|
||||
import com.ex.chatbotapi.domain.agent.intent.IntentDefinitionRegistry;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* intent-definitions.yml → OpenAI function(tool) 스키마 생성.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ToolDefinitionService {
|
||||
private final IntentDefinitionRegistry intentDefinitionRegistry;
|
||||
|
||||
public List<Map<String, Object>> listToolDefinitions() {
|
||||
List<Map<String, Object>> tools = new ArrayList<>();
|
||||
for (IntentDefinition definition : intentDefinitionRegistry.all().values()) {
|
||||
if (definition.getFetchOwner() == FetchOwner.NONE) {
|
||||
continue;
|
||||
}
|
||||
tools.add(toOpenAiTool(definition));
|
||||
}
|
||||
return tools;
|
||||
}
|
||||
|
||||
private Map<String, Object> toOpenAiTool(IntentDefinition definition) {
|
||||
Map<String, Object> properties = new LinkedHashMap<>();
|
||||
List<String> required = new ArrayList<>();
|
||||
|
||||
appendParamProperties(properties, required, definition.getRequiredParamDefinitions(), true);
|
||||
appendParamProperties(properties, required, definition.getOptionalParamDefinitions(), false);
|
||||
|
||||
Map<String, Object> parameters = new LinkedHashMap<>();
|
||||
parameters.put("type", "object");
|
||||
parameters.put("properties", properties);
|
||||
parameters.put("required", required);
|
||||
|
||||
Map<String, Object> function = new LinkedHashMap<>();
|
||||
function.put("name", definition.getIntentType());
|
||||
function.put(
|
||||
"description",
|
||||
definition.getDescription()
|
||||
+ " [fetchOwner="
|
||||
+ definition.getFetchOwner()
|
||||
+ ", uiType="
|
||||
+ definition.getUiType()
|
||||
+ "]"
|
||||
);
|
||||
function.put("parameters", parameters);
|
||||
|
||||
Map<String, Object> tool = new LinkedHashMap<>();
|
||||
tool.put("type", "function");
|
||||
tool.put("function", function);
|
||||
return tool;
|
||||
}
|
||||
|
||||
private void appendParamProperties(
|
||||
Map<String, Object> properties,
|
||||
List<String> required,
|
||||
List<IntentDefinition.ParamDefinition> params,
|
||||
boolean markRequired
|
||||
) {
|
||||
if (params == null) {
|
||||
return;
|
||||
}
|
||||
for (IntentDefinition.ParamDefinition param : params) {
|
||||
Map<String, Object> schema = new LinkedHashMap<>();
|
||||
schema.put("type", "string");
|
||||
schema.put("description", param.getDescription());
|
||||
if (param.getExamples() != null && !param.getExamples().isEmpty()) {
|
||||
schema.put("examples", param.getExamples());
|
||||
}
|
||||
properties.put(param.getName(), schema);
|
||||
if (markRequired) {
|
||||
required.add(param.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-147
@@ -1,147 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.service;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.client.KakaoSkillToolClient;
|
||||
import com.ex.chatbotapi.domain.agent.dto.ToolExecuteRequest;
|
||||
import com.ex.chatbotapi.domain.agent.dto.ToolExecuteResponse;
|
||||
import com.ex.chatbotapi.domain.agent.dto.ToolParamPipelineResult;
|
||||
import com.ex.chatbotapi.domain.agent.formatter.LlmDomainDataFormatter;
|
||||
import com.ex.chatbotapi.domain.agent.handler.IntentHandler;
|
||||
import com.ex.chatbotapi.domain.agent.intent.FetchOwner;
|
||||
import com.ex.chatbotapi.domain.agent.intent.IntentDefinition;
|
||||
import com.ex.chatbotapi.domain.agent.intent.IntentDefinitionRegistry;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class ToolExecutionService {
|
||||
private final IntentDefinitionRegistry intentDefinitionRegistry;
|
||||
private final Map<String, IntentHandler> handlerMap;
|
||||
private final Map<String, LlmDomainDataFormatter> llmFormatterMap;
|
||||
private final KakaoSkillToolClient kakaoSkillToolClient;
|
||||
private final ToolParamPipeline toolParamPipeline;
|
||||
|
||||
public ToolExecutionService(
|
||||
IntentDefinitionRegistry intentDefinitionRegistry,
|
||||
List<IntentHandler> handlers,
|
||||
List<LlmDomainDataFormatter> llmFormatters,
|
||||
KakaoSkillToolClient kakaoSkillToolClient,
|
||||
ToolParamPipeline toolParamPipeline
|
||||
) {
|
||||
this.intentDefinitionRegistry = intentDefinitionRegistry;
|
||||
this.handlerMap = handlers.stream()
|
||||
.collect(Collectors.toMap(IntentHandler::getIntentType, Function.identity()));
|
||||
this.llmFormatterMap = llmFormatters.stream()
|
||||
.collect(Collectors.toMap(LlmDomainDataFormatter::getIntentType, Function.identity()));
|
||||
this.kakaoSkillToolClient = kakaoSkillToolClient;
|
||||
this.toolParamPipeline = toolParamPipeline;
|
||||
}
|
||||
|
||||
public ToolExecuteResponse execute(ToolExecuteRequest request) {
|
||||
String toolName = request.getToolName();
|
||||
if (toolName == null || toolName.trim().isEmpty()) {
|
||||
return ToolExecuteResponse.builder()
|
||||
.status(false)
|
||||
.statusMsg("toolName is required")
|
||||
.build();
|
||||
}
|
||||
|
||||
IntentDefinition definition = intentDefinitionRegistry.get(toolName);
|
||||
if (definition == null) {
|
||||
return ToolExecuteResponse.builder()
|
||||
.status(false)
|
||||
.statusMsg("unknown tool: " + toolName)
|
||||
.build();
|
||||
}
|
||||
|
||||
ToolParamPipelineResult pipeline = toolParamPipeline.process(toolName, request.getArguments());
|
||||
if (!pipeline.isValid()) {
|
||||
return ToolExecuteResponse.builder()
|
||||
.status(false)
|
||||
.statusMsg(pipeline.isNeedsClarification()
|
||||
? "clarification required"
|
||||
: "parameter validation failed")
|
||||
.intentType(toolName)
|
||||
.fetchOwner(definition.getFetchOwner().name())
|
||||
.uiType(definition.getUiType())
|
||||
.params(pipeline.getParams())
|
||||
.needsClarification(pipeline.isNeedsClarification())
|
||||
.clarificationQuestion(pipeline.getClarificationQuestion())
|
||||
.missingParams(pipeline.getMissingParams())
|
||||
.icCandidates(pipeline.getIcCandidates())
|
||||
.build();
|
||||
}
|
||||
|
||||
Map<String, String> params = pipeline.getParams();
|
||||
FetchOwner fetchOwner = definition.getFetchOwner();
|
||||
Map<String, Object> domainData;
|
||||
|
||||
try {
|
||||
if (fetchOwner == FetchOwner.WAS) {
|
||||
domainData = fetchWasDomainData(toolName, params);
|
||||
} else if (fetchOwner == FetchOwner.WEB) {
|
||||
domainData = kakaoSkillToolClient.fetchDataPortal(
|
||||
toolName,
|
||||
params,
|
||||
request.getUserInput()
|
||||
);
|
||||
} else {
|
||||
return ToolExecuteResponse.builder()
|
||||
.status(false)
|
||||
.statusMsg("tool not executable: " + toolName)
|
||||
.intentType(toolName)
|
||||
.fetchOwner(fetchOwner.name())
|
||||
.uiType(definition.getUiType())
|
||||
.params(params)
|
||||
.build();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("tool execution failed - tool: {}, error: {}", toolName, e.getMessage(), e);
|
||||
return ToolExecuteResponse.builder()
|
||||
.status(false)
|
||||
.statusMsg("tool execution failed: " + e.getMessage())
|
||||
.intentType(toolName)
|
||||
.fetchOwner(fetchOwner.name())
|
||||
.uiType(definition.getUiType())
|
||||
.params(params)
|
||||
.build();
|
||||
}
|
||||
|
||||
boolean ok = domainData != null && Boolean.TRUE.equals(domainData.get("status"));
|
||||
String statusMsg = ok
|
||||
? "정상응답"
|
||||
: String.valueOf(domainData != null ? domainData.get("statusMsg") : "no data");
|
||||
|
||||
return ToolExecuteResponse.builder()
|
||||
.status(ok)
|
||||
.statusMsg(statusMsg)
|
||||
.intentType(toolName)
|
||||
.fetchOwner(fetchOwner.name())
|
||||
.uiType(definition.getUiType())
|
||||
.params(params)
|
||||
.domainData(domainData)
|
||||
.build();
|
||||
}
|
||||
|
||||
private Map<String, Object> fetchWasDomainData(String intentType, Map<String, String> params) {
|
||||
IntentHandler handler = handlerMap.get(intentType);
|
||||
if (handler == null) {
|
||||
Map<String, Object> fallback = new HashMap<>();
|
||||
fallback.put("status", false);
|
||||
fallback.put("statusMsg", "WAS handler not registered: " + intentType);
|
||||
return fallback;
|
||||
}
|
||||
Map<String, Object> domainData = handler.fetchDomainData(params);
|
||||
LlmDomainDataFormatter formatter = llmFormatterMap.get(intentType);
|
||||
if (formatter != null && domainData != null) {
|
||||
formatter.enrich(domainData);
|
||||
}
|
||||
return domainData;
|
||||
}
|
||||
}
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.service;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.dto.CarNoParamNormalizationResult;
|
||||
import com.ex.chatbotapi.domain.agent.dto.IcParamNormalizationResult;
|
||||
import com.ex.chatbotapi.domain.agent.dto.IntentValidationResult;
|
||||
import com.ex.chatbotapi.domain.agent.dto.RestAreaParamNormalizationResult;
|
||||
import com.ex.chatbotapi.domain.agent.dto.ToolParamPipelineResult;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* LlmAgentRouterService와 동일한 파라미터 검증·정규화 체인 (tool execute용).
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ToolParamPipeline {
|
||||
private final IntentAnalysisParamNormalizer intentAnalysisParamNormalizer;
|
||||
private final IntentParamValidator intentParamValidator;
|
||||
private final CarNoParamNormalizer carNoParamNormalizer;
|
||||
private final IcParamNormalizer icParamNormalizer;
|
||||
private final RestAreaParamNormalizer restAreaParamNormalizer;
|
||||
|
||||
public ToolParamPipelineResult process(String intentType, Map<String, ?> rawArguments) {
|
||||
Map<String, String> params = intentAnalysisParamNormalizer.toStringMap(rawArguments);
|
||||
|
||||
IntentValidationResult validation = intentParamValidator.validate(intentType, params);
|
||||
if (!validation.isValid()) {
|
||||
return ToolParamPipelineResult.clarify(
|
||||
validation.getCleanedParams(),
|
||||
validation.getClarificationQuestion(),
|
||||
validation.getMissingParams()
|
||||
);
|
||||
}
|
||||
|
||||
CarNoParamNormalizationResult carNoResult = carNoParamNormalizer.normalize(
|
||||
intentType,
|
||||
validation.getCleanedParams()
|
||||
);
|
||||
if (!carNoResult.isValid()) {
|
||||
return ToolParamPipelineResult.clarify(
|
||||
carNoResult.getNormalizedParams(),
|
||||
carNoResult.getClarificationQuestion(),
|
||||
java.util.Collections.singletonList("carNo")
|
||||
);
|
||||
}
|
||||
|
||||
IcParamNormalizationResult icResult = icParamNormalizer.normalize(
|
||||
intentType,
|
||||
carNoResult.getNormalizedParams()
|
||||
);
|
||||
if (icResult.isAmbiguous()) {
|
||||
return ToolParamPipelineResult.icAmbiguous(
|
||||
icResult.getNormalizedParams(),
|
||||
icResult.getClarificationQuestion(),
|
||||
icResult.getCandidates()
|
||||
);
|
||||
}
|
||||
|
||||
RestAreaParamNormalizationResult restAreaResult = restAreaParamNormalizer.normalize(
|
||||
intentType,
|
||||
icResult.getNormalizedParams()
|
||||
);
|
||||
|
||||
return ToolParamPipelineResult.success(restAreaResult.getNormalizedParams());
|
||||
}
|
||||
}
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.fare.api;
|
||||
|
||||
import com.ex.chatbotapi.domain.fare.service.FareService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 통행요금 관련 API 컨트롤러
|
||||
* HTTP 요청 처리만 담당하고, 비즈니스 로직은 FareService에 위임
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
public class FareApiController {
|
||||
|
||||
private final FareService fareService;
|
||||
|
||||
/**
|
||||
* Description : 환불내역 조회 메소드
|
||||
*/
|
||||
@GetMapping("/api/fare/refund")
|
||||
public ResponseEntity<Map<String, Object>> queryRefund(String carNo) {
|
||||
log.info("API 호출 : queryRefund(GET) - carNo: {}", carNo);
|
||||
return ResponseEntity.ok(fareService.getRefundInfo(carNo));
|
||||
}
|
||||
|
||||
/**
|
||||
* Description : 미납내역 조회 메소드
|
||||
*/
|
||||
@GetMapping("/api/fare/unpaid")
|
||||
public ResponseEntity<Map<String, Object>> queryUnpaid(String carNo) {
|
||||
log.info("API 호출 : queryUnpaid(GET) - carNo: {}", carNo);
|
||||
return ResponseEntity.ok(fareService.getUnpaidInfo(carNo));
|
||||
}
|
||||
|
||||
/**
|
||||
* Description : 영업소 전화번호 조회 메소드
|
||||
*/
|
||||
@GetMapping("/api/call/icTel")
|
||||
public ResponseEntity<Map<String, Object>> queryIcTel(String icName) {
|
||||
log.info("API 호출 : queryIcTel(GET) - icName: {}", icName);
|
||||
return ResponseEntity.ok(fareService.getIcTelInfo(icName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Description : 통행요금 조회 메소드
|
||||
*/
|
||||
@GetMapping("/api/fare/searchFare")
|
||||
public ResponseEntity<Map<String, Object>> querySearchFare(String fromIc, String toIc) {
|
||||
log.info("API 호출 : querySearchFare(GET) - fromIc: {}, toIc: {}", fromIc, toIc);
|
||||
return ResponseEntity.ok(fareService.searchFare(fromIc, toIc));
|
||||
}
|
||||
}
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.fare.formatter;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.formatter.LlmDomainDataFormatter;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* FARE_REFUND — 환불 가능 통행료 조회 결과 요약.
|
||||
*/
|
||||
@Component
|
||||
public class FareRefundLlmFormatter implements LlmDomainDataFormatter {
|
||||
|
||||
@Override
|
||||
public String getIntentType() {
|
||||
return "FARE_REFUND";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enrich(Map<String, Object> domainData) {
|
||||
if (domainData == null || domainData.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object st = domainData.get("status");
|
||||
boolean ok = st instanceof Boolean && (Boolean) st;
|
||||
String carNo = domainData.get("carNo") != null ? String.valueOf(domainData.get("carNo")) : "(차량번호 없음)";
|
||||
|
||||
if (!ok) {
|
||||
domainData.put("llmSummary",
|
||||
"차량번호 " + carNo + " 에 대해 환불 대상 통행료가 조회되지 않았거나, "
|
||||
+ "일부 노선에서는 환불 내역 확인이 제한될 수 있습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
domainData.put("llmSummary",
|
||||
"차량번호 " + carNo + " 에 환불 가능한 통행료 정보가 조회되었습니다.\n"
|
||||
+ "환불 신청 및 상세 내역은 한국도로공사 공식 홈페이지 또는 고객센터(1588-2504)를 이용해 주세요.");
|
||||
}
|
||||
}
|
||||
-105
@@ -1,105 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.fare.formatter;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.formatter.LlmDomainDataFormatter;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* FARE_SEARCH — 통행요금 조회 결과를 LLM용 요약 문자열로 정리한다.
|
||||
*/
|
||||
@Component
|
||||
public class FareSearchLlmFormatter implements LlmDomainDataFormatter {
|
||||
|
||||
@Override
|
||||
public String getIntentType() {
|
||||
return "FARE_SEARCH";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enrich(Map<String, Object> domainData) {
|
||||
if (domainData == null || domainData.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object st = domainData.get("status");
|
||||
boolean ok = st instanceof Boolean && (Boolean) st;
|
||||
|
||||
if (!ok) {
|
||||
String msg = domainData.get("statusMsg") != null
|
||||
? String.valueOf(domainData.get("statusMsg"))
|
||||
: "통행요금 조회에 실패했습니다.";
|
||||
domainData.put("llmSummary", msg);
|
||||
return;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> fare = (Map<String, Object>) domainData.get("fareData");
|
||||
if (fare == null || fare.isEmpty()) {
|
||||
domainData.put("llmSummary", "통행요금 조회는 성공했으나 fareData가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
String type = String.valueOf(fare.getOrDefault("type", ""));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
switch (type) {
|
||||
case "exact":
|
||||
sb.append("구간 통행요금(정액 산정 가능):\n");
|
||||
sb.append("- 출발: ").append(fare.get("fromIc")).append(", 도착: ").append(fare.get("toIc")).append("\n");
|
||||
appendVehicleClassLines(sb, fare);
|
||||
break;
|
||||
case "open_both":
|
||||
sb.append("개방식 영업소(출발·도착 모두 개방식으로 판단):\n");
|
||||
sb.append("- 기준 영업소: ").append(fare.get("icName")).append("\n");
|
||||
sb.append("- 개방식 요금은 해당 영업소 통과 기준 단일 구간 요금입니다.\n");
|
||||
appendVehicleClassLines(sb, fare);
|
||||
break;
|
||||
case "open_from":
|
||||
sb.append("개방식 영업소(출발이 개방식):\n");
|
||||
sb.append("- 출발 영업소: ").append(fare.get("icName")).append(", 목적지 쪽 영업소: ")
|
||||
.append(fare.get("toIc")).append("\n");
|
||||
Object outs = fare.get("outIcs");
|
||||
if (outs instanceof List && !((List<?>) outs).isEmpty()) {
|
||||
sb.append("- 출구 후보(일부): ").append(outs).append("\n");
|
||||
}
|
||||
appendVehicleClassLines(sb, fare);
|
||||
break;
|
||||
case "open_to":
|
||||
sb.append("개방식 영업소(도착이 개방식):\n");
|
||||
sb.append("- 도착 영업소: ").append(fare.get("icName")).append(", 출발 쪽 영업소: ")
|
||||
.append(fare.get("fromIc")).append("\n");
|
||||
Object outs2 = fare.get("outIcs");
|
||||
if (outs2 instanceof List && !((List<?>) outs2).isEmpty()) {
|
||||
sb.append("- 출구 후보(일부): ").append(outs2).append("\n");
|
||||
}
|
||||
appendVehicleClassLines(sb, fare);
|
||||
break;
|
||||
default:
|
||||
sb.append("통행요금 조회(type=").append(type).append("):\n");
|
||||
appendVehicleClassLines(sb, fare);
|
||||
}
|
||||
|
||||
domainData.put("llmSummary", sb.toString().trim());
|
||||
}
|
||||
|
||||
private static void appendVehicleClassLines(StringBuilder sb, Map<String, Object> fare) {
|
||||
sb.append("- 1종: ").append(won(fare.get("fare1")))
|
||||
.append(" (경차 1종: ").append(won(fare.get("fare1Small"))).append(")\n");
|
||||
sb.append("- 2종: ").append(won(fare.get("fare2"))).append("\n");
|
||||
sb.append("- 3종: ").append(won(fare.get("fare3"))).append("\n");
|
||||
sb.append("- 4종: ").append(won(fare.get("fare4"))).append("\n");
|
||||
sb.append("- 5종: ").append(won(fare.get("fare5"))).append("\n");
|
||||
}
|
||||
|
||||
private static String won(Object v) {
|
||||
if (v == null) {
|
||||
return "-";
|
||||
}
|
||||
if (v instanceof Number) {
|
||||
return ((Number) v).intValue() + "원";
|
||||
}
|
||||
return v + "원";
|
||||
}
|
||||
}
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.fare.formatter;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.formatter.LlmDomainDataFormatter;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* FARE_UNPAID — 미납 통행료 조회 결과 요약.
|
||||
*/
|
||||
@Component
|
||||
public class FareUnpaidLlmFormatter implements LlmDomainDataFormatter {
|
||||
|
||||
@Override
|
||||
public String getIntentType() {
|
||||
return "FARE_UNPAID";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enrich(Map<String, Object> domainData) {
|
||||
if (domainData == null || domainData.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object st = domainData.get("status");
|
||||
boolean ok = st instanceof Boolean && (Boolean) st;
|
||||
String carNo = domainData.get("carNo") != null ? String.valueOf(domainData.get("carNo")) : "(차량번호 없음)";
|
||||
long sum = toLong(domainData.get("sumAmt"));
|
||||
|
||||
if (!ok) {
|
||||
String message = domainData.get("statusMsg") != null
|
||||
? String.valueOf(domainData.get("statusMsg"))
|
||||
: "미납 통행료 조회에 실패했습니다.";
|
||||
domainData.put("llmSummary", message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sum < 100) {
|
||||
domainData.put("llmSummary",
|
||||
"차량번호 " + carNo + " 기준 조회된 미납 통행료는 없습니다.\n"
|
||||
+ "고객에게 미납금액이 없다고 명확히 안내하세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
domainData.put("llmSummary",
|
||||
"차량번호 " + carNo + " 기준 미납 통행료가 조회되었습니다.\n"
|
||||
+ "총 미납금액은 " + sum + "원입니다.\n"
|
||||
+ "상세 납부는 하이패스/홈페이지 등 공식 채널을 이용해야 합니다.");
|
||||
}
|
||||
|
||||
private static long toLong(Object o) {
|
||||
if (o == null) {
|
||||
return 0L;
|
||||
}
|
||||
if (o instanceof Number) {
|
||||
return ((Number) o).longValue();
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(String.valueOf(o));
|
||||
} catch (NumberFormatException e) {
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
}
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.fare.formatter;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.formatter.LlmDomainDataFormatter;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* IC_TEL — 영업소 대표 전화번호 조회 결과 요약.
|
||||
*/
|
||||
@Component
|
||||
public class IcTelLlmFormatter implements LlmDomainDataFormatter {
|
||||
|
||||
@Override
|
||||
public String getIntentType() {
|
||||
return "IC_TEL";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enrich(Map<String, Object> domainData) {
|
||||
if (domainData == null || domainData.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object st = domainData.get("status");
|
||||
boolean ok = st instanceof Boolean && (Boolean) st;
|
||||
|
||||
if (!ok) {
|
||||
String msg = domainData.get("statusMsg") != null
|
||||
? String.valueOf(domainData.get("statusMsg"))
|
||||
: "영업소 연락처를 조회하지 못했습니다.";
|
||||
domainData.put("llmSummary", msg);
|
||||
return;
|
||||
}
|
||||
|
||||
String icName = domainData.get("icName") != null ? String.valueOf(domainData.get("icName")) : "";
|
||||
String tel = domainData.get("tel") != null ? String.valueOf(domainData.get("tel")) : "";
|
||||
String code = domainData.get("tolofCd") != null ? String.valueOf(domainData.get("tolofCd")) : "";
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("고속도로 영업소 연락처 조회 결과:\n");
|
||||
sb.append("- 영업소명: ").append(icName).append("\n");
|
||||
sb.append("- 대표전화: ").append(tel).append("\n");
|
||||
if (!code.isEmpty()) {
|
||||
sb.append("- 영업소코드(참고): ").append(code);
|
||||
}
|
||||
domainData.put("llmSummary", sb.toString().trim());
|
||||
}
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.fare.handler;
|
||||
|
||||
import com.ex.chatbotapi.domain.fare.service.FareService;
|
||||
import com.ex.chatbotapi.domain.agent.handler.IntentHandler;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class FareRefundHandler implements IntentHandler {
|
||||
|
||||
private final FareService fareService;
|
||||
|
||||
@Override
|
||||
public String getIntentType() {
|
||||
return "FARE_REFUND";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> fetchDomainData(Map<String, String> params) {
|
||||
return fareService.getRefundInfo(params.get("carNo"));
|
||||
}
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.fare.handler;
|
||||
|
||||
import com.ex.chatbotapi.domain.fare.service.FareService;
|
||||
import com.ex.chatbotapi.domain.agent.handler.IntentHandler;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class FareSearchHandler implements IntentHandler {
|
||||
|
||||
private final FareService fareService;
|
||||
|
||||
@Override
|
||||
public String getIntentType() {
|
||||
return "FARE_SEARCH";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> fetchDomainData(Map<String, String> params) {
|
||||
return fareService.searchFare(params.get("fromIc"), params.get("toIc"));
|
||||
}
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.fare.handler;
|
||||
|
||||
import com.ex.chatbotapi.domain.fare.service.FareService;
|
||||
import com.ex.chatbotapi.domain.agent.handler.IntentHandler;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class FareUnpaidHandler implements IntentHandler {
|
||||
|
||||
private final FareService fareService;
|
||||
|
||||
@Override
|
||||
public String getIntentType() {
|
||||
return "FARE_UNPAID";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> fetchDomainData(Map<String, String> params) {
|
||||
return fareService.getUnpaidInfo(params.get("carNo"));
|
||||
}
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.fare.handler;
|
||||
|
||||
import com.ex.chatbotapi.domain.fare.service.FareService;
|
||||
import com.ex.chatbotapi.domain.agent.handler.IntentHandler;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class IcTelHandler implements IntentHandler {
|
||||
|
||||
private final FareService fareService;
|
||||
|
||||
@Override
|
||||
public String getIntentType() {
|
||||
return "IC_TEL";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> fetchDomainData(Map<String, String> params) {
|
||||
return fareService.getIcTelInfo(params.get("icName"));
|
||||
}
|
||||
}
|
||||
-171
File diff suppressed because one or more lines are too long
-163
@@ -1,163 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.fare.service;
|
||||
|
||||
import org.bson.Document;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MongoDB trs_amt.amt / sum 배열에서 차종별·구간별 요금과 웹용 breakdown을 추출한다.
|
||||
*/
|
||||
final class FareAmountHelper {
|
||||
|
||||
private static final String[] VEHICLE_LABELS = {"1종", "2종", "3종", "4종", "5종", "경차"};
|
||||
private static final String[] VEHICLE_DESCRIPTIONS = {
|
||||
"승용차, 소형승합차/화물차, 경차",
|
||||
"중형승합차, 중형화물차",
|
||||
"대형승합차, 2축 대형화물차",
|
||||
"3축 대형화물차",
|
||||
"4축 이상 특수 화물차",
|
||||
"1종(경차)"
|
||||
};
|
||||
|
||||
private FareAmountHelper() {
|
||||
}
|
||||
|
||||
static void applyExactFareAmounts(Map<String, Object> fareData, Document trsDoc) {
|
||||
List<Integer> totalList = firstNonNullList(
|
||||
readIntegerList(trsDoc.get("sum")),
|
||||
readAmtList(trsDoc, "sum")
|
||||
);
|
||||
if (totalList == null || totalList.size() < 6) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<Integer> kecList = firstNonNullList(
|
||||
readAmtList(trsDoc, "00"),
|
||||
totalList
|
||||
);
|
||||
List<Integer> privateList = subtractLists(totalList, kecList);
|
||||
|
||||
putLegacyFareFields(fareData, totalList);
|
||||
putSectionFareFields(fareData, "kec", kecList);
|
||||
putSectionFareFields(fareData, "private", privateList);
|
||||
|
||||
fareData.put("webBreakdown", buildWebBreakdown(
|
||||
String.valueOf(fareData.get("fromIc")),
|
||||
String.valueOf(fareData.get("toIc")),
|
||||
totalList,
|
||||
kecList,
|
||||
privateList
|
||||
));
|
||||
}
|
||||
|
||||
private static Map<String, Object> buildWebBreakdown(
|
||||
String fromIc,
|
||||
String toIc,
|
||||
List<Integer> totalList,
|
||||
List<Integer> kecList,
|
||||
List<Integer> privateList
|
||||
) {
|
||||
Map<String, Object> breakdown = new HashMap<>();
|
||||
breakdown.put("fromIc", fromIc);
|
||||
breakdown.put("toIc", toIc);
|
||||
breakdown.put("routeLabel", fromIc + " → " + toIc);
|
||||
breakdown.put("introText",
|
||||
"요청하신 경로의 통행요금입니다. 도로공사 구간 요금·민자도로 구간 요금 조회 결과입니다.");
|
||||
breakdown.put("totalFare", totalList.get(0));
|
||||
breakdown.put("kecFare", kecList.get(0));
|
||||
breakdown.put("privateFare", privateList.get(0));
|
||||
breakdown.put("compactTotalFare", totalList.get(5));
|
||||
breakdown.put("compactKecFare", kecList.get(5));
|
||||
breakdown.put("compactPrivateFare", privateList.get(5));
|
||||
|
||||
List<Map<String, Object>> vehicles = new ArrayList<>();
|
||||
for (int i = 0; i < 5; i++) {
|
||||
vehicles.add(vehicleEntry(i, totalList, kecList, privateList, false));
|
||||
}
|
||||
vehicles.add(vehicleEntry(5, totalList, kecList, privateList, true));
|
||||
breakdown.put("vehicles", vehicles);
|
||||
return breakdown;
|
||||
}
|
||||
|
||||
private static Map<String, Object> vehicleEntry(
|
||||
int index,
|
||||
List<Integer> totalList,
|
||||
List<Integer> kecList,
|
||||
List<Integer> privateList,
|
||||
boolean compact
|
||||
) {
|
||||
Map<String, Object> vehicle = new HashMap<>();
|
||||
vehicle.put("typeLabel", VEHICLE_LABELS[index]);
|
||||
vehicle.put("description", VEHICLE_DESCRIPTIONS[index]);
|
||||
vehicle.put("total", totalList.get(index));
|
||||
vehicle.put("kec", kecList.get(index));
|
||||
vehicle.put("private", privateList.get(index));
|
||||
vehicle.put("compact", compact);
|
||||
return vehicle;
|
||||
}
|
||||
|
||||
private static void putLegacyFareFields(Map<String, Object> fareData, List<Integer> totalList) {
|
||||
fareData.put("fare1", totalList.get(0));
|
||||
fareData.put("fare2", totalList.get(1));
|
||||
fareData.put("fare3", totalList.get(2));
|
||||
fareData.put("fare4", totalList.get(3));
|
||||
fareData.put("fare5", totalList.get(4));
|
||||
fareData.put("fare1Small", totalList.get(5));
|
||||
}
|
||||
|
||||
private static void putSectionFareFields(
|
||||
Map<String, Object> fareData,
|
||||
String prefix,
|
||||
List<Integer> values
|
||||
) {
|
||||
fareData.put(prefix + "Fare1", values.get(0));
|
||||
fareData.put(prefix + "Fare2", values.get(1));
|
||||
fareData.put(prefix + "Fare3", values.get(2));
|
||||
fareData.put(prefix + "Fare4", values.get(3));
|
||||
fareData.put(prefix + "Fare5", values.get(4));
|
||||
fareData.put(prefix + "Fare1Small", values.get(5));
|
||||
}
|
||||
|
||||
private static List<Integer> readAmtList(Document trsDoc, String key) {
|
||||
Object amtObj = trsDoc.get("amt");
|
||||
if (!(amtObj instanceof Document)) {
|
||||
return null;
|
||||
}
|
||||
return readIntegerList(((Document) amtObj).get(key));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static List<Integer> readIntegerList(Object value) {
|
||||
if (!(value instanceof List)) {
|
||||
return null;
|
||||
}
|
||||
List<Integer> result = new ArrayList<>();
|
||||
for (Object item : (List<?>) value) {
|
||||
if (item instanceof Number) {
|
||||
result.add(((Number) item).intValue());
|
||||
}
|
||||
}
|
||||
return result.isEmpty() ? null : result;
|
||||
}
|
||||
|
||||
private static List<Integer> subtractLists(List<Integer> totalList, List<Integer> kecList) {
|
||||
List<Integer> privateList = new ArrayList<>(totalList.size());
|
||||
for (int i = 0; i < totalList.size(); i++) {
|
||||
privateList.add(Math.max(0, totalList.get(i) - kecList.get(i)));
|
||||
}
|
||||
return privateList;
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
private static List<Integer> firstNonNullList(List<Integer>... lists) {
|
||||
for (List<Integer> list : lists) {
|
||||
if (list != null && !list.isEmpty()) {
|
||||
return list;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
-322
@@ -1,322 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.fare.service;
|
||||
|
||||
import com.ex.chatbotapi.domain.fare.repository.FareRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.bson.Document;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 통행요금 관련 비즈니스 로직 처리 서비스
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FareService {
|
||||
|
||||
private final FareRepository fareRepository;
|
||||
|
||||
/**
|
||||
* 환불내역 조회
|
||||
*/
|
||||
public Map<String, Object> getRefundInfo(String carNo) {
|
||||
log.info("{} 차량 환불내역 조회 시작", carNo);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
|
||||
try {
|
||||
List<Map<String, Object>> list = fareRepository.findRefundByCarNo(carNo);
|
||||
int refundCnt = list.size();
|
||||
|
||||
if (refundCnt == 0) {
|
||||
log.info("{} 차량 환불내역 존재하지 않음", carNo);
|
||||
response.put("status", false);
|
||||
response.put("statusMsg", "환불 내역이 존재하지 않습니다.");
|
||||
response.put("carNo", carNo);
|
||||
} else {
|
||||
log.info("{} 차량 환불내역이 존재합니다.", carNo);
|
||||
response.put("status", true);
|
||||
response.put("statusMsg", "환불 내역이 존재합니다.");
|
||||
response.put("carNo", carNo);
|
||||
response.put("refundData", list.get(0));
|
||||
}
|
||||
|
||||
return response;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("환불내역 조회 에러 : {}", e.getMessage(), e);
|
||||
response.put("status", false);
|
||||
response.put("statusMsg", "서비스에 일시적인 장애가 발생하였습니다.");
|
||||
response.put("errorMsg", e.getMessage());
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 미납내역 조회
|
||||
*/
|
||||
public Map<String, Object> getUnpaidInfo(String carNo) {
|
||||
log.info("{} 차량 미납내역 조회 시작", carNo);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
|
||||
try {
|
||||
List<Map<String, Object>> list = fareRepository.findUnpaidByCarNo(carNo);
|
||||
|
||||
long sumAmt = 0;
|
||||
for (Map<String, Object> map : list) {
|
||||
if (map.get("USER_NAME") != null) {
|
||||
sumAmt += ((java.math.BigDecimal) map.get("UNPAID_AMT")).toBigInteger().longValue();
|
||||
log.info("미납금액 : {}", map.get("UNPAID_AMT"));
|
||||
|
||||
map.remove("COL_WILL_AMT");
|
||||
map.remove("COL_EXTRA_WILL_AMT");
|
||||
map.remove("WORK_NO");
|
||||
map.remove("SER_NO");
|
||||
map.remove("LANETYPE");
|
||||
map.remove("OUT_IC_CODE");
|
||||
}
|
||||
}
|
||||
|
||||
if (sumAmt < 100) {
|
||||
log.info("{} 차량 미납내역 존재하지 않음", carNo);
|
||||
response.put("status", true);
|
||||
response.put("statusMsg", "미납 내역이 존재하지 않습니다.");
|
||||
response.put("carNo", carNo);
|
||||
response.put("sumAmt", sumAmt);
|
||||
response.put("hasUnpaid", false);
|
||||
} else {
|
||||
log.info("{} 차량 미납내역이 존재합니다. 총 금액: {}", carNo, sumAmt);
|
||||
response.put("status", true);
|
||||
response.put("statusMsg", "미납 내역이 존재합니다.");
|
||||
response.put("carNo", carNo);
|
||||
response.put("sumAmt", sumAmt);
|
||||
response.put("hasUnpaid", true);
|
||||
response.put("unpaidList", list);
|
||||
}
|
||||
|
||||
return response;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("미납내역 조회 에러 : {}", e.getMessage(), e);
|
||||
response.put("status", false);
|
||||
response.put("statusMsg", "서비스에 일시적인 장애가 발생하였습니다.");
|
||||
response.put("errorMsg", e.getMessage());
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 영업소 전화번호 조회
|
||||
*/
|
||||
public Map<String, Object> getIcTelInfo(String icName) {
|
||||
log.info("{} 영업소 연락처 조회를 시작합니다", icName);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
|
||||
try {
|
||||
if (icName == null || icName.isEmpty()) {
|
||||
log.warn("영업소 이름이 없습니다");
|
||||
response.put("status", false);
|
||||
response.put("statusMsg", "영업소 이름이 없습니다.");
|
||||
return response;
|
||||
}
|
||||
|
||||
Map<String, Object> icTelData = fareRepository.findIcTelByName(icName);
|
||||
|
||||
if (icTelData == null) {
|
||||
log.info("{} 영업소 정보를 찾을 수 없습니다.", icName);
|
||||
response.put("status", false);
|
||||
response.put("statusMsg", icName + " 영업소는 제가 모르는 영업소입니다.");
|
||||
return response;
|
||||
}
|
||||
|
||||
String tolofCd = (String) icTelData.get("TOLOF_CD");
|
||||
String korIcName = (String) icTelData.get("KOR_TOLOF_NM");
|
||||
String tel = (String) icTelData.get("TOLOF_TELNO");
|
||||
|
||||
log.info("영업소 정보 조회 성공: 코드={}, 이름={}, 전화번호={}", tolofCd, korIcName, tel);
|
||||
|
||||
if (tel == null || tel.equals("")) {
|
||||
log.warn("{} 영업소는 전화번호 정보가 없습니다.", icName);
|
||||
response.put("status", false);
|
||||
response.put("statusMsg", icName + " 영업소는 전화번호 정보가 없습니다.");
|
||||
return response;
|
||||
}
|
||||
|
||||
log.info("{} 영업소 연락처 조회 성공", icName);
|
||||
response.put("status", true);
|
||||
response.put("statusMsg", "정상응답");
|
||||
response.put("icName", korIcName);
|
||||
response.put("tel", tel);
|
||||
response.put("tolofCd", tolofCd);
|
||||
|
||||
return response;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("영업소 전화번호 조회 에러 : {}", e.getMessage(), e);
|
||||
response.put("status", false);
|
||||
response.put("statusMsg", "서비스에 일시적인 장애가 발생하였습니다.");
|
||||
response.put("errorMsg", e.getMessage());
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 통행요금 조회
|
||||
*/
|
||||
public Map<String, Object> searchFare(String fromIc, String toIc) {
|
||||
log.info("{}와 {} 간의 요금을 계산합니다", fromIc, toIc);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
|
||||
try {
|
||||
Document fromIcInfo = fareRepository.findIcInfoByName(fromIc);
|
||||
|
||||
if (fromIcInfo == null) {
|
||||
log.warn("{} 영업소를 찾을 수 없습니다", fromIc);
|
||||
response.put("status", false);
|
||||
response.put("statusMsg", fromIc + " : 제가 알지 못하는 영업소 입니다.");
|
||||
return response;
|
||||
}
|
||||
|
||||
Document toIcInfo = fareRepository.findIcInfoByName(toIc);
|
||||
|
||||
if (toIcInfo == null) {
|
||||
log.warn("{} 영업소를 찾을 수 없습니다", toIc);
|
||||
response.put("status", false);
|
||||
response.put("statusMsg", toIc + " : 제가 알지 못하는 영업소 입니다.");
|
||||
return response;
|
||||
}
|
||||
|
||||
String fromCode = fromIcInfo.get("ic_code").toString();
|
||||
String toCode = toIcInfo.get("ic_code").toString();
|
||||
|
||||
Document trsUnique = fareRepository.findFareByIcNames(fromIc, toIc);
|
||||
|
||||
if (trsUnique != null && !fromIc.equals("대구") && !toIc.equals("대구")) {
|
||||
Map<String, Object> fareData = new HashMap<>();
|
||||
fareData.put("type", "exact");
|
||||
fareData.put("fromIc", fromIc);
|
||||
fareData.put("toIc", toIc);
|
||||
FareAmountHelper.applyExactFareAmounts(fareData, trsUnique);
|
||||
|
||||
response.put("status", true);
|
||||
response.put("statusMsg", "정상응답");
|
||||
response.put("fareData", fareData);
|
||||
return response;
|
||||
}
|
||||
|
||||
int fromCodeInt = Integer.parseInt(fromCode);
|
||||
int toCodeInt = Integer.parseInt(toCode);
|
||||
|
||||
Map<String, Object> icTelMap = fareRepository.getIcTelMap();
|
||||
|
||||
if (fromCodeInt < 100 && toCodeInt < 100) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> fromIcTel = (Map<String, Object>) icTelMap.get(fromIc);
|
||||
if (fromIcTel == null || fromIcTel.get("amt") == null) {
|
||||
response.put("status", false);
|
||||
response.put("statusMsg", "해당구간의 통행요금을 찾지 못했습니다.");
|
||||
return response;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Integer> fromAmt = (List<Integer>) fromIcTel.get("amt");
|
||||
Map<String, Object> fareData = new HashMap<>();
|
||||
fareData.put("type", "open_both");
|
||||
fareData.put("icName", fromIc);
|
||||
fareData.put("fare1", fromAmt.get(0));
|
||||
fareData.put("fare1Small", fromAmt.get(5));
|
||||
fareData.put("fare2", fromAmt.get(1));
|
||||
fareData.put("fare3", fromAmt.get(2));
|
||||
fareData.put("fare4", fromAmt.get(3));
|
||||
fareData.put("fare5", fromAmt.get(4));
|
||||
|
||||
response.put("status", true);
|
||||
response.put("statusMsg", "정상응답");
|
||||
response.put("fareData", fareData);
|
||||
return response;
|
||||
}
|
||||
else if (fromCodeInt < 100) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> fromIcTel = (Map<String, Object>) icTelMap.get(fromIc);
|
||||
if (fromIcTel == null || fromIcTel.get("amt") == null) {
|
||||
response.put("status", false);
|
||||
response.put("statusMsg", "해당구간의 통행요금을 찾지 못했습니다.");
|
||||
return response;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Integer> fromAmt = (List<Integer>) fromIcTel.get("amt");
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> outIcs = (List<String>) fromIcTel.get("out");
|
||||
|
||||
Map<String, Object> fareData = new HashMap<>();
|
||||
fareData.put("type", "open_from");
|
||||
fareData.put("icName", fromIc);
|
||||
fareData.put("toIc", toIc);
|
||||
fareData.put("fare1", fromAmt.get(0));
|
||||
fareData.put("fare1Small", fromAmt.get(5));
|
||||
fareData.put("fare2", fromAmt.get(1));
|
||||
fareData.put("fare3", fromAmt.get(2));
|
||||
fareData.put("fare4", fromAmt.get(3));
|
||||
fareData.put("fare5", fromAmt.get(4));
|
||||
fareData.put("outIcs", outIcs);
|
||||
|
||||
response.put("status", true);
|
||||
response.put("statusMsg", "정상응답");
|
||||
response.put("fareData", fareData);
|
||||
return response;
|
||||
}
|
||||
else if (toCodeInt < 100) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> toIcTel = (Map<String, Object>) icTelMap.get(toIc);
|
||||
if (toIcTel == null || toIcTel.get("amt") == null) {
|
||||
response.put("status", false);
|
||||
response.put("statusMsg", "해당구간의 통행요금을 찾지 못했습니다.");
|
||||
return response;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Integer> toAmt = (List<Integer>) toIcTel.get("amt");
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> outIcs = (List<String>) toIcTel.get("out");
|
||||
|
||||
Map<String, Object> fareData = new HashMap<>();
|
||||
fareData.put("type", "open_to");
|
||||
fareData.put("icName", toIc);
|
||||
fareData.put("fromIc", fromIc);
|
||||
fareData.put("fare1", toAmt.get(0));
|
||||
fareData.put("fare1Small", toAmt.get(5));
|
||||
fareData.put("fare2", toAmt.get(1));
|
||||
fareData.put("fare3", toAmt.get(2));
|
||||
fareData.put("fare4", toAmt.get(3));
|
||||
fareData.put("fare5", toAmt.get(4));
|
||||
fareData.put("outIcs", outIcs);
|
||||
|
||||
response.put("status", true);
|
||||
response.put("statusMsg", "정상응답");
|
||||
response.put("fareData", fareData);
|
||||
return response;
|
||||
}
|
||||
else {
|
||||
log.warn("{}에서 {} 통행요금을 찾을 수 없습니다", fromIc, toIc);
|
||||
response.put("status", false);
|
||||
response.put("statusMsg", "해당구간의 통행요금을 찾지 못했습니다.");
|
||||
return response;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("통행요금 조회 에러 : {}", e.getMessage(), e);
|
||||
response.put("status", false);
|
||||
response.put("statusMsg", "서비스에 일시적인 장애가 발생하였습니다.");
|
||||
response.put("errorMsg", e.getMessage());
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.rag.api;
|
||||
|
||||
import com.ex.chatbotapi.domain.rag.dto.RagQueryRequest;
|
||||
import com.ex.chatbotapi.domain.rag.service.RagService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* RAG(Retrieval-Augmented Generation) 관련 API 컨트롤러
|
||||
* HTTP 요청 처리만 담당하고, 비즈니스 로직은 RagService에 위임
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/rag-only")
|
||||
@RequiredArgsConstructor
|
||||
public class RagApiController {
|
||||
private final RagService ragService;
|
||||
|
||||
/**
|
||||
* 챗봇 사용자 질문을 RAG 서버로 전달하고 응답을 반환하는 엔드포인트
|
||||
*/
|
||||
@PostMapping
|
||||
public ResponseEntity<Map<String, Object>> queryRag(@RequestBody RagQueryRequest request) {
|
||||
log.info("API 호출 : queryRag(POST) - 질문: {}, 봇ID: {}",
|
||||
request.getQuestion(), request.getBotId());
|
||||
return ResponseEntity.ok(ragService.processQuestion(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* RAG 서버(Docker 컨테이너) 헬스체크 엔드포인트
|
||||
*/
|
||||
@GetMapping("/health")
|
||||
public ResponseEntity<Map<String, Object>> checkHealth() {
|
||||
log.info("API 호출 : checkHealth(GET)");
|
||||
return ResponseEntity.ok(ragService.checkHealth());
|
||||
}
|
||||
}
|
||||
-76
@@ -1,76 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.rag.client;
|
||||
|
||||
import com.ex.chatbotapi.config.RagConfig;
|
||||
import com.ex.chatbotapi.domain.rag.dto.IntentAnalysisRequest;
|
||||
import com.ex.chatbotapi.domain.rag.dto.IntentAnalysisResponse;
|
||||
import com.ex.chatbotapi.domain.rag.dto.RagServerRequest;
|
||||
import com.ex.chatbotapi.domain.rag.dto.RagServerResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* RAG 서버와의 통신을 담당하는 클라이언트
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class RagServerClient {
|
||||
private final RestTemplate restTemplate;
|
||||
private final RagConfig ragConfig;
|
||||
|
||||
/**
|
||||
* RAG 서버에 질문을 전송하고 응답을 받아오는 메소드
|
||||
*/
|
||||
public RagServerResponse sendQuestion(RagServerRequest request) {
|
||||
String url = ragConfig.getFullUrl();
|
||||
log.info("RAG 서버 요청 시작 - URL: {}, 질문: {}", url, request.getQuery());
|
||||
|
||||
try {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
HttpEntity<RagServerRequest> entity = new HttpEntity<>(request, headers);
|
||||
|
||||
ResponseEntity<RagServerResponse> response = restTemplate.postForEntity(
|
||||
url, entity, RagServerResponse.class);
|
||||
|
||||
log.info("RAG 서버 응답 성공 - Status: {}", response.getStatusCode());
|
||||
return response.getBody();
|
||||
|
||||
} catch (RestClientException e) {
|
||||
log.error("RAG 서버 통신 실패 - URL: {}, 에러: {}", url, e.getMessage(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RAG 서버의 LLM intent 분석 엔드포인트 호출.
|
||||
*/
|
||||
public IntentAnalysisResponse analyzeIntent(IntentAnalysisRequest request) {
|
||||
String url = ragConfig.getIntentAnalyzeUrl();
|
||||
log.info("RAG intent 분석 요청 시작 - URL: {}, 질문: {}", url, request.getQuery());
|
||||
|
||||
try {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
HttpEntity<IntentAnalysisRequest> entity = new HttpEntity<>(request, headers);
|
||||
ResponseEntity<IntentAnalysisResponse> response = restTemplate.postForEntity(
|
||||
url, entity, IntentAnalysisResponse.class);
|
||||
|
||||
log.info("RAG intent 분석 응답 성공 - Status: {}", response.getStatusCode());
|
||||
return response.getBody();
|
||||
|
||||
} catch (RestClientException e) {
|
||||
log.error("RAG intent 분석 통신 실패 - URL: {}, 에러: {}", url, e.getMessage(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.rag.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class IntentAnalysisRequest {
|
||||
private String query;
|
||||
private String botId;
|
||||
private String pendingIntentType;
|
||||
private Map<String, String> pendingParams;
|
||||
private List<Map<String, Object>> intentDefinitions;
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.rag.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class IntentAnalysisResponse {
|
||||
private String intentType;
|
||||
private Double confidence;
|
||||
private Map<String, String> params = new HashMap<>();
|
||||
private String routeType;
|
||||
private Boolean needsClarification;
|
||||
private String clarificationQuestion;
|
||||
private String reason;
|
||||
|
||||
public double confidenceOrZero() {
|
||||
return confidence != null ? confidence : 0.0;
|
||||
}
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.rag.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 클라이언트(챗봇 웹)로부터 받는 RAG 질의 요청 DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RagQueryRequest {
|
||||
/** 사용자 질문 원본 */
|
||||
private String question;
|
||||
/** 사용자 봇 ID */
|
||||
private String botId;
|
||||
/**
|
||||
* 도메인 서비스에서 조회한 DB 데이터 (intentType에 따라 설정)
|
||||
* null이면 RAG 단독 처리
|
||||
*/
|
||||
private Map<String, Object> domainData;
|
||||
/** 의도 타입 (프롬프트 컨텍스트 제공용) */
|
||||
private String intentType;
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.rag.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* RAG 서버(Docker)로 전송하는 요청 DTO
|
||||
* API 스펙: POST /ask
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class RagServerRequest {
|
||||
/** 사용자 질문 (Docker API 필드명: "query") */
|
||||
private String query;
|
||||
/** 봇 ID */
|
||||
private String botId;
|
||||
/**
|
||||
* 도메인 서비스 DB 조회 결과 (null이면 전송 안 함)
|
||||
* Python 측 field: domain_data (alias: domainData)
|
||||
*/
|
||||
private Map<String, Object> domainData;
|
||||
/** 의도 타입 (Python 측 field: intent_type) */
|
||||
private String intentType;
|
||||
}
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.rag.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* RAG 서버(Docker)로부터 받는 응답 DTO
|
||||
* API 스펙: POST /ask 응답
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RagServerResponse {
|
||||
/** LLM이 생성한 답변 */
|
||||
private String answer;
|
||||
/** 가장 유사한 질문 목록 */
|
||||
@JsonProperty("matched_questions")
|
||||
private List<String> matchedQuestions;
|
||||
/** 유사도 점수 배열 */
|
||||
private List<Double> scores;
|
||||
/** 참조한 문서/질문 개수 */
|
||||
@JsonProperty("num_references")
|
||||
private Integer numReferences;
|
||||
/** 공식 FAQ 상세 URL 목록 */
|
||||
@JsonProperty("faq_urls")
|
||||
private List<String> faqUrls;
|
||||
/** 참조 문서 메타데이터 */
|
||||
private List<Map<String, Object>> references;
|
||||
}
|
||||
-117
@@ -1,117 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.rag.service;
|
||||
|
||||
import com.ex.chatbotapi.config.RagConfig;
|
||||
import com.ex.chatbotapi.domain.rag.client.RagServerClient;
|
||||
import com.ex.chatbotapi.domain.rag.dto.RagQueryRequest;
|
||||
import com.ex.chatbotapi.domain.rag.dto.RagServerRequest;
|
||||
import com.ex.chatbotapi.domain.rag.dto.RagServerResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* RAG 관련 비즈니스 로직을 처리하는 서비스
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RagService {
|
||||
private final RagServerClient ragServerClient;
|
||||
private final RagConfig ragConfig;
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
/**
|
||||
* 사용자 질문을 처리하고 RAG 서버로부터 응답을 받아오는 메소드
|
||||
*/
|
||||
public Map<String, Object> processQuestion(RagQueryRequest request) {
|
||||
log.info("RAG 질문 처리 시작 - 질문: {}, 봇ID: {}",
|
||||
request.getQuestion(), request.getBotId());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
try {
|
||||
validateRequest(request);
|
||||
|
||||
RagServerRequest serverRequest = RagServerRequest.builder()
|
||||
.query(request.getQuestion())
|
||||
.botId(request.getBotId())
|
||||
.domainData(request.getDomainData())
|
||||
.intentType(request.getIntentType())
|
||||
.build();
|
||||
|
||||
RagServerResponse serverResponse = ragServerClient.sendQuestion(serverRequest);
|
||||
|
||||
result.put("status", true);
|
||||
result.put("statusMsg", "정상응답");
|
||||
result.put("answer", serverResponse.getAnswer());
|
||||
result.put("matchedQuestions", serverResponse.getMatchedQuestions());
|
||||
result.put("scores", serverResponse.getScores());
|
||||
result.put("numReferences", serverResponse.getNumReferences());
|
||||
result.put("faqUrls", serverResponse.getFaqUrls());
|
||||
result.put("references", serverResponse.getReferences());
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("입력 검증 실패 - {}", e.getMessage());
|
||||
result.put("status", false);
|
||||
result.put("statusMsg", "입력값 오류: " + e.getMessage());
|
||||
|
||||
} catch (RestClientException e) {
|
||||
log.error("RAG 서버 통신 실패 - {}", e.getMessage());
|
||||
result.put("status", false);
|
||||
result.put("statusMsg", "RAG 서버 통신 실패");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("RAG 질문 처리 중 예외 발생 - {}", e.getMessage(), e);
|
||||
result.put("status", false);
|
||||
result.put("statusMsg", "처리 중 오류 발생");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* RAG 서버 헬스체크
|
||||
*/
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public Map<String, Object> checkHealth() {
|
||||
log.info("RAG 서버 헬스체크 시작");
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
try {
|
||||
String healthUrl = ragConfig.getHealthUrl();
|
||||
log.info("헬스체크 URL: {}", healthUrl);
|
||||
|
||||
ResponseEntity<Map> response = restTemplate.getForEntity(healthUrl, Map.class);
|
||||
Map<String, Object> healthData = (Map<String, Object>) response.getBody();
|
||||
|
||||
result.put("status", true);
|
||||
result.put("statusMsg", "헬스체크 성공");
|
||||
result.put("healthCheck", healthData);
|
||||
|
||||
if (healthData != null) {
|
||||
log.info("RAG 서버 상태: {}, 벡터 개수: {}",
|
||||
healthData.get("status"), healthData.get("vector_count"));
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("RAG 서버 헬스체크 실패 - {}", e.getMessage(), e);
|
||||
result.put("status", false);
|
||||
result.put("statusMsg", "헬스체크 실패: " + e.getMessage());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void validateRequest(RagQueryRequest request) {
|
||||
if (request.getQuestion() == null || request.getQuestion().trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("질문이 비어있습니다.");
|
||||
}
|
||||
}
|
||||
}
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.restarea.api;
|
||||
|
||||
import com.ex.chatbotapi.domain.restarea.service.RestAreaService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 휴게소 관련 API 컨트롤러
|
||||
* HTTP 요청 처리만 담당하고, 비즈니스 로직은 RestAreaService에 위임
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
public class RestAreaApiController {
|
||||
|
||||
private final RestAreaService restAreaService;
|
||||
|
||||
/**
|
||||
* Description : 휴게소 가격정보 조회 메소드
|
||||
*/
|
||||
@GetMapping("/api/restarea/priceInfo")
|
||||
public ResponseEntity<Map<String, Object>> queryPriceInfo(String restAreaName) {
|
||||
log.info("API 호출 : queryPriceInfo(GET) - restAreaName: {}", restAreaName);
|
||||
return ResponseEntity.ok(restAreaService.getPriceInfo(restAreaName));
|
||||
}
|
||||
}
|
||||
-65
@@ -1,65 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.restarea.formatter;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.formatter.LlmDomainDataFormatter;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* BRAND_SHOP — 휴게소 브랜드 매장 목록을 LLM용 요약으로 정리한다.
|
||||
*/
|
||||
@Component
|
||||
public class BrandShopLlmFormatter implements LlmDomainDataFormatter {
|
||||
|
||||
private static final int MAX_BRANDS = 20;
|
||||
|
||||
@Override
|
||||
public String getIntentType() {
|
||||
return "BRAND_SHOP";
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void enrich(Map<String, Object> domainData) {
|
||||
if (domainData == null) return;
|
||||
|
||||
Object st = domainData.get("status");
|
||||
boolean ok = st instanceof Boolean && (Boolean) st;
|
||||
|
||||
if (!ok) {
|
||||
String msg = domainData.get("statusMsg") != null
|
||||
? String.valueOf(domainData.get("statusMsg"))
|
||||
: "브랜드 매장 정보를 조회할 수 없습니다.";
|
||||
domainData.put("llmSummary", msg);
|
||||
return;
|
||||
}
|
||||
|
||||
String restAreaName = String.valueOf(domainData.getOrDefault("restAreaName", ""));
|
||||
Object totalCountObj = domainData.get("totalCount");
|
||||
int totalCount = totalCountObj instanceof Number ? ((Number) totalCountObj).intValue() : 0;
|
||||
|
||||
List<Map<String, Object>> list = (List<Map<String, Object>>) domainData.get("list");
|
||||
if (list == null || list.isEmpty()) {
|
||||
domainData.put("llmSummary", restAreaName + " 휴게소 매장 정보가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 브랜드명 목록 (중복 제거)
|
||||
List<String> brands = list.stream()
|
||||
.map(b -> String.valueOf(b.getOrDefault("brdName", "")))
|
||||
.distinct()
|
||||
.limit(MAX_BRANDS)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(restAreaName).append(" 휴게소 브랜드 매장 (총 ").append(totalCount).append("개):\n");
|
||||
sb.append(String.join(", ", brands));
|
||||
if (totalCount > MAX_BRANDS) {
|
||||
sb.append(" 외 ").append(totalCount - MAX_BRANDS).append("개");
|
||||
}
|
||||
|
||||
domainData.put("llmSummary", sb.toString().trim());
|
||||
}
|
||||
}
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.restarea.formatter;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.formatter.LlmDomainDataFormatter;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* REST_AREA_FOOD_LIST — 휴게소 음식 목록(전체 메뉴)을 LLM용 요약으로 정리한다.
|
||||
*/
|
||||
@Component
|
||||
public class FoodListLlmFormatter implements LlmDomainDataFormatter {
|
||||
|
||||
private static final int MAX_CHARS = 800;
|
||||
|
||||
@Override
|
||||
public String getIntentType() {
|
||||
return "REST_AREA_FOOD_LIST";
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void enrich(Map<String, Object> domainData) {
|
||||
if (domainData == null) return;
|
||||
|
||||
Object st = domainData.get("status");
|
||||
boolean ok = st instanceof Boolean && (Boolean) st;
|
||||
|
||||
if (!ok) {
|
||||
String msg = domainData.get("statusMsg") != null
|
||||
? String.valueOf(domainData.get("statusMsg"))
|
||||
: "음식 목록을 조회할 수 없습니다.";
|
||||
domainData.put("llmSummary", msg);
|
||||
return;
|
||||
}
|
||||
|
||||
String restAreaName = String.valueOf(domainData.getOrDefault("restAreaName", ""));
|
||||
Map<String, String> msgMap = (Map<String, String>) domainData.get("msgMap");
|
||||
|
||||
if (msgMap == null || msgMap.isEmpty()) {
|
||||
domainData.put("llmSummary", restAreaName + " 휴게소 메뉴 정보가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(restAreaName).append(" 휴게소 메뉴:\n");
|
||||
for (Map.Entry<String, String> entry : msgMap.entrySet()) {
|
||||
String restaurant = entry.getKey().replace("휴게소", "").trim();
|
||||
sb.append("\n[").append(restaurant).append("]\n");
|
||||
sb.append(entry.getValue()).append("\n");
|
||||
if (sb.length() > MAX_CHARS) {
|
||||
sb.append("...(이하 생략)");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
domainData.put("llmSummary", sb.toString().trim());
|
||||
}
|
||||
}
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.restarea.formatter;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.formatter.LlmDomainDataFormatter;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* GASSTATION — 휴게소 주유소 현황을 LLM용 요약으로 정리한다.
|
||||
*/
|
||||
@Component
|
||||
public class GasstationLlmFormatter implements LlmDomainDataFormatter {
|
||||
|
||||
@Override
|
||||
public String getIntentType() {
|
||||
return "GASSTATION";
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void enrich(Map<String, Object> domainData) {
|
||||
if (domainData == null) return;
|
||||
|
||||
Object st = domainData.get("status");
|
||||
boolean ok = st instanceof Boolean && (Boolean) st;
|
||||
|
||||
if (!ok) {
|
||||
String msg = domainData.get("statusMsg") != null
|
||||
? String.valueOf(domainData.get("statusMsg"))
|
||||
: "주유소 정보를 조회할 수 없습니다.";
|
||||
domainData.put("llmSummary", msg);
|
||||
return;
|
||||
}
|
||||
|
||||
String restAreaName = String.valueOf(domainData.getOrDefault("restAreaName", ""));
|
||||
|
||||
List<Map<String, Object>> list = (List<Map<String, Object>>) domainData.get("list");
|
||||
if (list == null || list.isEmpty()) {
|
||||
domainData.put("llmSummary", restAreaName + " 휴게소 주유 정보가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Map<String, Object> g : list) {
|
||||
String svcNm = gasstationDisplayName(restAreaName, g);
|
||||
String gasoline = String.valueOf(g.getOrDefault("gasolinePrice", "X"));
|
||||
String diesel = String.valueOf(g.getOrDefault("diselPrice", "X"));
|
||||
String lpgYn = String.valueOf(g.getOrDefault("lpgYn", "N"));
|
||||
String lpgPrice = String.valueOf(g.getOrDefault("lpgPrice", "X"));
|
||||
|
||||
sb.append(svcNm).append(" 주유 현황:\n");
|
||||
sb.append("- 휘발유: ").append("X".equals(gasoline) ? "정보 없음" : gasoline + "원").append("\n");
|
||||
sb.append("- 경유: ").append("X".equals(diesel) ? "정보 없음" : diesel + "원").append("\n");
|
||||
sb.append("- LPG: ").append("Y".equals(lpgYn) ? lpgPrice + "원" : "미운영").append("\n");
|
||||
}
|
||||
|
||||
domainData.put("llmSummary", sb.toString().trim());
|
||||
}
|
||||
|
||||
private String gasstationDisplayName(String restAreaName, Map<String, Object> gasInfo) {
|
||||
Object serviceAreaName = gasInfo.get("serviceAreaName");
|
||||
if (serviceAreaName != null) {
|
||||
return String.valueOf(serviceAreaName)
|
||||
.replace("주유소", "")
|
||||
.replace("휴게소", "");
|
||||
}
|
||||
Object directionObj = gasInfo.get("direction");
|
||||
if (directionObj != null && restAreaName != null && !restAreaName.trim().isEmpty()) {
|
||||
return restAreaName + "(" + String.valueOf(directionObj).trim() + ")";
|
||||
}
|
||||
return "주유소";
|
||||
}
|
||||
}
|
||||
-80
@@ -1,80 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.restarea.formatter;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.formatter.LlmDomainDataFormatter;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* REST_AREA_PRICE — 휴게소 간식·메뉴 가격 공시(구간별 min/max/avg)를 LLM용 요약으로 정리한다.
|
||||
*/
|
||||
@Component
|
||||
public class RestAreaPriceLlmFormatter implements LlmDomainDataFormatter {
|
||||
|
||||
@Override
|
||||
public String getIntentType() {
|
||||
return "REST_AREA_PRICE";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enrich(Map<String, Object> domainData) {
|
||||
if (domainData == null || domainData.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object st = domainData.get("status");
|
||||
boolean ok = st instanceof Boolean && (Boolean) st;
|
||||
|
||||
if (!ok) {
|
||||
String msg = domainData.get("statusMsg") != null
|
||||
? String.valueOf(domainData.get("statusMsg"))
|
||||
: "휴게소 가격정보 조회에 실패했습니다.";
|
||||
domainData.put("llmSummary", msg);
|
||||
return;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, List<Map<String, String>>> priceInfo =
|
||||
(Map<String, List<Map<String, String>>>) domainData.get("priceInfo");
|
||||
if (priceInfo == null || priceInfo.isEmpty()) {
|
||||
domainData.put("llmSummary", "가격 공시(priceInfo) 데이터가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("휴게소 메뉴·간식 가격 공시 요약:\n");
|
||||
|
||||
int restLimit = 5;
|
||||
int itemLimitPerRest = 12;
|
||||
int restCount = 0;
|
||||
|
||||
for (Map.Entry<String, List<Map<String, String>>> e : priceInfo.entrySet()) {
|
||||
if (restCount >= restLimit) {
|
||||
sb.append("- …외 휴게소 ").append(priceInfo.size() - restLimit).append("곳 더 있음\n");
|
||||
break;
|
||||
}
|
||||
String restNm = e.getKey();
|
||||
List<Map<String, String>> rows = e.getValue();
|
||||
sb.append("- ").append(restNm).append("\n");
|
||||
int n = 0;
|
||||
for (Map<String, String> row : rows) {
|
||||
if (n >= itemLimitPerRest) {
|
||||
sb.append(" · …외 ").append(rows.size() - itemLimitPerRest).append("개 항목\n");
|
||||
break;
|
||||
}
|
||||
String gb = row.getOrDefault("snackGb", "?");
|
||||
String min = row.getOrDefault("minCost", "?");
|
||||
String max = row.getOrDefault("maxCost", "?");
|
||||
String avg = row.getOrDefault("avgCost", "?");
|
||||
sb.append(" · ").append(gb)
|
||||
.append(": 최저 ").append(min).append("원, 최고 ").append(max)
|
||||
.append("원, 평균 ").append(avg).append("원\n");
|
||||
n++;
|
||||
}
|
||||
restCount++;
|
||||
}
|
||||
|
||||
domainData.put("llmSummary", sb.toString().trim());
|
||||
}
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.restarea.handler;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.handler.IntentHandler;
|
||||
import com.ex.chatbotapi.domain.restarea.service.RestAreaService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class RestAreaPriceHandler implements IntentHandler {
|
||||
|
||||
private final RestAreaService restAreaService;
|
||||
|
||||
@Override
|
||||
public String getIntentType() {
|
||||
return "REST_AREA_PRICE";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> fetchDomainData(Map<String, String> params) {
|
||||
return restAreaService.getPriceInfo(params.get("restAreaName"));
|
||||
}
|
||||
}
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.restarea.repository;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 휴게소 관련 데이터 접근 레이어
|
||||
*/
|
||||
@Slf4j
|
||||
@Repository
|
||||
@RequiredArgsConstructor
|
||||
public class RestAreaRepository {
|
||||
|
||||
@Qualifier("restAreaDataSource")
|
||||
private final DataSource restAreaDataSource;
|
||||
|
||||
/**
|
||||
* 휴게소 가격정보 조회
|
||||
*/
|
||||
public List<Map<String, Object>> findPriceInfoByRestAreaName(String restAreaName) {
|
||||
String query = "SELECT REST_NM, NM_REP as SNACK_GB, MIN_COST, MAX_COST, AVG_COST " +
|
||||
"FROM EXREST.RESTCM_SNACKRESTAVG_VW A " +
|
||||
"INNER JOIN EXREST.RESTCM_SNACKGB_VW B " +
|
||||
"ON A.SNACK_GB = B.NM_CD " +
|
||||
"WHERE REST_NM LIKE ?";
|
||||
|
||||
JdbcTemplate jdbcTemplate = new JdbcTemplate(restAreaDataSource);
|
||||
|
||||
try {
|
||||
String searchPattern = "%" + restAreaName + "%";
|
||||
return jdbcTemplate.queryForList(query, searchPattern);
|
||||
} catch (EmptyResultDataAccessException e) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
}
|
||||
-78
@@ -1,78 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.restarea.service;
|
||||
|
||||
import com.ex.chatbotapi.domain.restarea.repository.RestAreaRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 휴게소 관련 비즈니스 로직 처리 서비스
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RestAreaService {
|
||||
|
||||
private final RestAreaRepository restAreaRepository;
|
||||
|
||||
/**
|
||||
* 휴게소 가격정보 조회
|
||||
*/
|
||||
public Map<String, Object> getPriceInfo(String restAreaName) {
|
||||
log.info("{} 휴게소 가격공시 조회를 시작합니다", restAreaName);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
|
||||
try {
|
||||
List<Map<String, Object>> restAreaInfo = restAreaRepository
|
||||
.findPriceInfoByRestAreaName(restAreaName);
|
||||
|
||||
if (!restAreaInfo.isEmpty()) {
|
||||
Map<String, List<Map<String, String>>> priceInfoByRestArea = new HashMap<>();
|
||||
|
||||
for (Map<String, Object> map : restAreaInfo) {
|
||||
String restNm = (String) map.get("REST_NM");
|
||||
String snackGb = (String) map.get("SNACK_GB");
|
||||
int minCost = ((BigDecimal) map.get("MIN_COST")).intValue();
|
||||
int maxCost = ((BigDecimal) map.get("MAX_COST")).intValue();
|
||||
int avgCost = ((BigDecimal) map.get("AVG_COST")).intValue();
|
||||
|
||||
if (!priceInfoByRestArea.containsKey(restNm)) {
|
||||
priceInfoByRestArea.put(restNm, new ArrayList<>());
|
||||
}
|
||||
|
||||
Map<String, String> priceItem = new HashMap<>();
|
||||
priceItem.put("snackGb", snackGb);
|
||||
priceItem.put("minCost", String.valueOf(minCost));
|
||||
priceItem.put("maxCost", String.valueOf(maxCost));
|
||||
priceItem.put("avgCost", String.valueOf(avgCost));
|
||||
|
||||
priceInfoByRestArea.get(restNm).add(priceItem);
|
||||
}
|
||||
|
||||
response.put("status", true);
|
||||
response.put("statusMsg", "정상응답");
|
||||
response.put("priceInfo", priceInfoByRestArea);
|
||||
} else {
|
||||
log.info("{} 휴게소의 메뉴 및 가격정보가 없습니다.", restAreaName);
|
||||
response.put("status", false);
|
||||
response.put("statusMsg", restAreaName + " 휴게소의 메뉴 및 가격정보가 없습니다.");
|
||||
}
|
||||
|
||||
return response;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("가격정보 조회 에러 : {}", e.getMessage(), e);
|
||||
response.put("status", false);
|
||||
response.put("statusMsg", "서비스에 일시적인 장애가 발생하였습니다.");
|
||||
response.put("errorMsg", e.getMessage());
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.troad.api;
|
||||
|
||||
import com.ex.chatbotapi.domain.troad.service.TroadService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 도로교통 관련 API 컨트롤러 (경로 조회, 혼잡구간, 교통속보)
|
||||
* HTTP 요청 처리만 담당하고, 비즈니스 로직은 TroadService에 위임
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
public class TroadApiController {
|
||||
|
||||
private final TroadService troadService;
|
||||
|
||||
@GetMapping("/api/troad/searchRoute")
|
||||
public ResponseEntity<Map<String, Object>> querySearchRoute(String fromIc, String toIc) throws IOException {
|
||||
log.info("API 호출 : querySearchRoute(GET) - fromIc: {}, toIc: {}", fromIc, toIc);
|
||||
return ResponseEntity.ok(troadService.searchRoute(fromIc, toIc));
|
||||
}
|
||||
|
||||
@GetMapping("/api/troad/congestion")
|
||||
public ResponseEntity<Map<String, Object>> queryCongestion(String routeName) {
|
||||
log.info("API 호출 : queryCongestion(GET) - routeName: {}", routeName);
|
||||
return ResponseEntity.ok(troadService.getCongestion(routeName));
|
||||
}
|
||||
|
||||
@GetMapping("/api/troad/breakingNews")
|
||||
public ResponseEntity<Map<String, Object>> queryBreakingNews(String routeName) {
|
||||
log.info("API 호출 : queryBreakingNews(GET) - routeName: {}", routeName);
|
||||
return ResponseEntity.ok(troadService.getBreakingNews(routeName));
|
||||
}
|
||||
}
|
||||
-95
@@ -1,95 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.troad.formatter;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.formatter.LlmDomainDataFormatter;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* TROAD_BREAKING_NEWS — 노선별 교통속보(상·하행)를 LLM용 요약으로 정리한다.
|
||||
*/
|
||||
@Component
|
||||
public class TroadBreakingNewsLlmFormatter implements LlmDomainDataFormatter {
|
||||
|
||||
private static final String REGEX_DIST = "\\(\\d+(\\.\\d+)?K\\)";
|
||||
|
||||
@Override
|
||||
public String getIntentType() {
|
||||
return "TROAD_BREAKING_NEWS";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enrich(Map<String, Object> domainData) {
|
||||
if (domainData == null || domainData.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object st = domainData.get("status");
|
||||
boolean ok = st instanceof Boolean && (Boolean) st;
|
||||
|
||||
if (!ok) {
|
||||
String msg = domainData.get("statusMsg") != null
|
||||
? String.valueOf(domainData.get("statusMsg"))
|
||||
: "교통속보 조회에 실패했습니다.";
|
||||
domainData.put("llmSummary", msg);
|
||||
return;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> routeInfo = (Map<String, Object>) domainData.get("RouteInfo");
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> newsS = (List<Map<String, Object>>) domainData.get("RouteBreakingNewsS");
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> newsE = (List<Map<String, Object>>) domainData.get("RouteBreakingNewsE");
|
||||
|
||||
if (routeInfo == null || routeInfo.isEmpty()) {
|
||||
domainData.put("llmSummary", "노선 기본 정보(RouteInfo)가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
String routeNm = String.valueOf(routeInfo.getOrDefault("ROUTENM", "?"));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("노선 교통속보 요약 — ").append(routeNm).append("\n");
|
||||
|
||||
int cap = 10;
|
||||
appendNewsBlock(sb, "상행(S)", newsS, cap);
|
||||
appendNewsBlock(sb, "하행(E)", newsE, cap);
|
||||
|
||||
domainData.put("llmSummary", sb.toString().trim());
|
||||
}
|
||||
|
||||
private static void appendNewsBlock(StringBuilder sb, String label, List<Map<String, Object>> rows, int cap) {
|
||||
sb.append("\n【").append(label).append("】\n");
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
sb.append("- 속보 없음\n");
|
||||
return;
|
||||
}
|
||||
int n = 0;
|
||||
for (Map<String, Object> map : rows) {
|
||||
if (n >= cap) {
|
||||
sb.append("- …외 ").append(rows.size() - cap).append("건\n");
|
||||
break;
|
||||
}
|
||||
String inciTp = String.valueOf(map.getOrDefault("INCI_TP", ""));
|
||||
String when = String.valueOf(map.getOrDefault("INCIDENT_DATE", ""));
|
||||
String pos = String.valueOf(map.getOrDefault("INCIDENT_POS_NAME", ""));
|
||||
pos = pos.replaceAll(REGEX_DIST, "").trim();
|
||||
String msg = String.valueOf(map.getOrDefault("MSG", ""));
|
||||
msg = msg.replaceAll("\\s+", " ").trim();
|
||||
|
||||
sb.append("- ");
|
||||
if (!when.isEmpty() && !"null".equals(when)) {
|
||||
sb.append(when).append(" ");
|
||||
}
|
||||
if (!inciTp.isEmpty() && !"null".equals(inciTp)) {
|
||||
sb.append("[").append(inciTp).append("] ");
|
||||
}
|
||||
if (!pos.isEmpty() && !"null".equals(pos)) {
|
||||
sb.append(pos).append(" — ");
|
||||
}
|
||||
sb.append(msg.isEmpty() || "null".equals(msg) ? "(내용참조)" : msg).append("\n");
|
||||
n++;
|
||||
}
|
||||
}
|
||||
}
|
||||
-125
@@ -1,125 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.troad.formatter;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.formatter.LlmDomainDataFormatter;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* TROAD_CONGESTION — 노선별 혼잡 구간(상·하행)을 LLM용 요약으로 정리한다.
|
||||
*/
|
||||
@Component
|
||||
public class TroadCongestionLlmFormatter implements LlmDomainDataFormatter {
|
||||
|
||||
@Override
|
||||
public String getIntentType() {
|
||||
return "TROAD_CONGESTION";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enrich(Map<String, Object> domainData) {
|
||||
if (domainData == null || domainData.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object st = domainData.get("status");
|
||||
boolean ok = st instanceof Boolean && (Boolean) st;
|
||||
|
||||
if (!ok) {
|
||||
String msg = domainData.get("statusMsg") != null
|
||||
? String.valueOf(domainData.get("statusMsg"))
|
||||
: "노선 혼잡정보 조회에 실패했습니다.";
|
||||
domainData.put("llmSummary", msg);
|
||||
return;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> routeInfo = (Map<String, Object>) domainData.get("RouteInfo");
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> routeInfoS = (List<Map<String, Object>>) domainData.get("RouteInfoS");
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> routeInfoE = (List<Map<String, Object>>) domainData.get("RouteInfoE");
|
||||
|
||||
if (routeInfo == null || routeInfo.isEmpty()) {
|
||||
domainData.put("llmSummary", "노선 기본 정보(RouteInfo)가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
String routeNm = String.valueOf(routeInfo.getOrDefault("ROUTENM", "?"));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("노선 혼잡 구간 요약 — ").append(routeNm).append("\n");
|
||||
|
||||
int cap = 12;
|
||||
if (routeInfoS != null && !routeInfoS.isEmpty()) {
|
||||
appendDirectionBlock(sb, "상행 등(S)", routeInfoS, cap);
|
||||
} else {
|
||||
sb.append("\n【상행 등혼잡 구간】\n- 해당 방향 데이터 없음\n");
|
||||
}
|
||||
|
||||
if (routeInfoE != null && !routeInfoE.isEmpty()) {
|
||||
appendDirectionBlock(sb, "하행 등(E)", routeInfoE, cap);
|
||||
} else {
|
||||
sb.append("\n【하행 등혼잡 구간】\n- 해당 방향 데이터 없음\n");
|
||||
}
|
||||
|
||||
domainData.put("llmSummary", sb.toString().trim());
|
||||
}
|
||||
|
||||
private static void appendDirectionBlock(StringBuilder sb, String label, List<Map<String, Object>> rows, int cap) {
|
||||
sb.append("\n【").append(label).append("】\n");
|
||||
int n = 0;
|
||||
for (Map<String, Object> map : rows) {
|
||||
if (n >= cap) {
|
||||
sb.append("- …외 ").append(rows.size() - cap).append("건\n");
|
||||
break;
|
||||
}
|
||||
String node = String.valueOf(map.getOrDefault("NODE_NM", "?"));
|
||||
String grade = gradeToKo(map.get("TRAFFIC_GRADE"));
|
||||
double len = toDouble(map.get("LINK_LEN_KM"));
|
||||
double speed = toDouble(map.get("SPEED"));
|
||||
String dirNm = String.valueOf(map.getOrDefault("ROUTE_DIR_NM", ""));
|
||||
sb.append("- ").append(node).append(" (").append(String.format("%.1f", len)).append("km) — ")
|
||||
.append(grade).append(", 평균속도 ").append(String.format("%.0f", speed)).append("km/h");
|
||||
if (!dirNm.isEmpty() && !"?".equals(dirNm)) {
|
||||
sb.append(", ").append(dirNm);
|
||||
}
|
||||
sb.append("\n");
|
||||
n++;
|
||||
}
|
||||
if (rows.isEmpty()) {
|
||||
sb.append("- 해당 방향 데이터 없음\n");
|
||||
}
|
||||
}
|
||||
|
||||
private static String gradeToKo(Object code) {
|
||||
if (code == null) {
|
||||
return "정보없음";
|
||||
}
|
||||
String g = code.toString();
|
||||
switch (g) {
|
||||
case "G":
|
||||
return "원활";
|
||||
case "L":
|
||||
return "서행";
|
||||
case "R":
|
||||
return "정체";
|
||||
default:
|
||||
return g;
|
||||
}
|
||||
}
|
||||
|
||||
private static double toDouble(Object v) {
|
||||
if (v == null) {
|
||||
return 0;
|
||||
}
|
||||
if (v instanceof Number) {
|
||||
return ((Number) v).doubleValue();
|
||||
}
|
||||
try {
|
||||
return Double.parseDouble(v.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
-149
@@ -1,149 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.troad.formatter;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.formatter.LlmDomainDataFormatter;
|
||||
import com.google.gson.Gson;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* TROAD_SEARCH_ROUTE — 시터스 영업소 간 경로(Summary, 정체/사고/작업 Icdt 등)를 LLM용 요약으로 정리한다.
|
||||
*/
|
||||
@Component
|
||||
public class TroadSearchRouteLlmFormatter implements LlmDomainDataFormatter {
|
||||
|
||||
private static final Gson GSON = new Gson();
|
||||
private static final String REGEX_DIST = "\\(\\d+(\\.\\d+)?K\\)";
|
||||
private static final String REGEX_TIME = "\\[\\d{2}:\\d{2}-\\s*:\\s*\\]";
|
||||
|
||||
@Override
|
||||
public String getIntentType() {
|
||||
return "TROAD_SEARCH_ROUTE";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enrich(Map<String, Object> domainData) {
|
||||
if (domainData == null || domainData.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object st = domainData.get("status");
|
||||
boolean ok = st instanceof Boolean && (Boolean) st;
|
||||
|
||||
if (!ok) {
|
||||
String msg = domainData.get("statusMsg") != null
|
||||
? String.valueOf(domainData.get("statusMsg"))
|
||||
: "영업소 간 교통경로 조회에 실패했습니다.";
|
||||
domainData.put("llmSummary", msg);
|
||||
return;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> summary = (Map<String, Object>) domainData.get("Summary");
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> bunch = (Map<String, Object>) domainData.get("Bunch");
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> icdt = (Map<String, Object>) domainData.get("Icdt");
|
||||
|
||||
String srcName = String.valueOf(domainData.getOrDefault("SrcName", ""));
|
||||
String dstName = String.valueOf(domainData.getOrDefault("DstName", ""));
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("영업소 구간 교통요약\n");
|
||||
if (!srcName.isEmpty() || !dstName.isEmpty()) {
|
||||
sb.append("- 구간: ").append(srcName).append(" ⇒ ").append(dstName).append("\n");
|
||||
}
|
||||
|
||||
if (summary != null && !summary.isEmpty()) {
|
||||
double routeKm = toDouble(summary.get("RouteLength")) / 1000.0;
|
||||
double travelSec = toDouble(summary.get("TravelTime"));
|
||||
int hh = (int) (travelSec / 3600);
|
||||
int mm = (int) ((travelSec % 3600) / 60);
|
||||
sb.append("- 총거리: 약 ").append(Math.round(routeKm)).append("km\n");
|
||||
sb.append("- 소요시간(엔진 추정): ").append(hh).append("시간 ").append(mm).append("분\n");
|
||||
}
|
||||
|
||||
if (bunch != null && bunch.containsKey("BunchItems")) {
|
||||
sb.append("- 구간내 이벤트·구간 묶음 건수(BunchItems): ").append((int) toDouble(bunch.get("BunchItems"))).append("건\n");
|
||||
}
|
||||
|
||||
sb.append("\n【정체】\n");
|
||||
appendIcdtSection(sb, icdt, "정체");
|
||||
sb.append("\n【사고】\n");
|
||||
appendIcdtSection(sb, icdt, "사고");
|
||||
sb.append("\n【작업】\n");
|
||||
appendIcdtSection(sb, icdt, "작업");
|
||||
|
||||
domainData.put("llmSummary", sb.toString().trim());
|
||||
}
|
||||
|
||||
private static void appendIcdtSection(StringBuilder sb, Map<String, Object> icdt, String type) {
|
||||
List<String> lines = extractIcdtMessages(icdt, type, 25);
|
||||
if (lines.isEmpty()) {
|
||||
sb.append("- 없음\n");
|
||||
return;
|
||||
}
|
||||
for (String line : lines) {
|
||||
sb.append("» ").append(line).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kakao 스킬과 동일한 IcdtData·IcdtInfo 구조를 사용해 타입별 메시지 문자열을 수집한다.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static List<String> extractIcdtMessages(Map<String, Object> icdt, String incidentType, int maxLines) {
|
||||
List<String> out = new ArrayList<>();
|
||||
if (icdt == null) {
|
||||
return out;
|
||||
}
|
||||
Object raw = icdt.get("IcdtData");
|
||||
if (!(raw instanceof List)) {
|
||||
return out;
|
||||
}
|
||||
|
||||
List<Object> icdtDatas = (List<Object>) raw;
|
||||
for (Object icdtData : icdtDatas) {
|
||||
if (out.size() >= maxLines) {
|
||||
break;
|
||||
}
|
||||
Map<String, Object> icdtDataMap = GSON.fromJson(GSON.toJson(icdtData), Map.class);
|
||||
Object ii = icdtDataMap.get("IcdtInfo");
|
||||
if (!(ii instanceof List)) {
|
||||
continue;
|
||||
}
|
||||
List<?> icdtInfo = (List<?>) ii;
|
||||
if (icdtInfo.size() < 4) {
|
||||
continue;
|
||||
}
|
||||
String category = String.valueOf(icdtInfo.get(2));
|
||||
if (!incidentType.equals(category)) {
|
||||
continue;
|
||||
}
|
||||
String infoMsg = String.valueOf(icdtInfo.get(3));
|
||||
infoMsg = infoMsg.replaceAll(REGEX_DIST, "");
|
||||
infoMsg = infoMsg.replaceAll(REGEX_TIME, "");
|
||||
infoMsg = infoMsg.trim();
|
||||
if (!infoMsg.isEmpty()) {
|
||||
out.add(infoMsg);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static double toDouble(Object v) {
|
||||
if (v == null) {
|
||||
return 0;
|
||||
}
|
||||
if (v instanceof Number) {
|
||||
return ((Number) v).doubleValue();
|
||||
}
|
||||
try {
|
||||
return Double.parseDouble(v.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.troad.handler;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.handler.IntentHandler;
|
||||
import com.ex.chatbotapi.domain.troad.service.TroadService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class TroadBreakingNewsHandler implements IntentHandler {
|
||||
|
||||
private final TroadService troadService;
|
||||
|
||||
@Override
|
||||
public String getIntentType() {
|
||||
return "TROAD_BREAKING_NEWS";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> fetchDomainData(Map<String, String> params) {
|
||||
return troadService.getBreakingNews(params.get("routeName"));
|
||||
}
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.troad.handler;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.handler.IntentHandler;
|
||||
import com.ex.chatbotapi.domain.troad.service.TroadService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class TroadCongestionHandler implements IntentHandler {
|
||||
|
||||
private final TroadService troadService;
|
||||
|
||||
@Override
|
||||
public String getIntentType() {
|
||||
return "TROAD_CONGESTION";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> fetchDomainData(Map<String, String> params) {
|
||||
return troadService.getCongestion(params.get("routeName"));
|
||||
}
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.troad.handler;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.handler.IntentHandler;
|
||||
import com.ex.chatbotapi.domain.troad.service.TroadService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class TroadSearchRouteHandler implements IntentHandler {
|
||||
|
||||
private final TroadService troadService;
|
||||
|
||||
@Override
|
||||
public String getIntentType() {
|
||||
return "TROAD_SEARCH_ROUTE";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> fetchDomainData(Map<String, String> params) {
|
||||
try {
|
||||
String fromIc = params.get("fromIcCode") != null ? params.get("fromIcCode") : params.get("fromIc");
|
||||
String toIc = params.get("toIcCode") != null ? params.get("toIcCode") : params.get("toIc");
|
||||
return troadService.searchRoute(fromIc, toIc);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("시터스 엔진 조회 실패: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
-255
@@ -1,255 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.troad.service;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.internal.LinkedTreeMap;
|
||||
import com.mongodb.BasicDBObject;
|
||||
import com.mongodb.client.MongoCollection;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.bson.Document;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import org.springframework.data.mongodb.core.MongoTemplate;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TroadService {
|
||||
|
||||
private final MongoTemplate mongoTemplate;
|
||||
|
||||
@Qualifier("troadTemplate")
|
||||
private final JdbcTemplate troadTemplate;
|
||||
|
||||
private final List<String> exceptionRoute = Arrays.asList("남해선", "서울문산선", "중부내륙선", "호남선", "수도권제2순환선");
|
||||
|
||||
/**
|
||||
* 영업소(톨게이트) 간 경로 조회
|
||||
*/
|
||||
public Map<String, Object> searchRoute(String fromIc, String toIc) throws IOException {
|
||||
BasicDBObject condition = new BasicDBObject();
|
||||
condition.put("src", fromIc);
|
||||
condition.put("dst", toIc);
|
||||
Gson gson = new Gson();
|
||||
|
||||
MongoCollection<Document> icRoadInfo = mongoTemplate.getCollection("icRoadInfo");
|
||||
Document icMinGRS = icRoadInfo.find(condition).first();
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
if (icMinGRS != null) {
|
||||
String srcGrs = icMinGRS.getString("srcGrs80");
|
||||
String srcName = icMinGRS.getString("srcName");
|
||||
String dstGrs = icMinGRS.getString("dstGrs80");
|
||||
String dstName = icMinGRS.getString("dstName");
|
||||
|
||||
log.info("시터스 엔진 조회");
|
||||
String result = extractRouteInfo(srcGrs, dstGrs);
|
||||
Map<String, Object> resMap = gson.fromJson(result, Map.class);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> summary = (LinkedTreeMap<String, Object>) resMap.get("Summary");
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> icdt = (LinkedTreeMap<String, Object>) resMap.get("Icdt");
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> bunch = (LinkedTreeMap<String, Object>) resMap.get("Bunch");
|
||||
|
||||
response.put("status", true);
|
||||
response.put("statusMsg", "정상응답");
|
||||
response.put("Summary", summary);
|
||||
response.put("Icdt", icdt);
|
||||
response.put("Bunch", bunch);
|
||||
response.put("SrcName", srcName);
|
||||
response.put("SrcGrs", srcGrs);
|
||||
response.put("DstName", dstName);
|
||||
response.put("DstGrs", dstGrs);
|
||||
} else {
|
||||
response.put("status", false);
|
||||
response.put("statusMsg", "몽고db에 해당구간 존재안함");
|
||||
log.error("시터스 엔진 조회 에러(몽고db에 해당구간 존재안함) : {} - {}", fromIc, toIc);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 노선별 혼잡구간 조회
|
||||
*/
|
||||
public Map<String, Object> getCongestion(String routeName) {
|
||||
String queryGetRouteInfo = "SELECT ROUTENM, ROUTECD, GRPID FROM TROAD_APP.V_ROUTE_NEW_NM WHERE ROUTENM = ? GROUP BY ROUTENM, ROUTECD, GRPID";
|
||||
String queryGetRouteQuickReplyInfo = "SELECT ROUTENM, ROUTECD, GRPID FROM TROAD_APP.V_ROUTE_NEW_NM WHERE ROUTENM LIKE ? GROUP BY ROUTENM, ROUTECD, GRPID";
|
||||
String queryGetRouteCongestionInfoS = "SELECT RPRS_ROTNM, ROUTE_DIR_NM, NODE_NM, TRAFFIC_GRADE, LINK_LEN_KM, SPEED FROM ( SELECT ROW_NUMBER() OVER( PARTITION BY A.DRIVE_DRCT_CD, A.ROUTE_CD, A.GRP_ID ORDER BY A.DRIVE_DRCT_CD, A.ROUTE_CD, A.SECT_SQNO ASC ) AS STNUMB , ROW_NUMBER() OVER( PARTITION BY A.DRIVE_DRCT_CD, A.ROUTE_CD, A.GRP_ID ORDER BY A.DRIVE_DRCT_CD, A.ROUTE_CD, A.SECT_SQNO DESC ) AS EDNUMB , ROW_NUMBER() OVER( PARTITION BY A.CONZONE_ID, A.DRIVE_DRCT_CD ORDER BY CASE WHEN A.ROUTE_CD='3010' THEN A.SECT_SQNO END DESC, CASE WHEN A.ROUTE_CD='1000' THEN A.SECT_SQNO END , CASE WHEN MOD(B.ROUTESHORTNO,2)=1 THEN A.SECT_SQNO END, CASE WHEN MOD(B.ROUTESHORTNO,2)=0 THEN A.SECT_SQNO END DESC ) AS DETAIL_NO , ROW_NUMBER() OVER( PARTITION BY A.CONZONE_ID, A.DRIVE_DRCT_CD ORDER BY CASE WHEN A.ROUTE_CD='3010' THEN A.SECT_SQNO END, CASE WHEN A.ROUTE_CD='1000' THEN A.SECT_SQNO END DESC, CASE WHEN MOD(B.ROUTESHORTNO,2)=1 THEN A.SECT_SQNO END DESC, CASE WHEN MOD(B.ROUTESHORTNO,2)=0 THEN A.SECT_SQNO END ) AS DETAIL_NO2 , A.ROUTESORT, A.SECT_SQNO, B.ROUTEDIRNM AS ROUTE_DIR_NM , B.ROUTENM AS RPRS_ROTNM, A.ROUTE_CD, A.GRP_ID, A.DRIVE_DRCT_CD, A.NODE_NM, A.CONZONE_ID, A.ROUTE_NO_LIST, A.LINK_LEN_KM, A.BUS_YN, A.SPEED, A.TRAFFIC_GRADE, A.TRAFFIC_GRADE_BUS, A.CCTV_ID, A.CCTV_AREA, A.HLS_URL, A.FILEURL_IMG, A.INCIDENT_TYPE_CD, A.INCIDENT_DESC, A.MSG, A.INCIDENT_POS_NAME, A.DETR_ID, A.SPEED_DETR, A.TRAFFIC_GRADE_DETR, A.RESTAREA_ID, A.RESTAREA_NM, A.CONZONE_X, A.CONZONE_Y, A.WORK_CD, A.VVALUE01, A.VVALUE02, A.VVALUE03, A.VVALUE04, A.VVALUE05, A.VVALUE06, A.VVALUE07, A.VVALUE08, A.VVALUE09, CASE WHEN A.ROUTE_CD='3010' THEN 0 WHEN A.ROUTE_CD='1000' THEN 1 ELSE MOD(B.ROUTESHORTNO,2) END AS BGUBUN FROM V_ROUTE_TRFC_VER001 A LEFT OUTER JOIN V_ROUTE_NEW_NM B ON B.ROUTECD = A.ROUTE_CD AND B.GRPID = A.GRP_ID AND B.UPDOWNDIR = A.DRIVE_DRCT_CD WHERE 1=1 AND A.GRP_ID = ? ) WHERE 1=1 AND ROUTE_CD = ? AND DRIVE_DRCT_CD = 'S' AND GRP_ID = ? AND TRAFFIC_GRADE IN ('R','L') AND DETAIL_NO = '1' ORDER BY CASE WHEN BGUBUN=1 THEN STNUMB END, CASE WHEN BGUBUN=0 THEN STNUMB END DESC ";
|
||||
String queryGetRouteCongestionInfoE = "SELECT RPRS_ROTNM, ROUTE_DIR_NM, NODE_NM, TRAFFIC_GRADE, LINK_LEN_KM, SPEED FROM ( SELECT ROW_NUMBER() OVER( PARTITION BY A.DRIVE_DRCT_CD, A.ROUTE_CD, A.GRP_ID ORDER BY A.DRIVE_DRCT_CD, A.ROUTE_CD, A.SECT_SQNO ASC ) AS STNUMB , ROW_NUMBER() OVER( PARTITION BY A.DRIVE_DRCT_CD, A.ROUTE_CD, A.GRP_ID ORDER BY A.DRIVE_DRCT_CD, A.ROUTE_CD, A.SECT_SQNO DESC ) AS EDNUMB , ROW_NUMBER() OVER( PARTITION BY A.CONZONE_ID, A.DRIVE_DRCT_CD ORDER BY CASE WHEN A.ROUTE_CD='3010' THEN A.SECT_SQNO END, CASE WHEN A.ROUTE_CD='1000' THEN A.SECT_SQNO END DESC, CASE WHEN MOD(B.ROUTESHORTNO,2)=1 THEN A.SECT_SQNO END DESC, CASE WHEN MOD(B.ROUTESHORTNO,2)=0 THEN A.SECT_SQNO END ) AS DETAIL_NO , ROW_NUMBER() OVER( PARTITION BY A.CONZONE_ID, A.DRIVE_DRCT_CD ORDER BY CASE WHEN A.ROUTE_CD='3010' THEN A.SECT_SQNO END DESC, CASE WHEN A.ROUTE_CD='1000' THEN A.SECT_SQNO END , CASE WHEN MOD(B.ROUTESHORTNO,2)=1 THEN A.SECT_SQNO END, CASE WHEN MOD(B.ROUTESHORTNO,2)=0 THEN A.SECT_SQNO END DESC ) AS DETAIL_NO2 , A.ROUTESORT, A.SECT_SQNO, B.ROUTEDIRNM AS ROUTE_DIR_NM , B.ROUTENM AS RPRS_ROTNM, A.ROUTE_CD, A.GRP_ID, A.DRIVE_DRCT_CD, A.NODE_NM, A.CONZONE_ID, A.ROUTE_NO_LIST, A.LINK_LEN_KM, A.BUS_YN, A.SPEED, A.TRAFFIC_GRADE, A.TRAFFIC_GRADE_BUS, A.CCTV_ID, A.CCTV_AREA, A.HLS_URL, A.FILEURL_IMG, A.INCIDENT_TYPE_CD, A.INCIDENT_DESC, A.MSG, A.INCIDENT_POS_NAME, A.DETR_ID, A.SPEED_DETR, A.TRAFFIC_GRADE_DETR, A.RESTAREA_ID, A.RESTAREA_NM, A.CONZONE_X, A.CONZONE_Y, A.WORK_CD, A.VVALUE01, A.VVALUE02, A.VVALUE03, A.VVALUE04, A.VVALUE05, A.VVALUE06, A.VVALUE07, A.VVALUE08, A.VVALUE09, CASE WHEN A.ROUTE_CD='3010' THEN 0 WHEN A.ROUTE_CD='1000' THEN 1 ELSE MOD(B.ROUTESHORTNO,2) END AS BGUBUN FROM V_ROUTE_TRFC_VER001 A LEFT OUTER JOIN V_ROUTE_NEW_NM B ON B.ROUTECD = A.ROUTE_CD AND B.GRPID = A.GRP_ID AND B.UPDOWNDIR = A.DRIVE_DRCT_CD WHERE 1=1 AND A.GRP_ID = ? ) WHERE 1=1 AND ROUTE_CD = ? AND DRIVE_DRCT_CD = 'E' AND GRP_ID = ? AND TRAFFIC_GRADE IN ('R','L') AND DETAIL_NO = '1' ORDER BY CASE WHEN BGUBUN=1 THEN STNUMB END DESC, CASE WHEN BGUBUN=0 THEN STNUMB END ";
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
Map<String, Object> routeInfo;
|
||||
|
||||
try {
|
||||
routeInfo = troadTemplate.queryForMap(queryGetRouteInfo, routeName);
|
||||
} catch (EmptyResultDataAccessException e) {
|
||||
routeInfo = Collections.emptyMap();
|
||||
}
|
||||
|
||||
String quickReplyRouteName = exceptionRoute.contains(routeName)
|
||||
? routeName.substring(0, routeName.length() - 1)
|
||||
: routeName;
|
||||
log.info(quickReplyRouteName);
|
||||
|
||||
List<Map<String, Object>> routeQuickReplyInfo;
|
||||
if (routeName.equals("수도권순환선")) {
|
||||
routeQuickReplyInfo = new ArrayList<>();
|
||||
Map<String, Object> firstMap = new HashMap<>();
|
||||
firstMap.put("ROUTENM", "수도권제1순환선");
|
||||
firstMap.put("ROUTECD", "");
|
||||
firstMap.put("GRPID", "");
|
||||
Map<String, Object> secondMap = new HashMap<>();
|
||||
secondMap.put("ROUTENM", "수도권제2순환선");
|
||||
secondMap.put("ROUTECD", "");
|
||||
secondMap.put("GRPID", "");
|
||||
routeQuickReplyInfo.add(firstMap);
|
||||
routeQuickReplyInfo.add(secondMap);
|
||||
} else {
|
||||
routeQuickReplyInfo = troadTemplate.queryForList(queryGetRouteQuickReplyInfo,
|
||||
"%" + quickReplyRouteName + "%");
|
||||
}
|
||||
response.put("RouteQuickReplyInfo", routeQuickReplyInfo);
|
||||
|
||||
if (!routeInfo.isEmpty()) {
|
||||
String routeNm = (String) routeInfo.get("ROUTENM");
|
||||
String routeCd = (String) routeInfo.get("ROUTECD");
|
||||
String grpId = (String) routeInfo.get("GRPID");
|
||||
|
||||
log.info("{} 혼잡정보 조회", routeNm);
|
||||
log.info("노선코드 : {}, 그룹 식별자 : {}", routeCd, grpId);
|
||||
|
||||
List<Map<String, Object>> routeInfoS = troadTemplate.queryForList(queryGetRouteCongestionInfoS, grpId,
|
||||
routeCd, grpId);
|
||||
List<Map<String, Object>> routeInfoE = troadTemplate.queryForList(queryGetRouteCongestionInfoE, grpId,
|
||||
routeCd, grpId);
|
||||
|
||||
response.put("status", true);
|
||||
response.put("statusMsg", "정상응답");
|
||||
response.put("RouteInfo", routeInfo);
|
||||
response.put("RouteInfoS", routeInfoS);
|
||||
response.put("RouteInfoE", routeInfoE);
|
||||
} else {
|
||||
response.put("status", false);
|
||||
response.put("statusMsg", "유효하지 않은 노선입니다.");
|
||||
log.error("유효하지 않은 노선입니다. - {}", routeName);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 노선별 교통속보 조회
|
||||
*/
|
||||
public Map<String, Object> getBreakingNews(String routeName) {
|
||||
String queryGetRouteInfo = "SELECT ROUTENM, ROUTECD, GRPID FROM TROAD_APP.V_ROUTE_NEW_NM WHERE ROUTENM = ? GROUP BY ROUTENM, ROUTECD, GRPID";
|
||||
String queryGetRouteQuickReplyInfo = "SELECT ROUTENM, ROUTECD, GRPID FROM TROAD_APP.V_ROUTE_NEW_NM WHERE ROUTENM LIKE ? GROUP BY ROUTENM, ROUTECD, GRPID";
|
||||
String queryGetBreakingNewsS = "SELECT ROUTECD, GRPID, BOUND AS BND, Y.CMMN_CD_NM AS ROADNM, CASE WHEN BOUND = 'S' THEN SUBSTR(Y.DETL_CTNT, 1, INSTR(Y.DETL_CTNT, '$')-1) WHEN BOUND = 'E' THEN SUBSTR(Y.DETL_CTNT, INSTR(Y.DETL_CTNT, '$')+1) END BOUNDNM, ROUTESORT, ROADSHORTNO, INCIDENT_ID, '[' || TO_CHAR(TO_DATE(INCIDENT_ID, 'YYYYMMDDHH24MISS'), 'YYYY-MM-DD HH24:MI') || ']' AS INCIDENT_DATE, INCI_TP_ORDER, INCI_TP, MSG, INCIDENT_POS_NAME, OCRN_DSTNC, CNGS_LNGT, LAT, LON, CCTV_ID, CCTV_NM, THUMBIMAGE, HLS_URL, ST_D, EN_D, RANK() OVER( PARTITION BY ROUTESORT, Y.CMMN_CD_NM, BOUND ORDER BY ROUTESORT ASC, DECODE(INCI_TP_ORDER, '00','1', '04', '2', '03', '3', '4') ASC, ST_D DESC, INCIDENT_ID DESC ) AS STNUMB , RANK() OVER( PARTITION BY ROUTESORT, Y.CMMN_CD_NM, BOUND ORDER BY ROUTESORT ASC, DECODE(INCI_TP_ORDER, '00','1', '04', '2', '03', '3', '4') DESC, ST_D ASC, INCIDENT_ID ASC ) AS EDNUMB , ' ' AS VVALUE01, ' ' AS VVALUE02, ' ' AS VVALUE03, ' ' AS VVALUE04, ' ' AS VVALUE05 FROM V_ACDT_BRNWS_VER001 X LEFT OUTER JOIN TLISDEV.T_TRMB_CD_INFO01M1 Y ON ( X.ROUTECD || X.GRPID = Y.CMMN_CD AND Y.HGRN_CD_ID = 'COM064' AND Y.USE_XN = 'Y' ) WHERE 1=1 AND ROUTECD = ? AND ROADNM LIKE ? AND BOUND = 'S' AND INCI_TP_ORDER IN ('00','04','03','01')";
|
||||
String queryGetBreakingNewsE = "SELECT ROUTECD, GRPID, BOUND AS BND, Y.CMMN_CD_NM AS ROADNM, CASE WHEN BOUND = 'S' THEN SUBSTR(Y.DETL_CTNT, 1, INSTR(Y.DETL_CTNT, '$')-1) WHEN BOUND = 'E' THEN SUBSTR(Y.DETL_CTNT, INSTR(Y.DETL_CTNT, '$')+1) END BOUNDNM, ROUTESORT, ROADSHORTNO, INCIDENT_ID, '[' || TO_CHAR(TO_DATE(INCIDENT_ID, 'YYYYMMDDHH24MISS'), 'YYYY-MM-DD HH24:MI') || ']' AS INCIDENT_DATE, INCI_TP_ORDER, INCI_TP, MSG, INCIDENT_POS_NAME, OCRN_DSTNC, CNGS_LNGT, LAT, LON, CCTV_ID, CCTV_NM, THUMBIMAGE, HLS_URL, ST_D, EN_D, RANK() OVER( PARTITION BY ROUTESORT, Y.CMMN_CD_NM, BOUND ORDER BY ROUTESORT ASC, DECODE(INCI_TP_ORDER, '00','1', '04', '2', '03', '3', '4') ASC, ST_D DESC, INCIDENT_ID DESC ) AS STNUMB , RANK() OVER( PARTITION BY ROUTESORT, Y.CMMN_CD_NM, BOUND ORDER BY ROUTESORT ASC, DECODE(INCI_TP_ORDER, '00','1', '04', '2', '03', '3', '4') DESC, ST_D ASC, INCIDENT_ID ASC ) AS EDNUMB , ' ' AS VVALUE01, ' ' AS VVALUE02, ' ' AS VVALUE03, ' ' AS VVALUE04, ' ' AS VVALUE05 FROM V_ACDT_BRNWS_VER001 X LEFT OUTER JOIN TLISDEV.T_TRMB_CD_INFO01M1 Y ON ( X.ROUTECD || X.GRPID = Y.CMMN_CD AND Y.HGRN_CD_ID = 'COM064' AND Y.USE_XN = 'Y' ) WHERE 1=1 AND ROUTECD = ? AND ROADNM LIKE ? AND BOUND = 'E' AND INCI_TP_ORDER IN ('00','04','03','01')";
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
Map<String, Object> routeInfo;
|
||||
|
||||
try {
|
||||
routeInfo = troadTemplate.queryForMap(queryGetRouteInfo, routeName);
|
||||
} catch (EmptyResultDataAccessException e) {
|
||||
routeInfo = Collections.emptyMap();
|
||||
}
|
||||
|
||||
String quickReplyRouteName = exceptionRoute.contains(routeName)
|
||||
? routeName.substring(0, routeName.length() - 1)
|
||||
: routeName;
|
||||
log.info(quickReplyRouteName);
|
||||
|
||||
List<Map<String, Object>> routeQuickReplyInfo;
|
||||
if (routeName.equals("수도권순환선")) {
|
||||
routeQuickReplyInfo = new ArrayList<>();
|
||||
Map<String, Object> firstMap = new HashMap<>();
|
||||
firstMap.put("ROUTENM", "수도권제1순환선");
|
||||
firstMap.put("ROUTECD", "");
|
||||
firstMap.put("GRPID", "");
|
||||
Map<String, Object> secondMap = new HashMap<>();
|
||||
secondMap.put("ROUTENM", "수도권제2순환선");
|
||||
secondMap.put("ROUTECD", "");
|
||||
secondMap.put("GRPID", "");
|
||||
routeQuickReplyInfo.add(firstMap);
|
||||
routeQuickReplyInfo.add(secondMap);
|
||||
} else {
|
||||
routeQuickReplyInfo = troadTemplate.queryForList(queryGetRouteQuickReplyInfo,
|
||||
"%" + quickReplyRouteName + "%");
|
||||
}
|
||||
response.put("RouteQuickReplyInfo", routeQuickReplyInfo);
|
||||
|
||||
if (!routeInfo.isEmpty()) {
|
||||
String routeNm = (String) routeInfo.get("ROUTENM");
|
||||
String routeCd = (String) routeInfo.get("ROUTECD");
|
||||
String grpId = (String) routeInfo.get("GRPID");
|
||||
|
||||
log.info("{} 교통속보 조회", routeNm);
|
||||
log.info("노선코드 : {}, 그룹 식별자 : {}", routeCd, grpId);
|
||||
|
||||
List<Map<String, Object>> routeBreakingNewsS = troadTemplate.queryForList(queryGetBreakingNewsS, routeCd,
|
||||
"%" + routeNm + "%");
|
||||
List<Map<String, Object>> routeBreakingNewsE = troadTemplate.queryForList(queryGetBreakingNewsE, routeCd,
|
||||
"%" + routeNm + "%");
|
||||
|
||||
response.put("status", true);
|
||||
response.put("statusMsg", "정상응답");
|
||||
response.put("RouteInfo", routeInfo);
|
||||
response.put("RouteBreakingNewsS", routeBreakingNewsS);
|
||||
response.put("RouteBreakingNewsE", routeBreakingNewsE);
|
||||
} else {
|
||||
response.put("status", false);
|
||||
response.put("statusMsg", "유효하지 않은 노선입니다.");
|
||||
log.error("유효하지 않은 노선입니다. - {}", routeName);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 시터스 엔진 소켓 통신
|
||||
*/
|
||||
private String extractRouteInfo(String srcGrs, String dstGrs) throws IOException {
|
||||
String query = "CoordType=WGS84GEO,WGS84GEO&StartPoint=" + srcGrs
|
||||
+ "&EndPoint=" + dstGrs + "&MidPoint=" + ""
|
||||
+ "&Result=" + "" + "&SearchOption=12,0,1\n";
|
||||
|
||||
InetSocketAddress ipep = new InetSocketAddress("172.16.162.17", 8088);
|
||||
Socket client = new Socket();
|
||||
client.connect(ipep);
|
||||
|
||||
StringBuffer sb = new StringBuffer();
|
||||
try (OutputStream sender = client.getOutputStream();
|
||||
InputStream receiver = client.getInputStream()) {
|
||||
|
||||
byte[] data = query.getBytes("UTF-8");
|
||||
sender.write(data, 0, data.length);
|
||||
|
||||
data = new byte[1024000];
|
||||
int i;
|
||||
while ((i = receiver.read(data)) != -1) {
|
||||
sb.append(new String(data, 0, i, "UTF-8"));
|
||||
if (sb.toString().endsWith("\n.\n\n"))
|
||||
break;
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
String result = sb.toString();
|
||||
if (result.endsWith("\n.\n\n")) {
|
||||
result = result.replace("\n.\n\n", "");
|
||||
}
|
||||
client.close();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
# =============================================================================
|
||||
# chatbotApi (WAS) — 172.16.180.130
|
||||
# 동일 서버: exAiChatBot :28012, chatbotAdmin :8087, Qdrant, MongoDB
|
||||
# kakaoChatbotSkill: 별도 웹 서버 (SKILL_INTERNAL_BASE_URL 로 지정)
|
||||
# LLM 게이트웨이: 172.16.163.96
|
||||
# =============================================================================
|
||||
|
||||
server:
|
||||
port: 8086
|
||||
http2:
|
||||
enabled: true
|
||||
|
||||
spring:
|
||||
datasource:
|
||||
bpstl:
|
||||
driver-class-name: oracle.jdbc.OracleDriver
|
||||
url: jdbc:oracle:thin:@localhost:1521/orcl
|
||||
username: OPR_MAIN
|
||||
password: test
|
||||
# url: jdbc:oracle:thin:@172.16.181.22:1681/BPSTL
|
||||
# username: stl_app
|
||||
# password: "wjdtksdpdlvlvl!20"
|
||||
troad:
|
||||
driver-class-name: oracle.jdbc.OracleDriver
|
||||
url: jdbc:oracle:thin:@localhost:1521/orcl2
|
||||
username: OPR_MAIN
|
||||
password: test
|
||||
# url: jdbc:oracle:thin:@172.16.163.76:1522/TROAD
|
||||
# username: TROAD_APP
|
||||
# password: troad_app!2017
|
||||
restarea:
|
||||
driver-class-name: oracle.jdbc.OracleDriver
|
||||
url: jdbc:oracle:thin:@localhost:1521/orcl
|
||||
username: OPR_MAIN
|
||||
password: test
|
||||
data:
|
||||
mongodb:
|
||||
uri: ${MONGODB_URI:mongodb://exDev:ehfhrhdtk!!@127.0.0.1:27017/chatbot?authSource=chatbot&heartbeatFrequencyMS=30000&minPoolSize=3&maxIdleTimeMS=1800000}
|
||||
# uri: mongodb://exDev:ehfhrhdtk!!@exfareqc.ex.co.kr:27017/chatbot?heartbeatFrequencyMS=30000&minPoolSize=3&maxIdleTimeMS=1800000
|
||||
|
||||
restTemplate:
|
||||
factory:
|
||||
readTimeout: 5000
|
||||
connectTimeout: 3000
|
||||
httpClient:
|
||||
maxConnTotal: 100
|
||||
maxConnPerRoute: 5
|
||||
|
||||
# WAS → RAG (legacy /llm-agent, /intent/analyze) — RAG는 동일 서버 localhost
|
||||
rag:
|
||||
server:
|
||||
host: ${RAG_SERVER_HOST:127.0.0.1}
|
||||
port: ${RAG_SERVER_PORT:28012}
|
||||
ask-endpoint: /ask
|
||||
health-endpoint: /health
|
||||
intent-analyze-endpoint: /intent/analyze
|
||||
connect-timeout: 5000
|
||||
read-timeout: 30000
|
||||
|
||||
# Agent tool API (모드 분기는 Skill에서만 — WAS는 tool executor)
|
||||
agent:
|
||||
tool:
|
||||
internal-api-key: ${INTERNAL_TOOL_API_KEY:}
|
||||
execute-timeout-ms: ${TOOL_EXECUTE_TIMEOUT_MS:30000}
|
||||
|
||||
# WAS → kakaoChatbotSkill (WEB tool internal) — 웹 서버 IP/호스트명 설정
|
||||
kakao-skill:
|
||||
base-url: ${SKILL_INTERNAL_BASE_URL:http://128.200.100.61:8083}
|
||||
data-portal-tool-endpoint: /internal/tools/data-portal
|
||||
internal-api-key: ${INTERNAL_TOOL_API_KEY:}
|
||||
connect-timeout: 5000
|
||||
read-timeout: 30000
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,245 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"intents": {
|
||||
"FARE_SEARCH": {
|
||||
"description": "출발 IC와 도착 IC 사이 통행요금 조회",
|
||||
"fetchOwner": "WAS",
|
||||
"uiType": "FARE_CARD",
|
||||
"requiredParams": [
|
||||
{
|
||||
"name": "fromIc",
|
||||
"description": "출발 IC명. IC, 영업소, 톨게이트, 요금소 같은 접미사와 공백은 제거한 짧은 한글명으로 추출합니다. 사용자가 '여기', '현재 위치'처럼 말하면 실제 IC명으로 보지 않습니다.",
|
||||
"examples": ["판교", "서울", "대전"]
|
||||
},
|
||||
{
|
||||
"name": "toIc",
|
||||
"description": "도착 IC명. IC, 영업소, 톨게이트, 요금소 같은 접미사와 공백은 제거한 짧은 한글명으로 추출합니다.",
|
||||
"examples": ["신갈", "부산", "광주"]
|
||||
}
|
||||
],
|
||||
"optionalParams": [],
|
||||
"clarification": {
|
||||
"default": "통행요금 조회를 위해 출발 IC와 도착 IC를 알려주세요. 예: 판교에서 신갈까지",
|
||||
"missing": {
|
||||
"fromIc": "출발 IC를 알려주세요. 예: 판교에서 신갈까지의 통행요금",
|
||||
"toIc": "도착 IC를 알려주세요. 예: 판교에서 신갈까지의 통행요금"
|
||||
}
|
||||
}
|
||||
},
|
||||
"FARE_UNPAID": {
|
||||
"description": "차량의 미납 통행료를 실제로 조회하려는 의도. 전체 차량번호가 있으면 carNo로 추출하고, 조회 의사는 있지만 차량번호가 없으면 carNo를 null로 둡니다. 미납 조회 방법, 납부 방법, 조회 가능 경로만 묻는 안내성 질문은 이 intent가 아니라 일반 RAG로 처리합니다.",
|
||||
"fetchOwner": "WAS",
|
||||
"uiType": "TEXT_ONLY",
|
||||
"requiredParams": [
|
||||
{
|
||||
"name": "carNo",
|
||||
"description": "조회할 전체 차량번호입니다. 공백이나 하이픈은 제거해서 추출합니다. 끝자리만 있는 부분 차량번호는 사용하지 않습니다. 사용자가 실제 조회 의사를 말했지만 차량번호를 말하지 않으면 null로 둡니다.",
|
||||
"examples": ["12가3456", "123가4567"]
|
||||
}
|
||||
],
|
||||
"optionalParams": [],
|
||||
"minConfidence": 0.85,
|
||||
"clarification": {
|
||||
"default": "챗봇에서 미납 통행료 조회가 가능합니다. 조회하려면 전체 차량번호를 입력해 주세요. 예: 12가3456 미납 조회",
|
||||
"missing": {
|
||||
"carNo": "챗봇에서 미납 통행료 조회가 가능합니다. 조회하려면 전체 차량번호를 입력해 주세요. 예: 12가3456 미납 조회"
|
||||
}
|
||||
}
|
||||
},
|
||||
"FARE_REFUND": {
|
||||
"description": "차량번호 기반 환불 조회",
|
||||
"fetchOwner": "WAS",
|
||||
"uiType": "TEXT_ONLY",
|
||||
"requiredParams": [
|
||||
{
|
||||
"name": "carNo",
|
||||
"description": "환불 내역을 조회할 전체 차량번호입니다. 공백이나 하이픈은 제거해서 추출합니다. 끝자리만 있는 부분 차량번호는 사용하지 않습니다.",
|
||||
"examples": ["12가3456", "123가4567"]
|
||||
}
|
||||
],
|
||||
"optionalParams": [],
|
||||
"minConfidence": 0.85,
|
||||
"clarification": {
|
||||
"default": "환불 조회를 위해 차량번호를 알려주세요.",
|
||||
"missing": {
|
||||
"carNo": "조회할 차량번호를 알려주세요."
|
||||
}
|
||||
}
|
||||
},
|
||||
"IC_TEL": {
|
||||
"description": "IC 또는 영업소 전화번호 조회",
|
||||
"fetchOwner": "WAS",
|
||||
"uiType": "PHONE_CARD",
|
||||
"requiredParams": [
|
||||
{
|
||||
"name": "icName",
|
||||
"description": "전화번호를 확인할 IC명 또는 영업소명입니다. IC, 영업소, 톨게이트, 요금소 같은 접미사와 공백은 제거한 짧은 한글명으로 추출합니다.",
|
||||
"examples": ["판교", "서울", "신갈"]
|
||||
}
|
||||
],
|
||||
"optionalParams": [],
|
||||
"clarification": {
|
||||
"default": "전화번호를 확인할 IC 또는 영업소명을 알려주세요.",
|
||||
"missing": {
|
||||
"icName": "전화번호를 확인할 IC 또는 영업소명을 알려주세요."
|
||||
}
|
||||
}
|
||||
},
|
||||
"GASSTATION": {
|
||||
"description": "휴게소 주유소 유가/운영 정보 조회",
|
||||
"fetchOwner": "WEB",
|
||||
"uiType": "GASSTATION_CARD",
|
||||
"requiredParams": [
|
||||
{
|
||||
"name": "restAreaName",
|
||||
"description": "주유소 정보를 조회할 휴게소명입니다. 휴게소/주유소 접미사, 방향 표현(서울방향, 부산방향, 익산방향, 포항방향 등), 공백은 제외하고 짧은 한글명만 추출합니다.",
|
||||
"examples": ["죽전", "망향", "안성", "영천"]
|
||||
}
|
||||
],
|
||||
"optionalParams": [
|
||||
{
|
||||
"name": "restAreaDirection",
|
||||
"description": "사용자가 명시한 휴게소 방향 또는 상행/하행 구분입니다. '영천 주유소 익산방향'처럼 휴게소명 뒤에 붙은 방향도 반드시 분리해서 추출합니다. 방향을 말하지 않았으면 null로 둡니다.",
|
||||
"examples": ["서울방향", "부산방향", "익산방향", "포항방향", "상행", "하행"]
|
||||
}
|
||||
],
|
||||
"clarification": {
|
||||
"default": "조회할 휴게소명을 알려주세요.",
|
||||
"missing": {
|
||||
"restAreaName": "주유소 정보를 확인할 휴게소명을 알려주세요."
|
||||
}
|
||||
}
|
||||
},
|
||||
"BRAND_SHOP": {
|
||||
"description": "휴게소 브랜드 매장 조회",
|
||||
"fetchOwner": "WEB",
|
||||
"uiType": "BRAND_SHOP_CARD",
|
||||
"requiredParams": [
|
||||
{
|
||||
"name": "restAreaName",
|
||||
"description": "브랜드 매장을 조회할 휴게소명입니다. 휴게소 접미사와 공백은 제거한 짧은 한글명으로 추출합니다.",
|
||||
"examples": ["죽전", "망향", "안성"]
|
||||
}
|
||||
],
|
||||
"optionalParams": [
|
||||
{
|
||||
"name": "brand",
|
||||
"description": "찾고 싶은 브랜드 또는 매장명입니다.",
|
||||
"examples": ["스타벅스", "탐앤탐스", "롯데리아"]
|
||||
},
|
||||
{
|
||||
"name": "startIndex",
|
||||
"description": "목록 조회 시작 위치입니다. 사용자가 직접 말하지 않으면 추출하지 않습니다.",
|
||||
"examples": ["1", "10"]
|
||||
}
|
||||
],
|
||||
"clarification": {
|
||||
"default": "조회할 휴게소명을 알려주세요.",
|
||||
"missing": {
|
||||
"restAreaName": "브랜드 매장을 확인할 휴게소명을 알려주세요."
|
||||
}
|
||||
}
|
||||
},
|
||||
"REST_AREA_FOOD_LIST": {
|
||||
"description": "휴게소 음식/메뉴 목록 조회",
|
||||
"fetchOwner": "WEB",
|
||||
"uiType": "FOOD_LIST_CARD",
|
||||
"requiredParams": [
|
||||
{
|
||||
"name": "restAreaName",
|
||||
"description": "음식/메뉴 목록을 조회할 휴게소명입니다. 휴게소 접미사와 공백은 제거한 짧은 한글명으로 추출합니다.",
|
||||
"examples": ["죽전", "망향", "안성"]
|
||||
}
|
||||
],
|
||||
"optionalParams": [],
|
||||
"clarification": {
|
||||
"default": "조회할 휴게소명을 알려주세요.",
|
||||
"missing": {
|
||||
"restAreaName": "음식 정보를 확인할 휴게소명을 알려주세요."
|
||||
}
|
||||
}
|
||||
},
|
||||
"REST_AREA_PRICE": {
|
||||
"description": "휴게소 음식 가격 조회",
|
||||
"fetchOwner": "WAS",
|
||||
"uiType": "PRICE_CARD",
|
||||
"requiredParams": [
|
||||
{
|
||||
"name": "restAreaName",
|
||||
"description": "음식 가격을 확인할 휴게소명입니다. 휴게소 접미사와 공백은 제거한 짧은 한글명으로 추출합니다.",
|
||||
"examples": ["죽전", "망향", "안성"]
|
||||
}
|
||||
],
|
||||
"optionalParams": [],
|
||||
"clarification": {
|
||||
"default": "음식 가격을 확인할 휴게소명을 알려주세요.",
|
||||
"missing": {
|
||||
"restAreaName": "음식 가격을 확인할 휴게소명을 알려주세요."
|
||||
}
|
||||
}
|
||||
},
|
||||
"TROAD_SEARCH_ROUTE": {
|
||||
"description": "출발 IC와 도착 IC 기준 교통 경로 조회",
|
||||
"fetchOwner": "WAS",
|
||||
"uiType": "TEXT_ONLY",
|
||||
"requiredParams": [
|
||||
{
|
||||
"name": "fromIc",
|
||||
"description": "교통 경로 조회의 출발 IC명입니다. IC, 영업소, 톨게이트, 요금소 같은 접미사와 공백은 제거한 짧은 한글명으로 추출합니다.",
|
||||
"examples": ["판교", "서울", "대전"]
|
||||
},
|
||||
{
|
||||
"name": "toIc",
|
||||
"description": "교통 경로 조회의 도착 IC명입니다. IC, 영업소, 톨게이트, 요금소 같은 접미사와 공백은 제거한 짧은 한글명으로 추출합니다.",
|
||||
"examples": ["신갈", "부산", "광주"]
|
||||
}
|
||||
],
|
||||
"optionalParams": [],
|
||||
"clarification": {
|
||||
"default": "교통 경로 조회를 위해 출발 IC와 도착 IC를 알려주세요.",
|
||||
"missing": {
|
||||
"fromIc": "출발 IC를 알려주세요.",
|
||||
"toIc": "도착 IC를 알려주세요."
|
||||
}
|
||||
}
|
||||
},
|
||||
"TROAD_CONGESTION": {
|
||||
"description": "노선 정체 조회",
|
||||
"fetchOwner": "WAS",
|
||||
"uiType": "TEXT_ONLY",
|
||||
"requiredParams": [
|
||||
{
|
||||
"name": "routeName",
|
||||
"description": "정체 정보를 조회할 고속도로 노선명입니다.",
|
||||
"examples": ["경부선", "영동선", "서해안선"]
|
||||
}
|
||||
],
|
||||
"optionalParams": [],
|
||||
"clarification": {
|
||||
"default": "조회할 노선명을 알려주세요. 예: 경부선, 영동선",
|
||||
"missing": {
|
||||
"routeName": "조회할 노선명을 알려주세요. 예: 경부선, 영동선"
|
||||
}
|
||||
}
|
||||
},
|
||||
"TROAD_BREAKING_NEWS": {
|
||||
"description": "노선 사고/공사/돌발정보 조회",
|
||||
"fetchOwner": "WAS",
|
||||
"uiType": "TEXT_ONLY",
|
||||
"requiredParams": [
|
||||
{
|
||||
"name": "routeName",
|
||||
"description": "사고/공사/돌발정보를 조회할 고속도로 노선명입니다.",
|
||||
"examples": ["경부선", "영동선", "서해안선"]
|
||||
}
|
||||
],
|
||||
"optionalParams": [],
|
||||
"clarification": {
|
||||
"default": "조회할 노선명을 알려주세요. 예: 경부선, 영동선",
|
||||
"missing": {
|
||||
"routeName": "조회할 노선명을 알려주세요. 예: 경부선, 영동선"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
package com.ex.chatbotapi;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class ChatbotapiApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.service;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.dto.IntentValidationResult;
|
||||
import com.ex.chatbotapi.domain.agent.intent.IntentDefinitionRegistry;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class IntentParamValidatorTest {
|
||||
|
||||
private IntentParamValidator validator;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
IntentDefinitionRegistry registry = new IntentDefinitionRegistry();
|
||||
registry.load();
|
||||
validator = new IntentParamValidator(registry);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fareSearchRequiresRealDepartureAndDestination() {
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put("fromIc", "여기");
|
||||
params.put("toIc", "신갈");
|
||||
|
||||
IntentValidationResult result = validator.validate("FARE_SEARCH", params);
|
||||
|
||||
assertThat(result.isValid()).isFalse();
|
||||
assertThat(result.getMissingParams()).containsExactly("fromIc");
|
||||
assertThat(result.getCleanedParams()).containsEntry("toIc", "신갈");
|
||||
assertThat(result.getCleanedParams()).doesNotContainKey("fromIc");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fareSearchPassesWithBothIcParams() {
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put("fromIc", "판교");
|
||||
params.put("toIc", "신갈");
|
||||
|
||||
IntentValidationResult result = validator.validate("FARE_SEARCH", params);
|
||||
|
||||
assertThat(result.isValid()).isTrue();
|
||||
assertThat(result.getMissingParams()).isEmpty();
|
||||
assertThat(result.getCleanedParams()).containsEntry("fromIc", "판교");
|
||||
assertThat(result.getCleanedParams()).containsEntry("toIc", "신갈");
|
||||
}
|
||||
}
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
package com.ex.chatbotapi.domain.agent.service;
|
||||
|
||||
import com.ex.chatbotapi.domain.agent.intent.FetchOwner;
|
||||
import com.ex.chatbotapi.domain.agent.intent.IntentDefinition;
|
||||
import com.ex.chatbotapi.domain.agent.intent.IntentDefinitionRegistry;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ToolDefinitionServiceTest {
|
||||
|
||||
@Mock
|
||||
private IntentDefinitionRegistry intentDefinitionRegistry;
|
||||
|
||||
private ToolDefinitionService toolDefinitionService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
toolDefinitionService = new ToolDefinitionService(intentDefinitionRegistry);
|
||||
}
|
||||
|
||||
@Test
|
||||
void listToolDefinitions_excludesNoneFetchOwner() {
|
||||
IntentDefinition fare = definition("FARE_SEARCH", FetchOwner.WAS, "FARE_CARD");
|
||||
IntentDefinition ragOnly = definition("PURE_RAG", FetchOwner.NONE, "TEXT_ONLY");
|
||||
|
||||
when(intentDefinitionRegistry.all()).thenReturn(Map.of(
|
||||
"FARE_SEARCH", fare,
|
||||
"PURE_RAG", ragOnly
|
||||
));
|
||||
|
||||
List<Map<String, Object>> tools = toolDefinitionService.listToolDefinitions();
|
||||
|
||||
assertThat(tools).hasSize(1);
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> function = (Map<String, Object>) tools.get(0).get("function");
|
||||
assertThat(function.get("name")).isEqualTo("FARE_SEARCH");
|
||||
assertThat(function.get("description").toString()).contains("fetchOwner=WAS");
|
||||
}
|
||||
|
||||
private IntentDefinition definition(String name, FetchOwner owner, String uiType) {
|
||||
IntentDefinition definition = new IntentDefinition();
|
||||
definition.setIntentType(name);
|
||||
definition.setDescription("desc for " + name);
|
||||
definition.setFetchOwner(owner);
|
||||
definition.setUiType(uiType);
|
||||
|
||||
IntentDefinition.ParamDefinition fromIc = new IntentDefinition.ParamDefinition();
|
||||
fromIc.setName("fromIc");
|
||||
fromIc.setDescription("출발 IC");
|
||||
fromIc.setExamples(List.of("판교"));
|
||||
|
||||
IntentDefinition.ParamDefinition toIc = new IntentDefinition.ParamDefinition();
|
||||
toIc.setName("toIc");
|
||||
toIc.setDescription("도착 IC");
|
||||
|
||||
definition.setRequiredParamDefinitions(List.of(fromIc, toIc));
|
||||
definition.setRequiredParams(List.of("fromIc", "toIc"));
|
||||
return definition;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user