## 新增功能 - 文件管理模块:分类树 + 文件上传/下载/删除 - 文件上传支持拖拽(Dragger)+ 手动上传(customRequest 模式) ## 页面布局统一(参照评测执行页) - 仪表盘/评测对象/评测场景/评测报告 全部改为全高 flex 布局 - 统一内联页头样式(h2 + 竖线分隔 + 描述) - 表格撑满高度、overflow 处理 - 每页添加刷新按钮 ## Bug 修复 - 分类树操作按钮 hover 不可见(CSS 规则缺失) - 文件上传失败(multipart boundary 缺失) - LLM API 响应 content blocks 数组格式支持(_extract_content_from_api_response) - response_time_max_ms 被静默忽略(隐式规则传空 params) - 空 messages 导致 IndexError 崩溃 - poll_reply 异常中止整个 run(缺 try/catch) - engine finally 未关闭 session - 3 个页面 UTC 时间戳解析偏差 8 小时 ## 后端 - EvalEngine: poll_reply 异常保护、空 dialog 保护、session 关闭 - LLM API 响应解析支持 content-block-array 格式 - 隐式 response_time 规则正确传递 max_ms 参数 ## 前端 - api.ts: 移除手动 Content-Type(让浏览器自动添加 boundary) - Files.tsx: customRequest 替代 beforeUpload、布局优化 - index.css: 分类树 hover 规则 - Targets/Scenarios/Home/Reports: 全高布局改造 - 3 个页面时间戳改用 formatDateTime()(修复 UTC 偏差) Co-Authored-By: Claude <noreply@anthropic.com>
150 lines
3.9 KiB
Python
150 lines
3.9 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)
|
|
|
|
|
|
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)
|
|
|
|
@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 = ""
|