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>
144 lines
5.2 KiB
Python
144 lines
5.2 KiB
Python
"""Thread-safe monotonic counter tree utility for shared application state."""
|
|
|
|
import copy
|
|
import threading
|
|
from collections.abc import Mapping
|
|
from typing import Any
|
|
|
|
COUNTER_TREE_KEY = "_counter_tree"
|
|
COUNTER_LOCK_KEY = "_counter_tree_lock"
|
|
_COUNTER_INIT_LOCK = threading.Lock()
|
|
|
|
|
|
def _get_counter_lock(metadata: dict[str, Any]) -> Any:
|
|
"""Return the metadata-scoped lock, creating it once when needed."""
|
|
lock = metadata.get(COUNTER_LOCK_KEY)
|
|
if lock is not None:
|
|
return lock
|
|
|
|
# Two threads may reach the first counter operation concurrently. Guard
|
|
# initialization so they cannot install and then use different locks.
|
|
with _COUNTER_INIT_LOCK:
|
|
lock = metadata.get(COUNTER_LOCK_KEY)
|
|
if lock is None:
|
|
lock = threading.Lock()
|
|
metadata[COUNTER_LOCK_KEY] = lock
|
|
return lock
|
|
|
|
|
|
def global_counter_add_many(
|
|
metadata: dict[str, Any],
|
|
updates: Mapping[tuple[str, ...], int],
|
|
) -> dict[tuple[str, ...], int]:
|
|
"""Atomically fetch-and-add multiple counter paths.
|
|
|
|
All paths are validated before the counter tree is mutated. The returned
|
|
mapping contains each path's value immediately before its increment.
|
|
"""
|
|
normalized = dict(updates)
|
|
for path, value in normalized.items():
|
|
if not isinstance(path, tuple) or not all(isinstance(part, str) for part in path):
|
|
raise TypeError("counter paths must be tuples of strings")
|
|
if not isinstance(value, int):
|
|
raise TypeError("counter increments must be integers")
|
|
if not normalized:
|
|
return {}
|
|
|
|
lock = _get_counter_lock(metadata)
|
|
with lock:
|
|
tree = metadata.get(COUNTER_TREE_KEY)
|
|
if tree is None:
|
|
tree = {"value": 0, "children": {}}
|
|
metadata[COUNTER_TREE_KEY] = tree
|
|
|
|
nodes: dict[tuple[str, ...], dict[str, Any]] = {}
|
|
for path in normalized:
|
|
node = tree
|
|
for part in path:
|
|
child = node["children"].get(part)
|
|
if child is None:
|
|
child = {"value": 0, "children": {}}
|
|
node["children"][part] = child
|
|
node = child
|
|
nodes[path] = node
|
|
|
|
previous = {path: node["value"] for path, node in nodes.items()}
|
|
for path, value in normalized.items():
|
|
nodes[path]["value"] += value
|
|
return previous
|
|
|
|
|
|
def global_counter_add(metadata: dict[str, Any], key: list[str], val: int) -> int:
|
|
"""Fetch-and-add: return the old value for ``key``, then add ``val`` to it.
|
|
|
|
Walks the counter tree stored in ``metadata`` along ``key``, creating
|
|
missing nodes on the way, then returns the target node's current counter
|
|
value and adds ``val`` to it. Counters start at 0, so the first call
|
|
returns 0. An empty ``key`` targets the root node, which serves as a
|
|
process-wide thread-safe global counter.
|
|
|
|
The counter tree (``{"value": 0, "children": {}}``) and its
|
|
:class:`threading.Lock` are expected to live in ``metadata`` under
|
|
:data:`COUNTER_TREE_KEY` and :data:`COUNTER_LOCK_KEY` respectively.
|
|
If they are missing they are created lazily so the function is safe to
|
|
call with a plain ``dict``.
|
|
"""
|
|
path = tuple(key)
|
|
return global_counter_add_many(metadata, {path: val})[path]
|
|
|
|
|
|
def global_counter_inc(metadata: dict[str, Any], key: list[str]) -> int:
|
|
"""Fetch-and-increment: return the old value for ``key``, then add 1.
|
|
|
|
Counters start at 0, so the first call returns 0. See
|
|
:func:`global_counter_add` for details on the counter tree layout.
|
|
"""
|
|
return global_counter_add(metadata, key, 1)
|
|
|
|
|
|
def global_counter_get(metadata: dict[str, Any], key: list[str]) -> int:
|
|
"""Return the current value for ``key`` without modifying the tree.
|
|
|
|
Unlike :func:`global_counter_add`, missing nodes are never created; a
|
|
path that does not exist yet is reported as 0, matching the value the
|
|
node would hold right before its first increment.
|
|
"""
|
|
lock = _get_counter_lock(metadata)
|
|
|
|
with lock:
|
|
tree = metadata.get(COUNTER_TREE_KEY)
|
|
if tree is None:
|
|
return 0
|
|
|
|
node: dict[str, Any] | None = tree
|
|
for part in key:
|
|
assert isinstance(part, str)
|
|
node = node["children"].get(part)
|
|
if node is None:
|
|
return 0
|
|
return node["value"]
|
|
|
|
|
|
def global_counter_get_all(metadata: dict[str, Any], key: list[str]) -> dict[str, Any] | None:
|
|
"""Return a deep copy of the subtree rooted at ``key``, or ``None``.
|
|
|
|
Walks the counter tree along ``key`` without creating missing nodes and
|
|
returns a deep copy of the node found there (``{"value": ..., "children":
|
|
...}``), so callers can inspect it without racing concurrent updates.
|
|
Returns ``None`` when the tree or any part of ``key`` does not exist.
|
|
An empty ``key`` returns a copy of the whole tree.
|
|
"""
|
|
lock = _get_counter_lock(metadata)
|
|
|
|
with lock:
|
|
tree = metadata.get(COUNTER_TREE_KEY)
|
|
if tree is None:
|
|
return None
|
|
|
|
node: dict[str, Any] | None = tree
|
|
for part in key:
|
|
assert isinstance(part, str)
|
|
node = node["children"].get(part)
|
|
if node is None:
|
|
return None
|
|
return copy.deepcopy(node)
|