# Architecture — Loyalty Agent Service ## System Overview AI-powered assistant for managing a Loyalty Platform, built on the **Model Context Protocol (MCP)**. ```text ┌──────────────────┐ │ React/Vite UI │ │ :9333 │ └────────┬─────────┘ │ WebSocket / STOMP ▼ ┌──────────────────┐ │ Loyalty Agent │ │ :9332 │ │ │ │ LLM Orchestrator │ │ MCP Client │ │ Conversation │ │ PostgreSQL │ └────────┬─────────┘ │ MCP SSE (/sse, /message) ▼ ┌──────────────────┐ │ Loyalty MCP │ │ Server :9331 │ │ │ │ 25+ MCP Tools │ └────────┬─────────┘ │ REST + OAuth2 (Client Credentials) ▼ ┌──────────────────┐ │ Loyalty Core API │ │ :8081 │ └──────────────────┘ Keycloak ────── OAuth2 Token Provider PostgreSQL ──── Conversation Persistence Ollama ──────── Local LLM (Qwen3.5:4b) ``` ## Technology Stack | Technology | Version | Purpose | |----------------|---------|----------------------------| | Java | 25 | Runtime | | Spring Boot | 4.0.0 | Framework | | Spring AI | 2.0.0 | LLM & MCP integration | | Ollama | latest | Local LLM hosting | | Qwen | 3.5:4b | Primary LLM model | | PostgreSQL | 15+ | Conversation persistence | | Keycloak | latest | OAuth2/OIDC provider | | React + Vite | 19/6 | Frontend UI | ## Module Architecture ### loyalty-mcp-server (`:9331`) MCP Server exposing Loyalty Platform capabilities as MCP tools via SSE transport. **Responsibilities:** - Register 25+ MCP tools (campaign, rule, customer, transaction, etc.) - Validate tool inputs - Proxy requests to Loyalty Core API with OAuth2 client credentials - Return structured `Result` with agent instructions for LLM formatting - Handle errors (sanitize stack traces, structured error responses) **Key Classes:** - `CampaignTools` / `CampaignRuleTools` — Primary tool registration - `CampaignService` / `CampaignRuleService` — Business logic - `RestClientConfig` — OAuth2 REST client factory - `Result` — Standard response wrapper with `_agent_instruction` ### loyalty-agent (`:9332`) AI Agent / BFF — Orchestrates LLM, MCP, conversations, and WebSocket. **Responsibilities:** - Accept user messages via WebSocket/STOMP - Classify intent (regex-based, zero LLM latency) - Route to appropriate executor (SimpleAgent, Workflow, DirectChat) - Stream LLM responses back to frontend - Persist conversations to PostgreSQL **Key Architecture:** ```text AgentController (@MessageMapping /chat) ↓ LoyaltyAgentService ↓ AgentOrchestrator ├── Active Workflow? → WorkflowExecutor (multi-turn creation) ├── IntentClassifier → QUERY → SimpleAgentExecutor (ReAct + tools) ├── IntentClassifier → CONVERSATION → SimpleAgentExecutor (direct chat) └── IntentClassifier → CREATE_* → WorkflowExecutor ``` **Intent Classification (regex-based):** | Intent | Trigger Examples | |--------|-----------------| | `QUERY` | "tìm chiến dịch", "danh sách rule" | | `CONVERSATION` | "xin chào", "cảm ơn" | | `CREATE_CAMPAIGN` | "tạo chiến dịch mới" | | `CREATE_RULE` | "thêm thể lệ" | **Tool Scope Enforcement:** | Scope | Allowed Operations | |-------|-------------------| | `READ_ONLY` | search, get, find, count, check | | `CREATE_CAMPAIGN` | READ_ONLY + createCampaign + generateCampaignId | | `CREATE_RULE` | READ_ONLY + createRule + generateRuleId | ### Frontend (`:9333`) React/Vite conversational UI with STOMP WebSocket. **Key Features:** - STOMP over WebSocket for real-time messaging - Token streaming (character-by-character LLM output) - Tool execution status indicators - Conversation management (create, list, delete, rename) - Offline message queue with exponential backoff reconnect ## Security Architecture ```text Frontend ──(WebSocket)──> Agent ──(MCP SSE)──> MCP Server ──(OAuth2)──> Core API │ ▼ Keycloak ``` **Principles:** 1. **LLM is never an authorization boundary** — tool scope is enforced by `AgentToolScope` before tools reach the LLM 2. **OAuth2 Client Credentials** — MCP Server authenticates to Core API via Keycloak 3. **Tool result sanitization** — `ToolInterceptor` strips Java stack traces 4. **Input validation** — All tool parameters validated before downstream calls ## Data Flow ### Chat Request Flow ```text 1. User types message in React UI 2. STOMP message → /app/chat → AgentController 3. AgentController → LoyaltyAgentService.chat() 4. AgentOrchestrator classifies intent 5. Executor starts LLM streaming with tools 6. LLM selects tool → ToolInterceptor wraps & calls MCP 7. MCP Server executes tool → calls Core API 8. Tool result → LLM reasons → generates response 9. Response streamed back via STOMP → /user/queue/chat-events 10. React UI renders tokens in real-time ``` ## Resilience Patterns | Pattern | Implementation | |---------|---------------| | MCP auto-reconnect | `ToolInterceptor.attemptReconnect()` | | WebSocket reconnect | `StompService` exponential backoff + jitter | | Request timeout | `SimpleAgentExecutor` configurable via `agent.request-timeout-seconds` | | Tool result truncation | `ToolInterceptor` caps results > 4000 chars | | Error sanitization | `ToolInterceptor` strips stack traces | | Graceful shutdown | `server.shutdown: graceful` on both modules | | LLM search noise filtering | `SearchUtils.sanitizeSearch()` |