ReMe/reme/utils/evaluation_interface.py
xyf2020 6b035c6553
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(evaluation): track job calls and agent token usage in benchmarks (#406)
* 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 commit 85bf32064d.

* Reapply "fix: exclude stream replies from token accounting"

This reverts commit 6722c24dc5.

* 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>
2026-08-04 11:42:18 +08:00

191 lines
6.7 KiB
Python

"""Read-only evaluation helpers for application job execution statistics.
These helpers take before/after snapshots of application-lifetime counters.
They are intentionally not thread-safe request attribution: overlapping calls
in the same Application contribute to each other's deltas. They are intended
for the benchmark utilities, where each tracked evaluation runs without other
work sharing its Application instance.
"""
from typing import TYPE_CHECKING
from .counter import global_counter_get, global_counter_get_all
if TYPE_CHECKING:
from ..components.application_context import ApplicationContext
_TOKEN_METRICS = (
"input_tokens",
"output_tokens",
"total_tokens",
)
def check_job_count(job_name: str, app_context: "ApplicationContext") -> int:
"""Return the application-lifetime execution count for a registered job.
``app_context`` scopes the lookup because ReMe does not maintain a global
current Application instance. Unknown job names use the same ``KeyError``
contract as :meth:`Application.run_job`.
"""
if job_name not in app_context.jobs:
raise KeyError(f"Job '{job_name}' not found")
return global_counter_get(app_context.metadata, ["__job_counter", job_name])
class JobCountTracker:
"""Measure registered job calls made while this context is active.
Not thread-safe for per-request attribution; see the module docstring.
"""
def __init__(self, job_names: list[str], app_context: "ApplicationContext") -> None:
self.job_names = list(dict.fromkeys(job_names))
self.app_context = app_context
self._start_counts: dict[str, int] = {}
self.counts: dict[str, int] = {}
def __enter__(self) -> dict[str, int]:
self._start_counts = {name: check_job_count(name, self.app_context) for name in self.job_names}
return self.counts
def __exit__(self, exc_type, exc_value, traceback) -> bool:
self.counts.update(
{
name: check_job_count(name, self.app_context) - start_count
for name, start_count in self._start_counts.items()
},
)
return False
def track_job_counts(job_names: list[str], app_context: "ApplicationContext") -> JobCountTracker:
"""Return a context manager that reports call deltas for ``job_names``.
Example:
.. code-block:: python
with track_job_counts(["search"], app.context) as counts:
await app.run_job("agentic_answer", query="...")
assert counts == {"search": 2}
"""
return JobCountTracker(job_names, app_context)
def check_agent_token_count(
agent_name: str,
app_context: "ApplicationContext",
metric: str = "total_tokens",
) -> int:
"""Return one application-lifetime token metric for an agent wrapper.
The agent name is the configured ``agent_wrapper`` component name (for
example ``"bench"``), and ``metric`` is one leaf in ReMe's token counter
tree, such as ``input_tokens`` or ``total_tokens``.
"""
return global_counter_get(app_context.metadata, ["__token_counter", agent_name, metric])
def check_agent_token_usage(agent_name: str, app_context: "ApplicationContext") -> dict[str, int | None]:
"""Return all token metrics currently accumulated for one agent wrapper.
A metric remains ``None`` until the backend has reported it. This keeps
unavailable usage distinct from zero.
"""
tree = global_counter_get_all(app_context.metadata, ["__token_counter", agent_name])
children = tree.get("children", {}) if tree is not None else {}
usage: dict[str, int | None] = {}
for metric in _TOKEN_METRICS:
node = children.get(metric)
usage[metric] = node["value"] if node is not None else None
return usage
class AgentTokenCountTracker:
"""Measure one token metric for agent wrappers during a context block.
Not thread-safe for per-request attribution; see the module docstring.
"""
def __init__(
self,
agent_names: list[str],
app_context: "ApplicationContext",
metric: str = "total_tokens",
) -> None:
self.agent_names = list(dict.fromkeys(agent_names))
self.app_context = app_context
self.metric = metric
self._start_counts: dict[str, int] = {}
self.counts: dict[str, int] = {}
def __enter__(self) -> dict[str, int]:
self._start_counts = {
name: check_agent_token_count(name, self.app_context, self.metric) for name in self.agent_names
}
return self.counts
def __exit__(self, exc_type, exc_value, traceback) -> bool:
self.counts.update(
{
name: check_agent_token_count(name, self.app_context, self.metric) - start_count
for name, start_count in self._start_counts.items()
},
)
return False
def track_agent_token_counts(
agent_names: list[str],
app_context: "ApplicationContext",
metric: str = "total_tokens",
) -> AgentTokenCountTracker:
"""Return a context manager that reports agent token deltas.
Example:
.. code-block:: python
with track_agent_token_counts(["bench"], app.context) as counts:
await app.run_job("agentic_answer", query="...")
assert counts["bench"] > 0
"""
return AgentTokenCountTracker(agent_names, app_context, metric)
class AgentTokenUsageTracker:
"""Measure all token metrics for agent wrappers during a context block.
Not thread-safe for per-request attribution; see the module docstring.
"""
def __init__(self, agent_names: list[str], app_context: "ApplicationContext") -> None:
self.agent_names = list(dict.fromkeys(agent_names))
self.app_context = app_context
self._start_usage: dict[str, dict[str, int | None]] = {}
self.usages: dict[str, dict[str, int | None]] = {}
def __enter__(self) -> dict[str, dict[str, int | None]]:
self._start_usage = {name: check_agent_token_usage(name, self.app_context) for name in self.agent_names}
return self.usages
def __exit__(self, exc_type, exc_value, traceback) -> bool:
for name in self.agent_names:
end_usage = check_agent_token_usage(name, self.app_context)
delta: dict[str, int | None] = {}
for metric in _TOKEN_METRICS:
current = end_usage[metric]
start = self._start_usage[name][metric]
delta[metric] = None if current is None else current - (start or 0)
self.usages[name] = delta
return False
def track_agent_token_usage(
agent_names: list[str],
app_context: "ApplicationContext",
) -> AgentTokenUsageTracker:
"""Return a context manager that reports full per-agent token usage deltas."""
return AgentTokenUsageTracker(agent_names, app_context)