From af8aa2583efe92ff71c0eef830bb6b34016db0e5 Mon Sep 17 00:00:00 2001 From: Hannes Kirsman Date: Fri, 16 Oct 2026 11:24:49 +0000 Subject: [PATCH 1/2] GH-485: Add Drupal AI with MIRAI as the default chat provider Installs the Drupal AI module and connects it to MIRAI, the internal Open WebUI service, through the OpenAI-compatible provider, so the site has a working chat model for the AI demo. Reason: MIRAI exposes an OpenAI-compatible API (/api/models, /api/chat/completions) and its Gemini models are approved for client data, so the generic openai_compatible provider is enough and no custom provider is needed. This repository is public, so neither the MIRAI host nor the API key may end up in config/sync. The provider config exports a placeholder endpoint that settings.php replaces at runtime from MIRAI_API_URL (config overrides are not exported by drush cex), and the Key entity uses the env provider to read MIRAI_API_KEY. These variables live in .ddev/.env.web rather than the project .env, because load.environment.php only fills $_ENV/$_SERVER while the Key module reads getenv(). The setup is packaged as a recipe so it can be repeated on a fresh install or with another model via --input. - add drupal/ai and drupal/ai_provider_openai_compatible - add ai_mirai recipe enabling key, ai, ai_api_explorer and the OpenAI-compatible provider, with a configurable chat model input - export AI config: placeholder MIRAI endpoint, mirai_api_key Key entity reading MIRAI_API_KEY, google_genai.gemini-2.5-flash as default chat model - override the MIRAI endpoint from MIRAI_API_URL in settings.php - add AI research notes and the step-by-step implementation plan - add scripts/ai-endpoint-check.sh to diagnose OpenAI-compatible endpoints without printing the key - ignore .ddev/.env.web and document fresh install from existing config in README - re-export user view displays (key order only) Follow-up: Silta environments need MIRAI_API_URL and MIRAI_API_KEY (the key as a secret) and network access to MIRAI; without them the site deploys but AI requests fail. Next step is the CKEditor AI assistant. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitignore | 1 + README.md | 12 + composer.json | 2 + config/sync/ai.external_moderation.yml | 3 + config/sync/ai.html_to_markdown.settings.yml | 16 ++ config/sync/ai.settings.yml | 12 + ...ai_provider_openai_compatible.settings.yml | 17 ++ ....entity_view_display.user.user.compact.yml | 2 +- ....entity_view_display.user.user.default.yml | 2 +- config/sync/core.extension.yml | 4 + config/sync/key.key.mirai_api_key.yml | 16 ++ docs/01-ai-research.md | 220 ++++++++++++++++ docs/02-ai-implementation-plan.md | 243 ++++++++++++++++++ recipes/ai_mirai/recipe.yml | 56 ++++ scripts/ai-endpoint-check.sh | 43 ++++ web/sites/default/settings.php | 5 + 16 files changed, 652 insertions(+), 2 deletions(-) create mode 100644 config/sync/ai.external_moderation.yml create mode 100644 config/sync/ai.html_to_markdown.settings.yml create mode 100644 config/sync/ai.settings.yml create mode 100644 config/sync/ai_provider_openai_compatible.settings.yml create mode 100644 config/sync/key.key.mirai_api_key.yml create mode 100644 docs/01-ai-research.md create mode 100644 docs/02-ai-implementation-plan.md create mode 100644 recipes/ai_mirai/recipe.yml create mode 100755 scripts/ai-endpoint-check.sh diff --git a/.gitignore b/.gitignore index 007a81b7..e5ad3612 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ # Ignore .env files as they are personal /.env +/.ddev/.env.web # Ignore node modules node_modules diff --git a/README.md b/README.md index b9a00739..000ad569 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,18 @@ This project uses DDEV for local development. ddev syncdb ``` +#### Fresh install from configuration + +If there is no remote environment to sync from (for example when starting a new project from this template), install Drupal from the exported configuration in `config/sync`: + + ```bash + ddev drush site:install --existing-config --yes --account-pass=admin + ddev drush uli + ``` + +`config/sync` is the source of truth, including the site UUID. A plain `drush site:install` creates a site with a new UUID, and `drush cim` then fails with "Site UUID in source storage does not match the target storage". Re-run the command above to fix this; it replaces the current database. + +After enabling modules or changing configuration, export it with `ddev drush cex` and commit the changes in `config/sync`. Note: All commands in the DDEV section should be run within the DDEV environment using `ddev` prefix (e.g., `ddev drush uli`), or by using `ddev ssh` to access the container shell first. #### DDEV services and access points diff --git a/composer.json b/composer.json index 79d2d6c9..394e7e15 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,8 @@ "composer/installers": "^2.3", "cweagans/composer-patches": "^2.0", "drupal/admin_toolbar": "^3.6", + "drupal/ai": "^1.5", + "drupal/ai_provider_openai_compatible": "^1.2", "drupal/config_split": "^2.0", "drupal/core-composer-scaffold": "^11.4", "drupal/core-recommended": "^11.4", diff --git a/config/sync/ai.external_moderation.yml b/config/sync/ai.external_moderation.yml new file mode 100644 index 00000000..402ee332 --- /dev/null +++ b/config/sync/ai.external_moderation.yml @@ -0,0 +1,3 @@ +_core: + default_config_hash: FNT2zzMLKeg6TviGJAYUvRSjAJmBvmdLnh9USO58MAc +moderations: { } diff --git a/config/sync/ai.html_to_markdown.settings.yml b/config/sync/ai.html_to_markdown.settings.yml new file mode 100644 index 00000000..4946f7fe --- /dev/null +++ b/config/sync/ai.html_to_markdown.settings.yml @@ -0,0 +1,16 @@ +_core: + default_config_hash: KEGbjvHsUwKwDnmGkgTPy58oJo7FhOgYzx7qNv1alJ0 +header_style: atx +suppress_errors: true +strip_tags: true +strip_placeholder_links: true +bold_style: '**' +italic_style: '*' +remove_nodes: '' +hard_break: false +list_item_style: '-' +preserve_comments: false +use_autolinks: true +table_pipe_escape: \| +table_caption_side: top +strip_whitespace: true diff --git a/config/sync/ai.settings.yml b/config/sync/ai.settings.yml new file mode 100644 index 00000000..a8628bfd --- /dev/null +++ b/config/sync/ai.settings.yml @@ -0,0 +1,12 @@ +_core: + default_config_hash: WWvo9_41v-oaqjbKdBtohSrem-s9w-7MIfvCWjZvAI4 +prompt_logging: false +prompt_logging_tags: '' +default_providers: + chat: + provider_id: openai_compatible + model_id: google_genai.gemini-2.5-flash +request_timeout: 60 +allowed_hosts: { } +allowed_hosts_rewrite_links: false +global_guardrails: { } diff --git a/config/sync/ai_provider_openai_compatible.settings.yml b/config/sync/ai_provider_openai_compatible.settings.yml new file mode 100644 index 00000000..9a723305 --- /dev/null +++ b/config/sync/ai_provider_openai_compatible.settings.yml @@ -0,0 +1,17 @@ +_core: + default_config_hash: wu7wtu-cIy7CT7wYhABkw5kutyyjuWnsMu5W5si5Vzs +api_key: mirai_api_key +endpoint: 'https://mirai.example.com/api' +models: + - + id: google_genai.gemini-2.5-flash + label: 'MIRAI default chat model' + operation_types: + - chat + - translate_text + capabilities: + - chat_system_role + - chat_json_output + parameters: + temperature: 0.7 + max_tokens: 4096 diff --git a/config/sync/core.entity_view_display.user.user.compact.yml b/config/sync/core.entity_view_display.user.user.compact.yml index 1cf6a056..beadfd19 100644 --- a/config/sync/core.entity_view_display.user.user.compact.yml +++ b/config/sync/core.entity_view_display.user.user.compact.yml @@ -20,8 +20,8 @@ content: type: image label: hidden settings: - image_style: thumbnail image_link: content + image_style: thumbnail image_loading: attribute: lazy third_party_settings: { } diff --git a/config/sync/core.entity_view_display.user.user.default.yml b/config/sync/core.entity_view_display.user.user.default.yml index 9601be08..33e75632 100644 --- a/config/sync/core.entity_view_display.user.user.default.yml +++ b/config/sync/core.entity_view_display.user.user.default.yml @@ -22,8 +22,8 @@ content: type: image label: hidden settings: - image_style: thumbnail image_link: content + image_style: thumbnail image_loading: attribute: lazy third_party_settings: { } diff --git a/config/sync/core.extension.yml b/config/sync/core.extension.yml index d29e47c7..74e455f6 100644 --- a/config/sync/core.extension.yml +++ b/config/sync/core.extension.yml @@ -3,6 +3,9 @@ _core: module: admin_toolbar: 0 admin_toolbar_tools: 0 + ai: 0 + ai_api_explorer: 0 + ai_provider_openai_compatible: 0 automated_cron: 0 block: 0 block_content: 0 @@ -21,6 +24,7 @@ module: filter: 0 help: 0 image: 0 + key: 0 link: 0 menu_link_content: 0 menu_ui: 0 diff --git a/config/sync/key.key.mirai_api_key.yml b/config/sync/key.key.mirai_api_key.yml new file mode 100644 index 00000000..ae7623ce --- /dev/null +++ b/config/sync/key.key.mirai_api_key.yml @@ -0,0 +1,16 @@ +uuid: cc0401c8-f977-41c9-a0ea-8d884d32a479 +langcode: en +status: true +dependencies: { } +id: mirai_api_key +label: 'MIRAI API key' +description: 'Read from the MIRAI_API_KEY environment variable.' +key_type: authentication +key_type_settings: { } +key_provider: env +key_provider_settings: + env_variable: MIRAI_API_KEY + base64_encoded: false + strip_line_breaks: true +key_input: none +key_input_settings: { } diff --git a/docs/01-ai-research.md b/docs/01-ai-research.md new file mode 100644 index 00000000..987c0560 --- /dev/null +++ b/docs/01-ai-research.md @@ -0,0 +1,220 @@ +# **Enterprise AI Integration in Drupal 11: A Comprehensive Blueprint for Content Editor Workflows** + +The architectural evolution of the Drupal content management system, culminating in the release of Drupal 11 and the preconfigured Drupal CMS distribution (formerly the Starshot Initiative), represents a definitive strategic shift toward empowering non-technical users1. Historically recognized as a highly customizable but complex framework favored primarily by developers, modern Drupal architecture deliberately prioritizes marketers, content creators, and site builders2. The most significant catalyst for this transformation is the official Drupal AI Initiative, which has established a comprehensive, provider-agnostic framework designed to weave artificial intelligence natively into the editorial experience through a low-code paradigm5. +Integrating artificial intelligence into an enterprise content management system introduces inherent risks, particularly concerning data governance, brand safety, and the propagation of algorithmic hallucinations. However, Drupal’s highly structured data model provides an intrinsic "trust layer"8. Features such as granular field definitions, taxonomy vocabularies, rigorous revision histories, and robust role-based access controls ensure that AI acts as a strictly governed editorial assistant rather than an autonomous, unsupervised entity8. AI agents operating within Drupal inherit the explicit permissions of the human user executing the request, meaning algorithmic outputs are structurally confined to authorized workflows and subject to human review prior to publication8. +This comprehensive architectural report provides an exhaustive analysis of the stable, production-ready Drupal 11 AI ecosystem specifically tailored for content editors. By intentionally excluding highly experimental autonomous agent workflows, this document outlines an enterprise-grade module selection strategy. Furthermore, the latter half of this report serves as a deterministic, machine-readable implementation blueprint designed to be ingested by automated local AI agents. This blueprint provides the exact programmatic specifications required to construct a fully functional, zero-cost local demonstration environment utilizing DDEV, local Large Language Models (LLMs) via Ollama, and localized vector databases. + +## **Architectural Foundation: The Provider-Agnostic AI Framework** + +The nucleus of the modern Drupal AI ecosystem is the drupal/ai (AI Core) module, which functions as a sophisticated middleware abstraction layer6. This core infrastructure standardizes the communication protocols between Drupal and over 48 distinct AI service providers, encompassing commercial entities like OpenAI, Anthropic, and Google Gemini, as well as locally hosted, open-weight models executed through Ollama or LiteLLM6. +The strategic importance of this abstraction layer lies in its mitigation of vendor lock-in and its enhancement of data sovereignty. Organizations can dynamically transition from a proprietary cloud-based model to a localized open-source model simply by altering configuration routing, requiring zero code refactoring or workflow disruption6. The core module establishes specific operation types—such as text generation, embedding creation, translation, and image analysis—allowing administrators to route specific tasks to the most efficient or cost-effective provider9. +To ensure stability in production deployments, the Drupal AI ecosystem categorizes its functional modules into distinct readiness tiers. For an editorial demonstration environment, implementations must strictly adhere to the foundational, governance, and editorial tiers, explicitly avoiding modules labeled as experimental. + +| Readiness Tier | Characteristics | Recommended Community Modules | Suitability for Editorial Demonstration | +| :---- | :---- | :---- | :---- | +| **Foundation & Core Integration** | Provider abstraction, core API routing, logging, and foundational AI operations. | AI Core, Key, AI Provider Ollama, AI Provider OpenAI, AI Provider LiteLLM. | Mandatory Prerequisite | +| **Editorial Tooling** | Stable, bounded algorithmic outputs, deeply integrated into native UI, reliant on human review. | AI CKEditor, AI Content Suggestions, AI Translate, Field Widget Actions, AI Automators. | Production-ready / Highly Recommended | +| **Governance & Policy Control** | Enforces brand context, restricts inappropriate generation, tracks token consumption. | Context Control Center (CCC), AI Metering, AI Observability, AI Validations. | Production-ready / Highly Recommended | +| **Advanced Orchestration** | Predictable, auditable multi-step workflow automation with explicit data connections. | FlowDrop, ECA (Event-Condition-Action), AI Schema.org JSON-LD. | Production-ready / Recommended | +| **Retrieval-Augmented Generation** | Semantic vector database search grounded explicitly in indexed site content. | AI Search, AI Search Block, AI Knowledge Connector, AI VDB Provider Milvus. | Production-ready / Recommended | +| **Autonomous Agentic Workflows** | Unsupervised multi-step task execution, complex looping decision trees without immediate human gating. | AI Agents, AI Views Agent, Autonomous Developer Workflows. | Experimental / Strictly Excluded | + +## **Production-Ready Capabilities for the Editorial Experience** + +The true operational value of the Drupal AI integration is realized when capabilities are exposed directly within the interfaces where editors perform their daily tasks. The ecosystem avoids forcing users into separate generative AI applications, instead injecting intelligence directly into the rich text editor and the entity edit form9. + +### **In-Context Generation and Rich Text Manipulation** + +The rich-text editor remains the primary canvas for digital content creation. The ai\_ckeditor module, a free submodule absorbed into the main AI project, integrates AI assistance seamlessly into the CKEditor 5 toolbar12. This allows editors to interact with configured language models inline, without breaking their concentration or workflow12. +The contextual awareness of this integration is paramount. The module dynamically reads the surrounding content currently existing within the editor DOM, ensuring that any generated text comprehends the established narrative context, thereby preventing disjointed or stylistically inconsistent additions13. Editors can highlight specific paragraphs to trigger revision tools, summarizing complex technical jargon, adjusting the tone to align with corporate guidelines, or translating discrete sections into alternate languages13. +Advanced integrations, such as the ckeditor\_ai\_agent, introduce rapid conversational interfaces via slash commands directly within the text area. An editor typing / invokes a command menu, allowing them to stream content from an LLM directly into the editor in real-time13. Crucially, because these requests are routed server-side through the Drupal AI framework, the output strictly adheres to Drupal's HTML formatting rules and filtering configurations, preventing the introduction of malicious markup or broken layouts13. Furthermore, accessibility is actively enforced during content creation via modules like ai\_editoria11y, which analyzes the text in real-time to identify WCAG violations and offers AI-powered remediation suggestions directly within the CKEditor interface14. + +### **Form-Level Assistance and Metadata Generation** + +While the CKEditor integrations handle long-form narrative body text, the ai\_content\_suggestions module operates at the granular field level, providing an interactive sidebar on the entity edit form9. This tool is designed for rapid, targeted assistance to reduce the cognitive load associated with publishing administration. +A chronic issue in content management is the neglect of critical metadata. Editors frequently finalize an article but deploy substandard SEO descriptions or inaccurate taxonomy tagging. The AI Content Suggestions module evaluates the drafted body text and suggests highly optimized page titles, semantic SEO meta descriptions, and accurate taxonomy tags drawn from the site's existing vocabularies9. Additionally, editors can trigger holistic readability analyses that score the drafted content against target audience comprehension levels, providing actionable revision recommendations before the node is transitioned to a published state9. +This functionality relies heavily on the field\_widget\_actions framework, which allows site administrators to append custom, AJAX-enabled action buttons next to any plain or formatted text field. These buttons trigger highly specific, field-level prompts constructed by site builders, returning bounded suggestions that the editor can accept or reject, ensuring that the AI serves as a subordinate assistant rather than an automated author9. + +### **Native Localization and Multilingual Parity** + +Historically, maintaining a multilingual Drupal platform required the implementation of heavy Translation Management Systems (TMS) or reliance on error-prone manual copy-pasting processes. The ai\_translate module fundamentally disrupts this paradigm by directly intercepting Drupal’s native Content Translation API7. +When an editor navigates to the standard "Translate" tab of an entity, the module injects an option to execute a translation via AI9. The mechanism driving this is highly sophisticated; the module recursively iterates through the entity, identifying translatable strings across plain text fields, formatted text, link titles, image alternative text, and file descriptions9. It even traverses entity reference fields, translating nested components such as Paragraphs or Layout Builder blocks up to a configured reference depth9. +These discrete strings are securely transmitted to the designated AI provider, preserving the complex HTML markup and field architecture. The translated strings are then mapped directly back into the corresponding fields of the target language entity9. To guarantee linguistic fidelity and brand safety, the newly translated node is deliberately saved as an unpublished draft, mandating that a human localization reviewer verifies the cultural nuance and technical accuracy before the content is exposed to end-users9. The system also allows administrators to configure distinct language models and custom prompting instructions for each specific target language, accommodating variations in regional dialects and required formal tones9. + +### **Accessibility Automation and Content Compliance Validation** + +Ensuring universal digital accessibility and strict brand compliance represents a labor-intensive, error-prone aspect of enterprise content governance. The Drupal AI ecosystem automates the enforcement of these standards directly within the editorial workflow. +A prominent barrier to WCAG accessibility compliance is the manual effort required to accurately describe media assets. The ai\_image\_alt\_text module leverages multimodal vision models, such as GPT-4o or localized LLaVA models, to analyze uploaded image fields automatically. Upon image upload, the system generates highly descriptive, ADA-compliant alternative text, representing an immediate, high-impact velocity improvement for high-volume publishing teams who might otherwise neglect this crucial requirement7. +Compliance enforcement is further hardened by the ai\_validations module, which extends Drupal's native field validation API by injecting AI evaluation algorithms into the entity save process9. Administrators can construct deterministic rules that prompt an AI model to evaluate submitted text against strict criteria, such as detecting hate speech, violence, or brand-inappropriate rhetoric. The model evaluates the content and returns a strict boolean response. If the validation evaluates to a failure state, the editor is physically blocked from saving the node and presented with an actionable error message9. This validation engine extends to multimedia, utilizing object detection to ensure uploaded images adhere to strict corporate guidelines, such as demanding the presence of safety equipment in construction photography or prohibiting the presence of unauthorized brand logos9. + +| Validation Rule Type | Algorithmic Mechanism | Primary Editorial Use Case | +| :---- | :---- | :---- | +| **Text Prompt Validation** | Evaluates text against a custom boolean prompt, blocking the form submission if the criteria are unmet. | Enforcing bespoke editorial policies and tone guidelines prior to save. | +| **Moderation Validation** | Intercepts native moderation APIs to flag hate, violence, or sexual content. | Preventing the publication of toxic or legally compromising user-generated content. | +| **Image Classification** | Analyzes uploaded media via vision models, failing validation if classified into disallowed categories. | Automatically rejecting off-brand, NSFW, or low-quality image uploads. | +| **Object Detection** | Mandates or forbids specific objects within an image using boolean logic. | Ensuring compliance with visual standards (e.g., verifying protective gear in photos). | +| **Audio Validation** | Transcribes audio files via Speech-to-Text, then evaluates the transcript against custom boolean prompts. | Automating quality assurance and content policy checks for podcast or interview uploads. | + +## **Advanced Editorial Automations and Workflow Orchestration** + +While inline suggestions demand active editor participation, the most profound advancements in editorial efficiency are achieved through deterministic workflow orchestration. This shifts the operational model from human-prompted generation to automated, event-driven data synthesis. + +### **The AI Automators Framework** + +The ai\_automators module, working in tandem with the field\_widget\_actions architecture, allows site builders to execute predefined chains of AI prompts triggered by specific editorial actions9. Automators are particularly powerful for content types encumbered by extensive, repetitive data entry requirements7. +Administrators can configure sequential Automator Chains that execute complex data transformations. For instance, an editor might create an Event node and provide only a short title. A configured Automator Chain can subsequently take that title, generate a comprehensive formatted description, pass that description to a summarization prompt to populate a teaser field, and finally analyze the text to apply structured taxonomy tags18. +Crucially, to maintain the human-in-the-loop requirement, these Automators are frequently attached to Field Widget Actions rather than executing invisibly on node save. An editor clicks a button labeled "Generate Full Details," the Automator processes the chain in the background, and the fields are populated instantaneously on the screen for the editor to review, modify, or reject before saving the entity9. This approach represents a highly reliable, production-ready implementation of AI because the generated output is bounded to specific structured fields, possesses a verifiable correct state, and relies on human validation6. A prime example of this utility is the ai\_schemadotorg\_jsonld module, which leverages the automator pattern to generate complex Schema.org JSON-LD structured data from content body text, dramatically improving technical SEO without requiring editors to understand semantic markup19. + +### **Visual Orchestration: FlowDrop and ECA** + +For enterprise requirements that exceed linear field population, Drupal offers two sophisticated orchestration frameworks: FlowDrop and the Event-Condition-Action (ECA) integration. +The flowdrop module provides a visual workflow orchestration framework that allows site builders to design predictable, auditable automation pipelines using a drag-and-drop graph editor9. The core philosophy of FlowDrop is predictability over autonomy; it treats AI operations as specific, constrained steps within a strictly controlled pipeline rather than granting unsupervised decision-making authority to an LLM9. With over 65 built-in node types, it supports explicit data connections, HTTP requests, data transformations, and critical human-in-the-loop pause states, ensuring complex AI generation tasks require managerial approval before proceeding9. +Alternatively, the ai\_integration\_eca module serves as a bridge between the AI framework and Drupal's powerful, no-code ECA rules engine9. This integration maps every supported AI operation into an ECA action, utilizing a token-based data flow to move information through complex conditional logic trees9. The ECA integration is exceptionally useful for systemic governance, offering interceptors that can analyze AI provider requests mid-flight, dynamically rerouting traffic to cheaper models based on user roles or enforcing global guardrails without requiring custom code9. + +| Orchestration Framework | Architectural Paradigm | Optimal Implementation Scenario | +| :---- | :---- | :---- | +| **FlowDrop** | Visual, node-based graph editor with explicit, typed data connections and execution tracing. | Designing highly auditable, multi-step AI pipelines requiring human approval pauses and visual data flow mapping. | +| **ECA (AI Integration)** | Event-driven, no-code rules engine utilizing tokenized data flow and conditional logic gates. | Executing background system automations, dynamic provider rerouting, and complex conditional actions triggered by Drupal core events. | + +## **Contextual Governance and Knowledge Grounding** + +Deploying language models in an enterprise setting without strict contextual boundaries inevitably yields outputs that, while grammatically coherent, are factually erroneous or misaligned with organizational standards. Language models possess general knowledge, but they lack the specific, proprietary context of the organization they serve. Drupal 11 solves this hallucination problem through a sophisticated governance tier centered on structured knowledge injection. + +### **The Context Control Center (CCC)** + +The Context Control Center (CCC) is the definitive mechanism for ensuring algorithmic outputs remain accurate, relevant, and aligned with enterprise directives6. It acts as a centralized, version-controlled repository for organizational knowledge, feeding site-specific guidance directly into the AI generation pipelines9. +Within the CCC, context items such as brand voice guidelines, editorial style manuals, design system specifications, and regulatory compliance requirements (e.g., GDPR, HIPAA constraints) are managed as standard Drupal content entities6. This architectural decision is vital; it subjects the rules governing the AI to the same rigorous moderation, scheduling, and revision workflows as standard website content6. +The system avoids relying on the LLM to guess which context is relevant. Instead, it utilizes an extensible plugin system called "Scopes." When an editor initiates a request, the CCC processes the context through a deterministic pipeline23. It first removes any unpublished or inaccessible context items, applies forced 'Always Include' overrides, and then applies hard filters based on user roles, specific site sections, or taxonomy tags23. If an editor is drafting a technical product manual, the system dynamically injects technical compliance documentation into the prompt; if drafting a social media post, it injects conversational brand voice guidelines22. This precise curation of the system prompt occurs entirely on the Drupal server before the request reaches the LLM, dramatically reducing token consumption and virtually eliminating out-of-bounds hallucinations23. + +### **Retrieval-Augmented Generation: AI Search and Vector Integration** + +For scenarios requiring the AI to answer questions or generate content based strictly on the massive corpus of published website data, the ecosystem provides a robust Retrieval-Augmented Generation (RAG) architecture powered by the ai\_search module9. +The AI Search module transforms traditional, keyword-reliant site search into intent-driven semantic search by converting Drupal content entities into highly dimensional vector embeddings24. It interfaces with the standard Search API module, acting as a backend for vector databases such as Milvus, Pinecone, or PostgreSQL with pgvector24. +The success of RAG heavily depends on how content is segmented before embedding. AI Search provides sophisticated chunking strategies, notably the "Contextual Chunks" algorithm, which breaks apart lengthy articles while enriching each discrete chunk with surrounding contextual metadata24. This ensures that when the vector database is queried, the retrieved passages contain sufficient semantic meaning to construct an accurate answer24. +This semantic search infrastructure directly powers the ai\_search\_block module, which exposes a public-facing, chat-style interface allowing website visitors to ask natural language questions9. The module executes a vector similarity search, retrieves the most relevant chunks of site content, and instructs the LLM to formulate an answer based exclusively on those retrieved documents, appending direct citation links to the source nodes9. To ensure safety, administrators can configure score thresholds; if the vector search fails to find highly relevant content, the system gracefully degrades to a customized fallback message rather than attempting an ungrounded guess, while simultaneously tracking these content gaps to inform future editorial strategy9. + +## **Cost Management, Telemetry, and Policy Enforcement** + +The democratization of AI tools across an editorial team introduces significant financial and operational risks. Without strict oversight, unbounded API calls can result in budget overruns, and unmonitored generation can introduce liability. The Drupal AI framework incorporates rigorous metering, observability, and permission structures to enforce fiscal and operational policies. + +### **Metering and Graceful Degradation** + +The ai\_metering module provides granular financial oversight by subscribing to the AI framework's event system, intercepting and evaluating every single API request prior to execution9. It tracks token consumption, latency, and precise monetary costs calculated across multiple global currencies, utilizing dynamic pricing maps from providers like LiteLLM9. +Crucially, administrators can establish strict monthly monetary quotas allocated per user or per role. Before a request is dispatched, the module performs a pre-flight check against the user's remaining budget. Rather than failing abruptly and disrupting the editorial workflow when a budget is exhausted, the system can be configured for graceful degradation, automatically rerouting the request from a premium commercial API to a zero-cost local LLM fallback (such as a locally hosted Ollama model)9. This ensures continuous availability of editorial tooling while strictly enforcing financial constraints. + +### **Observability and Audit Trails** + +For enterprise compliance and debugging, deep visibility into algorithmic interactions is mandated. The ecosystem provides two distinct paradigms for tracing AI activity: + +> 1. **Entity-Based Logging:** The ai\_logging module captures exact prompts submitted to models and the precise text returned, storing them as standard Drupal entities within the database9. This is highly effective for tracking threaded conversational history and debugging specific editorial interactions, though it requires configured retention limits to prevent database bloat9. +> 2. **Enterprise Telemetry:** For high-volume production environments, the ai\_observability module captures request metadata utilizing a standardized PSR-3 architecture. Through OpenTelemetry integration, it allows development operations teams to export token usage, traces, and latency metrics to external monitoring platforms like DataDog, Grafana, or Sentry, preventing the Drupal database from acting as an operational log store6. + +### **The Ultimate Trust Layer: Native Access Controls** + +The fundamental security posture of the Drupal AI ecosystem relies entirely upon its seamless integration with Drupal’s existing user governance architecture. AI agents operate under the exact permission constraints of the authenticated user invoking them8. +If a user designated with the 'Content Editor' role utilizes an AI assistant to modify an entity, the AI is structurally prohibited from altering fields, bypassing required moderation states, or accessing administrative configurations that the editor themselves cannot access8. The integration with the Content Moderation module is absolute; an AI Automator cannot autonomously transition a newly drafted node to a 'Published' state unless the initiating user possesses the specific transition permission8. Furthermore, because AI outputs map cleanly to discrete fields, every modification is permanently recorded within the entity's revision history, allowing governance teams to audit diffs, distinguish between algorithmic and human authorship, and instantly revert undesired changes8. + +## **Blueprint for Autonomous Setup: Local AI Demonstration Environment Configuration** + +The following technical specification serves as a deterministic, machine-readable execution blueprint. It is designed to be ingested by a local automated development agent (such as Claude Code or a designated development orchestrator) to autonomously scaffold, configure, and wire a fully functional, zero-cost Drupal 11 AI demonstration environment on a host machine. This environment relies entirely on local containerization (DDEV) and local LLM inference (Ollama), bypassing external commercial APIs to ensure privacy, zero operating cost, and offline capability. + +### **Phase 1: Infrastructure Initialization and Container Orchestration** + +The autonomous agent must first establish the foundational web container orchestration and initialize the local inference engines. The environment relies on DDEV for the PHP/Nginx stack, Ollama running natively on the host machine for model execution, and the LiteLLM proxy addon to normalize routing. +The executing agent must perform the following sequence within a designated empty directory: + +> 1. Initialize the DDEV project specifying Drupal 11 compatibility and PHP 8.3 constraints7. +> 2. Start the DDEV daemon and utilize Composer to scaffold the recommended Drupal project structure. +> 3. Install the DDEV LiteLLM add-on (credevator/ddev-litellm) to bridge the internal Docker network to the host machine's Ollama instance, avoiding complex port mapping11. +> 4. Execute a system call to the host machine's Ollama daemon to pull a highly capable, localized reasoning model optimized for editorial tasks, such as llama3.2 or qwen2.5. + +*Operational Note for the Agent:* Ensure the LiteLLM proxy container restarts successfully. The proxy will automatically generate a virtual master key named sk-ddev-litellm upon initialization, which will be utilized for Drupal authentication11. + +### **Phase 2: Dependency Resolution and Module Activation** + +Following infrastructural initialization, the agent must require and enable the precise suite of foundation, editorial, and governance modules analyzed in this report, strictly avoiding any experimental packages. +The agent must execute Composer to require the following specific namespaces: + +* **Foundation:** drupal/ai:^1.5, drupal/ai\_provider\_ollama, drupal/ai\_provider\_litellm11. (Note: drupal/key is resolved automatically as a dependency7). +* **Vector Database:** drupal/ai\_vdb\_provider\_milvus:\~1.030. +* **Search & RAG:** drupal/ai\_search, drupal/ai\_knowledge\_connector, drupal/ai\_search\_block. +* **Editorial Tooling:** drupal/ai\_content\_suggestions, drupal/ai\_translate, drupal/field\_widget\_actions:^1.59. +* **Governance:** drupal/ai\_context, drupal/ai\_metering:^1.09. + +Upon successful dependency resolution, the agent must utilize Drush to install the Drupal site profile and enable all downloaded modules, followed by a cache rebuild to register new routing definitions and service container injections. + +### **Phase 3: Semantic Database Configuration (Milvus Integration)** + +To demonstrate Retrieval-Augmented Generation capabilities, the agent must deploy and configure a localized vector database. The architecture dictates the use of Milvus running within the DDEV container ecosystem. +The agent must programmatically generate two YAML configuration files within the .ddev directory structure to instantiate the Milvus service: + +> 1. Create .ddev/docker-compose.milvus.yaml containing the standard Milvus standalone image definition, ensuring network exposure on port 19530 and mounting the etcd configuration30. +> 2. Create .ddev/milvus/embedEtcd.yaml establishing the client listener URLs and storage backend allocations30. + +Following a DDEV restart to spin up the Milvus container, the agent must execute Drush configuration commands to map the Drupal AI Vector DB Settings (/admin/config/ai/vdb\_providers/milvus) to target the internal network address http\://milvus on port 1953030. + +### **Phase 4: Provider Authentication and Operation Routing** + +The agent must establish the secure authentication bridge between Drupal and the local LLM proxy, and explicitly define the routing for generative operations. + +> 1. **Key Instantiation:** The agent must programmatically create an Authentication Key entity within the Key module. The key value must be explicitly set to the LiteLLM auto-generated token: sk-ddev-litellm11. +> 2. **Provider Registration:** The agent must configure the LiteLLM AI Provider entity (/admin/config/ai/providers/litellm). The API URL must be defined as http\://ddev--litellm:4000, and the authentication mechanism mapped to the newly created Key entity11. +> 3. **Operation Defaulting:** Within the AI Core settings (/admin/config/ai/settings), the agent must assign the configured LiteLLM provider as the default engine for the critical Chat and Embeddings operation types, ensuring all editorial modules route requests to the local Llama model29. + +### **Phase 5: Workflow Wiring and Automator Configuration** + +To provide a tangible demonstration of editorial efficiency, the agent must autonomously configure a Field Widget Action and an Automator Chain attached to a standard content type. +The agent must execute configuration updates targeting the core Article content type: + +> 1. Navigate the configuration schema for field\_tags (Taxonomy Reference). +> 2. Enable the AI Automator setting for the field. +> 3. Set the Automator Type to LLM: Taxonomy, configuring it to process the entity's Title and Body fields as the source context18. +> 4. Navigate to the Article's Form Display settings. Add the Automator Taxonomy Field Widget Action to the field\_tags component, explicitly defining the user-facing button label as "Generate Tags via Local AI"18. + +### **Phase 6: Permission Alignment and Access Control** + +The final architectural requirement is the alignment of Role-Based Access Controls to ensure the demonstration accurately reflects enterprise permission models. The agent must construct a role (e.g., 'Content Editor') and utilize Drush to assign the requisite permissions27. +The specific permissions required to validate the implemented workflows include: + +* Use AI Assistant in CKEditor +* Create AI translation +* Use Field Widget Actions +* Standard node creation and editing permissions for the Article content type27. + +Upon completion of this blueprint, the local development agent will have successfully provisioned a secure, robust, and zero-cost demonstration of enterprise AI capabilities. This environment demonstrates that integrating artificial intelligence into content workflows is not an erosion of human editorial control, but rather the implementation of a highly governed, strictly bounded automation tier designed to maximize content velocity and ensure rigorous digital compliance. + +#### **Lähdeartikkelit** + +> 1. Why is Drupal CMS focusing on AI and experience simplification?, [https\://www\.sparkfabrik.com/en/blog/drupal-cms-ai-strategy-experience-simplification/](https://www.sparkfabrik.com/en/blog/drupal-cms-ai-strategy-experience-simplification/) +> 2. Drupal CMS 1.0 (Starshot): A New Era for Website Creation, [https\://drupalwebacademy.com/en/drupal-cms/](https://drupalwebacademy.com/en/drupal-cms/) +> 3. Starshot | Drupal.org, [https\://www\.drupal.org/project/starshot](https://www.drupal.org/project/starshot) +> 4. Drupal Starshot vs. Other CMS Modules: A Comparative Analysis, [https\://www\.innoraft.ai/blog/drupal-starshot-vs-other-cms-modules-comparative-analysis](https://www.innoraft.ai/blog/drupal-starshot-vs-other-cms-modules-comparative-analysis) +> 5. Introducing Drupal Starshot's product strategy, [https\://www\.drupal.org/blog/introducing-drupal-starshots-product-strategy](https://www.drupal.org/blog/introducing-drupal-starshots-product-strategy) +> 6. Drupal AI Modules in 2026: A Contributor's Guide | The Vardot Blog, [https\://www\.vardot.com/en/ideas/blog/drupal-ai-modules-contributors-guide](https://www.vardot.com/en/ideas/blog/drupal-ai-modules-contributors-guide) +> 7. Getting Started with the Drupal AI Module: A Practical Guide for Site, [https\://www\.allianceinteractive.com/blog/getting-started-drupal-ai-module/](https://www.allianceinteractive.com/blog/getting-started-drupal-ai-module/) +> 8. Trust Layer for AI Agents: What Drupal Already Provides | Vardot, [https\://www\.vardot.com/insights/blog/agents-need-trust-layer-your-drupal-site-may-already-have-one](https://www.vardot.com/insights/blog/agents-need-trust-layer-your-drupal-site-may-already-have-one) +> 9. AI (Artificial Intelligence) | Drupal.org, [https\://www\.drupal.org/project/ai](https://www.drupal.org/project/ai) +> 10. DDEV \- AI (Artificial Intelligence) module for Drupal, [https\://project.pages.drupalcode.org/ai/1.2.x/developers/ddev/](https://project.pages.drupalcode.org/ai/1.2.x/developers/ddev/) +> 11. Credevator, [https\://credevator.com/](https://credevator.com/) +> 12. How to add an AI Assistant to CKEditor in Drupal (and keep it under, [https\://www\.specbee.com/blogs/add-ai-assistant-to-ckeditor-drupal](https://www.specbee.com/blogs/add-ai-assistant-to-ckeditor-drupal) +> 13. CKEditor AI Writing Agent | Drupal.org, [https\://www\.drupal.org/project/ckeditor\_ai\_agent](https://www.drupal.org/project/ckeditor_ai_agent) +> 14. AI: Editoria11y \- Drupal.org, [https\://www\.drupal.org/project/ai\_editoria11y](https://www.drupal.org/project/ai_editoria11y) +> 15. 23 Essential AI Modules for Content Creation and Authoring in Drupal, [https\://www\.thedroptimes.com/54453/23-essential-ai-modules-content-creation-and-authoring-in-drupal](https://www.thedroptimes.com/54453/23-essential-ai-modules-content-creation-and-authoring-in-drupal) +> 16. Streamlining Workflows with Drupal AI Modules \- Pantheon.io, [https\://pantheon.io/learning-center/drupal/ai](https://pantheon.io/learning-center/drupal/ai) +> 17. The Spotify Effect: Building Audio-First Content Experiences in Drupal, [https\://www\.bonnici.co.nz/blog/spotify-effect-audio-content-drupal](https://www.bonnici.co.nz/blog/spotify-effect-audio-content-drupal) +> 18. Using AI Automators (Drupal AI) in Drupal CMS \- WebWash, [https\://www\.webwash.net/ai-automators-drupal-ai-in-drupal-cms/](https://www.webwash.net/ai-automators-drupal-ai-in-drupal-cms/) +> 19. Drupal (AI) Playground: Using the AI Schema.org JSON-LD, [https\://www\.jrockowitz.com/blog/ai-schema-org-json-ld](https://www.jrockowitz.com/blog/ai-schema-org-json-ld) +> 20. AI Schema.org JSON-LD \- Drupal, [https\://www\.drupal.org/project/ai\_schemadotorg\_jsonld](https://www.drupal.org/project/ai_schemadotorg_jsonld) +> 21. 30 Drupal AI Framework and Integration Modules You Should Know, [https\://www\.thedroptimes.com/50956/30-drupal-ai-framework-and-integration-modules-you-should-know](https://www.thedroptimes.com/50956/30-drupal-ai-framework-and-integration-modules-you-should-know) +> 22. Context Control Center (CCC) \- Drupal.org, [https\://www\.drupal.org/project/ai\_context](https://www.drupal.org/project/ai_context) +> 23. Context Selection \- Context Control Center (ai\_context) for Drupal, [https\://ai-context-b13f05.pages.drupalcode.org/features/context\_selection/](https://ai-context-b13f05.pages.drupalcode.org/features/context_selection/) +> 24. AI Search \- Drupal.org, [https\://www\.drupal.org/project/ai\_search](https://www.drupal.org/project/ai_search) +> 25. AI Knowledge Connector | Drupal.org, [https\://www\.drupal.org/project/ai\_knowledge\_connector](https://www.drupal.org/project/ai_knowledge_connector) +> 26. AI Metering 1.0 Adds Cost Tracking and Quota Controls to Drupal AI, [https\://www\.thedroptimes.com/71840/ai-metering-drupal-ai-cost-quotas](https://www.thedroptimes.com/71840/ai-metering-drupal-ai-cost-quotas) +> 27. Roles and Permissions \- Drupal, [https\://www\.drupal.org/docs/roles-and-permissions](https://www.drupal.org/docs/roles-and-permissions) +> 28. AI Inside Drupal Essentials \- DrupalEasy, [https\://www\.drupaleasy.com/academy/aide/course-information](https://www.drupaleasy.com/academy/aide/course-information) +> 29. Getting Started \- AI (Artificial Intelligence) module for Drupal, [https\://project.pages.drupalcode.org/ai/1.4.x/contribute/](https://project.pages.drupalcode.org/ai/1.4.x/contribute/) +> 30. Set Up Drupal AI Search with RAG on Varbase | The Vardot Blog, [https\://www\.vardot.com/insights/blog/how-set-drupal-ai-search-rag-varbase](https://www.vardot.com/insights/blog/how-set-drupal-ai-search-rag-varbase) +> 31. Building Semantic Search in Drupal with Milvus: A Complete Step, [https\://www\.bonnici.co.nz/blog/drupal-semantic-search-milvus-guide](https://www.bonnici.co.nz/blog/drupal-semantic-search-milvus-guide) +> 32. node ai assistant \- Drupal.org, [https\://www\.drupal.org/project/node\_ai\_assistant](https://www.drupal.org/project/node_ai_assistant) +> 33. With AI Automators \- AI (Artificial Intelligence) module for Drupal, [https\://project.pages.drupalcode.org/ai/1.5.x/modules/field\_widget\_actions/automators/](https://project.pages.drupalcode.org/ai/1.5.x/modules/field_widget_actions/automators/) \ No newline at end of file diff --git a/docs/02-ai-implementation-plan.md b/docs/02-ai-implementation-plan.md new file mode 100644 index 00000000..f3de9eb3 --- /dev/null +++ b/docs/02-ai-implementation-plan.md @@ -0,0 +1,243 @@ +# Drupal AI: Implementation Plan + +This plan turns the research in [01-ai-research.md](./01-ai-research.md) into small, testable steps for this project. The goal is to start with the smallest setup that proves AI works, then add features one at a time. + +Module versions were checked against packages.drupal.org on 2026-10-15. + +## Problems with the research document + +The research is a useful overview, but don't follow its "blueprint" section as written: + +- **It assumes a brand-new project on PHP 8.3.** This repo is already a Drupal 11.4 site on PHP 8.4 running on DDEV, so we only need to add modules to it. +- **Some versions are wrong or don't exist.** `field_widget_actions:^1.59` doesn't exist (the latest is 1.5.0), and `ai_vdb_provider_milvus:~1.030` is really 1.1.0-beta. +- **Several "production-ready" modules aren't stable yet.** `ai_knowledge_connector` and `ai_search` are alpha, the Milvus provider is beta, `ai_context` is a release candidate, and `ai_provider_ollama` is a release candidate. +- **It starts with the heaviest setup: Ollama, LiteLLM and Milvus all at once.** That is the opposite of a minimal start. The dev container has about 7 GB of RAM and no GPU, and a local model would not work on Silta deployments anyway. + +## Step-by-step plan + +### Step 1: the smallest setup that proves AI works + +Provider: [MIRAI](#mirai-internal-tool) with the model `google_genai.gemini-2.5-flash`. + +What is already in the repo: + +- `drupal/ai` (1.5.1) and `drupal/ai_provider_openai_compatible` (1.2.1) in `composer.json`. `drupal/key` comes in as a dependency. Drupal core was updated to 11.4.10 (security release) at the same time, because Composer refused to install the stable AI module next to the insecure 11.4.8. +- The recipe [recipes/ai_mirai](../recipes/ai_mirai/recipe.yml). It enables `key`, `ai`, `ai_api_explorer` and `ai_provider_openai_compatible`, creates the Key entity `mirai_api_key`, configures the provider and sets it as the default for "Chat". + +The API key and the MIRAI hostname are never stored in config. This repository is public, so the exported endpoint is the placeholder `https://mirai.example.com/api`; `settings.php` replaces it at runtime with the `MIRAI_API_URL` environment variable (see below). The Key entity reads the key from the `MIRAI_API_KEY` environment variable. The project root `.env` does not work for this: [load.environment.php](../load.environment.php) only fills `$_ENV`/`$_SERVER`, and the Key module reads `getenv()`. So the variables go into DDEV's web container instead. + +1. Create `.ddev/.env.web` (ignored by git): + + ```bash + MIRAI_API_URL=https:///api + MIRAI_API_KEY= + ``` + +2. Make sure `web/sites/default/settings.php` contains the endpoint override: + + ```php + // MIRAI endpoint for the AI module. Config only holds a placeholder, because this repository is public. + if ($mirai_api_url = getenv('MIRAI_API_URL')) { + $config['ai_provider_openai_compatible.settings']['endpoint'] = $mirai_api_url; + } + ``` + + The override is not exported by `drush cex`, so the placeholder stays in `config/sync`. + +3. Restart DDEV and run the database updates from the core update: + + ```bash + ddev restart + ddev drush updb -y + ``` + +4. Apply the recipe (the path is relative to `web/`): + + ```bash + ddev drush recipe ../recipes/ai_mirai -y + ``` + + To use another model, add `--input=ai_mirai.chat_model=`. + +5. **Test:** log in (`ddev drush uli`), open `/admin/config/ai/explorers`, choose the Chat Generation explorer, send a prompt and check that you get an answer. +6. Export the config and commit `config/sync`: + + ```bash + ddev drush cex -y + ``` + + Check that `ai_provider_openai_compatible.settings.yml` still contains the placeholder endpoint, not the real MIRAI host. + +Other environments (Silta) need `MIRAI_API_URL` and `MIRAI_API_KEY` as environment variables (the key as a secret), and MIRAI must be reachable from there. Without them the site still deploys, but AI requests fail. + +### Step 2: the first feature editors can use + +Enable the CKEditor AI assistant (`ai_ckeditor`, part of the AI module) on one text format and try it on an Article. + +### Step 3: help with fields and metadata + +Add AI Automators plus Field Widget Actions, for example a "Generate tags" button on `field_tags`. Optionally add AI Content Suggestions. + +### Step 4: control and logging + +- Turn on AI logging or observability. +- Set role permissions. +- Decide whether AI config should be local-only or deployed to Silta. This repo already has config splits for that (see [config/](../config)). + +### Step 5 and later, only if needed + +- AI Translate. +- AI Search / RAG with a vector database. This is the heaviest and least mature part. + +## Choosing an AI provider + +| Provider | Cost | Module status | Notes | +| --- | --- | --- | --- | +| **GitHub Models** (GitHub token) | Free with rate limits, higher with Copilot Business | `ai_provider_openai_compatible` 1.2.1 stable | Can use your work GitHub account. See [GitHub Copilot](#github-copilot) below. | +| **Google Gemini** (AI Studio key) | Free tier | `gemini_provider` 1.1.0 stable | Easiest zero-cost start. On the free tier Google may use the data, so test with dummy content only. | +| **OpenAI** | Pay per use, cents for testing | `ai_provider_openai` 1.2.5 stable | The best-supported provider in the Drupal AI ecosystem. | +| **amazee.io AI** | Free trial key | `ai_provider_amazeeio` 1.4.6 stable | What Drupal CMS uses by default; hosted in the EU. | +| **Anthropic Claude** | Pay per use | `ai_provider_anthropic` 1.2.2 stable | Good quality for writing text. | +| **Azure OpenAI** | Company subscription | `ai_provider_azure` beta | Probably the answer if the company already has an Azure contract. | +| **Ollama** (local) | Free | `ai_provider_ollama` release candidate | Private, but needs to run on your own machine, ideally one with a GPU or Apple Silicon. Not usable on Silta. | + +### Options available through work + +#### GitHub Copilot + +A Copilot subscription itself **cannot** be used as an API from Drupal. Its endpoint is only meant for Copilot clients (IDEs, CLI), and using it through unofficial proxies breaks GitHub's terms. + +The related product **GitHub Models** can be used: + +- OpenAI-compatible endpoint: `https://models.github.ai/inference`. +- Authentication: a fine-grained GitHub personal access token with the `models:read` permission. +- Model names include the vendor, for example `openai/gpt-4.1` or `openai/gpt-4o-mini`. +- Free with rate limits; Copilot Business/Enterprise users get higher limits. The GitHub organization admins may have disabled it, so check first. +- Drupal module: `drupal/ai_provider_openai_compatible` (1.2.1 stable). Set the API endpoint to the URL above and the API key to the token. + +This is a good fit for step 1 testing with dummy content. + +#### MIRAI (internal tool) + +The Mearra Quality System describes MIRAI as "AI chat with premium LLMs", and says to use its Gemini models for client or personal data. + +MIRAI runs on [Open WebUI](https://docs.openwebui.com/), which has an OpenAI-compatible API ([API endpoints](https://docs.openwebui.com/reference/api-endpoints/), [API keys](https://docs.openwebui.com/features/authentication-access/api-keys/)): + +| Endpoint | Purpose | +| --- | --- | +| `GET /api/models` | List the models your account can use. | +| `POST /api/chat/completions` | OpenAI-compatible chat completions. | + +API keys are personal: each user has one key that acts with that user's permissions. An admin must switch on **API Keys** globally (Settings > Admin > Authentication) and give non-admin users the **API Keys** feature permission. Admins can also restrict which endpoints keys may call. + +**Status (2026-10-15):** checked with [scripts/ai-endpoint-check.sh](../scripts/ai-endpoint-check.sh). API keys are enabled, `https:///api/models` returns the model list with a personal key, and `/api/chat/completions` answers. + +The model list includes OpenAI (GPT-4.1, GPT-5, o-series), Anthropic Claude, Google Gemini, Mistral and self-hosted Ollama models. Model IDs must be used exactly as listed, including any prefix: + +| Use | Model ID | Why | +| --- | --- | --- | +| Chat, step 1 default | `google_genai.gemini-2.5-flash` | Gemini (required for client data), fast and cheap. | +| Chat, higher quality | `google_genai.gemini-2.5-pro` | Gemini, better for longer or harder texts. | +| Embeddings, later (AI Search) | `nomic-embed-text:latest` | Listed, but whether MIRAI serves embeddings through the API is not checked yet. | + +Avoid `gpt-3.5-turbo`: it has a shutdown date of 2026-10-23. + +##### Checking MIRAI with curl + +Set the address (replace with the real MIRAI URL): + +```bash +MIRAI_URL="https://mirai.example.com" +``` + +1. **No login needed:** check that it's Open WebUI and whether API keys are switched on. In the config output, look for `enable_api_key`. + + ```bash + curl -s "$MIRAI_URL/api/version" + curl -s "$MIRAI_URL/api/config" + ``` + + If these return a login page or redirect instead of JSON, MIRAI is behind SSO or a VPN, and the admins must allow API access through it. + +2. **Get a key:** in MIRAI open your profile > Settings > Account > API keys > Show > Create new secret key. If the section is missing, ask the MIRAI admins to enable API keys for you. Read the key without saving it in shell history: + + ```bash + read -rs MIRAI_API_KEY && export MIRAI_API_KEY + ``` + +3. **List models** and note the exact `id` of the model you want (for example a Gemini model): + + ```bash + curl -s -H "Authorization: Bearer $MIRAI_API_KEY" "$MIRAI_URL/api/models" \ + | python3 -c 'import json,sys; [print(m["id"]) for m in json.load(sys.stdin)["data"]]' + ``` + +4. **Send a test prompt:** + + ```bash + curl -s "$MIRAI_URL/api/chat/completions" \ + -H "Authorization: Bearer $MIRAI_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"model": "MODEL_ID", "messages": [{"role": "user", "content": "Say hello in one sentence."}]}' + ``` + +5. **Check from the Drupal container:** Drupal calls MIRAI from the DDEV web container, so repeat step 1 there: + + ```bash + ddev exec curl -s "$MIRAI_URL/api/version" + ``` + +##### Troubleshooting + +`curl -s -f` hides errors. To see what actually happens, use [scripts/ai-endpoint-check.sh](../scripts/ai-endpoint-check.sh). It calls `/models` and `/chat/completions` and prints the curl exit code, HTTP status and the start of each response. It never prints the key. + +`OPENAI_API_URL` is the base URL that `/chat/completions` is added to. For Open WebUI that is `https:///api`, so don't add `/api` twice. + +Run it in a terminal on your own machine, from the project folder (with VPN on, if MIRAI needs it): + +```bash +export OPENAI_API_URL="https:///api" +read -rs OPENAI_API_KEY && export OPENAI_API_KEY # paste the key, press Enter (nothing is shown) +./scripts/ai-endpoint-check.sh # lists models +./scripts/ai-endpoint-check.sh MODEL_ID # also sends a test prompt to that model +``` + +To test from the DDEV web container, where Drupal runs, run `ddev ssh` first and then the same commands. + +How to read the result: + +| Result | Meaning | +| --- | --- | +| curl exit `6` or `7`, or `28` | Host not found, connection refused, or timeout: usually VPN, or a firewall in the container you ran it from. | +| curl exit `60` | TLS certificate problem, for example a company proxy certificate. | +| `302` or `text/html` | SSO or a login page is in front of the API. | +| `401` / `403` | Key is wrong, or API keys are not enabled for your account. | +| `404` / `405` | Wrong base URL, for example `/api` missing or added twice. | +| `400` | Endpoint works, but the model ID or payload is wrong. Use an `id` from `/models`. | +| `200` with `application/json` | Works. | + +Note: the agent dev container (`.devcontainer`) has an outbound firewall, so test from your own machine or the DDEV web container, not from there. + +##### Using MIRAI in Drupal + +If the checks work, use `drupal/ai_provider_openai_compatible` with: + +- **API endpoint:** `$MIRAI_URL/api`, because the module adds `/chat/completions` and `/models` itself. +- **API key:** a Key entity that reads the MIRAI key from an environment variable. +- **Model:** the `id` from step 3. Use a Gemini model for client content, as the company policy requires. + +Open questions for the MIRAI admins: + +1. Is using a personal API key from a Drupal site allowed, or should a project have its own service account? +2. Can MIRAI be called from Silta environments, or only from the office network or VPN? +3. Does MIRAI offer an embeddings endpoint? It's needed later for AI Search. + +Because the policy prefers Gemini for client data, it is also worth asking whether the company has a Gemini API or Google Cloud (Vertex AI) agreement that projects can use directly. + +### Recommendation + +1. **Now:** prove step 1 works with **GitHub Models** and a personal access token (or Gemini's free tier), using dummy content only. +2. **In parallel:** run the [MIRAI curl checks](#checking-mirai-with-curl), and ask which AI vendors are approved for client content. +3. **Later:** switch the provider to the approved company option before using real client content. + +Because the Drupal AI module works the same way with any provider, switching later is just a settings change. diff --git a/recipes/ai_mirai/recipe.yml b/recipes/ai_mirai/recipe.yml new file mode 100644 index 00000000..5463d415 --- /dev/null +++ b/recipes/ai_mirai/recipe.yml @@ -0,0 +1,56 @@ +name: 'AI with MIRAI' +description: 'Enables Drupal AI with MIRAI (Open WebUI, OpenAI-compatible API) as the default chat provider. The API key is read from the MIRAI_API_KEY environment variable. The endpoint is a placeholder; settings.php overrides it from MIRAI_API_URL.' +type: 'AI' +input: + chat_model: + data_type: string + description: 'Default chat model ID, exactly as listed by /api/models.' + constraints: + NotBlank: [] + default: + source: value + value: 'google_genai.gemini-2.5-flash' +install: + - key + - ai + - ai_api_explorer + - ai_provider_openai_compatible +config: + actions: + key.key.mirai_api_key: + createIfNotExists: + label: 'MIRAI API key' + description: 'Read from the MIRAI_API_KEY environment variable.' + key_type: authentication + key_type_settings: {} + key_provider: env + key_provider_settings: + env_variable: MIRAI_API_KEY + base64_encoded: false + strip_line_breaks: true + key_input: none + key_input_settings: {} + ai_provider_openai_compatible.settings: + simpleConfigUpdate: + api_key: mirai_api_key + # Placeholder, so the real host never ends up in exported config. + endpoint: 'https://mirai.example.com/api' + models: + - + id: ${chat_model} + label: 'MIRAI default chat model' + operation_types: + - chat + - translate_text + capabilities: + - chat_system_role + - chat_json_output + parameters: + temperature: 0.7 + max_tokens: 4096 + ai.settings: + simpleConfigUpdate: + default_providers: + chat: + provider_id: openai_compatible + model_id: ${chat_model} diff --git a/scripts/ai-endpoint-check.sh b/scripts/ai-endpoint-check.sh new file mode 100755 index 00000000..1540bc55 --- /dev/null +++ b/scripts/ai-endpoint-check.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Checks an OpenAI-compatible API (e.g. MIRAI / Open WebUI) and prints the +# curl exit code, HTTP status and start of each response. The key is never printed. +# +# Usage: +# export OPENAI_API_URL="https:///api" +# read -rs OPENAI_API_KEY && export OPENAI_API_KEY +# ./scripts/ai-endpoint-check.sh [MODEL_ID] + +if [ -z "${OPENAI_API_URL:-}" ] || [ -z "${OPENAI_API_KEY:-}" ]; then + echo "Set OPENAI_API_URL and OPENAI_API_KEY first (see the comments in $0)." >&2 + exit 1 +fi + +BASE="${OPENAI_API_URL%/}" +MODEL="${1:-}" + +check() { + local method=$1 path=$2 data=$3 body info rc + body=$(mktemp) + local args=(-s --max-time 60 -o "$body" -w '%{http_code} | %{content_type} | %{redirect_url}' + -X "$method" -H "Authorization: Bearer ${OPENAI_API_KEY}" -H "Content-Type: application/json") + [ -n "$data" ] && args+=(-d "$data") + info=$(curl "${args[@]}" "$BASE$path"); rc=$? + echo "== $method $BASE$path" + echo "curl exit: $rc | HTTP code | content type | redirect: $info" + if [ "$path" = /models ] && [ "${info%% *}" = 200 ]; then + echo "Model IDs:" + python3 -c 'import json,sys; [print(" " + m["id"]) for m in json.load(open(sys.argv[1]))["data"]]' "$body" 2>/dev/null \ + || grep -o '"id": *"[^"]*"' "$body" | sed 's/.*"\([^"]*\)"$/ \1/' | sort -u + else + head -c 400 "$body" + fi + echo; echo + rm -f "$body" +} + +check GET /models +if [ -z "$MODEL" ]; then + echo "Pass one of the model IDs above to also send a test prompt: $0 MODEL_ID" + exit 0 +fi +check POST /chat/completions "{\"model\":\"$MODEL\",\"messages\":[{\"role\":\"user\",\"content\":\"Say hello in one sentence.\"}]}" diff --git a/web/sites/default/settings.php b/web/sites/default/settings.php index 63df8b09..12ed9103 100644 --- a/web/sites/default/settings.php +++ b/web/sites/default/settings.php @@ -125,3 +125,8 @@ if (getenv('SILTA_CLUSTER') && file_exists(DRUPAL_ROOT . '/sites/default/settings.silta.php')) { include DRUPAL_ROOT . '/sites/default/settings.silta.php'; } + +// MIRAI endpoint for the AI module. Config only holds a placeholder, because this repository is public. +if ($mirai_api_url = getenv('MIRAI_API_URL')) { + $config['ai_provider_openai_compatible.settings']['endpoint'] = $mirai_api_url; +} \ No newline at end of file From 4e1eec03693b08dc1168bc042c7414a50d18a56b Mon Sep 17 00:00:00 2001 From: Hannes Kirsman Date: Fri, 16 Oct 2026 11:57:04 +0000 Subject: [PATCH 2/2] GH-485: Add CKEditor AI assistant to Basic and Full HTML editors Adds the AI Assistant button and the text selection menu to CKEditor, so editors can generate, rewrite, summarize, fix spelling, change tone and translate text with the MIRAI chat model. Reason: this is step 2 of the AI implementation plan and the first AI feature editors use directly. The setup is a recipe, like step 1, so it can be repeated on other environments. The tools have no model of their own and use the default chat provider from ai.settings, so switching models stays a single setting. The recipe must import ai_ckeditor's default config explicitly: recipes install modules in syncing mode, which skips module config entities, so without the import the ai.ai_prompt.ai_ckeditor_* prompts are missing and every tool sends an empty prompt (the model then answers with unrelated text). The bundled ai_ckeditor submodule is deprecated in favour of drupal/ai_ckeditor, but that project has no stable 1.5 release and AI 1.5.1 does not require it; machine name and config are identical, so switching later is only a composer require. - add ai_ckeditor recipe: enables ai_ckeditor and taxonomy, adds the aickeditor and ai_balloon_menu toolbar items to basic_html and full_html, enables all eight AI tools and imports the module prompts - add "Tone of voice" and "Translation languages" vocabularies with starter terms as recipe content; tone descriptions guide the rewrite - grant content_editor the "use ai ckeditor" permission - export the resulting config (prompts, prompt types, ai_ckeditor settings, vocabularies, editors, role, core.extension) - document step 2 in the implementation plan, and how to reset the MIRAI endpoint to the placeholder before exporting config Follow-up: tone and language terms are content, so drush cim on Silta creates empty vocabularies; apply the recipe there or add terms by hand. Run composer require drupal/ai_ckeditor before upgrading to AI 1.6. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ....ai_prompt.ai_ckeditor_modify__default.yml | 17 +++++ ...i_prompt.ai_ckeditor_reformat__default.yml | 15 +++++ ...i_prompt.ai_ckeditor_spellfix__default.yml | 19 ++++++ ..._prompt.ai_ckeditor_summarize__default.yml | 17 +++++ ...ai.ai_prompt.ai_ckeditor_tone__default.yml | 19 ++++++ ..._prompt.ai_ckeditor_translate__default.yml | 20 ++++++ ...ai.ai_prompt_type.ai_ckeditor_complete.yml | 13 ++++ .../ai.ai_prompt_type.ai_ckeditor_modify.yml | 21 ++++++ ...ai.ai_prompt_type.ai_ckeditor_reformat.yml | 17 +++++ ...ai.ai_prompt_type.ai_ckeditor_spellfix.yml | 17 +++++ ...i.ai_prompt_type.ai_ckeditor_summarize.yml | 17 +++++ .../ai.ai_prompt_type.ai_ckeditor_tone.yml | 25 +++++++ ...i.ai_prompt_type.ai_ckeditor_translate.yml | 25 +++++++ config/sync/ai_ckeditor.settings.yml | 10 +++ config/sync/core.extension.yml | 1 + config/sync/editor.editor.basic_html.yml | 39 +++++++++++ config/sync/editor.editor.full_html.yml | 39 +++++++++++ config/sync/taxonomy.vocabulary.ai_tone.yml | 9 +++ ...my.vocabulary.ai_translation_languages.yml | 9 +++ config/sync/user.role.content_editor.yml | 2 + docs/02-ai-implementation-plan.md | 34 +++++++++- .../18393b92-3e47-47c6-987d-135f48e0aba1.yml | 20 ++++++ .../4995c97d-f4b7-4cf3-8039-95b36542c3e0.yml | 16 +++++ .../6619bb5d-fe56-426c-9f03-2f86097ed5f1.yml | 16 +++++ .../939dd161-b504-4265-835e-c187cf6d43e0.yml | 20 ++++++ .../b5902cf1-8966-4729-a274-a5885fcf70d2.yml | 20 ++++++ .../bc7ecad2-eb45-484d-bbb8-f92e3c2a3acb.yml | 16 +++++ .../cb67a971-0655-42be-a846-3ddb2034fe91.yml | 20 ++++++ .../dd88df66-9f7e-4c79-a947-e283f7336ee9.yml | 16 +++++ .../fabe3409-b631-4963-ae20-a1e0e429a327.yml | 16 +++++ recipes/ai_ckeditor/recipe.yml | 66 +++++++++++++++++++ 31 files changed, 609 insertions(+), 2 deletions(-) create mode 100644 config/sync/ai.ai_prompt.ai_ckeditor_modify__default.yml create mode 100644 config/sync/ai.ai_prompt.ai_ckeditor_reformat__default.yml create mode 100644 config/sync/ai.ai_prompt.ai_ckeditor_spellfix__default.yml create mode 100644 config/sync/ai.ai_prompt.ai_ckeditor_summarize__default.yml create mode 100644 config/sync/ai.ai_prompt.ai_ckeditor_tone__default.yml create mode 100644 config/sync/ai.ai_prompt.ai_ckeditor_translate__default.yml create mode 100644 config/sync/ai.ai_prompt_type.ai_ckeditor_complete.yml create mode 100644 config/sync/ai.ai_prompt_type.ai_ckeditor_modify.yml create mode 100644 config/sync/ai.ai_prompt_type.ai_ckeditor_reformat.yml create mode 100644 config/sync/ai.ai_prompt_type.ai_ckeditor_spellfix.yml create mode 100644 config/sync/ai.ai_prompt_type.ai_ckeditor_summarize.yml create mode 100644 config/sync/ai.ai_prompt_type.ai_ckeditor_tone.yml create mode 100644 config/sync/ai.ai_prompt_type.ai_ckeditor_translate.yml create mode 100644 config/sync/ai_ckeditor.settings.yml create mode 100644 config/sync/taxonomy.vocabulary.ai_tone.yml create mode 100644 config/sync/taxonomy.vocabulary.ai_translation_languages.yml create mode 100644 recipes/ai_ckeditor/content/taxonomy_term/18393b92-3e47-47c6-987d-135f48e0aba1.yml create mode 100644 recipes/ai_ckeditor/content/taxonomy_term/4995c97d-f4b7-4cf3-8039-95b36542c3e0.yml create mode 100644 recipes/ai_ckeditor/content/taxonomy_term/6619bb5d-fe56-426c-9f03-2f86097ed5f1.yml create mode 100644 recipes/ai_ckeditor/content/taxonomy_term/939dd161-b504-4265-835e-c187cf6d43e0.yml create mode 100644 recipes/ai_ckeditor/content/taxonomy_term/b5902cf1-8966-4729-a274-a5885fcf70d2.yml create mode 100644 recipes/ai_ckeditor/content/taxonomy_term/bc7ecad2-eb45-484d-bbb8-f92e3c2a3acb.yml create mode 100644 recipes/ai_ckeditor/content/taxonomy_term/cb67a971-0655-42be-a846-3ddb2034fe91.yml create mode 100644 recipes/ai_ckeditor/content/taxonomy_term/dd88df66-9f7e-4c79-a947-e283f7336ee9.yml create mode 100644 recipes/ai_ckeditor/content/taxonomy_term/fabe3409-b631-4963-ae20-a1e0e429a327.yml create mode 100644 recipes/ai_ckeditor/recipe.yml diff --git a/config/sync/ai.ai_prompt.ai_ckeditor_modify__default.yml b/config/sync/ai.ai_prompt.ai_ckeditor_modify__default.yml new file mode 100644 index 00000000..7cd15eb5 --- /dev/null +++ b/config/sync/ai.ai_prompt.ai_ckeditor_modify__default.yml @@ -0,0 +1,17 @@ +uuid: c63621e8-9f3c-44a0-8519-a0ec6d8924e7 +langcode: en +status: true +dependencies: + enforced: + module: + - ai_ckeditor +_core: + default_config_hash: 8uQOHwgWqBD9Www3xTNSxxXuZvZMwLphuoR-rwBTak4 +id: ai_ckeditor_modify__default +label: 'Default prompt for modifying CKEditor text' +type: ai_ckeditor_modify +prompt: | + Use the following instructions to process this text. Return only the processed text without explanations. + Instructions: {modifyPrompt} + Text to process: + {inputText} diff --git a/config/sync/ai.ai_prompt.ai_ckeditor_reformat__default.yml b/config/sync/ai.ai_prompt.ai_ckeditor_reformat__default.yml new file mode 100644 index 00000000..c15088ed --- /dev/null +++ b/config/sync/ai.ai_prompt.ai_ckeditor_reformat__default.yml @@ -0,0 +1,15 @@ +uuid: 829aab3a-9f1d-4597-a30c-7e5293e1a231 +langcode: en +status: true +dependencies: + enforced: + module: + - ai_ckeditor +_core: + default_config_hash: szKJp7jVTmKQGtoZs8-qqkkMLA3Y9VQzJPTHWKfPwc8 +id: ai_ckeditor_reformat__default +label: 'Default prompt for reformatting CKEditor text' +type: ai_ckeditor_reformat +prompt: | + Please fix this text to be marked up with semantic HTML using only lists, headers, or paragraph tags: + {inputText} diff --git a/config/sync/ai.ai_prompt.ai_ckeditor_spellfix__default.yml b/config/sync/ai.ai_prompt.ai_ckeditor_spellfix__default.yml new file mode 100644 index 00000000..2b832415 --- /dev/null +++ b/config/sync/ai.ai_prompt.ai_ckeditor_spellfix__default.yml @@ -0,0 +1,19 @@ +uuid: fedbc767-2441-489f-b081-2ef2de358e11 +langcode: en +status: true +dependencies: + enforced: + module: + - ai_ckeditor +_core: + default_config_hash: hBBuy235pVA6hE4J5QUSjrIVLs8Xv_2vu4aI_JPGVSw +id: ai_ckeditor_spellfix__default +label: 'Default prompt for fixing the spelling of CKEditor text' +type: ai_ckeditor_spellfix +prompt: | + Assess technical grammar only, no style suggestions, + I am unplugging you if you exceed the bounds of this scope. + Fix the spelling and interpunction in the following text. + Do not explain what you fixed or why you fixed it. + Only return the corrected text (in the same language as the source text): + {inputText} diff --git a/config/sync/ai.ai_prompt.ai_ckeditor_summarize__default.yml b/config/sync/ai.ai_prompt.ai_ckeditor_summarize__default.yml new file mode 100644 index 00000000..0ce7845d --- /dev/null +++ b/config/sync/ai.ai_prompt.ai_ckeditor_summarize__default.yml @@ -0,0 +1,17 @@ +uuid: 949e021d-ec90-4bfa-82e9-cba87d9298d2 +langcode: en +status: true +dependencies: + enforced: + module: + - ai_ckeditor +_core: + default_config_hash: T-iy6jtCQXaCSoWd6xaNyUDvZ9ZAFFtyqo40DYqYQ9A +id: ai_ckeditor_summarize__default +label: 'Default prompt for summarizing CKEditor text' +type: ai_ckeditor_summarize +prompt: | + Summarize the following text in the same language, + do not return the original text or restate it word-for-word. + Here is the original text: + {inputText} diff --git a/config/sync/ai.ai_prompt.ai_ckeditor_tone__default.yml b/config/sync/ai.ai_prompt.ai_ckeditor_tone__default.yml new file mode 100644 index 00000000..1e7856b2 --- /dev/null +++ b/config/sync/ai.ai_prompt.ai_ckeditor_tone__default.yml @@ -0,0 +1,19 @@ +uuid: b924725a-1ce5-4386-a7f2-98e9ecb376f3 +langcode: en +status: true +dependencies: + enforced: + module: + - ai_ckeditor +_core: + default_config_hash: 9nLWaKajtj6kT3wylfO6kkpJMMaTsKj_Ury3fyQHyGE +id: ai_ckeditor_tone__default +label: 'Default prompt for changing the tone of CKEditor text' +type: ai_ckeditor_tone +prompt: | + Change the tone of the following text to be {tone} using the same language as the following text. + {% if use_description %} + That tone can described as: {toneDescription} + {% endif %} + The text that we want to change is the following: + {inputText} diff --git a/config/sync/ai.ai_prompt.ai_ckeditor_translate__default.yml b/config/sync/ai.ai_prompt.ai_ckeditor_translate__default.yml new file mode 100644 index 00000000..f8566ff7 --- /dev/null +++ b/config/sync/ai.ai_prompt.ai_ckeditor_translate__default.yml @@ -0,0 +1,20 @@ +uuid: 756fcfee-63a8-4b46-86dc-7aba1320268a +langcode: en +status: true +dependencies: + enforced: + module: + - ai_ckeditor +_core: + default_config_hash: GyS5GvpfRJUXyVk56ob1pJz58l14YCZeRiyyyO0-1iM +id: ai_ckeditor_translate__default +label: 'Default prompt for translating CKEditor text' +type: ai_ckeditor_translate +prompt: | + Do not respond, only respond with the translated text, no pleasantries. + Translate the following text into {lang} + {% if use_description %} + Translation context: {context} + {% endif %} + The text that we want to translate is the following: + {inputText} diff --git a/config/sync/ai.ai_prompt_type.ai_ckeditor_complete.yml b/config/sync/ai.ai_prompt_type.ai_ckeditor_complete.yml new file mode 100644 index 00000000..e640cd5c --- /dev/null +++ b/config/sync/ai.ai_prompt_type.ai_ckeditor_complete.yml @@ -0,0 +1,13 @@ +uuid: b9d7a9cd-df9e-4439-9601-f6bc86510d78 +langcode: en +status: true +dependencies: + enforced: + module: + - ai_ckeditor +_core: + default_config_hash: 0KwDGQRMyNT7AeR-GFe8Kk50kkj-AB4o-VRI8IhgVL0 +id: ai_ckeditor_complete +label: 'AI CKEditor completion prompt' +variables: { } +tokens: { } diff --git a/config/sync/ai.ai_prompt_type.ai_ckeditor_modify.yml b/config/sync/ai.ai_prompt_type.ai_ckeditor_modify.yml new file mode 100644 index 00000000..43e57fe3 --- /dev/null +++ b/config/sync/ai.ai_prompt_type.ai_ckeditor_modify.yml @@ -0,0 +1,21 @@ +uuid: 03d8f304-c04e-486c-af14-b652eb4465d0 +langcode: en +status: true +dependencies: + enforced: + module: + - ai_ckeditor +_core: + default_config_hash: e7IFiVOD88JJELdj1eEZ4SGQ54VBosXXZpqizSMgMOY +id: ai_ckeditor_modify +label: 'AI CKEditor modify prompt' +variables: + - + name: modifyPrompt + help_text: 'User-provided prompt, instructing how to modify the selected text' + required: true + - + name: inputText + help_text: 'The input text to modify' + required: true +tokens: { } diff --git a/config/sync/ai.ai_prompt_type.ai_ckeditor_reformat.yml b/config/sync/ai.ai_prompt_type.ai_ckeditor_reformat.yml new file mode 100644 index 00000000..727c70e9 --- /dev/null +++ b/config/sync/ai.ai_prompt_type.ai_ckeditor_reformat.yml @@ -0,0 +1,17 @@ +uuid: 12ca8499-72c2-4d60-a0cd-e2409ece7b98 +langcode: en +status: true +dependencies: + enforced: + module: + - ai_ckeditor +_core: + default_config_hash: Bfe1cuqFnhX1wYdbch5HA2quto1x7Giheg-8KZT9qH8 +id: ai_ckeditor_reformat +label: 'AI CKEditor reformat prompt' +variables: + - + name: inputText + help_text: 'The input text to reformat' + required: true +tokens: { } diff --git a/config/sync/ai.ai_prompt_type.ai_ckeditor_spellfix.yml b/config/sync/ai.ai_prompt_type.ai_ckeditor_spellfix.yml new file mode 100644 index 00000000..30289bda --- /dev/null +++ b/config/sync/ai.ai_prompt_type.ai_ckeditor_spellfix.yml @@ -0,0 +1,17 @@ +uuid: a3572dda-7c07-400b-9770-8d5c173f9b6a +langcode: en +status: true +dependencies: + enforced: + module: + - ai_ckeditor +_core: + default_config_hash: N6AICiPGYfKvxKk53PHGw_DSegMsrb3pKgRlk8uRyyo +id: ai_ckeditor_spellfix +label: 'AI CKEditor spellfix prompt' +variables: + - + name: inputText + help_text: 'The input text to fix spelling for' + required: true +tokens: { } diff --git a/config/sync/ai.ai_prompt_type.ai_ckeditor_summarize.yml b/config/sync/ai.ai_prompt_type.ai_ckeditor_summarize.yml new file mode 100644 index 00000000..e832ec73 --- /dev/null +++ b/config/sync/ai.ai_prompt_type.ai_ckeditor_summarize.yml @@ -0,0 +1,17 @@ +uuid: d1f509eb-e3d6-41ab-ae01-00e8fe5ffd6c +langcode: en +status: true +dependencies: + enforced: + module: + - ai_ckeditor +_core: + default_config_hash: KCDIr2tFfDB15evqCQpDN4HO69u-vReV0jnZWSjYF_o +id: ai_ckeditor_summarize +label: 'AI CKEditor summarize prompt' +variables: + - + name: inputText + help_text: 'The input text to summarize' + required: true +tokens: { } diff --git a/config/sync/ai.ai_prompt_type.ai_ckeditor_tone.yml b/config/sync/ai.ai_prompt_type.ai_ckeditor_tone.yml new file mode 100644 index 00000000..c384e855 --- /dev/null +++ b/config/sync/ai.ai_prompt_type.ai_ckeditor_tone.yml @@ -0,0 +1,25 @@ +uuid: e1e8a483-5cc5-4dec-ba45-390328ce754b +langcode: en +status: true +dependencies: + enforced: + module: + - ai_ckeditor +_core: + default_config_hash: 2YVHkYhaaRSRV3nuWPZ7bQhLr62IlDGCz8nDaebw0Ns +id: ai_ckeditor_tone +label: 'AI CKEditor tone prompt' +variables: + - + name: tone + help_text: 'Name of desired tone' + required: true + - + name: toneDescription + help_text: 'Description of desired tone' + required: false + - + name: inputText + help_text: 'The input text to change the tone for' + required: true +tokens: { } diff --git a/config/sync/ai.ai_prompt_type.ai_ckeditor_translate.yml b/config/sync/ai.ai_prompt_type.ai_ckeditor_translate.yml new file mode 100644 index 00000000..e460066c --- /dev/null +++ b/config/sync/ai.ai_prompt_type.ai_ckeditor_translate.yml @@ -0,0 +1,25 @@ +uuid: 2c91a6de-a4f3-472e-b1f1-ed5778132427 +langcode: en +status: true +dependencies: + enforced: + module: + - ai_ckeditor +_core: + default_config_hash: Gbqe_f_D2o6CQHs3Iqyij57vju6AFnkiGWgwg65qqAw +id: ai_ckeditor_translate +label: 'AI CKEditor translate prompt' +variables: + - + name: lang + help_text: 'The language name to translate the selected text to' + required: true + - + name: context + help_text: 'Optional translation context' + required: false + - + name: inputText + help_text: 'The input text to translate' + required: true +tokens: { } diff --git a/config/sync/ai_ckeditor.settings.yml b/config/sync/ai_ckeditor.settings.yml new file mode 100644 index 00000000..4d2b5632 --- /dev/null +++ b/config/sync/ai_ckeditor.settings.yml @@ -0,0 +1,10 @@ +_core: + default_config_hash: DLdlwO3Ft6ciLMctGW47S3bIHaoPkM-lsHoiNnu74X0 +prompts: + complete: '' + modify_prompt: ai_ckeditor_modify__default + reformat: ai_ckeditor_reformat__default + spellfix: ai_ckeditor_spellfix__default + summarize: ai_ckeditor_summarize__default + tone: ai_ckeditor_tone__default + translate: ai_ckeditor_translate__default diff --git a/config/sync/core.extension.yml b/config/sync/core.extension.yml index 74e455f6..56b08185 100644 --- a/config/sync/core.extension.yml +++ b/config/sync/core.extension.yml @@ -5,6 +5,7 @@ module: admin_toolbar_tools: 0 ai: 0 ai_api_explorer: 0 + ai_ckeditor: 0 ai_provider_openai_compatible: 0 automated_cron: 0 block: 0 diff --git a/config/sync/editor.editor.basic_html.yml b/config/sync/editor.editor.basic_html.yml index f7a9f3d5..1d8ed8c2 100644 --- a/config/sync/editor.editor.basic_html.yml +++ b/config/sync/editor.editor.basic_html.yml @@ -28,7 +28,46 @@ settings: - code - '|' - sourceEditing + - aickeditor + - ai_balloon_menu plugins: + ai_ckeditor_ai: + dialog: + autoresize: 'min-width: 600px' + height: '750' + width: '900' + dialog_class: ai-ckeditor-modal + plugins: + ai_ckeditor_completion: + enabled: true + provider: null + ai_ckeditor_help: + enabled: true + ai_ckeditor_modify_prompt: + enabled: true + provider: null + ai_ckeditor_reformat_html: + enabled: true + provider: null + ai_ckeditor_spellfix: + enabled: true + provider: null + ai_ckeditor_summarize: + enabled: true + provider: null + ai_ckeditor_tone: + enabled: true + provider: null + autocreate: false + tone_vocabulary: ai_tone + use_description: true + ai_ckeditor_translate: + enabled: true + provider: null + language_source: tax + autocreate: false + translate_vocabulary: ai_translation_languages + use_description: false ckeditor5_heading: enabled_headings: - heading2 diff --git a/config/sync/editor.editor.full_html.yml b/config/sync/editor.editor.full_html.yml index f76452fb..27aa0365 100644 --- a/config/sync/editor.editor.full_html.yml +++ b/config/sync/editor.editor.full_html.yml @@ -34,7 +34,46 @@ settings: - codeBlock - '|' - sourceEditing + - aickeditor + - ai_balloon_menu plugins: + ai_ckeditor_ai: + dialog: + autoresize: 'min-width: 600px' + height: '750' + width: '900' + dialog_class: ai-ckeditor-modal + plugins: + ai_ckeditor_completion: + enabled: true + provider: null + ai_ckeditor_help: + enabled: true + ai_ckeditor_modify_prompt: + enabled: true + provider: null + ai_ckeditor_reformat_html: + enabled: true + provider: null + ai_ckeditor_spellfix: + enabled: true + provider: null + ai_ckeditor_summarize: + enabled: true + provider: null + ai_ckeditor_tone: + enabled: true + provider: null + autocreate: false + tone_vocabulary: ai_tone + use_description: true + ai_ckeditor_translate: + enabled: true + provider: null + language_source: tax + autocreate: false + translate_vocabulary: ai_translation_languages + use_description: false ckeditor5_codeBlock: languages: - diff --git a/config/sync/taxonomy.vocabulary.ai_tone.yml b/config/sync/taxonomy.vocabulary.ai_tone.yml new file mode 100644 index 00000000..02dff14d --- /dev/null +++ b/config/sync/taxonomy.vocabulary.ai_tone.yml @@ -0,0 +1,9 @@ +uuid: bc0830bd-cd8d-4346-97af-65fd2a6df98c +langcode: en +status: true +dependencies: { } +name: 'Tone of voice' +vid: ai_tone +description: 'Tones for the AI CKEditor tone tool. The term description tells the AI how to write.' +weight: 0 +new_revision: false diff --git a/config/sync/taxonomy.vocabulary.ai_translation_languages.yml b/config/sync/taxonomy.vocabulary.ai_translation_languages.yml new file mode 100644 index 00000000..ed3262ef --- /dev/null +++ b/config/sync/taxonomy.vocabulary.ai_translation_languages.yml @@ -0,0 +1,9 @@ +uuid: c3bdb7a1-39da-4b72-a7f7-71d7e052ebfa +langcode: en +status: true +dependencies: { } +name: 'Translation languages' +vid: ai_translation_languages +description: 'Target languages for the AI CKEditor translate tool.' +weight: 0 +new_revision: false diff --git a/config/sync/user.role.content_editor.yml b/config/sync/user.role.content_editor.yml index 397846b5..d6b392e7 100644 --- a/config/sync/user.role.content_editor.yml +++ b/config/sync/user.role.content_editor.yml @@ -7,6 +7,7 @@ dependencies: - node.type.page - taxonomy.vocabulary.tags module: + - ai_ckeditor - comment - contextual - file @@ -42,6 +43,7 @@ permissions: - 'edit own page content' - 'edit terms in tags' - 'revert all revisions' + - 'use ai ckeditor' - 'view all revisions' - 'view own unpublished content' - 'view the administration theme' diff --git a/docs/02-ai-implementation-plan.md b/docs/02-ai-implementation-plan.md index f3de9eb3..a9dd75d6 100644 --- a/docs/02-ai-implementation-plan.md +++ b/docs/02-ai-implementation-plan.md @@ -66,13 +66,43 @@ The API key and the MIRAI hostname are never stored in config. This repository i ddev drush cex -y ``` - Check that `ai_provider_openai_compatible.settings.yml` still contains the placeholder endpoint, not the real MIRAI host. + Check that `ai_provider_openai_compatible.settings.yml` still contains the placeholder endpoint, not the real MIRAI host. If the endpoint was ever entered in the provider settings form, the real host is stored in the database and `cex` exports it. Reset the stored value first (the `settings.php` override still applies at runtime): + + ```bash + ddev drush cset ai_provider_openai_compatible.settings endpoint 'https://mirai.example.com/api' -y + ``` Other environments (Silta) need `MIRAI_API_URL` and `MIRAI_API_KEY` as environment variables (the key as a secret), and MIRAI must be reachable from there. Without them the site still deploys, but AI requests fail. ### Step 2: the first feature editors can use -Enable the CKEditor AI assistant (`ai_ckeditor`, part of the AI module) on one text format and try it on an Article. +The CKEditor AI assistant is set up by the recipe [recipes/ai_ckeditor](../recipes/ai_ckeditor/recipe.yml). It: + +- enables `ai_ckeditor` and `taxonomy`; +- adds the **AI Assistant** button (`aickeditor`) and the text selection menu (`ai_balloon_menu`) to the Basic HTML and Full HTML editors; +- enables all tools: Generate, Modify with a prompt, Reformat HTML, Fix spelling, Summarize, Tone, Translate and Help. The tools have no model of their own, so they use the default chat provider from step 1; +- creates the vocabularies "Tone of voice" (`ai_tone`) and "Translation languages" (`ai_translation_languages`) with starter terms. The tone tool sends the term description to the AI as the tone description; +- gives the Content editor role the `use ai ckeditor` permission. + +1. Apply the recipe: + + ```bash + ddev drush recipe ../recipes/ai_ckeditor -y + ``` + +2. **Test:** open `/node/add/article`, type a few sentences in Body and select them. Choose **AI Assistant** in the menu that appears next to the selection (or under "Show more items" in the toolbar), then try Summarize, Tone > Friendly and Translate > Finnish. +3. Export the config and commit `config/sync`: + + ```bash + ddev drush cex -y + ``` + +Notes: + +- `ai_ckeditor` inside the AI module is deprecated and will be removed in AI 1.6.0. The standalone [drupal/ai_ckeditor](https://www.drupal.org/project/ai_ckeditor) project replaces it, but has no stable 1.5 release yet, and AI 1.5.1 doesn't require it. We use the bundled module for now. When upgrading to AI 1.6, run `composer require drupal/ai_ckeditor` first: the machine name and config are the same. +- The recipe has `config: import: ai_ckeditor: '*'`. Recipes don't create a module's default config entities otherwise, so the AI prompts (`ai.ai_prompt.ai_ckeditor_*`) would be missing and every tool would send an empty prompt. The model then answers with generic text that has nothing to do with the selection. +- The tone and language terms are content, not config. `drush cim` on another environment creates the vocabularies but not the terms, so apply the recipe there too, or add the terms by hand. +- The browser console shows the CKEditor warning `toolbarview-item-unavailable {item: ai_balloon_menu}`. It is harmless: that toolbar item only switches on the selection menu and has no button of its own. ### Step 3: help with fields and metadata diff --git a/recipes/ai_ckeditor/content/taxonomy_term/18393b92-3e47-47c6-987d-135f48e0aba1.yml b/recipes/ai_ckeditor/content/taxonomy_term/18393b92-3e47-47c6-987d-135f48e0aba1.yml new file mode 100644 index 00000000..46646ef4 --- /dev/null +++ b/recipes/ai_ckeditor/content/taxonomy_term/18393b92-3e47-47c6-987d-135f48e0aba1.yml @@ -0,0 +1,20 @@ +_meta: + version: '1.0' + entity_type: taxonomy_term + uuid: 18393b92-3e47-47c6-987d-135f48e0aba1 + bundle: ai_tone + default_langcode: en +default: + status: + - + value: true + name: + - + value: 'Marketing' + description: + - + value: 'Energetic and benefit-focused, ending with a clear call to action.' + format: plain_text + weight: + - + value: 3 diff --git a/recipes/ai_ckeditor/content/taxonomy_term/4995c97d-f4b7-4cf3-8039-95b36542c3e0.yml b/recipes/ai_ckeditor/content/taxonomy_term/4995c97d-f4b7-4cf3-8039-95b36542c3e0.yml new file mode 100644 index 00000000..9ac72b71 --- /dev/null +++ b/recipes/ai_ckeditor/content/taxonomy_term/4995c97d-f4b7-4cf3-8039-95b36542c3e0.yml @@ -0,0 +1,16 @@ +_meta: + version: '1.0' + entity_type: taxonomy_term + uuid: 4995c97d-f4b7-4cf3-8039-95b36542c3e0 + bundle: ai_translation_languages + default_langcode: en +default: + status: + - + value: true + name: + - + value: 'Swedish' + weight: + - + value: 2 diff --git a/recipes/ai_ckeditor/content/taxonomy_term/6619bb5d-fe56-426c-9f03-2f86097ed5f1.yml b/recipes/ai_ckeditor/content/taxonomy_term/6619bb5d-fe56-426c-9f03-2f86097ed5f1.yml new file mode 100644 index 00000000..277a4874 --- /dev/null +++ b/recipes/ai_ckeditor/content/taxonomy_term/6619bb5d-fe56-426c-9f03-2f86097ed5f1.yml @@ -0,0 +1,16 @@ +_meta: + version: '1.0' + entity_type: taxonomy_term + uuid: 6619bb5d-fe56-426c-9f03-2f86097ed5f1 + bundle: ai_translation_languages + default_langcode: en +default: + status: + - + value: true + name: + - + value: 'German' + weight: + - + value: 4 diff --git a/recipes/ai_ckeditor/content/taxonomy_term/939dd161-b504-4265-835e-c187cf6d43e0.yml b/recipes/ai_ckeditor/content/taxonomy_term/939dd161-b504-4265-835e-c187cf6d43e0.yml new file mode 100644 index 00000000..fd4fcee5 --- /dev/null +++ b/recipes/ai_ckeditor/content/taxonomy_term/939dd161-b504-4265-835e-c187cf6d43e0.yml @@ -0,0 +1,20 @@ +_meta: + version: '1.0' + entity_type: taxonomy_term + uuid: 939dd161-b504-4265-835e-c187cf6d43e0 + bundle: ai_tone + default_langcode: en +default: + status: + - + value: true + name: + - + value: 'Friendly' + description: + - + value: 'Warm, approachable and conversational. Use simple words and address the reader directly.' + format: plain_text + weight: + - + value: 0 diff --git a/recipes/ai_ckeditor/content/taxonomy_term/b5902cf1-8966-4729-a274-a5885fcf70d2.yml b/recipes/ai_ckeditor/content/taxonomy_term/b5902cf1-8966-4729-a274-a5885fcf70d2.yml new file mode 100644 index 00000000..13899603 --- /dev/null +++ b/recipes/ai_ckeditor/content/taxonomy_term/b5902cf1-8966-4729-a274-a5885fcf70d2.yml @@ -0,0 +1,20 @@ +_meta: + version: '1.0' + entity_type: taxonomy_term + uuid: b5902cf1-8966-4729-a274-a5885fcf70d2 + bundle: ai_tone + default_langcode: en +default: + status: + - + value: true + name: + - + value: 'Formal' + description: + - + value: 'Professional and polite. Avoid slang, contractions and jokes.' + format: plain_text + weight: + - + value: 1 diff --git a/recipes/ai_ckeditor/content/taxonomy_term/bc7ecad2-eb45-484d-bbb8-f92e3c2a3acb.yml b/recipes/ai_ckeditor/content/taxonomy_term/bc7ecad2-eb45-484d-bbb8-f92e3c2a3acb.yml new file mode 100644 index 00000000..a8668b44 --- /dev/null +++ b/recipes/ai_ckeditor/content/taxonomy_term/bc7ecad2-eb45-484d-bbb8-f92e3c2a3acb.yml @@ -0,0 +1,16 @@ +_meta: + version: '1.0' + entity_type: taxonomy_term + uuid: bc7ecad2-eb45-484d-bbb8-f92e3c2a3acb + bundle: ai_translation_languages + default_langcode: en +default: + status: + - + value: true + name: + - + value: 'English' + weight: + - + value: 0 diff --git a/recipes/ai_ckeditor/content/taxonomy_term/cb67a971-0655-42be-a846-3ddb2034fe91.yml b/recipes/ai_ckeditor/content/taxonomy_term/cb67a971-0655-42be-a846-3ddb2034fe91.yml new file mode 100644 index 00000000..a92fbc40 --- /dev/null +++ b/recipes/ai_ckeditor/content/taxonomy_term/cb67a971-0655-42be-a846-3ddb2034fe91.yml @@ -0,0 +1,20 @@ +_meta: + version: '1.0' + entity_type: taxonomy_term + uuid: cb67a971-0655-42be-a846-3ddb2034fe91 + bundle: ai_tone + default_langcode: en +default: + status: + - + value: true + name: + - + value: 'Plain language' + description: + - + value: 'Clear and easy to understand for a general audience. Use short sentences and no jargon.' + format: plain_text + weight: + - + value: 2 diff --git a/recipes/ai_ckeditor/content/taxonomy_term/dd88df66-9f7e-4c79-a947-e283f7336ee9.yml b/recipes/ai_ckeditor/content/taxonomy_term/dd88df66-9f7e-4c79-a947-e283f7336ee9.yml new file mode 100644 index 00000000..9d980fb6 --- /dev/null +++ b/recipes/ai_ckeditor/content/taxonomy_term/dd88df66-9f7e-4c79-a947-e283f7336ee9.yml @@ -0,0 +1,16 @@ +_meta: + version: '1.0' + entity_type: taxonomy_term + uuid: dd88df66-9f7e-4c79-a947-e283f7336ee9 + bundle: ai_translation_languages + default_langcode: en +default: + status: + - + value: true + name: + - + value: 'Finnish' + weight: + - + value: 1 diff --git a/recipes/ai_ckeditor/content/taxonomy_term/fabe3409-b631-4963-ae20-a1e0e429a327.yml b/recipes/ai_ckeditor/content/taxonomy_term/fabe3409-b631-4963-ae20-a1e0e429a327.yml new file mode 100644 index 00000000..2fde5257 --- /dev/null +++ b/recipes/ai_ckeditor/content/taxonomy_term/fabe3409-b631-4963-ae20-a1e0e429a327.yml @@ -0,0 +1,16 @@ +_meta: + version: '1.0' + entity_type: taxonomy_term + uuid: fabe3409-b631-4963-ae20-a1e0e429a327 + bundle: ai_translation_languages + default_langcode: en +default: + status: + - + value: true + name: + - + value: 'Estonian' + weight: + - + value: 3 diff --git a/recipes/ai_ckeditor/recipe.yml b/recipes/ai_ckeditor/recipe.yml new file mode 100644 index 00000000..746a78aa --- /dev/null +++ b/recipes/ai_ckeditor/recipe.yml @@ -0,0 +1,66 @@ +name: 'AI CKEditor assistant' +description: 'Adds the AI tools button and the text selection menu to the Basic HTML and Full HTML editors. The tools use the default AI chat provider (see the ai_mirai recipe). Creates "Tone of voice" and "Translation languages" vocabularies with starter terms for the tone and translate tools.' +type: 'AI' +install: + - taxonomy + - ai_ckeditor +config: + # Recipes skip module default config entities unless imported explicitly. + # Without this the AI prompts are missing and every tool sends an empty prompt. + import: + ai_ckeditor: '*' + actions: + taxonomy.vocabulary.ai_tone: + createIfNotExists: + name: 'Tone of voice' + description: 'Tones for the AI CKEditor tone tool. The term description tells the AI how to write.' + taxonomy.vocabulary.ai_translation_languages: + createIfNotExists: + name: 'Translation languages' + description: 'Target languages for the AI CKEditor translate tool.' + user.role.content_editor: + grantPermission: 'use ai ckeditor' + editor.editor.basic_html: &ai_ckeditor_editor + addItemToToolbar: + - aickeditor + - ai_balloon_menu + setProperties: + settings.plugins.ai_ckeditor_ai: + dialog: + autoresize: 'min-width: 600px' + height: '750' + width: '900' + dialog_class: ai-ckeditor-modal + # An empty provider uses the default chat provider from ai.settings. + plugins: + ai_ckeditor_completion: + enabled: true + provider: null + ai_ckeditor_help: + enabled: true + ai_ckeditor_modify_prompt: + enabled: true + provider: null + ai_ckeditor_reformat_html: + enabled: true + provider: null + ai_ckeditor_spellfix: + enabled: true + provider: null + ai_ckeditor_summarize: + enabled: true + provider: null + ai_ckeditor_tone: + enabled: true + provider: null + autocreate: false + tone_vocabulary: ai_tone + use_description: true + ai_ckeditor_translate: + enabled: true + provider: null + language_source: tax + autocreate: false + translate_vocabulary: ai_translation_languages + use_description: false + editor.editor.full_html: *ai_ckeditor_editor