strapi-plugin-ai-assistant
v0.2.0
Published
Draft, rewrite, translate, and SEO-assist Strapi content with OpenAI, Claude, or Gemini.
Maintainers
Readme
strapi-plugin-ai-assistant
Draft, rewrite, translate, and SEO-assist Strapi 5 content from the admin, using OpenAI, Claude, or Gemini.
The plugin reads your content-type schema and fills titles, teasers, rich text (Markdown, HTML, Blocks, or TipTap), SEO fields, dates, and image alt text. Editors can also create a draft from a public URL, a Word/PDF upload, or a short brief.
Requirements
- Strapi 5.13+ (homepage widget and Content Manager side panel)
- Node 20+
- An API key for at least one provider: OpenAI, Anthropic (Claude), or Google Gemini
Install
npm install strapi-plugin-ai-assistantRebuild the admin panel and restart Strapi:
npm run build
npm run developStrapi loads installed plugins automatically. If you need to toggle it explicitly, add this to config/plugins.ts:
export default {
'ai-assistant': {
enabled: true,
},
};Configure
Open Settings → AI Assistant.
- Paste an API key for OpenAI, Claude, and/or Gemini. Keys are stored encrypted with
ENCRYPTION_KEYor, if that is unset, the first value inAPP_KEYS. If you rotate that secret, saved keys become unreadable and must be pasted again. - Pick the default provider and model. Editors can override the model per request.
- Write a voice prompt for your brand. The default is a short generic editorial prompt — replace it with yours.
- Enable the content types the assistant may edit, and exclude fields it should leave alone.
Optional environment fallbacks (the Settings UI is enough for most sites):
AI_ASSISTANT_PROVIDER=openai
OPENAI_API_KEY=
OPENAI_MODEL=gpt-5.6-luna
ANTHROPIC_API_KEY=
ANTHROPIC_MODEL=claude-sonnet-5
GEMINI_API_KEY=
GEMINI_MODEL=gemini-3.7-flash
ENCRYPTION_KEY= # recommended; otherwise APP_KEYS is usedPermissions
After install, grant these in Settings → Administration panel → Roles:
| Action | Who needs it | | --- | --- | | Use AI Assistant | Editors who generate, translate, or create drafts | | Configure AI Assistant | Admins who manage keys, voice, and content types |
Super Admin already has both.
What it can do
From the Content Manager side panel, Create with AI in the menu, or the homepage widget:
- Generate or rewrite copy for the current entry, in a locale you pick
- Write SEO title / description / keywords
- Draft from a public URL or a
.docx/.pdf/.txt/.mdfile - Improve, shorten, or retitle
- Generate image alt text
- Translate into other locales (Internationalization)
- Create a new draft entry mapped onto your schema
It skips relations, dynamic zones (except a required zone it can seed), passwords, and most custom fields it does not understand. TipTap JSON, Strapi Blocks, and HTML rich text are all supported.
Search Assistant (semantic search)
The plugin indexes published entries into a vector table in Strapi's own database and exposes POST /api/ai-assistant/search. A natural-language query is embedded and ranked by cosine similarity against those vectors.
This is not Meilisearch/Algolia (typos, SKUs, facets). Ranking is embeddings + cosine. A lexical fallback still keeps an entry if the query (or every significant word) appears in the indexed text, so a short keyword like transport can hit an article about medical transport even when cosine is below the tolerance. Intent prefixes (quiero buscar, search for, …) are always stripped. Rewrite query with AI (off by default) produces two short phrases — the user’s language plus a second-language variant — and embeds both, so the index language does not have to match the query. Pass locale when the type is i18n; that locale is a hint for the second phrase, not a reason to drop the first. Answer with AI (off by default) then writes a short grounded paragraph from the top hits; it does not rerank.
No external vector DB is required.
Enable
Open Settings → AI Assistant → Search:
- Turn on Enable Search Assistant. You need an OpenAI or Gemini API key. Anthropic has no embeddings API, so Claude cannot index or search.
- Pick the embedding provider and model (defaults:
text-embedding-3-smallfor OpenAI,gemini-embedding-001for Gemini). - Pick a tolerance (strict / normal / loose — cosine thresholds ~0.75 / 0.60 / 0.45). Weaker models often score unrelated dummy text around 0.6, so Normal can look noisy; Strict is safer until you have real copy.
- Optionally turn on Rewrite query with AI. Requires a chat API key (OpenAI, Claude, or Gemini) on the main Settings page. Conversational queries become a short phrase in the user’s language plus a second-language variant; both are embedded. If the rewrite fails, search uses the stripped original. Leave it off for a direct embed of what the user typed.
- Optionally turn on Answer with AI. After ranking, the same chat model writes a short paragraph from the top hits, in the user’s language. Search results do not change. If the model fails or times out, the response has no
answerfield. - For each content type, tick Enable for search and untick fields you do not want indexed. Dynamic zones and repeatable components are one row each and cover every text field inside their blocks.
Save before Reindex. The queue uses the saved provider/model, not the unsaved dropdown. Wait until the page shows Idle.
Indexing runs in the background. You can re-index one type or everything.
One embedding space
The same saved embedding model embeds both the documents and the query. OpenAI and Gemini vectors are not comparable.
- Do not switch provider in the UI and search without saving and reindexing.
- After a model change, leftover chunks from the previous
provider:modelare ignored. Search against an empty matching set returns{ "results": [], "total": 0 }(not 503, unless the whole table is empty). - Changing the saved model triggers a full reindex. Finish that job before querying.
What is indexed
Text only: strings, rich text (Markdown, HTML, Blocks, TipTap), and text inside dynamic zones / components you left ticked. Media, relations, and passwords are skipped — images are not described or embedded.
Indexing populates components and dynamic zones (not media/relations). Nested titles such as card.Title are included in the chunk text and used as the hit title. Later chunks of a long article are prefixed with that title so it is not lost after the first split.
Endpoint
POST /api/ai-assistant/search
Content-Type: application/json
{
"query": "remote work regulation in 2024",
"uid": "api::article.article",
"from": "2024-01-01",
"to": "2024-12-31",
"limit": 10
}| Field | Required | Notes |
| --- | --- | --- |
| query | yes | Natural language. Intent prefixes are stripped. If Rewrite query with AI is on, the chat model may shorten it before embedding. |
| uid | no | Must be search-enabled. Omit to search all enabled types. |
| from / to | no | Filter on the entry's publishedAt. |
| locale | no | Only for i18n types. Chunks from types without Internationalization are stored with an empty locale. Sending "locale": "en" against those types returns zero hits. Omit locale unless the content type is localized. |
| limit | no | Number of results to return. Default 10, hard cap 50. There is no pagination (offset / cursor). |
Response:
{
"results": [
{
"uid": "api::article.article",
"documentId": "…",
"locale": "en",
"slug": "remote-work-2024",
"title": "Remote work rules updated in 2024",
"publishedAt": "2024-03-12T…",
"score": 0.78,
"entry": { }
}
],
"threshold": 0.6,
"total": 3,
"query": "quiero buscar remote work",
"rewrittenQuery": "trabajo remoto",
"rewrittenQueryAlt": "remote work",
"answer": "The 2024 remote-work rules apply to employees who work from home more than three days a week."
}results— at mostlimit(capped at 50), one hit per document (best chunk).total— how many documents passed the cosine threshold before the cap. Iftotalis 80 andlimitis 10, you get 10 items; the rest are not listed and cannot be paged.threshold— the tolerance in effect.query— the string the client sent.rewrittenQuery— primary phrase that was embedded (after strip and optional LLM rewrite). Same asquerywhen nothing changed.rewrittenQueryAlt— optional second-language phrase, also embedded. Omitted when unused.answer— optional short paragraph grounded in the top hits. Present only when Answer with AI is on and the chat model succeeded.- Only published entries from search-enabled types.
Errors: 400 (bad input / search disabled / type not enabled), 429 (rate limit — 30 req/min per token or IP), 503 (index table empty — run Reindex). An empty results array with total: 0 means the table has chunks but none matched the filters, model tag, or threshold — not that search is “down”.
Auth
The route is registered as content-api with auth: false: it is callable without an API token. Rate limiting still applies. Tighten this in your app (API tokens, Users & Permissions, or a proxy) if the index should not be public.
How it works
- On publish/update of an enabled type, the plugin extracts the ticked text fields, splits them into ~1.8k-character chunks with a small overlap, and embeds each chunk with the saved embedding model.
- Vectors live in
plugin::ai-assistant.search-chunk(hidden — not in Content Manager or the Content-Type Builder), tagged withprovider:model. - On search, intent prefixes are stripped. If Rewrite query with AI is on, the chat model may return a short phrase in the user’s language and a second-language variant (on failure, the stripped original is used). Unique phrases are embedded with the saved embedding model. SQL keeps chunks whose
embeddingModelmatches and that passuid/ date / locale filters. Survivors are ranked by the best cosine across those phrases. Chunks whose text already contains a phrase are kept even when cosine is below the tolerance, then hydrated into full published entries (components populated). If Answer with AI is on, the chat model writes a short paragraph from the top ranked chunks; ranking is unchanged.
Unpublish/delete removes those chunks. Turning off a content type or the global toggle purges its chunks. Changing the embedding model reindexes everything.
Document uploads
Word and PDF parsing ship with the plugin. Uploads go to the server as multipart files (not JSON), up to 6 MB. Extracted text is capped at about 12 000 characters.
| Package | Used for | Installed with the plugin |
| --- | --- | --- |
| mammoth | Word (.docx) text | Yes |
| jszip | Images embedded in .docx | Yes |
| pdf-parse | PDF text | Yes |
| unpdf | Images inside PDFs | Yes |
| sharp | Encoding those PDF images as JPEG | Optional — see below |
Notes:
.doc(Word 97–2003) is not supported. Save as.docxor PDF.- Scanned PDFs with no text layer cannot be read. Copy the text into Brief, or export a text PDF.
- Google Docs: share with Anyone with the link, or File → Download → .docx / .pdf and upload the file.
- If an image field is enabled for the content type, Create with AI / From source uploads the first usable image from the URL or document into the Media Library.
sharp (PDF cover images)
Strapi already depends on sharp for the Media Library, so PDF image encoding usually works with no extra install. This plugin lists sharp as an optional dependency for package managers that do not hoist Strapi’s copy (for example pnpm).
If Settings → AI Assistant reports that sharp is missing, install it on the Strapi app (not only this plugin):
npm install sharpWithout sharp, Word/PDF text still works. Cover images from PDFs may be skipped; images from .docx (word/media) still work.
Internationalization
Translate requires the Strapi Internationalization plugin (bundled with Strapi 5) with at least two locales. Enable i18n on the content type. Translations are saved as drafts; media and dates are copied, not rewritten.
Generation uses a Generate in locale picker (the default locale is selected first). The model is instructed to write in that language, including Create with AI drafts.
TipTap and other rich text
- Markdown (
richtext) and Blocks work with no extra plugin. - TipTap (JSON
doc) is supported when the field’scustomFieldlooks like TipTap / a rich-text custom field. Install that custom-field plugin in your Strapi app if you use it. Applying a preview syncs TipTap editors and the other edit-view fields (including components in the right sidebar) from the form state.
Local development
This package is built with the Strapi Plugin SDK.
npm install
npm run watch:linkIn a Strapi 5.13+ app:
npx yalc add --link strapi-plugin-ai-assistant
npm install
npm run developnpm run build
npm run verifyPublish
- Upload
logo.png(512×512) on the Marketplace form. npm loginandnpm publish --access public.- Submit the npm URL on the Strapi Marketplace (guidelines).
Issues
Open an issue at github.com/cnp1984/strapi-plugin-ai-assistant/issues. Include your Strapi version, the content-type UID, and whether the request used a URL, a document, or a brief.
License
MIT
