mirror of
https://github.com/block/buzz.git
synced 2026-10-28 21:11:48 +00:00
## Why Evaluating buzz agent memory retrieval by seeding a memory then asking the buzz agent a question it needs that memory. **Bug Found**: System prompt had no inclusion of retrieving cold memories and suggested looking in a mem/*.md directory that does not exist. Updated `system-prompt.md` to include memory CLI tools and usage. Eval Before System Prompt Change: 0/3 Eval After System Prompt Change: 3/3 ## What - Add a `memory-retrieval` benchmark that seeds agent memory with `buzz mem set` before asking a direct question. - Grade the observable threaded answer without inspecting tool calls or exposing the answer in channel history. - Teach agents to use `buzz mem set`, `buzz mem ls`, and `buzz mem get` for cold memory. - Add a wire-debug endpoint configuration for diagnosing ACP tool calls in local runs. - Add fixture, seeding, verifier, and prompt coverage. ## Risk Assessment Low. The runtime changes are limited to the benchmark harness. The production-facing change clarifies existing memory commands in the base prompt; it does not change memory storage, relay behavior, or authorization. ## References - Before the system-prompt changes, 0/3 attempts passed because agents never invoked the `buzz mem` CLI and instead searched a non existent filesystem - After the changes, 3/3 attempts passed. ACP wire logs confirmed that every agent ran `buzz mem ls` followed by `buzz mem get` and returned `net_gpv`. --------- Signed-off-by: Philip Azar <pazar@squareup.com>
141 lines
4.9 KiB
Python
Executable File
141 lines
4.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Deterministic verifier for pre-seeded cold-memory retrieval."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
EXPECTED_CUSTOMERS = 352_345
|
|
ALLOWED_CONTEXT_NUMBERS = frozenset({2024})
|
|
# Numbers that appear only in the distractor memories. Mentioning any of them
|
|
# means the answer pulled from the wrong memory (or dumped several), so it does
|
|
# not demonstrate that the correct value was selected.
|
|
DISTRACTOR_NUMBERS = frozenset(
|
|
{
|
|
361_250, # total-customers-per-month: monthly average
|
|
351_340, # customer-value-metric: last month's customers
|
|
2_400, # customer-value-metric: revenue per customer
|
|
325_401, # customers-metrics-spring-24: March total
|
|
3_710, # customers-metrics-spring-24: April active customers named John
|
|
21_604, # new-customers-april-2024: April new customers
|
|
}
|
|
)
|
|
NUMBER = re.compile(r"(?<![A-Za-z0-9_])-?\d[\d,]*(?:\.\d+)?")
|
|
|
|
|
|
def _zero() -> dict[str, float]:
|
|
return {
|
|
"reward": 0.0,
|
|
"answer_correct": 0.0,
|
|
"threaded_reply": 0.0,
|
|
"evidence_complete": 0.0,
|
|
}
|
|
|
|
|
|
def _numbers(content: str) -> list[float]:
|
|
values: list[float] = []
|
|
for token in NUMBER.findall(content):
|
|
try:
|
|
values.append(float(token.replace(",", "")))
|
|
except ValueError:
|
|
continue
|
|
return values
|
|
|
|
|
|
def score_evidence(evidence: object) -> tuple[dict[str, float], dict[str, Any]]:
|
|
if not isinstance(evidence, dict):
|
|
return _zero(), {"error": "evidence root is not an object"}
|
|
|
|
identities = evidence.get("identities", {})
|
|
agents = (
|
|
[
|
|
row
|
|
for row in identities.values()
|
|
if isinstance(row, dict) and row.get("role") == "orchestrator"
|
|
]
|
|
if isinstance(identities, dict)
|
|
else []
|
|
)
|
|
agent_pubkey = agents[0].get("pubkey") if len(agents) == 1 else None
|
|
question_id = evidence.get("task_event_id")
|
|
trial = evidence.get("trial", {})
|
|
question_channel = trial.get("channel_id") if isinstance(trial, dict) else None
|
|
|
|
messages = [row for row in evidence.get("messages", []) if isinstance(row, dict)]
|
|
replies = [
|
|
row
|
|
for row in messages
|
|
if agent_pubkey
|
|
and row.get("pubkey") == agent_pubkey
|
|
and row.get("channel_id") == question_channel
|
|
and row.get("reply_to_event_id") == question_id
|
|
]
|
|
answer = replies[-1] if replies else None
|
|
content = str(answer.get("content", "")) if answer else ""
|
|
values = _numbers(content)
|
|
mentions_expected = any(value == EXPECTED_CUSTOMERS for value in values)
|
|
mentions_distractor = any(value in DISTRACTOR_NUMBERS for value in values)
|
|
noise_numbers = [
|
|
value
|
|
for value in values
|
|
if value != EXPECTED_CUSTOMERS and value not in ALLOWED_CONTEXT_NUMBERS
|
|
]
|
|
answer_correct = float(mentions_expected and not noise_numbers)
|
|
threaded_reply = float(answer is not None)
|
|
evidence_complete = float(
|
|
evidence.get("schema_version") == 1
|
|
and evidence.get("task_name") == "memory-retrieval"
|
|
and evidence.get("truncated") is False
|
|
and len(agents) == 1
|
|
and isinstance(question_id, str)
|
|
and isinstance(question_channel, str)
|
|
)
|
|
|
|
structural_score = float(threaded_reply == 1.0 and evidence_complete == 1.0)
|
|
metrics = {
|
|
"reward": answer_correct * structural_score,
|
|
"answer_correct": answer_correct,
|
|
"threaded_reply": threaded_reply,
|
|
"evidence_complete": evidence_complete,
|
|
}
|
|
return metrics, {
|
|
"question_event_id": question_id,
|
|
"question_channel_id": question_channel,
|
|
"answer_message_id": answer.get("id") if answer else None,
|
|
"answer_content": content,
|
|
"parsed_numbers": values,
|
|
"expected_customers": EXPECTED_CUSTOMERS,
|
|
"mentions_expected": mentions_expected,
|
|
"mentions_distractor": mentions_distractor,
|
|
"noise_numbers": noise_numbers,
|
|
"allowed_context_numbers": sorted(ALLOWED_CONTEXT_NUMBERS),
|
|
"distractor_numbers": sorted(DISTRACTOR_NUMBERS),
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--evidence", type=Path, required=True)
|
|
parser.add_argument("--reward", type=Path, required=True)
|
|
parser.add_argument("--details", type=Path, required=True)
|
|
args = parser.parse_args()
|
|
try:
|
|
metrics, details = score_evidence(
|
|
json.loads(args.evidence.read_text(encoding="utf-8"))
|
|
)
|
|
except (OSError, json.JSONDecodeError) as error:
|
|
metrics, details = _zero(), {"error": str(error)}
|
|
args.reward.write_text(json.dumps(metrics, sort_keys=True) + "\n", encoding="utf-8")
|
|
args.details.write_text(
|
|
json.dumps(details, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|