Files
inbuxa-server/crates/features/src/ai/writes.rs
T
jcoffey-dev 9490fc4677 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.
2026-09-19 00:41:00 -07:00

71 lines
2.3 KiB
Rust

/*
* 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(()))
}