Skip to content

Commit 68c318e

Browse files
committed
fix(evaluation): 统一审计口径并补强提示词恢复
1 parent 7c2aaf1 commit 68c318e

9 files changed

Lines changed: 322 additions & 129 deletions

examples/optimization/eval_optimize_loop/artifact_writer.py

Lines changed: 8 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
from .schemas import ArtifactIndex, ArtifactReference, FailureReport
2525
from .schemas import OptimizationReport, ReportPhase
2626
from .schemas import TraceCandidateProposal
27+
from .sensitive_config import SensitiveConfigError
28+
from .sensitive_config import validate_persisted_sensitive_values
2729

2830

2931
ArtifactType: TypeAlias = Literal[
@@ -36,46 +38,6 @@
3638
]
3739

3840
_INPUT_COPY_DISABLED = "artifacts.copy_input_files=false"
39-
_SENSITIVE_CONFIG_KEYS = {
40-
"accesstoken",
41-
"apikey",
42-
"auth",
43-
"authorization",
44-
"authtoken",
45-
"baseurl",
46-
"bearertoken",
47-
"clientsecret",
48-
"credential",
49-
"credentials",
50-
"password",
51-
"passwd",
52-
"privatekey",
53-
"secret",
54-
"secretkey",
55-
"token",
56-
"xapikey",
57-
}
58-
_SENSITIVE_CONFIG_KEY_SUFFIXES = {
59-
"accesstoken",
60-
"apikey",
61-
"authtoken",
62-
"baseurl",
63-
"bearertoken",
64-
"clientsecret",
65-
"credential",
66-
"credentials",
67-
"endpointurl",
68-
"password",
69-
"passwd",
70-
"privatekey",
71-
"secretkey",
72-
}
73-
_APPROVED_SENSITIVE_VALUES = {
74-
"",
75-
"${TRPC_AGENT_API_KEY}",
76-
"${TRPC_AGENT_BASE_URL}",
77-
"fake-not-used-in-offline-mode",
78-
}
7941
_AT_FDCWD = -100
8042
_RENAME_NOREPLACE = 1
8143
_RENAME_EXCL = 0x4
@@ -179,52 +141,17 @@ def _json_text(model: BaseModel) -> str:
179141
return model.model_dump_json(by_alias=False, indent=2) + "\n"
180142

181143

182-
def _normalized_sensitive_key(key: str) -> str:
183-
return key.replace("_", "").replace("-", "").casefold()
184-
185-
186-
def _is_sensitive_config_key(key: str) -> bool:
187-
normalized = _normalized_sensitive_key(key)
188-
return normalized in _SENSITIVE_CONFIG_KEYS or any(
189-
normalized.endswith(suffix)
190-
for suffix in _SENSITIVE_CONFIG_KEY_SUFFIXES
191-
)
192-
193-
194-
def _validate_sensitive_config_values(value: object, *, path: str = "$") -> None:
195-
if isinstance(value, str):
196-
if value.strip().casefold().startswith(("http://", "https://")):
197-
raise ArtifactWriteError(
198-
"sensitive optimizer config value is not an approved "
199-
f"placeholder: {path}"
200-
)
201-
return
202-
if isinstance(value, list):
203-
for index, item in enumerate(value):
204-
_validate_sensitive_config_values(item, path=f"{path}[{index}]")
205-
return
206-
if not isinstance(value, dict):
207-
return
208-
for key, item in value.items():
209-
item_path = f"{path}.{key}"
210-
if _is_sensitive_config_key(key):
211-
if not isinstance(item, str) or item not in _APPROVED_SENSITIVE_VALUES:
212-
raise ArtifactWriteError(
213-
"sensitive optimizer config value is not an approved "
214-
f"placeholder: {item_path}"
215-
)
216-
else:
217-
_validate_sensitive_config_values(item, path=item_path)
218-
219-
220144
def _validate_optimizer_config_for_copy(path: Path) -> None:
221145
try:
222146
payload = json.loads(path.read_text(encoding="utf-8"))
223147
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
224148
raise ArtifactWriteError(
225149
f"failed to parse optimizer config snapshot: {path}: {exc}"
226150
) from exc
227-
_validate_sensitive_config_values(payload)
151+
try:
152+
validate_persisted_sensitive_values(payload)
153+
except SensitiveConfigError as exc:
154+
raise ArtifactWriteError(str(exc)) from exc
228155

229156

230157
def _rename_directory_no_replace(source: Path, target: Path) -> None:
@@ -621,6 +548,8 @@ def publish_report_bundle(
621548

622549
for relative in native_paths:
623550
native_path = root / relative
551+
if native_path.name == "optimizer.runtime.json":
552+
_validate_optimizer_config_for_copy(native_path)
624553
references.append(
625554
_available_reference(
626555
root,

examples/optimization/eval_optimize_loop/candidate_provider.py

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from .schemas import CandidateScenario
2525
from .schemas import OptimizerCandidateProposal
2626
from .schemas import OptimizerRuntimeParameters
27+
from .sensitive_config import replace_persisted_sensitive_values
2728

2829

2930
class CandidateProviderError(RuntimeError):
@@ -96,26 +97,7 @@ def __init__(self, call_agent: CallAgent) -> None:
9697
@staticmethod
9798
def _replace_persisted_connection_values(value: object) -> object:
9899
"""递归将可能被 SDK 复制到产物的连接值替换为环境占位符。"""
99-
if isinstance(value, list):
100-
return [
101-
AgentOptimizerCandidateProvider._replace_persisted_connection_values(item)
102-
for item in value
103-
]
104-
if not isinstance(value, dict):
105-
return value
106-
placeholders = {
107-
"api_key": "${TRPC_AGENT_API_KEY}",
108-
"apiKey": "${TRPC_AGENT_API_KEY}",
109-
"base_url": "${TRPC_AGENT_BASE_URL}",
110-
"baseUrl": "${TRPC_AGENT_BASE_URL}",
111-
}
112-
return {
113-
key: placeholders.get(
114-
key,
115-
AgentOptimizerCandidateProvider._replace_persisted_connection_values(item),
116-
)
117-
for key, item in value.items()
118-
}
100+
return replace_persisted_sensitive_values(value)
119101

120102
@staticmethod
121103
def _prepare_runtime_config(request: CandidateRequest) -> Path:

examples/optimization/eval_optimize_loop/pipeline.py

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
from trpc_agent_sdk.evaluation import CallAgent
2525
from trpc_agent_sdk.evaluation import EvalCaseResult
2626
from trpc_agent_sdk.evaluation import EvalSet
27-
from trpc_agent_sdk.evaluation import EvalStatus
2827
from trpc_agent_sdk.evaluation import OptimizeConfigFile
2928
from trpc_agent_sdk.evaluation import TargetPrompt
3029
from trpc_agent_sdk.evaluation import load_optimize_config
@@ -41,6 +40,7 @@
4140
from .config import PipelineConfig
4241
from .config import load_pipeline_config
4342
from .evaluation_adapter import EvaluationAnalysisError
43+
from .evaluation_adapter import standardize_snapshot
4444
from .fake import DeterministicFakeModel
4545
from .gate import evaluate_gate
4646
from .gate import GateEvaluationError
@@ -473,22 +473,6 @@ def prepare_run(pipeline_config_path: str | Path, *, run_id: str | None = None)
473473
raise
474474

475475

476-
def _summarize_results(
477-
eval_results_by_eval_id: dict[str, list[EvalCaseResult]],
478-
) -> tuple[int, int, float | None]:
479-
passed_cases = 0
480-
scores: list[float] = []
481-
for runs in eval_results_by_eval_id.values():
482-
if runs and all(getattr(run, "final_eval_status", None) == EvalStatus.PASSED for run in runs):
483-
passed_cases += 1
484-
for run in runs:
485-
for metric in getattr(run, "overall_eval_metric_results", []):
486-
if metric.score is not None:
487-
scores.append(float(metric.score))
488-
average_score = sum(scores) / len(scores) if scores else None
489-
return passed_cases, len(eval_results_by_eval_id), average_score
490-
491-
492476
def _validate_results(
493477
*,
494478
eval_set: EvalSet,
@@ -547,18 +531,34 @@ async def _evaluate_split(
547531
phase=phase,
548532
split=split,
549533
)
550-
passed_cases, total_cases, average_score = _summarize_results(eval_results_by_eval_id)
551-
return EvaluationSnapshot(
534+
snapshot = EvaluationSnapshot(
552535
phase=phase,
553536
split=split,
554537
eval_set_id=eval_set.eval_set_id,
555538
failed_summary=failed_summary,
556539
details_lines=details_lines,
557540
result_lines=result_lines,
558541
eval_results_by_eval_id=eval_results_by_eval_id,
559-
passed_case_count=passed_cases,
560-
total_case_count=total_cases,
561-
average_score=average_score,
542+
passed_case_count=0,
543+
total_case_count=len(eval_results_by_eval_id),
544+
average_score=None,
545+
)
546+
try:
547+
standardized = standardize_snapshot(snapshot)
548+
except EvaluationAnalysisError as exc:
549+
raise PipelineStageExecutionError(
550+
f"{phase} {split} evaluation result standardization failed: {exc}"
551+
) from exc
552+
return snapshot.model_copy(
553+
update={
554+
"passed_case_count": standardized.passed_case_count,
555+
"total_case_count": len(standardized.cases),
556+
"average_score": (
557+
standardized.average_score.value
558+
if standardized.average_score.status == "available"
559+
else None
560+
),
561+
}
562562
)
563563

564564

@@ -567,14 +567,34 @@ async def _restore_working_baseline(
567567
baseline_prompts: dict[str, str],
568568
) -> bool:
569569
"""Restore optimizer leftovers and prove the isolated baseline is present."""
570+
initial_read_error: Exception | None = None
571+
was_modified = True
570572
try:
571573
current = await prepared.working_target.read_all()
574+
except Exception as exc:
575+
initial_read_error = exc
576+
else:
572577
was_modified = current != baseline_prompts
573-
if was_modified:
578+
579+
if was_modified:
580+
try:
574581
await prepared.working_target.write_all(baseline_prompts)
582+
except Exception as exc:
583+
if initial_read_error is not None:
584+
raise PipelineStageExecutionError(
585+
"failed to restore optimizer working prompts after initial "
586+
f"read failed ({initial_read_error}): {exc}"
587+
) from exc
588+
raise PipelineStageExecutionError(
589+
f"failed to restore optimizer working prompts: {exc}"
590+
) from exc
591+
592+
try:
575593
restored = await prepared.working_target.read_all()
576594
except Exception as exc:
577-
raise PipelineStageExecutionError(f"failed to restore optimizer working prompts: {exc}") from exc
595+
raise PipelineStageExecutionError(
596+
f"failed to verify restored optimizer working prompts: {exc}"
597+
) from exc
578598
if restored != baseline_prompts:
579599
raise PipelineStageExecutionError("optimizer working prompts did not match baseline after restoration")
580600
return was_modified
@@ -592,7 +612,6 @@ async def _execute_offline_stage(
592612
the isolated working target on success or candidate-evaluation failure so
593613
the run can be inspected later.
594614
"""
595-
progress.enter("baseline_train")
596615
if prepared.config.execution.mode != "offline":
597616
raise PipelineStageExecutionError(
598617
"run_offline_stage requires execution.mode='offline', got "
@@ -756,7 +775,6 @@ async def _execute_real_stage(
756775
progress: _MutableReportProgress,
757776
) -> RealStageResult:
758777
"""Generate a real optimizer candidate and run the full guarded regression."""
759-
progress.enter("baseline_train")
760778
if prepared.config.execution.mode != "real":
761779
raise PipelineStageExecutionError(
762780
f"run_real_stage requires execution.mode='real', got {prepared.config.execution.mode!r}"

0 commit comments

Comments
 (0)