Assistant
The chat widget on the public site. It answers only from the Fund's approved material — the FAQ base, financing programmes, legal acts register, reception schedule and organisation chart in Catalog, the latest 15 published stories from News, plus a free-form knowledge page staff edit in the admin panel. When nothing matches, it says so and gives the trust line rather than inventing a rate.
How an answer is produced
Two generators are tried in order:
- Gemini (Vertex AI) — used only when
GCP_PROJECTis set. The composed knowledge base goes into the system prompt; the model is instructed to stay inside it and cite the act it relied on. - Knowledge search — a weighted keyword search over the FAQ base. Always available, needs no credentials, and returns verbatim approved text with the source act attached.
If neither produces an answer the reply is a plain "I cannot answer that,
call +998 71 231-02-17" with confident: false. That case is deliberate:
a wrong rate is worse than no rate.
Deployment note: with no GCP project configured the assistant still works — it runs on knowledge search alone. Adding the credentials later needs no code change.
Conversation rules
The Gemini layer follows the same rules as the OKS site assistant:
- Reply language is detected from the visitor's own text (
uz,uz_cyrl,ru,en), falling back toAccept-Language. Any other language is answered in Uzbek. - Name: the first reply answers the question and asks the visitor's name
once. The widget sends it back as
name; from then on the assistant may address the visitor by name and never asks again. - Scope: only Fund topics — programmes, terms, acts, reception, contacts, news. Anything else (arithmetic, weather, code, politics…) is politely refused and steered back.
- Style: 2–4 short sentences, plain text (no markdown), greeting only in the first reply, no promises — a loan is never "approved" in the chat.
- Phone: when the visitor wants a specialist, the assistant offers to take a phone number for a callback. A valid Uzbek number in any message is stored on the session (once) and staff are notified.
- Live employee: a request for a real person in the chat notifies staff; once an employee replies from the admin panel the conversation is in human takeover — the assistant stays silent until it is released.
POST /api/v1/assistant/ask
Public. Rate limited to 20 requests per minute per IP, plus a per-device
daily cap (ASSISTANT_DAILY_LIMIT, default 100 messages).
{ "device_id": "dev-3f8a2b91c4", "question": "Kartoshka uchun kredit shartlari qanday?", "name": "Xusan" }
device_id is generated by the browser and stored in localStorage:
8–64 characters, A-Z a-z 0-9 . _ -. It is what ties a conversation
together — there is no sign-in.
question is 1–500 characters (ASSISTANT_MAX_CHARS). name is optional
(≤100) — the widget sends it once, when the visitor answers the name
question; later values are ignored.
Response
{
"status": "ok",
"data": {
"answer": "Jamg'arma mablag'lari hisobidan hosil qiymatining 50 foizigacha…",
"source": "PQ-269 — Kartoshkachilikni rivojlantirish…",
"legal_document_id": 17,
"generator": "knowledge",
"confident": true,
"name": "Xusan",
"card": null,
"human_takeover": false
}
}
| Field | Meaning |
|---|---|
answer |
the reply to render |
source |
human-readable origin — the act, or the FAQ category when no act is linked |
legal_document_id |
link target for /api/v1/legal-documents/{id}; null when unknown |
generator |
gemini, knowledge, fallback, card (location card) or operator (employee has the chat) |
confident |
false only for the fallback — render it in a muted style |
name |
the visitor's stored name, or null |
card |
a location card (kind, pin, rows[{icon,label,value,href?}], action{label,href}) when the visitor asked where the office is — answer is then empty; otherwise null. The card is read from the knowledge page (Manzil:, Ish vaqti:, Ishonch telefoni: lines and the first map link) |
human_takeover |
true when an employee holds the conversation: answer is empty, the message was stored, and the widget should poll history for operator messages |
429 when the daily cap is reached; the message is localized.
GET /api/v1/assistant/history/{deviceId}
Public, 60/min. Returns the last 20 turns oldest-first so the widget can restore the conversation after a reload. An unknown device returns an empty array, not a 404.
Each message carries role (user / assistant / operator — an employee
writing from the admin panel), content, created_at and source (null,
or the act with number, title and url). The widget polls this every
few seconds while open to show employee replies.
Knowledge base (admin)
Both routes live under the /api/v1/admin prefix and require auth:sanctum
plus a permission: GET requires view assistant-knowledge, PUT/PATCH
requires update assistant-knowledge. A signed-in user without the
permission gets 403 Forbidden. POST /api/v1/assistant/ask and GET /api/v1/assistant/history/{deviceId} above are unaffected — they stay
fully anonymous, no auth or permission required, and keep their public
URLs.
GET /api/v1/admin/assistant/knowledge
Returns { content, updated_at, source } — the hand-written part only.
While nothing has been saved yet, content is the repository file the
assistant is actually using, updated_at is null and source is
file; after the first save it is the stored text with source =
database. The programmes, legal acts, FAQ, reception schedule,
organisation chart and latest news are composed in automatically and are
edited through the Catalog and News endpoints instead.
PUT|PATCH /api/v1/admin/assistant/knowledge
Body: { "content": "…" }, markdown, up to 200 000 characters. Replaces
the single row (there is no history) and clears the composed-knowledge
cache for every supported locale — uz, ru and uz_cyrl — so no
reader is served a stale base while the TTL runs out.
Use it for what the FAQ does not cover: address, working hours, what the Fund is. Anything that is a question with an answer belongs in the FAQ base — that way it is searchable on the site as well.
Conversations (admin)
Every exchange with the widget is stored per device, so staff can see what
visitors ask and how the assistant answered. Routes live under
/api/v1/admin, require auth:sanctum and a permission; a signed-in user
without it gets 403.
| Endpoint | Permission | Notes |
|---|---|---|
GET /api/v1/admin/assistant/sessions |
view assistant-history |
paginated (meta.pagination), newest activity first |
GET /api/v1/admin/assistant/sessions/{id} |
view assistant-history |
one session with its messages, oldest first |
DELETE /api/v1/admin/assistant/sessions/{id} |
delete assistant-history |
removes the session and all its messages; 404 in the envelope for an unknown id |
POST /api/v1/admin/assistant/sessions/{id}/messages |
reply assistant-history |
body { content } (≤2000). Stores an operator message and puts the session into human takeover — the assistant stops answering that visitor. Returns the message plus human_takeover: true, 201 |
POST /api/v1/admin/assistant/sessions/{id}/release |
reply assistant-history |
hands the conversation back to the assistant (human_takeover: false) |
List filters and sorts follow the list contract:
filter[search] (device id, name or phone digits), filter[device_id]
(partial match), filter[locale] (uz, ru, uz_cyrl),
filter[with_phone]=1 (only visitors who left a number),
filter[human_takeover]=1 (only conversations held by an employee); sort
accepts last_seen_at, messages_count, id (default -last_seen_at).
A session carries id, device_id, ip, locale, name, phone,
human_takeover, messages_count, daily_count (messages today),
last_seen_at, created_at. Each message carries id, role (user /
assistant / operator), content, generator (gemini, knowledge,
fallback, card, or null for a user or operator turn), source (the
cited act, or null) and created_at.
Staff notifications
With TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID set, the staff group gets a
message when a visitor gives their name (conversation started, with the
first real question), leaves a phone number, or asks for a live employee
(at most once per six hours per visitor). ADMIN_URL adds a link that opens
the transcript in the admin panel. Left empty, nothing is sent and the chat
works as before.
Caching
The composed knowledge and the search index are cached for 10 minutes. A FAQ, act, schedule row or news story added in the admin panel becomes answerable within that window; editing the knowledge page clears its cache immediately.