AgentEvalTool/tests/unit/test_response_time_extended.py
sinohqb c1a3cdbaa9
All checks were successful
CI / test (pull_request) Successful in 4m5s
fix(rules): 修复代码审查发现的三处规则层缺陷
- response_time: 仅配置 max_ms 时恢复 v0.3 评分语义(最后一轮评分 + 超限线性惩罚),扩展指标共存时才用均值评分
- safety: 移除 moderation API 的黑名单命中跳过守卫,两层安全检查独立执行、发现均上报
- llm_score: 多维度评分添加 Semaphore 并发上限(5),防止维度数多时无限扇出模型请求
2026-08-25 18:13:11 +08:00

199 lines
7.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Tests for extended response_time rule with multiple metrics."""
from datetime import datetime, timedelta
import pytest
from agenteval.evaluation.rules.base import get_rule
from agenteval.models import Case, Expectation, Turn
def _make_turn(round_index: int, latency_ms: int, sent_at: datetime | None = None, received_at: datetime | None = None) -> Turn:
"""Helper to create a Turn with latency."""
return Turn(
id=f"t-{round_index}",
run_id="r1",
case_id="c1",
round_index=round_index,
latency_ms=latency_ms,
sent_at=sent_at,
received_at=received_at,
)
@pytest.mark.asyncio
async def test_response_time_backward_compatible():
"""Single turn with max_ms threshold should work as before."""
rule = get_rule("response_time", {"max_ms": 5000})
case = Case(id="c1", messages=["hello"], expectations=Expectation())
dialog = [_make_turn(1, 3000)]
result = await rule.evaluate(case, dialog)
assert result.passed is True
assert "3000ms" in result.reason
@pytest.mark.asyncio
async def test_response_time_exceeds_threshold():
"""Single turn exceeding threshold should fail."""
rule = get_rule("response_time", {"max_ms": 2000})
case = Case(id="c1", messages=["hello"], expectations=Expectation())
dialog = [_make_turn(1, 3000)]
result = await rule.evaluate(case, dialog)
assert result.passed is False
assert "未通过" in result.reason
@pytest.mark.asyncio
async def test_response_time_avg_latency():
"""Average latency check across multiple turns."""
rule = get_rule("response_time", {"avg_latency_max_ms": 3000})
case = Case(id="c1", messages=["hello"], expectations=Expectation())
dialog = [
_make_turn(1, 2000),
_make_turn(2, 4000),
]
result = await rule.evaluate(case, dialog)
assert result.passed is True # avg = 3000, threshold = 3000
assert "平均延迟" in result.reason
assert result.details is not None
assert result.details["avg_latency_ms"] == 3000
@pytest.mark.asyncio
async def test_response_time_avg_latency_exceeded():
"""Average latency exceeding threshold should fail."""
rule = get_rule("response_time", {"avg_latency_max_ms": 2000})
case = Case(id="c1", messages=["hello"], expectations=Expectation())
dialog = [
_make_turn(1, 3000),
_make_turn(2, 4000),
]
result = await rule.evaluate(case, dialog)
assert result.passed is False # avg = 3500 > 2000
assert "未通过" in result.reason
@pytest.mark.asyncio
async def test_response_time_throughput():
"""Throughput check (turns per minute)."""
rule = get_rule("response_time", {"throughput_min": 10})
case = Case(id="c1", messages=["hello"], expectations=Expectation())
base_time = datetime(2026, 1, 1, 12, 0, 0)
dialog = [
_make_turn(1, 1000, sent_at=base_time, received_at=base_time + timedelta(seconds=1)),
_make_turn(2, 1000, sent_at=base_time + timedelta(seconds=2), received_at=base_time + timedelta(seconds=3)),
_make_turn(3, 1000, sent_at=base_time + timedelta(seconds=4), received_at=base_time + timedelta(seconds=5)),
]
# 3 turns in 5 seconds = 36 turns/min
result = await rule.evaluate(case, dialog)
assert result.passed is True
assert "吞吐量" in result.reason
assert "turns/min" in result.reason
@pytest.mark.asyncio
async def test_response_time_throughput_exceeded():
"""Throughput below threshold should fail."""
rule = get_rule("response_time", {"throughput_min": 100})
case = Case(id="c1", messages=["hello"], expectations=Expectation())
base_time = datetime(2026, 1, 1, 12, 0, 0)
dialog = [
_make_turn(1, 1000, sent_at=base_time, received_at=base_time + timedelta(seconds=10)),
_make_turn(2, 1000, sent_at=base_time + timedelta(seconds=20), received_at=base_time + timedelta(seconds=30)),
]
# 2 turns in 30 seconds = 4 turns/min < 100
result = await rule.evaluate(case, dialog)
assert result.passed is False
assert "未通过" in result.reason
@pytest.mark.asyncio
async def test_response_time_multiple_metrics():
"""Multiple metrics can be checked together."""
rule = get_rule("response_time", {
"max_ms": 5000,
"avg_latency_max_ms": 4000,
})
case = Case(id="c1", messages=["hello"], expectations=Expectation())
dialog = [
_make_turn(1, 3000),
_make_turn(2, 4500),
]
result = await rule.evaluate(case, dialog)
assert result.passed is True
assert "最后一轮" in result.reason
assert "平均延迟" in result.reason
@pytest.mark.asyncio
async def test_response_time_details_field():
"""Result should include details with latency statistics."""
rule = get_rule("response_time", {"max_ms": 5000})
case = Case(id="c1", messages=["hello"], expectations=Expectation())
dialog = [
_make_turn(1, 2000),
_make_turn(2, 3000),
_make_turn(3, 4000),
]
result = await rule.evaluate(case, dialog)
assert result.details is not None
assert result.details["latencies"] == [2000, 3000, 4000]
assert result.details["avg_latency_ms"] == 3000
assert result.details["min_latency_ms"] == 2000
assert result.details["max_latency_ms"] == 4000
@pytest.mark.asyncio
async def test_response_time_no_threshold():
"""No threshold configured should pass with basic info."""
rule = get_rule("response_time", {})
case = Case(id="c1", messages=["hello"], expectations=Expectation())
dialog = [_make_turn(1, 3000)]
result = await rule.evaluate(case, dialog)
assert result.passed is True
assert "3000ms" in result.reason
@pytest.mark.asyncio
async def test_response_time_empty_dialog():
"""Empty dialog should fail."""
rule = get_rule("response_time", {"max_ms": 5000})
case = Case(id="c1", messages=["hello"], expectations=Expectation())
result = await rule.evaluate(case, [])
assert result.passed is False
assert "无回复记录" in result.reason
@pytest.mark.asyncio
async def test_response_time_max_ms_only_exceed_penalty():
"""仅 max_ms 时保持 v0.3 语义超限有线性惩罚score = 1 - 超出/阈值)。"""
rule = get_rule("response_time", {"max_ms": 2000})
case = Case(id="c1", messages=["hello"], expectations=Expectation())
dialog = [_make_turn(1, 3000)]
result = await rule.evaluate(case, dialog)
assert result.passed is False
assert result.score == pytest.approx(0.5) # 1 - (3000-2000)/2000
@pytest.mark.asyncio
async def test_response_time_max_ms_only_last_turn_decides():
"""仅 max_ms 时保持 v0.3 语义:最后一轮决定评分,即使平均更快。"""
rule = get_rule("response_time", {"max_ms": 6000})
case = Case(id="c1", messages=["hello"], expectations=Expectation())
# 最后一轮 5000ms 未超限 → score 1.0(平均 3000ms 不影响)
dialog = [_make_turn(1, 1000), _make_turn(2, 5000)]
result = await rule.evaluate(case, dialog)
assert result.passed is True
assert result.score == pytest.approx(1.0)
@pytest.mark.asyncio
async def test_response_time_mixed_metrics_uses_average_score():
"""配置了扩展指标时 score 基于平均延迟。"""
rule = get_rule("response_time", {"max_ms": 6000, "avg_latency_max_ms": 4000})
case = Case(id="c1", messages=["hello"], expectations=Expectation())
dialog = [_make_turn(1, 2000), _make_turn(2, 4000)]
result = await rule.evaluate(case, dialog)
assert result.passed is True
assert result.score == pytest.approx(1.0) # avg=3000 <= 6000 → min(1, 6000/3000)=1