Phase 3: JEV classification, rules table, editable categories (AUD-69)
Implements FR-3, FR-4, FR-5 and FR-14. - internal/classify: TypeSafe System One (JEV) HTTP client with the confirmed request/response shape, retry with backoff on 429/529/5xx, a plain-text rule engine (first match wins, overrides only the named fields), and bounded orchestration with a per-run cap. - internal/store: migration v3 adds an editable `categories` table (name = JEV Choice option and classifications key; rename cascades, remove reassigns) and seeds a starter rules version once. Classification persistence, coverage, and pending/all message queries. - internal/server: POST /actions/classify and /actions/reclassify, plus the FR-14 category editor (GET /categories and add/rename/remove). The triage list now groups by the live category list and shows action + confidence. - CLI: -classify and -classify-all for n8n. - Docs: docs/phase3-classify.md with the confirmed JEV shape, a live read-only classification, and the DoD evidence. DoD: all 100 stored messages have a non-null category, importance, and suggested action (0 nulls); JEV picked a newly added/renamed category for 6 real messages over HTTP. One live JEV call recorded in the phase doc. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -17,6 +17,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitea.lab.audasmedia.com.au/sam/email_agent/internal/classify"
|
||||
"gitea.lab.audasmedia.com.au/sam/email_agent/internal/config"
|
||||
"gitea.lab.audasmedia.com.au/sam/email_agent/internal/mailfetch"
|
||||
"gitea.lab.audasmedia.com.au/sam/email_agent/internal/session"
|
||||
@@ -32,13 +33,14 @@ func init() {
|
||||
|
||||
// Server holds the HTTP dependencies.
|
||||
type Server struct {
|
||||
cfg *config.Config
|
||||
store *store.Store
|
||||
logger *slog.Logger
|
||||
http *http.Server
|
||||
tmpl *template.Template
|
||||
sessions *session.Manager
|
||||
fetcher *mailfetch.Fetcher
|
||||
cfg *config.Config
|
||||
store *store.Store
|
||||
logger *slog.Logger
|
||||
http *http.Server
|
||||
tmpl *template.Template
|
||||
sessions *session.Manager
|
||||
fetcher *mailfetch.Fetcher
|
||||
classifier *classify.Classifier
|
||||
}
|
||||
|
||||
// New builds a Server. It does not start listening.
|
||||
@@ -64,6 +66,10 @@ func New(cfg *config.Config, st *store.Store, logger *slog.Logger) (*Server, err
|
||||
tmpl: tmpl,
|
||||
sessions: sessions,
|
||||
fetcher: mailfetch.NewFetcher(cfg, st, logger),
|
||||
classifier: classify.NewClassifier(
|
||||
classify.NewClient(cfg.JEVBaseURL, cfg.JEVAPIKey, cfg.JEVModel, logger),
|
||||
st, logger, cfg.ClassifyConcurrency, cfg.ClassifyMaxMessages,
|
||||
),
|
||||
}
|
||||
s.http = &http.Server{
|
||||
Addr: cfg.Addr,
|
||||
@@ -113,6 +119,14 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.HandleFunc("GET /{$}", s.requireAuth(s.handleDashboard))
|
||||
mux.HandleFunc("GET /partials/inbox", s.requireAuth(s.handleInboxPartial))
|
||||
mux.HandleFunc("POST /actions/fetch", s.requireAuth(s.handleFetchMail))
|
||||
mux.HandleFunc("POST /actions/classify", s.requireAuth(s.handleClassify))
|
||||
mux.HandleFunc("POST /actions/reclassify", s.requireAuth(s.handleReclassify))
|
||||
|
||||
// FR-14: editable category list.
|
||||
mux.HandleFunc("GET /categories", s.requireAuth(s.handleCategories))
|
||||
mux.HandleFunc("POST /categories/add", s.requireAuth(s.handleCategoryAdd))
|
||||
mux.HandleFunc("POST /categories/rename", s.requireAuth(s.handleCategoryRename))
|
||||
mux.HandleFunc("POST /categories/remove", s.requireAuth(s.handleCategoryRemove))
|
||||
|
||||
return s.recoverPanic(s.logRequests(mux))
|
||||
}
|
||||
@@ -234,6 +248,10 @@ type statusRecorder struct {
|
||||
status int
|
||||
}
|
||||
|
||||
// Unwrap lets http.ResponseController reach the underlying writer (the classify
|
||||
// handler extends its write deadline for long runs).
|
||||
func (r *statusRecorder) Unwrap() http.ResponseWriter { return r.ResponseWriter }
|
||||
|
||||
func (r *statusRecorder) WriteHeader(code int) {
|
||||
r.status = code
|
||||
r.ResponseWriter.WriteHeader(code)
|
||||
|
||||
Reference in New Issue
Block a user