Skip to content

AI Hub

Per-country configuration surface for the EnvIA AI agent: business hours, holidays, operational documents, and a chat sandbox. Lives at /ai/config in the admon and talks to the agentic-ai backend.

Overview

The admon historically exposed six "agents" (Tracking, Shipping, Ticket, Pickup, Sales, General) as editable entities. Those entities no longer exist as routable agents — the unified support graph in agentic-ai absorbed all of them. This module replaces that legacy view with:

  1. A single EnvIA agent card (read-only metadata).
  2. A per-country settings panel (business hours + holidays) that persists to the countries Supabase table.
  3. A per-country documents panel (PDF / Markdown) that uploads to the country-documents Supabase Storage bucket and indexes them in the RAG pipeline.
  4. An AgentSandbox that hits POST /v1/chat for live testing.

The previous /ai/knowledge cross-country view was retired so managers cannot see documents from other countries. All knowledge management happens per-country from /ai/config.

Key Concepts

ConceptDescription
EnvIAThe single unified support agent. Lives as the support graph in agentic-ai/src/graphs/support/. Configurable surface (hours, holidays, docs) is per-country.
Country settingsbusiness_hours and holidays JSONB columns on the countries table in Supabase. Read at runtime by business-availability.util.ts and isHoliday().
Country documentsRows in country_documents referencing files in the country-documents Supabase Storage bucket. Indexed into document_chunks for RAG retrieval.
SandboxThe right column of /ai/config. Calls POST /v1/chat with channel: 'http' and a per-conversation userId so admin tests never collide with real user state.
Permission gateA single permission with class_name = 'menu-ai-hub-config' drives sidebar visibility. Granted via role assignment or per-user override.

Architecture

┌────────────────────────────────────────────────────────────────┐
│ admon (Vue 3)                                                   │
│                                                                 │
│   views/ai/config/index.vue                                     │
│     ├─ CountrySettingsPanel.vue   (hours + holidays)            │
│     ├─ CountryDocumentsPanel.vue  (PDF/MD upload + signed URL)  │
│     └─ AgentSandbox.vue           (POST /v1/chat)               │
└────────────┬───────────────────────────────────────────────────┘
             │  HTTP /_/ai-hub/*  (admon JWT cookie + permission gate)

┌────────────────────────────────────────────────────────────────┐
│ admon (Node + Hapi) — proxy only                                │
│                                                                 │
│   routes/aiHub.routes.js                                        │
│     auth: token_admin                                           │
│     pre:  permissionMiddleware.canByName('menu-ai-hub-config')  │
│                                                                 │
│   controllers/aiHub.controller.js                               │
│     forwards /ai-hub/{path*} → ${ENVIA_AGENT_API_URL}/{path*}   │
│     adds `x-admin-token: ${BOT_AI_TOKEN}` server-side           │
└────────────┬───────────────────────────────────────────────────┘


┌────────────────────────────────────────────────────────────────┐
│ agentic-ai (Node + Hapi)                                        │
│                                                                 │
│   src/routes/countries.route.ts                                 │
│     GET   /v1/countries           (lists with settings)         │
│     PATCH /v1/countries/{code}    (writes back settings)        │
│                                                                 │
│   src/routes/documents.route.ts                                 │
│     POST   /api/documents/upload         (multipart)            │
│     GET    /api/documents/{country_code} (list per country)     │
│     GET    /api/documents/{id}/preview   (signed URL, 1h TTL)   │
│     DELETE /api/documents/{id}           (cascade delete)       │
│                                                                 │
│   src/routes/chat.route.ts                                      │
│     POST  /v1/chat                       (sandbox + production) │
│                                                                 │
│   src/config/countries.config.ts                                │
│     isHoliday(country, date, dbHolidays?, dbTimezone?)          │
│       └─ DB-loaded holidays take precedence over holidays.json  │
│                                                                 │
│   src/shared/business-availability.util.ts                      │
│     isBusinessAvailable(countryConfig, country)                 │
│       └─ Reads business_hours.weekday/weekend + DB timezone     │
└────────────┬───────────────────────────────────────────────────┘

             ▼  pg pool (writes) + countryService cache (reads)

┌────────────▼───────────────────────────────────────────────────┐
│ Supabase (Postgres + Storage)                                   │
│                                                                 │
│   countries          (business_hours, holidays jsonb)           │
│   country_documents  (bucket_path, processing_status, …)        │
│   document_chunks    (chunked + embedded for RAG)               │
│                                                                 │
│   Storage bucket: country-documents (private, signed URLs)      │
└────────────────────────────────────────────────────────────────┘

The Supabase ↔ agentic-ai layer also feeds countryService (3-level cache: memory → Redis → pg) which the support graph consults on every conversation. See agentic-ai/src/services/country.service.ts.

Data Flow

Manager edits business hours and holidays

Manager uploads a document

Manager opens a document

Click "Abrir"

api.ai.getDocumentPreview(doc.id)

GET /api/documents/{id}/preview

supabase.storage.from('country-documents').createSignedUrl(path, 3600)

{ previewUrl } (1-hour TTL)

window.open(url, '_blank', 'noopener,noreferrer')

The signed URL is never persisted in localStorage, the Vuex store, or any log line. noopener,noreferrer prevents the destination tab from accessing window.opener or leaking the URL via the Referer header.

Database

Tables

TableUsed forNotes
catalog_admin_permission_groupsSidebar grouping ("AI" with sort_order = 50)Single row added by the seed
catalog_admin_permissionsOne row keyed by class_name = 'menu-ai-hub-config'parent_id = 0 (top-level), action = 'ai/config', is_menu = 1
administrator_role_permissionsGrants the permission to a roleDefault seed assigns to role_id = 1 (Super Admin) only
admin_permissions_overridesPer-user grant/revokeUsed to give individual managers access without changing their role
countries (Supabase Postgres)business_hours, holidays, timezone columnscountryService reads with cache, PATCH writes through
country_documents (Supabase Postgres)One row per uploaded documentbucket_path references the Supabase Storage object
document_chunks (Supabase Postgres)Chunked + embedded for RAGPopulated by the background ingestion in processDocumentInBackground

Permission seed

Run manually on each environment before enabling the module. The idempotent SQL and required env vars belong in the pull request description (or internal DBA runbook), not in this repo’s documentation.

Tables touched: catalog_admin_permission_groups, catalog_admin_permissions, administrator_role_permissions (default grant to Super Admin, role_id = 1). Per-user access without role changes uses admin_permissions_overrides.

Key Decisions

DecisionReasoningAlternatives considered
Single hardcoded EnvIA agentThe unified support graph is the only routable agent. Listing six legacy fantoms misled managers into thinking they could edit each one.Keep the list but mark them deprecated (rejected — still confusing). Fetch the list dynamically (rejected — there is only one agent).
One top-level menu permission (no parent + child)The module only has one route (/ai/config). A parent container without an action just added an empty intermediate level.Parent + child structure as originally seeded (rejected — extra clicks, no value when only one child exists).
Restrict uploads to PDF + MarkdownThe chunker performs noticeably better on structured Markdown (## / ### heading boundaries). PDF preserves layout for documents that already exist as such. CSV/JSON/XLSX/images added noise.Accept all formats and let the chunker handle them (rejected — quality dropped on unstructured inputs).
Filter metadata.global = true at the admon UIGlobal documents are managed in a different surface (cross-country), and exposing them per-country drowned the manager in irrelevant rows.Filter at the backend GET (rejected — would affect other consumers).
Per-row window.open with noopener,noreferrer for previewsSigned URLs are short-lived but still bearer-equivalent. Preventing Referer and window.opener leakage is cheap and standard.Inline iframe preview (used previously — failed on CORS / X-Frame-Options for some bucket configs).
channel: 'http' in the sandbox (was 'admin-sandbox')The chat validator only accepts the enum http / respondio / whatsapp / telegram / enviaclaw / clients_portal. 'admin-sandbox' returned HTTP 400. The traceability moved to metadata.source.Add 'admin-sandbox' to the backend enum (rejected — pollutes the production enum with a UI-only value).
isHoliday() reads from DB first, JSON fallbackThe previous behaviour ignored the countries.holidays column entirely. Managers could edit it without effect. New signature takes optional dbHolidays and dbTimezone; callers pass values from countryConfig.Move all holidays to DB and delete the JSON (rejected — keep the JSON as emergency fallback when DB is unavailable).
Strip bucket_path from preview error logsBucket paths leak country and region identifiers into observability pipelines. The document UUID is enough to correlate via DB.Keep the path for easier debugging (rejected — privacy first).
Backend write goes through PATCH /v1/countries/:code, not a generic admin endpointScope is tight: only business_hours and holidays. Validation is column-specific. Auth uses the existing x-admin-token pattern.Generic /admin/countries/:id route (rejected — broader attack surface, harder validation).
Frontend falls back to localStorage if the PATCH failsThe manager would lose all their edits otherwise. The draft is restored on the next mount and the badge "cambios guardados aquí" makes the state visible.Block the save and show an error (rejected — bad UX during transient backend outages).

Dependencies

Internal (admon)

  • views/ai/config/index.vue (entry point)
  • components/CountrySettingsPanel.vue, CountryDocumentsPanel.vue, AgentSandbox.vue
  • services/ai.service.jsgetCountries, updateCountrySettings, getDocuments, uploadDocument, getDocumentPreview, deleteDocument, chat
  • composables/useAiLocale.js (cookie-driven country sync)
  • Stores useAiStore (countries cache) and useAuthStore (menu rendering)

External (agentic-ai)

  • src/routes/countries.route.ts, src/routes/documents.route.ts, src/routes/chat.route.ts
  • src/services/country.service.ts (3-level cache)
  • src/config/countries.config.ts (isHoliday, JSON fallback)
  • src/shared/business-availability.util.ts
  • src/services/documents/document-extractor.service.ts
  • Supabase Storage SDK (@supabase/supabase-js)

Permissions / lang

  • catalog_admin_permission_groups row "AI"
  • catalog_admin_permissions row menu-ai-hub-config
  • i18n keys (menu.group.ai, menu.ai.hub, etc.) in 20 lang files at backend/resources/langs/*.json. Synced to S3 by the existing language pipeline.

OpenSpec proposals

This module is documented across three OpenSpec changes in agentic-ai/openspec/changes/:

FolderScope
expose-country-settings/GET /v1/countries returns business_hours + holidays; isHoliday() accepts DB-loaded inputs
admon-ai-config-polish/UX overhaul: single agent, PDF/MD only, link-only previews, manager-friendly copy, pagination, capabilities panel
persist-country-settings-from-admon/ (pending)PATCH /v1/countries/:code + frontend wiring (this proposal not yet formalized but the code lives in the same commit cluster)

Envia Admin