Runbook: Manually Re-Ingesting CMS Content into the AI Assistant, and Verifying It¶
Who this is for: José and anyone operating the platform. What it does: explains how to manually push content from the CMS into the AI assistant's knowledge, and how to confirm it worked — for the occasions when the automatic path is not enough. How long it takes: a few minutes per content type.
1. What this is about, in plain words¶
The public website has an AI assistant — a chat box where a visitor types a question and gets a written answer. (AI stands for Artificial Intelligence; here it simply means a computer composes the answer instead of a person.) The assistant does not make answers up: it answers using the content that has been published in the CMS (Content Management System — the control panel, built on a product called Strapi, where the content author writes the website's pages).
For the assistant to be able to use a page, the page's text has to be copied into a special store that the assistant can search quickly. That copying step is called ingesting (or indexing). The whole approach — answer a question by first retrieving the most relevant stored passages and then writing an answer from them — is known by the acronym RAG (Retrieval-Augmented Generation). You do not need the theory; the practical point is: content only reaches the assistant after it has been ingested.
Normally, ingesting happens automatically the moment the content author presses Publish in the CMS. The CMS is wired to notify the assistant's service, which then reads and stores the new content within about a minute. This runbook is for the cases where you need to do that step by hand.
A few more terms you will meet below, explained once:
- ai-service: the part of the platform (written in Python) that runs the assistant and does the ingesting. In production it runs in a container named
po-ai-service-prod. (A container is an isolated, running copy of a service on the server.) - Namespace: a labelled drawer in the assistant's knowledge store. Different kinds of content go into different drawers so the assistant can search the right one. The website's marketing pages go into the namespace
web_presence_public; partner profiles go intopartner_profiles_public. - Embedding / vector store: the technical form in which the assistant keeps your text so it can find passages by meaning rather than by exact words. The store is a PostgreSQL database table called
rag_document_vectors. You rarely need to touch it directly. - CLI (Command-Line Interface): a way to run a tool by typing a command, as opposed to clicking buttons. The ai-service ships a small CLI for ingesting content; it is the recommended manual method.
- API (Application Programming Interface): a set of web addresses one program calls to ask another program to do something. The ai-service also exposes ingest and status operations as an API, protected by an admin key.
- Admin key: a password-like secret (stored as the environment variable
RAG_INGEST_ADMIN_KEY) that protects the ingest and status API so that only operators can call it.
2. When to use this runbook¶
Reach for a manual ingest in any of these situations:
- First-time bulk load. A batch of pages has just been published (for example, the content author has filled in the whole site for the first time) and you want to be certain every page was read, rather than trusting each automatic notification.
- Content is on the website but the assistant does not know it. The author confirms a page is published and visible on the public site, but the assistant keeps saying it has no information on that topic even after a few minutes.
- After a restore, migration, or fresh deployment where the knowledge store might be empty or out of date (for example, the production assistant's store is empty because the site has only just gone live).
- The daily ingest budget blocked an automatic run. The assistant limits how much content it will process per day to control cost; if that limit was reached, some automatic ingests may have been rejected and need to be re-run (see Troubleshooting, Section 7).
If none of these apply, you usually do not need this runbook — publishing in the CMS is enough.
3. Before you start (prerequisites)¶
- Access to the production server over SSH (Secure Shell — an encrypted remote terminal connection):
ssh root@31.97.159.7 - The production assistant container is running and healthy. Check with:
docker ps --filter name=po-ai-service-prodYou should see it listed asUp … (healthy). - The CMS access token is in place. The CLI reads the website's content through the CMS, using a token stored in the environment variable
STRAPI_API_TOKENinside the container. This was set during launch; if you are unsure, the web-presence prod launch work confirmed it. If ingests fail with an authentication error, re-check it (Section 7). - You do not need to handle any secret values by hand for the normal path: the commands below read the tokens from inside the container, where they already live.
4. The content map: which page goes into which namespace¶
Each CMS content type feeds a specific namespace. The kind column tells the CLI whether the content type is a one-of-a-kind page (single) or a list of items (collection).
| CMS content type | Namespace (drawer) | Kind |
|---|---|---|
landing-page |
web_presence_public |
single |
about-page |
web_presence_public |
single |
how-it-works |
web_presence_public |
single |
become-partner-page |
web_presence_public |
single |
faq-page |
web_presence_public |
single |
contact-page |
web_presence_public |
single |
partner |
partner_profiles_public |
collection |
lugar (places gallery) |
lugar_gallery |
collection |
This mapping is defined in the CMS code (each content type's lifecycles.ts file) and is the source of truth if this table ever drifts.
A note on language (locale). Content is authored per language — Portuguese (pt), English (en), Spanish (es). The CLI ingests one language at a time, chosen with --locale. Ingest the languages that actually have published content; Portuguese is normally the primary.
5. The recommended method: the ingest CLI¶
The CLI runs inside the ai-service container, where the database connection and the CMS token already exist. It produces exactly the same result as the automatic path. Re-running it is safe: rows are keyed on (namespace, source, passage number), so a re-run updates existing entries rather than creating duplicates.
5.1 Ingest one page¶
The general shape of the command is:
docker exec po-ai-service-prod \
python -m app.cli.ingest strapi \
--namespace <namespace> \
--content-type <content-type> \
--kind <single|collection> \
--locale <pt|en|es>
For example, to ingest the Portuguese home page:
docker exec po-ai-service-prod \
python -m app.cli.ingest strapi \
--namespace web_presence_public \
--content-type landing-page \
--kind single \
--locale pt
When it finishes, it prints a single summary line, for example:
ingested is the number of passages stored; failed should be 0. An ingested=0 result almost always means there is no published content for that content type in that language yet (see Troubleshooting).
5.2 Ingest all the website pages at once (Portuguese)¶
To load every marketing page that feeds the assistant, in Portuguese, run them in a loop:
for ct in landing-page about-page how-it-works become-partner-page faq-page contact-page; do
echo "== ingesting $ct (pt) =="
docker exec po-ai-service-prod \
python -m app.cli.ingest strapi \
--namespace web_presence_public \
--content-type "$ct" \
--kind single \
--locale pt
done
To also load English and Spanish, repeat the loop with --locale en and --locale es.
5.3 Ingest the partner profiles¶
Partner profiles are a list (a collection), and they go into their own namespace:
docker exec po-ai-service-prod \
python -m app.cli.ingest strapi \
--namespace partner_profiles_public \
--content-type partner \
--kind collection \
--locale pt
6. Verifying the result¶
After ingesting, confirm it in one or more of these ways, from quickest to most thorough.
6.1 Read the CLI output¶
The simplest check: the command's own summary line. status=success with ingested greater than zero and failed=0 means the passages were stored.
6.2 Ask the assistant directly (end-to-end check)¶
This proves the whole path — store, retrieval, and answer. The public chat is open (no key needed), rate-limited per visitor:
curl -s -X POST https://api.portugalodyssey.pt/public/ai/chat \
-H 'Content-Type: application/json' \
-d '{"prompt":"Porque foi criada a Portugal Odyssey?","language":"pt"}'
A healthy response is a JSON object containing a written answer that reflects your published content, usually with a list of sources. If the assistant replies that it has no information, the content was probably not ingested, or you asked about a topic the published pages do not yet cover.
6.3 Inspect the knowledge store (counts per namespace)¶
The ai-service offers a status endpoint that reports how many passages each namespace holds. It is protected by the admin key, so run it from inside the container, which reads the key from its own environment without ever printing it:
docker exec po-ai-service-prod sh -c \
'curl -s -H "X-Admin-API-Key: $RAG_INGEST_ADMIN_KEY" http://localhost:8000/api/v1/rag/stats'
The reply lists each namespace with its row_count (number of stored passages), source_count, when it was last updated, and the day's token spend. After a successful website ingest you should see web_presence_public with a non-zero row_count.
If the admin key is not set in production, the endpoint answers without the header too (it logs a warning instead of refusing). Setting the key is recommended; the in-container command above works either way.
7. Troubleshooting¶
The command prints ingested=0.
Almost always this means there is no published content for that content type in that language. Confirm in the CMS that the page is Published (not only saved as a draft) and that you ingested the same language you published. An unpublished page is intentionally invisible to ingest.
Authentication error mentioning Strapi or the CMS (HTTP 401 or 403).
The CLI could not read the CMS because the content token is missing or wrong. (HTTP — HyperText Transfer Protocol — status 401 means "not authenticated," 403 means "not allowed.") Re-check that STRAPI_API_TOKEN is set in the container's environment and is a valid token generated in the CMS admin panel. You can confirm the assistant reaches the CMS at all with a quick read test from inside the container; if a fresh token is needed, generate one in the CMS under Settings → API Tokens and place it in the production environment file, then recreate the container so it picks up the new value.
The status or ingest API answers HTTP 401 "invalid admin key."
The admin key you sent does not match the one configured. Use the in-container form shown in Section 6.3 ($RAG_INGEST_ADMIN_KEY) so the correct key is read from the environment rather than typed.
Ingest rejected with HTTP 429 "daily token budget reached."
The assistant caps how much content it will process per day to control cost. (HTTP 429 means "too many requests" / limit reached.) For a large planned load you can clear the day's counter and retry. Clear it from inside the container so the admin key stays private:
docker exec po-ai-service-prod sh -c \
'curl -s -X POST -H "X-Admin-API-Key: $RAG_INGEST_ADMIN_KEY" http://localhost:8000/api/v1/rag/budget/reset'
Then re-run the ingest. The cap also resets automatically at midnight UTC (Coordinated Universal Time, the reference time zone).
A page was unpublished or deleted, but the assistant still mentions it. Tell the assistant to forget that content with a soft delete (a reversible removal). To clear a whole namespace's content, run from inside the container:
docker exec po-ai-service-prod sh -c \
'curl -s -X POST -H "X-Admin-API-Key: $RAG_INGEST_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d "{\"namespace\":\"web_presence_public\"}" \
http://localhost:8000/api/v1/rag/soft-delete'
To remove only one source rather than the whole drawer, add its source_uri to the JSON body. (Note: the automatic path already does this for you when a page is unpublished — this is the manual equivalent.)
Automatic ingest never fires when the author publishes.
The CMS notifies the assistant using an address stored in the CMS environment variable AI_SERVICE_INGEST_URL. If automatic ingest is not happening at all, confirm that variable is set on the CMS container (po-strapi-cms-prod) and points to the assistant (http://ai-service-prod:8000). The manual CLI in Section 5 is the immediate workaround while you investigate.
8. Quick reference¶
Container: po-ai-service-prod · Internal address: http://ai-service-prod:8000 · CMS container: po-strapi-cms-prod
Ingest CLI (inside the container):
python -m app.cli.ingest strapi --namespace <ns> --content-type <ct> --kind <single|collection> --locale <pt|en|es>
python -m app.cli.ingest static-json --namespace <ns> --file <path.json> # for curated seed files
python -m app.cli.ingest docling --namespace <ns> --file-key <key> # for uploaded documents (PDF, etc.)
Admin API (call from inside the container so the key stays private):
| Purpose | Method and path |
|---|---|
| Schedule an ingest job | POST /api/v1/rag/ingest |
| Check a job's progress | GET /api/v1/rag/jobs/{job_id} |
| List recent jobs | GET /api/v1/rag/jobs |
| Inventory per namespace | GET /api/v1/rag/stats |
| Forget content (reversible) | POST /api/v1/rag/soft-delete |
| Clear today's cost cap | POST /api/v1/rag/budget/reset |
| Re-process stored content | POST /api/v1/rag/reembed?namespace=<ns>&force=true |
All admin endpoints require the header X-Admin-API-Key: <value of RAG_INGEST_ADMIN_KEY>.
End-to-end test (public, no key): POST https://api.portugalodyssey.pt/public/ai/chat with body {"prompt":"…","language":"pt"}.
Namespaces: web_presence_public (marketing pages) · partner_profiles_public (partner profiles) · lugar_gallery (places).