Batch Analyse requests and survive opaque provider schema budgets
Analyse now classifies up to ten same-kind transactions per provider request: the registry and history travel once per batch, so a thousand-row backfill costs about a hundred paced requests instead of a thousand. The answer schema appears once — an array item carrying an enum-bound ref — because providers meter strict schemas by token cost: duplicating registry enums per row, or bounding arrays with minItems/maxItems that Gemini expands per element, rejects real registries with a bare HTTP 400. Row count, duplicate refs, duplicate tags and taxonomy bounds are all enforced server-side instead, and a request still rejected outright halves until accepted, remembering the working size for the run. Batch requests scale the HTTP budget by row count, chunk failures cannot abort a run whose later rows succeeded, and rows resolved against one snapshot share one minted merchant. Measured on a real 165-row month over a zero-data-retention route: 165 analysed, 152 proposals, 0 errors, 17 requests, under 8 minutes. Fresh installs default to google/gemini-3.8-flash, the model that demonstrably honors strict structured outputs over a ZDR route. Preview changes now carry counterparty, amount and currency, and the review list shows the amount with a counterparty fallback for banks that leave descriptions empty.
This commit is contained in:
@@ -270,6 +270,9 @@ func mockClassifier(t *testing.T, a *App, inspect ...func(*http.Request)) {
|
||||
return
|
||||
}
|
||||
var prompt struct {
|
||||
Transactions []struct {
|
||||
Ref string `json:"ref"`
|
||||
} `json:"transactions"`
|
||||
Categories []struct{ ID, Path string } `json:"categories"`
|
||||
}
|
||||
if len(req.Messages) != 2 || json.Unmarshal([]byte(req.Messages[1].Content), &prompt) != nil {
|
||||
@@ -282,7 +285,21 @@ func mockClassifier(t *testing.T, a *App, inspect ...func(*http.Request)) {
|
||||
category = c.ID
|
||||
}
|
||||
}
|
||||
content, _ := json.Marshal(map[string]any{"merchant_id": nil, "new_merchant": "REWE", "category_id": category, "tag_ids": []string{}, "confidence": "high"})
|
||||
answer := map[string]any{"merchant_id": nil, "new_merchant": "REWE", "category_id": category, "tag_ids": []string{}, "confidence": "high"}
|
||||
var content []byte
|
||||
if len(prompt.Transactions) > 0 {
|
||||
items := make([]map[string]any, 0, len(prompt.Transactions))
|
||||
for _, row := range prompt.Transactions {
|
||||
item := map[string]any{"ref": row.Ref}
|
||||
for k, v := range answer {
|
||||
item[k] = v
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
content, _ = json.Marshal(map[string]any{"transactions": items})
|
||||
} else {
|
||||
content, _ = json.Marshal(answer)
|
||||
}
|
||||
json.NewEncoder(w).Encode(map[string]any{"choices": []any{map[string]any{"finish_reason": "stop", "message": map[string]any{"content": string(content)}}}})
|
||||
}))
|
||||
t.Cleanup(mock.Close)
|
||||
|
||||
Reference in New Issue
Block a user