Spam / Not spam appeared on every message in every thread. "Not spam" on an
inbox message is nonsense — it was never filed as spam — and a pair of buttons
everywhere is noise that trains nobody.
The slot now shows a single action chosen by where the message actually is:
"Spam" in the inbox or archive, "Not spam" in the spam folder, nothing at all in
Sent, Drafts or Trash, where no move is a verdict about spam. A message already
taught says so instead of offering the same lesson again.
That needs the message's folder, which the slot was never told. Rather than a
core change to the slot props, /verdict/{id} becomes /context/{id} and returns
the folder alongside the verdict — one round-trip, answered for any message
including ones never classified.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SqJYt7jD7oUEkoY68sBZo
74 lines
1.9 KiB
Python
74 lines
1.9 KiB
Python
"""Request/response models for the antispam API (SPEC §C.6)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
class SettingsOut(BaseModel):
|
|
enabled_accounts: list[str]
|
|
move_threshold: float
|
|
banner_threshold: float
|
|
network_checks: bool
|
|
learn_from_provider_junk: bool
|
|
|
|
|
|
class SettingsIn(BaseModel):
|
|
"""Merge semantics: omitted fields keep their current value."""
|
|
|
|
enabled_accounts: list[str] | None = None
|
|
# Bounds are guard rails, not opinions: 0 would file everything as spam and
|
|
# the engine's own scale tops out well below 100.
|
|
move_threshold: float | None = Field(default=None, gt=0, le=100)
|
|
banner_threshold: float | None = Field(default=None, gt=0, le=100)
|
|
network_checks: bool | None = None
|
|
learn_from_provider_junk: bool | None = None
|
|
|
|
|
|
class RuleOut(BaseModel):
|
|
id: uuid.UUID
|
|
kind: str
|
|
pattern: str
|
|
|
|
|
|
class RuleIn(BaseModel):
|
|
kind: str = Field(pattern="^(allow|deny)$")
|
|
# An address (a@b.test) or a whole domain (@b.test).
|
|
pattern: str = Field(min_length=3, max_length=320)
|
|
|
|
|
|
class LearnIn(BaseModel):
|
|
message_id: uuid.UUID
|
|
verdict: str = Field(pattern="^(spam|ham)$")
|
|
|
|
|
|
class VerdictOut(BaseModel):
|
|
"""What the banner slot needs to explain itself."""
|
|
|
|
message_id: uuid.UUID
|
|
score: float
|
|
action: str
|
|
symbols: list[str]
|
|
learned: str | None
|
|
|
|
|
|
class MessageContextOut(BaseModel):
|
|
"""What the per-message UI slots need in one round-trip.
|
|
|
|
``special_use`` is where the message currently sits, which decides which
|
|
action is meaningful: "Spam" in the inbox, "Not spam" in the spam folder,
|
|
neither in Sent or Drafts.
|
|
"""
|
|
|
|
message_id: uuid.UUID
|
|
special_use: str | None
|
|
verdict: VerdictOut | None
|
|
|
|
|
|
class StatsOut(BaseModel):
|
|
counts: dict[str, int]
|
|
top_symbols: list[dict[str, object]]
|
|
bayes: dict[str, object] | None
|