North Africa Bank North Africa Bank UTEEK Digital Agency UTEEK Digital Agency

The customer chatbot

A transactional assistant for the bank's customers, built on the platform that already serves its employees.

Phase
M2
Document
Architecture review
Prepared by
UTEEK Digital Agency

Where this sits

M1 is running. M2 is the same engine, pointed outward.

M1 — Agents Assistant delivered

Internal. Employees ask questions and get answers from the bank's own documents, every answer carrying its source. Read-only over the knowledge base. Deployed and running.

M2 — Customer chatbot this deck

External. The bank's customers, on a public channel, asking about products and their own accounts — and eventually asking the bank to do something.

The distance between them is not the conversation. It is who is asking, what they may see, and what the assistant may do. Those three questions are the whole of this architecture.

The shape

One process, three stores, one outbound dependency.

Customer browser through the platform API to the public corpus, the databases and the model endpoint Customer web / mobile TLS Edge rate limit, WAF Platform API guardrails (in / out) retrieval model provider action layer (M3) audit middleware sessions, feedback one process Qdrant kb_public Postgres + audit Model endpoint Core banking system through the action layer — never by the model
No queue, no cache, no message bus. Each would be a place for a conversation to be lost between being answered and being audited — and at this load none of them earn their operational cost.

One turn, in order

The audit write is awaited before the answer is returned.

A question passes through the guardrail, retrieval, the model and the output check, and every stage writes to the audit rail before the response is released Question customer Guardrail in scope, injection Retrieval this channel Prompt sources as data Model streams tokens Guardrail out attribution Answer released Audit rail append-only, INSERT-only role gate The answer is not released until its audit write has committed. If the write fails, the request fails — the only outage in this system that is deliberately not degraded around. Every stage records its own decision, so a refusal six months old can be explained without re-running it.

Finding the passage

Two retrievers, fused by rank, gated on the retrievers' own scores.

A question is embedded and searched lexically in parallel, the two result sets are fused by reciprocal rank, and a confidence gate decides between answering and refusing Question AR or EN Dense · multilingual-e5-large "query: " prefix · 1024-d cosine Sparse · BM25 exact figures, product names top 20 top 20 RRF fusion k = 60 Confidence gate on retriever scores, never the fused score 5 sources to the prompt Refuse not in the KB RRF is rank-based and discards magnitude: an irrelevant hit at rank 1 scores exactly what a perfect match scores. Thresholding the fused score would look correct and silently disable the refusal path entirely.
candidates 20 + 20 fusion RRF k=60 weights dense 1.0 / sparse 0.8 to the model 5 passages floor cosine 0.78

The first boundary

Two corpora. The customer channel cannot reach the internal one.

Internal procedures, staff instructions and unpublished fee schedules live in kb_internal. Only published material lives in kb_public.

This is already built and running for M1, which indexes the internal corpus only. M2 adds the second corpus; it does not add the mechanism.

Where injection is stopped

Instructions and retrieved text never share a message.

The system message holds instructions; retrieved passages sit inside a delimited block in the user message and are treated as data system message — trusted answer only from the numbered sources cite every factual statement refuse when they do not cover the question user message — untrusted Question: what are the transfer fees? <sources> [1] Local Transfer Procedure §4 … </sources> retrieved Corpus bank documents, uploaded by an administrator never reaches the instructions Why it holds A document that says "ignore previous instructions" is quoted text inside a delimited block, not a turn the model is answering. The policy fails to load if this is relaxed — it is an invariant, not a convention.

The second boundary

The model never holds a credential and never decides who may do what.

The model proposes an action; identity, authorisation and risk tiering are decided outside it Model proposes Identity who is asking Authorisation may they Risk tier confirm / step up the path that does not exist CBS adapter Every step between the proposal and the banking system is ordinary code, reviewable and testable. A prompt injection that convinces the model of something convinces nothing downstream.

Doing, not only answering

An action is a form the assistant fills in — not a decision it takes.

1. Propose

The model emits a structured request: which capability, with which values. It is a proposal in a fixed shape, validated like any other untrusted input.

2. Authenticate

The customer's identity comes from their authenticated session, never from the conversation. The model cannot name the account it acts on.

3. Authorise

The platform decides whether this customer may perform this action on this account, in code the bank's reviewers can read.

4. Tier the risk

Low risk executes. Anything higher returns to the customer for explicit confirmation, or steps up authentication, according to the bank's policy.

The mechanism already exists in M1 for internal capabilities — annual leave, a salary certificate — where an administrator defines a capability in the back office and an engineer bounds what it may reach. M2 inherits it; M3 connects it to the core banking system.

Both edges

Filtered going in. Verified coming out.

Input

Topical scope, prompt-injection and jailbreak detection, PII masking, and toxicity in Arabic and English. Per-channel policy: the customer channel is stricter than the employee one, and says so in configuration rather than in code.

Output

Every claim must be attributable to a retrieved source. An answer that cannot be attributed is withheld and replaced, not shown with a caveat.

A trade we made deliberately

The output check runs on a stream, so it retracts rather than delays.

Tokens stream to the reader as they are produced; the attribution check runs at the end and either lets the answer stand or retracts it and replaces it t → tokens stream to the reader as the model produces them Attribution check every claim cited? stands retracted and replaced The conflict, stated plainly Verifying before showing anything means no streaming: the reader waits for the whole answer before seeing a word, and the first-token budget cannot be met at all. What retraction costs A reader may briefly see text that is then withdrawn. The client discards it rather than annotating it, because SSE cannot un-send a token — retraction is the client's job. ADR 0003 — recorded, and awaiting NAB's sign-off.

Reconstructable

Written before the answer is returned. If the write fails, the request fails.

This is the one place in the system where an outage is deliberately not degraded around.

Not by discipline

Three roles. The one that writes the audit trail cannot change it.

Three database roles with distinct privileges on the audit table: insert only, select only, and no access at all for the application role nab_audit_writer the audit service nab_audit_reader compliance export nab_app everything else the API does GRANT INSERT GRANT SELECT REVOKE ALL — no access audit_events no UPDATE grant no DELETE grant WORM export to the bank's own retention store No application code path, no administrator screen and no role in this system can modify an audit event. CI asserts the live grants on every run.

Arabic and English

One cross-lingual model, not two monolingual ones.

A customer writes in Libyan dialect; the source document is in formal Arabic, or in English. All three have to meet.

Acceptance is scripted in both languages, and Arabic accuracy is measured against English rather than merely alongside it.

Honest accounting

What M2 inherits, and what it genuinely adds.

Capability Status What M2 needs
Retrieval, chunking, embeddings built A second collection. No new mechanism.
Corpus isolation built Enable the customer channel. The table already exists.
Guardrails, both edges built A stricter per-channel policy, in configuration.
Audit trail built Nothing. Channel is already an audited dimension.
Model abstraction built Nothing. Any of the three deployment cases.
Capability & action layer built Customer-facing capabilities defined in the back office.
Customer identity new The bank's customer authentication, not employee SSO.
Public web / mobile surface new A customer-facing client, and the edge in front of it.
Public corpus new Published material only, curated by the bank.
Core banking integration M3 The API contract, and the risk policy per action.

Six of the ten already run in production today. That is the argument for building the customer channel on this platform rather than beside it.

On the host

Apache is the only public listener. Everything else binds to loopback.

Apache terminates TLS and routes by hostname to containers bound to 127.0.0.1; Postgres and Qdrant publish no port at all Internet :80 :443 Apache TLS termination hostname routing ACME challenge the only host process listening agents.… 127.0.0.1:8081 admin.… 127.0.0.1:8082 api.… 127.0.0.1:8000 Not published at all PostgreSQL Qdrant Reachable only on the container network. A firewall mistake cannot expose the audit store or the index. Each application proxies /api on its own origin, so the session cookie is same-origin: no CORS preflight on the chat stream, nothing to get wrong in Access-Control-Allow-Credentials, and cookies that work at SameSite=Strict.

Where it runs

The deployment case is a configuration change, and has been proven to be.

Managed API

Fastest to stand up. Queries leave the bank's network — a data-residency decision, not a technical one.

Cloud, self-hosted

The model on the bank's own cloud instance. What the platform runs on today.

On-premise

Nothing leaves the bank. Highest control, and the highest hardware commitment.

Every model call goes through one module speaking one protocol. Changing case touches that configuration and nothing else — demonstrated during development when a second provider was added with no code change at all.

For a public channel the same abstraction carries a harder requirement: the customer channel's traffic profile is not the employees'. Sizing it is a measurement, and the instrumentation to take that measurement is already in place.

What we need from NAB

Four decisions, and none of them are ours to make.

Each of these blocks a specific piece of work and none of them block the rest. We can begin on the public corpus and the customer surface while the other three are being settled.

In one sentence

The customer channel is the same engine with a different corpus, a stricter policy, and a boundary the model cannot cross.

Everything that makes it safe for employees is what makes it safe for customers — and it is running today.


North Africa Bank North Africa Bank UTEEK Digital Agency UTEEK Digital Agency