feat: add evaluation label mapping
This commit is contained in:
+37
-2
@@ -47,7 +47,12 @@ from src.agent_types import (
|
|||||||
)
|
)
|
||||||
from src.bundled_skills import ALWAYS_ENABLED_HIDDEN_SKILL_NAMES, get_bundled_skills
|
from src.bundled_skills import ALWAYS_ENABLED_HIDDEN_SKILL_NAMES, get_bundled_skills
|
||||||
from src.data_agent_inputs import DataAgentInputError, load_input_sources
|
from src.data_agent_inputs import DataAgentInputError, load_input_sources
|
||||||
from src.evaluation_runtime import EvaluationError, EvaluationRuntime
|
from src.evaluation_runtime import (
|
||||||
|
DEFAULT_CONCURRENCY,
|
||||||
|
MAX_CONCURRENCY,
|
||||||
|
EvaluationError,
|
||||||
|
EvaluationRuntime,
|
||||||
|
)
|
||||||
from src.jupyter_runtime import (
|
from src.jupyter_runtime import (
|
||||||
DEFAULT_JUPYTER_WORKSPACE_ROOT,
|
DEFAULT_JUPYTER_WORKSPACE_ROOT,
|
||||||
JupyterRuntimeError,
|
JupyterRuntimeError,
|
||||||
@@ -1145,6 +1150,14 @@ class EvaluationDatasetMappingRequest(BaseModel):
|
|||||||
mapping: dict[str, Any]
|
mapping: dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class EvaluationLabelMappingSuggestRequest(BaseModel):
|
||||||
|
account_id: str = Field(min_length=1)
|
||||||
|
mapping: dict[str, Any]
|
||||||
|
skill_name: str = 'label-master'
|
||||||
|
snapshot_id: str | None = None
|
||||||
|
model: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class EvaluationAnalyzeRequest(BaseModel):
|
class EvaluationAnalyzeRequest(BaseModel):
|
||||||
account_id: str = Field(min_length=1)
|
account_id: str = Field(min_length=1)
|
||||||
query: str = Field(min_length=1, max_length=20_000)
|
query: str = Field(min_length=1, max_length=20_000)
|
||||||
@@ -1165,7 +1178,11 @@ class EvaluationExperimentCreateRequest(BaseModel):
|
|||||||
skill_name: str = 'label-master'
|
skill_name: str = 'label-master'
|
||||||
snapshot_id: str | None = None
|
snapshot_id: str | None = None
|
||||||
model: str | None = None
|
model: str | None = None
|
||||||
concurrency: int = Field(default=2, ge=1, le=8)
|
concurrency: int = Field(
|
||||||
|
default=DEFAULT_CONCURRENCY,
|
||||||
|
ge=1,
|
||||||
|
le=MAX_CONCURRENCY,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class EvaluationSkillVersionSaveRequest(BaseModel):
|
class EvaluationSkillVersionSaveRequest(BaseModel):
|
||||||
@@ -1702,6 +1719,24 @@ def create_app(state: AgentState) -> FastAPI:
|
|||||||
except EvaluationError as exc:
|
except EvaluationError as exc:
|
||||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
@app.post('/api/evaluations/datasets/{dataset_id}/label-mapping/suggest')
|
||||||
|
async def suggest_evaluation_label_mapping(
|
||||||
|
dataset_id: str,
|
||||||
|
payload: EvaluationLabelMappingSuggestRequest,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
return await asyncio.to_thread(
|
||||||
|
state.evaluation_runtime.suggest_label_mapping,
|
||||||
|
dataset_id,
|
||||||
|
_safe_account_id(payload.account_id),
|
||||||
|
mapping=payload.mapping,
|
||||||
|
skill_name=payload.skill_name,
|
||||||
|
snapshot_id=payload.snapshot_id,
|
||||||
|
model=payload.model,
|
||||||
|
)
|
||||||
|
except EvaluationError as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
|
||||||
@app.get('/api/evaluations/experiments')
|
@app.get('/api/evaluations/experiments')
|
||||||
async def list_evaluation_experiments(
|
async def list_evaluation_experiments(
|
||||||
account_id: str,
|
account_id: str,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
RotateCcwIcon,
|
RotateCcwIcon,
|
||||||
SaveIcon,
|
SaveIcon,
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
|
SparklesIcon,
|
||||||
SquareIcon,
|
SquareIcon,
|
||||||
UploadIcon,
|
UploadIcon,
|
||||||
XCircleIcon,
|
XCircleIcon,
|
||||||
@@ -126,6 +127,16 @@ type Dataset = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type LabelMappingSuggestion = {
|
||||||
|
label_map: Record<string, string>;
|
||||||
|
unmapped: string[];
|
||||||
|
reason: string;
|
||||||
|
raw_labels: string[];
|
||||||
|
skill_name: string;
|
||||||
|
skill_version: string;
|
||||||
|
model: string;
|
||||||
|
};
|
||||||
|
|
||||||
type CanonicalCase = {
|
type CanonicalCase = {
|
||||||
case_id: string;
|
case_id: string;
|
||||||
query: string;
|
query: string;
|
||||||
@@ -270,7 +281,7 @@ function EvaluationWorkspace() {
|
|||||||
const [skillVersions, setSkillVersions] = useState<SkillVersion[]>([]);
|
const [skillVersions, setSkillVersions] = useState<SkillVersion[]>([]);
|
||||||
const [snapshotId, setSnapshotId] = useState("");
|
const [snapshotId, setSnapshotId] = useState("");
|
||||||
const [model, setModel] = useState("");
|
const [model, setModel] = useState("");
|
||||||
const [concurrency, setConcurrency] = useState(2);
|
const [concurrency, setConcurrency] = useState(8);
|
||||||
const [experimentName, setExperimentName] = useState("");
|
const [experimentName, setExperimentName] = useState("");
|
||||||
const [singleQuery, setSingleQuery] = useState("");
|
const [singleQuery, setSingleQuery] = useState("");
|
||||||
const [singleResult, setSingleResult] = useState<EvaluationCase | null>(null);
|
const [singleResult, setSingleResult] = useState<EvaluationCase | null>(null);
|
||||||
@@ -281,6 +292,7 @@ function EvaluationWorkspace() {
|
|||||||
const [queryFilter, setQueryFilter] = useState("");
|
const [queryFilter, setQueryFilter] = useState("");
|
||||||
const [busy, setBusy] = useState("");
|
const [busy, setBusy] = useState("");
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
const [labelMappingNote, setLabelMappingNote] = useState("");
|
||||||
const [skillEditorOpen, setSkillEditorOpen] = useState(false);
|
const [skillEditorOpen, setSkillEditorOpen] = useState(false);
|
||||||
const [skillEditorBusy, setSkillEditorBusy] = useState("");
|
const [skillEditorBusy, setSkillEditorBusy] = useState("");
|
||||||
const [skillEditorError, setSkillEditorError] = useState("");
|
const [skillEditorError, setSkillEditorError] = useState("");
|
||||||
@@ -386,6 +398,9 @@ function EvaluationWorkspace() {
|
|||||||
? ((await stateResponse.json()) as ClawState)
|
? ((await stateResponse.json()) as ClawState)
|
||||||
: {};
|
: {};
|
||||||
setMetadata(metadataPayload);
|
setMetadata(metadataPayload);
|
||||||
|
setConcurrency((current) =>
|
||||||
|
Math.max(1, Math.min(current, metadataPayload.max_concurrency)),
|
||||||
|
);
|
||||||
setSingleAnalyses(analysisPayload);
|
setSingleAnalyses(analysisPayload);
|
||||||
setDatasets(datasetPayload);
|
setDatasets(datasetPayload);
|
||||||
setExperiments(experimentPayload);
|
setExperiments(experimentPayload);
|
||||||
@@ -425,6 +440,7 @@ function EvaluationWorkspace() {
|
|||||||
if (nextDataset && nextDataset.id !== selectedDatasetIdRef.current) {
|
if (nextDataset && nextDataset.id !== selectedDatasetIdRef.current) {
|
||||||
setSelectedDatasetId(nextDataset.id);
|
setSelectedDatasetId(nextDataset.id);
|
||||||
setMapping(nextDataset.mapping);
|
setMapping(nextDataset.mapping);
|
||||||
|
setLabelMappingNote("");
|
||||||
}
|
}
|
||||||
const nextExperiment =
|
const nextExperiment =
|
||||||
experimentPayload.find(
|
experimentPayload.find(
|
||||||
@@ -605,6 +621,7 @@ function EvaluationWorkspace() {
|
|||||||
setDatasets((current) => [payload, ...current]);
|
setDatasets((current) => [payload, ...current]);
|
||||||
setSelectedDatasetId(payload.id);
|
setSelectedDatasetId(payload.id);
|
||||||
setMapping(payload.mapping);
|
setMapping(payload.mapping);
|
||||||
|
setLabelMappingNote("");
|
||||||
setExperimentName(
|
setExperimentName(
|
||||||
`${metadata?.default_skill || "label-master"} · ${payload.name}`,
|
`${metadata?.default_skill || "label-master"} · ${payload.name}`,
|
||||||
);
|
);
|
||||||
@@ -635,6 +652,46 @@ function EvaluationWorkspace() {
|
|||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function suggestLabelMapping() {
|
||||||
|
if (!selectedDataset) return;
|
||||||
|
if (!mapping.fields.gold_label) {
|
||||||
|
setError("请先映射人工标签字段");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setBusy("label-map");
|
||||||
|
setError("");
|
||||||
|
setLabelMappingNote("");
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/claw/evaluations/datasets/${encodeURIComponent(selectedDataset.id)}/label-mapping/suggest`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
mapping,
|
||||||
|
skill_name: skillName,
|
||||||
|
snapshot_id: snapshotId || undefined,
|
||||||
|
model: model || undefined,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const payload = await readJson<LabelMappingSuggestion>(response);
|
||||||
|
setMapping((current) => ({
|
||||||
|
...current,
|
||||||
|
label_map: payload.label_map,
|
||||||
|
}));
|
||||||
|
const notes = [
|
||||||
|
payload.reason,
|
||||||
|
payload.unmapped.length ? `未映射:${payload.unmapped.join("、")}` : "",
|
||||||
|
].filter(Boolean);
|
||||||
|
setLabelMappingNote(notes.join(" "));
|
||||||
|
} catch (reason) {
|
||||||
|
setError(messageFrom(reason));
|
||||||
|
} finally {
|
||||||
|
setBusy("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function createAndStart() {
|
async function createAndStart() {
|
||||||
if (!selectedDataset) return;
|
if (!selectedDataset) return;
|
||||||
if (!mapping.fields.query) {
|
if (!mapping.fields.query) {
|
||||||
@@ -1084,6 +1141,7 @@ function EvaluationWorkspace() {
|
|||||||
const next = datasets.find((item) => item.id === value);
|
const next = datasets.find((item) => item.id === value);
|
||||||
if (next) {
|
if (next) {
|
||||||
setMapping(next.mapping);
|
setMapping(next.mapping);
|
||||||
|
setLabelMappingNote("");
|
||||||
setExperimentName(`${skillName} · ${next.name}`);
|
setExperimentName(`${skillName} · ${next.name}`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -1096,6 +1154,8 @@ function EvaluationWorkspace() {
|
|||||||
uploadRef={uploadRef}
|
uploadRef={uploadRef}
|
||||||
onUpload={uploadDataset}
|
onUpload={uploadDataset}
|
||||||
onCreate={createAndStart}
|
onCreate={createAndStart}
|
||||||
|
onSuggestLabelMapping={suggestLabelMapping}
|
||||||
|
labelMappingNote={labelMappingNote}
|
||||||
busy={busy}
|
busy={busy}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -1476,6 +1536,8 @@ function ExperimentSetup(props: {
|
|||||||
uploadRef: React.RefObject<HTMLInputElement | null>;
|
uploadRef: React.RefObject<HTMLInputElement | null>;
|
||||||
onUpload: (file: File) => void;
|
onUpload: (file: File) => void;
|
||||||
onCreate: () => void;
|
onCreate: () => void;
|
||||||
|
onSuggestLabelMapping: () => void;
|
||||||
|
labelMappingNote: string;
|
||||||
busy: string;
|
busy: string;
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
@@ -1493,6 +1555,8 @@ function ExperimentSetup(props: {
|
|||||||
uploadRef,
|
uploadRef,
|
||||||
onUpload,
|
onUpload,
|
||||||
onCreate,
|
onCreate,
|
||||||
|
onSuggestLabelMapping,
|
||||||
|
labelMappingNote,
|
||||||
busy,
|
busy,
|
||||||
} = props;
|
} = props;
|
||||||
const mappedPreview = useMemo(
|
const mappedPreview = useMemo(
|
||||||
@@ -1566,14 +1630,33 @@ function ExperimentSetup(props: {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="mt-4 border-y py-5 sm:rounded-md sm:border sm:px-5">
|
<section className="mt-4 border-y py-5 sm:rounded-md sm:border sm:px-5">
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between gap-3">
|
||||||
<h3 className="font-medium text-sm">2. 字段映射</h3>
|
<h3 className="font-medium text-sm">2. 字段映射</h3>
|
||||||
{selectedDataset ? (
|
<div className="flex items-center gap-3">
|
||||||
<span className="text-muted-foreground text-xs">
|
{selectedDataset ? (
|
||||||
{selectedDataset.validation.has_gold_label}/
|
<span className="text-muted-foreground text-xs">
|
||||||
{selectedDataset.row_count} 条含人工标签
|
{selectedDataset.validation.has_gold_label}/
|
||||||
</span>
|
{selectedDataset.row_count} 条含人工标签
|
||||||
) : null}
|
</span>
|
||||||
|
) : null}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={
|
||||||
|
!selectedDataset ||
|
||||||
|
!mapping.fields.gold_label ||
|
||||||
|
busy === "label-map"
|
||||||
|
}
|
||||||
|
onClick={onSuggestLabelMapping}
|
||||||
|
>
|
||||||
|
{busy === "label-map" ? (
|
||||||
|
<LoaderCircleIcon className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<SparklesIcon />
|
||||||
|
)}
|
||||||
|
生成标签映射
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-x-5 gap-y-3 sm:grid-cols-2">
|
<div className="grid gap-x-5 gap-y-3 sm:grid-cols-2">
|
||||||
{FIELD_OPTIONS.map(([key, label, required]) => (
|
{FIELD_OPTIONS.map(([key, label, required]) => (
|
||||||
@@ -1609,6 +1692,55 @@ function ExperimentSetup(props: {
|
|||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{Object.keys(mapping.label_map).length ? (
|
||||||
|
<div className="mt-5 border-t pt-4">
|
||||||
|
<div className="mb-2 font-medium text-xs">标签映射</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
{Object.entries(mapping.label_map).map(([source, target]) => (
|
||||||
|
<div
|
||||||
|
key={source}
|
||||||
|
className="grid grid-cols-[minmax(0,1fr)_20px_minmax(0,1fr)_28px] items-center gap-2"
|
||||||
|
>
|
||||||
|
<code className="truncate rounded bg-muted px-2 py-1.5 text-xs">
|
||||||
|
{source}
|
||||||
|
</code>
|
||||||
|
<span className="text-center text-muted-foreground">→</span>
|
||||||
|
<textarea
|
||||||
|
value={target}
|
||||||
|
onChange={(event) =>
|
||||||
|
setMapping({
|
||||||
|
...mapping,
|
||||||
|
label_map: {
|
||||||
|
...mapping.label_map,
|
||||||
|
[source]: event.target.value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
rows={target.includes("\n") ? 2 : 1}
|
||||||
|
className="min-h-8 resize-y rounded-md border bg-transparent px-3 py-1.5 font-mono text-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title="删除映射"
|
||||||
|
onClick={() => {
|
||||||
|
const nextLabelMap = { ...mapping.label_map };
|
||||||
|
delete nextLabelMap[source];
|
||||||
|
setMapping({ ...mapping, label_map: nextLabelMap });
|
||||||
|
}}
|
||||||
|
className="grid size-7 place-items-center rounded text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||||
|
>
|
||||||
|
<XCircleIcon className="size-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{labelMappingNote ? (
|
||||||
|
<p className="mt-2 text-muted-foreground text-xs">
|
||||||
|
{labelMappingNote}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
{mappedPreview.length ? (
|
{mappedPreview.length ? (
|
||||||
<div className="mt-5 overflow-hidden rounded-md border">
|
<div className="mt-5 overflow-hidden rounded-md border">
|
||||||
<div className="grid grid-cols-[72px_minmax(0,1fr)_140px] gap-3 border-b bg-muted/40 px-3 py-2 font-medium text-muted-foreground text-xs">
|
<div className="grid grid-cols-[72px_minmax(0,1fr)_140px] gap-3 border-b bg-muted/40 px-3 py-2 font-medium text-muted-foreground text-xs">
|
||||||
@@ -1647,7 +1779,7 @@ function ExperimentSetup(props: {
|
|||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min={1}
|
min={1}
|
||||||
max={metadata?.max_concurrency ?? 2}
|
max={metadata?.max_concurrency ?? 8}
|
||||||
value={concurrency}
|
value={concurrency}
|
||||||
onChange={(event) => setConcurrency(Number(event.target.value))}
|
onChange={(event) => setConcurrency(Number(event.target.value))}
|
||||||
className="min-w-0 flex-1 accent-foreground"
|
className="min-w-0 flex-1 accent-foreground"
|
||||||
@@ -1664,9 +1796,7 @@ function ExperimentSetup(props: {
|
|||||||
<Button
|
<Button
|
||||||
size="lg"
|
size="lg"
|
||||||
onClick={onCreate}
|
onClick={onCreate}
|
||||||
disabled={
|
disabled={!selectedDataset || !mapping.fields.query || Boolean(busy)}
|
||||||
!selectedDataset || !mapping.fields.query || busy === "create"
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{busy === "create" ? (
|
{busy === "create" ? (
|
||||||
<LoaderCircleIcon className="animate-spin" />
|
<LoaderCircleIcon className="animate-spin" />
|
||||||
|
|||||||
+372
-16
@@ -32,9 +32,10 @@ from src.bundled_skills import BundledSkill, load_directory_skills
|
|||||||
TERMINAL_CASE_STATUSES = {'completed', 'failed', 'cancelled'}
|
TERMINAL_CASE_STATUSES = {'completed', 'failed', 'cancelled'}
|
||||||
ACTIVE_EXPERIMENT_STATUSES = {'queued', 'running', 'pausing'}
|
ACTIVE_EXPERIMENT_STATUSES = {'queued', 'running', 'pausing'}
|
||||||
SUPPORTED_DATASET_SUFFIXES = {'.csv', '.tsv', '.json', '.jsonl', '.xlsx'}
|
SUPPORTED_DATASET_SUFFIXES = {'.csv', '.tsv', '.json', '.jsonl', '.xlsx'}
|
||||||
DEFAULT_CONCURRENCY = 2
|
DEFAULT_CONCURRENCY = 8
|
||||||
MAX_CONCURRENCY = 8
|
MAX_CONCURRENCY = 32
|
||||||
MAX_DATASET_ROWS = 20_000
|
MAX_DATASET_ROWS = 20_000
|
||||||
|
MAX_LABEL_MAPPING_VALUES = 200
|
||||||
VISIBLE_SKILL_FILE_SUFFIXES = {'.json', '.md', '.py', '.txt', '.yaml', '.yml'}
|
VISIBLE_SKILL_FILE_SUFFIXES = {'.json', '.md', '.py', '.txt', '.yaml', '.yml'}
|
||||||
EDITABLE_SKILL_FILE_SUFFIXES = {'.json', '.md'}
|
EDITABLE_SKILL_FILE_SUFFIXES = {'.json', '.md'}
|
||||||
MAX_SKILL_FILE_BYTES = 512 * 1024
|
MAX_SKILL_FILE_BYTES = 512 * 1024
|
||||||
@@ -75,6 +76,27 @@ EVALUATION_OUTPUT_SCHEMA = OutputSchemaConfig(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
LABEL_MAPPING_OUTPUT_SCHEMA = OutputSchemaConfig(
|
||||||
|
name='evaluation_label_mapping',
|
||||||
|
strict=False,
|
||||||
|
schema={
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'label_map': {
|
||||||
|
'type': 'object',
|
||||||
|
'additionalProperties': {'type': 'string'},
|
||||||
|
},
|
||||||
|
'unmapped': {
|
||||||
|
'type': 'array',
|
||||||
|
'items': {'type': 'string'},
|
||||||
|
},
|
||||||
|
'reason': {'type': 'string'},
|
||||||
|
},
|
||||||
|
'required': ['label_map', 'unmapped', 'reason'],
|
||||||
|
'additionalProperties': False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ExperimentControl:
|
class ExperimentControl:
|
||||||
@@ -489,6 +511,78 @@ class EvaluationRuntime:
|
|||||||
payload['validation'] = validation
|
payload['validation'] = validation
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
def suggest_label_mapping(
|
||||||
|
self,
|
||||||
|
dataset_id: str,
|
||||||
|
account_id: str,
|
||||||
|
*,
|
||||||
|
mapping: dict[str, Any] | None,
|
||||||
|
skill_name: str,
|
||||||
|
snapshot_id: str | None = None,
|
||||||
|
model: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
dataset = self.get_dataset(dataset_id, account_id, include_rows=True)
|
||||||
|
normalized_mapping = normalize_mapping(mapping or dataset['mapping'])
|
||||||
|
gold_field = normalized_mapping['fields']['gold_label']
|
||||||
|
if not gold_field:
|
||||||
|
raise EvaluationError('请先映射人工标签字段')
|
||||||
|
raw_labels: list[str] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
for row in dataset['rows']:
|
||||||
|
value = stringify_value(resolve_field(row, gold_field)).strip()
|
||||||
|
if not value or value in seen:
|
||||||
|
continue
|
||||||
|
seen.add(value)
|
||||||
|
raw_labels.append(value)
|
||||||
|
if len(raw_labels) >= MAX_LABEL_MAPPING_VALUES:
|
||||||
|
break
|
||||||
|
if not raw_labels:
|
||||||
|
raise EvaluationError('人工标签字段中没有可用于映射的值')
|
||||||
|
|
||||||
|
snapshot = self.resolve_skill_snapshot(
|
||||||
|
account_id,
|
||||||
|
skill_name,
|
||||||
|
snapshot_id=snapshot_id,
|
||||||
|
)
|
||||||
|
snapshot_root = Path(snapshot['path'])
|
||||||
|
skill = self._load_snapshot_skill(snapshot_root, skill_name)
|
||||||
|
model_name = model or self.model_config_for(account_id).model
|
||||||
|
run_id = f'label_map_{uuid4().hex[:16]}'
|
||||||
|
case_row_id = f'dataset_{sanitize_filename(dataset_id)}'
|
||||||
|
cancel_event = threading.Event()
|
||||||
|
agent = self._build_evaluation_agent(
|
||||||
|
account_id=account_id,
|
||||||
|
experiment_id=run_id,
|
||||||
|
case_row_id=case_row_id,
|
||||||
|
model=model_name,
|
||||||
|
snapshot_root=snapshot_root,
|
||||||
|
skill=skill,
|
||||||
|
cancel_event=cancel_event,
|
||||||
|
output_schema=LABEL_MAPPING_OUTPUT_SCHEMA,
|
||||||
|
)
|
||||||
|
prompt, runtime_context = build_label_mapping_prompt(
|
||||||
|
skill=skill,
|
||||||
|
snapshot_root=snapshot_root,
|
||||||
|
raw_labels=raw_labels,
|
||||||
|
)
|
||||||
|
run_result = agent.run(
|
||||||
|
prompt,
|
||||||
|
session_id=f'{run_id}_{case_row_id}',
|
||||||
|
runtime_context=runtime_context,
|
||||||
|
)
|
||||||
|
result = normalize_label_mapping_output(
|
||||||
|
run_result.final_output,
|
||||||
|
raw_labels=raw_labels,
|
||||||
|
skill_name=skill_name,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
**result,
|
||||||
|
'raw_labels': raw_labels,
|
||||||
|
'skill_name': skill_name,
|
||||||
|
'skill_version': snapshot['content_hash'][:12],
|
||||||
|
'model': model_name,
|
||||||
|
}
|
||||||
|
|
||||||
def analyze_case(
|
def analyze_case(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -909,7 +1003,30 @@ class EvaluationRuntime:
|
|||||||
params.extend(case_ids)
|
params.extend(case_ids)
|
||||||
elif disagreements_only:
|
elif disagreements_only:
|
||||||
where.append("status = 'completed'")
|
where.append("status = 'completed'")
|
||||||
where.append("gold_label <> '' AND prediction <> gold_label")
|
completed_rows = self.store.fetchall(
|
||||||
|
"""
|
||||||
|
SELECT id, gold_label, prediction, complex_json
|
||||||
|
FROM evaluation_cases
|
||||||
|
WHERE experiment_id = ? AND status = 'completed'
|
||||||
|
AND gold_label <> ''
|
||||||
|
""",
|
||||||
|
(experiment_id,),
|
||||||
|
)
|
||||||
|
disagreement_ids = [
|
||||||
|
row['id']
|
||||||
|
for row in completed_rows
|
||||||
|
if not labels_equivalent(
|
||||||
|
row.get('gold_label') or '',
|
||||||
|
row.get('prediction') or '',
|
||||||
|
parse_complex_json(row.get('complex_json')),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
if disagreement_ids:
|
||||||
|
placeholders = ','.join('?' for _ in disagreement_ids)
|
||||||
|
where.append(f'id IN ({placeholders})')
|
||||||
|
params.extend(disagreement_ids)
|
||||||
|
else:
|
||||||
|
where.append('1 = 0')
|
||||||
else:
|
else:
|
||||||
where.append("status IN ('failed', 'cancelled')")
|
where.append("status IN ('failed', 'cancelled')")
|
||||||
self.store.execute(
|
self.store.execute(
|
||||||
@@ -1734,6 +1851,7 @@ class EvaluationRuntime:
|
|||||||
snapshot_root: Path,
|
snapshot_root: Path,
|
||||||
skill: BundledSkill,
|
skill: BundledSkill,
|
||||||
cancel_event: threading.Event,
|
cancel_event: threading.Event,
|
||||||
|
output_schema: OutputSchemaConfig = EVALUATION_OUTPUT_SCHEMA,
|
||||||
) -> LocalCodingAgent:
|
) -> LocalCodingAgent:
|
||||||
account_paths = self.account_paths_for(account_id)
|
account_paths = self.account_paths_for(account_id)
|
||||||
snapshot_skill_dir = snapshot_root / 'skills' / skill.name
|
snapshot_skill_dir = snapshot_root / 'skills' / skill.name
|
||||||
@@ -1769,7 +1887,7 @@ class EvaluationRuntime:
|
|||||||
),
|
),
|
||||||
additional_working_directories=(snapshot_skill_dir,),
|
additional_working_directories=(snapshot_skill_dir,),
|
||||||
disable_claude_md_discovery=True,
|
disable_claude_md_discovery=True,
|
||||||
output_schema=EVALUATION_OUTPUT_SCHEMA,
|
output_schema=output_schema,
|
||||||
session_directory=session_dir,
|
session_directory=session_dir,
|
||||||
scratchpad_root=scratchpad_root,
|
scratchpad_root=scratchpad_root,
|
||||||
python_env_dir=account_paths.get('python_env'),
|
python_env_dir=account_paths.get('python_env'),
|
||||||
@@ -2032,8 +2150,9 @@ class EvaluationRuntime:
|
|||||||
def _case_payload(self, row: dict[str, Any]) -> dict[str, Any]:
|
def _case_payload(self, row: dict[str, Any]) -> dict[str, Any]:
|
||||||
gold_label = row.get('gold_label') or ''
|
gold_label = row.get('gold_label') or ''
|
||||||
prediction = row.get('prediction') or ''
|
prediction = row.get('prediction') or ''
|
||||||
|
complex_value = parse_complex_json(row.get('complex_json'))
|
||||||
correct = (
|
correct = (
|
||||||
labels_equivalent(gold_label, prediction)
|
labels_equivalent(gold_label, prediction, complex_value)
|
||||||
if gold_label and prediction and row.get('status') == 'completed'
|
if gold_label and prediction and row.get('status') == 'completed'
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
@@ -2045,7 +2164,7 @@ class EvaluationRuntime:
|
|||||||
'gold_label': gold_label,
|
'gold_label': gold_label,
|
||||||
'prediction': prediction,
|
'prediction': prediction,
|
||||||
'function_output': row.get('function_output') or '',
|
'function_output': row.get('function_output') or '',
|
||||||
'complex': json.loads(row.get('complex_json') or 'null'),
|
'complex': complex_value,
|
||||||
'correct': correct,
|
'correct': correct,
|
||||||
'reason': row.get('reason') or '',
|
'reason': row.get('reason') or '',
|
||||||
'candidates': json.loads(row.get('candidates_json') or '[]'),
|
'candidates': json.loads(row.get('candidates_json') or '[]'),
|
||||||
@@ -2575,22 +2694,96 @@ def build_evaluation_prompt(
|
|||||||
不要把人工标签当作输入依据;下面数据不会提供 gold_label。
|
不要把人工标签当作输入依据;下面数据不会提供 gold_label。
|
||||||
|
|
||||||
<case>
|
<case>
|
||||||
{json.dumps({key: value for key, value in canonical_case.items() if key != 'gold_label'}, ensure_ascii=False, indent=2)}
|
{json.dumps(evaluation_case_for_prompt(canonical_case), ensure_ascii=False, indent=2)}
|
||||||
</case>
|
</case>
|
||||||
""".strip()
|
""".strip()
|
||||||
return prompt, runtime_context
|
return prompt, runtime_context
|
||||||
|
|
||||||
|
|
||||||
|
def build_label_mapping_prompt(
|
||||||
|
*,
|
||||||
|
skill: BundledSkill,
|
||||||
|
snapshot_root: Path,
|
||||||
|
raw_labels: list[str],
|
||||||
|
) -> tuple[str, str]:
|
||||||
|
snapshot_skill_dir = snapshot_root / 'skills' / skill.name
|
||||||
|
skill_instructions = skill.get_prompt(
|
||||||
|
None, # type: ignore[arg-type]
|
||||||
|
'离线评测标签映射模式:只比较标签表达,不分析具体 case。',
|
||||||
|
)
|
||||||
|
if skill.name == 'label-fast-slow-routing':
|
||||||
|
target_contract = """
|
||||||
|
本 Skill 的主标签规范值为:`快`、`慢`、`模糊`。
|
||||||
|
复杂度是独立维度,规范写法为 `complex=true` 或 `complex=false`。
|
||||||
|
同一原始值同时包含复杂度与主标签时,用两行保留,例如:
|
||||||
|
|
||||||
|
complex=true
|
||||||
|
慢
|
||||||
|
|
||||||
|
`慢系统`与`慢`等价,`快系统`与`快`等价;`Ture`按明显拼写错误
|
||||||
|
`true`处理。不得把只有复杂度的信息猜成快慢标签。
|
||||||
|
""".strip()
|
||||||
|
else:
|
||||||
|
target_contract = (
|
||||||
|
'规范值必须遵循该 Skill 的实际输出协议。'
|
||||||
|
'只合并表达形式不同但语义完全相同的标签,不要创造新类别。'
|
||||||
|
)
|
||||||
|
prompt = f"""
|
||||||
|
你正在为离线评测生成一次性标签映射。
|
||||||
|
|
||||||
|
指定 Skill:{skill.name}
|
||||||
|
不可变快照目录:{snapshot_skill_dir}
|
||||||
|
|
||||||
|
{skill_instructions}
|
||||||
|
|
||||||
|
{target_contract}
|
||||||
|
|
||||||
|
要求:
|
||||||
|
1. 第一步必须调用 `read_file`、`grep_search` 或 `glob_search`,读取快照中
|
||||||
|
至少一份直接定义输出标签或判断协议的文件。
|
||||||
|
2. 只比较下面的原始人工标签,不读取 query,不进行逐条分类。
|
||||||
|
3. `label_map` 的 key 必须原样使用输入值,value 使用规范标签。
|
||||||
|
4. 可以返回恒等映射,便于用户检查。
|
||||||
|
5. 无法可靠对应的值放入 `unmapped`,不要猜测。
|
||||||
|
6. 最终只返回符合输出 schema 的 JSON。
|
||||||
|
|
||||||
|
<raw_labels>
|
||||||
|
{json.dumps(raw_labels, ensure_ascii=False, indent=2)}
|
||||||
|
</raw_labels>
|
||||||
|
""".strip()
|
||||||
|
runtime_context = f"""
|
||||||
|
离线标签映射运行时。
|
||||||
|
当前工作目录:{snapshot_skill_dir}
|
||||||
|
只能读取当前 Skill 快照;禁止修改文件。
|
||||||
|
""".strip()
|
||||||
|
return prompt, runtime_context
|
||||||
|
|
||||||
|
|
||||||
|
PROMPT_CASE_FIELDS = (
|
||||||
|
'case_id',
|
||||||
|
'query',
|
||||||
|
'history',
|
||||||
|
'context',
|
||||||
|
'domain',
|
||||||
|
'request_id',
|
||||||
|
'metadata',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def evaluation_case_for_prompt(canonical_case: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
key: canonical_case[key]
|
||||||
|
for key in PROMPT_CASE_FIELDS
|
||||||
|
if key in canonical_case
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def build_evidence_retry_prompt(
|
def build_evidence_retry_prompt(
|
||||||
skill_name: str,
|
skill_name: str,
|
||||||
reason: str,
|
reason: str,
|
||||||
canonical_case: dict[str, Any],
|
canonical_case: dict[str, Any],
|
||||||
) -> str:
|
) -> str:
|
||||||
case_payload = {
|
case_payload = evaluation_case_for_prompt(canonical_case)
|
||||||
key: value
|
|
||||||
for key, value in canonical_case.items()
|
|
||||||
if key != 'gold_label'
|
|
||||||
}
|
|
||||||
return f"""
|
return f"""
|
||||||
上一轮结果不能进入评测指标:{reason}。
|
上一轮结果不能进入评测指标:{reason}。
|
||||||
|
|
||||||
@@ -2732,6 +2925,101 @@ def normalize_evaluation_output(raw_output: str) -> dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_label_mapping_output(
|
||||||
|
raw_output: str,
|
||||||
|
*,
|
||||||
|
raw_labels: list[str],
|
||||||
|
skill_name: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
payload = parse_json_object_output(
|
||||||
|
raw_output,
|
||||||
|
error_message='模型返回的标签映射不是合法 JSON',
|
||||||
|
)
|
||||||
|
raw_mapping = payload.get('label_map')
|
||||||
|
if not isinstance(raw_mapping, dict):
|
||||||
|
raise EvaluationError('模型标签映射缺少 label_map')
|
||||||
|
by_stripped = {value.strip(): value for value in raw_labels}
|
||||||
|
label_map = deterministic_label_mapping(raw_labels, skill_name=skill_name)
|
||||||
|
for raw_key, raw_value in raw_mapping.items():
|
||||||
|
source = by_stripped.get(str(raw_key).strip())
|
||||||
|
if source is None:
|
||||||
|
continue
|
||||||
|
target = canonicalize_mapping_target(
|
||||||
|
str(raw_value),
|
||||||
|
skill_name=skill_name,
|
||||||
|
)
|
||||||
|
if target:
|
||||||
|
label_map[source] = target
|
||||||
|
unmapped = [
|
||||||
|
value
|
||||||
|
for value in raw_labels
|
||||||
|
if value not in label_map
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
'label_map': label_map,
|
||||||
|
'unmapped': unmapped,
|
||||||
|
'reason': str(payload.get('reason') or '').strip(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_json_object_output(
|
||||||
|
raw_output: str,
|
||||||
|
*,
|
||||||
|
error_message: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
text = raw_output.strip()
|
||||||
|
if text.startswith('```'):
|
||||||
|
text = re.sub(r'^```(?:json)?\s*', '', text)
|
||||||
|
text = re.sub(r'\s*```$', '', text)
|
||||||
|
try:
|
||||||
|
payload = json.loads(text)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
match = re.search(r'\{.*\}', text, re.DOTALL)
|
||||||
|
if not match:
|
||||||
|
raise EvaluationError(error_message)
|
||||||
|
try:
|
||||||
|
payload = json.loads(match.group(0))
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
raise EvaluationError(error_message) from exc
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
raise EvaluationError(error_message)
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def deterministic_label_mapping(
|
||||||
|
raw_labels: list[str],
|
||||||
|
*,
|
||||||
|
skill_name: str,
|
||||||
|
) -> dict[str, str]:
|
||||||
|
if skill_name != 'label-fast-slow-routing':
|
||||||
|
return {}
|
||||||
|
result: dict[str, str] = {}
|
||||||
|
for raw_label in raw_labels:
|
||||||
|
target = canonicalize_mapping_target(
|
||||||
|
raw_label,
|
||||||
|
skill_name=skill_name,
|
||||||
|
)
|
||||||
|
if target:
|
||||||
|
result[raw_label] = target
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def canonicalize_mapping_target(value: str, *, skill_name: str) -> str:
|
||||||
|
text = str(value or '').strip()
|
||||||
|
if not text:
|
||||||
|
return ''
|
||||||
|
if skill_name != 'label-fast-slow-routing':
|
||||||
|
return text
|
||||||
|
label = normalize_label_value(text)
|
||||||
|
complex_value = extract_complex_value(text)
|
||||||
|
parts: list[str] = []
|
||||||
|
if complex_value is not None:
|
||||||
|
parts.append(f"complex={'true' if complex_value else 'false'}")
|
||||||
|
if label in {'快', '慢', '模糊'}:
|
||||||
|
parts.append(label)
|
||||||
|
return '\n'.join(parts)
|
||||||
|
|
||||||
|
|
||||||
def clean_string_list(value: Any) -> list[str]:
|
def clean_string_list(value: Any) -> list[str]:
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
return [value] if value.strip() else []
|
return [value] if value.strip() else []
|
||||||
@@ -2883,8 +3171,31 @@ def calculate_metrics(cases: list[dict[str, Any]]) -> dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def labels_equivalent(gold: str, prediction: str) -> bool:
|
def labels_equivalent(
|
||||||
return normalize_label_value(gold) == normalize_label_value(prediction)
|
gold: str,
|
||||||
|
prediction: str,
|
||||||
|
prediction_complex: bool | None = None,
|
||||||
|
) -> bool:
|
||||||
|
gold_label = normalize_label_value(gold)
|
||||||
|
prediction_label = normalize_label_value(prediction)
|
||||||
|
gold_complex = extract_complex_value(gold)
|
||||||
|
embedded_prediction_complex = extract_complex_value(prediction)
|
||||||
|
effective_prediction_complex = (
|
||||||
|
prediction_complex
|
||||||
|
if prediction_complex is not None
|
||||||
|
else embedded_prediction_complex
|
||||||
|
)
|
||||||
|
|
||||||
|
if gold_label and gold_label != prediction_label:
|
||||||
|
return False
|
||||||
|
if not gold_label and gold_complex is None:
|
||||||
|
return prediction_label == ''
|
||||||
|
if gold_complex is not None:
|
||||||
|
if effective_prediction_complex is None:
|
||||||
|
return bool(gold_label)
|
||||||
|
if gold_complex != effective_prediction_complex:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def normalize_label_value(value: str) -> str:
|
def normalize_label_value(value: str) -> str:
|
||||||
@@ -2896,8 +3207,53 @@ def normalize_label_value(value: str) -> str:
|
|||||||
)
|
)
|
||||||
if agent_match:
|
if agent_match:
|
||||||
value = agent_match.group(1)
|
value = agent_match.group(1)
|
||||||
value = re.sub(r'^complex\s*=\s*(?:true|false)\s*', '', value, flags=re.I)
|
value = re.sub(
|
||||||
return re.sub(r'\s+', '', value).lower()
|
r'complex\s*[:=]\s*(?:true|ture|false)',
|
||||||
|
'',
|
||||||
|
value,
|
||||||
|
flags=re.I,
|
||||||
|
)
|
||||||
|
value = re.sub(
|
||||||
|
r'^(?:prediction|label|route|标签|路由)\s*[:=:]\s*',
|
||||||
|
'',
|
||||||
|
value,
|
||||||
|
flags=re.I,
|
||||||
|
)
|
||||||
|
normalized = re.sub(r'[\s,,;;|]+', '', value).lower()
|
||||||
|
aliases = {
|
||||||
|
'快系统': '快',
|
||||||
|
'快速系统': '快',
|
||||||
|
'fast': '快',
|
||||||
|
'fastsystem': '快',
|
||||||
|
'慢系统': '慢',
|
||||||
|
'慢速系统': '慢',
|
||||||
|
'slow': '慢',
|
||||||
|
'slowsystem': '慢',
|
||||||
|
'模糊系统': '模糊',
|
||||||
|
'ambiguous': '模糊',
|
||||||
|
}
|
||||||
|
return aliases.get(normalized, normalized)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_complex_value(value: str) -> bool | None:
|
||||||
|
match = re.search(
|
||||||
|
r'complex\s*[:=]\s*(true|ture|false)',
|
||||||
|
str(value or ''),
|
||||||
|
flags=re.I,
|
||||||
|
)
|
||||||
|
if not match:
|
||||||
|
return None
|
||||||
|
return match.group(1).lower() in {'true', 'ture'}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_complex_json(value: Any) -> bool | None:
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return value
|
||||||
|
try:
|
||||||
|
parsed = json.loads(str(value or 'null'))
|
||||||
|
except (TypeError, ValueError, json.JSONDecodeError):
|
||||||
|
return None
|
||||||
|
return parsed if isinstance(parsed, bool) else None
|
||||||
|
|
||||||
|
|
||||||
def hash_directory(directory: Path) -> str:
|
def hash_directory(directory: Path) -> str:
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from src.evaluation_runtime import (
|
|||||||
calculate_metrics,
|
calculate_metrics,
|
||||||
extract_accessed_refs,
|
extract_accessed_refs,
|
||||||
labels_equivalent,
|
labels_equivalent,
|
||||||
|
normalize_label_mapping_output,
|
||||||
normalize_label_master_result,
|
normalize_label_master_result,
|
||||||
normalize_evaluation_output,
|
normalize_evaluation_output,
|
||||||
parse_dataset_bytes,
|
parse_dataset_bytes,
|
||||||
@@ -288,6 +289,22 @@ class EvaluationRuntimeTests(unittest.TestCase):
|
|||||||
'地图导航',
|
'地图导航',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
self.assertTrue(labels_equivalent('慢系统', '慢'))
|
||||||
|
self.assertTrue(
|
||||||
|
labels_equivalent(
|
||||||
|
'complex=Ture\n慢系统',
|
||||||
|
'慢',
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertTrue(labels_equivalent('complex=Ture', '慢', True))
|
||||||
|
self.assertFalse(
|
||||||
|
labels_equivalent(
|
||||||
|
'complex=Ture\n慢系统',
|
||||||
|
'慢',
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
)
|
||||||
snapshot_metadata = json.dumps(
|
snapshot_metadata = json.dumps(
|
||||||
{
|
{
|
||||||
'label_catalog': {
|
'label_catalog': {
|
||||||
@@ -340,6 +357,24 @@ class EvaluationRuntimeTests(unittest.TestCase):
|
|||||||
normalized_label['function_output'],
|
normalized_label['function_output'],
|
||||||
'Agent(tag="地图导航")',
|
'Agent(tag="地图导航")',
|
||||||
)
|
)
|
||||||
|
mapping = normalize_label_mapping_output(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
'label_map': {
|
||||||
|
'慢系统': '慢',
|
||||||
|
'complex=Ture': 'complex=true',
|
||||||
|
},
|
||||||
|
'unmapped': [],
|
||||||
|
'reason': '统一快慢和复杂度格式',
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
),
|
||||||
|
raw_labels=['慢系统', 'complex=Ture', '未知'],
|
||||||
|
skill_name='label-fast-slow-routing',
|
||||||
|
)
|
||||||
|
self.assertEqual(mapping['label_map']['慢系统'], '慢')
|
||||||
|
self.assertEqual(mapping['label_map']['complex=Ture'], 'complex=true')
|
||||||
|
self.assertEqual(mapping['unmapped'], ['未知'])
|
||||||
|
|
||||||
def test_fast_slow_prompt_uses_skill_contract_and_relative_paths(self) -> None:
|
def test_fast_slow_prompt_uses_skill_contract_and_relative_paths(self) -> None:
|
||||||
skill = BundledSkill(
|
skill = BundledSkill(
|
||||||
@@ -351,7 +386,15 @@ class EvaluationRuntimeTests(unittest.TestCase):
|
|||||||
prompt, runtime_context = build_evaluation_prompt(
|
prompt, runtime_context = build_evaluation_prompt(
|
||||||
skill=skill,
|
skill=skill,
|
||||||
snapshot_root=Path('/tmp/evaluation-snapshot'),
|
snapshot_root=Path('/tmp/evaluation-snapshot'),
|
||||||
canonical_case={'query': '打开空调', 'gold_label': '快'},
|
canonical_case={
|
||||||
|
'query': '打开空调',
|
||||||
|
'gold_label': '快',
|
||||||
|
'source_row': {
|
||||||
|
'query': '打开空调',
|
||||||
|
'人工标签': '快',
|
||||||
|
'secret': 'should-not-leak',
|
||||||
|
},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
self.assertIn('填写“快”“慢”或“模糊”', prompt)
|
self.assertIn('填写“快”“慢”或“模糊”', prompt)
|
||||||
self.assertNotIn('填写 fast 或 slow', prompt)
|
self.assertNotIn('填写 fast 或 slow', prompt)
|
||||||
@@ -360,6 +403,64 @@ class EvaluationRuntimeTests(unittest.TestCase):
|
|||||||
'不要添加 `skills/label-fast-slow-routing/` 前缀',
|
'不要添加 `skills/label-fast-slow-routing/` 前缀',
|
||||||
prompt,
|
prompt,
|
||||||
)
|
)
|
||||||
|
self.assertNotIn('should-not-leak', prompt)
|
||||||
|
self.assertNotIn('"人工标签"', prompt)
|
||||||
|
|
||||||
|
def test_label_mapping_suggestion_uses_one_agent_run(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
_write_fast_slow_skill(root)
|
||||||
|
runtime = EvaluationRuntime(
|
||||||
|
root=root / 'evaluations',
|
||||||
|
cwd_for_account=lambda _account: root,
|
||||||
|
model_config_for=lambda _account: ModelConfig(model='test-model'),
|
||||||
|
account_paths_for=lambda _account: {'python_env': root / '.venv'},
|
||||||
|
max_workers=1,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
dataset = runtime.create_dataset(
|
||||||
|
account_id='alice',
|
||||||
|
name='fast-slow',
|
||||||
|
filename='fast-slow.csv',
|
||||||
|
rows=[
|
||||||
|
{'query': '打开空调', 'label': '快系统'},
|
||||||
|
{'query': '帮我规划路线', 'label': '慢系统'},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
fake_result = AgentRunResult(
|
||||||
|
final_output=json.dumps(
|
||||||
|
{
|
||||||
|
'label_map': {
|
||||||
|
'快系统': '快',
|
||||||
|
'慢系统': '慢',
|
||||||
|
},
|
||||||
|
'unmapped': [],
|
||||||
|
'reason': '统一路由标签',
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
),
|
||||||
|
turns=1,
|
||||||
|
tool_calls=1,
|
||||||
|
transcript=(),
|
||||||
|
usage=UsageStats(input_tokens=10, output_tokens=5),
|
||||||
|
)
|
||||||
|
with patch(
|
||||||
|
'src.evaluation_runtime.LocalCodingAgent.run',
|
||||||
|
return_value=fake_result,
|
||||||
|
) as run:
|
||||||
|
suggestion = runtime.suggest_label_mapping(
|
||||||
|
dataset['id'],
|
||||||
|
'alice',
|
||||||
|
mapping=dataset['mapping'],
|
||||||
|
skill_name='label-fast-slow-routing',
|
||||||
|
)
|
||||||
|
self.assertEqual(run.call_count, 1)
|
||||||
|
self.assertEqual(
|
||||||
|
suggestion['label_map'],
|
||||||
|
{'快系统': '快', '慢系统': '慢'},
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
runtime.shutdown()
|
||||||
|
|
||||||
def test_single_analysis_uses_snapshot_without_dataset_records(self) -> None:
|
def test_single_analysis_uses_snapshot_without_dataset_records(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
|||||||
Reference in New Issue
Block a user