Agrofin

Assistant — the digital helper

On this page 11

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:

  1. Gemini (Vertex AI) — used only when GCP_PROJECT is 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.
  2. 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 to Accept-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).

json
{ "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

json
{
  "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.