48c8811d69
Auto-apply is now ON by default (operator-asked: opt-OUT, not opt-in) — migration 0059 + model default flipped. The support (>=30) + measured-precision gates keep it safe and every auto-tag is reversible. Observability so the operator can tune from real data: - MISFIRE = an auto-applied (source='head_auto') tag the operator later removes. UNDER-FIRE = a tag with a head the operator adds by hand (the head missed it). Both captured at correction time in TagService.add_to_image/remove_from_image (source is lost on delete) into durable per-tag counters (head_metric), keyed by tag so they survive head retrain/prune. - Daily snapshot_head_metrics writes a per-concept time-series point (head_metrics_snapshot): auto-applied volume + cumulative misfires/under-fires + head quality; 180-day retention; daily beat. - GET /api/heads/metrics: per-concept current counts + realized misfire rate + head quality, plus the snapshot time-series — the report to tune the precision target + support floor. Migration 0060. Tests: misfire/under-fire counting (and the negatives — manual removal isn't a misfire, headless manual add isn't an under-fire), snapshot time-series, metrics API. What's the autofire threshold? There's no single number — each graduated head derives its OWN probability cutoff from its PR curve: the operating point that holds precision >= head_auto_apply_precision (0.97) at max recall. The global knobs are that target + the >=30 support floor. NEXT (slice 3): UI — enable toggle, dry-run preview, per-concept trends. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ttrj5P7upUTueSfoJcxEqa
33 lines
1.3 KiB
Python
33 lines
1.3 KiB
Python
"""HeadMetric — running correction counters per concept (#114 observability).
|
|
|
|
Earned auto-apply fires graduated heads; to TUNE it we need to know how often a
|
|
head's auto-applied tag was wrong (the operator removed it = a MISFIRE) and how
|
|
often the operator had to add a tag a head exists for by hand (an UNDER-FIRE,
|
|
the head missed it). image_tag.source is lost when a row is deleted, so these
|
|
are captured as durable cumulative counters at correction time — they survive
|
|
head retrain/prune (keyed by tag, not by the head row). The daily snapshot reads
|
|
them into the time-series.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import DateTime, ForeignKey, Integer, func
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from .base import Base
|
|
|
|
|
|
class HeadMetric(Base):
|
|
__tablename__ = "head_metric"
|
|
|
|
tag_id: Mapped[int] = mapped_column(
|
|
ForeignKey("tag.id", ondelete="CASCADE"), primary_key=True
|
|
)
|
|
# An auto-applied (source='head_auto') tag the operator later REMOVED.
|
|
n_misfires: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
# A tag with a head that the operator added by HAND (the head missed it).
|
|
n_underfires: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
)
|