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