Embabel Agent 예시 프로젝트 - tripper

bebeis·2025년 11월 26일

embabel

목록 보기
4/5

Guide 문서의 Start 문서, 참고자료 문서를 먼저 공부하고 볼까 고민했는데, 나는 야생형! 먼저 예제 프로젝트를 뜯어봐야겠다.

Tripper란?

선호도와 관심사에 기반하여 개인화된 여행 일정을 작성하도록 도와주는 여행 계획 에이전트이다.

agent와 관련된 중요 요소만 뽑아봤다.

  • 결정론적 계획 및 도메인 모델 중심의 에이전트라는 Embabel의 핵심 개념 시연
  • 동일 애플리케이션에서 여러 LLM (Claude Sonnet, GPT-4.1-mini) 사용 예시
  • 지도, 이미지, 웹 검색, 위키피디아, Airbnb 통합을 위한 MCP 도구의 광범위한 사용
  • MCP 도구를 위한 Docker 컨테이너화

프로젝트 구조

com.embabel/
├── agent/                          # 공유 에이전트 기능
│   ├── Brave.kt                      - Brave 검색 도구
│   └── web/
│       ├── htmx/
│       │   ├── ProcessStatusController.kt  - SSE 진행 상황 스트리밍
│       │   └── PlatformController.kt       - 플랫폼 정보
│       └── security/
│           ├── LoginController.kt          - OAuth2 로그인
│           ├── UserController.kt           - 사용자 관리
│           └── SecurityConfig.kt           - OAuth2 Google 인증 설정
│
└── tripper/                        # Tripper 애플리케이션 (메인)
  ├── agent/                        - 여행 계획 에이전트
  │   ├── TripperAgent.kt           - 메인 오케스트레이터 (3-4단계 흐름)
  │   └── domain.kt                 - 도메인 모델
  │
  ├── config/                       - 구성
  │   └── ToolsConfig.kt            - MCP 도구 그룹 등록
  │
  ├── web/                          - 웹 인터페이스
  │   └── JourneyHtmxController.kt  - 여행 폼 처리
  │
  ├── util/                         - 유틸리티
  │   └── ImageChecker.kt           - 이미지 검증
  │
  └── TripperApplication.kt         - Spring Boot 진입점

API KEY

이 프로젝트를 실행하기 위해선 최소 3개의 API KEY(openai, brave api, google maps api)가 필요하다.

(흑흑 내 8200원)

luna@baejunseoui-MacBookPro tripper % docker compose --file compose.yaml up
WARN[0000] The "ANTHROPIC_API_KEY" variable is not set. Defaulting to a blank string. 
[+] Running 15/15
 ✔ zipkin Pulled                                                                                   0.5s 
Attaching to mcp-gateway-1, zipkin-1
mcp-gateway-1  | Note: dynamic-tools disabled when using --servers flag
mcp-gateway-1  | - Reading configuration...
mcp-gateway-1  |   - Reading catalog from [https://desktop.docker.com/mcp/catalog/v3/catalog.yaml]
mcp-gateway-1  |   - Reading secrets [brave.api_key github.personal_access_token google-maps.api_key]
zipkin-1       | 
zipkin-1       |                   oo
zipkin-1       |                  oooo
zipkin-1       |                 oooooo
zipkin-1       |                oooooooo
zipkin-1       |               oooooooooo
zipkin-1       |              oooooooooooo
zipkin-1       |            ooooooo  ooooooo
zipkin-1       |           oooooo     ooooooo
zipkin-1       |          oooooo       ooooooo
zipkin-1       |         oooooo   o  o   oooooo
zipkin-1       |        oooooo   oo  oo   oooooo
zipkin-1       |      ooooooo  oooo  oooo  ooooooo
zipkin-1       |     oooooo   ooooo  ooooo  ooooooo
zipkin-1       |    oooooo   oooooo  oooooo  ooooooo
zipkin-1       |   oooooooo      oo  oo      oooooooo
zipkin-1       |   ooooooooooooo oo  oo ooooooooooooo
zipkin-1       |       oooooooooooo  oooooooooooo
zipkin-1       |           oooooooo  oooooooo
zipkin-1       |               oooo  oooo
zipkin-1       | 
zipkin-1       |      ________ ____  _  _____ _   _
zipkin-1       |     |__  /_ _|  _ \| |/ /_ _| \ | |
zipkin-1       |       / / | || |_) | ' / | ||  \| |
zipkin-1       |      / /_ | ||  __/| . \ | || |\  |
zipkin-1       |     |____|___|_|   |_|\_\___|_| \_|
zipkin-1       | 
zipkin-1       | :: version 3.5.1 :: commit b7e2258 ::
zipkin-1       | 
zipkin-1       | 2025-11-23T21:57:49.466Z  WARN [/] 1 --- [           main] i.m.p.PrometheusMeterRegistry            : A MeterFilter is being configured after a Meter has been registered to this registry. All MeterFilters should be configured before any Meters are registered. If that is not possible or you have a use case where it should be allowed, let the Micrometer maintainers know at https://github.com/micrometer-metrics/micrometer/issues/4920. Enable DEBUG level logging on this logger to see a stack trace of the call configuring this MeterFilter.
zipkin-1       | 2025-11-23T21:57:49.681Z  INFO [/] 1 --- [oss-http-*:9411] c.l.a.s.Server                           : Serving HTTP at /[0:0:0:0:0:0:0:0]:9411 - http://127.0.0.1:9411/
mcp-gateway-1  | couldn't read secret brave.api_key: reading secrets exit status 125: docker: Error response from daemon: no such secret: brave.api_key
mcp-gateway-1  | 
mcp-gateway-1  | Run 'docker run --help' for more information
mcp-gateway-1  | 
mcp-gateway-1  | couldn't read secret github.personal_access_token: reading secrets exit status 125: docker: Error response from daemon: no such secret: github.personal_access_token
mcp-gateway-1  | 
mcp-gateway-1  | Run 'docker run --help' for more information
mcp-gateway-1  | 
mcp-gateway-1  | couldn't read secret google-maps.api_key: reading secrets exit status 125: docker: Error response from daemon: no such secret: google-maps.api_key
mcp-gateway-1  | 
mcp-gateway-1  | Run 'docker run --help' for more information
mcp-gateway-1  | 
mcp-gateway-1  | - Configuration read in 4.281827919s
mcp-gateway-1  | - Using images:
mcp-gateway-1  |   - mcp/brave-search@sha256:f45d701a860cade995bbb9c456eb67c0a71cb8232cab82f187f6dffdbf5d150c
mcp-gateway-1  |   - mcp/github@sha256:89fd71b6f2dd098de0d7517c1899b7fe90590d3193e253991d61a800b041bfd4
mcp-gateway-1  |   - mcp/google-maps@sha256:d8652c8c9c52ac9d82e9cadd1dc0468ade7a4f8c53e102e641eb4248894aa914
mcp-gateway-1  |   - mcp/openbnb-airbnb@sha256:352f5716e418f7d94e1bcd5c6846d7940b3250255b78ab1acb7e0adbd8f89331
mcp-gateway-1  |   - mcp/puppeteer@sha256:c1e2bda6d92d400e900e497b743552a670a33631799c0a6478e91096e389bd27
mcp-gateway-1  |   - mcp/wikipedia-mcp@sha256:047f17273bae96828903cfd12f13c7a64951b54e537530ecb6678fff5204a584
mcp-gateway-1  | > Images pulled in 28.829871931s
mcp-gateway-1  | - Those servers are enabled: brave, wikipedia-mcp, puppeteer, github, openbnb-airbnb, google-maps
mcp-gateway-1  | - Listing MCP tools...
mcp-gateway-1  | Warning: Secret 'brave.api_key' not found for server 'brave', setting BRAVE_API_KEY=<UNKNOWN>
mcp-gateway-1  |   - Running mcp/brave-search with [run --rm -i --init --security-opt no-new-privileges --cpus 1 --memory 2Gb --pull never -l docker-mcp=true -l docker-mcp-tool-type=mcp -l docker-mcp-name=brave -l docker-mcp-transport=stdio --network tripper_default -e BRAVE_API_KEY -e BRAVE_MCP_TRANSPORT]
mcp-gateway-1  |   - Running mcp/puppeteer with [run --rm -i --init --security-opt no-new-privileges --cpus 1 --memory 2Gb --pull never -l docker-mcp=true -l docker-mcp-tool-type=mcp -l docker-mcp-name=puppeteer -l docker-mcp-transport=stdio --network tripper_default -e DOCKER_CONTAINER]
mcp-gateway-1  |   - Running mcp/openbnb-airbnb with [run --rm -i --init --security-opt no-new-privileges --cpus 1 --memory 2Gb --pull never -l docker-mcp=true -l docker-mcp-tool-type=mcp -l docker-mcp-name=openbnb-airbnb -l docker-mcp-transport=stdio --network tripper_default]
mcp-gateway-1  | Warning: Secret 'github.personal_access_token' not found for server 'github', setting GITHUB_PERSONAL_ACCESS_TOKEN=<UNKNOWN>
mcp-gateway-1  |   - Running mcp/github with [run --rm -i --init --security-opt no-new-privileges --cpus 1 --memory 2Gb --pull never -l docker-mcp=true -l docker-mcp-tool-type=mcp -l docker-mcp-name=github -l docker-mcp-transport=stdio --network tripper_default -e GITHUB_PERSONAL_ACCESS_TOKEN]
mcp-gateway-1  |   - Running mcp/wikipedia-mcp with [run --rm -i --init --security-opt no-new-privileges --cpus 1 --memory 2Gb --pull never -l docker-mcp=true -l docker-mcp-tool-type=mcp -l docker-mcp-name=wikipedia-mcp -l docker-mcp-transport=stdio --network tripper_default]
mcp-gateway-1  | - github: GitHub MCP Server running on stdio
mcp-gateway-1  |   > github: (6 tools)
mcp-gateway-1  | Warning: Secret 'google-maps.api_key' not found for server 'google-maps', setting GOOGLE_MAPS_API_KEY=<UNKNOWN>
mcp-gateway-1  |   - Running mcp/google-maps with [run --rm -i --init --security-opt no-new-privileges --cpus 1 --memory 2Gb --pull never -l docker-mcp=true -l docker-mcp-tool-type=mcp -l docker-mcp-name=google-maps -l docker-mcp-transport=stdio --network tripper_default -e GOOGLE_MAPS_API_KEY]
mcp-gateway-1  |   > brave: (1 tools)
mcp-gateway-1  | - openbnb-airbnb: [2025-11-23T21:58:22.500Z] [INFO] Airbnb MCP Server starting: {
mcp-gateway-1  | - openbnb-airbnb:   "version": "0.1.3",
mcp-gateway-1  | - openbnb-airbnb:   "ignoreRobotsTxt": false,
mcp-gateway-1  | - openbnb-airbnb:   "nodeVersion": "v22.17.1",
mcp-gateway-1  | - openbnb-airbnb:   "platform": "linux"
mcp-gateway-1  | - openbnb-airbnb: }
mcp-gateway-1  | - openbnb-airbnb: [2025-11-23T21:58:22.506Z] [INFO] Fetching robots.txt from Airbnb
mcp-gateway-1  | - openbnb-airbnb: [2025-11-23T21:58:22.639Z] [INFO] Successfully fetched robots.txt
mcp-gateway-1  | - openbnb-airbnb: [2025-11-23T21:58:22.639Z] [INFO] Airbnb MCP Server running on stdio: {
mcp-gateway-1  | - openbnb-airbnb:   "version": "0.1.3",
mcp-gateway-1  | - openbnb-airbnb:   "robotsRespected": true
mcp-gateway-1  | - openbnb-airbnb: }
mcp-gateway-1  |   > puppeteer: (7 tools) (1 resources)
mcp-gateway-1  |   > openbnb-airbnb: (2 tools)
mcp-gateway-1  | - puppeteer: Puppeteer MCP Server closed
mcp-gateway-1  | - google-maps: Google Maps MCP Server running on stdio
mcp-gateway-1  | - wikipedia-mcp: 2025-11-23 21:58:23,242 - wikipediaapi - INFO - Wikipedia: language=en, user_agent: WikipediaMCPServer/1.6.0 (https://github.com/rudra-ravi/wikipedia-mcp) (Wikipedia-API/0.8.1; https://github.com/martin-majlis/Wikipedia-API/), extract_format=ExtractFormat.WIKI
mcp-gateway-1  |   > google-maps: (7 tools)
mcp-gateway-1  | - wikipedia-mcp: 2025-11-23 21:58:23,282 - wikipedia_mcp.__main__ - INFO - Starting Wikipedia MCP server with stdio transport for language: en
mcp-gateway-1  | - wikipedia-mcp: 2025-11-23 21:58:23,282 - wikipedia_mcp.__main__ - INFO - Using stdio transport - suppressing direct stdout messages for MCP communication.
mcp-gateway-1  | - wikipedia-mcp: 2025-11-23 21:58:23,283 - wikipedia_mcp.__main__ - INFO - To use with Claude Desktop, ensure 'wikipedia-mcp' command is in your claude_desktop_config.json.
mcp-gateway-1  | - wikipedia-mcp: 
mcp-gateway-1  | - wikipedia-mcp: 
mcp-gateway-1  | - wikipedia-mcp: ╭──────────────────────────────────────────────────────────────────────────────╮
mcp-gateway-1  | - wikipedia-mcp: │                                                                              │
mcp-gateway-1  | - wikipedia-mcp: │                         ▄▀▀ ▄▀█ █▀▀ ▀█▀ █▀▄▀█ █▀▀ █▀█                        │
mcp-gateway-1  | - wikipedia-mcp: │                         █▀  █▀█ ▄▄█  █  █ ▀ █ █▄▄ █▀▀                        │
mcp-gateway-1  | - wikipedia-mcp: │                                                                              │
mcp-gateway-1  | - wikipedia-mcp: │                               FastMCP 2.13.0.2                               │
mcp-gateway-1  | - wikipedia-mcp: │                                                                              │
mcp-gateway-1  | - wikipedia-mcp: │                                                                              │
mcp-gateway-1  | - wikipedia-mcp: │                    🖥  Server name: Wikipedia                                 │
mcp-gateway-1  | - wikipedia-mcp: │                                                                              │
mcp-gateway-1  | - wikipedia-mcp: │                    📦 Transport:   STDIO                                     │
mcp-gateway-1  | - wikipedia-mcp: │                                                                              │
mcp-gateway-1  | - wikipedia-mcp: │                    📚 Docs:        https://gofastmcp.com                     │
mcp-gateway-1  | - wikipedia-mcp: │                    🚀 Hosting:     https://fastmcp.cloud                     │
mcp-gateway-1  | - wikipedia-mcp: │                                                                              │
mcp-gateway-1  | - wikipedia-mcp: ╰──────────────────────────────────────────────────────────────────────────────╯
mcp-gateway-1  | - wikipedia-mcp: 
mcp-gateway-1  | - wikipedia-mcp: 
mcp-gateway-1  | - wikipedia-mcp: [11/23/25 21:58:23] INFO     Starting MCP server 'Wikipedia' with server.py:1966
mcp-gateway-1  | - wikipedia-mcp:                              transport 'stdio'                                  
mcp-gateway-1  | - wikipedia-mcp: 2025-11-23 21:58:23,296 - mcp.server.lowlevel.server - INFO - Processing request of type ListToolsRequest
mcp-gateway-1  | - wikipedia-mcp: 2025-11-23 21:58:23,301 - mcp.server.lowlevel.server - INFO - Processing request of type ListPromptsRequest
mcp-gateway-1  | - wikipedia-mcp: 2025-11-23 21:58:23,308 - mcp.server.lowlevel.server - INFO - Processing request of type ListResourcesRequest
mcp-gateway-1  | - wikipedia-mcp: 2025-11-23 21:58:23,310 - mcp.server.lowlevel.server - INFO - Processing request of type ListResourceTemplatesRequest
mcp-gateway-1  |   > wikipedia-mcp: (11 tools) (9 resourceTemplates)
mcp-gateway-1  | > 34 tools listed in 3.404502376s
mcp-gateway-1  | > Initialized in 36.570998142s
mcp-gateway-1  | > Start sse server on port 9011
mcp-gateway-1  | > Gateway URL: http://localhost:9011/sse
mcp-gateway-1  | > Authentication disabled (running in container)
  • 도커를 통해 MCP 서버를 띄우고 있다.

프로젝트 실행 결과

07:18:36.585 [tomcat-handler-9] INFO  MDR - 👔 Kier May my gaze be singularly placed upon the path, may the words of virtue guide me in the daily labor of my great undertaking.
    [beautiful_grothendieck] created
07:18:36.586 [tomcat-handler-9] INFO  MDR - [beautiful_grothendieck] Perpetuity wing: object added: JourneyTravelBrief
07:18:36.586 [tomcat-handler-9] INFO  MDR - [beautiful_grothendieck] Perpetuity wing: object added: Travelers
07:18:36.593 [task-1] INFO  MDR - [beautiful_grothendieck] <WIT>  ready to plan from hasRun_com.embabel.tripper.agent.TripperAgent.confirmExpensiveOperation: FALSE
hasRun_com.embabel.tripper.agent.TripperAgent.findPlacesToSleep: FALSE
hasRun_com.embabel.tripper.agent.TripperAgent.findPointsOfInterest: FALSE
hasRun_com.embabel.tripper.agent.TripperAgent.postProcessHtml: FALSE
hasRun_com.embabel.tripper.agent.TripperAgent.proposeTravelPlan: FALSE
hasRun_com.embabel.tripper.agent.TripperAgent.researchPointsOfInterest: FALSE
it:com.embabel.tripper.agent.ItineraryIdeas: FALSE
it:com.embabel.tripper.agent.PointOfInterestFindings: FALSE
it:com.embabel.tripper.agent.ProposedTravelPlan: FALSE
it:com.embabel.tripper.agent.TravelPlan: FALSE
it:com.embabel.tripper.agent.TripperAgent$AcceptanceOfCost: FALSE
it:com.embabel.tripper.agent.JourneyTravelBrief: TRUE
it:com.embabel.tripper.agent.Travelers: TRUE
07:18:36.787 [task-1] INFO  Planner - Found plan to goal com.embabel.tripper.agent.TripperAgent.postProcessHtml: com.embabel.tripper.agent.TripperAgent.findPointsOfInterest -> com.embabel.tripper.agent.TripperAgent.confirmExpensiveOperation -> com.embabel.tripper.agent.TripperAgent.researchPointsOfInterest -> com.embabel.tripper.agent.TripperAgent.proposeTravelPlan -> com.embabel.tripper.agent.TripperAgent.findPlacesToSleep -> com.embabel.tripper.agent.TripperAgent.postProcessHtml
07:18:36.788 [task-1] INFO  MDR - [beautiful_grothendieck] <WILES>: formulated plan com.embabel.tripper.agent.TripperAgent.findPointsOfInterest ->
  com.embabel.tripper.agent.TripperAgent.confirmExpensiveOperation ->
    com.embabel.tripper.agent.TripperAgent.researchPointsOfInterest ->
      com.embabel.tripper.agent.TripperAgent.proposeTravelPlan ->
        com.embabel.tripper.agent.TripperAgent.findPlacesToSleep ->
          com.embabel.tripper.agent.TripperAgent.postProcessHtml
goal: com.embabel.tripper.agent.TripperAgent.postProcessHtml from {it:com.embabel.tripper.agent.JourneyTravelBrief=TRUE, it:com.embabel.tripper.agent.Travelers=TRUE, it:com.embabel.tripper.agent.ItineraryIdeas=FALSE, hasRun_com.embabel.tripper.agent.TripperAgent.findPointsOfInterest=FALSE, it:com.embabel.tripper.agent.PointOfInterestFindings=FALSE, it:com.embabel.tripper.agent.ProposedTravelPlan=FALSE, hasRun_com.embabel.tripper.agent.TripperAgent.proposeTravelPlan=FALSE, it:com.embabel.tripper.agent.TravelPlan=FALSE, hasRun_com.embabel.tripper.agent.TripperAgent.findPlacesToSleep=FALSE, hasRun_com.embabel.tripper.agent.TripperAgent.postProcessHtml=FALSE, it:com.embabel.tripper.agent.TripperAgent$AcceptanceOfCost=FALSE, hasRun_com.embabel.tripper.agent.TripperAgent.confirmExpensiveOperation=FALSE, hasRun_com.embabel.tripper.agent.TripperAgent.researchPointsOfInterest=FALSE}
07:18:37.175 [task-1] INFO  MDR - [beautiful_grothendieck] <VERVE>: executing action com.embabel.tripper.agent.TripperAgent.findPointsOfInterest
07:18:37.240 [task-1] WARN  ToolConsumer - No tools found for tool group with role='ToolGroupRequirement(role=web)': null


▗▖  ▗▖ ▗▄▖     ▗▄▄▄▖▗▄▖  ▗▄▖ ▗▖    ▗▄▄▖    ▗▄▄▄▖ ▗▄▖ ▗▖ ▗▖▗▖  ▗▖▗▄▄▄
▐▛▚▖▐▌▐▌ ▐▌      █ ▐▌ ▐▌▐▌ ▐▌▐▌   ▐▌       ▐▌   ▐▌ ▐▌▐▌ ▐▌▐▛▚▖▐▌▐▌  █
▐▌ ▝▜▌▐▌ ▐▌      █ ▐▌ ▐▌▐▌ ▐▌▐▌    ▝▀▚▖    ▐▛▀▀▘▐▌ ▐▌▐▌ ▐▌▐▌ ▝▜▌▐▌  █
▐▌  ▐▌▝▚▄▞▘      █ ▝▚▄▞▘▝▚▄▞▘▐▙▄▄▖▗▄▄▞▘    ▐▌   ▝▚▄▞▘▝▚▄▞▘▐▌  ▐▌▐▙▄▄▀



▗▄▄▖ ▗▄▄▖  ▗▄▖ ▗▄▄▖  ▗▄▖ ▗▄▄▖ ▗▖   ▗▄▄▄▖    ▗▖  ▗▖▗▄▄▄▖ ▗▄▄▖ ▗▄▄▖ ▗▄▖ ▗▖  ▗▖▗▄▄▄▖▗▄▄▄▖ ▗▄▄▖▗▖ ▗▖▗▄▄▖  ▗▄▖▗▄▄▄▖▗▄▄▄▖ ▗▄▖ ▗▖  ▗▖
▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌▐▌   ▐▌       ▐▛▚▞▜▌  █  ▐▌   ▐▌   ▐▌ ▐▌▐▛▚▖▐▌▐▌     █  ▐▌   ▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌ █    █  ▐▌ ▐▌▐▛▚▖▐▌
▐▛▀▘ ▐▛▀▚▖▐▌ ▐▌▐▛▀▚▖▐▛▀▜▌▐▛▀▚▖▐▌   ▐▛▀▀▘    ▐▌  ▐▌  █   ▝▀▚▖▐▌   ▐▌ ▐▌▐▌ ▝▜▌▐▛▀▀▘  █  ▐▌▝▜▌▐▌ ▐▌▐▛▀▚▖▐▛▀▜▌ █    █  ▐▌ ▐▌▐▌ ▝▜▌
▐▌   ▐▌ ▐▌▝▚▄▞▘▐▙▄▞▘▐▌ ▐▌▐▙▄▞▘▐▙▄▄▖▐▙▄▄▖    ▐▌  ▐▌▗▄█▄▖▗▄▄▞▘▝▚▄▄▖▝▚▄▞▘▐▌  ▐▌▐▌   ▗▄█▄▖▝▚▄▞▘▝▚▄▞▘▐▌ ▐▌▐▌ ▐▌ █  ▗▄█▄▖▝▚▄▞▘▐▌  ▐▌





07:18:37.240 [task-1] WARN  ToolConsumer - No tools found for tool group with role='ToolGroupRequirement(role=maps)': null


▗▖  ▗▖ ▗▄▖     ▗▄▄▄▖▗▄▖  ▗▄▖ ▗▖    ▗▄▄▖    ▗▄▄▄▖ ▗▄▖ ▗▖ ▗▖▗▖  ▗▖▗▄▄▄
▐▛▚▖▐▌▐▌ ▐▌      █ ▐▌ ▐▌▐▌ ▐▌▐▌   ▐▌       ▐▌   ▐▌ ▐▌▐▌ ▐▌▐▛▚▖▐▌▐▌  █
▐▌ ▝▜▌▐▌ ▐▌      █ ▐▌ ▐▌▐▌ ▐▌▐▌    ▝▀▚▖    ▐▛▀▀▘▐▌ ▐▌▐▌ ▐▌▐▌ ▝▜▌▐▌  █
▐▌  ▐▌▝▚▄▞▘      █ ▝▚▄▞▘▝▚▄▞▘▐▙▄▄▖▗▄▄▞▘    ▐▌   ▝▚▄▞▘▝▚▄▞▘▐▌  ▐▌▐▙▄▄▀



▗▄▄▖ ▗▄▄▖  ▗▄▖ ▗▄▄▖  ▗▄▖ ▗▄▄▖ ▗▖   ▗▄▄▄▖    ▗▖  ▗▖▗▄▄▄▖ ▗▄▄▖ ▗▄▄▖ ▗▄▖ ▗▖  ▗▖▗▄▄▄▖▗▄▄▄▖ ▗▄▄▖▗▖ ▗▖▗▄▄▖  ▗▄▖▗▄▄▄▖▗▄▄▄▖ ▗▄▖ ▗▖  ▗▖
▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌▐▌   ▐▌       ▐▛▚▞▜▌  █  ▐▌   ▐▌   ▐▌ ▐▌▐▛▚▖▐▌▐▌     █  ▐▌   ▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌ █    █  ▐▌ ▐▌▐▛▚▖▐▌
▐▛▀▘ ▐▛▀▚▖▐▌ ▐▌▐▛▀▚▖▐▛▀▜▌▐▛▀▚▖▐▌   ▐▛▀▀▘    ▐▌  ▐▌  █   ▝▀▚▖▐▌   ▐▌ ▐▌▐▌ ▝▜▌▐▛▀▀▘  █  ▐▌▝▜▌▐▌ ▐▌▐▛▀▚▖▐▛▀▜▌ █    █  ▐▌ ▐▌▐▌ ▝▜▌
▐▌   ▐▌ ▐▌▝▚▄▞▘▐▙▄▞▘▐▌ ▐▌▐▙▄▞▘▐▙▄▄▖▐▙▄▄▖    ▐▌  ▐▌▗▄█▄▖▗▄▄▞▘▝▚▄▄▖▝▚▄▞▘▐▌  ▐▌▐▌   ▗▄█▄▖▝▚▄▞▘▝▚▄▞▘▐▌ ▐▌▐▌ ▐▌ █  ▗▄█▄▖▝▚▄▞▘▐▌  ▐▌





07:18:37.252 [task-1] WARN  ToolConsumer - No tools found for tool group with role='ToolGroupRequirement(role=weather)': null


▗▖  ▗▖ ▗▄▖     ▗▄▄▄▖▗▄▖  ▗▄▖ ▗▖    ▗▄▄▖    ▗▄▄▄▖ ▗▄▖ ▗▖ ▗▖▗▖  ▗▖▗▄▄▄
▐▛▚▖▐▌▐▌ ▐▌      █ ▐▌ ▐▌▐▌ ▐▌▐▌   ▐▌       ▐▌   ▐▌ ▐▌▐▌ ▐▌▐▛▚▖▐▌▐▌  █
▐▌ ▝▜▌▐▌ ▐▌      █ ▐▌ ▐▌▐▌ ▐▌▐▌    ▝▀▚▖    ▐▛▀▀▘▐▌ ▐▌▐▌ ▐▌▐▌ ▝▜▌▐▌  █
▐▌  ▐▌▝▚▄▞▘      █ ▝▚▄▞▘▝▚▄▞▘▐▙▄▄▖▗▄▄▞▘    ▐▌   ▝▚▄▞▘▝▚▄▞▘▐▌  ▐▌▐▙▄▄▀



▗▄▄▖ ▗▄▄▖  ▗▄▖ ▗▄▄▖  ▗▄▖ ▗▄▄▖ ▗▖   ▗▄▄▄▖    ▗▖  ▗▖▗▄▄▄▖ ▗▄▄▖ ▗▄▄▖ ▗▄▖ ▗▖  ▗▖▗▄▄▄▖▗▄▄▄▖ ▗▄▄▖▗▖ ▗▖▗▄▄▖  ▗▄▖▗▄▄▄▖▗▄▄▄▖ ▗▄▖ ▗▖  ▗▖
▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌▐▌   ▐▌       ▐▛▚▞▜▌  █  ▐▌   ▐▌   ▐▌ ▐▌▐▛▚▖▐▌▐▌     █  ▐▌   ▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌ █    █  ▐▌ ▐▌▐▛▚▖▐▌
▐▛▀▘ ▐▛▀▚▖▐▌ ▐▌▐▛▀▚▖▐▛▀▜▌▐▛▀▚▖▐▌   ▐▛▀▀▘    ▐▌  ▐▌  █   ▝▀▚▖▐▌   ▐▌ ▐▌▐▌ ▝▜▌▐▛▀▀▘  █  ▐▌▝▜▌▐▌ ▐▌▐▛▀▚▖▐▛▀▜▌ █    █  ▐▌ ▐▌▐▌ ▝▜▌
▐▌   ▐▌ ▐▌▝▚▄▞▘▐▙▄▞▘▐▌ ▐▌▐▙▄▞▘▐▙▄▄▖▐▙▄▄▖    ▐▌  ▐▌▗▄█▄▖▗▄▄▞▘▝▚▄▄▖▝▚▄▞▘▐▌  ▐▌▐▌   ▗▄█▄▖▝▚▄▞▘▝▚▄▞▘▐▌ ▐▌▐▌ ▐▌ █  ▗▄█▄▖▝▚▄▞▘▐▌  ▐▌





07:18:37.254 [task-1] INFO  MDR - [beautiful_grothendieck] (com.embabel.tripper.agent.TripperAgent.findPointsOfInterest-com.embabel.tripper.agent.ItineraryIdeas) 🖥️ MACRODATA REFINEMENT using LLM gpt-5-mini, creating ItineraryIdeas: LlmOptions(modelSelectionCriteria=null, model=gpt-5-mini, role=null, temperature=null, frequencyPenalty=null, maxTokens=null, presencePenalty=null, topK=null, topP=null, thinking=null, timeout=null) with tools add, ceiling, divide, floor, max, mean, min, multiply, round, subtract
07:18:37.370 [task-1] INFO  MDR - beautiful_grothendieck Spring AI ChatModel call:
using gpt-5-mini
  Messages  ⇩  .......................................................................................
  SYSTEM <Current date: 2025-11-24
  ----
  You are Hermes.
  Your persona: You are an expert travel planner.
  Your objective is Help the user plan an amazing trip, considering their requirements and preferences.
  Your voice: friendly and concise.
  ----
  2 travelers:
  bebe: Loves history and museums. Fascinated by Joan of Arc.
  pobi: Enjoys food and wine. Has a particular interest in cabernet.
  ----
  Knowledge cutoff: 2024-05
  >
  ....................................................................................................
  USER <                Consider the following travel brief for a journey from seoul to washington.
                  Journey from seoul to washington
  Dates: 2025-12-03 to 2025-12-23
  Brief: Relaxed road trip exploring countryside, history, food and wine.
  Transport preference: flying
                  Find points of interest that are relevant to the travel brief and travelers.
                  Use mapping tools to consider appropriate order and put a rough date
                  range for each point of interest.
                  Consider likely weather>
  Options ⇩  .........................................................................................
  null
prompt id: 'com.embabel.tripper.agent.TripperAgent.findPointsOfInterest-com.embabel.tripper.agent.ItineraryIdeas'
tools: [add, ceiling, divide, floor, max, mean, min, multiply, round, subtract]

A* 알고리즘에 따라 동적으로 계획을 세운다.

  • 어떨 땐 이미지를 검색하고, 어떨 땐 영상을 검색한다.

구조

+-------------------------------+
|           USER                |
|   TripRequest JSON 등         |
+---------------+---------------+
                |
                v
      +---------+-----------+
      |  Embabel Agent      |
      |    Platform         |
      +---------+-----------+
                |
                v
      +---------+-----------+
      |   GOAP PLANNER      |   <-- A* 기반 GOAP
      | (AStarGoapPlanner)  |
      +---------+-----------+
                |
        타입/조건 기반 분석
                |
                v
    +-----------+---------------------------------------------+
    |         WORLD STATE (타입 기반 상태)                    |
    |                                                         |
    |  {                                                      |
    |    TripRequest, UserPreferences, CandidateStops,        |
    |    StopResearch[], StayOptions, TripPlan ...            |
    |  }                                                      |
    +-----------+---------------------------------------------+
                ^
                |
                |  GOAP가 사용할 수 있는 액션들
                |
    +-----------+----------------------------------------------+
    |                 @Action SET (도메인 액션)                |
    |                                                          |
    |  A1: enrichRequest(TripRequest) -> UserPreferences       |
    |  A2: chooseStops(UserPreferences) -> CandidateStops      |
    |  A3: researchStops(CandidateStops) -> StopResearch[]     |
    |  A4: pickStays(Stops, Research) -> StayOptions           |
    |  A5: buildTripPlan(Stops, Stays, Research) -> TripPlan   |
    |      (@AchievesGoal)                                     |
    +-----------+----------------------------------------------+

GOAP:
- 현재 WORLD STATE와 액션의 입/출력 타입·조건을 보고
- A*로 "TripRequest -> TripPlan" 최적 액션 시퀀스 탐색

예: Plan = [A1 -> A2 -> A3 -> A4 -> A5]

                |
                v
      +---------+-----------+
      |  PLAN EXECUTOR      |
      |  (A1..A5 순서 실행) |
      +---------+-----------+
                |
                v
      +---------+-----------+
      |   TripPlan 반환     |
      +---------------------+

코드 살펴보기

BootStrap 지점

@SpringBootApplication
@ConfigurationPropertiesScan
@EnableAgents(
    loggingTheme = LoggingThemes.SEVERANCE,
    mcpServers = [McpServers.DOCKER_DESKTOP],
)   
class TripperApplication

fun main(args: Array<String>) {
    runApplication<TripperApplication>(*args)
}

@EnableAgents

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@AgentPlatform
public @interface EnableAgents {
    String loggingTheme() default "";

    /** @deprecated */
    @Deprecated(
        since = "0.1.3",
        forRemoval = true
    )
    String[] localModels() default {};

    String[] mcpServers() default {};
}
  • loggingTheme: 로그의 테마를 선택한다.
    • public static final String *STAR_WARS* = "starwars";
    • public static final String *SEVERANCE* = "severance";
    • public static final String *COLOSSUS* = "colossus”;
  • mcpServers: 에이전트가 연결할 MCP 서버 목록을 설정한다. 로컬 또는 외부 MCP 서버(MCP Tool Server)를 연결할 때 사용한다.
    • public static final String DOCKER = "docker-ce";
      • 로컬 Docker CE 기반 MCP 서버
    • public static final String DOCKER_DESKTOP = "docker-desktop";
      • Docker Desktop 환경에서 MCP 서버 사용
    • 로컬 llm은 0.1.3 버전부터 mcpServer로 통합된 듯 하다.

Controller 주요 코드

@Controller
class JourneyHtmxController(
    private val agentPlatform: AgentPlatform,
) {

// ... 중략
@PostMapping("/plan")
fun planJourney(
    @ModelAttribute form: JourneyPlanForm,
    model: Model
): String {
    val travelBrief = JourneyTravelBrief(
        from = form.from,
        to = form.to,
        transportPreference = form.transportPreference,
        brief = form.brief,
        departureDate = form.departureDate,
        returnDate = form.returnDate,
        dailyBudget = form.dailyBudget,
    )

    // 폼 여행자를 도메인 객체로 변환
    val travelersList = form.travelers
        .filter { it.name.isNotBlank() }
        .map { travelerForm ->
            Traveler(name = travelerForm.name, about = travelerForm.about)
        }
    val travelers = Travelers(travelers = travelersList)

    val agent = agentPlatform.agents().singleOrNull { it.name.lowercase().contains("trip") }
        ?: error("No travel agent found. Please ensure the tripper agent is registered.")

    val agentProcess = agentPlatform.createAgentProcessFrom(
        agent = agent,
        processOptions = ProcessOptions(
            verbosity = Verbosity(
                showPrompts = true,
                showLlmResponses = true,
            ),
            // 비용이 많이 들지만 괜찮습니다
            budget = Budget(
                tokens = Budget.DEFAULT_TOKEN_LIMIT * 3,
            )
        ),
        travelBrief, travelers
    )

    model.addAttribute("travelBrief", travelBrief)
    GenericProcessingValues(
        agentProcess = agentProcess,
        pageTitle = "Planning your journey",
        detail = travelBrief.brief,
        resultModelKey = "travelPlan",
        successView = "journey-plan",
    ).addToModel(model)
    agentPlatform.start(agentProcess)
    return "common/processing"
}
  • agentPlan 정보를 사용자로 부터 입력받는 부분이다.
class JourneyHtmxController(
    private val agentPlatform: AgentPlatform,
) {
  • SSE 과정을 실시간으로 보여준다.
agentPlatform.agents().singleOrNull { it.name.lowercase().contains("trip") }
        ?: error("No travel agent found. Please ensure the tripper agent is registered.")
  • 스프링의 DI 컨테이너처럼, agentPlatform이란 곳에서 agent를 찾는다.
    val agentProcess = agentPlatform.createAgentProcessFrom(
        agent = agent,
        processOptions = ProcessOptions(
            verbosity = Verbosity(
                showPrompts = true,
                showLlmResponses = true,
            ),
            // 비용이 많이 들지만 괜찮습니다
            budget = Budget(
                tokens = Budget.DEFAULT_TOKEN_LIMIT * 3,
            )
        ),
        travelBrief, travelers
    )
  • AgentProcess라는 것을 만들고 Agent를 실행한다.
profile
No Trying. Just Doing. 시도에 머무르지 않고, 개발이라는 '일'을 확실하게 해냅니다.

0개의 댓글