mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-08-28 05:25:04 +00:00
Some checks are pending
Pre-commit / run (ubuntu-latest) (push) Waiting to run
Tests ReMe / Unit Tests - py3.11 (push) Waiting to run
Tests ReMe / Unit Tests - py3.12 (push) Waiting to run
Tests ReMe / Unit Tests - py3.13 (push) Waiting to run
Windows Smoke / CLI smoke - py3.11 (push) Waiting to run
* feat(counter): extend counter tree utils and record job call statistics - replace global_counter_next with fetch-and-add style global_counter_add/inc, plus read-only global_counter_get and global_counter_get_all - record per-job call counts in app_context.metadata via BaseJob._record_call, covering background/cron/stream jobs - update agentic_answer step and utils exports; add unit tests for job counting and counter utils * feat(evaluation): add check_job_count interface and report search calls in benchmarks - Extract _counter_key from BaseJob._record_call for reusable counter lookup - Add reme.utils.evaluation_interface.check_job_count read-only helper - Track and report average search calls per query in beam and longmemeval benchmarks * job counter * token消耗量统计 * benchmark输出完整token消耗统计 * benchmark统计输出改用标准差 - beam/longmemeval 的工具调用与 token 统计由方差改为标准差输出 - 修复 lint: 局部变量遮蔽 importlib.metadata、补充测试 docstring - black 格式化 * fix(evaluation): preserve complete token usage metrics * fix: exclude stream replies from token accounting * Revert "fix: exclude stream replies from token accounting" This reverts commit85bf32064d. * Reapply "fix: exclude stream replies from token accounting" This reverts commit6722c24dc5. * support agent scope 2.0.5 * feat: support injection_config to disable runtime state injection in benchmarks - Add InjectionConfig passthrough in AsAgentWrapper.reply() - Disable inject_runtime_state in BaseAgenticAnswerStep to avoid wall-clock time conflicting with benchmark query_time anchors - Disable inject_runtime_state in beam/lme llm_judge calls * feat: agentscope dual-version compat & benchmark improvements - Add version_tuple utility for semantic version comparison - AsAgentWrapper: version-aware InjectionConfig, max_iters doubling, and token usage collection (reply vs reply_stream) for AS>=2.0.5/<2.0.5 - Default inject_runtime_state=False in wrapper to avoid benchmark time-anchor conflicts; remove per-callsite injection_config overrides - longmemeval run.py: support question_ids filter in dataset config - Fix unused import in test_evaluation_interface; format fixes * chore: remove temporary flip-test benchmark config * revert: pin agentscope to 2.0.4.post1 and drop dual-version compat * fix(evaluation): clarify usage semantics and atomic counters --------- Co-authored-by: sa-buc <jiangniurou.xyf@dail-algo011164204033.ET135> Co-authored-by: jinli.yl <jinli.yl@alibaba-inc.com>
261 lines
8.2 KiB
Python
261 lines
8.2 KiB
Python
"""Tests for small utilities in ``reme.utils``."""
|
|
|
|
import asyncio
|
|
import sys
|
|
import threading
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from reme.utils import common_utils
|
|
from reme.utils.counter import (
|
|
COUNTER_LOCK_KEY,
|
|
COUNTER_TREE_KEY,
|
|
global_counter_add,
|
|
global_counter_add_many,
|
|
global_counter_get,
|
|
global_counter_get_all,
|
|
global_counter_inc,
|
|
)
|
|
from reme.utils.similarity_utils import batch_cosine_similarity, cosine_similarity
|
|
|
|
|
|
def test_batch_cosine_similarity_rejects_1d_inputs():
|
|
"""1D vectors should fail with a clear validation error, not IndexError."""
|
|
with pytest.raises(ValueError, match="Expected 2D arrays"):
|
|
batch_cosine_similarity(np.array([1.0, 0.0]), np.array([[1.0, 0.0]]))
|
|
|
|
|
|
def test_batch_cosine_similarity_pairwise_matrix():
|
|
"""Batch cosine returns the full pairwise matrix for valid 2D inputs."""
|
|
result = batch_cosine_similarity(
|
|
np.array([[1.0, 0.0], [0.0, 1.0]]),
|
|
np.array([[1.0, 0.0], [1.0, 1.0]]),
|
|
)
|
|
|
|
assert result.shape == (2, 2)
|
|
np.testing.assert_allclose(result[0], [1.0, 2**-0.5])
|
|
np.testing.assert_allclose(result[1], [0.0, 2**-0.5])
|
|
|
|
|
|
def test_cosine_similarity_rejects_mismatched_lengths():
|
|
"""Single-vector cosine validates dimensions before computing."""
|
|
with pytest.raises(ValueError, match="Vectors must have same length"):
|
|
cosine_similarity([1.0], [1.0, 2.0])
|
|
|
|
|
|
def test_mock_reme_server_uses_reme_entrypoint(monkeypatch):
|
|
"""The test server helper should spawn the reme CLI module, not legacy reme."""
|
|
captured: dict[str, list[str]] = {}
|
|
|
|
class DummyProcess:
|
|
"""Process stub returned by the patched Popen."""
|
|
|
|
stdout = None
|
|
|
|
def poll(self):
|
|
"""Return a successful process status."""
|
|
return 0
|
|
|
|
def fake_popen(cmd, **_kwargs):
|
|
"""Capture the spawned command."""
|
|
captured["cmd"] = cmd
|
|
return DummyProcess()
|
|
|
|
async def fake_wait_ready(_host, _port, _timeout):
|
|
return None
|
|
|
|
monkeypatch.setattr(common_utils.subprocess, "Popen", fake_popen)
|
|
monkeypatch.setattr(common_utils, "_wait_reme_ready", fake_wait_ready)
|
|
|
|
async def run():
|
|
async with common_utils.mock_reme_server(port=45678, log_to_file=False, enable_logo=False):
|
|
pass
|
|
|
|
asyncio.run(run())
|
|
|
|
assert captured["cmd"][:4] == [sys.executable, "-m", "reme.reme", "start"]
|
|
|
|
|
|
def test_inc_returns_old_value_starting_at_zero():
|
|
"""First call returns 0, then values increase by 1 per call."""
|
|
metadata: dict = {}
|
|
|
|
assert global_counter_inc(metadata, ["a"]) == 0
|
|
assert global_counter_inc(metadata, ["a"]) == 1
|
|
assert global_counter_inc(metadata, ["a"]) == 2
|
|
|
|
|
|
def test_add_returns_old_value_and_adds_val():
|
|
"""``add`` is fetch-and-add: old value out, ``val`` added in."""
|
|
metadata: dict = {}
|
|
|
|
assert global_counter_add(metadata, ["a"], 10) == 0
|
|
assert global_counter_add(metadata, ["a"], 5) == 10
|
|
assert global_counter_inc(metadata, ["a"]) == 15
|
|
assert global_counter_get(metadata, ["a"]) == 16
|
|
|
|
|
|
def test_add_many_returns_old_values_and_updates_all_paths():
|
|
"""``add_many`` updates sibling metrics under one counter-tree lock."""
|
|
metadata: dict = {}
|
|
global_counter_add(metadata, ["usage", "input"], 2)
|
|
|
|
previous = global_counter_add_many(
|
|
metadata,
|
|
{
|
|
("usage", "input"): 3,
|
|
("usage", "output"): 4,
|
|
("usage", "total"): 7,
|
|
},
|
|
)
|
|
|
|
assert previous == {
|
|
("usage", "input"): 2,
|
|
("usage", "output"): 0,
|
|
("usage", "total"): 0,
|
|
}
|
|
assert global_counter_get(metadata, ["usage", "input"]) == 5
|
|
assert global_counter_get(metadata, ["usage", "output"]) == 4
|
|
assert global_counter_get(metadata, ["usage", "total"]) == 7
|
|
|
|
|
|
def test_add_many_holds_the_counter_lock_once_for_the_batch():
|
|
"""A multi-metric update is one critical section, not several writes."""
|
|
|
|
class CountingLock:
|
|
"""Lock test double that counts entered critical sections."""
|
|
|
|
def __init__(self):
|
|
self.lock = threading.Lock()
|
|
self.entries = 0
|
|
|
|
def __enter__(self):
|
|
self.lock.acquire()
|
|
self.entries += 1
|
|
return self
|
|
|
|
def __exit__(self, *_args):
|
|
self.lock.release()
|
|
|
|
lock = CountingLock()
|
|
metadata = {COUNTER_LOCK_KEY: lock}
|
|
|
|
global_counter_add_many(
|
|
metadata,
|
|
{
|
|
("usage", "input"): 3,
|
|
("usage", "output"): 4,
|
|
("usage", "total"): 7,
|
|
},
|
|
)
|
|
|
|
assert lock.entries == 1
|
|
|
|
|
|
def test_add_many_validates_every_update_before_mutating():
|
|
"""One invalid update leaves every valid counter unchanged."""
|
|
metadata: dict = {}
|
|
|
|
with pytest.raises(TypeError, match="increments must be integers"):
|
|
global_counter_add_many(
|
|
metadata,
|
|
{
|
|
("usage", "input"): 3,
|
|
("usage", "output"): "invalid",
|
|
},
|
|
)
|
|
|
|
assert COUNTER_TREE_KEY not in metadata
|
|
|
|
|
|
def test_counters_are_isolated_by_key_path():
|
|
"""Sibling and nested keys, plus the root, hold independent counters."""
|
|
metadata: dict = {}
|
|
|
|
assert global_counter_inc(metadata, ["a"]) == 0
|
|
assert global_counter_inc(metadata, ["b"]) == 0
|
|
assert global_counter_inc(metadata, ["a", "child"]) == 0
|
|
assert global_counter_inc(metadata, []) == 0
|
|
|
|
assert global_counter_get(metadata, ["a"]) == 1
|
|
assert global_counter_get(metadata, ["b"]) == 1
|
|
assert global_counter_get(metadata, ["a", "child"]) == 1
|
|
assert global_counter_get(metadata, []) == 1
|
|
|
|
|
|
def test_get_does_not_create_missing_nodes():
|
|
"""``get`` reports 0 for missing paths and leaves the tree untouched."""
|
|
metadata: dict = {}
|
|
|
|
assert global_counter_get(metadata, ["missing"]) == 0
|
|
assert COUNTER_TREE_KEY not in metadata
|
|
|
|
global_counter_inc(metadata, ["a"])
|
|
assert global_counter_get(metadata, ["a", "missing"]) == 0
|
|
assert "missing" not in metadata[COUNTER_TREE_KEY]["children"]["a"]["children"]
|
|
|
|
|
|
def test_get_all_returns_none_for_missing_key():
|
|
"""``get_all`` returns None when the tree or the path does not exist."""
|
|
metadata: dict = {}
|
|
|
|
assert global_counter_get_all(metadata, []) is None
|
|
assert global_counter_get_all(metadata, ["missing"]) is None
|
|
|
|
global_counter_inc(metadata, ["a"])
|
|
assert global_counter_get_all(metadata, ["missing"]) is None
|
|
assert global_counter_get_all(metadata, ["a", "missing"]) is None
|
|
|
|
|
|
def test_get_all_returns_subtree_and_whole_tree():
|
|
"""``get_all`` returns the node at ``key``; an empty key returns the root."""
|
|
metadata: dict = {}
|
|
global_counter_add(metadata, ["a"], 2)
|
|
global_counter_add(metadata, ["a", "child"], 3)
|
|
|
|
subtree = global_counter_get_all(metadata, ["a"])
|
|
assert subtree == {"value": 2, "children": {"child": {"value": 3, "children": {}}}}
|
|
|
|
root = global_counter_get_all(metadata, [])
|
|
assert root["value"] == 0
|
|
assert root["children"]["a"] == subtree
|
|
|
|
|
|
def test_get_all_returns_deep_copy():
|
|
"""Mutating the returned subtree must not affect the live counter tree."""
|
|
metadata: dict = {}
|
|
global_counter_add(metadata, ["a", "child"], 3)
|
|
|
|
subtree = global_counter_get_all(metadata, ["a"])
|
|
subtree["value"] = 999
|
|
subtree["children"]["child"]["value"] = 999
|
|
subtree["children"]["extra"] = {"value": 1, "children": {}}
|
|
|
|
assert global_counter_get(metadata, ["a"]) == 0
|
|
assert global_counter_get(metadata, ["a", "child"]) == 3
|
|
assert global_counter_get_all(metadata, ["a", "extra"]) is None
|
|
|
|
|
|
def test_concurrent_inc_yields_unique_values():
|
|
"""Parallel increments on one key never return duplicate values."""
|
|
metadata: dict = {}
|
|
results: list[int] = []
|
|
results_lock = threading.Lock()
|
|
calls_per_thread = 200
|
|
|
|
def worker():
|
|
for _ in range(calls_per_thread):
|
|
value = global_counter_inc(metadata, ["shared"])
|
|
with results_lock:
|
|
results.append(value)
|
|
|
|
threads = [threading.Thread(target=worker) for _ in range(8)]
|
|
for thread in threads:
|
|
thread.start()
|
|
for thread in threads:
|
|
thread.join()
|
|
|
|
total = len(threads) * calls_per_thread
|
|
assert sorted(results) == list(range(total))
|
|
assert global_counter_get(metadata, ["shared"]) == total
|