C: bulk playbook record — operational rating wiring
POST /v1/matrix/playbooks/bulk accepts an array of playbook entries
and records each independently — failures per-entry don't abort the
batch. Designed for two operational use cases:
1. Backfilling historical placement data into the playbook
substrate (the Rust system has 4,701 fill operations recorded
with embeddings; that data deserves to feed the Go learning
loop without a 4,701-call procedural script).
2. Batched click-tracking from a session's worth of coordinator
interactions, posted once at idle rather than per-click.
Per-entry response shape: {index, playbook_id} on success or
{index, error} on failure. Caller can inspect failures without
diffing.
Smoke (scripts/playbook_smoke.sh, new assertion #4):
Bulk POST 3 entries: 2 valid (alpha→widget-a, bravo→widget-b) +
1 invalid (empty query_text). Verifies recorded=2, failed=1,
the 2 valid ones get playbook_ids back, and the invalid one
surfaces its validation error in-line.
Single-record /matrix/playbooks/record from 06e7152 still works
unchanged; bulk is additive. The corpus field can be set per-
entry or once at the batch level (entry-level wins on collision).
Per the small-model autonomous pipeline framing: this is the
"the playbook gets denser with each iteration" mechanism. Click
tracking → bulk POST → playbook entries → future similar queries
get those answers boosted via the existing /matrix/search
use_playbook path. The learning loop now has both inflows wired
(single + bulk) — what remains is the demo UI shim that calls
/feedback on result interaction (deferred — no Go demo UI yet).
15-smoke regression all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b199093d1f
commit
6392772f41
@ -7,8 +7,12 @@
|
|||||||
// GET /matrix/corpora — list known vectord indexes
|
// GET /matrix/corpora — list known vectord indexes
|
||||||
// POST /matrix/relevance — adjacency-pollution filter
|
// POST /matrix/relevance — adjacency-pollution filter
|
||||||
// POST /matrix/downgrade — strong-model downgrade gate
|
// POST /matrix/downgrade — strong-model downgrade gate
|
||||||
// POST /matrix/playbooks/record — record a (query → answer)
|
// POST /matrix/playbooks/record — record a single (query → answer)
|
||||||
// success for the learning loop
|
// success for the learning loop
|
||||||
|
// POST /matrix/playbooks/bulk — bulk-record N successes; useful
|
||||||
|
// for backfilling historical
|
||||||
|
// placement data into the
|
||||||
|
// playbook substrate
|
||||||
//
|
//
|
||||||
// matrixd talks to embedd (for query-text embedding) and vectord
|
// matrixd talks to embedd (for query-text embedding) and vectord
|
||||||
// (for per-corpus search) via HTTP. Both URLs come from
|
// (for per-corpus search) via HTTP. Both URLs come from
|
||||||
@ -66,6 +70,7 @@ func (h *handlers) register(r chi.Router) {
|
|||||||
r.Post("/matrix/relevance", h.handleRelevance)
|
r.Post("/matrix/relevance", h.handleRelevance)
|
||||||
r.Post("/matrix/downgrade", h.handleDowngrade)
|
r.Post("/matrix/downgrade", h.handleDowngrade)
|
||||||
r.Post("/matrix/playbooks/record", h.handlePlaybookRecord)
|
r.Post("/matrix/playbooks/record", h.handlePlaybookRecord)
|
||||||
|
r.Post("/matrix/playbooks/bulk", h.handlePlaybookBulk)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *handlers) handleSearch(w http.ResponseWriter, r *http.Request) {
|
func (h *handlers) handleSearch(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -142,6 +147,67 @@ func (h *handlers) handlePlaybookRecord(w http.ResponseWriter, r *http.Request)
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// playbookBulkRequest is the POST /matrix/playbooks/bulk body —
|
||||||
|
// component C (operational rating wiring). Used to backfill
|
||||||
|
// historical placement data, or batch-record a session's worth of
|
||||||
|
// coordinator click-tracking. Each Entry is recorded independently;
|
||||||
|
// failures are reported per-entry without aborting the batch.
|
||||||
|
type playbookBulkRequest struct {
|
||||||
|
Entries []playbookRecordRequest `json:"entries"`
|
||||||
|
Corpus string `json:"corpus,omitempty"` // applies to all if entry-level not set
|
||||||
|
}
|
||||||
|
|
||||||
|
// playbookBulkResult reports per-entry outcomes plus the aggregate
|
||||||
|
// count. Errors include the entry index so callers can locate the
|
||||||
|
// offending record without diffing.
|
||||||
|
type playbookBulkResult struct {
|
||||||
|
Recorded int `json:"recorded"`
|
||||||
|
Failed int `json:"failed"`
|
||||||
|
Results []playbookBulkItemResult `json:"results"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type playbookBulkItemResult struct {
|
||||||
|
Index int `json:"index"`
|
||||||
|
PlaybookID string `json:"playbook_id,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handlers) handlePlaybookBulk(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req playbookBulkRequest
|
||||||
|
if !decodeJSON(w, r, &req) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(req.Entries) == 0 {
|
||||||
|
http.Error(w, "entries must be non-empty", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out := playbookBulkResult{
|
||||||
|
Results: make([]playbookBulkItemResult, len(req.Entries)),
|
||||||
|
}
|
||||||
|
for i, item := range req.Entries {
|
||||||
|
corpus := item.Corpus
|
||||||
|
if corpus == "" {
|
||||||
|
corpus = req.Corpus
|
||||||
|
}
|
||||||
|
entry := matrix.NewPlaybookEntry(item.QueryText, item.AnswerID, item.AnswerCorpus, item.Score, item.Tags)
|
||||||
|
if err := entry.Validate(); err != nil {
|
||||||
|
out.Results[i] = playbookBulkItemResult{Index: i, Error: err.Error()}
|
||||||
|
out.Failed++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pbID, err := h.r.Record(r.Context(), entry, corpus)
|
||||||
|
if err != nil {
|
||||||
|
out.Results[i] = playbookBulkItemResult{Index: i, Error: err.Error()}
|
||||||
|
out.Failed++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out.Results[i] = playbookBulkItemResult{Index: i, PlaybookID: pbID}
|
||||||
|
out.Recorded++
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, out)
|
||||||
|
}
|
||||||
|
|
||||||
// downgradeRequest is the POST /matrix/downgrade body. Mirrors
|
// downgradeRequest is the POST /matrix/downgrade body. Mirrors
|
||||||
// matrix.DowngradeInput. When ForceFullOverride is omitted from
|
// matrix.DowngradeInput. When ForceFullOverride is omitted from
|
||||||
// the body, the value falls back to matrixd's process env
|
// the body, the value falls back to matrixd's process env
|
||||||
|
|||||||
@ -166,6 +166,29 @@ else
|
|||||||
echo " ✗ ratio out of band: $RATIO"; FAILED=1
|
echo " ✗ ratio out of band: $RATIO"; FAILED=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── 4. /matrix/playbooks/bulk — component C (operational rating wiring)
|
||||||
|
echo "[playbook-smoke] bulk record 3 entries:"
|
||||||
|
BULK_RESP="$(curl -sS -X POST http://127.0.0.1:3110/v1/matrix/playbooks/bulk \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d "$(jq -n '{
|
||||||
|
entries: [
|
||||||
|
{query_text: "alpha test query", answer_id: "widget-a", answer_corpus: "widgets", score: 0.9},
|
||||||
|
{query_text: "bravo test query", answer_id: "widget-b", answer_corpus: "widgets", score: 0.8},
|
||||||
|
{query_text: "", answer_id: "x", answer_corpus: "widgets", score: 0.5}
|
||||||
|
]
|
||||||
|
}')")"
|
||||||
|
RECORDED="$(echo "$BULK_RESP" | jq -r '.recorded')"
|
||||||
|
FAIL="$(echo "$BULK_RESP" | jq -r '.failed')"
|
||||||
|
GOT_PB_A="$(echo "$BULK_RESP" | jq -r '.results[0].playbook_id // empty')"
|
||||||
|
ERR_BAD="$(echo "$BULK_RESP" | jq -r '.results[2].error // empty')"
|
||||||
|
if [ "$RECORDED" = "2" ] && [ "$FAIL" = "1" ] && [ -n "$GOT_PB_A" ] && [ -n "$ERR_BAD" ]; then
|
||||||
|
echo " ✓ 2 recorded, 1 failed (empty query_text caught), per-entry IDs/errors returned"
|
||||||
|
else
|
||||||
|
echo " ✗ recorded=$RECORDED failed=$FAIL pb_a=$GOT_PB_A err=$ERR_BAD"
|
||||||
|
echo " full: $BULK_RESP"
|
||||||
|
FAILED=1
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$FAILED" -eq 0 ]; then
|
if [ "$FAILED" -eq 0 ]; then
|
||||||
echo "[playbook-smoke] Playbook acceptance gate: PASSED"
|
echo "[playbook-smoke] Playbook acceptance gate: PASSED"
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user