Let manual corrections outrank the model's own precedent
History rows now carry a source label: manual edits and merchant rules are the user's decisions, ranked ahead of equally similar rows the model classified itself and guaranteed slots in a full history window. Without the distinction, precedent fed the model its own uncorrected answers as majority evidence, so a correction never won against the rows it was meant to fix. Both system prompts state that user entries outrank ai entries. Alias write-back on manual merchant links and the per-merchant usual category already learned locally; this closes the loop for categories and tags.
This commit is contained in:
@@ -160,6 +160,10 @@ type promptHistory struct {
|
||||
CategoryID string `json:"category_id"`
|
||||
MerchantID string `json:"merchant_id,omitempty"`
|
||||
TagIDs []string `json:"tag_ids"`
|
||||
// Source separates the user's own decisions ("user") from earlier model
|
||||
// output ("ai"): without the distinction, precedent feeds the model its
|
||||
// own past answers as evidence and a manual correction never wins.
|
||||
Source string `json:"source"`
|
||||
}
|
||||
type candidateSet struct {
|
||||
categories []categoryPrompt
|
||||
@@ -314,10 +318,16 @@ func answerSchema(d domain.Dataset, kind string) map[string]any {
|
||||
return retrieve("", kind, d, nil, nil).schema()
|
||||
}
|
||||
|
||||
// history selects precedent for the prompt: the nearest rows by word overlap,
|
||||
// filled out with the most recent. The user's own decisions — manual edits
|
||||
// and locally applied merchant rules — outrank rows the model classified
|
||||
// itself, so one correction beats any number of uncorrected AI answers for
|
||||
// the same payee.
|
||||
func history(f domain.Facts, d domain.Dataset, clean func(string) string, limit int) []promptHistory {
|
||||
type row struct {
|
||||
tx domain.Transaction
|
||||
score int
|
||||
user bool
|
||||
}
|
||||
rows := []row{}
|
||||
for _, tx := range d.Transactions {
|
||||
@@ -325,19 +335,50 @@ func history(f domain.Facts, d domain.Dataset, clean func(string) string, limit
|
||||
if tx.Facts.ID == f.ID || e.Kind == "transfer" || e.CategoryID == "" || e.CategoryID == domain.ExpenseFallback || e.CategoryID == domain.IncomeFallback {
|
||||
continue
|
||||
}
|
||||
rows = append(rows, row{tx: tx, score: similarity(f.RawDescription+" "+f.Counterparty, tx.Facts.RawDescription+" "+tx.Facts.Counterparty)})
|
||||
source := tx.Enrichment.Classification.Source
|
||||
rows = append(rows, row{
|
||||
tx: tx,
|
||||
score: similarity(f.RawDescription+" "+f.Counterparty, tx.Facts.RawDescription+" "+tx.Facts.Counterparty),
|
||||
user: source == "manual" || source == "rule",
|
||||
})
|
||||
}
|
||||
sort.Slice(rows, func(i, j int) bool {
|
||||
if rows[i].score != rows[j].score {
|
||||
return rows[i].score > rows[j].score
|
||||
}
|
||||
if rows[i].user != rows[j].user {
|
||||
return rows[i].user
|
||||
}
|
||||
if rows[i].tx.Facts.BookingDate != rows[j].tx.Facts.BookingDate {
|
||||
return rows[i].tx.Facts.BookingDate > rows[j].tx.Facts.BookingDate
|
||||
}
|
||||
return rows[i].tx.Facts.ID < rows[j].tx.Facts.ID
|
||||
})
|
||||
if limit > 0 && len(rows) > limit {
|
||||
rows = rows[:limit]
|
||||
// Never let recent AI output crowd every correction out of a full
|
||||
// window: user rows keep their slots ahead of equally similar AI rows.
|
||||
kept := make([]row, 0, limit)
|
||||
users := 0
|
||||
for _, r := range rows {
|
||||
if r.user {
|
||||
users++
|
||||
}
|
||||
}
|
||||
userBudget := min(users, limit/2)
|
||||
aiBudget := limit - userBudget
|
||||
for _, r := range rows {
|
||||
if r.user && userBudget > 0 {
|
||||
kept = append(kept, r)
|
||||
userBudget--
|
||||
} else if !r.user && aiBudget > 0 {
|
||||
kept = append(kept, r)
|
||||
aiBudget--
|
||||
} else if r.user && aiBudget > 0 {
|
||||
kept = append(kept, r)
|
||||
aiBudget--
|
||||
}
|
||||
}
|
||||
rows = kept
|
||||
}
|
||||
out := make([]promptHistory, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
@@ -345,11 +386,16 @@ func history(f domain.Facts, d domain.Dataset, clean func(string) string, limit
|
||||
if tags == nil {
|
||||
tags = []string{}
|
||||
}
|
||||
source := "ai"
|
||||
if row.user {
|
||||
source = "user"
|
||||
}
|
||||
out = append(out, promptHistory{
|
||||
Date: row.tx.Facts.BookingDate, Amount: string(row.tx.Facts.Amount),
|
||||
Description: clean(row.tx.Facts.RawDescription), Counterparty: clean(row.tx.Facts.Counterparty),
|
||||
CategoryID: row.tx.Enrichment.CategoryID, MerchantID: row.tx.Enrichment.MerchantID,
|
||||
TagIDs: append([]string{}, tags...),
|
||||
Source: source,
|
||||
})
|
||||
}
|
||||
return out
|
||||
|
||||
Reference in New Issue
Block a user