AI spam classification: the model's opinion as one bounded spam signal, and the llm_prompt Sieve function (AI-1 to AI-28)
The classifier sends only the subject and text, between unforgeable markers after the operator's prompt, to an OpenAI-compatible endpoint the operator configured; nothing is preset. Its answer maps to an LLM_ tag whose score is clamped (+5.0, -1.0 by default) and can never discard or reject on its own; X-Spam-LLM is sanitized, encoded and folded, and a planted one is removed. Failures, timeouts past the ceiling, a full slot or a paused model leave mail flowing untagged. llm_prompt answers trusted scripts, and accounts holding interactAi within an hourly limit. Redirects aren't followed and no content or secret is logged. The limits live in inbuxa:AiLimits. Acceptance tests 1 and 3 to 21; test 2 as the re-enabled shared llm case, whose setup no longer waits on a rules file from a developer's own path; test 22 written as the ignored ai_compat.
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Coffey Labs
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
//! What `/set` may store for the classifier and its models (AI-12, AI-18,
|
||||
//! and the spec's "Errors"): refused with `invalidProperties` naming the
|
||||
//! field. A model in use can't be destroyed: the registry's foreign key
|
||||
//! already refuses that, naming `x:SpamLlm`.
|
||||
|
||||
use jmap_proto::error::set::SetError;
|
||||
use registry::schema::{
|
||||
prelude::{Object, ObjectInner, Property},
|
||||
structs::{AiModel, SpamLlm},
|
||||
};
|
||||
use store::RegistryStore;
|
||||
|
||||
fn refuse(property: Property, why: &str) -> SetError<Property> {
|
||||
SetError::invalid_properties()
|
||||
.with_property(property)
|
||||
.with_description(why.to_string())
|
||||
}
|
||||
|
||||
fn temperature_ok(t: f64) -> bool {
|
||||
t.is_finite() && (0.0..=1.0).contains(&t)
|
||||
}
|
||||
|
||||
/// Checks an `x:SpamLlm` or `x:AiModel` being written.
|
||||
pub async fn check(registry: &RegistryStore, new: &Object) -> trc::Result<Result<(), SetError<Property>>> {
|
||||
match &new.inner {
|
||||
ObjectInner::SpamLlm(SpamLlm::Enable(settings)) => {
|
||||
if settings.separator.is_empty() {
|
||||
return Ok(Err(refuse(Property::Separator, "The separator can't be empty.")));
|
||||
}
|
||||
if settings.categories.iter().count() < 2 {
|
||||
return Ok(Err(refuse(
|
||||
Property::Categories,
|
||||
"At least two categories are needed.",
|
||||
)));
|
||||
}
|
||||
if !temperature_ok(settings.temperature.into_inner()) {
|
||||
return Ok(Err(refuse(
|
||||
Property::Temperature,
|
||||
"The temperature must be from 0.0 to 1.0.",
|
||||
)));
|
||||
}
|
||||
if registry
|
||||
.object::<AiModel>(settings.model_id)
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
return Ok(Err(refuse(
|
||||
Property::ModelId,
|
||||
"No AI model has this id.",
|
||||
)));
|
||||
}
|
||||
}
|
||||
ObjectInner::AiModel(model) => {
|
||||
if !temperature_ok(model.temperature.into_inner()) {
|
||||
return Ok(Err(refuse(
|
||||
Property::Temperature,
|
||||
"The temperature must be from 0.0 to 1.0.",
|
||||
)));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(Ok(()))
|
||||
}
|
||||
Reference in New Issue
Block a user