AI Service
This is the reference page for lumie-backend/modules/ai, the tenant-scoped module that stores conversation state, proxies chat requests to chatbot-svc, validates read-only SQL, and executes confirmed tools inside the monolith.
Source Paths
| Path | Role |
|---|---|
lumie-backend/modules/ai/src/main/java/com/lumie/ai/adapter/in/web/{ChatController,ConversationController,InternalChatbotController}.java | Public chat endpoints, conversation reads, and worker callback surface |
lumie-backend/modules/ai/src/main/java/com/lumie/ai/adapter/out/external/ChatbotClient.java | HTTP proxy from the monolith to chatbot-svc, including SSE passthrough |
lumie-backend/modules/ai/src/main/java/com/lumie/ai/application/service/{ChatService,ConversationQueryService,SchemaDiscoveryService,SqlValidator,ScheduledTaskExecutor}.java | Persistence, schema discovery, SQL safety, and scheduled-task execution |
lumie-backend/modules/ai/src/main/java/com/lumie/ai/domain/entity/{Conversation,ChatMessage,ScheduledTask}.java | AI conversation, message, and scheduled-task aggregates |
lumie-backend/app/src/main/java/com/lumie/app/config/internal/InternalHmacAuthFilter.java | HMAC protection for /internal/chatbot/** |
lumie-backend/app/src/main/resources/db/migration/public/{V18__rls_baseline,V27__langgraph_schema}.sql | Base AI tables and LangGraph-related schema additions |
Public Surface
| Endpoint | Purpose |
|---|---|
POST /v1/chat | Non-streaming chat request proxied to chatbot-svc |
POST /v1/chat/stream | SSE chat stream proxied to chatbot-svc |
POST /v1/chat/confirm | Resume a pending tool/action after user confirmation |
GET /v1/conversations, GET /v1/conversations/{id}, DELETE /v1/conversations/{id} | Staff-only conversation list, detail, and soft-delete operations |
POST /internal/chatbot/query | Worker callback for read-only SQL execution inside the monolith |
POST /internal/chatbot/tools/{toolName} | Worker callback for confirmed tool execution |
GET /internal/chatbot/schema | Worker callback to fetch a schema description |
GET /internal/chatbot/history | Worker callback to fetch recent conversation history |
POST /internal/chatbot/pending-action, POST /internal/chatbot/save-message, POST /internal/chatbot/conversations | Worker callback endpoints to persist pending actions, messages, and new conversations |
All public chat and conversation endpoints reject Role.STUDENT; they are staff-only surfaces.
Internal Surface And Dependencies
| Surface | Role |
|---|---|
chatbot-svc | Owns graph orchestration and model interaction; the monolith never runs a local LLM fallback anymore |
/internal/chatbot/** | HMAC-protected callback surface that keeps SQL, writes, and persistence inside the monolith |
SqlValidator | Rejects non-SELECT SQL, semicolons, DDL/DML keywords, and catalog access |
ToolRegistry | Runtime registry for confirmed write tools executed on behalf of the worker |
TenantService.listActiveTenants() | Cross-tenant source used by ScheduledTaskExecutor to run due tasks |
Aggregates And Tables
| Aggregate | Notes |
|---|---|
Conversation | Conversation shell with owning user and active flag |
ChatMessage | Stored assistant, user, tool-call, and pending-action message records |
ScheduledTask | Tenant-scoped scheduled tool execution state |
Runtime Flow
Contract Notes
Confirmed write tools are explicitly allowlisted at the internal controller boundary.
// lumie-backend/modules/ai/src/main/java/com/lumie/ai/adapter/in/web/InternalChatbotController.java
private static final Set<String> ALLOWED_TOOLS = Set.of(
"create_announcement", "send_sms", "send_telegram", "schedule_task");
Even an HMAC-authenticated worker request is rejected if the requested tool is outside that set or absent from ToolRegistry.
Example Contracts
These examples come directly from ChatController, ChatReplyResponse, ChatbotClient, InternalChatbotController, and chatbot-svc/src/graph/tools.py.
Confirm A Pending Chat Action
POST /v1/chat/confirm
Idempotency-Key: confirm-99
Content-Type: application/json
{
"messageId": 99,
"confirmed": true
}
HTTP/1.1 200 OK
{
"conversationId": 10,
"message": "Announcement created.",
"pendingAction": null
}
One easy-to-miss detail: ChatReplyResponse.pendingAction is a String. When the worker returns a follow-up pending action, ChatbotClient.toReply(...) serializes that worker object as a JSON string, not as a nested object.
Internal Tool Execution Payload
The worker tool schema uses camelCase argument keys, and InternalChatbotController.executeTool(...) expects those keys unchanged inside arguments.
POST /internal/chatbot/tools/send_sms
Content-Type: application/json
{
"tenantSlug": "acme",
"tenantId": 15,
"userId": 7,
"arguments": {
"phoneNumber": "01012345678",
"message": "Tomorrow's class starts at 10:00."
}
}
HTTP/1.1 200 OK
{
"success": true,
"data": "<tool-result-json>",
"error": null
}
Failure, Retry, And Observability
ChatbotClientpins backend-to-worker HTTP to HTTP/1.1 because the worker is not compatible with the default JDK HTTP/2 negotiation path.chat/confirmsupports optionalIdempotency-Key; plainchatandchat/streamdo not use the idempotency layer.SqlValidatorblocks write SQL, multiple statements,pg_*, andinformation_schemaaccess before any query reaches the database.InternalChatbotControllerre-establishes tenant and user context for every worker callback so RLS and user-aware tool logic run inside the monolith.ScheduledTaskExecutorloops active tenants, runs each due task under tenant context, and marks failed tasksFAILED.ChatServiceis persistence-only. The code comments explicitly state that the previous in-monolith Spring AI path was removed in the Phase 5 cutover.
Verification
cd lumie-backend
./gradlew :modules:ai:test
./gradlew :modules:ai:test --tests '*Chat*'
./gradlew :modules:ai:test --tests '*ScheduledTask*'
Expected success signals:
- Gradle exits with
BUILD SUCCESSFUL, and the AI module tests still cover chat, confirm, and scheduled-task flows. InternalChatbotControllerstill accepts onlycreate_announcement,send_sms,send_telegram, andschedule_task, and the worker tool schema still uses camelCase keys such asphoneNumberandtoolName.