AI assistance
How to connect your own AI provider (Azure OpenAI, OpenAI, Anthropic, Mistral or any OpenAI-compatible endpoint), which authoring tasks AI drafts and where, the review queue, budgets and the data boundary, prompt overrides, public disclosure and the ai commands.
Meridian can draft some of the tedious parts of authoring: alt text and captions, structured opening hours from a pasted sentence, tags from your vocabulary, SEO titles and descriptions, summaries, and translations. It runs against a provider you configure, with your credentials; nothing is proxied and nothing is sent anywhere by default.
Three rules hold whatever you configure:
- No provider, no AI. With
ai.enabledfalse or no provider, every AI control is absent from the admin and the portal, not disabled. - A person decides. AI only proposes. Every suggestion waits for a person to accept, edit or reject it, and accepting writes through the same validation as a person’s own edit. Nothing is published by AI.
- Authoring only. AI runs in the admin and, in the DMO bundle, the member portal. The public site never calls a provider: there is no chatbot and no AI on visitor pages.
Connect a provider
# config/engine.yaml
ai:
enabled: ${AI_ENABLED:-false}
provider: ${AI_PROVIDER:-none}
openai_compatible:
base_url: ${AI_BASE_URL:-http://localhost:11434/v1}
api_key: ${AI_API_KEY}
model_text: ${AI_TEXT_MODEL:-llama3.1}
model_vision: ${AI_VISION_MODEL:-}
budget:
monthly_cost_cap: "20"
per_user_daily_requests: 100
pricing:
input_per_million: "0.15"
output_per_million: "0.60" Then confirm it, and give your editors the permission:
engine-dmo check # provider keys, models, and which features stay unavailable
engine-dmo ai test # one round trip with no content
engine-dmo ai status # provider, models, health and this month's spend # config/access/roles.yaml: a role holding the AI permissions
roles:
- key: ai-drafter
name: { en: AI drafting }
permissions: [ai.use, ai.bulk] # config/access/grants.yaml: give it to the editors group beside its editor role
grants:
- { group: administrators, role: owner }
- { group: editors, role: editor }
- { group: editors, role: ai-drafter } Run engine-dmo check --fix to write the role’s id. No built-in role holds the AI permissions except Owner; grant them in roles.yaml.
Providers
Write only the block for the provider you use. API keys are secrets: a bare ${VAR} with no default.
provider | Block keys | Required | Notes |
|---|---|---|---|
openai_compatible | base_url (up to and including /v1), api_key, model_text, model_vision, model_embedding | base_url, model_text | Ollama, vLLM, a sovereign or air-gapped endpoint. A local endpoint may need no key (set the variable empty). |
openai | as above; base_url defaults to https://api.openai.com/v1 | model_text | |
mistral | as above; base_url defaults to https://api.mistral.ai/v1 | model_text | |
anthropic | api_key, base_url (default https://api.anthropic.com), model_text, model_vision | model_text | No embeddings, so duplicate detection is unavailable. |
azure_openai | endpoint, api_key, deployment_text, deployment_vision, deployment_embedding, api_version (default 2024-10-21) | endpoint, deployment_text | |
none | The default. |
Alt text drafting uses the vision model, else the text model (which may not accept images; check warns). Duplicate detection needs an embedding model (check warns while there is none). ai.models overrides the model for one feature: models: { alt_text: gpt-4o-mini } (for Azure, a deployment name).
What AI does, and where
Each feature has its own switch under ai.features, and each needs ai.use on the site.
| Feature | Key (default) | Where | What it does |
|---|---|---|---|
| Alt text and captions | alt_text (on) | An asset’s page; the alt text queue’s “Draft these with AI”; engine-dmo ai bulk alt-text | Drafts alt text or a caption per locale, for the default or one site’s own wording. Held to 125 characters and never “image of”. |
| Opening hours extraction | hours_extraction (on) | A document’s assistance page, for an opening_hours field; the member portal (DMO) | Turns what a business wrote (“12 til 3, closed Mondays”) into structured hours, shown beside the source text. |
| Tag suggestion | tag_suggestion (on) | A document’s assistance page | Suggests tags from your configured vocabulary only; the reviewer ticks which to add. |
| SEO drafting | seo_drafting (on) | A document’s assistance page, for an seo field | Drafts a title and a description within the field’s limits, from the document’s own text. |
| Summarisation | summarisation (off) | A document’s assistance page, for a text field that declares summary_of: <field> | Drafts a summary of the source field, held to the field’s max (280 when it has none). |
| Translation drafting | translation (off) | The side-by-side translation editor | Drafts the field being translated, with the glossary and translation memory as context. |
| Zero-result content briefs | zero_result_briefs (off) | Insight, search (analytics.view too) | Turns the site’s commonest searches that found nothing into up to five briefs (a listing, an article, a synonym or a redirect to consider). Writes nothing. |
| Duplicate detection | duplicate_detection (on) | CRM organisations (DMO) | Finds likely duplicate organisations by embedding. |
| Member listing improvement | member_improvement (off) | The member portal (DMO) | Suggests clearer wording for a member’s own listing text. |
A field that summarises another:
fields:
- name: body
type: text
multiline: true
- name: summary
type: text
max: 280
summary_of: body ai.features.image_generation: true is refused: there is no image generation. quality_review and ai.translation.quality_signals are accepted and do nothing. There is no Amazon Bedrock adapter; reach a model hosted there through an OpenAI-compatible gateway.
The editor’s SEO advice panel (a title that duplicates another page, internal link suggestions) is computed, not generated, and is shown with or without a provider.
How a suggestion is made and reviewed
The admin’s AI queue lists every suggestion waiting for review; engine-dmo ai queue does the same. Bulk runs (ai.bulk) print an estimate of the cost before they queue anything, and each draft still waits in the queue. Suggestions that have been decided are kept for retention.ai_suggestions (thirteen months).
engine-dmo ai bulk alt-text --locale cy --dry-run # count and estimate only
engine-dmo ai bulk alt-text --locale cy --site main # queue drafts of the site's own wording Cost control
| Key | Default | Meaning |
|---|---|---|
budget.monthly_cost_cap | 50 | A hard stop in the site currency; at the cap every AI feature is unavailable. 0 is no cap. |
budget.monthly_tokens | 5000000 | A token cap per month. |
budget.per_user_daily_requests | 200 | Requests per person per day. |
budget.per_feature_monthly_requests | {} | Caps per feature: { alt_text: 2000 }. |
budget.sites | {} | A site’s own monthly cost cap: { main: "10" }. |
budget.estimate_before_bulk | true | Accepted and not read: a bulk run always prints its estimate first. |
budget.estimated_tokens_per_call | {} | Override the engine’s estimate per feature. |
pricing.input_per_million, pricing.output_per_million | Your provider’s prices in the site currency, for the cap and the estimate. The engine never bills. | |
cache.enabled, cache.ttl | true, 720h | The same question about unchanged content is not sent twice. |
circuit_breaker.failures, window, cooldown | 3, 1m, 2m | After repeated failures every feature pauses, sending nothing, until the cool-down ends. |
timeout | 30s | Per request. |
engine-dmo ai budget --period 2026-10 # spend by feature and by user against the caps
engine-dmo ai log --from 2026-10-01 # every request: feature, user, model, tokens, cost, outcome, data sent The data boundary
| Key | Default | Meaning |
|---|---|---|
data_boundary.allow_content | true | The content being assisted may be sent. |
data_boundary.allow_personal_data | false | CRM personal fields and phone and email values are stripped. true is a warning in check. |
data_boundary.redact_before_send | true | Email addresses and phone numbers inside free text are replaced before sending. |
data_boundary.allow_analytics | aggregate_only | aggregate_only or none. Raw events are never sent; none turns off zero-result briefs. |
data_boundary.exclude_fields | [] | Fields never sent: listing.internalNotes, *.bookingNote. |
Credentials, session tokens, audit entries and membership records are never sent. engine-dmo ai data-flow-statement writes a data protection annex from your configuration: the provider and where requests go, each feature and what it sends, the boundary, the budget and what is recorded. It needs no database:
engine-dmo ai data-flow-statement --out ai-data-flow.md Telling visitors
Accepting a suggestion records how the text was made: drafted (accepted as written), edited, extracted, translated, suggested. On public pages, entry.aiAssisted is true when the published text carries a record the disclosure policy covers, and aiDisclosure(entry) prints the disclosure sentence, or nothing:
{{ aiDisclosure(entry) }} The sentence is the ai.disclosure string in your string catalogue, in the page’s locale, else “Parts of this page were drafted with the help of AI and reviewed by a person before publication.” Reading the records calls no provider.
ai.disclosure.public | Discloses |
|---|---|
never | Nothing. |
drafted_only (default) | Text accepted as drafted without material change, and text written by an agent through the content MCP. |
all | Any assisted text, edited and extracted included. Tag suggestions are never counted. |
ai.disclosure.admin is always and cannot be changed: the admin always shows provenance.
Prompt overrides
Each feature’s prompt can be replaced with your own wording, one file per prompt in ai.prompts_dir (config/ai/prompts):
# config/ai/prompts/alt-text.yaml
key: alt-text
version: 2
system: |
You write alt text for an image on a public website about an arts centre.
...
Answer with a JSON object: {"alt": "<the alt text>"}. The keys are alt-text, caption, hours-extraction, tag-suggestion, seo-drafting, summarisation, translation, zero-result-briefs, member-improvement and duplicate-detection. version (1 or more) is recorded on every suggestion the prompt produces; raise it when you change the text. check refuses an unknown key, a missing version and empty text. Keep the answer format the engine’s prompt asks for: the answer is validated against it.
Commands
| Command | What it does |
|---|---|
ai status | Provider, models, capabilities, health and spend this month. |
ai test | One round trip with no content: confirms the endpoint, the key and the model. |
ai queue | Suggestions waiting for review. |
ai bulk alt-text | Queue alt text drafts for every image missing alt text in a locale (--locale, --site, --dry-run). |
ai budget | Spend by feature and user against the caps (--period). |
ai log | The activity log (--from, --limit). |
ai data-flow-statement | The data protection annex (--out). |
All but data-flow-statement need the database.
Permissions
| Permission | Allows |
|---|---|
ai.use | Asking for a draft on a site. Accepting also needs the permission the write itself needs, such as media.edit for alt text or content.edit for a field. |
ai.bulk | Bulk runs and the duplicate scan. |
ai.configure | The admin’s AI usage screen: spend by feature and user against the caps. |
Only Owner holds them by default.
Configuration reference
The ai: section of config/engine.yaml, beyond the provider blocks and the tables above:
| Key | Default | Meaning |
|---|---|---|
enabled | false | Must read true or false after interpolation. |
provider | none | azure_openai, anthropic, openai, mistral, openai_compatible or none. |
models | {} | A model per feature. Not duplicate_detection. |
features.* | see the features table | One switch per feature. |
duplicates.threshold | 0.9 | Similarity, 0.5 to 1, at or above which two organisations are offered as duplicates. |
translation.memory_examples | 8 | Prior translations sent per request, 0 to 50. |
translation.length_ratio_bands | {} | Per language pair, the expected length ratio: { "en:de": [0.9, 1.5] }. |
disclosure.public | drafted_only | never, drafted_only or all. |
prompts_dir | config/ai/prompts | Where prompt overrides live. |
Analytics
What the built-in, cookieless analytics collects and how (server-side views, the beacon, tracked links, impressions), why it needs no consent banner, the template helpers, dashboards in analytics/dashboards.yaml, exports, retention and every analytics setting.
Accessibility
What the engine does for accessibility on public pages and what a template author must still do, the accessibility checks in engine-dmo check and engine-dmo a11y audit, the alt text policy, and the accessibility statement, issues register and conformance report the engine generates.