Skip to content

Developer Guide

This section covers the data model, configuration reference, AI pipeline architecture, required integration points, and extension points available to developers integrating AI Support into an application.

Data Model

AI Support persists entities across three domains. The conversation domain covers sessions, messages, participants, and draft messages. The configuration domain covers assistants, LLM definitions, prompts, and categories. The knowledge domain covers documents and their vector embeddings. Supporting entities handle chat memory, LLM exchange audit records, message attachments, prompt revisions, and channel integrations (WhatsApp, Facebook, Instagram).

Channels use joined-table inheritance: the common columns live in as_channel, discriminated by channel_type, and the platform-specific credentials live in as_channel_whatsapp (phone_number, meta_phone_id, access_token), as_channel_facebook (page_id, access_token), and as_channel_instagram (instagram_account_id, page_id, access_token). Each child table's primary key is also a foreign key to as_channel.

A session records three activity timestamps rather than one: last_public_activity is updated by messages everyone can see, last_admin_activity by administrator-only messages, and last_total_activity by both. The scheduled session closer selects on last_total_activity.

---
config:
  layout: elk
---
erDiagram
    as_session {
        bigint  id                   PK
        string  user_id
        string  type
        string  status
        instant event_date
        int     rating
        string  rating_comment
        bigint  assistant_id         FK
        string  summary
        instant last_public_activity
        instant last_admin_activity
        instant last_total_activity
        bigint  channel_id           FK
        string  external_id
    }
    as_message {
        bigint  id                     PK
        bigint  session_id             FK
        string  user_id
        string  sender_type
        text    content
        text    context
        instant event_date
        boolean profanity
        boolean processed
        boolean admin_only
        boolean partial
        boolean failed_while_generating
        boolean early_stop
        string  early_stopped_by
        int     content_token_count
        bigint  answered_by            FK
        bigint  exchange_id
        string  external_id
    }
    as_participant {
        bigint  id                       PK
        bigint  session_id               FK
        string  user_id
        boolean is_admin
        boolean is_assistant
        boolean is_active
        boolean subscribed
        instant join_date
        instant leave_date
        string  added_by
        string  removed_by
        instant last_public_activity_seen
        bigint  last_message_seen        FK
        bigint  last_message_sent        FK
    }
    as_channel {
        bigint  id                 PK
        string  name
        string  channel_type
        boolean enabled
        boolean failed
        instant last_status_change
        bigint  assistant_id       FK
    }
    as_assistant {
        bigint  id               PK
        string  name
        boolean enabled
        boolean default_assistant
        boolean advanced_rag
        bigint  prompt_id        FK
        bigint  llm_id           FK
    }
    as_llm {
        bigint  id            PK
        string  name
        string  type
        boolean enabled
        boolean system
        string  configuration
    }
    as_prompt {
        bigint  id      PK
        string  name
        text    content
        boolean system
        boolean enabled
    }
    as_category {
        bigint  id          PK
        string  name
        string  description
        boolean enabled
    }
    as_document {
        bigint  id            PK
        string  name
        boolean enabled
        boolean partial
        boolean external_data
        string  external_id
        instant last_update
    }
    as_embedding {
        uuid    id        PK
        text    text
        jsonb   metadata
        vector  embedding
    }
    as_chat_memory {
        bigint  id      PK
        text    message
    }
    as_llm_exchange {
        bigint  id                  PK
        bigint  session_id
        bigint  message_id
        bigint  assistant_id
        string  assistant_name
        bigint  llm_id
        string  llm_name
        string  llm_type
        bigint  prompt_id
        text    request
        text    response
        int     input_token_count
        int     output_token_count
        int     prompt_token_count
        instant request_date
        instant response_date
        boolean advanced_rag
        json    embedding_chunks
    }
    as_draft_message {
        bigint  id           PK
        string  user_id
        bigint  session_id   FK
        bigint  assistant_id FK
        text    message
    }
    as_message_attachment {
        bigint  id         PK
        bigint  message_id FK
        string  filename
        string  type
        string  mime_type
    }

    as_session      ||--o{ as_message          : "contains"
    as_session      ||--o{ as_participant      : "has"
    as_session      }o--|| as_assistant        : "uses"
    as_session      }o--o| as_channel          : "arrived through"
    as_channel      }o--|| as_assistant        : "answered by"
    as_message      }o--o| as_message          : "answered by"
    as_message      ||--o{ as_message_attachment : "has"
    as_assistant    }o--|| as_llm              : "uses"
    as_assistant    }o--|| as_prompt           : "uses"
    as_assistant    }o--o{ as_category         : "scoped to"
    as_document     }o--o{ as_category         : "tagged with"
    as_document     ||--o{ as_embedding        : "chunked into"
Hold "Alt" / "Option" to enable pan & zoom

Database Schema

AI Support manages twenty-one tables. The schema below represents the DDL generated by Hibernate for PostgreSQL, which is the only supported database for this appjar: the as_embedding table requires the pgvector extension.

Two of the tables are not application entities. as_prompt_aud and revinfo are Hibernate Envers audit tables, and they are what implements the prompt revision history described in the Prompts page: every save appends a row to as_prompt_aud, and revtype distinguishes an insert from an update or a delete.

CREATE EXTENSION IF NOT EXISTS vector;

-- Configuration ------------------------------------------------------------

CREATE TABLE as_llm (
    id            BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
    name          VARCHAR(255) NOT NULL,
    type          VARCHAR(255) NOT NULL,
    configuration VARCHAR(255),
    enabled       BOOLEAN NOT NULL,
    system        BOOLEAN NOT NULL,
    CONSTRAINT pk_as_llm      PRIMARY KEY (id),
    CONSTRAINT ck_as_llm_type CHECK (type IN ('OPEN_AI', 'OLLAMA'))
);

CREATE TABLE as_prompt (
    id             BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
    name           VARCHAR(255) NOT NULL,
    content        TEXT NOT NULL,
    change_comment TEXT,
    modified_by    VARCHAR(255),
    enabled        BOOLEAN NOT NULL,
    system         BOOLEAN NOT NULL,
    CONSTRAINT pk_as_prompt PRIMARY KEY (id)
);

CREATE TABLE as_category (
    id          BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
    name        VARCHAR(255) NOT NULL,
    description VARCHAR(255) NOT NULL,
    enabled     BOOLEAN NOT NULL,
    CONSTRAINT pk_as_category PRIMARY KEY (id)
);

CREATE TABLE as_assistant (
    id                BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
    name              VARCHAR(255) NOT NULL,
    llm_id            BIGINT NOT NULL,
    prompt_id         BIGINT NOT NULL,
    enabled           BOOLEAN NOT NULL,
    default_assistant BOOLEAN NOT NULL,
    advanced_rag      BOOLEAN DEFAULT FALSE,
    CONSTRAINT pk_as_assistant        PRIMARY KEY (id),
    CONSTRAINT fk_as_assistant_llm    FOREIGN KEY (llm_id)    REFERENCES as_llm (id),
    CONSTRAINT fk_as_assistant_prompt FOREIGN KEY (prompt_id) REFERENCES as_prompt (id)
);

CREATE TABLE as_assistant_categories (
    assistant_entity_id BIGINT NOT NULL,
    categories_id       BIGINT NOT NULL,
    CONSTRAINT fk_as_assistant_categories_assistant FOREIGN KEY (assistant_entity_id) REFERENCES as_assistant (id),
    CONSTRAINT fk_as_assistant_categories_category  FOREIGN KEY (categories_id)       REFERENCES as_category (id)
);

-- Channels (joined-table inheritance) --------------------------------------

CREATE TABLE as_channel (
    id                 BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
    name               VARCHAR(255),
    channel_type       VARCHAR(31) NOT NULL,
    enabled            BOOLEAN NOT NULL,
    failed             BOOLEAN NOT NULL,
    last_status_change TIMESTAMP(6) WITH TIME ZONE,
    assistant_id       BIGINT,
    CONSTRAINT pk_as_channel           PRIMARY KEY (id),
    CONSTRAINT fk_as_channel_assistant FOREIGN KEY (assistant_id) REFERENCES as_assistant (id),
    CONSTRAINT ck_as_channel_type      CHECK (channel_type IN ('FACEBOOK', 'INSTAGRAM', 'WHATSAPP'))
);

CREATE TABLE as_channel_whatsapp (
    id            BIGINT NOT NULL,
    phone_number  VARCHAR(255),
    meta_phone_id VARCHAR(255),
    access_token  TEXT,
    CONSTRAINT pk_as_channel_whatsapp PRIMARY KEY (id),
    CONSTRAINT fk_as_channel_whatsapp FOREIGN KEY (id) REFERENCES as_channel (id)
);

CREATE TABLE as_channel_facebook (
    id           BIGINT NOT NULL,
    page_id      VARCHAR(255),
    access_token TEXT,
    CONSTRAINT pk_as_channel_facebook PRIMARY KEY (id),
    CONSTRAINT fk_as_channel_facebook FOREIGN KEY (id) REFERENCES as_channel (id)
);

CREATE TABLE as_channel_instagram (
    id                   BIGINT NOT NULL,
    instagram_account_id VARCHAR(255),
    page_id              VARCHAR(255),
    access_token         TEXT,
    CONSTRAINT pk_as_channel_instagram PRIMARY KEY (id),
    CONSTRAINT fk_as_channel_instagram FOREIGN KEY (id) REFERENCES as_channel (id)
);

-- Conversations ------------------------------------------------------------

CREATE TABLE as_session (
    id                   BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
    user_id              VARCHAR(255) NOT NULL,
    type                 VARCHAR(255) NOT NULL,
    status               VARCHAR(255) NOT NULL,
    summary              VARCHAR(255),
    rating               INTEGER NOT NULL,
    rating_comment       VARCHAR(255),
    event_date           TIMESTAMP(6) WITH TIME ZONE NOT NULL,
    last_public_activity TIMESTAMP(6) WITH TIME ZONE,
    last_admin_activity  TIMESTAMP(6) WITH TIME ZONE,
    last_total_activity  TIMESTAMP(6) WITH TIME ZONE,
    assistant_id         BIGINT NOT NULL,
    channel_id           BIGINT,
    external_id          VARCHAR(255),
    CONSTRAINT pk_as_session           PRIMARY KEY (id),
    CONSTRAINT fk_as_session_assistant FOREIGN KEY (assistant_id) REFERENCES as_assistant (id),
    CONSTRAINT fk_as_session_channel   FOREIGN KEY (channel_id)   REFERENCES as_channel (id),
    CONSTRAINT ck_as_session_status    CHECK (status IN ('ACTIVE', 'ARCHIVED', 'CLOSED')),
    CONSTRAINT ck_as_session_type      CHECK (type IN ('AUTOMATIC', 'MANUAL'))
);

CREATE TABLE as_message (
    id                      BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
    session_id              BIGINT NOT NULL,
    user_id                 VARCHAR(255) NOT NULL,
    sender_type             VARCHAR(255) NOT NULL,
    content                 TEXT NOT NULL,
    context                 TEXT,
    event_date              TIMESTAMP(6) WITH TIME ZONE NOT NULL,
    content_token_count     INTEGER,
    mentions                VARCHAR(255)[],
    profanity               BOOLEAN NOT NULL,
    processed               BOOLEAN NOT NULL,
    admin_only              BOOLEAN NOT NULL,
    partial                 BOOLEAN,
    failed_while_generating BOOLEAN,
    early_stop              BOOLEAN,
    early_stopped_by        VARCHAR(255),
    answered_by             BIGINT,
    exchange_id             BIGINT,
    external_id             VARCHAR(255),
    CONSTRAINT pk_as_message             PRIMARY KEY (id),
    CONSTRAINT fk_as_message_session     FOREIGN KEY (session_id)  REFERENCES as_session (id),
    CONSTRAINT fk_as_message_answered_by FOREIGN KEY (answered_by) REFERENCES as_message (id),
    CONSTRAINT ck_as_message_sender_type CHECK (sender_type IN ('WHATSAPP', 'FACEBOOK', 'INSTAGRAM',
                                                                'MANUAL', 'AUTOMATIC', 'ADMINISTRATOR', 'SYSTEM'))
);

CREATE TABLE as_message_attachment (
    id                 BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
    message_id         BIGINT,
    filename           VARCHAR(255) NOT NULL,
    type               VARCHAR(255) NOT NULL,
    mime_type          VARCHAR(255) NOT NULL,
    content_store_type VARCHAR(255) NOT NULL,
    text_content       OID,
    kb_size            INTEGER,
    CONSTRAINT pk_as_message_attachment         PRIMARY KEY (id),
    CONSTRAINT fk_as_message_attachment_message FOREIGN KEY (message_id) REFERENCES as_message (id),
    CONSTRAINT ck_as_message_attachment_type    CHECK (type IN ('TEXT', 'IMAGE', 'AUDIO', 'VIDEO', 'PDF')),
    CONSTRAINT ck_as_message_attachment_store   CHECK (content_store_type IN ('URL', 'BASE64', 'URI', 'PLAIN_TEXT'))
);

CREATE TABLE as_participant (
    id                        BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
    session_id                BIGINT,
    user_id                   VARCHAR(255) NOT NULL,
    is_admin                  BOOLEAN NOT NULL,
    is_assistant              BOOLEAN NOT NULL,
    is_active                 BOOLEAN NOT NULL,
    subscribed                BOOLEAN,
    join_date                 TIMESTAMP(6) WITH TIME ZONE NOT NULL,
    leave_date                TIMESTAMP(6) WITH TIME ZONE,
    added_by                  VARCHAR(255),
    removed_by                VARCHAR(255),
    last_public_activity_seen TIMESTAMP(6) WITH TIME ZONE,
    last_summary_seen         VARCHAR(255),
    last_assistant_seen       BIGINT,
    last_message_seen         BIGINT,
    last_message_sent         BIGINT,
    last_public_message_sent  BIGINT,
    CONSTRAINT pk_as_participant         PRIMARY KEY (id),
    CONSTRAINT fk_as_participant_session FOREIGN KEY (session_id) REFERENCES as_session (id),
    CONSTRAINT uq_as_participant_sent    UNIQUE (last_message_sent),
    CONSTRAINT uq_as_participant_public  UNIQUE (last_public_message_sent)
);

CREATE TABLE as_draft_message (
    id           BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
    session_id   BIGINT,
    assistant_id BIGINT,
    user_id      VARCHAR(255) NOT NULL,
    message      TEXT NOT NULL,
    CONSTRAINT pk_as_draft_message  PRIMARY KEY (id),
    CONSTRAINT uq_as_draft_message  UNIQUE (session_id, user_id, assistant_id)
);

CREATE TABLE as_chat_memory (
    id      BIGINT NOT NULL,
    message TEXT,
    CONSTRAINT pk_as_chat_memory PRIMARY KEY (id)
);

-- Knowledge base -----------------------------------------------------------

CREATE TABLE as_document (
    id            BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
    name          VARCHAR(255) NOT NULL,
    owner         VARCHAR(255),
    enabled       BOOLEAN NOT NULL,
    partial       BOOLEAN DEFAULT FALSE,
    external_data BOOLEAN NOT NULL,
    external_id   VARCHAR(255),
    last_update   TIMESTAMP(6) WITH TIME ZONE NOT NULL,
    CONSTRAINT pk_as_document PRIMARY KEY (id)
);

CREATE TABLE as_document_categories (
    document_entity_id BIGINT NOT NULL,
    categories_id      BIGINT NOT NULL,
    CONSTRAINT fk_as_document_categories_document FOREIGN KEY (document_entity_id) REFERENCES as_document (id),
    CONSTRAINT fk_as_document_categories_category FOREIGN KEY (categories_id)      REFERENCES as_category (id)
);

CREATE TABLE as_embedding (
    embedding_id UUID NOT NULL,
    text         TEXT,
    metadata     JSONB,
    embedding    VECTOR(384),
    CONSTRAINT pk_as_embedding PRIMARY KEY (embedding_id)
);

-- Audit --------------------------------------------------------------------

CREATE TABLE as_llm_exchange (
    id                     BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
    session_id             BIGINT NOT NULL,
    message_id             BIGINT,
    assistant_id           BIGINT NOT NULL,
    assistant_name         VARCHAR(255) NOT NULL,
    llm_id                 BIGINT NOT NULL,
    llm_name               VARCHAR(255) NOT NULL,
    llm_type               VARCHAR(255) NOT NULL,
    llm_content            TEXT NOT NULL,
    prompt_id              BIGINT,
    prompt_name            VARCHAR(255),
    prompt_content         TEXT,
    category_names         VARCHAR(255)[],
    request                TEXT NOT NULL,
    response               TEXT NOT NULL,
    input_token_count      INTEGER NOT NULL,
    output_token_count     INTEGER NOT NULL,
    prompt_token_count     INTEGER,
    advanced_rag           BOOLEAN,
    rag_request_content    TEXT,
    rag_queries            JSONB,
    embedding_chunks       JSONB,
    rag_input_token_count  INTEGER,
    rag_output_token_count INTEGER,
    rag_time_ms            BIGINT,
    rag_query_time_ms      BIGINT,
    request_date           TIMESTAMP(6) WITH TIME ZONE NOT NULL,
    response_date          TIMESTAMP(6) WITH TIME ZONE,
    CONSTRAINT pk_as_llm_exchange      PRIMARY KEY (id),
    CONSTRAINT ck_as_llm_exchange_type CHECK (llm_type IN ('OPEN_AI', 'OLLAMA'))
);

CREATE SEQUENCE revinfo_seq START WITH 1 INCREMENT BY 50;

CREATE TABLE revinfo (
    rev      INTEGER NOT NULL,
    revtstmp BIGINT,
    CONSTRAINT pk_revinfo PRIMARY KEY (rev)
);

CREATE TABLE as_prompt_aud (
    rev            INTEGER NOT NULL,
    revtype        SMALLINT,
    id             BIGINT NOT NULL,
    name           VARCHAR(255),
    content        TEXT,
    change_comment TEXT,
    modified_by    VARCHAR(255),
    CONSTRAINT pk_as_prompt_aud  PRIMARY KEY (rev, id),
    CONSTRAINT fk_as_prompt_aud  FOREIGN KEY (rev) REFERENCES revinfo (rev)
);

A few details are worth calling out:

  • Identifiers are BIGINT identity columns. The only sequence is revinfo_seq, which Envers uses for revision numbers.
  • as_chat_memory.id is not generated: it is the session id, which is what makes the memory of a session directly addressable.
  • as_llm_exchange denormalises deliberately. It stores the assistant, LLM, and prompt names and the prompt content alongside their ids, so an exchange can still be inspected after the configuration behind it has been edited or deleted. This is also why pruning old exchanges is a size concern: request, response, and the RAG columns hold full text.
  • as_message.mentions and as_llm_exchange.category_names are PostgreSQL array columns, not join tables.
  • as_embedding is created and managed by the pgvector store rather than by an application entity, and its vector width must match the embedding model. It is the one table whose shape changes when the model is replaced.
  • The two join tablesas_assistant_categories and as_document_categories — have no primary key of their own, following Hibernate's default for a @ManyToMany collection.

Module Overview

AI Support is structured as six Maven modules following the AppJars layered architecture:

Module Artifact ID Description
Model appjars-ai-support-model DTOs, enums, StreamingHandler, AiSupportConfiguration auto-configuration
Business API appjars-ai-support-business AssistantService, SessionService, MessageService, MessageAttachmentService, DocumentService, LlmService, PromptService, CategoryService, LlmExchangeService, ParticipantService, ChatMemoryService, DraftMessageService, AuthenticatedUserProvider, UserProfilePictureProvider, SessionSummaryProvider interfaces
Business Impl appjars-ai-support-business-impl Service implementations, AiSupportAutoConfiguration, LangChain4j wiring, RAG pipeline, RequestConfigurationFactory, ToolScanner, ScoringModelProvider
Data API appjars-ai-support-data DAO interfaces
Data Impl appjars-ai-support-data-impl JPA entities and DAO implementations
Flow UI appjars-ai-support-flow Vaadin views, RouteConfigurer, chat components

A monolithic application includes the three implementation modules (-business-impl, -data-impl, -flow). The API modules (-business, -data) are pulled in transitively.

Spring Auto-Configuration

AI Support registers itself through Spring Boot's auto-configuration mechanism. The entry point is AiSupportConfiguration, declared in:

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

This class scans the com.appjars.aisupport package for all Spring components and JPA entities. The business implementation module's AiSupportAutoConfiguration class registers two additional beans at startup:

  • EmbeddingModel — defaults to E5SmallV2EmbeddingModel, a local ONNX model with 384-dimensional output. A custom model path can be supplied via properties; supplying a model without its tokenizer, or the other way round, fails at startup.
  • EmbeddingStore — a PgVectorEmbeddingStore backed by PostgreSQL with the pgvector extension, using the as_embedding table.

Two further components initialise themselves on startup events rather than through this configuration class:

  • ToolScanner scans the application context for @Tool-annotated beans once the application is ready. See Tool Calling.
  • ScoringModelProvider loads the optional ONNX re-ranking model. See Re-ranking Retrieved Chunks.

Configuration Properties

Embedding Model

Property Default Description
appjars.aisupport.embedding.model (empty — uses built-in E5SmallV2) Path to a custom ONNX embedding model file
appjars.aisupport.embedding.tokenizer (empty — uses built-in) Path to a custom tokenizer for the ONNX model
appjars.aisupport.embedding.pooling-mode mean Pooling strategy for embedding generation (mean or cls)
spring.ai.vectorstore.pgvector.dimensions 384 Dimensionality of the embedding vectors stored in pgvector
spring.ai.vectorstore.pgvector.table-name as_embedding Table name used by the pgvector store

LLM and Moderation

Property Default Description
com.appjars.aisupport.models.timeout 120 Timeout in seconds for LLM API calls
com.appjars.aisupport.models.openai.moderation omni-moderation-latest OpenAI moderation model used for profanity detection
appjars.aisupport.profanity.check true Whether profanity detection is applied to incoming messages
appjars.aisupport.profanity.message (built-in default) Response message shown when profanity is detected
appjars.aisupport.profanity.prompt (built-in default) Custom system prompt used for profanity classification
appjars.aisupport.summarize.prompt (built-in default) System prompt used for automatic session summarization
appjars.aisupport.summarize.provider (empty — uses the default) Bean name of a custom SessionSummaryProvider implementation
appjars.aisupport.models.ollama.context-window 4096 Context window in tokens assumed for Ollama models

Advanced RAG

Property Default Description
appjars.aisupport.advanced-rag.prompt (empty — disabled) System prompt for semantic query expansion; leave empty to disable advanced RAG
appjars.aisupport.advanced-rag.queryCount 5 Number of semantic queries generated per user message when advanced RAG is active
appjars.aisupport.advanced-rag.queryMaxChars 300 Maximum character length of each generated semantic query
appjars.aisupport.advanced-rag.includeAttachments true Whether message attachments are included as context in RAG retrieval
Property Default Description
appjars.aisupport.embedding-search.maxResults 16 Maximum number of embedding chunks returned per retrieval query
appjars.aisupport.embedding-search.minScore 0.7 Minimum similarity score for a chunk to be included in retrieval results
appjars.aisupport.embedding-search.maxResultsAfterScoring 8 Chunks kept per query after the scoring model re-ranks them. Ignored when no scoring model is configured
appjars.aisupport.embedding-search.maxResultsAfterAggregation 32 Chunks passed to the language model after the results of all queries are merged

Re-ranking

Property Default Description
appjars.aisupport.rag.scoring-model.path (empty — disabled) Path to an ONNX scoring model used to re-rank retrieved chunks
appjars.aisupport.rag.scoring-model.tokenizer (empty — disabled) Path to the tokenizer paired with the scoring model

Content Injection

Property Default Description
appjars.aisupport.content-injector.prefix (built-in default) Text prepended to the block of retrieved document chunks injected into the prompt
appjars.aisupport.context-injector.prefix (built-in default) Text prepended to the additional session context block

Chat Attachments

Property Default Description
appjars.aisupport.chat.attachments.max-files 3 Maximum number of files attached to a single message
appjars.aisupport.chat.attachments.max-size 10 Maximum size in megabytes per attached file
appjars.aisupport.chat.attachments.base-path (empty) Directory where message attachments are stored
appjars.aisupport.attachments.resource-expires-after 300 Seconds an attachment download URL remains valid

Tool Calling

Property Default Description
appjars.aisupport.tooling.allowedPackages (empty — disabled) Comma-separated packages scanned for @Tool-annotated Spring beans

Background Tasks

Property Default Description
appjars.aisupport.close-inactive-session.schedule (unset — task disabled) Cron expression controlling when inactive sessions are closed
appjars.aisupport.close-inactive-session.minLastActivityDayAge 60 Minimum days since the last activity before a session is closed
appjars.aisupport.close-inactive-session.maxToProcessPerRun -1 Maximum sessions closed per run; -1 means no limit
appjars.aisupport.close-inactive-session.zone UTC Time zone of the cron expression
appjars.aisupport.exchange-pruner.schedule (unset — task disabled) Cron expression controlling when old LLM exchange records are deleted
appjars.aisupport.exchange-pruner.minDayAge 60 Minimum age in days before an exchange record is deleted
appjars.aisupport.exchange-pruner.maxToProcessPerRun -1 Maximum records deleted per run; -1 means no limit
appjars.aisupport.exchange-pruner.zone UTC Time zone of the cron expression

Channels

Property Default Description
appjars.aisupport.integrations.whatsapp.enabled false Registers the WhatsApp webhook endpoint
appjars.aisupport.integrations.facebook.enabled false Registers the Facebook webhook endpoint
appjars.aisupport.integrations.instagram.enabled false Registers the Instagram webhook endpoint
appjars.aisupport.integrations.whatsapp.verify-token some_secure_token Token Meta uses to verify the WhatsApp webhook subscription
appjars.aisupport.integrations.facebook.verify-token some_secure_token Token Meta uses to verify the Facebook webhook subscription
appjars.aisupport.integrations.instagram.verify-token some_secure_token Token Meta uses to verify the Instagram webhook subscription
appjars.aisupport.integrations.locale en Locale used for the automatic replies sent to external platforms
appjars.aisupport.meta.resolve-usernames true Whether display names are resolved from the Meta APIs for incoming messages
appjars.aisupport.public-url (empty) Public base URL of the application, used to hand attachments to Meta

Warning

Each integrations.<platform>.enabled property defaults to false, and the corresponding webhook controller is only registered when it is set to true. Creating a channel in the Channels view is not sufficient — without the matching property, the webhook endpoint does not exist and Meta receives a 404.

Display Formats

Property Default Description
appjars.aisupport.config.datepattern dd/MM/YY Pattern used to render dates
appjars.aisupport.config.timepattern hh::mm a Pattern used to render times
appjars.aisupport.config.datetimepattern dd/MM/YY, hh:mm a Pattern used to render combined date and time values

Ollama Tokenizers

Property Default Description
appjars.aisupport.tokenizer.llama3 (empty — uses built-in) Path to a custom Llama 3 tokenizer
appjars.aisupport.tokenizer.mistral (empty — uses built-in) Path to a custom Mistral tokenizer
appjars.aisupport.tokenizer.gemma (empty — uses built-in) Path to a custom Gemma tokenizer

View URLs

Property Default Description
com.appjars.aisupport.url.views.chat as/chat URL path of the chat view
com.appjars.aisupport.url.views.assistants as/assistants URL path of the assistant management view
com.appjars.aisupport.url.views.documentsview as/documents URL path of the document management view
com.appjars.aisupport.url.views.prompts as/prompts URL path of the prompt management view
com.appjars.aisupport.url.views.llms as/llms URL path of the LLM management view
com.appjars.aisupport.url.views.inspector as/inspector URL path of the LLM exchange inspector view
com.appjars.aisupport.url.views.categories as/categories URL path of the category management view
com.appjars.aisupport.url.views.channels as/channels URL path of the unified channels view

Required Integrations

AuthenticatedUserProvider

AI Support requires an implementation of AuthenticatedUserProvider to identify the current user and resolve user search queries. This is used by the chat view, session management, and participant handling.

@Primary
@Component
public class AppAuthenticatedUserProvider implements AuthenticatedUserProvider {

    @Autowired
    private UserService userService;

    @Override
    public Optional<UserDto> getAuthenticatedUser() {
        String username = SecurityContextHolder.getContext()
            .getAuthentication().getName();
        return userService.findByUsername(username)
            .map(u -> new UserDto(u.getUsername(), u.hasRole("ADMIN")));
    }

    @Override
    public Set<UserDto> findUsersByStartWith(String match, boolean toLowercase) {
        String prefix = toLowercase ? match.toLowerCase() : match;
        return userService.findAll().stream()
            .filter(u -> u.getUsername().startsWith(prefix))
            .map(u -> new UserDto(u.getUsername(), u.hasRole("ADMIN")))
            .collect(Collectors.toSet());
    }
}

UserDto in this context is com.appjars.aisupport.model.UserDto, which carries a username string and an isAdmin boolean. The @Primary annotation is required to override any default implementation.

UserProfilePictureProvider

UserProfilePictureProvider supplies avatar images for users displayed in the chat interface. It is optional — the default implementation returns empty, which causes the UI to render initials instead of a photo.

@Primary
@Component
public class AppUserProfilePictureProvider implements UserProfilePictureProvider {

    @Autowired
    private UserProfileService userProfileService;

    @Override
    public Optional<byte[]> getProfilePictureByUsername(String username) {
        return userProfileService.findByUsername(username)
            .map(UserProfileDto::getAvatar);
    }
}

When User Profile is also integrated, delegate to its service as shown above.

AI Pipeline Architecture

AI Support is built on LangChain4j and uses a retrieval-augmented generation (RAG) pipeline to ground assistant responses in the application's knowledge base.

flowchart TD
    A[User message] --> B[Profanity check]
    B -->|clean| C{Advanced RAG?}
    B -->|flagged| D[Return profanity response]
    C -->|yes| E[SemanticQueryTransformer\ngenerates N queries]
    C -->|no| F[Original query]
    E --> G[EmbeddingStore retrieval\nper query]
    F --> G
    G --> H{Scoring model\nconfigured?}
    H -->|yes| I[Re-rank chunks\nkeep maxResultsAfterScoring]
    H -->|no| J[Keep retrieval order]
    I --> K[ContentAggregator merges queries\nkeep maxResultsAfterAggregation]
    J --> K
    K --> L[ContentInjector\nbuilds prompt]
    L --> M[ChatMemory\nlast 10 messages]
    M --> N[ChatLanguageModel\nOpenAI or Ollama]
    N --> O{Tool call\nrequested?}
    O -->|yes| P[Invoke @Tool bean\nfeed result back]
    P --> N
    O -->|no| Q[Response message]
    Q --> R[Session summary update]
    Q --> S[LlmExchange record]
Hold "Alt" / "Option" to enable pan & zoom

Standard RAG retrieves embedding chunks whose similarity score exceeds minScore (default 0.7), up to maxResults (default 16) per query, filtered to the categories assigned to the active assistant.

Advanced RAG adds a query transformation step: a secondary LLM call generates queryCount (default 5) semantically varied reformulations of the user's message. Each reformulation is used independently to query the embedding store, and the results are aggregated before injection. This improves recall for ambiguous or short queries.

Retrieval narrows the candidate set in three stages. Each query returns at most maxResults chunks. When a scoring model is configured, those chunks are re-ranked and cut to maxResultsAfterScoring. Finally the results of all queries are merged and cut to maxResultsAfterAggregation before they reach the prompt.

Re-ranking Retrieved Chunks

Similarity search alone ranks chunks by vector distance, which does not always match relevance. Configuring an ONNX scoring model adds a re-ranking pass that scores each retrieved chunk against the original query:

appjars.aisupport.rag.scoring-model.path=reranker/model.onnx
appjars.aisupport.rag.scoring-model.tokenizer=reranker/tokenizer.json

Both properties are required together. When either is missing, ScoringModelProvider yields no model and retrieval keeps the raw similarity ordering. A model that fails to load is logged as an error and treated the same way, so a broken path degrades retrieval quality rather than preventing startup.

Tool Calling

Assistants can call application code during a conversation. ToolScanner inspects the Spring context once the application is ready and collects every bean with methods annotated with LangChain4j's @Tool; those methods are offered to the language model as callable functions.

Scanning is restricted to an explicit allow list, and is disabled when the list is empty:

appjars.aisupport.tooling.allowedPackages=com.example.tools,com.example.crm

A tool is an ordinary Spring bean:

@Component
public class OrderTools {

    @Tool("Returns the delivery status of a customer order")
    public String orderStatus(String orderNumber) {
        return orderService.findByNumber(orderNumber)
            .map(order -> order.getStatus().name())
            .orElse("Unknown order");
    }
}

Only beans whose package starts with one of the allowed prefixes are scanned, so adding a package makes every @Tool method inside it reachable by the model. Scope the list narrowly, and treat tool arguments as untrusted input: the model chooses them.

Tool results re-enter the model as part of the same exchange, which means a single user message can produce several requests to the language model. Messages carrying tool calls are exempt from chat-memory eviction so that a partially completed tool sequence is never truncated.

Background Tasks

Two scheduled maintenance tasks ship with the appjar. Both are inactive until a cron expression is supplied, and both require @EnableScheduling on the host application.

Inactive session closer (CloseInactiveSessionTask) closes active sessions whose last activity — public or administrator-only — is older than minLastActivityDayAge, and deletes their draft messages:

appjars.aisupport.close-inactive-session.schedule=0 0 3 * * *
appjars.aisupport.close-inactive-session.minLastActivityDayAge=60
appjars.aisupport.close-inactive-session.maxToProcessPerRun=500
appjars.aisupport.close-inactive-session.zone=UTC

Exchange pruner (LlmExchangePrunerTask) deletes LLM exchange audit records older than minDayAge:

appjars.aisupport.exchange-pruner.schedule=0 30 3 * * *
appjars.aisupport.exchange-pruner.minDayAge=60
appjars.aisupport.exchange-pruner.maxToProcessPerRun=1000
appjars.aisupport.exchange-pruner.zone=UTC

Pruning exchange records removes the data behind the LLM Inspector for the affected messages; the messages themselves are kept.

LLM Configuration

LLM definitions are stored in the as_llm table and managed through the LLMs view. Each definition has a type (OPEN_AI or OLLAMA) and a configuration JSON field that holds the model name, API key, and endpoint. At most one LLM is marked as system = true, and it is used for the moderation and summarization tasks regardless of which assistant is active. The flag does not reserve the model for those tasks: a system LLM can also be referenced by assistants and used to answer users.

The flag is optional. LlmService.findSystemLlm() is resolved as follows, in RequestConfigurationFactory and in DefaultSessionSummaryProvider:

LlmDto systemLlm = llmService.findSystemLlm()
    .map(l -> l.isEnabled() ? l : null)
    .orElse(assistantLlm);

When no system LLM exists, or the one marked is disabled, the active assistant's LLM is used instead. These tasks therefore never lack a model, and an application that never sets the flag behaves correctly.

Supported content types per LLM type:

LLM type Supported attachment types
OPEN_AI Text, image, video, audio, PDF
OLLAMA Text, image

Document Ingestion

Documents are ingested through DocumentService. The service splits each document into text segments, generates embeddings using the configured EmbeddingModel, and stores the segments in the PgVectorEmbeddingStore with metadata linking each chunk to its source document and categories.

// Add a document from a file or external source
Long id = documentService.addDocument(documentDto, categories);

// Add with additional metadata applied to all embedding chunks
Long id = documentService.addDocument(documentDto, categories, Map.of("source", "wiki"));

// Process a directory of files
documentService.processFolder("/data/knowledge-base", categories);

// Async processing (returns immediately; processes in background)
CompletableFuture<DocumentDto> future = documentService.processAsync(documentDto, categories);

Documents tagged with one or more categories are only retrieved when an assistant is scoped to those same categories. An assistant with no categories assigned retrieves from the full knowledge base.

Chat Bubble Component

The floating chat window described in The Chat Bubble is the AISupportChatAssistant component. Integrating it is covered in Add the Chat Bubble; this section covers what it is built on and how its appearance is controlled.

Relationship with the Chat view

Both user interfaces are driven by the same ChatCore, which owns the conversation logic — sending, streaming, unread tracking, mentions, participants, drafts, and private mode. Each host declares which of the two it is through ChatType:

Value Interface
ChatType.MAIN_VIEW The full ChatView
ChatType.FAB The floating AISupportChatAssistant

A host implements ChatHost and delegates to ChatCore, which is why the two interfaces stay behaviourally identical: a feature added to the core reaches both. The differences are confined to layout — the view has a session sidebar, the bubble a session combobox in its header.

The underlying add-on

AISupportChatAssistant extends ChatAssistant<ChatAssistantMessage> from the Chat Assistant add-on, an Apache 2.0 licensed Vaadin add-on published by Flowing Code. The add-on contributes the floating button, the draggable and resizable window, and the unread badge; AI Support supplies the conversation behaviour on top of it.

The add-on is a transitive dependency of appjars-ai-support-flow; no separate declaration is required.

The base class provides the appearance and sizing API:

// Replace the floating button icon, optionally with a size in pixels
chatAssistant.setFabIcon(new SvgIcon("/icons/support.svg"), 36);

// Constrain the chat window; any CSS length is accepted
chatAssistant.setWindowWidth("28rem");
chatAssistant.setWindowHeight("40rem");
chatAssistant.setWindowMinWidth("20rem");
chatAssistant.setWindowMaxHeight("80vh");

// Open, close or query the window programmatically
chatAssistant.open();
chatAssistant.close();
boolean opened = chatAssistant.isOpened();

AI Support sets a robot icon at 36 px by default and replaces the add-on's header with its own, so setHeaderComponent should not be called on it. The button is 60 px with a 25 px margin from the viewport edge, and dragging is separated from clicking by a 25 px threshold. The window is resizable from all four edges and all four corners.

Neither the button position nor the window size is persisted: both are per page visit, and a refresh restores the defaults.

Scope

The component is a prototype-scoped Spring bean, because it holds per-user conversation state:

@SpringComponent
@Scope("prototype")
public class AISupportChatAssistant extends ChatAssistant<ChatAssistantMessage>

Inject it into a router layout that is itself scoped per UI, as shown in the getting started guide. Injecting it into a singleton would share one chat window between users.

Notifications

The bubble subscribes to the appjar's Broadcaster and reacts to message, participant, session, and document events. Two behaviours are worth knowing when integrating:

  • The floating button's badge aggregates unread counts across every session the user participates in, not only the loaded one.
  • A message preview notification is only raised when ChatStatusHandler.shouldNotifyMessage passes, which requires the participant to be active and subscribed to the session, the message to be newer than the last one they saw, and — for administrator-only messages — the recipient to be an administrator. The user's own messages, streaming fragments, and system messages never raise one.

Because the component needs server push to receive these events, @Push is required on the host application, as noted in the getting started guide.

Chat Memory

Each session maintains a persistent chat memory stored in the as_chat_memory table. The memory ID equals the session ID. The window keeps the most recent 10 messages; older messages remain in the database but are not included in new LLM requests.

Eviction uses a custom policy rather than LangChain4j's default. Three kinds of message are never evicted automatically: system messages, which are not counted against the limit at all; messages carrying non-text attachments such as images, PDFs, audio, or video; and messages that are part of an unfinished tool-call sequence. As a result a long conversation with several attachments can hold more than 10 messages in its context window.

The chat memory is populated automatically by AssistantService before each LLM call and updated with the response. Application code does not need to manage memory directly.

Streaming Responses

AssistantService.processMessagesWithStreaming() returns a CompletableFuture<MessageDto> and delivers partial response chunks via a StreamingHandler callback:

StreamingHandler handler = StreamingHandler.builder()
    .sessionId(sessionId)
    .startedBy(username)
    .onPartialMessage(partial -> {
        // push partial text to UI (e.g., via Vaadin Push)
    })
    .onFailedWhileGenerating(failed -> {
        // handle generation failure
    })
    .onSummaryChanged(session -> {
        // session summary was updated
    })
    .build();

assistantService.processMessagesWithStreaming(sessionId, false, handler);

Streaming can be cancelled at any time by calling handler.cancel(username).

LLM Exchange Audit

Every LLM interaction is recorded in as_llm_exchange. Each record stores the full request and response text, input and output token counts, the embedding chunks that were retrieved (for RAG calls), and a snapshot of the assistant, LLM, and prompt configuration at the time of the call. This allows administrators to inspect the exact reasoning behind any response in the Inspector view.

Exchange records can be pruned by age:

// Delete exchanges older than 30 days, up to 1000 records per call
int deleted = llmExchangeService.deleteOlderThan(Duration.ofDays(30), 1000);

Service API

AssistantService

// Add a message to a session
MessageDto addMessage(Long sessionId, String content, boolean shouldProcess,
                      SenderType senderType, String userId);

// Add a message with explicit context (injected alongside RAG content)
MessageDto addMessage(Long sessionId, String content, String context,
                      boolean shouldProcess, SenderType senderType, String userId);

// Process pending messages and return the AI response
CompletableFuture<MessageDto> processMessages(Long sessionId, boolean adminOnly);

// Process pending messages with real-time streaming
CompletableFuture<MessageDto> processMessagesWithStreaming(Long sessionId,
                      boolean adminOnly, StreamingHandler handler);

SessionService

// Create a new session and return its ID
long createSession(SessionDto sessionDto);

// Find all sessions where a user is a participant
List<SessionDto> findByParticipant(String username);

// Find sessions matching a filter
List<SessionDto> findByFilter(SessionFilter filter);
long countByFilter(SessionFilter filter);

DocumentService

// Ingest a document and return its ID
Long addDocument(DocumentDto document, List<CategoryDto> categories)
    throws CannotAddDocumentException;

// Remove a document and its embeddings
void removeDocument(DocumentDto document);

// Re-process an existing document (re-split and re-embed)
CompletableFuture<DocumentDto> reprocessAsync(DocumentDto document);

// Browse embedding chunks for a document
List<EmbeddingDto> findEmbeddingSegmentsByTextContains(Long documentId,
    String filterText, int offset, int limit);

MessageService

// Add a system-generated message to a session, such as an assistant update,
// a response error, or a participant change
MessageDto addSystemMessage(SessionDto session, String content, String userId,
                            boolean adminOnly, SystemMessageContext context);

// Delete every message belonging to a session
void deleteBySession(Long sessionId);

// Find messages matching a filter
List<MessageDto> findByFilter(MessageFilter filter);
long countByFilter(MessageFilter filter);

Customisation

Assigning a Router Layout

By default, AI Support registers its views without a parent layout. To wrap them in the application's main layout, inject the RouteConfigurer bean using its qualifier and call setViewsRouterLayout in a @PostConstruct method:

@Autowired
@Qualifier("AISupportRouteConfigurer")
private RouteConfigurer routeConfigurer;

@PostConstruct
public void configure() {
    routeConfigurer.setViewsRouterLayout(MainLayout.class);
}

The @Qualifier is required because multiple AppJars may contribute a bean named RouteConfigurer.

Customising View URLs

The default URL paths can be overridden in application.properties:

com.appjars.aisupport.url.views.chat=myapp/chat
com.appjars.aisupport.url.views.assistants=myapp/assistants
com.appjars.aisupport.url.views.documentsview=myapp/documents
com.appjars.aisupport.url.views.llms=myapp/llms
com.appjars.aisupport.url.views.inspector=myapp/ai-inspector

Replacing the Session Summary Provider

Session summaries — the short titles shown on each session card — are produced by a SessionSummaryProvider. The default implementation asks the system LLM to summarise the recent messages, using the prompt in appjars.aisupport.summarize.prompt.

To generate summaries differently, for example without a model call, implement the interface and register the bean:

@Component("firstMessageSummaryProvider")
public class FirstMessageSummaryProvider implements SessionSummaryProvider {
    // ...
}

Then select it by bean name:

appjars.aisupport.summarize.provider=firstMessageSummaryProvider

When the property is empty, the default provider is used.

Replacing the Embedding Model

The default embedding model (E5SmallV2, 384 dimensions) is suitable for English-language content. To use a different model, provide the ONNX model file and configure the path:

appjars.aisupport.embedding.model=/models/my-model.onnx
appjars.aisupport.embedding.tokenizer=/models/my-tokenizer.json
appjars.aisupport.embedding.pooling-mode=mean
spring.ai.vectorstore.pgvector.dimensions=768

The dimensions property must match the output size of the chosen model. Changing the embedding model requires re-processing all existing documents, as stored embeddings will be incompatible with a model of different dimensionality.