PrimeBrickPrimeBrick
  • Docs
  • Contact
  • MIT License
  • Documentation
  • MCP Server
  • API Catalog
  • Services
  • Libraries
PrimeBrickPrimeBrick

© 2026 PrimeBrick. MIT License. v3.11.2

github

AI Microservice

The AI microservice provides an in-app chat assistant that answers questions about Primebrick using RAG (Retrieval-Augmented Generation). It embeds documentation into a pgvector knowledge base, retrieves relevant chunks at query time, and streams responses from a local or cloud LLM.

The service is inference-only: it never calls application tools itself. The agentic loop is owned by the caller (the Frontend) — the AI emits client-tool-call SSE events, the caller executes them against the BE's MCP gateway (/system/mcp/call), and resubmits the results.

  • Package: primebrick-ai
  • Service code: AI
  • Default port: 3004
  • Runtime: Bun (dev: bun --hot, prod: bun dist/index.js)
  • Database schema: ai (isolated per-service schema)
  • Vector extension: pgvector (cosine similarity, HNSW index)
  • Declared capabilities: llm_orchestrator, chat_orchestrator, vectorizing_engine

Architecture

Components

RAG pipeline

The RAG (Retrieval-Augmented Generation) pipeline powers documentation-aware answers:

  1. Embedding: The user's query is embedded using Xenova/all-MiniLM-L6-v2 (384-dim, CPU/WASM via Transformers.js).
  2. Vector search: The embedding is compared against ai.docs_kb using pgvector cosine distance (<=> operator) with an HNSW index.
  3. Context injection: The top-k chunks are injected into the LLM prompt as context.
  4. LLM response: The LLM generates an answer grounded in the retrieved documentation.

Caller-driven tool loop

The orchestrator does not connect to the Backend's MCP server. Instead, the caller drives the loop end to end:

  1. The caller fetches the tool catalog once from the BE: GET /system/mcp/tools (includes a read_only flag per tool).
  2. The caller passes tools[] (OpenAI-style definitions) to POST /api/v1/ai/chat.
  3. When the LLM requests a tool, the AI emits a client-tool-call SSE event and stops — it does not execute it.
  4. The caller executes the tool via POST /system/mcp/call — read-only tools immediately, write tools behind a user confirmation — then resubmits POST /api/v1/ai/chat with the reconstructed history, tool_results[], and no message.
  5. Repeat until the model produces a final answer (finish event). The caller bounds the loop (FE: MAX_LOOP_STEPS = 8).

The only server-side tool the AI executes itself is search_docs, a local tool that queries the RAG knowledge base — the LLM calls it when the user asks about product documentation, architecture, APIs, or configuration.

PII redaction

When REDACT_PII=true (recommended for cloud LLMs), the orchestrator redacts personally identifiable information from messages before sending them to the LLM:

  • Emails → [EMAIL_1], [EMAIL_2], ...
  • Phone numbers → [PHONE_1], ...
  • Credit card numbers → [CARD_1], ... (with Luhn validation)

Original messages with real PII are persisted to the database unchanged. Only the LLM sees the redacted version.

HTTP routes

All routes are proxied by the Backend via /ws/ai/*. Authentication is handled by the gateway (headers: x-user-id, Authorization: Bearer <jwt>).

MethodPathPurposeAuth
GET/api/v1/ai/healthAI health check (LLM connectivity + model info)Gateway
POST/api/v1/ai/chatChat endpoint with SSE streamingGateway + rate limit
GET/api/v1/ai/conversationsList user's conversationsGateway
POST/api/v1/ai/conversationsCreate new conversationGateway
GET/api/v1/ai/conversations/:uuidGet conversation with messagesGateway
PATCH/api/v1/ai/conversations/:uuidUpdate conversation titleGateway
DELETE/api/v1/ai/conversations/:uuidDelete conversationGateway
POST/api/v1/ai/feedbackSubmit thumbs up/down on a messageGateway
GET/api/v1/ai/feedbackList user's feedbackGateway
GET/api/v1/ai/telemetryAggregate stats + recent requestsAUTHENTICATED_ADMIN
POST/api/v1/ai/reindexTrigger embedding pipelineAUTHENTICATED_ADMIN
POST/api/v1/ai/embedServer-side embedding of query textAUTHENTICATED_USER
POST/api/v1/system/docs/searchSemantic search over docs_kbAUTHENTICATED_USER
POST/api/v1/system/docs/documentFetch a doc's chunks by pathAUTHENTICATED_USER

Chat endpoint

POST /api/v1/ai/chat streams the response via Server-Sent Events (SSE) and returns the conversation id in the X-Conversation-UUID header.

Request body — message OR tool_results is required:

Code
{ "message": "How does RBAC work?", "conversation_uuid": "optional-uuid", "history": ["full message history incl. assistant tool-call parts"], "tools": ["OpenAI-style tool defs from GET /system/mcp/tools"], "tool_results": ["results for a tool-loop continuation (no message)"] }

SSE events:

EventPayloadDescription
text-delta{ text: string }Partial text chunk
tool-call{ toolName, input }LLM invoked a server-side tool (search_docs)
tool-result{ toolName, result }Server-side tool returned a result
client-tool-call{ toolName, input }Caller must execute via /system/mcp/call
finish{ text, tokensIn, tokensOut, steps }Stream complete
error{ message }Error occurred

Rate limiting

The chat endpoint uses a token bucket algorithm per user (identified by x-user-id header). Configuration:

Config keyDefaultDescription
rate_limit_capacity10Max tokens in bucket per user
rate_limit_refill_rate1Tokens refilled per second
rate_limit_token_cost1Tokens consumed per chat request

If the shared redis_url config is set, rate limiting is distributed across instances. Otherwise, an in-memory token bucket is used (single-instance only).

Database schema

All tables are in the ai schema (microservice isolation).

docs_kb — RAG knowledge base

ColumnTypeDescription
idbigintAuto-increment PK
repotextSource repo (backend, frontend, sdk, dal, microservices, api)
pathtextDoc path within the repo
titletextDoc title (from frontmatter)
chunk_idxintChunk index within the doc
contenttextChunk text content
embeddingvector(384)Embedding vector (pgvector)
metadatajsonbAdditional metadata (source, content_type, heading_path)
content_hashtextSHA-256 of the exact embedding input: title, heading path, and chunk body

Index: HNSW on embedding with vector_cosine_ops (m=16, ef_construction=64).

ai_conversations — Chat conversations

ColumnTypeDescription
uuiduuidPK
user_uuiduuidOwner
titletextConversation title
created_attimestamptzCreation timestamp
updated_attimestamptzLast update timestamp

ai_messages — Chat messages

ColumnTypeDescription
uuiduuidPK
conversation_uuiduuidFK → ai_conversations (CASCADE)
roletextuser, assistant
contentjsonb{ text, tool_calls, tool_results }
tokens_inintInput tokens
tokens_outintOutput tokens
created_attimestamptzCreation timestamp

ai_telemetry — Per-request telemetry

ColumnTypeDescription
idbigintAuto-increment PK
uuiduuidUnique request ID
conversation_uuiduuidAssociated conversation
user_uuiduuidRequesting user
modeltextLLM model name
tokens_inintTotal input tokens
tokens_outintTotal output tokens
stepsintReasoning steps (tool call rounds)
tool_callsintTotal tool calls
rag_hitsintsearch_docs calls
latency_msintEnd-to-end latency
successbooleanWhether the request succeeded
errortextError message (if failed)

ai_feedback — User feedback

ColumnTypeDescription
idbigintAuto-increment PK
uuiduuidUnique feedback ID
message_uuiduuidMessage being rated
user_uuiduuidUser who rated
ratingtextup or down
commenttextOptional comment

Embedding pipeline (indexing)

Production source: service docs packs. Every service ships a dist/docs-pack (built by pnpm generate:docs, served at GET /api/v1/system/docs/pack). On service.register with the docs_pack capability the AI pulls the pack and ingests it under the pack's repo key; service.unregister drops that repo's docs. The BE's pack is pulled at startup and on every gateway_online broadcast. Packs are self-contained: they carry openapi.json (the service runtime spec) alongside the MDX docs, so openapi_path is retired.

The filesystem pipeline below is the dev fallback (set docs_fs_pipeline=true). It populates docs_kb from the documentation source files. Run it with:

TerminalCode
cd ai && pnpm run index-docs

Flow:

  1. Load docs from DOCS_PATH (MDX files) and OPENAPI_PATH (JSON specs).
  2. Parse frontmatter with LF or CRLF line endings, extracting the document title, then chunk the body by headings and paragraphs.
  3. Build embedding input for each chunk from the document title, heading path, and chunk body. The stored content remains the original chunk body.
  4. Compute SHA-256 over that exact embedding input; skip chunks whose content_hash matches the database.
  5. Batch embed changed inputs (batch size 16) via the embedding provider.
  6. Upsert content, embedding, metadata, and the input hash into docs_kb (ON CONFLICT update).
  7. Delete stale chunks (docs removed from source or chunks no longer present).

The pipeline is idempotent — re-running it only re-embeds chunks whose source content, title, or heading path changed.

Environment variables

VariableRequiredDefaultDescription
DATABASE_URLYes—PostgreSQL connection string
DATABASE_SCHEMAYes—Declared schema domain (ai; CSV/JSON array for multi-schema — [0] is owned)

These are the only env-sourced settings (env policy). All runtime configuration lives in ai.config_entries and is read through src/config/ai-config.ts:

Config keyDefaultDescription
service_base_urlhttp://localhost:3004Service URL for BE proxy
llm_base_urlhttp://localhost:8080/v1LLM endpoint (OpenAI-compatible)
llm_modelQwen3-4B-Instruct-2507LLM model name
llm_api_keynot-requiredLLM API key (not needed for local)
embedding_providertransformerstransformers or openai
embedding_modelXenova/all-MiniLM-L6-v2Embedding model
embedding_base_url—OpenAI-compatible embeddings URL
embedding_api_key—Embeddings API key
embedding_dim384Vector dimension (OpenAI provider)
embedding_local_only / embedding_dtypefalse / q8transformers.js runtime options
be_base_urlhttp://localhost:3001Backend URL — required by ServiceRegistrar registration, no longer used for MCP
redact_piifalsetrue for cloud LLM PII redaction
rate_limit_capacity / rate_limit_refill_rate / rate_limit_token_cost10 / 1 / 1Chat token-bucket settings
docs_fs_pipelinefalseDev fallback: true runs the legacy FS pipeline at startup and enables POST /api/v1/ai/reindex
docs_path../../primebrick-v3-docs/pagesMDX docs path (fallback only)

| nats_url, service_code, http_port, service_client_shield_key | — | SDK bootstrap settings |

Distributed rate limiting uses the shared redis_url key (BE-owned global config), not a service env var.

LLM runtime

The default LLM is a local mistral.rs container running Qwen3-4B-Instruct-2507 (Q4_K_M quantization). See the Backend's infra/docker-compose.ai-llm.yml for the container configuration.

The LLM provider is pluggable — any OpenAI-compatible endpoint works (OpenAI, Anthropic via proxy, DeepSeek, Groq, etc.) by changing LLM_BASE_URL, LLM_API_KEY, and LLM_MODEL.

Local development

Prerequisites

  1. PostgreSQL with pgvector (from infra/docker-compose.postgres.yml)
  2. LLM container (from infra/docker-compose.ai-llm.yml) or a cloud LLM
  3. Backend running on port 3001 — needed for service registration and for the caller-driven tool loop (/system/mcp/*); the AI itself no longer depends on it to serve chat

Steps

  1. Copy .env.example to .env and adjust values
  2. Apply migrations: pnpm run db:migrate
  3. Index docs: pnpm run index-docs
  4. Start dev server: pnpm run dev
  5. Verify health: curl http://localhost:3004/health
Last modified on October 28, 2026
On this page
  • Architecture
  • Components
    • RAG pipeline
    • Caller-driven tool loop
    • PII redaction
  • HTTP routes
    • Chat endpoint
    • Rate limiting
  • Database schema
    • docs_kb — RAG knowledge base
    • ai_conversations — Chat conversations
    • ai_messages — Chat messages
    • ai_telemetry — Per-request telemetry
    • ai_feedback — User feedback
  • Embedding pipeline (indexing)
  • Environment variables
  • LLM runtime
  • Local development
    • Prerequisites
    • Steps
JSON