Configuration¶
AI Hub combines environment-level infrastructure settings with an application
configuration document. MongoDB deployments store one Config document in
each tenant database. PostgreSQL deployments use one global configuration in
the database selected by DATABASE_URL. Never commit real keys, tokens or
connection strings.
Client configuration normalization¶
The administrator endpoint
PUT /admin/api/v1/configuration/clients/normalize reconciles stored configuration
with src/templates/config_file.json. It adds missing defaults, recursively
removes keys outside the template and preserves existing scalar values and
lists. A structural section that is not an object is rejected before writing.
Normalization is idempotent: an already normalized configuration is not written
again. Existing credential sanitization and SQL import identifier validation
still apply.
The following dynamic mappings are preserved in full when present, including empty objects:
hard_filters,chatbot.hard_filtersandrules;ai_configuration.llm_profiles, including configured models and reasoning settings;skill_scores.weight_type;standardization.{user,opportunity,job,training}.skills_inferring.reference_ids.
If a dynamic section is absent, it receives its template default. The canonical
template also includes the supported user/opportunity import field selectors,
user/opportunity/feedback transformation client selectors, pool.additional_pool
and jpm_scores controls, so normalization retains these active settings. The
PostgreSQL initialization seed includes the same defaults for newly created
configuration rows; existing rows change only when normalization is requested.
MongoDB configuration reads and replacements, client enumeration and PostgreSQL configuration access use asynchronous drivers. The request returns HTTP 202; completion counts and failures are emailed to the selected configuration's recipients (the template for all-client MongoDB normalization). See Administration for client selection, global processing, response counts and errors.
Docker image¶
The Dockerfile uses python:3.14-slim and installs runtime and documentation
dependencies from requirements.txt, without retaining the pip download cache
or dependency bytecode. It installs only the entries above the exact
# Test dependencies marker; keep that marker unchanged and place every runtime
or documentation dependency above it. Local development and CI quality jobs
still install the complete file, including the trailing test and development
tools. All versions remain defined in that single file.
defusedxml is an explicit runtime dependency so XML parsing protections do not
depend on the installation of development tools.
The runtime image excludes evaluation cases and results, and removes the bundled
tests directories from NumPy, SciPy, pandas and scikit-learn. Runtime datasets
and testing helper modules remain available. Native extension debug information
is stripped while executable code and dynamic symbols are retained; binutils
is installed and removed within the same build layer. Native source-level
debugging therefore requires an image built without that stripping step.
These packaging changes do not remove scientific libraries or alter their
configured algorithms. Development and CI environments retain the full packages.
Development caches, tests and Python bytecode are also excluded from the build
context. The image keeps the application sources, Alembic migrations,
configuration and documentation tooling needed to generate the English site at
/docs/ in a background subprocess after API initialization. See the
documentation lifecycle.
Python can generate bytecode normally when the container runs.
The database addresses in .env must be reachable from the container;
localhost points to the container itself. Use the database service name on a
shared Docker network, or host.docker.internal on Docker Desktop for a database
running on the host. Check http://localhost:8000/health-check after startup.
The image runs Gunicorn with uvicorn_worker.UvicornWorker, three workers and
port 80. uvicorn-worker supplies the Gunicorn integration; uvicorn remains
the explicitly pinned ASGI server dependency.
The release Dockerfile also remains compatible with the Kaniko build used by CI.
The staging and production build job uses gzip compression level 6 for newly
built image and cache layers, reducing registry upload size at the cost of more
compression CPU time. This changes transfer size, not the installed image
contents, and does not replace diagnosis of Harbor or reverse-proxy errors.
The runtime documentation builder reads API versions and dependencies directly
from source files. Docker builds and Rundeck/Ansible source copies require no
version build argument or release metadata file. Global release tags are created
or verified by the CI version job on develop and release/*. Requirements-only,
documentation-only and tests/ changes increment that tag without changing API
versions.
Core environment variables¶
Deployment environment¶
FastAPI resolves one deployment environment during application startup. Set
APP_IDENTIFIER_SHORT to the environment name. If it is absent, empty or equal
to mutu ignoring case, AI Hub falls back to ENV. Startup fails if the
resulting value is empty.
The resolved value is shared with every mounted application and all background
work. It supplies the Swagger default for environment query parameters and
scopes MongoDB database and generated collection names. An explicit API input
must match it; a different or empty value returns 400. API keys do not carry
or override this setting in either MongoDB or PostgreSQL mode.
MongoDB¶
MONGO_CONNECTION_STR='mongodb://user:password@host:27017'
MONGO_MAX_POOL_SIZE='20'
MONGO_PROVISIONING_TIMEOUT_SECONDS='120'
This connection provides every MongoDB database used by AI Hub, including
common_data, reference_data and tenant databases. When DATABASE_URL is
absent, it also stores LangGraph checkpoints and long-term memory. It must
support the Search and Vector Search features used by embeddings and hybrid
search. PwC deployments can additionally provide MONGO_PWC_STAGING_DSN and
MONGO_PWC_PROD_DSN.
Application and administrative operations use native async MongoDB clients. Only the pinned LangGraph MongoDB adapters retain a dedicated synchronous client for the default DSN; no synchronous PwC clients are initialized. Shared document types live separately from client construction. See LangGraph persistence for lifecycle ownership.
MongoDB clients validate the connection with a ping during initialization.
If the URI is rejected or authentication fails with MongoDB error code 18 and
the password requires encoding, AI Hub retries once after URL-encoding only
that component. Existing percent-encoded passwords are preserved. Successful
sync and async connections produce an application-level info log without
exposing the URI or credentials. Other connection, authorization and network
errors are returned without this fallback.
AI Hub supplies the Certifi CA bundle to MongoDB clients by default. For a
deployment that does not expose TLS, add tls=false to its URI. The connection
factory detects this option and does not pass tlsCAFile, including for the
sync, async and PwC clients:
MONGO_CONNECTION_STR='mongodb://user:password@mongo1:27017,mongo2:27017/?replicaSet=rs0&authSource=admin&tls=false'
Set the option on every non-TLS URI. Connections without an explicit
tls=false retain the CA bundle and existing TLS behavior.
AI providers¶
Without DATABASE_URL, provider and model definitions are read from the
MongoDB AIProviders collection. PostgreSQL deployments read the same logical
objects from aihub_ai_providers. Alembic seeds the ovh, openai,
anthropic and azure providers, with OpenAI as the default LLM and embedding
provider. Voyage is not supported and its obsolete PostgreSQL configuration is
removed by migration. Model dictionaries remain JSONB so provider-specific
attributes are preserved, while provider identity and default flags are
relational columns.
MongoDB deployments are not modified by Alembic. Remove any legacy voyage
document from their AIProviders collection before selecting or exposing the
available providers; the runtime no longer contains a Voyage client.
Provide credentials only for providers referenced by tenant configuration:
# OpenAI
OPENAI_API_KEY='...'
# Azure OpenAI
AZURE_OPENAI_API_KEY='...'
AZURE_OPENAI_URL='https://<resource>.openai.azure.com'
# Anthropic
ANTHROPIC_API_KEY='...'
# OVH AI Endpoints
OVH_API_KEY='...'
Live voice sessions use the direct OpenAI provider and therefore require
OPENAI_API_KEY. OPENAI_CA_BUNDLE can select a custom trusted CA bundle for
the Live WebSocket.
Direct outbound HTTP¶
AIHub-owned direct HTTP calls use HTTPX2 and verify certificates against the
operating-system trust store. Minimal containers and deployments behind a
TLS-inspecting corporate proxy must install their CA system-wide or configure
SSL_CERT_FILE or SSL_CERT_DIR. HTTP clients owned by OpenAI, LangChain and
other frameworks retain their own dependency and certificate behavior.
Environment and authentication¶
ENV='integration'
APP_IDENTIFIER_SHORT='integration'
AI_HUB_USER_API_KEY='<user-api-key>'
AI_HUB_ADMIN_API_KEY='<admin-api-key>'
JWT_PUBLIC_KEY_PATH='/run/secrets/jwt-public-key.pem'
JWT_PUBLIC_KEY_PATH is required only when clients send bearer JWTs. See
authentication for the RS256 contract and Client-ID
routing. Standard endpoints accept AI_HUB_USER_API_KEY or
AI_HUB_ADMIN_API_KEY; administrator endpoints accept only
AI_HUB_ADMIN_API_KEY. Each non-empty applicable variable is authoritative. If
none is configured, MongoDB fallback is available only without DATABASE_URL;
PostgreSQL mode rejects the request. The generated Zensical site, Swagger,
ReDoc and OpenAPI schemas are public.
Request resource limits¶
Untrusted spreadsheet, CSV, JSON and other non-audio uploads are read in
bounded chunks. The default limit is 10 MiB. Chat speech and speech-to-text
uploads use a separate 25 MiB default. Oversized content returns HTTP 413,
including when no Content-Length header is available.
Upload and LLM-heavy POST routes share a fixed-window IP quota checked before
authentication, including requests with missing or invalid keys. After API-key
validation, separate upload and LLM quotas also apply to the valid key across
client addresses. Counters store SHA-256 digests of addresses and keys. SQLite
shares counters between the three Gunicorn workers in one container; it performs
its short transactions on worker threads. The file is not shared between pods,
so multi-pod deployments must retain a global rate limit at the ingress or API
gateway. Exceeding either quota returns 429 with Retry-After; unavailable
counter storage returns 503 before business processing.
The IP quota uses the ASGI client address. AIHub does not parse Forwarded or
X-Forwarded-For itself. Behind an ingress, configure Gunicorn's
FORWARDED_ALLOW_IPS with the trusted proxy addresses or networks and ensure that
the ingress sanitizes forwarding headers. The container's Uvicorn worker passes
this trust configuration to its proxy middleware. Without it, clients behind a
proxy may share the proxy's quota. Do not trust arbitrary forwarding headers or
set FORWARDED_ALLOW_IPS=* on a directly accessible server. Tune the IP quota for
legitimate clients sharing an address.
| Variable | Default | Behavior |
|---|---|---|
UPLOAD_MAX_BYTES |
10485760 |
Maximum upload size for Talent Intelligence, matching statistics and administrator synchronization files |
AUDIO_UPLOAD_MAX_BYTES |
26214400 |
Maximum chat speech and speech-to-text upload size |
RATE_LIMIT_ENABLED |
true |
Enables sensitive-route rate limiting |
RATE_LIMIT_WINDOW_SECONDS |
60 |
Fixed-window duration |
RATE_LIMIT_IP_REQUESTS |
120 |
Total sensitive-route requests per client IP and window, before authentication |
RATE_LIMIT_LLM_REQUESTS |
60 |
LLM-heavy requests per valid API key and window |
RATE_LIMIT_UPLOAD_REQUESTS |
20 |
Upload requests per valid API key and window |
RATE_LIMIT_STORAGE_PATH |
system temporary directory under aihub-<uid>/rate-limits.sqlite3 |
Shared counter database for workers in one container |
LangSmith¶
The legacy LANGCHAIN_API_KEY, LANGCHAIN_TRACING and
LANGCHAIN_TRACING_V2 names are also recognized. A key and at least one truthy
tracing flag are required. These settings trace named LangGraph executions and
their named children, including connector LLM calls and Training Finder database
searches. LLM calls made outside an active graph are not sent to LangSmith. The
PostgreSQL migration smoke job explicitly disables all tracing flags so its
synthetic persistence graph cannot appear in an environment project. See
LLM runtime for usage, cost, prompt-cache and
database-search fields.
Blocking work pool¶
The shared run_blocking helper runs document extraction, catalogue indexing,
chatbot job/opportunity vector deduplication, spreadsheet processing and local
SFTP key/stream reads in a dedicated executor. SFTP network transfers use AsyncSSH directly.
The SQLite request limiter keeps using asyncio's separate default executor.
| Variable | Default | Constraint |
|---|---|---|
BLOCKING_WORKERS |
4 |
Positive integer; maximum concurrent executor calls |
BLOCKING_MAX_PENDING |
4 |
Nonnegative integer; additional submitted calls waiting for a thread |
Limits apply per application worker; three Gunicorn workers therefore allow up to twelve concurrent calls in these pools. Libraries can have additional internal threads. Requests beyond the admission limit wait asynchronously before submission; this bounds the executor queue, not the total number or memory of incoming HTTP requests. Invalid settings fail startup.
Cancelled queued calls are skipped when a thread reaches them. Their slots remain reserved until then, preventing cancelled entries from accumulating beyond the queue limit. An already running thread continues and retains its capacity until completion; Python cannot forcibly stop it. Its result or exception is retrieved even if its caller has disconnected. The lifespan drains the pool before closing database resources. Shutdown can therefore wait for a running synchronous operation; no hard timeout is implied. Request correlation context is copied into each submitted call.
Standalone asynchronous scripts that call these workflows must own the same scope (the Lexi evaluation command already does):
from src.common.blocking_work import blocking_work_scope, run_blocking
async def main():
async with blocking_work_scope():
result = await run_blocking(str, 42)
Batched MongoDB vector searches¶
MONGO_VECTOR_SEARCH_CONCURRENCY defaults to 8 and must be a positive integer.
It bounds batched skill searches and the skill/opportunity/job similarity
lookups in atlas_vector_search.py. Each call creates at most this many workers,
and all these calls share the same active-query budget per application worker.
The budget is not a limit on every MongoDB query or on PostgreSQL searches.
With three Gunicorn workers, these MongoDB lookups can run up to 24 queries
concurrently across the process group.
Input ordering, result dictionaries, cosine conversion, thresholds, candidate filters, projections and index selection are preserved. Ordinary query failures allow the other items to finish before the first observed exception is propagated; partial results are not returned. Request cancellation stops the workers and awaits cursor cleanup before returning, including if cancellation is repeated. Cursors are also closed when processing fails.
FastAPI owns the budget through its lifespan. Standalone scripts using these
MongoDB workflows must enter mongo_search_scope() from
src.common.connectors.mongodb.search_concurrency, alongside
blocking_work_scope() when they also invoke synchronous processing.
Datalake and batch processing¶
Configure Azure Blob storage with:
When BLOB_ENDPOINT is absent or empty, Datalake writes are silently skipped.
Batch controls:
See batch processing for queueing, backpressure and file compatibility.
Outgoing email¶
All outgoing email, including administrative reports, Lexi results and error
notifications, uses one shared asynchronous connector. A nonblank BREVO_API_KEY
selects the Brevo HTTPS API:
Use a Brevo API key, not an SMTP key. The sender address must be authorized in
the Brevo account. With a key configured, MAIL_SENDER is required; a missing
sender is a configuration error. Brevo failures never trigger SMTP delivery.
If BREVO_API_KEY is missing, empty or whitespace-only, the connector selects
the legacy SMTP transport instead. This supports environments where Brevo is
unavailable, including US staging and production. Configure all five variables:
MAIL_SENDER='aihub@example.com'
MAIL_SMTP_LOGIN='...'
MAIL_SMTP_KEY='...'
MAIL_SMTP_URL='smtp.example.com'
MAIL_SMTP_PORT='587'
MAIL_SMTP_URL is a hostname, without a URL scheme. SMTP uses authenticated
STARTTLS with certificate verification, not implicit TLS. The port must be an
integer between 1 and 65535; an invalid configured port is a configuration error.
The password is preserved exactly as configured.
If any required SMTP variable is missing, empty or whitespace-only, the connector logs one warning listing the missing variable names and skips delivery without opening a network connection. The calling process continues, but no email is sent. Environment diagnostics expose only presence flags for the Brevo key and SMTP login, key, host and port; their values are never included. The SMTP path does not import the Brevo SDK.
Both transports support plain text, HTML and file attachments. On Brevo, JSON
attachment names gain a .txt suffix, such as
operation-123.json.txt, because Brevo does not accept .json attachments; their
contents are unchanged. SMTP retains original attachment names, including .json,
and safely encodes special characters in MIME attachment headers. Both retain
attachment bytes and use results.csv when no filename is provided.
Each send owns and closes its connection, including on failure or cancellation. An overall 20-second deadline bounds delivery. The Brevo SDK alone retries transient connection/server/rate-limit failures, with at most three attempts; authentication errors are not retried. SMTP makes one attempt without automatic retries. Configured delivery errors propagate to the existing caller boundary; administrative reports isolate them from the operation result. Successful acceptance by the selected transport is logged without recipient details and does not guarantee inbox delivery.
Lexi batching¶
Lexi has separate controls because its intentionally large generation outputs must not use the generic LLM limits:
| Variable | Default | Maximum |
|---|---|---|
J2S_DESCRIPTION_BATCH_SIZE |
25 skills |
100 |
J2S_DESCRIPTION_CONCURRENCY |
5 |
20 |
J2S_LEVEL_BATCH_SIZE |
10 jobs |
50 |
J2S_LEVEL_CONCURRENCY |
5 |
20 |
LEXI_MISSION_BATCH_SIZE |
10 jobs |
50 |
LEXI_MISSION_CONCURRENCY |
5 |
20 |
J2S_ASSIGNMENT_BATCH_SKILL_THRESHOLD |
1000 assignments |
1000000 |
LEXI_SEMANTIC_EMBEDDING_CHUNK_SIZE |
512 labels |
2048 |
LEXI_SEMANTIC_EMBEDDING_CONCURRENCY |
16 |
32 |
LEXI_DETERMINISTIC_ANALYSIS_WORKERS |
2 |
8 |
LEXI_SEMANTIC_ANALYSIS_WORKERS |
2 |
8 |
LEXI_QUALITY_TRANSFER_CONCURRENCY |
8 pages |
32 |
Anthropic generation additionally supports
J2S_GENERATION_CLASSIC_MAX_TOKENS, J2S_GENERATION_MAX_TOKENS,
J2S_GENERATION_BATCH_POLL_INTERVAL_SECONDS and
J2S_GENERATION_BATCH_TIMEOUT_SECONDS. These are output/call-mode controls,
not global chatbot limits.
Smart Mobility SQL imports¶
Numeric tenant and entity IDs are validated before opening the SQL connection
and bound as integers. Decimal integer strings such as "0042" are accepted as
ID 42; alphanumeric values, booleans, fractional numbers and values outside
0..9223372036854775807 are rejected. Searches by opportunity code, employee
number or email preserve the original text, including leading zeros.
Opportunity imports retain custom_job.code as joboffer_code in imported
records and code in standardized documents. Null values remain null;
non-null values are stored as strings, preserving existing text, whitespace and
leading zeros. The field is descriptive metadata for identifying offers and is
not added to matching, inference, translation or embedding inputs. Existing
MongoDB documents receive the source value on their next import and standardization.
Optional standardization.user.import_fields.contract_type,
standardization.user.import_fields.relationship and
standardization.opportunity.import_fields.contract_type accept integer IDs or
decimal integer strings, including the disabled sentinel -1. Omit unused
settings; explicit null or empty values are invalid. Updating configuration via
PUT /admin/api/v1/configuration/clients rejects invalid values with HTTP 422
before persistence. PostgreSQL configuration writes apply the same validation;
existing configurations written outside AIHub must also satisfy these rules.
standardization.user.import_fields.technical_classification and
standardization.opportunity.import_fields.technical_classification follow the
same identifier validation and disabled -1 convention. They select Smart
Editor fields independently for users and offers. The SQL import and PostgreSQL
projections read the selected option's company_entity_item.name when
company_item_id is set; otherwise they read the association's name. An
unresolved option is missing data, never its numeric option ID. Both joins are
restricted to the entity's tenant and selected field. Single-valued fields are
expected; if duplicate associations exist, the lowest association ID wins.
The resulting technicalClassification is a string, with "-1" for a null or
empty value. MongoDB imports extract a numeric prefix followed by a colon from
the resolved value: "19: Other subsidiaries - Executive" becomes "19".
This applies to direct association values and selected options for both users
and offers. Plain codes and values without that numeric prefix remain unchanged.
Existing MongoDB documents require reimport to receive this normalization.
PostgreSQL projections currently preserve source labels. Neither path uses
attached-job grading; jobClassification remains independent.
For La Poste (NB-9232), the import settings are 6530 for users and 6529
for opportunities. These are tenant settings, not global defaults. Configure
hard_filters.jom.user.technical_classification and
hard_filters.jom.opportunity.technical_classification with each direction's
existing additional_parameters.classification_level_matches. Replace the JOM
job_classification rule when activating this change; configuring both rules
would apply both restrictions. Preserve the separate JPM job_classification
rules and their mappings. Existing MongoDB documents require reimport and stored
matching results require recalculation; deploying code alone does not activate
this tenant configuration or populate historical documents.
Offer activities preserve their source code without converting it to an
integer. User completion, entity and hierarchy data remain available to
reporting even when they are not stored in MongoDB user documents.
Active training imports always combine tenant and requested-ID filters with
end IS NULL OR end >= NOW().
Job imports set status=true when job.is_active=1 and either status_id is
null or the associated JobStatus.is_visible=1. A missing status therefore
keeps an active job active. A non-null status reference with no matching
JobStatus row, or a status whose visibility is null or false, does not activate
the job. is_hidden is imported separately and does not affect status.
Smart Mobility reads use SQLAlchemy's async engine with the asyncmy driver.
The connector supports a direct database connection or an SSH tunnel managed by
asyncssh.
For a uniform direct connection, set:
SM_HOST='mariadb.internal'
SM_PORT='3306'
SM_USERNAME='...'
SM_PASSWORD='...'
SM_DATABASE='optional_database_override'
When SM_USERNAME is unset, the connector selects environment-specific values
from the source database name:
| Source | SQL credentials | Direct host | Optional SSH tunnel |
|---|---|---|---|
integration (int_neobrain_io) |
SM_INTEGRATION_USERNAME, SM_INTEGRATION_PASSWORD |
127.0.0.1 |
SM_SSH_INTEGRATION_HOST, SM_SSH_INTEGRATION_USERNAME, SM_SSH_INTEGRATION_PASSWORD |
production (smartmobility_io) |
SM_PROD_USERNAME, SM_PROD_PASSWORD |
SM_PROD_HOST |
SM_SSH_HOST, SM_SSH_USERNAME, SM_SSH_PASSWORD |
| other/test database | SM_TEST_USERNAME, SM_TEST_PASSWORD |
SM_TEST_HOST |
SM_SSH_TEST_HOST, SM_SSH_TEST_USERNAME, SM_SSH_TEST_PASSWORD |
SSH ports default to 22; SQL ports default to 3306 in these fallback
branches. The current tunnel preserves the former permissive behavior by not
verifying the SSH server host key (known_hosts=None). Deployments should
therefore restrict network access and credentials appropriately.
The query result is converted to a pandas DataFrame and then records. Batch
standardization is bounded, but the SQL result itself is materialized in memory.
PostgreSQL runtime and migrations¶
The PostgreSQL notification listener, independent job workers, SQLAlchemy runtime and Alembic migrations share the same database connection settings:
DATABASE_URL='postgresql://user:password@host:5432/sm_mutu'
PG_SCHEMA='mysql'
PG_LANGGRAPH_POOL_MAX_SIZE='10'
PG_EMBEDDING_POOL_MAX_SIZE='10'
PG_JOB_POLL_INTERVAL_SECONDS='10'
PG_JOB_WORKER_RETRY_DELAY_SECONDS='5'
PG_JOB_CANCELLATION_CLEANUP_TIMEOUT_SECONDS='5'
PG_JOB_SHUTDOWN_TIMEOUT_SECONDS='15'
PG_JOB_FORCE_CANCEL_TIMEOUT_SECONDS='1'
PG_EMBEDDING_LISTENER_RETRY_DELAY='5'
PG_EMBEDDING_NOTIFY_CHANNEL='aihub_embedding_jobs'
PG_EMBEDDING_BATCH_SIZE='25'
PG_EMBEDDING_MAX_RETRIES='3'
PG_EMBEDDING_LEASE_SECONDS='900'
PG_EMBEDDING_RETRY_BASE_SECONDS='10'
PG_EMBEDDING_RETRY_MAX_SECONDS='300'
PG_USER_INFERENCE_NOTIFY_CHANNEL='aihub_user_skill_inference_jobs'
PG_USER_INFERENCE_BATCH_SIZE='10'
PG_USER_INFERENCE_MAX_RETRIES='3'
PG_USER_INFERENCE_LEASE_SECONDS='900'
PG_USER_INFERENCE_RETRY_BASE_SECONDS='10'
PG_USER_INFERENCE_RETRY_MAX_SECONDS='300'
PG_USER_RECONCILIATION_PAGE_SIZE='500'
PG_USER_RECONCILIATION_MAX_RETRIES='3'
PG_USER_RECONCILIATION_LEASE_SECONDS='300'
PG_USER_SKILL_GAP_RECONCILIATION_PAGE_SIZE='500'
PG_USER_SKILL_GAP_RECONCILIATION_MAX_RETRIES='3'
PG_ENTITY_RECONCILIATION_NOTIFY_CHANNEL='aihub_entity_reconciliation_jobs'
PG_ENTITY_RECONCILIATION_PAGE_SIZE='1000'
PG_ENTITY_RECONCILIATION_MAX_RETRIES='5'
PG_ENTITY_RECONCILIATION_LEASE_SECONDS='300'
PG_SKILL_SEARCH_EMBEDDING_DIMENSION='1536'
PG_JOB_SEARCH_EMBEDDING_DIMENSION='1536'
PG_JOB_EMBEDDING_NOTIFY_CHANNEL='aihub_job_embedding_jobs'
PG_JOB_EMBEDDING_BATCH_SIZE='25'
PG_JOB_EMBEDDING_MAX_RETRIES='3'
PG_JOB_EMBEDDING_LEASE_SECONDS='900'
PG_JOB_EMBEDDING_RETRY_BASE_SECONDS='10'
PG_JOB_EMBEDDING_RETRY_MAX_SECONDS='300'
PG_OPPORTUNITY_SEARCH_EMBEDDING_DIMENSION='1536'
PG_OPPORTUNITY_EMBEDDING_NOTIFY_CHANNEL='aihub_opportunity_embedding_jobs'
PG_OPPORTUNITY_EMBEDDING_BATCH_SIZE='10'
PG_OPPORTUNITY_EMBEDDING_MAX_RETRIES='3'
PG_OPPORTUNITY_EMBEDDING_LEASE_SECONDS='900'
PG_OPPORTUNITY_EMBEDDING_RETRY_BASE_SECONDS='10'
PG_OPPORTUNITY_EMBEDDING_RETRY_MAX_SECONDS='300'
PG_TRAINING_SEARCH_EMBEDDING_DIMENSION='1536'
PG_TRAINING_ENRICHMENT_NOTIFY_CHANNEL='aihub_training_enrichment_jobs'
PG_TRAINING_ENRICHMENT_BATCH_SIZE='25'
PG_TRAINING_ENRICHMENT_MAX_RETRIES='3'
PG_TRAINING_ENRICHMENT_LEASE_SECONDS='900'
PG_TRAINING_ENRICHMENT_RETRY_BASE_SECONDS='10'
PG_TRAINING_ENRICHMENT_RETRY_MAX_SECONDS='300'
PG_MATCHING_NOTIFY_CHANNEL='aihub_matching_jobs'
PG_MATCHING_BATCH_SIZE='10'
PG_MATCHING_MAX_RETRIES='3'
PG_MATCHING_LEASE_SECONDS='1800'
PG_MATCHING_RETRY_BASE_SECONDS='30'
PG_MATCHING_RETRY_MAX_SECONDS='900'
PG_MATCHING_EXPORT_MAX_ROWS='250000'
PG_RAG_EMBEDDING_DIMENSION='1536'
DATABASE_URL is the canonical PostgreSQL connection string. SQLAlchemy and
Alembic normalize it to the postgresql+psycopg:// dialect when required;
direct psycopg configuration readers remove that SQLAlchemy-only driver marker.
PG_SCHEMA defaults to mysql and qualifies the PostgreSQL
skill_translation, aihub_embedding_jobs, aihub_entity_enrichment_jobs,
aihub_ai_providers, aihub_config and LangGraph persistence
tables. The current
SQLAlchemy models map the AIHub-owned tables and the read-only
mysql.skill_search_v1, mysql.aihub_user_v1 and
mysql.aihub_job_v1, mysql.aihub_opportunity_v1 and
mysql.aihub_training_v1 views, plus the Chatbot-owned aihub_conversations,
aihub_chat_message_log, aihub_events, aihub_documents,
aihub_rag_source_files, aihub_rag_chunks and aihub_rag_metrics tables.
The Alembic migration bootstrap assumes the existing relational business tables
already exist; it does not create the source user, skill, job, opportunity,
training or organization model.
The user projection expects training-attendance associations in
company_entity_item_formation, with the source user ID stored as text in
name for a same-tenant company_entity whose flag_id is 4.
PG_EMBEDDING_JOBS_TABLE can override the queue table, but defaults to
aihub_embedding_jobs. PG_EMBEDDING_NOTIFY_CHANNEL independently overrides
the notification channel and uses the same aihub_embedding_jobs default.
Deployments that explicitly configured either variable with the former
embedding_jobs value must update it when applying the renaming migration.
One psycopg connection listens to all configured notification channels. Each
queue has its own asyncio worker, so a slow matching or AI call does not block
skill, user, opportunity, job or training work. Notifications wake only the
workers bound to that channel; PG_JOB_POLL_INTERVAL_SECONDS also makes every
worker poll periodically so committed jobs remain discoverable after a lost or
coalesced notification. Every pass claims a bounded batch and immediately
yields before claiming another one.
Long-running jobs receive a unique lease token and refresh heartbeat_at while
external AI or matching work is running. A worker may finalize a row only while
it still owns that token. Expired leases are reclaimed, retries use the
per-domain bounded exponential backoff, and a clean cancellation normally
returns the interrupted batch to pending immediately. Skill embeddings,
entity enrichments and matching allow a newer pending row to coexist with an
older processing row for the same entity. If reclaim, retry or cancellation
finds that newer row, the older claim becomes terminal instead of attempting to
create a duplicate pending key; the newer work remains queued. Parent
reconciliation jobs use keyset pages instead of scanning a complete tenant in a
trigger or one worker transaction. PG_JOB_WORKER_RETRY_DELAY_SECONDS applies
only when a worker pass itself fails; the per-domain retry variables apply to
failed durable jobs. Shutdown waits up to PG_JOB_SHUTDOWN_TIMEOUT_SECONDS for
workers after cancellation. Lease and reconciliation-claim cleanup is bounded
by PG_JOB_CANCELLATION_CLEANUP_TIMEOUT_SECONDS; work that cannot be returned
within that deadline remains safe for normal lease-expiry recovery. A worker
still running after the graceful deadline receives a second cancellation and
gets PG_JOB_FORCE_CANCEL_TIMEOUT_SECONDS to stop.
The pgvector extension may live in a different schema: the embedding listener
reads its actual schema from the PostgreSQL catalog and uses a fully qualified
type cast when saving an embedding. Shared SQLAlchemy connections add the
configured business schema and the installed vector, pg_trgm and unaccent
extension schemas to their PostgreSQL search path.
PG_SKILL_SEARCH_EMBEDDING_DIMENSION defaults to 1536, constrains
skill_translation.embeddings to a fixed pgvector dimension and must match the
embedding model used by both the listener and skill-search requests. The skill
search migration requires pgvector 0.8 or newer and installs the pg_trgm and
unaccent extensions. It creates:
- the automatically maintained
skill_search_v1relational view; - B-tree indexes for translation language and repository joins;
- GIN full-text and trigram indexes on normalized translation titles;
- an HNSW cosine index on translation embeddings.
skill_search_v1 and the skill-title embedding triggers include only clients
whose setting.type is LIVE or DEMO and skills whose referentiel_id
resolves to an existing repository. Repository attachment enqueues missing
vectors, while client deactivation and repository removal clear pending work.
Client activation does not scan existing skills; use the PostgreSQL
initialization endpoints to enqueue that backlog. The migration reuses the
existing tenant, repository, translation and partial queue indexes for these
transitions.
When DATABASE_URL is set, the two single-skill endpoints use this PostgreSQL
search path. An unset value keeps their existing MongoDB path. PostgreSQL
search failures are surfaced to the caller and do not silently fall back to
MongoDB. The PostgreSQL text branch mirrors Atlas sequential autocomplete with
accent folding, prefixes and one-edit fuzzy matching. Its vector branch maps
cosine distance to the Atlas score range before the shared weighted RRF merge.
The PostgreSQL user, opportunity, job and training views expose only rows owned
by a setting.type of LIVE or DEMO. Their inference and enrichment queues
apply the same tenant rule in addition to each entity's enabled, visibility,
validity or active-date conditions. User reconciliation is processed with
keyset pages in aihub_user_reconciliation_jobs. Tenant deactivation
automatically schedules cleanup; activation and Alembic never scan every user.
Existing users are queued through the explicit PostgreSQL initialization API.
PG_USER_RECONCILIATION_PAGE_SIZE bounds each database page. The lease and retry
settings reclaim interrupted parent and user-inference jobs after a worker or
pod restart. Moving an individual entity into an eligible state enqueues its
missing asynchronous work; moving it out removes pending work.
JOM/JPM are intentionally different: tenant deactivation removes pending user
matching jobs, but activation does not create an initial matching backfill.
PostgreSQL JOM/JPM spreadsheet exports resolve identifiers inside the requested
tenant, apply their per-entity limit in SQL and reject results exceeding
PG_MATCHING_EXPORT_MAX_ROWS instead of materializing an unbounded score table.
Talent Finder searches the relational location, skills, attached role, department and textual experience fields with Atlas-like token and fuzzy semantics. Education, certification and total-experience-duration criteria are not supported. Work arrangement remains optional; when supplied, PostgreSQL returns no match if its relational projection cannot enforce that value.
Existing-tenant initialization is exposed under
/admin/api/v1/postgres-initialization. Its optional client_id is
setting.id; omitting it targets every LIVE or DEMO setting. The all
route sequences skills, users, opportunities, jobs and trainings. A run is
complete once reconciliation has inserted all required child job rows; the API
does not wait for the embedding and inference workers to consume them.
PG_JOB_SEARCH_EMBEDDING_DIMENSION defaults to 1536, constrains
job.embeddings and must match the embedding provider used by the PostgreSQL
job worker. The migration creates the live aihub_job_v1 view and adds:
- B-tree indexes on
(setting_id, is_active, status_id, id)and(setting_id, code, id)for tenant lists, counts and exact code lookups; - a reverse
(status_id, is_active, setting_id, id)index for status-visibility transition cleanup; - the existing Job-to-family and
jobskillindexes plus reverse family-item indexes required by family-change propagation; - a partial HNSW cosine index for non-null job embeddings.
No full-text, trigram or JSON GIN index is added because current job searches
are exact-ID, exact-code or vector operations. PostgreSQL triggers enqueue a
deduplicated job_embedding task only for fields used by
title + " - " + jobFamily + ": " + description: title, mission, formation,
experience, results, note and the effective family associations/names. Skill
data, classification, hidden/active/status fields and stored vectors do not
trigger regeneration. PG_JOB_EMBEDDING_NOTIFY_CHANNEL controls the listener
channel; the batch-size and retry variables control job claiming. Job
standardization is MongoDB-only and rejects PostgreSQL mode. With PostgreSQL,
the main Job API, chatbot job mapping and JPM text similarity use the relational
repository without a MongoDB vector-search fallback. The view requires a
LIVE or DEMO tenant; the embedding queue additionally requires an active
job with a visible status.
PG_OPPORTUNITY_SEARCH_EMBEDDING_DIMENSION also defaults to 1536 and must
match the embedding provider used by the PostgreSQL opportunity worker. The
opportunity migration constrains all three custom_job vector columns to that
dimension and rejects existing vectors with incompatible dimensions. It creates
the live aihub_opportunity_v1 view, deterministic translation and public-skill
projection, B-tree join/filter indexes, normalized-title GIN indexes and HNSW
cosine indexes for:
- the source opportunity title;
- the inferred title;
- the reconstructed posting used by matching similarity.
With DATABASE_URL, the main opportunity list/count endpoints and the
opportunity_embedding worker use this relational path. PostgreSQL triggers
enqueue a single deduplicated job only after changes to the effective title or
description, the attached job, its public-skill associations, public skill
titles/locales or repository visibility. Changes to skill levels, location,
validity, deletion state, organization fields and generated AIHub columns do
not enqueue embedding work. PG_OPPORTUNITY_EMBEDDING_NOTIFY_CHANNEL controls
the listener channel; the batch-size and retry variables control job claiming.
Opportunity standardization is MongoDB-only and rejects PostgreSQL mode.
PwC-specific fields remain MongoDB-only. Chatbot opportunity title search uses
the relational inferred-title full-text and HNSW indexes when PostgreSQL is
active: it retrieves 50 text and vector candidates concurrently, normalizes the
vector score to the Atlas range and applies the same 50/50 weighted RRF.
Structured title criteria generate embeddings before entering that hybrid
path. The view requires a LIVE or DEMO tenant; pending embedding work
additionally requires a non-deleted opportunity whose validity has not expired.
PG_TRAINING_SEARCH_EMBEDDING_DIMENSION defaults to 1536 and constrains the
formation.aihub_enriched_title_embeddings vector used by Learning Finder.
The training migration creates the live aihub_training_v1 view, reconstructing
the training document from formation, organisme, training skills and
training access-group entities. It adds:
- tenant/locale/date and join indexes for active accessible training searches;
- normalized-title and enriched-title GIN trigram and full-text indexes;
- a partial HNSW cosine index for non-null enriched-title vectors.
With DATABASE_URL, Learning Finder, Training Matching, rationale generation
and development-objective training grounding use this relational repository
without a MongoDB training-search fallback. PostgreSQL triggers enqueue one
deduplicated training_enrichment job only when formation.name or
formation.description changes, because those are the only fields used to
generate the enriched title. Skill associations, organization, dates, duration,
locale and AIHub-generated columns do not enqueue work. Training
standardization is MongoDB-only and rejects PostgreSQL mode.
PG_TRAINING_ENRICHMENT_NOTIFY_CHANNEL controls the listener channel; the
batch-size and retry variables control job claiming. The view requires a
LIVE or DEMO tenant; enrichment jobs additionally require a non-empty title
and an active date window. Learning Finder's PostgreSQL text branch uses
Atlas-compatible any-term full-text semantics plus per-token fuzzy matching;
the training organization is used as the provider when the relational provider
projection is empty. Its language-filtered hybrid pass still retries once
without language constraints only when the first fused result is empty.
DATABASE_URL also selects LangGraph's official asynchronous PostgreSQL
checkpointer and store. At application startup, their official setup()
methods create or migrate checkpoint_migrations, checkpoints,
checkpoint_blobs, checkpoint_writes, store_migrations and store, plus
their official indexes, inside PG_SCHEMA. If PostgreSQL returns
InsufficientPrivilege, AI Hub permits startup only after confirming that both
stored migration versions match the installed packages and all required tables
already exist. This allows a personal runtime account without CREATE to use a
schema previously initialized by the service account. Alembic deliberately
leaves these vendor-owned tables to LangGraph.
Their names remain unchanged so AI Hub can use the package without overriding
its SQL. The store uses exact namespace/key operations only, so no
store_vectors table or vector index is created.
LangGraph owns a dedicated psycopg pool because its official implementations
require autocommit, disabled prepared statements and dictionary rows.
PG_LANGGRAPH_POOL_MAX_SIZE controls its maximum size and defaults to 10.
Startup fails explicitly for every setup error except the validated
InsufficientPrivilege case above. A personal account still fails when the
schema is missing, outdated or inconsistent. It does not fall back to MongoDB,
and it does not copy existing MongoDB checkpoints or memories.
Alembic also creates the AIHub-prefixed Chatbot persistence tables used when
PostgreSQL resources exist. Chat history and event APIs write
aihub_conversations, aihub_chat_message_log and aihub_events; document
tools write aihub_documents; tenant RAG ingestion and retrieval use
aihub_rag_source_files, aihub_rag_chunks and aihub_rag_metrics.
PG_RAG_EMBEDDING_DIMENSION defaults to 1536, constrains
aihub_rag_chunks.embedding and must match the tenant embedding model used for
RAG sync and retrieval. The migration adds B-tree, partial feedback, trigram,
full-text and HNSW indexes for the current Chatbot history, event, document and
RAG query shapes. Conversation and message searches use all-token autocomplete
with prefix and two-edit fuzzy matching across the same fields as their Atlas
indexes. PostgreSQL RAG text retrieval uses Atlas-compatible any-term matching;
both retain deterministic score and recency ordering.
Alembic creates aihub_config as a singleton and seeds it with a frozen copy of
src/templates/config_file.json. When DATABASE_URL is set,
get_config_file_async and get_ai_configuration_async read these two
PostgreSQL tables and ignore MongoDB database separation. PostgreSQL errors are
surfaced rather than triggering a MongoDB fallback. The Chatbot frontend config
endpoints also use this backend: GET /chatbot/v1/configs reads the public
chatbot section from aihub_config, and PUT /chatbot/v1/configs validates the
requesting administrator through PostgreSQL user data before updating the same
row. Without DATABASE_URL, both endpoints keep using the tenant MongoDB
Config document.
The canonical chatbot.agents section lists the currently supported agents.
Peer-feedback agents are no longer registered. Existing stored configuration
keys do not enable removed agents; configuration normalization uses the current
template. Historical Alembic revisions and their frozen seeds remain unchanged.
Alembic revision 20260827_23 removes the obsolete aihub_api_keys table.
Protected HTTP, WebSocket and MCP operations accept X-API-Key against
AI_HUB_USER_API_KEY or AI_HUB_ADMIN_API_KEY; administrator routes accept
only AI_HUB_ADMIN_API_KEY. Generated documentation, Swagger, ReDoc and
OpenAPI schemas remain public. A non-empty applicable environment key never
falls back to MongoDB after a mismatch. If no applicable key is configured,
authentication reads the MongoDB APIKeys collection only when DATABASE_URL
is absent; PostgreSQL mode returns 403. MongoDB tenant routing prefers its
clientId and falls back to the Client-ID header. Environment-backed keys
also use that header when a client is needed. Stored data-source, environment
and email fields remain ignored; routing uses the fixed smartmobility data
source and the environment resolved at FastAPI startup.
The embedding listener resolves its provider and model from the PostgreSQL
provider table in this mode without requiring a MongoDB connection.
PG_EMBEDDING_AI_PROVIDER and PG_EMBEDDING_MODEL override that selection when
set. If the shared configuration cannot be loaded or does not identify an
embedding provider, the listener logs a warning and uses OpenAI as its final
fallback.
Every PostgreSQL schema change must be delivered through an Alembic migration in
alembic/versions/. Production and staging migrations are executed by the
infrastructure workflow outside GitLab CI; the repository CI only validates the
migration chain against an ephemeral pgvector-enabled PostgreSQL service.
Administrators can also trigger alembic upgrade head from the running service
through POST /admin/api/v1/diagnostics/postgres/migrations/run. The endpoint
uses the current process DATABASE_URL and PG_SCHEMA and returns HTTP 202.
Template notification recipients are resolved before admission. A process-local
lock rejects overlapping listener/migration operations, while a PostgreSQL advisory
lock scoped by database and schema prevents overlap across workers, pods and
CLI processes. Execution conflicts and failures are reported in the final email;
successful reports include the target and current revision. The listener restarts
only after successful migration. Cancellation waits for the migration thread to
finish before releasing its operation lock.
Alembic records internal migration details at DEBUG and executed SQL at
INFO; SQL parameter values are hidden so configuration and persisted data are
not exposed in application logs. The equivalent verbose CLI invocation is
alembic -x debug=true upgrade head.
GET /admin/api/v1/diagnostics/postgres/jobs/summary returns an exact snapshot
of the seven physical queues consumed by the PostgreSQL job runtime. It includes
database-wide and per-table status counts, plus discriminator breakdowns such as
entity type, matching type, reconciliation reason and initialization scope.
finished is the sum of processed, failed, skipped and canceled rows;
outstanding covers every remaining status, including pending, processing
and waiting. The endpoint is administrator-protected, PostgreSQL-only and does
not change or retry any job.
Alembic also installs database-side retention for these seven queues. The
cleanup_aihub_job_history function deletes at most 10,000 rows from each queue
per invocation when their terminal timestamp is older than three calendar
months. It uses processed_at for embedding, enrichment and matching jobs, and
completed_at for reconciliation and initialization jobs. Only processed,
failed, skipped or canceled statuses supported by each queue are eligible;
pending, processing and waiting rows are never removed. Deleting an expired
initialization job also removes its steps through their existing cascade.
Partial timestamp indexes keep each bounded deletion from scanning active work.
Alembic builds these small partial indexes one queue at a time and commits after
each one. A regular index build allows long-lived read snapshots to continue;
it briefly pauses writes to the queue instead of waiting indefinitely for those
snapshots as CREATE INDEX CONCURRENTLY would. Index lock acquisition is capped
at 10 seconds and construction at five minutes, so contention produces an
explicit migration error. A subsequent run repairs an invalid index left by an
interrupted concurrent attempt with REINDEX INDEX.
PostgreSQL does not provide a native TTL scheduler. When the pg_cron extension
is already installed, Alembic registers aihub_job_history_cleanup:<schema> at
03:17 every day in the timezone configured for pg_cron. If the extension is
available but not installed, or unavailable, Alembic logs a warning without
blocking the structural migration. Infrastructure must configure
shared_preload_libraries and cron.database_name as required by its PostgreSQL
installation, then run:
Replace mysql with PG_SCHEMA when it differs. The scheduling helper is
idempotent for the migration role's named job. A bounded cleanup can also be
invoked manually with SELECT mysql.cleanup_aihub_job_history();; its optional
arguments are the retention interval and per-queue batch size.
RabbitMQ¶
RabbitMQ producers and consumers share the same connection configuration.
RABBITMQ_URL is the only connection setting. Supply the complete AMQP URL,
including credentials, host, port and virtual host, for example:
Single-host URLs, including a load balancer or cluster endpoint, retain the native
aio-pika connection behavior. To configure several nodes of the same cluster,
separate their addresses with commas:
RABBITMQ_URL="amqp://service:example-password@rabbitmq-1.example.com:5672,rabbitmq-2.example.com:5672,rabbitmq-3.example.com:5672/service"
AIHub expands the host list before calling the AMQP client. Credentials, virtual
host, TLS scheme and URL options apply to every endpoint. Bracket IPv6 addresses,
for example [2001:db8::1]:5672. Commas in credentials, the virtual host or query
parameters are not treated as host separators.
For multi-host URLs, initial connection attempts follow the configured order,
with a 10-second connection timeout per endpoint. A failed endpoint is closed
before trying the next. Initial attempts fail fast per endpoint even if the URL
contains fail_fast=0. If every endpoint fails, producers report the connection
failure and consumers retry the complete list after RABBITMQ_RECONNECT_INTERVAL.
After a connection interruption, the robust connection selects the next endpoint,
wrapping to the first after the last, and retains native channel and consumer
restoration. Reconnection attempts use the same timeout and configured interval.
This does not add message replay after a failed publication. Single-host URLs
keep their existing timeout and retry behavior.
Percent-encode reserved characters in the URL credentials. When RABBITMQ_URL
is absent (read as None), empty or whitespace-only, RabbitMQ is unavailable:
the shared connector logs a warning and opens no connection. Consumers exit
without registering queues or retrying. Score producers skip publication;
queued client imports fail explicitly because RabbitMQ is required.
Score publication reports the existing RabbitMQ failure result instead of
logging a successful send. No hostname or credential environment variables are
used as a fallback.
A nonblank URL still follows the configured single-host or multi-host connection behavior described above; configuration and connection failures are not treated as disabled RabbitMQ.
GET /admin/api/v1/diagnostics/environment exposes the boolean
RABBITMQ_URL is present without returning the URL or its credentials. The flag
is false for absent, empty or whitespace-only values. It reports configuration
presence, not broker reachability.
Operational tuning includes
RABBITMQ_PREFETCH_COUNT, RABBITMQ_HEARTBEAT,
RABBITMQ_RECONNECT_INTERVAL and RABBITMQ_QUEUES_PREFIXES.
All RabbitMQ consumers acknowledge (ack) each completed processing attempt,
including failures and malformed JSON. Processing exceptions reaching the RabbitMQ
boundary are logged once with their traceback, then acknowledged without requesting
requeue or dead-lettering. A failed message must be republished manually to retry it.
User, opportunity and job messages whose tenant lookup reports an unknown client
database emit one concise ERROR with entity type, client ID, data source and
environment, without an exception traceback. They return normally and are
acknowledged as well. This includes a missing client registration or an unavailable
registered database.
Cancellation during processing still propagates and leaves the message unacknowledged. An acknowledgement failure emits a warning. If the channel or connection closes before an acknowledgement reaches RabbitMQ, the broker can redeliver the message; this policy does not guarantee exactly-once processing.
Automatic startup and administrative controls allow RabbitMQ consumers in any
zone except eastus, including proxmox, gra11, and an unset or empty ZONE.
The local deployment environment remains excluded. Automatic startup logs when
the local environment or the eastus zone prevents consumers from starting.
When DATABASE_URL is set, RabbitMQ consumers are disabled at application
startup and the generic administration routes under
/admin/api/v1/consumers/{start,stop,restart} reject PostgreSQL mode. Those
generic routes remain MongoDB/RabbitMQ-only. The separately named
/admin/api/v1/consumers/postgres-listener/{start,stop,restart} routes start,
stop or restart the shared PostgreSQL notification listener and all of its
independent job workers as one runtime. They never start RabbitMQ consumers.
RABBITMQ_QUEUES_PREFIXES retains its existing base-prefix contract. For example,
sm_integration expands tenant prefixes sm_integration_nextgen and
sm_integration_legacy. Queue names add the existing transport namespace:
sm_sm_integration_legacy.user_profile. Queued initialization shares this exact
construction with consumers and does not rename existing queues. The tenant's
rabbitmqPrefix must match one of the expanded prefixes. The existing listener
activation requires sm_ in RABBITMQ_QUEUES_PREFIXES.
Configured Smart Mobility prefixes create consumers for user profiles, job
offers, trainings, feedback, skills and job resource actions in every
environment. All consumers in one application instance share a single robust
connection and use one channel per queue. aio-pika restores the channels,
queues and subscriptions after a transport interruption. Training queues are
declared passively: if a queue does not exist, only that consumer logs a warning
and is not registered. An initial connection failure is retried according to
RABBITMQ_RECONNECT_INTERVAL.
Logging¶
config/logging.yaml is the central logging configuration. Application logs
written with Loguru and standard-library logs from Gunicorn, Uvicorn and
third-party clients are sent through the same Loguru sink.
The default auto profile selects:
consolewith colors when the process writes to an interactive terminal;plainwithout ANSI colors when output is captured by an IDE, process manager, container runtime or log collector.
Both profiles use the same concise single-line format. The json profile is
available as an explicit opt-in and emits one compact object containing only
the useful log fields; it does not expose Loguru's verbose internal
text/record envelope. All profiles write to the process stream. File
rotation and Loki transport remain the responsibility of the container or
hosting platform, which avoids competing files and transports across Gunicorn
workers.
The following environment variables can override operational settings without modifying the YAML file:
| Variable | Default | Behavior |
|---|---|---|
LOG_CONFIG_FILE |
config/logging.yaml |
Alternative configuration file |
LOG_PROFILE |
auto |
auto, console, plain or json |
LOG_LEVEL |
INFO |
Minimum Loguru level |
LOG_SOURCE_WIDTH |
52 |
Maximum source-column width |
LOG_COLORIZE |
profile value | Enable ANSI colors |
LOG_SERIALIZE |
profile value | Emit compact single-line JSON records |
LOG_BACKTRACE |
false |
Accepted compatibility setting; safe exception summaries take precedence |
LOG_DIAGNOSE |
false |
Accepted compatibility setting; variable values remain excluded |
LOG_ENQUEUE |
false |
Process logs through Loguru's internal queue |
Request logging emits one completion event with the HTTP method, route
template, status and duration. X-Request-ID is reused when safe, generated
otherwise, returned in the response and propagated to logs emitted while the
request is processed. Client-ID is propagated when present. Query strings,
request bodies, API keys, bearer tokens and arbitrary headers are never added
to the request event. Health and metrics paths can be excluded in the YAML
configuration.
MongoDB tenant resolution emits a WARNING with
event=client_database_resolution_failed when routing cannot select a client
database. Search plain logs for Client database resolution failed. The message
includes the effective client_id, data_source, environment and a reason:
configuration_not_found, invalid_client_database (missing or non-string
clientDB), database_not_found, or missing_client_id (a datasource or
environment was supplied without a client ID). These values reflect the lookup
after authentication/default resolution, not the full HTTP inputs. The structured
lookup_client_id field preserves the effective value and its string/integer
type separately from the middleware's header-derived client_id correlation.
Both synchronous and asynchronous lookups retain the active request context. Routing text is limited to 128 input characters and sanitized; oversized values and nonscalar objects are omitted. Successful lookups and calls without any routing parameters, used to access only common/reference databases, are silent. The diagnostic does not change HTTP responses, database queries or consumer continuation behavior. Consumer-specific skip messages remain available.
OpenAI failures emit a centralized ERROR event with event=openai_error,
including safe provider metadata and the active request/client correlation.
Search plain logs for OpenAI request failed. See
OpenAI error diagnostics
for supported operations, timeout details and retry visibility.
All configured profiles apply the same confidentiality filter from
src/core/log_redaction.py before output or queueing. Nested credential fields
(API keys, passwords, tokens, authorization/cookie headers, connection strings,
private keys and signatures) and SQL parameter fields are replaced with
[REDACTED], regardless of common casing or separator variants. The caller's
objects are not modified. Ordinary event, request/client correlation, status
and duration fields remain available.
Messages also redact recognizable credential assignments, Basic/Bearer tokens,
JWTs, private-key blocks and SQLAlchemy SQL/parameter detail blocks. URLs retain
their destination/path but lose user credentials, query values and fragments.
Control characters are escaped to prevent forged lines or terminal sequences.
An individual string over 8,192 characters is omitted whole. Structured context
has a 16,384-character budget, 200 visited values, depth 6 and 50 entries per
container; excessive content and cycles become [OMITTED]. Custom objects and
binary values are omitted without calling their representation methods.
Exceptions retain bounded type and frame-location summaries, including causes
and exception-group members (up to eight exceptions and 30 frames in total).
For each traceback, the remaining frame budget retains its innermost frames,
including the failure location. When outer frames are dropped, [OMITTED]
appears before the retained frames. Later causes or group members keep their
types but omit frames if the shared budget is exhausted. Raw exception
messages, SQL parameters, source lines and local variables are never rendered
from exception objects, even with LOG_BACKTRACE or LOG_DIAGNOSE enabled.
Application exception propagation and HTTP responses are unchanged.
This filter cannot recognize every unlabeled secret, personal datum or token
embedded in an arbitrary URL path or message. Call sites must still minimize
payloads and use safe operation/reason codes. Direct print, independent sinks
and external tracing systems are outside this configured logging boundary.
Backend response cookie¶
Every HTTP response emitted by AIHub carries a session cookie named SRV,
whose value is read once from socket.gethostname() when the Main API middleware
stack is initialized. No environment variable is required. Local runs report
the computer hostname; container deployments report the container hostname,
normally the pod hostname in Kubernetes. Workers sharing one host report the
same value.
For an HTTPS request, a response includes a header such as:
The cookie is host-only, has no expiry or maximum age, and applies to all paths.
Plain HTTP responses omit Secure, including local development. Behind a TLS
terminating proxy, the scheme must be resolved by the server's trusted proxy
configuration (FORWARDED_ALLOW_IPS); AIHub does not trust raw forwarded headers
itself. Existing response cookies are preserved, and an incoming SRV value is
never used to identify the serving backend.
The same behavior covers the Main API and all mounted applications, including MCP, documentation, redirects, CORS preflight, validation errors and unhandled HTTP 500 responses. Streaming and SSE responses send the cookie with their initial headers without buffering the body. WebSocket and application lifecycle messages pass through unchanged.
To diagnose a failing request, inspect its Set-Cookie response header in the
browser Network panel or with an HTTP client. HttpOnly prevents JavaScript
from reading the cookie, and the existing CORS policy is unchanged; cross-origin
clients must not assume the browser persists it. Inspect the individual response
because a later response may report another backend. Errors generated entirely
by an ingress or proxy cannot receive this application cookie.
Tenant AI configuration¶
LLM routing is based on semantic profiles rather than model names in application
code. The common ai_configuration document for each provider owns both the model
catalogue and the default mapping. For example, an OpenAI document can contain:
See LLM providers, models and tasks for the functional profile-to-task mapping, provider capabilities and last-resort model table.
{
"provider": "openai",
"is_default_llm_provider": true,
"is_default_embedding_provider": true,
"llm_models": {
"luna": {"llm_name": "gpt-6-luna"},
"sol_6_1": {"llm_name": "gpt-6.1-sol"},
"terra_6_1": {"llm_name": "gpt-6.1-terra"}
},
"llm_profiles": {
"default": {"model": "luna", "reasoning_effort": "low"},
"tiny": {"model": "luna", "reasoning_effort": "none"},
"mini": {"model": "luna", "reasoning_effort": "low"},
"large": {"model": "terra_6_1", "reasoning_effort": "medium"},
"reasoning": {"model": "sol_6_1", "reasoning_effort": "medium"}
},
"llm_realtime_models": {
"gpt_live_1": {"llm_name": "gpt-live-1"}
},
"default_llm_realtime_model": "gpt_live_1",
"embedding_models": {
"embedding_small": {
"embedding_name": "text-embedding-3-small",
"mongo_vector_index": "embedding_small_vector_index",
"mongo_vector_path": "embedding_small_vector",
"embedding_dimension": 1536,
"threshold_delta": 0
}
},
"default_embedding_model": "embedding_small",
"embedding_profiles": {
"lexi": "embedding_small"
}
}
Azure uses the same profile names but maps them to its own deployment names.
The generation connector uses AZURE_OPENAI_URL with /openai/v1/ and does not
read per-model API versions. The current standard catalogue and profiles are:
{
"provider": "azure",
"is_default_llm_provider": false,
"is_default_embedding_provider": false,
"llm_models": {
"gpt_5_4_mini": {"llm_name": "gpt-5.4-mini"},
"gpt_5_2": {"llm_name": "gpt-5.2"},
"gpt_4_1_nano": {"llm_name": "gpt-4.1-nano"}
},
"llm_profiles": {
"default": {"model": "gpt_5_4_mini", "reasoning_effort": "low"},
"tiny": "gpt_4_1_nano",
"mini": {"model": "gpt_5_4_mini", "reasoning_effort": "low"},
"large": {"model": "gpt_5_4_mini", "reasoning_effort": "medium"},
"reasoning": {"model": "gpt_5_2", "reasoning_effort": "medium"}
}
}
Keep other existing model entries and the audio, Realtime and embedding catalogues.
Existing provider records need the explicit
Azure configuration migration;
changing the initialization seed alone does not update them. Only
is_default_llm_provider and
is_default_embedding_provider are used to choose defaults; the old
is_default_provider field can be removed. This keeps task routing
provider-independent.
See the provider guide for complete Azure OpenAI deployment and OVH model-ID examples, connection variables and current connector capabilities.
OVH uses the shared https://oai.endpoints.kepler.ai.cloud.ovh.net/v1 API for
both generation and embeddings. Its catalogue stores model IDs in llm_name
and embedding_name. Existing URL-based provider records need the explicit
OVH configuration migration
alongside deployment; changing the initialization seed alone does not update them.
The BGE embedding models, dimensions and vector storage fields remain unchanged.
The tenant document normally selects only providers; profile overrides are sparse:
{
"ai_configuration": {
"llm_provider": "openai",
"llm_profiles": {
"large": "sol"
},
"embedding_provider": "openai",
"embedding_model": "embedding_small"
}
}
Each profile value can be a model-catalogue alias, an object such as
{"model": "sol", "reasoning_effort": "high"}, or a complete model
configuration whose model is declared in that provider's catalogue. Unknown
references fall back like missing references. Unspecified tenant profiles inherit the selected provider
defaults. The default, tiny, mini, large and reasoning profiles are
available to all connector calls and LangGraph models. Translation fallback
uses tiny, while conversation summaries use mini directly.
llm_profiles.default should be present in every provider document. The other
profiles are optional. A missing tiny mapping uses the provider-specific
built-in minimal model; mini and large inherit default, and reasoning
inherits large. Unresolved profiles use provider-specific code defaults without
replacing valid sibling profiles. Provider selection is client/request selection
when valid, then a valid database default, then the code default (OpenAI for
LLMs, Azure for embeddings). This applies to both MongoDB and PostgreSQL, including
empty provider catalogues. A provider switch discards overrides belonging to the
previous provider. Embedding and audio/Realtime models follow the same
client model, database default, code default ordering within their model family.
Database failures still propagate; this policy handles missing configuration.
Reasoning in AIProviders¶
Store reasoning beside the model inside llm_profiles. MongoDB uses
common_data.AIProviders; PostgreSQL uses the existing JSONB llm_profiles
column in aihub_ai_providers. No separate reasoning column is needed.
For example, this fragment intentionally overrides the code's mini=low default:
Merge this fragment into the provider record whose llm_models declares luna;
it is not a replacement for the complete provider document. A tenant may set
"mini": {"reasoning_effort": "high"} in its own ai_configuration.llm_profiles
to retain the provider's model while overriding its effort.
For each field, resolution follows explicit tenant configuration, then that
profile in AIProviders, then the centralized profile default. GenAI additionally
accepts an explicit external request effort with highest priority. Missing or
JSON null effort inherits; the string "none" explicitly requests disabled
reasoning where supported. Defaults are tiny=none, mini/default=low and
large/reasoning=medium. Inheriting a model from another profile does not copy
its reasoning level. Existing configured levels remain authoritative.
The chosen model must accept the requested control. Azure GPT-4.1 nano and OVH's
current Llama large model omit it. Native OpenAI Decisions has no effort
parameter, even though its model is selected through a profile. Generation,
streaming, batches and document extraction otherwise consume the profile setting.
See provider limitations and
GenAI request controls.
Updating code or the initialization seed does not rewrite existing provider or tenant records. Review and apply the desired profile edits to the database used by the deployment separately. Keep unrelated models, embeddings and audio settings.
Continuous voice uses llm_realtime_model, now selecting gpt-live-1 through
the OpenAI Live API. Its delegated Responses backend uses the OpenAI mini
semantic profile, including configured reasoning effort. The separate Realtime
transcription catalogue/selector has been removed. Existing stored selections
require the Live migration procedure.
File transcription still uses llm_speech_to_text_model (gpt-transcribe), and
simulation MP3 generation uses llm_text_to_speech_model (gpt-4o-mini-tts).
See the speech model table.
Model configurations may include an optional pricing block containing USD
prices per million input, output, cache-read and cache-write tokens. Provider- or
gateway-reported costs take priority; configured prices and the built-in table are
fallbacks when the API response contains no complete cost information.
See the embedding model table for provider defaults, dimensions and the independent Lexi selection. Changing an embedding model or dimension requires rebuilding the corresponding stored vectors and aligning the MongoDB/PostgreSQL vector indexes. It must not be treated as a transparent runtime-only switch.
embedding_profiles selects a specific embedding model for one domain,
independently from default_embedding_model/embedding_model. Only the
lexi profile exists today: Lexi's duplicate-detection and provided-skills
matching compute embeddings on the fly and never persist them, so that
profile can move to a newer embedding model without the reindexing constraint above.
The initialization seed selects text-embedding-3-small for OpenAI/Azure and
bge-multilingual-gemma2 for OVH. PostgreSQL loads these profiles when the profile
column is created, preserving existing mappings on replay. Seed changes do not
rewrite provider rows in databases whose migrations have already run. If a
provider document has no embedding_profiles.lexi entry, Lexi falls back to
text-embedding-3-small in code. Other embedding consumers (RAG, matching,
skills library, training) are unaffected by this profile and keep using
default_embedding_model/embedding_model.
Chatbot RAG configuration¶
RAG reads chatbot.rag from the active tenant configuration backend: the tenant
MongoDB Config document when DATABASE_URL is absent, or the singleton
PostgreSQL aihub_config row when PostgreSQL resources exist. A representative
configuration is:
{
"chatbot": {
"rag": {
"enabled": true,
"chunk_size": 1200,
"chunk_overlap": 200,
"sync_interval_seconds": 300,
"num_candidates": 100,
"min_vector_score": 0.0,
"rerank_pool_size": 40,
"hybrid_vector_weight": 0.65,
"hybrid_text_weight": 0.35,
"query_cache_ttl_seconds": 90,
"background_sync": {
"enabled": false,
"interval_seconds": 300
},
"mongo": {
"chunks_collection": "RagChunks",
"files_collection": "RagSourceFiles",
"metrics_collection": "RagMetrics",
"vector_index_name": "rag_chunks_vector_index",
"text_index_name": "rag_chunks_text_index",
"vector_path": "embeddings",
"embedding_dimension": 1536
},
"azure_datalake": {
"container": "ai-hub",
"source_paths": ["knowledge/faq"],
"allowed_extensions": [".txt", ".md", ".json", ".csv"],
"max_files_per_path": 120,
"max_file_size_bytes": 1500000,
"cache_ttl_seconds": 300
}
}
}
}
The RAG query cache retains at most 256 entries per application process, shared
across tenants and backends. Each insertion removes expired entries, then evicts
the oldest stored entries if necessary. query_cache_ttl_seconds still controls
validity; synchronization also invalidates the affected tenant's entries.
The mongo block applies only to the MongoDB backend. New-client provisioning
creates the RAG classic, text-search and vector-search indexes from these
settings. Existing tenants and newly configured collection or index names require
POST /admin/api/v1/clients/indexes reconciliation before use. Inspect its
read-only plan first; tenant reconciliation creates missing indexes, updates
compatible definitions, replaces incompatible indexes, and drops indexes absent
from the catalog. RAG sync and reindex only process documents and never create
indexes. Index-management rights are needed by the
administrative provisioning/reconciliation workflows.
When PostgreSQL resources exist, RAG uses aihub_rag_source_files,
aihub_rag_chunks and aihub_rag_metrics instead of RagSourceFiles,
RagChunks and RagMetrics. The admin reindex endpoint calls
sync_rag_documents_to_postgres, and retrieval calls the PostgreSQL hybrid
vector/text query path. The background RAG sync worker remains MongoDB-only and
is not started in PostgreSQL mode; PostgreSQL deployments should use the
tenant-scoped admin reindex path for explicit synchronization.
Matching and standardization settings¶
The same tenant document controls:
use_matching.jomanduse_matching.jpm;- entity standardization, translation and skill inference;
- JOM/JPM hard filters and score rules;
- pool size and matching weights;
- chatbot activation and available agents;
- report recipients and streaming flags.
The nightly_batch template field is retained for compatibility but AI Hub no
longer schedules a nightly matching batch. Use the batch or processing APIs.
skill_scores.missing_target_skill_level_as_acquired controls, by matching
type and processing side, whether a target skill without a level (null or
999) receives a linear level score of 1. The setting defaults to false so
existing tenants keep the former behavior. To enable it only for recruiters
processing users for an opportunity, use:
A required job or opportunity skill level of 0 is always treated like a
missing level (null or 999) and therefore receives a linear level score of
1. A user level of 0 remains a real level and does not satisfy a positive
requirement.
{
"skill_scores": {
"missing_target_skill_level_as_acquired": {
"jom": {
"user": false,
"opportunity": true
}
}
}
}
Administrative background execution¶
Long administrative operations use report_mail_addresses from the tenant
configuration; client creation, index reconciliation with apply=true and tenant-independent skill
operations use the configuration template. Configure Brevo or the SMTP fallback
as described in outgoing email for completion delivery.
Empty recipient lists prevent task acceptance. Without a Brevo API key and
complete SMTP configuration, delivery is skipped with a warning without
preventing task execution.
Client creation awaits resource reconciliation and sends its confirmation before
HTTP 200. Initial imports remain in the background, with one existing batch email
per completed entity publication (jobs, opportunities, trainings, users).
All five notifications use the template recipients captured at task admission.
Successful runs have no additional final summary. Empty imports are reported as
no_data; partial failures retain the global partial_failure report. Exceptions
and cancellation retain their terminal report. These notifications do not change
the task status to success until publication finishes. They do not track consumer
completion. Resource-only requests have no background import task.
| Variable | Default | Purpose |
|---|---|---|
ADMIN_TASK_WORKERS |
2 |
Concurrent administrative operations per process |
ADMIN_TASK_MAX_PENDING |
8 |
Waiting operations per process |
ADMIN_TASK_SHUTDOWN_SECONDS |
30 |
Grace period before cancelling unfinished operations |
These tasks are not durable across process restarts. See
background operations for
acceptance, reports and shutdown semantics. The admin-only
GET /admin/api/v1/operations/{task_id} endpoint checks in-memory states across
sibling workers in the same container. Each worker retains active tasks and its
latest 1,000 completed outcomes; there is no database or disk history. The runtime
needs a writable OS temporary directory for private Unix socket endpoints
(aih-ops-<uid>-<parent-pid>). Workers must share that directory and OS user.
Cross-container lookup is not supported. A worker restart loses its outcomes.
MongoDB indexes¶
POST /admin/api/v1/clients waits for collection/index commands and client
registration, then returns HTTP 200. Only the optional initial import is scheduled
in the background; the returned task_id tracks its publication. With
initialisation_import=false, task_id is null. Atlas Search/Vector builds may
still be running after their commands succeed. Initial
imports are enabled by default (initialisation_import=true). Resource preparation
uses the same reconciliation engine as /clients/indexes, creating missing
resources, updating compatible definitions, replacing incompatible indexes, and
dropping obsolete or duplicate indexes in catalog-managed collections on every
call. It registers the tenant in ClientConfig only after all collection/index commands succeed.
Existing configuration and registration values are preserved. Index operation
failures are reported and prevent registration until reconciliation succeeds.
Retrying requires no stored progress. Initial imports run again when enabled; use initialisation_import=false for resource-only repair.
This includes classic indexes plus the Search and Vector Search indexes
for matching, Talent Finder, chat history, events and RAG. When
rabbitmq_prefix is omitted, provisioning stores
sm_<APP_IDENTIFIER_SHORT>_legacy. This value deliberately uses
APP_IDENTIFIER_SHORT directly, including mutu, and does not fall back to
ENV. An explicit prefix is preserved. Existing tenants require explicit
reconciliation when an index definition changes. /clients/indexes includes PwC
registrations identified by ClientConfig.dataSource=pwc, reuses their provisioning
catalog and preserves indexes outside that catalog.
POST /admin/api/v1/clients/indexes accepts an optional db_name query
parameter and defaults apply to false: the request waits for the read-only
plan and returns the full JSON report with HTTP 200, without a task or email.
With apply=true, it returns HTTP 202 with a task ID and emails the completed
background report. Without db_name, it processes every
client database named by clientDB in common_data.ClientConfig, excluding
PwC tenants, plus reference_data and its managed skill collections. Each
database is processed once; the results are aggregated without stopping after
an individual database failure. With apply=true, it reconciles managed
collections and indexes for the selected scope. Explicit db_name still
targets only that database; common_data requires explicit selection.
Search and Vector Search definitions are updated in place when the type is
unchanged; TTL-only changes use collMod where supported. Other conflicts are
replaced, and obsolete or duplicate indexes are removed. The _id_ index is kept.
Type replacement waits up to 60 seconds for Atlas to release the old name before
creating the new index; a shorter provisioning deadline still applies. See
MongoDB indexes.
MONGO_PROVISIONING_TIMEOUT_SECONDS sets a positive, finite driver timeout in
seconds (default 120) for each synchronous provisioning step, including
configuration/registration access and managed index inspection or mutation.
It does not limit the whole background task or the initial imports. Shorter
connection timeouts still apply. Timeout errors are reported and completed
resources remain available for the next call; the deadline does not guarantee
cancellation of a server-side index build. Atlas Search command acceptance is
asynchronous and does not wait for queryability.
Configuration ownership¶
requirements.txt: exact dependency versions;src/templates/config_file.json: default tenant behavior;alembic/seeds/20260817_04_default_config.json: frozen PostgreSQL default configuration seed;alembic/seeds/20260817_04_ai_providers.json: frozen PostgreSQL provider/model seed;- MongoDB
AIProvidersor PostgreSQLaihub_ai_providers: runtime provider/model definitions, selected throughDATABASE_URL; - MongoDB
APIKeys: fallback authentication records used only whenDATABASE_URLis absent and the relevant environment API key is absent or empty; - environment variables: primary API keys, credentials and infrastructure endpoints;
config/logging.yaml: log profiles, dependency levels and request context;zensical.toml: documentation structure and rendering.
Legacy SFTP exports¶
Statistics delivery uses AsyncSSH at the version pinned in requirements.txt
(see current dependency constraints).
The existing PWC_SFTP_HOSTNAME,
PWC_SFTP_PORT, PWC_SFTP_USERNAME, PWC_SFTP_PASSWORD,
PWC_SFTP_PRIVATE_KEY_PATH, PWC_SFTP_PASSPHRASE and corresponding BPCE
host/port/username/password variables retain their roles. A PwC private key is
loaded with its optional passphrase; BPCE uses the existing password path.
The existing SFTP connector explicitly adds ssh-dss to host-key and signature
algorithms. Its legacy permissive host-key policy is unchanged and remains
confined to that connector. MariaDB SSH configuration is unaffected. Connection
establishment has a 30-second deadline; connection, retention and transfer share
a 300-second deadline. Retention still precedes the upload, remote file size is
confirmed, and delivery failures remain best effort. Cancellation closes the
owned resources and propagates.
DSA is verified with local test servers. Actual customer server negotiation must still be checked in staging; enabling DSA alone does not enable every obsolete cipher or key-exchange algorithm.
Excel readers and exports¶
Excel imports use Pandas with the Calamine engine (python-calamine); generated
XLSX exports use XlsxWriter. CSV inputs retain their existing reader. Existing
upload size and decompression limits remain in place.
- Readers consume stored cell values, including the workbook's cached formula results. They do not calculate formulas; files with missing or stale caches must be recalculated and saved in a spreadsheet application before import.
- Identifier readers explicitly use
dtype=str, preserving zeros in text cells. A numeric cell displayed as000123still has the value123; its display mask does not become part of the identifier. Other imports retain Pandas' normal type and missing-value inference, including its default NA tokens. - Date cells use the workbook's 1900 or 1904 epoch. Excel dates have no timezone; callers must not infer UTC from a spreadsheet date alone.
- Chat attachments extract all nonempty sheets. Other import paths still read the first sheet. Leading and interior blank rows are retained; trailing blank areas do not represent records.
- Exports create a new workbook containing tabular data. Untrusted strings remain literal, including formula-like strings and URLs. Automatic hyperlinks are disabled. These paths do not edit a template or preserve macros, styles, charts, merged cells or validation rules from an uploaded workbook.
Calamine and XlsxWriter are not drop-in APIs for arbitrary OpenPyXL use: future work that edits existing workbooks or depends on advanced Excel features needs its own compatibility review. The validated scope is the application's current value-reading and fresh tabular-export workflows.