--- trigger: always_on --- # Project Context: Loyalty Agent Service This repository is a local development workspace for a loyalty AI assistant. It has a React chat UI at the repository root and two Spring Boot backend modules managed by a parent Maven project. ## Architecture ### Frontend: root directory - Stack: React 19, Vite, TypeScript, Tailwind CSS, shadcn-style UI primitives, Zustand, `@stomp/stompjs`, `react-markdown`, `remark-gfm`. - Entry point: `src/main.tsx` renders `src/App.tsx`, which renders `ChatLayout`. - Dev port: `9333` from `vite.config.ts`. - Main UI flow: - `src/store/useChatStore.ts` owns chat state and STOMP connection. - WebSocket URL is currently hard-coded as `ws://localhost:9332/ws`. - User messages are published to `/app/chat`. - Agent events are received from `/user/queue/chat-events`. - `CampaignList` fetches campaign data from `http://localhost:9332/api/v1/agent/campaigns` and campaign rules from `http://localhost:9332/api/v1/agent/campaigns/{campaignId}/rules`. ### `loyalty-agent` - Spring Boot application: `dev.sonpx.loyalty.agent.LoyaltyAgentApplication`. - Runtime port: `9332`. - Role: BFF and AI orchestration layer for the frontend. - Key files: - `controller/AgentController.java`: STOMP chat endpoint and REST campaign proxy endpoints. - `service/LoyaltyAgentService.java`: builds a Spring AI `ChatClient`, streams LLM output, wraps tool calls, and emits tool status events. - `config/WebSocketConfig.java`: STOMP endpoint `/ws`, application prefix `/app`, user destination prefix `/user`, broker prefixes `/topic` and `/queue`. - `config/AgentConfig.java`: OAuth2-enabled `RestClient` for the loyalty core API. - `controller/ConversationController.java`: simple in-memory conversation/message REST endpoints. - AI configuration: - Uses Ollama at `http://192.168.99.10:11434`. - Default model in `application.yml`: `qwen3.5:4b`. - MCP client connects to `${MCP_SERVER_URL:http://localhost:9331/sse}`. - Agent event types sent to the UI: `TOKEN`, `TOOL_STATUS`, `DONE`, `ERROR`. ### `loyalty-mcp-server` - Spring Boot application: `dev.sonpx.loyalty.mcp.McpServerApplication`. - Runtime port: `9331`. - Role: MCP tool server for loyalty operations. The agent calls this service through Spring AI MCP SSE. - MCP endpoints from `application.yml`: - SSE endpoint: `/sse` - Message endpoint: `/message` - Key files: - `config/McpServerConfig.java`: registers MCP tool objects. - `tool/LoyaltyAgentTools.java`: member tiers, point pools, campaign creation, static attributes. - `tool/reward/CampaignTools.java`: campaign listing and campaign draft lifecycle. - `tool/reward/CampaignRuleTools.java`: campaign rule listing, creation, editing, and draft rule attachment. - `tool/pool/PointPoolTools.java`: pool and SOP draft tools. - `draft/DraftSessionManager.java`: in-memory draft sessions keyed by `conversationId`, with a 1-hour TTL. - `draft/DependencyValidator.java`: validates draft dependencies before tools mutate or submit drafts. - API clients call the loyalty core service with OAuth2 client credentials. ## External Services And Environment Create `.env` from `.env.example` for local development. Important variables: - `KEYCLOAK_CLIENT_SECRET` - `KEYCLOAK_TOKEN_URI` - `LOYALTY_CORE_BASE_URL` - `MCP_SERVER_URL` - `VITE_AGENT_HTTP_URL` - `VITE_AGENT_WS_URL` Current defaults: - Keycloak token URI: `http://192.168.99.235/auth/realms/ols-cn-sit/protocol/openid-connect/token` - Loyalty core base URL: `http://192.168.99.242:8081` - MCP server URL: `http://localhost:9331` - Agent HTTP URL: `http://localhost:9332` - Agent WS URL: `ws://localhost:9332` Note: some frontend code still uses hard-coded localhost URLs instead of the `VITE_*` variables. ## Local Startup Use the root script for the full local stack: ```bash ./start-all.sh ``` What the script does: 1. Loads `.env` if present. 2. Kills processes on ports `9331`, `9332`, and `9333`. 3. Starts `loyalty-mcp-server` and waits for port `9331`. 4. Starts a `nodemon` compile watcher for the MCP server. 5. Starts `loyalty-agent` and waits for port `9332`. 6. Starts a `nodemon` compile watcher for the agent. 7. Starts the frontend with `pnpm dev` on port `9333`. 8. Writes logs to `log/`. Manual startup: ```bash ./mvnw spring-boot:run -pl loyalty-mcp-server ./mvnw spring-boot:run -pl loyalty-agent pnpm dev ``` The script currently invokes `-Pgen`, but the checked-in POM files do not define a `gen` Maven profile. Do not assume OpenAPI generation is wired unless you verify or add that profile. ## Build, Lint, And Tests Frontend: ```bash pnpm install pnpm lint pnpm build ``` Backend: ```bash ./mvnw test ./mvnw test -pl loyalty-agent ./mvnw test -pl loyalty-mcp-server ``` Relevant tests: - `loyalty-agent/src/test/java/dev/sonpx/loyalty/agent/controller/AgentControllerTest.java` - `loyalty-agent/src/test/java/dev/sonpx/loyalty/agent/domain/AgentEventTest.java` - `loyalty-mcp-server/src/test/java/dev/sonpx/loyalty/mcp/tool/McpOrchestratorTest.java` - `loyalty-mcp-server/src/test/java/dev/sonpx/loyalty/mcp/tool/reward/CampaignRuleMapperTest.java` - `loyalty-mcp-server/src/test/java/dev/sonpx/loyalty/mcp/tool/LoyaltyAgentToolsRealApiTest.java` `LoyaltyAgentToolsRealApiTest` may depend on real external API availability and credentials. ## Development Guidelines For Agents - Keep the boundary clear: - `loyalty-agent` owns frontend-facing chat orchestration and REST proxy endpoints. - `loyalty-mcp-server` owns tool implementations and loyalty core API access. - The frontend owns STOMP state, message rendering, chat layout, and campaign context panels. - Preserve the STOMP contract unless intentionally changing both sides: - Client publishes to `/app/chat`. - Server handles `@MessageMapping("/chat")`. - Server sends events to `/user/queue/chat-events`. - Preserve `conversationId` propagation. MCP draft tools depend on it to keep draft campaign/pool/rule state in `DraftSessionManager`. - Tool responses may contain `presentation.content`; the agent system prompt requires displaying that content exactly. - Reuse existing UI primitives in `src/components/ui` and existing Tailwind conventions. - Do not introduce persistent storage assumptions. Conversation repositories and draft sessions are currently in memory. - Do not hard-code new service URLs when a matching environment variable already exists. - Be careful with ports: MCP is `9331`, agent is `9332`, frontend is `9333`.