Skip to main content

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

PathRole
lumie-backend/modules/ai/src/main/java/com/lumie/ai/adapter/in/web/{ChatController,ConversationController,InternalChatbotController}.javaPublic chat endpoints, conversation reads, and worker callback surface
lumie-backend/modules/ai/src/main/java/com/lumie/ai/adapter/out/external/ChatbotClient.javaHTTP 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}.javaPersistence, schema discovery, SQL safety, and scheduled-task execution
lumie-backend/modules/ai/src/main/java/com/lumie/ai/domain/entity/{Conversation,ChatMessage,ScheduledTask}.javaAI conversation, message, and scheduled-task aggregates
lumie-backend/app/src/main/java/com/lumie/app/config/internal/InternalHmacAuthFilter.javaHMAC protection for /internal/chatbot/**
lumie-backend/app/src/main/resources/db/migration/public/{V18__rls_baseline,V27__langgraph_schema}.sqlBase AI tables and LangGraph-related schema additions

Public Surface

EndpointPurpose
POST /v1/chatNon-streaming chat request proxied to chatbot-svc
POST /v1/chat/streamSSE chat stream proxied to chatbot-svc
POST /v1/chat/confirmResume 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/queryWorker callback for read-only SQL execution inside the monolith
POST /internal/chatbot/tools/{toolName}Worker callback for confirmed tool execution
GET /internal/chatbot/schemaWorker callback to fetch a schema description
GET /internal/chatbot/historyWorker callback to fetch recent conversation history
POST /internal/chatbot/pending-action, POST /internal/chatbot/save-message, POST /internal/chatbot/conversationsWorker 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

SurfaceRole
chatbot-svcOwns 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
SqlValidatorRejects non-SELECT SQL, semicolons, DDL/DML keywords, and catalog access
ToolRegistryRuntime 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

AggregateNotes
ConversationConversation shell with owning user and active flag
ChatMessageStored assistant, user, tool-call, and pending-action message records
ScheduledTaskTenant-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

  • ChatbotClient pins backend-to-worker HTTP to HTTP/1.1 because the worker is not compatible with the default JDK HTTP/2 negotiation path.
  • chat/confirm supports optional Idempotency-Key; plain chat and chat/stream do not use the idempotency layer.
  • SqlValidator blocks write SQL, multiple statements, pg_*, and information_schema access before any query reaches the database.
  • InternalChatbotController re-establishes tenant and user context for every worker callback so RLS and user-aware tool logic run inside the monolith.
  • ScheduledTaskExecutor loops active tenants, runs each due task under tenant context, and marks failed tasks FAILED.
  • ChatService is 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.
  • InternalChatbotController still accepts only create_announcement, send_sms, send_telegram, and schedule_task, and the worker tool schema still uses camelCase keys such as phoneNumber and toolName.