AI comment moderation with thresholds in the model¶
Version v1
You can apply this model as a recipe (Drupal 10.3 or later) to your own Drupal site:
1 2 3 4 5 6 7 8 9 10 11 | |
Purpose¶
A comment is submitted, an AI model reads it and returns two scores and a one sentence justification. That is the whole of the AI's involvement. The decision about what happens to the comment is taken afterwards, by ordinary ECA conditions comparing those scores against thresholds that are stored in the model as plain values.
The split is deliberate. The agent supplies judgment, the workflow supplies policy. When someone asks months later why a particular comment was blocked, the answer is the model plus the log record of that run, not a prompt and a shrug.
What the AI is asked for¶
One call to the Chat action from AI Integration - ECA, with a strict JSON schema, returns exactly three values.
toxicity, an integer from 0 to 100.spam, an integer from 0 to 100.reason, one short sentence explaining both scores.
The prompt states no threshold and asks for no recommendation. It
explicitly tells the model not to mention publishing, review or blocking,
because the site decides what the scores mean. The response is a plain
string, and Tamper: Encode/Decode in Json Decode mode turns it into
structured data under the token verdict.
Where the thresholds live¶
Two "Token: set value" actions run before the AI call and hold the entire policy.
- "Threshold: block from this score" sets
threshold_blockto70. - "Threshold: hold for review from this score" sets
threshold_reviewto30.
To change the policy, open one of those two actions and type a different number. Nothing else in the model changes and the prompt is not touched, because the prompt never knew the numbers in the first place.
Both thresholds are applied to both scores. A comment is blocked when
either score reaches threshold_block, held when either score reaches
threshold_review, and published when both stay below it.
The three exits¶
The routing is a cascade of scalar comparisons, so exactly one exit runs.
Each exit writes the comment status, records the outcome on the comment and
writes one log record to the eca_comment_verdict channel naming both
scores, both thresholds and the exit taken.
| Exit | status |
field_ai_moderation |
Log severity |
|---|---|---|---|
| Publish | 1 | approved |
Info |
| Hold for review | 0 | review |
Notice |
| Block | 0 | blocked |
Warning |
Publish and the other two differ in the comment status. Hold and block
differ in field_ai_moderation, which is the point of that field: an
unpublished comment on its own does not say whether a human is expected to
look at it. A held comment is waiting for a moderator, a blocked comment is
not, and a site can act on that distinction with a view filtered on the
field.
field_ai_reason carries the sentence the AI wrote and is set once, before
the routing, because it is evidence rather than policy.
Both fields are deliberately absent from the comment form display, so that a commenter cannot set them, and from the comment view display, so that the assessment is not published alongside the comment. Add them to a view or to an administrative display to review them.
When the AI call fails¶
Nothing is published unless a decision says so. Before the AI call, two
actions named "Until a decision is taken ..." unpublish the comment and set
field_ai_moderation to review. Every later exit overwrites that, so a
comment that reaches no exit at all, because the AI call raised an
exception or because no chat model has been selected yet, is still held
rather than left at whatever status it arrived with.
On top of that, two checks send an unusable answer down a branch that logs what happened.
- Before decoding, "Response contains a toxicity score" tests the raw string. An empty response, which is what the Chat action leaves behind when no provider can be loaded, and a refusal in prose both fail this test. The check exists because Json Decode raises an exception on input that is not JSON, and an exception ends the run without reaching any branch.
- After decoding, "Verdict carries a toxicity score" catches an answer that is valid JSON but does not carry the value the routing needs.
Either way the comment is unpublished, field_ai_moderation is review,
and the raw response is written to the log at Error severity so that the
string that could not be used is still available. An assessment that did
not happen is treated as an assessment that could not clear the comment.
A response that contains "toxicity" but is still malformed, a truncated
answer for example, raises the decoder exception anyway. ECA logs the
exception on its own channel and the run ends there, with the comment left
unpublished by the default above.
How it stays out of its own way¶
The event is Presave, not Insert, so the status and the two fields are changed on the entity Drupal is already about to write. The model never calls save on the comment and therefore cannot trigger itself.
The first condition on the event is "Entity: is new". A moderator who later publishes, edits or unpublishes the comment causes another presave, and that condition ends the run before the AI call. Each comment is assessed exactly once, when it is submitted.
Before you run it¶
Select a chat model in the "Ask AI to score the comment" action. No Drupal site has an AI provider configured out of the box, so this step is required, and the provider has to support structured output or the schema is ignored and the response will not decode. Expect the comment submission to take a few seconds while the provider answers.
Do this before the site accepts comments. With no model selected the Chat action has nothing to call and raises an exception, which ends the run. The comment is still saved and still unpublished, because that is the default the model sets before the call, but no verdict is logged and every comment piles up in the moderation queue.
Requirements¶
This recipe installs core's comment module, together with the eca_base,
eca_content and eca_log submodules that ship inside the ECA project itself.
Two genuinely separate contributed projects are also required: AI Integration -
ECA (ai_integration_eca) for the Chat action, and ECA Tamper (eca_tamper)
for the Json Decode step. Composer installs both alongside this recipe.
It includes the library's article_content_type recipe, which provides the
Article content type with its body field. Drupal 11.4 removed that content
type from the Standard profile and Drupal 12 removed core's recipe for it,
so the library ships its own. Composer installs it alongside this recipe.
The recipe also ships the comment comment type that the model is
restricted to, its comment body field, the two moderation fields, the
comment and moderation field displays, and the comment field on the Article
content type, so that a fresh site has something to comment on.
Dependencies¶
- config
- comment.type.comment
- field.field.comment.comment.comment_body
- field.field.comment.comment.field_ai_moderation
- field.field.comment.comment.field_ai_reason
- field.storage.comment.comment_body
- field.storage.comment.field_ai_moderation
- field.storage.comment.field_ai_reason
- module
- ai_integration_eca
- comment
- eca_base
- eca_content
- eca_log
- eca_tamper
- modeler_api
- text
Used plugins¶
Events¶
Conditions¶
- Comment is newly submitted
- Comment has text
- Verdict carries a toxicity score
- Verdict is empty or did not decode
- Response contains a toxicity score
- Response is empty or is not an assessment
- Toxicity reaches the block threshold
- Spam reaches the block threshold
- Toxicity stays below the block threshold
- Spam stays below the block threshold
- Toxicity reaches the review threshold
- Spam reaches the review threshold
- Toxicity stays below the review threshold
- Spam stays below the review threshold
Actions¶
- AND, the comment has text
- Threshold: block from this score
- Threshold: hold for review from this score
- Until a decision is taken, the comment stays unpublished
- Until a decision is taken, the outcome is review
- Turn the comment into plain text
- Ask AI to score the comment
- Decode the verdict
- Record the reason the AI gave
- AND, spam is below the block threshold too
- AND, spam is below the review threshold too
- Exit 1, publish the comment
- Record the approved outcome
- Log the publish exit
- Exit 2, hold the comment for review
- Record the review outcome
- Log the review exit
- Exit 3, block the comment
- Record the blocked outcome
- Log the block exit
- No usable verdict, hold the comment
- Record the review outcome after a failure
- Log the unusable AI response
Changelog¶
Initial version