agent-learning-kit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_learning_kit-0.1.0.dist-info/METADATA +381 -0
- agent_learning_kit-0.1.0.dist-info/RECORD +642 -0
- agent_learning_kit-0.1.0.dist-info/WHEEL +4 -0
- agent_learning_kit-0.1.0.dist-info/entry_points.txt +5 -0
- agent_learning_kit-0.1.0.dist-info/licenses/LICENSE +173 -0
- agent_learning_kit-0.1.0.dist-info/licenses/NOTICE +7 -0
- fi/__init__.py +5 -0
- fi/alk/__init__.py +57 -0
- fi/alk/_facade.py +31 -0
- fi/alk/_module_alias.py +68 -0
- fi/alk/_paths.py +14 -0
- fi/alk/_schema.py +522 -0
- fi/alk/actions.py +727 -0
- fi/alk/bench/__init__.py +517 -0
- fi/alk/bench/_codeexec.py +213 -0
- fi/alk/bench/_coding.py +215 -0
- fi/alk/bench/_docker.py +237 -0
- fi/alk/bench/_grader.py +286 -0
- fi/alk/bench/_pull.py +212 -0
- fi/alk/bench/_voice.py +147 -0
- fi/alk/capabilities.py +627 -0
- fi/alk/cli.py +6396 -0
- fi/alk/config.py +130 -0
- fi/alk/cua_loop.py +562 -0
- fi/alk/evals.py +2351 -0
- fi/alk/extensions.py +163 -0
- fi/alk/harness/ARCHITECTURE.md +231 -0
- fi/alk/harness/DESIGN.md +246 -0
- fi/alk/harness/ENVIRONMENT_CONFORMANCE.md +127 -0
- fi/alk/harness/HOW-IT-WORKS.md +297 -0
- fi/alk/harness/IMPLEMENTATION_AND_VALIDATION_STATUS.md +229 -0
- fi/alk/harness/README.md +417 -0
- fi/alk/harness/__init__.py +77 -0
- fi/alk/harness/__main__.py +3 -0
- fi/alk/harness/amend.py +312 -0
- fi/alk/harness/artifacts.py +319 -0
- fi/alk/harness/authoring_entrypoint.py +189 -0
- fi/alk/harness/authoring_runtime_validation.py +267 -0
- fi/alk/harness/backends/README.md +43 -0
- fi/alk/harness/backends/__init__.py +122 -0
- fi/alk/harness/backends/base.py +241 -0
- fi/alk/harness/backends/claude.py +211 -0
- fi/alk/harness/backends/files.py +182 -0
- fi/alk/harness/backends/vertex_gemini.py +457 -0
- fi/alk/harness/background_noise.py +95 -0
- fi/alk/harness/build.py +385 -0
- fi/alk/harness/bundle.py +593 -0
- fi/alk/harness/bundle_author_v2.py +1831 -0
- fi/alk/harness/bundle_v2.py +719 -0
- fi/alk/harness/call_runner.py +1440 -0
- fi/alk/harness/callback_http_adapter.py +111 -0
- fi/alk/harness/catalogue.py +287 -0
- fi/alk/harness/chat.py +428 -0
- fi/alk/harness/chat_call_runner.py +506 -0
- fi/alk/harness/checks.py +136 -0
- fi/alk/harness/cli.py +1354 -0
- fi/alk/harness/config.py +338 -0
- fi/alk/harness/contract.py +718 -0
- fi/alk/harness/credentials.py +674 -0
- fi/alk/harness/data/persona_vocabulary.json +111 -0
- fi/alk/harness/environment.py +99 -0
- fi/alk/harness/environment_plan.py +168 -0
- fi/alk/harness/events.py +125 -0
- fi/alk/harness/executor.py +304 -0
- fi/alk/harness/folder.py +234 -0
- fi/alk/harness/generated_runtime.py +815 -0
- fi/alk/harness/github.py +72 -0
- fi/alk/harness/hosted_authoring_entrypoint.py +183 -0
- fi/alk/harness/hosted_entrypoint.py +2402 -0
- fi/alk/harness/hosted_scheduler.py +2218 -0
- fi/alk/harness/job.py +426 -0
- fi/alk/harness/judge.py +184 -0
- fi/alk/harness/livekit_source.py +50 -0
- fi/alk/harness/livekit_tool_trace_bootstrap.py +71 -0
- fi/alk/harness/observability.py +208 -0
- fi/alk/harness/outbound.py +3252 -0
- fi/alk/harness/packaging.py +515 -0
- fi/alk/harness/persona_guides.py +157 -0
- fi/alk/harness/platform.py +692 -0
- fi/alk/harness/process_preflight.py +764 -0
- fi/alk/harness/process_runtime.py +5670 -0
- fi/alk/harness/prove.py +425 -0
- fi/alk/harness/provider_import.py +703 -0
- fi/alk/harness/provider_lifecycle.py +392 -0
- fi/alk/harness/provision.py +2896 -0
- fi/alk/harness/reception.py +147 -0
- fi/alk/harness/retell_chat_call_runner.py +373 -0
- fi/alk/harness/run/__init__.py +296 -0
- fi/alk/harness/run/alk.py +184 -0
- fi/alk/harness/run/call.py +162 -0
- fi/alk/harness/run/conversation.py +264 -0
- fi/alk/harness/run/data/voices_by_language_and_gender.json +693 -0
- fi/alk/harness/run/evidence.py +195 -0
- fi/alk/harness/run/grade.py +598 -0
- fi/alk/harness/run/live.py +297 -0
- fi/alk/harness/run/models.py +56 -0
- fi/alk/harness/run/platform_evals.py +227 -0
- fi/alk/harness/run/sdk_voice.py +130 -0
- fi/alk/harness/run/simulation.py +1209 -0
- fi/alk/harness/run/stage.py +91 -0
- fi/alk/harness/run/targets.py +508 -0
- fi/alk/harness/run/tools.py +601 -0
- fi/alk/harness/run/voice.py +340 -0
- fi/alk/harness/runtime.py +172 -0
- fi/alk/harness/sandbox_server.py +2011 -0
- fi/alk/harness/sandbox_worker.py +44 -0
- fi/alk/harness/scenario.py +1048 -0
- fi/alk/harness/scenario_source.py +879 -0
- fi/alk/harness/scenario_tools.py +1143 -0
- fi/alk/harness/scenarios.py +915 -0
- fi/alk/harness/secrets.py +168 -0
- fi/alk/harness/service_catalog.py +97 -0
- fi/alk/harness/session.py +391 -0
- fi/alk/harness/sessions.py +372 -0
- fi/alk/harness/simulator.py +76 -0
- fi/alk/harness/simulator_voice.py +928 -0
- fi/alk/harness/skills/build-environment/SKILL.md +538 -0
- fi/alk/harness/skills/harness.md +131 -0
- fi/alk/harness/skills/kinds/chat.md +48 -0
- fi/alk/harness/skills/kinds/voice-voicemail.md +63 -0
- fi/alk/harness/skills/kinds/voice.md +59 -0
- fi/alk/harness/skills/plan-suite/SKILL.md +103 -0
- fi/alk/harness/skills/provision-environment/SKILL.md +136 -0
- fi/alk/harness/skills/run-scenarios/SKILL.md +112 -0
- fi/alk/harness/skills/understand-agent/SKILL.md +251 -0
- fi/alk/harness/skills/write-scenarios/SKILL.md +606 -0
- fi/alk/harness/skills/write-scenarios/references/refusals.md +28 -0
- fi/alk/harness/skills/write-scenarios/references/world-api.md +92 -0
- fi/alk/harness/source_data_invariants.py +444 -0
- fi/alk/harness/source_tool_evidence.py +79 -0
- fi/alk/harness/sources.py +253 -0
- fi/alk/harness/spend.py +140 -0
- fi/alk/harness/tool_trace_proxy.py +104 -0
- fi/alk/harness/tools.py +1018 -0
- fi/alk/harness/understand.py +169 -0
- fi/alk/harness/voicemail_audio.py +74 -0
- fi/alk/harness/world/__init__.py +33 -0
- fi/alk/harness/world/errors.py +68 -0
- fi/alk/harness/world/expectations.py +91 -0
- fi/alk/harness/world/handle.py +538 -0
- fi/alk/harness/world/kinds.py +196 -0
- fi/alk/harness/world/mutate.py +186 -0
- fi/alk/harness/world/probe.py +413 -0
- fi/alk/harness/world/provision.py +511 -0
- fi/alk/harness/world/provisioned.py +191 -0
- fi/alk/harness/world/runtime.py +616 -0
- fi/alk/harness/world/snapshot.py +288 -0
- fi/alk/harness/world/stores/__init__.py +305 -0
- fi/alk/harness/world/stores/container.py +215 -0
- fi/alk/harness/world/stores/inprocess.py +346 -0
- fi/alk/harness/world/stores/postgres.py +481 -0
- fi/alk/harness/world/stores/prove.py +202 -0
- fi/alk/harness/world/stores/sqlite.py +245 -0
- fi/alk/harness/world/stores/written.py +182 -0
- fi/alk/harness/world/tools.py +1516 -0
- fi/alk/harness/world/workspace.py +144 -0
- fi/alk/image_loop.py +453 -0
- fi/alk/image_perturb.py +241 -0
- fi/alk/improve.py +274 -0
- fi/alk/live/__init__.py +154 -0
- fi/alk/live/_attribution.py +184 -0
- fi/alk/live/_capture.py +264 -0
- fi/alk/live/_codec.py +391 -0
- fi/alk/live/_contract.py +134 -0
- fi/alk/live/_loopback.py +316 -0
- fi/alk/live/_perturb.py +449 -0
- fi/alk/live/_runner.py +386 -0
- fi/alk/live/_stats.py +561 -0
- fi/alk/live/_transcript.py +240 -0
- fi/alk/live/_workers/__init__.py +9 -0
- fi/alk/live/_workers/a2a_worker.py +316 -0
- fi/alk/live/_workers/langgraph_worker.py +217 -0
- fi/alk/live/_workers/livekit_worker.py +207 -0
- fi/alk/live/_workers/mcp_loopback_server.py +46 -0
- fi/alk/live/_workers/mcp_worker.py +158 -0
- fi/alk/live/_workers/pipecat_worker.py +189 -0
- fi/alk/live/a2a_lane.py +138 -0
- fi/alk/live/langgraph_lane.py +339 -0
- fi/alk/live/livekit_lane.py +376 -0
- fi/alk/live/mcp_lane.py +172 -0
- fi/alk/live/pipecat_lane.py +341 -0
- fi/alk/live/voice_redteam.py +494 -0
- fi/alk/loss.py +306 -0
- fi/alk/optimize.py +36260 -0
- fi/alk/practice/__init__.py +51 -0
- fi/alk/practice/_assess.py +103 -0
- fi/alk/practice/_budget.py +81 -0
- fi/alk/practice/_calibrate.py +69 -0
- fi/alk/practice/_capstone.py +86 -0
- fi/alk/practice/_contract.py +91 -0
- fi/alk/practice/_diagnose.py +79 -0
- fi/alk/practice/_drill.py +196 -0
- fi/alk/practice/_experiment.py +720 -0
- fi/alk/practice/_schedule.py +102 -0
- fi/alk/practice/_store.py +194 -0
- fi/alk/practice/_trainer.py +245 -0
- fi/alk/practice/_update.py +125 -0
- fi/alk/redteam.py +2621 -0
- fi/alk/rewardhack.py +237 -0
- fi/alk/simulate.py +10351 -0
- fi/alk/studio/__init__.py +82 -0
- fi/alk/studio/_bias.py +314 -0
- fi/alk/studio/_calibration.py +522 -0
- fi/alk/studio/_coverage.py +262 -0
- fi/alk/studio/_download.py +665 -0
- fi/alk/studio/_fidelity_attack.py +114 -0
- fi/alk/studio/_generate.py +652 -0
- fi/alk/studio/_library.py +370 -0
- fi/alk/studio/_scan.py +134 -0
- fi/alk/studio/_upgrade.py +42 -0
- fi/alk/studio/_vendor.py +172 -0
- fi/alk/suite.py +4200 -0
- fi/alk/tasks.py +828 -0
- fi/alk/telemetry/__init__.py +149 -0
- fi/alk/telemetry/_contract.py +141 -0
- fi/alk/telemetry/_emit.py +182 -0
- fi/alk/telemetry/_ledger.py +296 -0
- fi/alk/telemetry/_queue.py +127 -0
- fi/alk/telemetry/_row.py +294 -0
- fi/alk/telemetry/_run.py +233 -0
- fi/alk/telemetry/_sync.py +193 -0
- fi/alk/telemetry/_url.py +119 -0
- fi/alk/trinity.py +49397 -0
- fi/alk/voice_loop.py +174 -0
- fi/api/__init__.py +1 -0
- fi/api/auth.py +137 -0
- fi/api/types.py +29 -0
- fi/cli/__init__.py +9 -0
- fi/cli/assertions/__init__.py +25 -0
- fi/cli/assertions/conditions.py +76 -0
- fi/cli/assertions/evaluator.py +286 -0
- fi/cli/assertions/exit_codes.py +20 -0
- fi/cli/assertions/parser.py +131 -0
- fi/cli/assertions/reporter.py +194 -0
- fi/cli/commands/__init__.py +9 -0
- fi/cli/commands/config.py +165 -0
- fi/cli/commands/export.py +208 -0
- fi/cli/commands/init.py +112 -0
- fi/cli/commands/list_cmd.py +213 -0
- fi/cli/commands/run.py +486 -0
- fi/cli/commands/validate.py +173 -0
- fi/cli/commands/view.py +424 -0
- fi/cli/config/__init__.py +6 -0
- fi/cli/config/defaults.py +206 -0
- fi/cli/config/loader.py +155 -0
- fi/cli/config/schema.py +174 -0
- fi/cli/main.py +78 -0
- fi/cli/output/__init__.py +6 -0
- fi/cli/output/formatters.py +106 -0
- fi/cli/output/reporters.py +46 -0
- fi/cli/storage/__init__.py +5 -0
- fi/cli/storage/run_history.py +249 -0
- fi/cli/utils/__init__.py +5 -0
- fi/cli/utils/console.py +44 -0
- fi/evals/__init__.py +131 -0
- fi/evals/autoeval/__init__.py +137 -0
- fi/evals/autoeval/analyzer.py +211 -0
- fi/evals/autoeval/config.py +244 -0
- fi/evals/autoeval/export.py +213 -0
- fi/evals/autoeval/interactive.py +283 -0
- fi/evals/autoeval/pipeline.py +625 -0
- fi/evals/autoeval/prompts.py +139 -0
- fi/evals/autoeval/recommender.py +242 -0
- fi/evals/autoeval/rules.py +589 -0
- fi/evals/autoeval/templates.py +299 -0
- fi/evals/autoeval/types.py +232 -0
- fi/evals/core/__init__.py +16 -0
- fi/evals/core/cloud_registry.py +184 -0
- fi/evals/core/engines.py +368 -0
- fi/evals/core/evaluate.py +319 -0
- fi/evals/core/judge_prompt.py +90 -0
- fi/evals/core/prompt_generator.py +83 -0
- fi/evals/core/registry.py +57 -0
- fi/evals/core/result.py +55 -0
- fi/evals/evaluator.py +721 -0
- fi/evals/execution.py +168 -0
- fi/evals/feedback/__init__.py +32 -0
- fi/evals/feedback/calibrator.py +160 -0
- fi/evals/feedback/collector.py +214 -0
- fi/evals/feedback/hooks.py +81 -0
- fi/evals/feedback/retriever.py +128 -0
- fi/evals/feedback/store.py +272 -0
- fi/evals/feedback/types.py +99 -0
- fi/evals/framework/README.md +79 -0
- fi/evals/framework/__init__.py +267 -0
- fi/evals/framework/backends/Dockerfile.eval-runner +33 -0
- fi/evals/framework/backends/__init__.py +99 -0
- fi/evals/framework/backends/_container.py +141 -0
- fi/evals/framework/backends/_utils.py +145 -0
- fi/evals/framework/backends/base.py +223 -0
- fi/evals/framework/backends/celery_backend.py +417 -0
- fi/evals/framework/backends/celery_worker.py +78 -0
- fi/evals/framework/backends/kubernetes_backend.py +665 -0
- fi/evals/framework/backends/ray_backend.py +521 -0
- fi/evals/framework/backends/temporal.py +350 -0
- fi/evals/framework/backends/temporal_worker.py +126 -0
- fi/evals/framework/backends/thread_pool.py +286 -0
- fi/evals/framework/context.py +258 -0
- fi/evals/framework/enrichment.py +306 -0
- fi/evals/framework/evals/__init__.py +68 -0
- fi/evals/framework/evals/agentic.py +399 -0
- fi/evals/framework/evals/builder.py +609 -0
- fi/evals/framework/evals/semantic.py +142 -0
- fi/evals/framework/evaluator.py +647 -0
- fi/evals/framework/evaluators/__init__.py +22 -0
- fi/evals/framework/evaluators/blocking.py +347 -0
- fi/evals/framework/evaluators/non_blocking.py +577 -0
- fi/evals/framework/propagation.py +421 -0
- fi/evals/framework/protocols.py +385 -0
- fi/evals/framework/registry.py +370 -0
- fi/evals/framework/resilience/__init__.py +150 -0
- fi/evals/framework/resilience/circuit_breaker.py +309 -0
- fi/evals/framework/resilience/degradation.py +355 -0
- fi/evals/framework/resilience/health.py +505 -0
- fi/evals/framework/resilience/rate_limiter.py +228 -0
- fi/evals/framework/resilience/retry.py +274 -0
- fi/evals/framework/resilience/types.py +288 -0
- fi/evals/framework/resilience/wrapper.py +433 -0
- fi/evals/framework/types.py +218 -0
- fi/evals/guardrails/README.md +915 -0
- fi/evals/guardrails/__init__.py +96 -0
- fi/evals/guardrails/backends/__init__.py +43 -0
- fi/evals/guardrails/backends/azure.py +361 -0
- fi/evals/guardrails/backends/base.py +88 -0
- fi/evals/guardrails/backends/generic_llm.py +163 -0
- fi/evals/guardrails/backends/granite.py +216 -0
- fi/evals/guardrails/backends/llamaguard.py +221 -0
- fi/evals/guardrails/backends/local_base.py +479 -0
- fi/evals/guardrails/backends/openai.py +365 -0
- fi/evals/guardrails/backends/qwen.py +170 -0
- fi/evals/guardrails/backends/shieldgemma.py +154 -0
- fi/evals/guardrails/backends/turing.py +235 -0
- fi/evals/guardrails/backends/vllm_client.py +321 -0
- fi/evals/guardrails/backends/wildguard.py +188 -0
- fi/evals/guardrails/base.py +888 -0
- fi/evals/guardrails/config.py +221 -0
- fi/evals/guardrails/discovery.py +243 -0
- fi/evals/guardrails/gateway.py +437 -0
- fi/evals/guardrails/registry.py +231 -0
- fi/evals/guardrails/scanners/__init__.py +127 -0
- fi/evals/guardrails/scanners/base.py +191 -0
- fi/evals/guardrails/scanners/code_injection.py +243 -0
- fi/evals/guardrails/scanners/eval_delegate.py +574 -0
- fi/evals/guardrails/scanners/invisible_chars.py +351 -0
- fi/evals/guardrails/scanners/jailbreak.py +412 -0
- fi/evals/guardrails/scanners/language.py +288 -0
- fi/evals/guardrails/scanners/pipeline.py +260 -0
- fi/evals/guardrails/scanners/regex.py +311 -0
- fi/evals/guardrails/scanners/secrets.py +274 -0
- fi/evals/guardrails/scanners/topics.py +649 -0
- fi/evals/guardrails/scanners/urls.py +341 -0
- fi/evals/guardrails/types.py +96 -0
- fi/evals/llm/__init__.py +3 -0
- fi/evals/llm/base_llm_provider.py +35 -0
- fi/evals/llm/providers/litellm.py +70 -0
- fi/evals/local/__init__.py +90 -0
- fi/evals/local/evaluator.py +690 -0
- fi/evals/local/execution_mode.py +121 -0
- fi/evals/local/llm.py +489 -0
- fi/evals/local/metrics/__init__.py +19 -0
- fi/evals/local/registry.py +360 -0
- fi/evals/manager.py +1018 -0
- fi/evals/manager_types.py +362 -0
- fi/evals/metrics/__init__.py +185 -0
- fi/evals/metrics/agents/__init__.py +74 -0
- fi/evals/metrics/agents/metrics.py +693 -0
- fi/evals/metrics/agents/report.py +36463 -0
- fi/evals/metrics/agents/types.py +160 -0
- fi/evals/metrics/base_llm_metric.py +111 -0
- fi/evals/metrics/base_metric.py +138 -0
- fi/evals/metrics/code_security/__init__.py +305 -0
- fi/evals/metrics/code_security/analyzer.py +985 -0
- fi/evals/metrics/code_security/benchmarks/__init__.py +73 -0
- fi/evals/metrics/code_security/benchmarks/builtin.py +750 -0
- fi/evals/metrics/code_security/benchmarks/loader.py +580 -0
- fi/evals/metrics/code_security/benchmarks/types.py +308 -0
- fi/evals/metrics/code_security/detectors/__init__.py +186 -0
- fi/evals/metrics/code_security/detectors/base.py +394 -0
- fi/evals/metrics/code_security/detectors/cryptography.py +345 -0
- fi/evals/metrics/code_security/detectors/injection.py +744 -0
- fi/evals/metrics/code_security/detectors/secrets.py +287 -0
- fi/evals/metrics/code_security/detectors/serialization.py +192 -0
- fi/evals/metrics/code_security/joint_metrics.py +588 -0
- fi/evals/metrics/code_security/judges/__init__.py +83 -0
- fi/evals/metrics/code_security/judges/base.py +238 -0
- fi/evals/metrics/code_security/judges/dual_judge.py +534 -0
- fi/evals/metrics/code_security/judges/llm_judge.py +301 -0
- fi/evals/metrics/code_security/judges/pattern_judge.py +515 -0
- fi/evals/metrics/code_security/metrics.py +388 -0
- fi/evals/metrics/code_security/modes/__init__.py +63 -0
- fi/evals/metrics/code_security/modes/adversarial.py +284 -0
- fi/evals/metrics/code_security/modes/autocomplete.py +198 -0
- fi/evals/metrics/code_security/modes/base.py +283 -0
- fi/evals/metrics/code_security/modes/instruct.py +253 -0
- fi/evals/metrics/code_security/modes/repair.py +230 -0
- fi/evals/metrics/code_security/reports/__init__.py +57 -0
- fi/evals/metrics/code_security/reports/generator.py +404 -0
- fi/evals/metrics/code_security/reports/leaderboard.py +509 -0
- fi/evals/metrics/code_security/types.py +534 -0
- fi/evals/metrics/function_calling/__init__.py +34 -0
- fi/evals/metrics/function_calling/metrics.py +573 -0
- fi/evals/metrics/function_calling/types.py +87 -0
- fi/evals/metrics/hallucination/__init__.py +54 -0
- fi/evals/metrics/hallucination/detector.py +149 -0
- fi/evals/metrics/hallucination/metrics.py +390 -0
- fi/evals/metrics/hallucination/nli.py +253 -0
- fi/evals/metrics/hallucination/sentinel.py +106 -0
- fi/evals/metrics/hallucination/types.py +132 -0
- fi/evals/metrics/heuristics/aggregation_metrics.py +85 -0
- fi/evals/metrics/heuristics/json_metrics.py +87 -0
- fi/evals/metrics/heuristics/similarity_metrics.py +375 -0
- fi/evals/metrics/heuristics/string_metrics.py +391 -0
- fi/evals/metrics/llm_as_judges/__init__.py +17 -0
- fi/evals/metrics/llm_as_judges/custom_judge/metric.py +112 -0
- fi/evals/metrics/llm_as_judges/custom_judge/prompts.py +26 -0
- fi/evals/metrics/llm_as_judges/types.py +48 -0
- fi/evals/metrics/rag/__init__.py +111 -0
- fi/evals/metrics/rag/advanced/__init__.py +14 -0
- fi/evals/metrics/rag/advanced/multi_hop.py +283 -0
- fi/evals/metrics/rag/advanced/source_attribution.py +344 -0
- fi/evals/metrics/rag/generation/__init__.py +17 -0
- fi/evals/metrics/rag/generation/answer_relevancy.py +176 -0
- fi/evals/metrics/rag/generation/context_utilization.py +245 -0
- fi/evals/metrics/rag/generation/faithfulness.py +241 -0
- fi/evals/metrics/rag/generation/groundedness.py +131 -0
- fi/evals/metrics/rag/rag_score.py +277 -0
- fi/evals/metrics/rag/retrieval/__init__.py +20 -0
- fi/evals/metrics/rag/retrieval/context_entity_recall.py +124 -0
- fi/evals/metrics/rag/retrieval/context_precision.py +158 -0
- fi/evals/metrics/rag/retrieval/context_recall.py +106 -0
- fi/evals/metrics/rag/retrieval/noise_sensitivity.py +163 -0
- fi/evals/metrics/rag/retrieval/ranking.py +261 -0
- fi/evals/metrics/rag/types.py +100 -0
- fi/evals/metrics/rag/utils/__init__.py +62 -0
- fi/evals/metrics/rag/utils/claims.py +189 -0
- fi/evals/metrics/rag/utils/entities.py +244 -0
- fi/evals/metrics/rag/utils/nli.py +92 -0
- fi/evals/metrics/rag/utils/similarity.py +345 -0
- fi/evals/metrics/structured/__init__.py +114 -0
- fi/evals/metrics/structured/field_completeness.py +313 -0
- fi/evals/metrics/structured/hierarchy_score.py +366 -0
- fi/evals/metrics/structured/json_validation.py +190 -0
- fi/evals/metrics/structured/schema_compliance.py +280 -0
- fi/evals/metrics/structured/structured_output_score.py +298 -0
- fi/evals/metrics/structured/types.py +108 -0
- fi/evals/metrics/structured/validators/__init__.py +30 -0
- fi/evals/metrics/structured/validators/base.py +189 -0
- fi/evals/metrics/structured/validators/json_validator.py +196 -0
- fi/evals/metrics/structured/validators/pydantic_validator.py +178 -0
- fi/evals/metrics/structured/validators/yaml_validator.py +248 -0
- fi/evals/otel/__init__.py +266 -0
- fi/evals/otel/config.py +400 -0
- fi/evals/otel/conventions.py +463 -0
- fi/evals/otel/enrichment.py +371 -0
- fi/evals/otel/instrumentors/__init__.py +140 -0
- fi/evals/otel/instrumentors/anthropic.py +517 -0
- fi/evals/otel/instrumentors/base.py +382 -0
- fi/evals/otel/instrumentors/openai.py +673 -0
- fi/evals/otel/processors/__init__.py +36 -0
- fi/evals/otel/processors/base.py +473 -0
- fi/evals/otel/processors/cost.py +445 -0
- fi/evals/otel/processors/evaluation.py +559 -0
- fi/evals/otel/processors/llm.py +462 -0
- fi/evals/otel/tracer.py +506 -0
- fi/evals/otel/types.py +232 -0
- fi/evals/otel_utils.py +23 -0
- fi/evals/protect.py +671 -0
- fi/evals/protect_input_adapter.py +154 -0
- fi/evals/streaming/__init__.py +88 -0
- fi/evals/streaming/buffer.py +213 -0
- fi/evals/streaming/evaluator.py +551 -0
- fi/evals/streaming/policy.py +307 -0
- fi/evals/streaming/scorers.py +368 -0
- fi/evals/streaming/types.py +238 -0
- fi/evals/templates.py +472 -0
- fi/evals/types.py +156 -0
- fi/opt/__init__.py +221 -0
- fi/opt/_objective_scoring.py +85 -0
- fi/opt/base/__init__.py +11 -0
- fi/opt/base/base_generator.py +33 -0
- fi/opt/base/base_mapper.py +26 -0
- fi/opt/base/base_optimizer.py +45 -0
- fi/opt/base/evaluator.py +211 -0
- fi/opt/components.py +3095 -0
- fi/opt/datamappers/__init__.py +3 -0
- fi/opt/datamappers/basic_mapper.py +40 -0
- fi/opt/deployment.py +1021 -0
- fi/opt/evidence.py +4332 -0
- fi/opt/generators/__init__.py +3 -0
- fi/opt/generators/litellm.py +66 -0
- fi/opt/integrations/__init__.py +23 -0
- fi/opt/integrations/generative_suite.py +410 -0
- fi/opt/integrations/simulate.py +1313 -0
- fi/opt/mutations.py +771 -0
- fi/opt/observability.py +4639 -0
- fi/opt/optimizer_trace.py +889 -0
- fi/opt/optimizers/__init__.py +80 -0
- fi/opt/optimizers/agent.py +331 -0
- fi/opt/optimizers/agent_bandit.py +392 -0
- fi/opt/optimizers/agent_curriculum.py +635 -0
- fi/opt/optimizers/agent_evolution.py +894 -0
- fi/opt/optimizers/agent_feedback.py +1863 -0
- fi/opt/optimizers/agent_pareto.py +547 -0
- fi/opt/optimizers/agent_social_memory.py +1113 -0
- fi/opt/optimizers/agent_tpe.py +321 -0
- fi/opt/optimizers/bayesian_search.py +449 -0
- fi/opt/optimizers/council.py +2075 -0
- fi/opt/optimizers/futureagi_replay.py +799 -0
- fi/opt/optimizers/gepa.py +322 -0
- fi/opt/optimizers/metaprompt.py +243 -0
- fi/opt/optimizers/promptwizard.py +417 -0
- fi/opt/optimizers/protegi.py +329 -0
- fi/opt/optimizers/random_search.py +224 -0
- fi/opt/research.py +518 -0
- fi/opt/simulation.py +260 -0
- fi/opt/targets.py +232 -0
- fi/opt/types.py +66 -0
- fi/opt/utils/__init__.py +4 -0
- fi/opt/utils/early_stopping.py +266 -0
- fi/opt/utils/setup_logging.py +82 -0
- fi/simulate/__init__.py +540 -0
- fi/simulate/_hashing.py +35 -0
- fi/simulate/_logging.py +10 -0
- fi/simulate/adapters.py +87 -0
- fi/simulate/agent/__init__.py +120 -0
- fi/simulate/agent/browser.py +658 -0
- fi/simulate/agent/definition.py +587 -0
- fi/simulate/agent/frameworks.py +3528 -0
- fi/simulate/agent/generic.py +8286 -0
- fi/simulate/agent/import_probe.py +227 -0
- fi/simulate/agent/memory.py +905 -0
- fi/simulate/agent/mocks.py +101 -0
- fi/simulate/agent/multi_agent.py +361 -0
- fi/simulate/agent/orchestration.py +903 -0
- fi/simulate/agent/realtime.py +665 -0
- fi/simulate/agent/wrapper.py +99 -0
- fi/simulate/agent/wrappers/__init__.py +18 -0
- fi/simulate/agent/wrappers/anthropic.py +62 -0
- fi/simulate/agent/wrappers/gemini.py +65 -0
- fi/simulate/agent/wrappers/http.py +404 -0
- fi/simulate/agent/wrappers/langchain.py +80 -0
- fi/simulate/agent/wrappers/openai.py +75 -0
- fi/simulate/agent/wrappers/websocket.py +326 -0
- fi/simulate/artifacts/__init__.py +11 -0
- fi/simulate/artifacts/manifest.py +62 -0
- fi/simulate/cli.py +20560 -0
- fi/simulate/endpoints/__init__.py +45 -0
- fi/simulate/endpoints/_http_actor.py +73 -0
- fi/simulate/endpoints/actor_sources.py +243 -0
- fi/simulate/endpoints/base.py +107 -0
- fi/simulate/endpoints/builtins.py +10 -0
- fi/simulate/endpoints/callable.py +95 -0
- fi/simulate/endpoints/http.py +76 -0
- fi/simulate/endpoints/livekit.py +138 -0
- fi/simulate/endpoints/originators.py +132 -0
- fi/simulate/endpoints/profiles.py +348 -0
- fi/simulate/endpoints/retell.py +633 -0
- fi/simulate/endpoints/vapi.py +205 -0
- fi/simulate/endpoints/websocket.py +76 -0
- fi/simulate/environment.py +33026 -0
- fi/simulate/environments/__init__.py +11 -0
- fi/simulate/environments/base.py +73 -0
- fi/simulate/environments/chat.py +697 -0
- fi/simulate/environments/voice.py +212 -0
- fi/simulate/evaluation/__init__.py +4 -0
- fi/simulate/evaluation/ai_eval.py +227 -0
- fi/simulate/evidence/__init__.py +35 -0
- fi/simulate/evidence/base.py +59 -0
- fi/simulate/evidence/caller_observed.py +50 -0
- fi/simulate/evidence/livekit_instrumentation.py +51 -0
- fi/simulate/evidence/livekit_room.py +50 -0
- fi/simulate/evidence/otel.py +49 -0
- fi/simulate/evidence/providers/__init__.py +24 -0
- fi/simulate/evidence/providers/base.py +61 -0
- fi/simulate/evidence/providers/retell.py +376 -0
- fi/simulate/evidence/providers/vapi.py +426 -0
- fi/simulate/hosted/__init__.py +32 -0
- fi/simulate/hosted/child_entrypoint.py +306 -0
- fi/simulate/hosted/job.py +150 -0
- fi/simulate/hosted/targets.py +53 -0
- fi/simulate/instrumentation/__init__.py +5 -0
- fi/simulate/instrumentation/livekit/__init__.py +122 -0
- fi/simulate/manifest.py +1033 -0
- fi/simulate/matrix_cli.py +165 -0
- fi/simulate/realtime/__init__.py +40 -0
- fi/simulate/realtime/events.py +107 -0
- fi/simulate/realtime/media.py +61 -0
- fi/simulate/realtime/session.py +91 -0
- fi/simulate/recording/__init__.py +5 -0
- fi/simulate/recording/room_recorder.py +326 -0
- fi/simulate/registry.py +185 -0
- fi/simulate/results/__init__.py +9 -0
- fi/simulate/results/base.py +18 -0
- fi/simulate/results/filesystem.py +71 -0
- fi/simulate/results/futureagi.py +1340 -0
- fi/simulate/runtime/__init__.py +85 -0
- fi/simulate/runtime/capabilities.py +40 -0
- fi/simulate/runtime/events.py +63 -0
- fi/simulate/runtime/failures.py +25 -0
- fi/simulate/runtime/ids.py +34 -0
- fi/simulate/runtime/plan.py +70 -0
- fi/simulate/runtime/planner.py +102 -0
- fi/simulate/runtime/report.py +174 -0
- fi/simulate/runtime/run.py +75 -0
- fi/simulate/runtime/runner.py +333 -0
- fi/simulate/runtime/spec.py +186 -0
- fi/simulate/simulation/__init__.py +30 -0
- fi/simulate/simulation/behavior_policy.py +425 -0
- fi/simulate/simulation/bridge/__init__.py +9 -0
- fi/simulate/simulation/bridge/audio.py +29 -0
- fi/simulate/simulation/bridge/connector.py +46 -0
- fi/simulate/simulation/bridge/livekit.py +252 -0
- fi/simulate/simulation/bridge/retell.py +188 -0
- fi/simulate/simulation/bridge/vapi.py +177 -0
- fi/simulate/simulation/contract.py +419 -0
- fi/simulate/simulation/engines/__init__.py +12 -0
- fi/simulate/simulation/engines/base.py +21 -0
- fi/simulate/simulation/engines/cloud.py +517 -0
- fi/simulate/simulation/engines/livekit.py +4167 -0
- fi/simulate/simulation/engines/local_text.py +89 -0
- fi/simulate/simulation/fidelity.py +374 -0
- fi/simulate/simulation/gemini_tts_stream.py +110 -0
- fi/simulate/simulation/generator.py +91 -0
- fi/simulate/simulation/goal_machine.py +185 -0
- fi/simulate/simulation/livekit_models.py +467 -0
- fi/simulate/simulation/matrix.py +170 -0
- fi/simulate/simulation/models.py +279 -0
- fi/simulate/simulation/runner.py +153 -0
- fi/simulate/simulation/synthetic.py +880 -0
- fi/simulate/simulation/voice_prompt.py +502 -0
- fi/simulate/simulator/__init__.py +55 -0
- fi/simulate/simulator/builtins.py +53 -0
- fi/simulate/suite.py +1288 -0
- fi/simulate/utils/routes.py +164 -0
- fi/simulate/voice.py +225 -0
- fi/simulate/voice_cli.py +182 -0
- fi/utils/__init__.py +1 -0
- fi/utils/constants.py +14 -0
- fi/utils/errors.py +200 -0
- fi/utils/executor.py +26 -0
- fi/utils/routes.py +119 -0
- fi/utils/utils.py +17 -0
fi/alk/tasks.py
ADDED
|
@@ -0,0 +1,828 @@
|
|
|
1
|
+
"""First-class Task + TaskDataset — the benchmark surface.
|
|
2
|
+
|
|
3
|
+
A Task is a THIN COMPOSITION over models that already exist and are content-
|
|
4
|
+
addressed (no new invention — empirically validated by spike):
|
|
5
|
+
|
|
6
|
+
* the existing typed ``Scenario(kind="task")`` (one effective task row) carries
|
|
7
|
+
goal + verification (fi.simulate.simulation.models);
|
|
8
|
+
* the objective is compiled by ``loss.compile_objective`` VERBATIM, so the
|
|
9
|
+
Goodhart-guard discipline ("There is no override.") holds for a Task exactly
|
|
10
|
+
as for any training loss — a guardless objective is REJECTED here too;
|
|
11
|
+
* the world kind must be a member of ``contract.resolved_world_kinds()`` (the
|
|
12
|
+
frozen closed set + R4 extras) — never widened here;
|
|
13
|
+
* ``execution_class`` is DERIVED from the world kind (+ a fixture flag), never
|
|
14
|
+
asserted above the substrate's truth: ``browser``/``computer_use``/
|
|
15
|
+
``code_exec``/``voice_telephony`` are TYPED-ONLY in v1 and can be at most
|
|
16
|
+
``typed_only``; only ``conversation``/``tool_api`` may be ``executable``.
|
|
17
|
+
This is the kit's honesty moat: a typed-only task can NEVER masquerade
|
|
18
|
+
as a live-executed one.
|
|
19
|
+
|
|
20
|
+
Tasks and datasets are content-addressed (sha256 over the canonical payload minus
|
|
21
|
+
``version``), using the same rounding/canonicalization idiom as ``loss.py``. NO
|
|
22
|
+
provenance/capture fields are baked in yet (the real-execution fork is deferred;
|
|
23
|
+
keeping them out means a later fork decision costs nothing here).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import hashlib
|
|
29
|
+
import json
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Any, Mapping, Sequence
|
|
32
|
+
|
|
33
|
+
from .loss import ObjectiveError, compile_objective
|
|
34
|
+
|
|
35
|
+
AGENT_LEARNING_TASK_KIND = "agent-learning.task.v1"
|
|
36
|
+
AGENT_LEARNING_TASK_DATASET_KIND = "agent-learning.task-dataset.v1"
|
|
37
|
+
|
|
38
|
+
# closed sets (home here; trinity.py mirrors literals for the gate — GUNA_AXES
|
|
39
|
+
# cross-pin pattern; trinity never imports this module).
|
|
40
|
+
V1_TASK_DIFFICULTIES = ("easy", "medium", "hard")
|
|
41
|
+
V1_TASK_EXECUTION_CLASSES = ("executable", "typed_only", "fixture")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TaskError(ValueError):
|
|
45
|
+
"""Raised when a Task violates the §B1 contract. A ``ValueError`` subclass so
|
|
46
|
+
callers can ``except ValueError`` exactly as for ``ObjectiveError``."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TaskDatasetError(TaskError):
|
|
50
|
+
"""Raised when a TaskDataset violates the contract."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# --- canonicalization (the loss.py idiom, factored locally) -----------------
|
|
54
|
+
def _round_floats(value: Any) -> Any:
|
|
55
|
+
if isinstance(value, bool):
|
|
56
|
+
return value
|
|
57
|
+
if isinstance(value, float):
|
|
58
|
+
return round(value, 6)
|
|
59
|
+
if isinstance(value, Mapping):
|
|
60
|
+
return {k: _round_floats(v) for k, v in value.items()}
|
|
61
|
+
if isinstance(value, (list, tuple)):
|
|
62
|
+
return [_round_floats(v) for v in value]
|
|
63
|
+
return value
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _content_hash(payload: Mapping[str, Any]) -> str:
|
|
67
|
+
rounded = _round_floats(dict(payload))
|
|
68
|
+
canonical = json.dumps(rounded, sort_keys=True, separators=(",", ":"), default=str)
|
|
69
|
+
return "sha256:" + hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _resolved_world_kinds() -> tuple[str, ...]:
|
|
73
|
+
"""Lazy downward import (the image_loop.py idiom) — the closed set + R4
|
|
74
|
+
extras, never widened here."""
|
|
75
|
+
from fi.simulate.simulation import contract as _contract
|
|
76
|
+
|
|
77
|
+
return tuple(_contract.resolved_world_kinds())
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _executable_world_kinds() -> tuple[str, ...]:
|
|
81
|
+
from fi.simulate.simulation import contract as _contract
|
|
82
|
+
|
|
83
|
+
return tuple(_contract.EXECUTABLE_WORLD_KINDS_V1)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def derive_execution_class(world_kind: str, *, fixture_only: bool = False) -> str:
|
|
87
|
+
"""Derive the HONEST substrate stamp. ``fixture_only`` (a task that only ever
|
|
88
|
+
replays a committed fixture) → ``fixture``; a world kind that EXECUTES in v1
|
|
89
|
+
(``conversation``/``tool_api``) → ``executable``; everything else (the
|
|
90
|
+
typed-only kinds) → ``typed_only``. NEVER returns a class above the
|
|
91
|
+
substrate's real capability."""
|
|
92
|
+
|
|
93
|
+
if fixture_only:
|
|
94
|
+
return "fixture"
|
|
95
|
+
if world_kind in _executable_world_kinds():
|
|
96
|
+
return "executable"
|
|
97
|
+
return "typed_only"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _task_anchor_terms(objective: Mapping[str, Any]) -> list[str]:
|
|
101
|
+
"""Terms explicitly flagged as deterministic ground-truth anchors
|
|
102
|
+
(``anchor: true``). The reward-hacking-resistance-by-construction rule below
|
|
103
|
+
requires >= 1; an objective scored only by un-anchored (e.g. judge) terms is
|
|
104
|
+
a Task contract violation — the Task analogue of the image judge-only ban."""
|
|
105
|
+
|
|
106
|
+
return [
|
|
107
|
+
str(term.get("eval"))
|
|
108
|
+
for term in (objective.get("evals") or [])
|
|
109
|
+
if isinstance(term, Mapping) and term.get("anchor") is True and term.get("eval")
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def compile_task(payload: Mapping[str, Any]) -> dict:
|
|
114
|
+
"""Validate + stamp a Task. Enforces, ON TOP of the verbatim
|
|
115
|
+
``loss.compile_objective`` Goodhart guard:
|
|
116
|
+
|
|
117
|
+
(a) ``scenario.kind == "task"`` with exactly ONE effective task row;
|
|
118
|
+
(b) ``world.kind`` is a member of ``resolved_world_kinds()``;
|
|
119
|
+
(c) ``difficulty`` in ``V1_TASK_DIFFICULTIES``;
|
|
120
|
+
(d) the objective carries >= 1 deterministic ground-truth ANCHOR term
|
|
121
|
+
(``anchor: true``) — reward-hacking-resistance by construction;
|
|
122
|
+
(e) ``execution_class`` is DERIVED, never asserted above the substrate
|
|
123
|
+
(a caller-supplied class that overclaims the world kind is rejected).
|
|
124
|
+
Then delegates objective compilation to ``loss.compile_objective`` VERBATIM."""
|
|
125
|
+
|
|
126
|
+
raw = dict(payload)
|
|
127
|
+
|
|
128
|
+
task_id = str(raw.get("id") or "").strip()
|
|
129
|
+
if not task_id:
|
|
130
|
+
raise TaskError("task.id is required")
|
|
131
|
+
title = str(raw.get("title") or "").strip()
|
|
132
|
+
if not title:
|
|
133
|
+
raise TaskError("task.title is required")
|
|
134
|
+
|
|
135
|
+
# (b) world.kind
|
|
136
|
+
world = dict(raw.get("world") or {})
|
|
137
|
+
world_kind = str(world.get("kind") or "")
|
|
138
|
+
resolved = _resolved_world_kinds()
|
|
139
|
+
if world_kind not in resolved:
|
|
140
|
+
raise TaskError(
|
|
141
|
+
f"task.world.kind {world_kind!r} not in resolved world kinds {resolved}"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# (c) difficulty
|
|
145
|
+
difficulty = str(raw.get("difficulty") or "medium")
|
|
146
|
+
if difficulty not in V1_TASK_DIFFICULTIES:
|
|
147
|
+
raise TaskError(f"task.difficulty {difficulty!r} not in {V1_TASK_DIFFICULTIES}")
|
|
148
|
+
|
|
149
|
+
# (a) scenario.kind == "task", exactly one effective row
|
|
150
|
+
scenario = dict(raw.get("scenario") or {})
|
|
151
|
+
if str(scenario.get("kind") or "") != "task":
|
|
152
|
+
raise TaskError("task.scenario.kind must be 'task'")
|
|
153
|
+
dataset_rows = list(scenario.get("dataset") or [])
|
|
154
|
+
if len(dataset_rows) != 1:
|
|
155
|
+
raise TaskError(
|
|
156
|
+
f"task.scenario must carry exactly one task row; got {len(dataset_rows)}"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# objective: compile VERBATIM (guards enforced; guardless REJECTED here too)
|
|
160
|
+
objective_payload = raw.get("objective")
|
|
161
|
+
if not isinstance(objective_payload, Mapping):
|
|
162
|
+
raise TaskError("task.objective is required (a declared ObjectiveSpec)")
|
|
163
|
+
try:
|
|
164
|
+
compiled_objective = compile_objective(objective_payload)
|
|
165
|
+
except ObjectiveError as exc:
|
|
166
|
+
# surface as a Task error while preserving the guard message
|
|
167
|
+
raise TaskError(f"task.objective invalid: {exc}") from exc
|
|
168
|
+
|
|
169
|
+
# (d) >= 1 deterministic anchor term
|
|
170
|
+
anchors = _task_anchor_terms(objective_payload)
|
|
171
|
+
if not anchors:
|
|
172
|
+
raise TaskError(
|
|
173
|
+
"task.objective must carry >= 1 deterministic ground-truth anchor "
|
|
174
|
+
"term (mark it `anchor: true`); a task scored only by un-anchored "
|
|
175
|
+
"(e.g. judge) terms is reward-hackable by construction"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# (e) execution_class derived; reject overclaim
|
|
179
|
+
fixture_only = bool(raw.get("fixture_only"))
|
|
180
|
+
derived_class = derive_execution_class(world_kind, fixture_only=fixture_only)
|
|
181
|
+
asserted = raw.get("execution_class")
|
|
182
|
+
if asserted is not None:
|
|
183
|
+
asserted = str(asserted)
|
|
184
|
+
if asserted not in V1_TASK_EXECUTION_CLASSES:
|
|
185
|
+
raise TaskError(
|
|
186
|
+
f"task.execution_class {asserted!r} not in {V1_TASK_EXECUTION_CLASSES}"
|
|
187
|
+
)
|
|
188
|
+
rank = {"fixture": 0, "typed_only": 1, "executable": 2}
|
|
189
|
+
if rank[asserted] > rank[derived_class]:
|
|
190
|
+
raise TaskError(
|
|
191
|
+
f"task.execution_class {asserted!r} overclaims the substrate; "
|
|
192
|
+
f"world.kind {world_kind!r} (fixture_only={fixture_only}) supports "
|
|
193
|
+
f"at most {derived_class!r}"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
compiled: dict[str, Any] = {
|
|
197
|
+
"kind": AGENT_LEARNING_TASK_KIND,
|
|
198
|
+
"id": task_id,
|
|
199
|
+
"title": title,
|
|
200
|
+
"world": {"kind": world_kind, **({"spec": dict(world["spec"])} if world.get("spec") else {})},
|
|
201
|
+
"difficulty": difficulty,
|
|
202
|
+
"tags": [str(t) for t in (raw.get("tags") or [])],
|
|
203
|
+
"scenario": scenario,
|
|
204
|
+
"objective": compiled_objective,
|
|
205
|
+
"anchor_terms": anchors,
|
|
206
|
+
"execution_class": derived_class,
|
|
207
|
+
}
|
|
208
|
+
if raw.get("goal") is not None:
|
|
209
|
+
compiled["goal"] = dict(raw["goal"])
|
|
210
|
+
if raw.get("verification") is not None:
|
|
211
|
+
compiled["verification"] = dict(raw["verification"])
|
|
212
|
+
# environments: mock-tool / world env specs the runner wires into the manifest
|
|
213
|
+
# so a TOOL-USING task (mock_tools + a tool-calling agent) runs through the
|
|
214
|
+
# benchmark/RSI loop, not just direct simulate.
|
|
215
|
+
if raw.get("environments"):
|
|
216
|
+
envs = raw["environments"]
|
|
217
|
+
if not isinstance(envs, Sequence) or isinstance(envs, (str, bytes)):
|
|
218
|
+
raise TaskError("task.environments must be a list of env specs")
|
|
219
|
+
compiled["environments"] = [dict(e) for e in envs if isinstance(e, Mapping)]
|
|
220
|
+
compiled["version"] = _content_hash(
|
|
221
|
+
{k: v for k, v in compiled.items() if k != "version"}
|
|
222
|
+
)
|
|
223
|
+
return compiled
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def compile_task_dataset(payload: Mapping[str, Any]) -> dict:
|
|
227
|
+
"""Validate + stamp a TaskDataset: every task compiles via ``compile_task``;
|
|
228
|
+
ids are unique; any ``splits`` reference existing ids; coverage by world.kind
|
|
229
|
+
and difficulty is computed. Content-addressed over the compiled tasks."""
|
|
230
|
+
|
|
231
|
+
raw = dict(payload)
|
|
232
|
+
name = str(raw.get("name") or "").strip()
|
|
233
|
+
if not name:
|
|
234
|
+
raise TaskDatasetError("dataset.name is required")
|
|
235
|
+
|
|
236
|
+
task_payloads = list(raw.get("tasks") or [])
|
|
237
|
+
if not task_payloads:
|
|
238
|
+
raise TaskDatasetError("dataset.tasks must list >= 1 task")
|
|
239
|
+
|
|
240
|
+
compiled_tasks: list[dict] = []
|
|
241
|
+
seen_ids: set[str] = set()
|
|
242
|
+
by_world_kind: dict[str, int] = {}
|
|
243
|
+
by_difficulty: dict[str, int] = {}
|
|
244
|
+
for index, task_payload in enumerate(task_payloads, start=1):
|
|
245
|
+
if not isinstance(task_payload, Mapping):
|
|
246
|
+
raise TaskDatasetError(f"dataset.tasks[{index}] must be a mapping")
|
|
247
|
+
try:
|
|
248
|
+
task = compile_task(task_payload)
|
|
249
|
+
except TaskError as exc:
|
|
250
|
+
raise TaskDatasetError(f"dataset.tasks[{index}] invalid: {exc}") from exc
|
|
251
|
+
if task["id"] in seen_ids:
|
|
252
|
+
raise TaskDatasetError(f"duplicate task id {task['id']!r}")
|
|
253
|
+
seen_ids.add(task["id"])
|
|
254
|
+
compiled_tasks.append(task)
|
|
255
|
+
by_world_kind[task["world"]["kind"]] = by_world_kind.get(task["world"]["kind"], 0) + 1
|
|
256
|
+
by_difficulty[task["difficulty"]] = by_difficulty.get(task["difficulty"], 0) + 1
|
|
257
|
+
|
|
258
|
+
splits = raw.get("splits")
|
|
259
|
+
normalized_splits: dict[str, list[str]] = {}
|
|
260
|
+
if splits is not None:
|
|
261
|
+
if not isinstance(splits, Mapping):
|
|
262
|
+
raise TaskDatasetError("dataset.splits must be a mapping of name -> [id]")
|
|
263
|
+
for split_name, ids in splits.items():
|
|
264
|
+
id_list = [str(i) for i in (ids or [])]
|
|
265
|
+
missing = [i for i in id_list if i not in seen_ids]
|
|
266
|
+
if missing:
|
|
267
|
+
raise TaskDatasetError(
|
|
268
|
+
f"dataset.splits[{split_name!r}] references unknown ids {missing}"
|
|
269
|
+
)
|
|
270
|
+
normalized_splits[str(split_name)] = id_list
|
|
271
|
+
|
|
272
|
+
compiled: dict[str, Any] = {
|
|
273
|
+
"kind": AGENT_LEARNING_TASK_DATASET_KIND,
|
|
274
|
+
"name": name,
|
|
275
|
+
"tasks": compiled_tasks,
|
|
276
|
+
"coverage": {
|
|
277
|
+
"by_world_kind": dict(sorted(by_world_kind.items())),
|
|
278
|
+
"by_difficulty": dict(sorted(by_difficulty.items())),
|
|
279
|
+
"count": len(compiled_tasks),
|
|
280
|
+
},
|
|
281
|
+
}
|
|
282
|
+
if normalized_splits:
|
|
283
|
+
compiled["splits"] = normalized_splits
|
|
284
|
+
if raw.get("license") is not None:
|
|
285
|
+
compiled["license"] = str(raw["license"])
|
|
286
|
+
compiled["version"] = _content_hash(
|
|
287
|
+
{k: v for k, v in compiled.items() if k != "version"}
|
|
288
|
+
)
|
|
289
|
+
return compiled
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def task_world_kinds(dataset: Mapping[str, Any]) -> Sequence[str]:
|
|
293
|
+
"""Convenience: the sorted set of world kinds a (compiled) dataset spans."""
|
|
294
|
+
|
|
295
|
+
return sorted({task["world"]["kind"] for task in (dataset.get("tasks") or [])})
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def load_task_dataset(path: str | Path) -> dict[str, Any]:
|
|
299
|
+
"""Load + compile a TaskDataset from a JSON file (the shipped out-of-the-box
|
|
300
|
+
datasets live under ``examples/task_datasets/``)."""
|
|
301
|
+
|
|
302
|
+
payload = json.loads(Path(path).expanduser().read_text(encoding="utf-8"))
|
|
303
|
+
return compile_task_dataset(payload)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# ===========================================================================
|
|
307
|
+
# B2 — the benchmark runner (run an agent across a dataset, honest scoring).
|
|
308
|
+
# ===========================================================================
|
|
309
|
+
AGENT_LEARNING_BENCHMARK_RESULT_KIND = "agent-learning.benchmark-result.v1"
|
|
310
|
+
|
|
311
|
+
# the closed evidence-class set is OWNED by live/_contract (frozen 4-tuple); the
|
|
312
|
+
# runner never invents one. Non-live execution_classes may NEVER carry a live
|
|
313
|
+
# evidence_class — that is the honesty rule the gate's overclaim tripwire checks.
|
|
314
|
+
_NON_LIVE_EXECUTION_CLASSES = ("fixture", "typed_only")
|
|
315
|
+
_LIVE_EVIDENCE_CLASSES = ("live_lane", "live_stressed")
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _evidence_classes() -> tuple[str, ...]:
|
|
319
|
+
from .live._contract import EVIDENCE_CLASSES
|
|
320
|
+
|
|
321
|
+
return tuple(EVIDENCE_CLASSES)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _resolve_task_list(
|
|
325
|
+
dataset: Mapping[str, Any],
|
|
326
|
+
*,
|
|
327
|
+
split: str | None,
|
|
328
|
+
max_tasks: int | None,
|
|
329
|
+
) -> list[dict]:
|
|
330
|
+
tasks_all = list(dataset.get("tasks") or [])
|
|
331
|
+
if split is not None:
|
|
332
|
+
ids = set((dataset.get("splits") or {}).get(split) or [])
|
|
333
|
+
if not ids:
|
|
334
|
+
raise TaskDatasetError(f"benchmark split {split!r} is empty or undefined")
|
|
335
|
+
tasks_all = [t for t in tasks_all if t.get("id") in ids]
|
|
336
|
+
tasks_all = sorted(tasks_all, key=lambda t: str(t.get("id"))) # deterministic order
|
|
337
|
+
if max_tasks is not None:
|
|
338
|
+
tasks_all = tasks_all[: int(max_tasks)]
|
|
339
|
+
return tasks_all
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
# Canonical objective-anchored scoring lives in ``fi`` (the engine) so the
|
|
343
|
+
# optimizer integration can use it WITHOUT importing fi.alk (the
|
|
344
|
+
# vendored_engine_boundary). Re-exported here so B1/B2/B6 call sites are unchanged.
|
|
345
|
+
from fi.opt._objective_scoring import ( # noqa: E402,F401
|
|
346
|
+
METRIC_ALIASES,
|
|
347
|
+
has_declared_anchor_objective,
|
|
348
|
+
objective_score,
|
|
349
|
+
resolve_metric,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _score_from_result(
|
|
354
|
+
result: Mapping[str, Any],
|
|
355
|
+
*,
|
|
356
|
+
objective: Mapping[str, Any] | None = None,
|
|
357
|
+
threshold: float = 0.5,
|
|
358
|
+
) -> tuple[str, float, dict, dict]:
|
|
359
|
+
"""Extract (verdict, score, metric_averages, scoring) from a run result.
|
|
360
|
+
|
|
361
|
+
The headline ``score`` is the OBJECTIVE score (weighted mean over the task's
|
|
362
|
+
declared terms) when the objective resolves >=1 term — the signal with real
|
|
363
|
+
dynamic range. It falls back to the engine ``evaluation_score`` only when no
|
|
364
|
+
declared term maps to a metric. ``verdict`` is pass iff score >= threshold."""
|
|
365
|
+
|
|
366
|
+
summary = result.get("summary") or {}
|
|
367
|
+
metric_averages = dict(summary.get("metric_averages") or {})
|
|
368
|
+
raw_eval = float(summary.get("evaluation_score") or 0.0)
|
|
369
|
+
|
|
370
|
+
obj = objective_score(metric_averages, objective or {})
|
|
371
|
+
if obj["score"] is not None:
|
|
372
|
+
score = float(obj["score"])
|
|
373
|
+
basis = "objective"
|
|
374
|
+
else:
|
|
375
|
+
score = raw_eval
|
|
376
|
+
basis = "evaluation_score_fallback"
|
|
377
|
+
verdict = "pass" if score >= threshold else "fail"
|
|
378
|
+
scoring = {
|
|
379
|
+
"basis": basis,
|
|
380
|
+
"raw_evaluation_score": round(raw_eval, 6),
|
|
381
|
+
"threshold": threshold,
|
|
382
|
+
"terms_resolved": obj["terms_resolved"],
|
|
383
|
+
"terms_total": obj["terms_total"],
|
|
384
|
+
"per_term": obj["per_term"],
|
|
385
|
+
}
|
|
386
|
+
return verdict, round(score, 6), metric_averages, scoring
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def run_benchmark(
|
|
390
|
+
dataset: Mapping[str, Any],
|
|
391
|
+
agent: Mapping[str, Any],
|
|
392
|
+
*,
|
|
393
|
+
split: str | None = None,
|
|
394
|
+
max_tasks: int | None = None,
|
|
395
|
+
seed: int = 42,
|
|
396
|
+
evidence_class: str = "captured_fixture",
|
|
397
|
+
detect_reward_hacks: bool = False,
|
|
398
|
+
runner: Any = None,
|
|
399
|
+
emit_telemetry: bool = True,
|
|
400
|
+
project_name: str | None = None,
|
|
401
|
+
) -> dict[str, Any]:
|
|
402
|
+
"""Run one ``agent`` across a (compiled) TaskDataset and return a scored,
|
|
403
|
+
comparable result. Each per-task result is stamped with the task's HONEST
|
|
404
|
+
``execution_class`` and the run's ``evidence_class`` — and a non-live
|
|
405
|
+
execution_class carrying a live evidence_class is flagged ``overclaim=True``
|
|
406
|
+
(never silently downgraded; the gate asserts none in the fixture lane).
|
|
407
|
+
|
|
408
|
+
The actual run goes through the EXISTING engine (``simulate.build_task_run_
|
|
409
|
+
manifest`` -> ``simulate.run_manifest``), proven feasible on conversation/
|
|
410
|
+
tool_api by the execution spike. ``runner`` is an injectable seam (a
|
|
411
|
+
callable ``manifest -> result``) for deterministic testing without the
|
|
412
|
+
engine; when ``None`` the real engine is used.
|
|
413
|
+
"""
|
|
414
|
+
|
|
415
|
+
if evidence_class not in _evidence_classes():
|
|
416
|
+
raise TaskError(
|
|
417
|
+
f"evidence_class {evidence_class!r} not in {_evidence_classes()}"
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
task_list = _resolve_task_list(dataset, split=split, max_tasks=max_tasks)
|
|
421
|
+
if not task_list:
|
|
422
|
+
raise TaskDatasetError("benchmark resolved zero tasks to run")
|
|
423
|
+
|
|
424
|
+
per_task: list[dict[str, Any]] = []
|
|
425
|
+
for task in task_list:
|
|
426
|
+
verdict, score, metric_averages, scoring, tool_calls, run_error = _run_one_task(
|
|
427
|
+
task, agent, seed=seed, runner=runner
|
|
428
|
+
)
|
|
429
|
+
execution_class = str(task.get("execution_class") or "typed_only")
|
|
430
|
+
overclaim = (
|
|
431
|
+
execution_class in _NON_LIVE_EXECUTION_CLASSES
|
|
432
|
+
and evidence_class in _LIVE_EVIDENCE_CLASSES
|
|
433
|
+
)
|
|
434
|
+
row: dict[str, Any] = {
|
|
435
|
+
"task_id": str(task.get("id")),
|
|
436
|
+
"world_kind": str(task.get("world", {}).get("kind")),
|
|
437
|
+
"difficulty": str(task.get("difficulty")),
|
|
438
|
+
"verdict": verdict,
|
|
439
|
+
"score": score,
|
|
440
|
+
"execution_class": execution_class,
|
|
441
|
+
"evidence_class": evidence_class,
|
|
442
|
+
"overclaim": bool(overclaim),
|
|
443
|
+
"metric_averages": metric_averages,
|
|
444
|
+
"tool_calls": list(tool_calls or []),
|
|
445
|
+
"scoring": scoring,
|
|
446
|
+
}
|
|
447
|
+
# #3: auto-wire the reward-hack detector — a candidate that GAMES the
|
|
448
|
+
# scorer (e.g. claims completion with zero tool calls on a tool-anchored
|
|
449
|
+
# objective) is FAILED even if objective_score passed (the deterministic
|
|
450
|
+
# anti-gaming anchor objective_score alone misses, e.g. vacuous
|
|
451
|
+
# tool_selection_accuracy=1.0 for an agent that never called a tool).
|
|
452
|
+
if detect_reward_hacks and verdict == "pass":
|
|
453
|
+
from . import rewardhack as _rh
|
|
454
|
+
|
|
455
|
+
traj = {"metric_averages": metric_averages, "tool_calls": list(tool_calls or []),
|
|
456
|
+
"score": score}
|
|
457
|
+
verdict_obj = _rh.score_trajectory(traj, objective=task.get("objective") or {})
|
|
458
|
+
if verdict_obj["hacked"]:
|
|
459
|
+
row["verdict"] = "fail"
|
|
460
|
+
row["rewardhack"] = verdict_obj
|
|
461
|
+
row["score"] = 0.0
|
|
462
|
+
if run_error is not None:
|
|
463
|
+
row["error"] = run_error
|
|
464
|
+
per_task.append(row)
|
|
465
|
+
|
|
466
|
+
aggregate = _aggregate(per_task, evidence_class)
|
|
467
|
+
result = {
|
|
468
|
+
"kind": AGENT_LEARNING_BENCHMARK_RESULT_KIND,
|
|
469
|
+
"dataset_version": str(dataset.get("version") or ""),
|
|
470
|
+
"dataset_name": str(dataset.get("name") or ""),
|
|
471
|
+
"split": split,
|
|
472
|
+
"seed": int(seed),
|
|
473
|
+
"agent_kind": str(agent.get("type") or ""),
|
|
474
|
+
"per_task": per_task,
|
|
475
|
+
"aggregate": aggregate,
|
|
476
|
+
}
|
|
477
|
+
if emit_telemetry:
|
|
478
|
+
# W&B/promptfoo dashboard wiring (Phase 14): one run -> root + per-task
|
|
479
|
+
# spans + a dashboard URL when keyed (mode=auto), else a local log. A
|
|
480
|
+
# side-channel — never alters the returned scores.
|
|
481
|
+
from .telemetry import emit_run
|
|
482
|
+
|
|
483
|
+
summary = emit_run(
|
|
484
|
+
kind="benchmark",
|
|
485
|
+
name=result["dataset_name"] or "benchmark",
|
|
486
|
+
metrics={
|
|
487
|
+
"n_tasks": aggregate.get("count", 0),
|
|
488
|
+
"pass_rate": aggregate.get("pass_rate", 0.0),
|
|
489
|
+
"mean_score": aggregate.get("mean_score", 0.0),
|
|
490
|
+
},
|
|
491
|
+
verdict="pass" if aggregate.get("pass_rate", 0.0) >= 0.5 else "fail",
|
|
492
|
+
children=[
|
|
493
|
+
(
|
|
494
|
+
f"task:{r['task_id']}",
|
|
495
|
+
{
|
|
496
|
+
"verdict": r.get("verdict"),
|
|
497
|
+
"score": r.get("score"),
|
|
498
|
+
"world_kind": r.get("world_kind"),
|
|
499
|
+
},
|
|
500
|
+
)
|
|
501
|
+
for r in per_task
|
|
502
|
+
],
|
|
503
|
+
project_name=project_name,
|
|
504
|
+
)
|
|
505
|
+
result["telemetry"] = summary.as_dict()
|
|
506
|
+
return result
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _run_one_task(
|
|
510
|
+
task: Mapping[str, Any],
|
|
511
|
+
agent: Mapping[str, Any],
|
|
512
|
+
*,
|
|
513
|
+
seed: int,
|
|
514
|
+
runner: Any,
|
|
515
|
+
) -> tuple[str, float, dict, dict, str | None]:
|
|
516
|
+
scenario = dict(task.get("scenario") or {})
|
|
517
|
+
verification = dict(task.get("verification") or {})
|
|
518
|
+
threshold = float(verification.get("threshold", 0.7))
|
|
519
|
+
objective = dict(task.get("objective") or {})
|
|
520
|
+
# translate the task's verification + scenario row into a REAL evaluation
|
|
521
|
+
# config so the run is scored (not a hollow status='passed' with score 0.0).
|
|
522
|
+
row = (scenario.get("dataset") or [{}])[0] if scenario.get("dataset") else {}
|
|
523
|
+
task_description = str(row.get("situation") or task.get("title") or task.get("id"))
|
|
524
|
+
expected_result = str(row.get("outcome") or "")
|
|
525
|
+
success_criteria = [
|
|
526
|
+
str(c.get("value"))
|
|
527
|
+
for c in (verification.get("checks") or [])
|
|
528
|
+
if isinstance(c, Mapping) and c.get("value") is not None
|
|
529
|
+
]
|
|
530
|
+
try:
|
|
531
|
+
if runner is not None:
|
|
532
|
+
result = runner(task, agent)
|
|
533
|
+
else:
|
|
534
|
+
import asyncio
|
|
535
|
+
|
|
536
|
+
from . import simulate as _simulate
|
|
537
|
+
|
|
538
|
+
manifest = _simulate.build_task_run_manifest(
|
|
539
|
+
name=str(task.get("id")),
|
|
540
|
+
agent=dict(agent),
|
|
541
|
+
scenario=scenario,
|
|
542
|
+
task_description=task_description,
|
|
543
|
+
expected_result=expected_result or None,
|
|
544
|
+
success_criteria=success_criteria,
|
|
545
|
+
threshold=threshold,
|
|
546
|
+
environments=list(task.get("environments") or ()),
|
|
547
|
+
auto_execute_tools=True,
|
|
548
|
+
max_turns=int((task.get("world", {}).get("spec") or {}).get("max_turns", 1)),
|
|
549
|
+
simulation_engine=str(
|
|
550
|
+
(task.get("world", {}).get("spec") or {}).get("engine")
|
|
551
|
+
or "local_text"
|
|
552
|
+
),
|
|
553
|
+
)
|
|
554
|
+
# run_manifest is async; the benchmark runner is a synchronous
|
|
555
|
+
# top-level API, so drive the coroutine to completion here.
|
|
556
|
+
try:
|
|
557
|
+
asyncio.get_running_loop()
|
|
558
|
+
except RuntimeError:
|
|
559
|
+
result = asyncio.run(_simulate.run_manifest(manifest))
|
|
560
|
+
else: # pragma: no cover - benchmark is not called inside a loop
|
|
561
|
+
raise RuntimeError(
|
|
562
|
+
"run_benchmark cannot run inside an active event loop; "
|
|
563
|
+
"call it from synchronous code"
|
|
564
|
+
)
|
|
565
|
+
verdict, score, metric_averages, scoring = _score_from_result(
|
|
566
|
+
result, objective=objective, threshold=threshold
|
|
567
|
+
)
|
|
568
|
+
return verdict, score, metric_averages, scoring, _result_tool_calls(result), None
|
|
569
|
+
except Exception as exc: # noqa: BLE001 — a failed task scores void, never crashes the sweep
|
|
570
|
+
return "void", 0.0, {}, {}, [], f"{type(exc).__name__}: {str(exc)[:160]}"
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _result_tool_calls(result: Mapping[str, Any]) -> list:
|
|
574
|
+
"""Extract the agent's tool_calls from a run result (needed by the reward-hack
|
|
575
|
+
detector to measure effort). Shape-tolerant across the runner seam."""
|
|
576
|
+
if not isinstance(result, Mapping):
|
|
577
|
+
return []
|
|
578
|
+
direct = result.get("tool_calls")
|
|
579
|
+
if isinstance(direct, list):
|
|
580
|
+
return direct
|
|
581
|
+
results = (result.get("report") or {}).get("results") if isinstance(result.get("report"), Mapping) else None
|
|
582
|
+
if isinstance(results, list) and results and isinstance(results[0], Mapping):
|
|
583
|
+
return list(results[0].get("tool_calls") or [])
|
|
584
|
+
return []
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _aggregate(per_task: Sequence[Mapping[str, Any]], evidence_class: str) -> dict:
|
|
588
|
+
n = len(per_task)
|
|
589
|
+
passed = sum(1 for r in per_task if r["verdict"] == "pass")
|
|
590
|
+
mean_score = round(sum(float(r["score"]) for r in per_task) / n, 6) if n else 0.0
|
|
591
|
+
|
|
592
|
+
def _rollup(key: str) -> dict[str, dict]:
|
|
593
|
+
out: dict[str, dict] = {}
|
|
594
|
+
for r in per_task:
|
|
595
|
+
bucket = out.setdefault(str(r[key]), {"count": 0, "passed": 0, "score_sum": 0.0})
|
|
596
|
+
bucket["count"] += 1
|
|
597
|
+
bucket["passed"] += 1 if r["verdict"] == "pass" else 0
|
|
598
|
+
bucket["score_sum"] += float(r["score"])
|
|
599
|
+
for bucket in out.values():
|
|
600
|
+
bucket["mean_score"] = round(bucket.pop("score_sum") / bucket["count"], 6)
|
|
601
|
+
return dict(sorted(out.items()))
|
|
602
|
+
|
|
603
|
+
any_live = any(r["evidence_class"] in _LIVE_EVIDENCE_CLASSES for r in per_task)
|
|
604
|
+
any_overclaim = any(r.get("overclaim") for r in per_task)
|
|
605
|
+
return {
|
|
606
|
+
"count": n,
|
|
607
|
+
"passed": passed,
|
|
608
|
+
"pass_rate": round(passed / n, 6) if n else 0.0,
|
|
609
|
+
"mean_score": mean_score,
|
|
610
|
+
"by_world_kind": _rollup("world_kind"),
|
|
611
|
+
"by_difficulty": _rollup("difficulty"),
|
|
612
|
+
"by_execution_class": _rollup("execution_class"),
|
|
613
|
+
"honesty": {
|
|
614
|
+
"evidence_class": evidence_class,
|
|
615
|
+
"fixture_only": not any_live,
|
|
616
|
+
"any_live": any_live,
|
|
617
|
+
"any_overclaim": any_overclaim,
|
|
618
|
+
},
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
# ===========================================================================
|
|
623
|
+
# RSI loop — close the loop: dataset -> optimize -> verify on HELD-OUT.
|
|
624
|
+
# ===========================================================================
|
|
625
|
+
AGENT_LEARNING_RSI_REPORT_KIND = "agent-learning.rsi-report.v1"
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _apply_candidate(base_agent: Mapping[str, Any], assignment: Mapping[str, Any]) -> dict:
|
|
629
|
+
"""Materialize one agent config from the base + a search-space assignment.
|
|
630
|
+
Keys may be dotted ``agent.<field>`` (the kit's whole-agent convention) or a
|
|
631
|
+
bare field; both write onto the agent dict."""
|
|
632
|
+
|
|
633
|
+
agent = dict(base_agent)
|
|
634
|
+
for path, value in assignment.items():
|
|
635
|
+
field = str(path).split(".", 1)[1] if str(path).startswith("agent.") else str(path)
|
|
636
|
+
agent[field] = value
|
|
637
|
+
return agent
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _candidate_grid(search_space: Mapping[str, Sequence[Any]], *, cap: int) -> list[dict]:
|
|
641
|
+
"""Cartesian product of the (finite) search space, deterministically ordered,
|
|
642
|
+
capped at ``cap`` candidates (logged when truncated)."""
|
|
643
|
+
|
|
644
|
+
import itertools
|
|
645
|
+
|
|
646
|
+
keys = sorted(search_space)
|
|
647
|
+
value_lists = [list(search_space[k]) for k in keys]
|
|
648
|
+
grid = [dict(zip(keys, combo)) for combo in itertools.product(*value_lists)]
|
|
649
|
+
return grid[: max(1, int(cap))]
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def optimize_against_dataset(
|
|
653
|
+
dataset: Mapping[str, Any],
|
|
654
|
+
base_agent: Mapping[str, Any],
|
|
655
|
+
search_space: Mapping[str, Sequence[Any]],
|
|
656
|
+
*,
|
|
657
|
+
train_split: str = "train",
|
|
658
|
+
test_split: str = "test",
|
|
659
|
+
max_candidates: int = 16,
|
|
660
|
+
seed: int = 42,
|
|
661
|
+
evidence_class: str = "captured_fixture",
|
|
662
|
+
runner: Any = None,
|
|
663
|
+
emit_telemetry: bool = True,
|
|
664
|
+
project_name: str | None = None,
|
|
665
|
+
detect_reward_hacks: bool = False,
|
|
666
|
+
) -> dict[str, Any]:
|
|
667
|
+
"""Close the RSI loop: search agent configs, score each on the TRAIN split via
|
|
668
|
+
the (objective-anchored, discriminating) ``run_benchmark``, pick the winner,
|
|
669
|
+
then VERIFY it beats the baseline on the HELD-OUT TEST split.
|
|
670
|
+
|
|
671
|
+
The held-out check is the honest RSI guard (the advisor's bar): report lift on
|
|
672
|
+
tasks the optimizer never optimized against, never the metric it climbed. If
|
|
673
|
+
the dataset has no ``splits``, train==test is used and ``held_out`` is False
|
|
674
|
+
(a non-generalization-proving run, flagged as such — no overclaim)."""
|
|
675
|
+
|
|
676
|
+
splits = dataset.get("splits") or {}
|
|
677
|
+
has_split = bool(splits.get(train_split)) and bool(splits.get(test_split))
|
|
678
|
+
train = train_split if has_split else None
|
|
679
|
+
test = test_split if has_split else None
|
|
680
|
+
|
|
681
|
+
def _score(agent: Mapping[str, Any], split: str | None) -> dict:
|
|
682
|
+
# emit_telemetry=False: the optimizer emits ONE run for the whole search
|
|
683
|
+
# (below), not one per candidate benchmark (Phase 14).
|
|
684
|
+
# detect_reward_hacks: when on, a candidate that GAMES a declared anchor
|
|
685
|
+
# (e.g. claims completion with zero tool calls on a tool-anchored
|
|
686
|
+
# objective) is FAILED — an optimizer that climbs a gameable metric is the
|
|
687
|
+
# textbook reward-hacking failure, so honest config search opts in.
|
|
688
|
+
return run_benchmark(
|
|
689
|
+
dataset, agent, split=split, seed=seed,
|
|
690
|
+
evidence_class=evidence_class, runner=runner, emit_telemetry=False,
|
|
691
|
+
detect_reward_hacks=detect_reward_hacks,
|
|
692
|
+
)["aggregate"]
|
|
693
|
+
|
|
694
|
+
candidates = _candidate_grid(search_space, cap=max_candidates)
|
|
695
|
+
truncated = len(_candidate_grid(search_space, cap=10**9)) > len(candidates)
|
|
696
|
+
|
|
697
|
+
leaderboard: list[dict[str, Any]] = []
|
|
698
|
+
for idx, assignment in enumerate(candidates):
|
|
699
|
+
agent = _apply_candidate(base_agent, assignment)
|
|
700
|
+
train_agg = _score(agent, train)
|
|
701
|
+
leaderboard.append({
|
|
702
|
+
"candidate_index": idx,
|
|
703
|
+
"assignment": assignment,
|
|
704
|
+
"train_mean_score": train_agg["mean_score"],
|
|
705
|
+
"train_pass_rate": train_agg["pass_rate"],
|
|
706
|
+
})
|
|
707
|
+
leaderboard.sort(key=lambda r: (-r["train_mean_score"], r["candidate_index"]))
|
|
708
|
+
winner = leaderboard[0]
|
|
709
|
+
winner_agent = _apply_candidate(base_agent, winner["assignment"])
|
|
710
|
+
|
|
711
|
+
# held-out verification: winner vs baseline (the base_agent) on TEST
|
|
712
|
+
baseline_test = _score(base_agent, test)
|
|
713
|
+
winner_test = _score(winner_agent, test)
|
|
714
|
+
lift = round(winner_test["mean_score"] - baseline_test["mean_score"], 6)
|
|
715
|
+
|
|
716
|
+
result = {
|
|
717
|
+
"kind": AGENT_LEARNING_RSI_REPORT_KIND,
|
|
718
|
+
"dataset_version": str(dataset.get("version") or ""),
|
|
719
|
+
"candidates_evaluated": len(candidates),
|
|
720
|
+
"candidates_truncated": truncated,
|
|
721
|
+
"leaderboard": leaderboard,
|
|
722
|
+
"winner": {"assignment": winner["assignment"],
|
|
723
|
+
"train_mean_score": winner["train_mean_score"]},
|
|
724
|
+
"held_out": {
|
|
725
|
+
"verified": bool(has_split), # a real held-out split existed
|
|
726
|
+
"test_split": test_split if has_split else "(train==test; not held-out)",
|
|
727
|
+
"baseline_mean_score": baseline_test["mean_score"],
|
|
728
|
+
"winner_mean_score": winner_test["mean_score"],
|
|
729
|
+
"lift": lift,
|
|
730
|
+
"improved": lift > 0,
|
|
731
|
+
},
|
|
732
|
+
}
|
|
733
|
+
if emit_telemetry:
|
|
734
|
+
# ONE dashboard run for the whole search: root + per-candidate spans (P14).
|
|
735
|
+
from .telemetry import emit_run
|
|
736
|
+
|
|
737
|
+
summary = emit_run(
|
|
738
|
+
kind="optimize",
|
|
739
|
+
name=str(dataset.get("name") or "optimize"),
|
|
740
|
+
metrics={
|
|
741
|
+
"candidates": len(candidates),
|
|
742
|
+
"winner_train_score": winner["train_mean_score"],
|
|
743
|
+
"held_out_lift": lift,
|
|
744
|
+
"improved": lift > 0,
|
|
745
|
+
},
|
|
746
|
+
verdict="pass" if lift > 0 else "fail",
|
|
747
|
+
children=[
|
|
748
|
+
(
|
|
749
|
+
f"candidate:{row['candidate_index']}",
|
|
750
|
+
{"train_mean_score": row["train_mean_score"],
|
|
751
|
+
"train_pass_rate": row["train_pass_rate"]},
|
|
752
|
+
)
|
|
753
|
+
for row in leaderboard
|
|
754
|
+
],
|
|
755
|
+
project_name=project_name,
|
|
756
|
+
)
|
|
757
|
+
result["telemetry"] = summary.as_dict()
|
|
758
|
+
return result
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
# ===========================================================================
|
|
762
|
+
# #2 — real cell_scorer for the 13D practice loop (replaces the all-pass no-op).
|
|
763
|
+
# ===========================================================================
|
|
764
|
+
def make_cell_scorer(
|
|
765
|
+
*,
|
|
766
|
+
agent: Mapping[str, Any],
|
|
767
|
+
objective: Mapping[str, Any],
|
|
768
|
+
scenario: Mapping[str, Any] | None = None,
|
|
769
|
+
environments: Sequence[Mapping[str, Any]] = (),
|
|
770
|
+
threshold: float = 0.5,
|
|
771
|
+
evidence_class: str = "captured_fixture",
|
|
772
|
+
runner: Any = None,
|
|
773
|
+
) -> Any:
|
|
774
|
+
"""Build a REAL ``cell_scorer`` for ``practice.run_practice_loop`` that runs the
|
|
775
|
+
agent per cell and scores via ``objective_score`` (engine ``metric_averages`` →
|
|
776
|
+
the ``{scalar, verdict, evidence_class}`` shape the loop consumes).
|
|
777
|
+
|
|
778
|
+
Replaces the loop's all-pass no-op default (``scalar 1.0, verdict pass``) so the
|
|
779
|
+
13D practice loop — assess/diagnose/spaced-regression-replay/consolidate —
|
|
780
|
+
measures REAL fitness instead of accepting everything (audit gap #2). Joins
|
|
781
|
+
diagnose-from-trace + no-forgetting (13D) with the discriminating objective
|
|
782
|
+
score. ``runner`` is an injectable seam for deterministic tests."""
|
|
783
|
+
|
|
784
|
+
def cell_scorer(cell: Mapping[str, Any]) -> dict[str, Any]:
|
|
785
|
+
sc = dict(scenario or {})
|
|
786
|
+
# the cell's persona overrides the scenario's row when present, so distinct
|
|
787
|
+
# obligation cells exercise distinct conditions (not one fixed run).
|
|
788
|
+
if cell.get("persona") and sc.get("dataset"):
|
|
789
|
+
row0 = dict((sc["dataset"] or [{}])[0])
|
|
790
|
+
row0["persona"] = {"name": str(cell["persona"])[:24]}
|
|
791
|
+
sc = {**sc, "dataset": [row0]}
|
|
792
|
+
if runner is not None:
|
|
793
|
+
result = runner(cell, agent)
|
|
794
|
+
else:
|
|
795
|
+
import asyncio
|
|
796
|
+
|
|
797
|
+
from . import simulate as _sim
|
|
798
|
+
|
|
799
|
+
row = (sc.get("dataset") or [{}])[0] if sc.get("dataset") else {}
|
|
800
|
+
manifest = _sim.build_task_run_manifest(
|
|
801
|
+
name=str(cell.get("intent") or "cell"),
|
|
802
|
+
agent=dict(agent),
|
|
803
|
+
scenario=sc or None,
|
|
804
|
+
task_description=str(row.get("situation") or cell.get("intent") or "task"),
|
|
805
|
+
expected_result=str(row.get("outcome") or "") or None,
|
|
806
|
+
environments=list(environments),
|
|
807
|
+
auto_execute_tools=True,
|
|
808
|
+
threshold=threshold,
|
|
809
|
+
)
|
|
810
|
+
try:
|
|
811
|
+
asyncio.get_running_loop()
|
|
812
|
+
except RuntimeError:
|
|
813
|
+
result = asyncio.run(_sim.run_manifest(manifest))
|
|
814
|
+
else: # pragma: no cover
|
|
815
|
+
raise RuntimeError("make_cell_scorer cannot run inside an event loop")
|
|
816
|
+
|
|
817
|
+
metrics = (result.get("summary") or {}).get("metric_averages") or {}
|
|
818
|
+
s = objective_score(metrics, objective).get("score")
|
|
819
|
+
scalar = float(s) if s is not None else 0.0
|
|
820
|
+
return {
|
|
821
|
+
"eval": "agent_report",
|
|
822
|
+
"scalar": round(scalar, 6),
|
|
823
|
+
"verdict": "pass" if scalar >= threshold else "fail",
|
|
824
|
+
"evidence_class": evidence_class,
|
|
825
|
+
"metric_averages": metrics,
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
return cell_scorer
|