## 新规则(共 6 种,增加 3 种) ### semantic_similarity - 调用 OpenAI 兼容 embedding API(asyncio.gather 并发两路请求) - 余弦相似度与 reference 比对,min_score 可配置(默认 0.7) - API 异常时明确返回失败原因,不隐藏错误 ### json_schema - 验证回复是否为合法 JSON(支持 markdown 代码块剥离) - required_keys / forbidden_keys / key_types 三维校验 - dot-path 支持嵌套字段("data.id") - strict_json=false 模式非阻断校验 ### safety - 双层检测:关键词黑名单(零延迟)+ 可选 moderation API - API 不可用时自动降级黑名单,不中止评测 - 支持自定义 flagged_categories ## 规则组合逻辑(rule_logic + rule_pass_threshold) - models.py: EvalRuleConfig 增加 weight 字段;Case 增加 rule_logic / rule_pass_threshold - models.py: 新增 RuleLogic 枚举(all / any / weighted) - engine._save_rule_results: 按 rule_logic 决定 case 通过/失败 - ALL:全部通过才通过(原有行为,向下兼容) - ANY:至少一条通过即通过 - WEIGHTED:加权平均分 >= rule_pass_threshold ## 测试(43 → 76,新增 33) - test_s2_rules_and_logic.py:3 个新规则的 pass/fail/边界/API 降级 + 5 个组合逻辑集成测试 Co-Authored-By: Claude <noreply@anthropic.com>
161 lines
4.3 KiB
Python
161 lines
4.3 KiB
Python
"""Shared Pydantic models for AgentEvalTool."""
|
|
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
from typing import Any, Optional
|
|
|
|
from pydantic import BaseModel, Field, field_validator
|
|
|
|
|
|
class PlatformType(str, Enum):
|
|
AI_DIGITAL_EMPLOYEE = "ai_digital_employee"
|
|
AI_ASSISTANT = "ai_assistant"
|
|
|
|
|
|
class ChannelType(str, Enum):
|
|
TUTU_API = "tutu-api"
|
|
OPENCLAW = "openclaw"
|
|
HTTP = "http"
|
|
|
|
|
|
class TargetStatus(str, Enum):
|
|
ACTIVE = "active"
|
|
INACTIVE = "inactive"
|
|
ERROR = "error"
|
|
|
|
|
|
class CaseType(str, Enum):
|
|
SINGLE = "single"
|
|
MULTI_TURN = "multi_turn"
|
|
DYNAMIC = "dynamic"
|
|
|
|
|
|
class EvalTarget(BaseModel):
|
|
"""Evaluation target (the agent being evaluated)."""
|
|
|
|
id: Optional[str] = None
|
|
name: str
|
|
description: str = ""
|
|
platform: PlatformType = PlatformType.AI_DIGITAL_EMPLOYEE
|
|
channel_type: ChannelType = ChannelType.TUTU_API
|
|
channel_config: dict[str, Any] = Field(default_factory=dict)
|
|
status: TargetStatus = TargetStatus.ACTIVE
|
|
created_at: Optional[datetime] = None
|
|
updated_at: Optional[datetime] = None
|
|
|
|
|
|
class Expectation(BaseModel):
|
|
"""Expected behavior for a test case."""
|
|
|
|
intent: Optional[str] = None
|
|
keywords_include: list[str] = Field(default_factory=list)
|
|
keywords_exclude: list[str] = Field(default_factory=list)
|
|
response_time_max_ms: Optional[int] = None
|
|
coherence_min_score: Optional[float] = None
|
|
|
|
|
|
class EvalRuleConfig(BaseModel):
|
|
"""Configuration for an evaluation rule."""
|
|
|
|
type: str
|
|
params: dict[str, Any] = Field(default_factory=dict)
|
|
weight: float = 1.0 # used when rule_logic == "weighted"
|
|
|
|
|
|
class RuleLogic(str, Enum):
|
|
"""How to combine multiple rule results for a case."""
|
|
|
|
ALL = "all" # all rules must pass (default)
|
|
ANY = "any" # at least one rule must pass
|
|
WEIGHTED = "weighted" # weighted average score >= threshold
|
|
|
|
|
|
class Case(BaseModel):
|
|
"""A single evaluation case within a scenario."""
|
|
|
|
id: str
|
|
type: CaseType = CaseType.SINGLE
|
|
messages: list[str] = Field(default_factory=list)
|
|
prompt: Optional[str] = None
|
|
turns: int = 3
|
|
expectations: Expectation = Field(default_factory=Expectation)
|
|
eval_rules: list[EvalRuleConfig] = Field(default_factory=list)
|
|
rule_logic: RuleLogic = RuleLogic.ALL
|
|
rule_pass_threshold: float = 0.6 # used when rule_logic == "weighted"
|
|
|
|
@field_validator("messages")
|
|
@classmethod
|
|
def messages_not_empty(cls, v: list[str], info) -> list[str]:
|
|
data = info.data
|
|
case_type = data.get("type") if data else None
|
|
if case_type and case_type != CaseType.DYNAMIC and not v:
|
|
raise ValueError("messages must not be empty for non-dynamic cases")
|
|
return v
|
|
|
|
|
|
class Scenario(BaseModel):
|
|
"""A collection of evaluation cases."""
|
|
|
|
id: Optional[str] = None
|
|
name: str
|
|
description: str = ""
|
|
tags: list[str] = Field(default_factory=list)
|
|
cases: list[Case] = Field(default_factory=list)
|
|
llm_config: Optional[dict[str, Any]] = None
|
|
created_at: Optional[datetime] = None
|
|
updated_at: Optional[datetime] = None
|
|
|
|
@field_validator("cases")
|
|
@classmethod
|
|
def cases_not_empty(cls, v: list[Case]) -> list[Case]:
|
|
if not v:
|
|
raise ValueError("scenario must contain at least one case")
|
|
return v
|
|
|
|
|
|
class RunStatus(str, Enum):
|
|
PENDING = "pending"
|
|
RUNNING = "running"
|
|
COMPLETED = "completed"
|
|
FAILED = "failed"
|
|
|
|
|
|
class EvalRun(BaseModel):
|
|
"""A single evaluation run."""
|
|
|
|
id: Optional[str] = None
|
|
target_id: str
|
|
scenario_id: str
|
|
status: RunStatus = RunStatus.PENDING
|
|
started_at: Optional[datetime] = None
|
|
completed_at: Optional[datetime] = None
|
|
summary: Optional[dict[str, Any]] = None
|
|
|
|
|
|
class Turn(BaseModel):
|
|
"""A single turn in a conversation during evaluation."""
|
|
|
|
id: Optional[str] = None
|
|
run_id: str
|
|
case_id: str
|
|
round_index: int
|
|
sent_message: dict[str, Any] = Field(default_factory=dict)
|
|
sent_at: Optional[datetime] = None
|
|
question_msg_id: Optional[str] = None
|
|
reply: Optional[dict[str, Any]] = None
|
|
received_at: Optional[datetime] = None
|
|
latency_ms: Optional[int] = None
|
|
|
|
|
|
class EvalResult(BaseModel):
|
|
"""Result of applying one evaluation rule to one turn."""
|
|
|
|
id: Optional[str] = None
|
|
run_id: str
|
|
case_id: str
|
|
turn_id: str
|
|
rule_type: str
|
|
passed: bool
|
|
score: Optional[float] = None
|
|
reason: str = ""
|