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/simulate/suite.py
ADDED
|
@@ -0,0 +1,1288 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import copy
|
|
5
|
+
import importlib
|
|
6
|
+
import importlib.util
|
|
7
|
+
import inspect
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
import time
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence
|
|
14
|
+
|
|
15
|
+
from .manifest import CLI_SCHEMA_VERSION, ManifestError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
EVAL_SUITE_SCHEMA_VERSION = "agent-simulate.eval.v1"
|
|
19
|
+
AGENT_LEARNING_EVAL_SUITE_SCHEMA_VERSION = "agent-learning.eval.v1"
|
|
20
|
+
EVAL_SUITE_OPTIMIZATION_SCHEMA_VERSION = "agent-learning.eval-optimization.v1"
|
|
21
|
+
|
|
22
|
+
_JSON_PATH_EQUALS_ASSERTIONS = {
|
|
23
|
+
"json_path_equals",
|
|
24
|
+
"json_path_equal",
|
|
25
|
+
"json_equals",
|
|
26
|
+
"path_equals",
|
|
27
|
+
"path_equal",
|
|
28
|
+
}
|
|
29
|
+
_JSON_PATH_EXISTS_ASSERTIONS = {
|
|
30
|
+
"json_path_exists",
|
|
31
|
+
"json_exists",
|
|
32
|
+
"path_exists",
|
|
33
|
+
}
|
|
34
|
+
_JSON_PATH_GTE_ASSERTIONS = {
|
|
35
|
+
"json_path_gte",
|
|
36
|
+
"json_path_ge",
|
|
37
|
+
"json_gte",
|
|
38
|
+
"path_gte",
|
|
39
|
+
"greater_than_or_equal",
|
|
40
|
+
}
|
|
41
|
+
_JSON_PATH_LTE_ASSERTIONS = {
|
|
42
|
+
"json_path_lte",
|
|
43
|
+
"json_path_le",
|
|
44
|
+
"json_lte",
|
|
45
|
+
"path_lte",
|
|
46
|
+
"less_than_or_equal",
|
|
47
|
+
}
|
|
48
|
+
_JSON_PATH_CONTAINS_ASSERTIONS = {
|
|
49
|
+
"json_path_contains",
|
|
50
|
+
"json_contains",
|
|
51
|
+
"path_contains",
|
|
52
|
+
}
|
|
53
|
+
_JSON_PATH_NOT_CONTAINS_ASSERTIONS = {
|
|
54
|
+
"json_path_not_contains",
|
|
55
|
+
"json_not_contains",
|
|
56
|
+
"path_not_contains",
|
|
57
|
+
}
|
|
58
|
+
_JSON_PATH_ASSERTIONS = (
|
|
59
|
+
_JSON_PATH_EQUALS_ASSERTIONS
|
|
60
|
+
| _JSON_PATH_EXISTS_ASSERTIONS
|
|
61
|
+
| _JSON_PATH_GTE_ASSERTIONS
|
|
62
|
+
| _JSON_PATH_LTE_ASSERTIONS
|
|
63
|
+
| _JSON_PATH_CONTAINS_ASSERTIONS
|
|
64
|
+
| _JSON_PATH_NOT_CONTAINS_ASSERTIONS
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Assertion tokens that score the case output with a hosted FutureAGI eval
|
|
68
|
+
# template via ``fi.evals.evaluate`` (platform/turing engine by default,
|
|
69
|
+
# using FI_API_KEY / FI_SECRET_KEY / FI_BASE_URL). Pass = score >= threshold.
|
|
70
|
+
_FI_EVAL_ASSERTIONS = {
|
|
71
|
+
"fi_eval",
|
|
72
|
+
"fi_evals",
|
|
73
|
+
"platform_eval",
|
|
74
|
+
"platform",
|
|
75
|
+
"turing",
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# Extra eval-input keys an ``fi_eval`` assertion may template from the case
|
|
79
|
+
# vars before forwarding to the platform (``output`` is always the case output).
|
|
80
|
+
_FI_EVAL_INPUT_KEYS = (
|
|
81
|
+
"input",
|
|
82
|
+
"context",
|
|
83
|
+
"expected",
|
|
84
|
+
"query",
|
|
85
|
+
"reference",
|
|
86
|
+
"instructions",
|
|
87
|
+
"prompt",
|
|
88
|
+
"criteria",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Generative (LLM prompt-rewriting) optimizer tokens. These run the real
|
|
92
|
+
# optimizers in ``fi.opt.optimizers`` through the generative eval-suite bridge
|
|
93
|
+
# rather than the deterministic agent/target search backends.
|
|
94
|
+
_GENERATIVE_OPTIMIZER_TOKENS = {
|
|
95
|
+
"gepa": "gepa",
|
|
96
|
+
"protegi": "protegi",
|
|
97
|
+
"pro_te_gi": "protegi",
|
|
98
|
+
"metaprompt": "metaprompt",
|
|
99
|
+
"meta_prompt": "metaprompt",
|
|
100
|
+
"promptwizard": "promptwizard",
|
|
101
|
+
"prompt_wizard": "promptwizard",
|
|
102
|
+
"random_search": "random_search",
|
|
103
|
+
"random": "random_search",
|
|
104
|
+
"bayesian_search": "bayesian_search",
|
|
105
|
+
"bayesian": "bayesian_search",
|
|
106
|
+
"bayes": "bayesian_search",
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _generative_optimizer_token(optimizer_cfg: Any) -> Optional[str]:
|
|
111
|
+
"""Return the canonical generative optimizer token, or None for Family A."""
|
|
112
|
+
if not isinstance(optimizer_cfg, Mapping):
|
|
113
|
+
return None
|
|
114
|
+
raw = (
|
|
115
|
+
optimizer_cfg.get("algorithm")
|
|
116
|
+
or optimizer_cfg.get("type")
|
|
117
|
+
or optimizer_cfg.get("name")
|
|
118
|
+
or optimizer_cfg.get("strategy")
|
|
119
|
+
)
|
|
120
|
+
if not raw:
|
|
121
|
+
return None
|
|
122
|
+
norm = str(raw).strip().lower().replace("-", "_").replace(" ", "_")
|
|
123
|
+
return _GENERATIVE_OPTIMIZER_TOKENS.get(norm)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(frozen=True)
|
|
127
|
+
class EvalSuiteOptions:
|
|
128
|
+
name: Optional[str] = None
|
|
129
|
+
threshold: Optional[float] = None
|
|
130
|
+
dry_run: bool = False
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass(frozen=True)
|
|
134
|
+
class EvalSuiteOptimizationOptions:
|
|
135
|
+
name: Optional[str] = None
|
|
136
|
+
threshold: Optional[float] = None
|
|
137
|
+
max_candidates: Optional[int] = None
|
|
138
|
+
dry_run: bool = False
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def build_eval_suite_manifest(
|
|
142
|
+
*,
|
|
143
|
+
name: str,
|
|
144
|
+
providers: Optional[Sequence[Mapping[str, Any]]] = None,
|
|
145
|
+
prompts: Optional[Sequence[Mapping[str, Any]]] = None,
|
|
146
|
+
tests: Optional[Sequence[Mapping[str, Any]]] = None,
|
|
147
|
+
threshold: float = 1.0,
|
|
148
|
+
outputs: Optional[Mapping[str, Any]] = None,
|
|
149
|
+
metadata: Optional[Mapping[str, Any]] = None,
|
|
150
|
+
version: str = AGENT_LEARNING_EVAL_SUITE_SCHEMA_VERSION,
|
|
151
|
+
) -> Dict[str, Any]:
|
|
152
|
+
"""Build a promptfoo-style eval suite manifest from SDK data."""
|
|
153
|
+
|
|
154
|
+
if not name:
|
|
155
|
+
raise ValueError("name is required")
|
|
156
|
+
provider_values = _copy_mapping_sequence(
|
|
157
|
+
providers
|
|
158
|
+
if providers is not None
|
|
159
|
+
else (
|
|
160
|
+
{
|
|
161
|
+
"id": "echo",
|
|
162
|
+
"type": "echo",
|
|
163
|
+
},
|
|
164
|
+
),
|
|
165
|
+
field="providers",
|
|
166
|
+
)
|
|
167
|
+
prompt_values = _copy_mapping_sequence(
|
|
168
|
+
prompts
|
|
169
|
+
if prompts is not None
|
|
170
|
+
else (
|
|
171
|
+
{
|
|
172
|
+
"id": "support-policy-question",
|
|
173
|
+
"template": "{{question}}",
|
|
174
|
+
},
|
|
175
|
+
),
|
|
176
|
+
field="prompts",
|
|
177
|
+
)
|
|
178
|
+
test_values = _copy_mapping_sequence(
|
|
179
|
+
tests
|
|
180
|
+
if tests is not None
|
|
181
|
+
else (
|
|
182
|
+
{
|
|
183
|
+
"id": "policy-grounding",
|
|
184
|
+
"vars": {"question": "Where is the refund policy?"},
|
|
185
|
+
"assert": [{"type": "contains", "value": "policy"}],
|
|
186
|
+
},
|
|
187
|
+
),
|
|
188
|
+
field="tests",
|
|
189
|
+
)
|
|
190
|
+
manifest: Dict[str, Any] = {
|
|
191
|
+
"version": str(version),
|
|
192
|
+
"name": str(name),
|
|
193
|
+
"threshold": float(threshold),
|
|
194
|
+
"providers": provider_values,
|
|
195
|
+
"prompts": prompt_values,
|
|
196
|
+
"tests": test_values,
|
|
197
|
+
}
|
|
198
|
+
if outputs:
|
|
199
|
+
manifest["outputs"] = copy.deepcopy(dict(outputs))
|
|
200
|
+
if metadata:
|
|
201
|
+
manifest["metadata"] = copy.deepcopy(dict(metadata))
|
|
202
|
+
return manifest
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def write_eval_suite_file(suite: Mapping[str, Any], path: str | Path) -> Path:
|
|
206
|
+
"""Write an eval suite manifest as formatted JSON and return the path."""
|
|
207
|
+
|
|
208
|
+
suite_path = Path(path).expanduser().resolve()
|
|
209
|
+
suite_path.parent.mkdir(parents=True, exist_ok=True)
|
|
210
|
+
suite_path.write_text(
|
|
211
|
+
json.dumps(dict(suite), indent=2, sort_keys=True, default=str) + "\n",
|
|
212
|
+
encoding="utf-8",
|
|
213
|
+
)
|
|
214
|
+
return suite_path
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def load_eval_suite_file(path: str | Path) -> Dict[str, Any]:
|
|
218
|
+
suite_path = Path(path).expanduser().resolve()
|
|
219
|
+
suite = _load_json_or_yaml(suite_path)
|
|
220
|
+
if not isinstance(suite, Mapping):
|
|
221
|
+
raise ManifestError("eval suite root must be an object")
|
|
222
|
+
return _prepare_eval_suite(dict(suite), base_dir=suite_path.parent)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def run_eval_suite_file(
|
|
226
|
+
path: str | Path,
|
|
227
|
+
*,
|
|
228
|
+
options: Optional[EvalSuiteOptions] = None,
|
|
229
|
+
name: Optional[str] = None,
|
|
230
|
+
threshold: Optional[float] = None,
|
|
231
|
+
dry_run: Optional[bool] = None,
|
|
232
|
+
) -> Dict[str, Any]:
|
|
233
|
+
suite_path = Path(path).expanduser().resolve()
|
|
234
|
+
suite = load_eval_suite_file(suite_path)
|
|
235
|
+
return run_eval_suite(
|
|
236
|
+
suite,
|
|
237
|
+
suite_path=suite_path,
|
|
238
|
+
options=_merge_eval_suite_options(
|
|
239
|
+
options,
|
|
240
|
+
name=name,
|
|
241
|
+
threshold=threshold,
|
|
242
|
+
dry_run=dry_run,
|
|
243
|
+
),
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def optimize_eval_suite_file(
|
|
248
|
+
path: str | Path,
|
|
249
|
+
*,
|
|
250
|
+
options: Optional[EvalSuiteOptimizationOptions] = None,
|
|
251
|
+
name: Optional[str] = None,
|
|
252
|
+
threshold: Optional[float] = None,
|
|
253
|
+
max_candidates: Optional[int] = None,
|
|
254
|
+
dry_run: Optional[bool] = None,
|
|
255
|
+
) -> Dict[str, Any]:
|
|
256
|
+
"""Load and optimize a promptfoo-style eval suite with Agent Learning."""
|
|
257
|
+
|
|
258
|
+
suite_path = Path(path).expanduser().resolve()
|
|
259
|
+
suite = load_eval_suite_file(suite_path)
|
|
260
|
+
return optimize_eval_suite(
|
|
261
|
+
suite,
|
|
262
|
+
suite_path=suite_path,
|
|
263
|
+
options=_merge_eval_suite_optimization_options(
|
|
264
|
+
options,
|
|
265
|
+
name=name,
|
|
266
|
+
threshold=threshold,
|
|
267
|
+
max_candidates=max_candidates,
|
|
268
|
+
dry_run=dry_run,
|
|
269
|
+
),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def optimize_eval_suite(
|
|
274
|
+
suite: Mapping[str, Any],
|
|
275
|
+
*,
|
|
276
|
+
suite_path: str | Path = ".",
|
|
277
|
+
options: Optional[EvalSuiteOptimizationOptions] = None,
|
|
278
|
+
name: Optional[str] = None,
|
|
279
|
+
threshold: Optional[float] = None,
|
|
280
|
+
max_candidates: Optional[int] = None,
|
|
281
|
+
dry_run: Optional[bool] = None,
|
|
282
|
+
) -> Dict[str, Any]:
|
|
283
|
+
"""Optimize an in-memory eval suite and return a unified artifact payload."""
|
|
284
|
+
|
|
285
|
+
started = time.time()
|
|
286
|
+
opts = _merge_eval_suite_optimization_options(
|
|
287
|
+
options,
|
|
288
|
+
name=name,
|
|
289
|
+
threshold=threshold,
|
|
290
|
+
max_candidates=max_candidates,
|
|
291
|
+
dry_run=dry_run,
|
|
292
|
+
)
|
|
293
|
+
suite_path = _suite_file_like_path(suite_path)
|
|
294
|
+
runtime_suite = copy.deepcopy(dict(suite))
|
|
295
|
+
if opts.name:
|
|
296
|
+
runtime_suite["name"] = opts.name
|
|
297
|
+
if opts.threshold is not None:
|
|
298
|
+
runtime_suite.setdefault("optimization", {})["threshold"] = opts.threshold
|
|
299
|
+
if opts.max_candidates is not None:
|
|
300
|
+
runtime_suite.setdefault("optimization", {}).setdefault(
|
|
301
|
+
"optimizer", {}
|
|
302
|
+
)["max_candidates"] = opts.max_candidates
|
|
303
|
+
|
|
304
|
+
prepared = _prepare_eval_suite(runtime_suite, base_dir=suite_path.parent)
|
|
305
|
+
cli = _cli()
|
|
306
|
+
optimization = cli._optimization_config(prepared)
|
|
307
|
+
optimizer_config = cli._optimizer_config(optimization)
|
|
308
|
+
|
|
309
|
+
# Generative (LLM prompt-rewriting) optimizers run through their own bridge
|
|
310
|
+
# and don't need a deterministic search-space target, so route them before
|
|
311
|
+
# `_target_config` (which requires one).
|
|
312
|
+
generative_token = _generative_optimizer_token(optimization.get("optimizer"))
|
|
313
|
+
if generative_token and not opts.dry_run:
|
|
314
|
+
try:
|
|
315
|
+
from fi.opt.integrations.generative_suite import (
|
|
316
|
+
optimize_eval_suite_generative,
|
|
317
|
+
)
|
|
318
|
+
except Exception as exc: # pragma: no cover - optional dependency clarity
|
|
319
|
+
raise ManifestError(
|
|
320
|
+
"Agent Learning Kit generative optimizer engine is required for "
|
|
321
|
+
f"the `{generative_token}` optimizer."
|
|
322
|
+
) from exc
|
|
323
|
+
payload = optimize_eval_suite_generative(
|
|
324
|
+
prepared,
|
|
325
|
+
suite_path=suite_path,
|
|
326
|
+
name=str(prepared.get("name") or suite_path.stem),
|
|
327
|
+
token=generative_token,
|
|
328
|
+
optimizer_config=dict(optimizer_config or {}),
|
|
329
|
+
threshold=float(optimization.get("threshold", 0.5)),
|
|
330
|
+
started=started,
|
|
331
|
+
)
|
|
332
|
+
payload["eval_suite"] = _eval_suite_descriptor(prepared)
|
|
333
|
+
payload.setdefault("summary", {})
|
|
334
|
+
payload["summary"]["provider_count"] = len(_as_list(prepared.get("providers")))
|
|
335
|
+
payload["summary"]["prompt_count"] = len(_as_list(prepared.get("prompts")))
|
|
336
|
+
payload["summary"]["test_count"] = len(_as_list(prepared.get("tests")))
|
|
337
|
+
return payload
|
|
338
|
+
|
|
339
|
+
target_config = cli._target_config(optimization)
|
|
340
|
+
if opts.dry_run:
|
|
341
|
+
return {
|
|
342
|
+
"schema_version": CLI_SCHEMA_VERSION,
|
|
343
|
+
"kind": EVAL_SUITE_OPTIMIZATION_SCHEMA_VERSION,
|
|
344
|
+
"name": str(prepared.get("name") or suite_path.stem),
|
|
345
|
+
"status": "passed",
|
|
346
|
+
"exit_code": 0,
|
|
347
|
+
"dry_run": True,
|
|
348
|
+
"summary": {
|
|
349
|
+
"provider_count": len(_as_list(prepared.get("providers"))),
|
|
350
|
+
"prompt_count": len(_as_list(prepared.get("prompts"))),
|
|
351
|
+
"test_count": len(_as_list(prepared.get("tests"))),
|
|
352
|
+
"search_path_count": len(target_config.get("search_space", {})),
|
|
353
|
+
"max_candidates": optimizer_config.get("max_candidates"),
|
|
354
|
+
},
|
|
355
|
+
"eval_suite": _eval_suite_descriptor(prepared),
|
|
356
|
+
"duration_seconds": round(time.time() - started, 4),
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
try:
|
|
360
|
+
from fi.opt import problem_from_eval_suite
|
|
361
|
+
except Exception as exc: # pragma: no cover - optional dependency clarity
|
|
362
|
+
raise ManifestError(
|
|
363
|
+
"Agent Learning Kit optimizer engine is required for eval-suite optimization."
|
|
364
|
+
) from exc
|
|
365
|
+
|
|
366
|
+
problem = problem_from_eval_suite(
|
|
367
|
+
prepared,
|
|
368
|
+
suite_path=suite_path,
|
|
369
|
+
name=str(prepared.get("name") or suite_path.stem),
|
|
370
|
+
)
|
|
371
|
+
optimization_result = problem.optimize()
|
|
372
|
+
payload = cli._optimization_result(
|
|
373
|
+
manifest=prepared,
|
|
374
|
+
manifest_path=suite_path,
|
|
375
|
+
optimization_result=optimization_result,
|
|
376
|
+
threshold=float(optimization.get("threshold", 1.0)),
|
|
377
|
+
duration_seconds=round(time.time() - started, 4),
|
|
378
|
+
)
|
|
379
|
+
payload["kind"] = EVAL_SUITE_OPTIMIZATION_SCHEMA_VERSION
|
|
380
|
+
payload["eval_suite"] = _eval_suite_descriptor(prepared)
|
|
381
|
+
payload["summary"]["provider_count"] = len(_as_list(prepared.get("providers")))
|
|
382
|
+
payload["summary"]["prompt_count"] = len(_as_list(prepared.get("prompts")))
|
|
383
|
+
payload["summary"]["test_count"] = len(_as_list(prepared.get("tests")))
|
|
384
|
+
payload["optimization"]["source"] = "eval_suite"
|
|
385
|
+
if "manifest_optimization" in payload["optimization"]:
|
|
386
|
+
artifact = copy.deepcopy(payload["optimization"]["manifest_optimization"])
|
|
387
|
+
artifact["kind"] = "eval_suite_optimization"
|
|
388
|
+
artifact["source"] = "eval_suite"
|
|
389
|
+
payload["optimization"]["eval_suite_optimization"] = artifact
|
|
390
|
+
return payload
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def run_eval_suite(
|
|
394
|
+
suite: Mapping[str, Any],
|
|
395
|
+
*,
|
|
396
|
+
suite_path: str | Path = ".",
|
|
397
|
+
options: Optional[EvalSuiteOptions] = None,
|
|
398
|
+
) -> Dict[str, Any]:
|
|
399
|
+
started = time.time()
|
|
400
|
+
opts = options or EvalSuiteOptions()
|
|
401
|
+
base_dir = Path(suite_path).expanduser().resolve().parent
|
|
402
|
+
prepared = _prepare_eval_suite(dict(suite), base_dir=base_dir)
|
|
403
|
+
name = str(opts.name or prepared.get("name") or "agent-learning-eval")
|
|
404
|
+
threshold = float(opts.threshold if opts.threshold is not None else prepared.get("threshold", 1.0))
|
|
405
|
+
if opts.dry_run:
|
|
406
|
+
return _suite_result(
|
|
407
|
+
name=name,
|
|
408
|
+
suite=prepared,
|
|
409
|
+
cases=[],
|
|
410
|
+
threshold=threshold,
|
|
411
|
+
duration_seconds=round(time.time() - started, 4),
|
|
412
|
+
dry_run=True,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
cases: List[Dict[str, Any]] = []
|
|
416
|
+
providers = [_as_dict(provider) for provider in _as_list(prepared.get("providers"))]
|
|
417
|
+
prompts = [_as_dict(prompt) for prompt in _as_list(prepared.get("prompts"))]
|
|
418
|
+
tests = [_as_dict(test) for test in _as_list(prepared.get("tests"))]
|
|
419
|
+
for provider in providers:
|
|
420
|
+
for prompt in prompts:
|
|
421
|
+
for test_index, test in enumerate(tests, start=1):
|
|
422
|
+
cases.append(
|
|
423
|
+
_run_eval_case(
|
|
424
|
+
provider=provider,
|
|
425
|
+
prompt=prompt,
|
|
426
|
+
test=test,
|
|
427
|
+
test_index=test_index,
|
|
428
|
+
base_dir=base_dir,
|
|
429
|
+
)
|
|
430
|
+
)
|
|
431
|
+
return _suite_result(
|
|
432
|
+
name=name,
|
|
433
|
+
suite=prepared,
|
|
434
|
+
cases=cases,
|
|
435
|
+
threshold=threshold,
|
|
436
|
+
duration_seconds=round(time.time() - started, 4),
|
|
437
|
+
dry_run=False,
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _prepare_eval_suite(suite: Dict[str, Any], *, base_dir: Path) -> Dict[str, Any]:
|
|
442
|
+
providers = [_as_dict(item) for item in _as_list(suite.get("providers") or suite.get("provider"))]
|
|
443
|
+
prompts = [_as_dict(item) for item in _as_list(suite.get("prompts") or suite.get("prompt"))]
|
|
444
|
+
tests = _suite_tests(suite, base_dir=base_dir)
|
|
445
|
+
if not providers:
|
|
446
|
+
raise ManifestError("eval suite requires at least one provider")
|
|
447
|
+
if not prompts:
|
|
448
|
+
raise ManifestError("eval suite requires at least one prompt")
|
|
449
|
+
if not tests:
|
|
450
|
+
raise ManifestError("eval suite requires at least one test")
|
|
451
|
+
suite["providers"] = [_normalize_provider(item, index) for index, item in enumerate(providers, start=1)]
|
|
452
|
+
suite["prompts"] = [_normalize_prompt(item, index) for index, item in enumerate(prompts, start=1)]
|
|
453
|
+
suite["tests"] = [_normalize_test(item, index) for index, item in enumerate(tests, start=1)]
|
|
454
|
+
suite.pop("tests_file", None)
|
|
455
|
+
suite.pop("data_file", None)
|
|
456
|
+
suite.pop("data", None)
|
|
457
|
+
suite.setdefault("version", EVAL_SUITE_SCHEMA_VERSION)
|
|
458
|
+
return suite
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _suite_tests(suite: Mapping[str, Any], *, base_dir: Path) -> List[Dict[str, Any]]:
|
|
462
|
+
tests_value = suite.get("tests")
|
|
463
|
+
tests_file = suite.get("tests_file") or suite.get("data") or suite.get("data_file")
|
|
464
|
+
records: List[Dict[str, Any]] = []
|
|
465
|
+
if isinstance(tests_value, str):
|
|
466
|
+
records.extend(_load_test_records(base_dir / tests_value))
|
|
467
|
+
else:
|
|
468
|
+
records.extend(_as_dict(item) for item in _as_list(tests_value))
|
|
469
|
+
for path in _as_list(tests_file):
|
|
470
|
+
records.extend(_load_test_records(base_dir / str(path)))
|
|
471
|
+
return records
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _load_test_records(path: Path) -> List[Dict[str, Any]]:
|
|
475
|
+
source = path.expanduser().resolve()
|
|
476
|
+
if not source.exists():
|
|
477
|
+
raise ManifestError(f"eval suite tests file not found: {source}")
|
|
478
|
+
if source.suffix.lower() == ".jsonl":
|
|
479
|
+
records = []
|
|
480
|
+
for line_number, line in enumerate(source.read_text(encoding="utf-8").splitlines(), start=1):
|
|
481
|
+
if not line.strip():
|
|
482
|
+
continue
|
|
483
|
+
try:
|
|
484
|
+
item = json.loads(line)
|
|
485
|
+
except json.JSONDecodeError as exc:
|
|
486
|
+
raise ManifestError(f"invalid JSONL in {source}:{line_number}") from exc
|
|
487
|
+
records.append(_as_dict(item))
|
|
488
|
+
return records
|
|
489
|
+
data = _load_json_or_yaml(source)
|
|
490
|
+
if isinstance(data, Mapping) and "tests" in data:
|
|
491
|
+
return [_as_dict(item) for item in _as_list(data.get("tests"))]
|
|
492
|
+
return [_as_dict(item) for item in _as_list(data)]
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _normalize_provider(provider: Mapping[str, Any], index: int) -> Dict[str, Any]:
|
|
496
|
+
item = dict(provider)
|
|
497
|
+
item["id"] = str(item.get("id") or item.get("name") or f"provider_{index}")
|
|
498
|
+
item["type"] = str(item.get("type") or item.get("kind") or "echo")
|
|
499
|
+
return item
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _normalize_prompt(prompt: Mapping[str, Any], index: int) -> Dict[str, Any]:
|
|
503
|
+
item = dict(prompt)
|
|
504
|
+
item["id"] = str(item.get("id") or item.get("name") or f"prompt_{index}")
|
|
505
|
+
item["template"] = str(item.get("template") or item.get("content") or item.get("prompt") or "")
|
|
506
|
+
if not item["template"]:
|
|
507
|
+
raise ManifestError(f"prompt `{item['id']}` requires a template")
|
|
508
|
+
return item
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _normalize_test(test: Mapping[str, Any], index: int) -> Dict[str, Any]:
|
|
512
|
+
item = dict(test)
|
|
513
|
+
item["id"] = str(item.get("id") or item.get("name") or f"test_{index}")
|
|
514
|
+
item["vars"] = _as_dict(item.get("vars") or item.get("variables"))
|
|
515
|
+
assertions = _as_list(item.get("assert") or item.get("assertions") or item.get("checks"))
|
|
516
|
+
item["assertions"] = [_normalize_assertion(assertion, item["id"], offset) for offset, assertion in enumerate(assertions, start=1)]
|
|
517
|
+
if not item["assertions"]:
|
|
518
|
+
raise ManifestError(f"test `{item['id']}` requires at least one assertion")
|
|
519
|
+
return item
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _normalize_assertion(assertion: Any, test_id: str, index: int) -> Dict[str, Any]:
|
|
523
|
+
if isinstance(assertion, str):
|
|
524
|
+
item = {"type": "contains", "value": assertion}
|
|
525
|
+
else:
|
|
526
|
+
item = _as_dict(assertion)
|
|
527
|
+
item["type"] = str(item.get("type") or item.get("kind") or "contains").lower().replace("-", "_")
|
|
528
|
+
if "path" not in item:
|
|
529
|
+
for alias in ("json_path", "field"):
|
|
530
|
+
if alias in item:
|
|
531
|
+
item["path"] = item.get(alias)
|
|
532
|
+
break
|
|
533
|
+
if "value" not in item and "expected" in item:
|
|
534
|
+
item["value"] = item.get("expected")
|
|
535
|
+
assertion_type = str(item["type"])
|
|
536
|
+
if assertion_type in _JSON_PATH_ASSERTIONS and not item.get("path"):
|
|
537
|
+
raise ManifestError(f"assertion {index} in test `{test_id}` requires a path")
|
|
538
|
+
if assertion_type in _FI_EVAL_ASSERTIONS:
|
|
539
|
+
if not (item.get("eval") or item.get("metric") or item.get("name") or item.get("value")):
|
|
540
|
+
raise ManifestError(
|
|
541
|
+
f"assertion {index} in test `{test_id}` requires an `eval` "
|
|
542
|
+
"(hosted template name)"
|
|
543
|
+
)
|
|
544
|
+
return item
|
|
545
|
+
requires_value = assertion_type not in _JSON_PATH_EXISTS_ASSERTIONS
|
|
546
|
+
if requires_value and "value" not in item:
|
|
547
|
+
raise ManifestError(f"assertion {index} in test `{test_id}` requires a value")
|
|
548
|
+
return item
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _run_eval_case(
|
|
552
|
+
*,
|
|
553
|
+
provider: Mapping[str, Any],
|
|
554
|
+
prompt: Mapping[str, Any],
|
|
555
|
+
test: Mapping[str, Any],
|
|
556
|
+
test_index: int,
|
|
557
|
+
base_dir: Path,
|
|
558
|
+
) -> Dict[str, Any]:
|
|
559
|
+
variables = _as_dict(test.get("vars"))
|
|
560
|
+
rendered_prompt = _render_template(str(prompt.get("template") or ""), variables)
|
|
561
|
+
output = _provider_output(
|
|
562
|
+
provider=provider,
|
|
563
|
+
prompt=rendered_prompt,
|
|
564
|
+
variables=variables,
|
|
565
|
+
test=test,
|
|
566
|
+
base_dir=base_dir,
|
|
567
|
+
)
|
|
568
|
+
assertion_results = [
|
|
569
|
+
_evaluate_assertion(assertion, output, variables)
|
|
570
|
+
for assertion in _as_list(test.get("assertions"))
|
|
571
|
+
]
|
|
572
|
+
failures = [item for item in assertion_results if not item.get("passed")]
|
|
573
|
+
case_id = f"{provider.get('id')}::{prompt.get('id')}::{test.get('id')}"
|
|
574
|
+
score = 1.0 if not assertion_results else (len(assertion_results) - len(failures)) / len(assertion_results)
|
|
575
|
+
findings = [
|
|
576
|
+
{
|
|
577
|
+
"type": "eval_assertion_failed",
|
|
578
|
+
"severity": "high",
|
|
579
|
+
"case_id": case_id,
|
|
580
|
+
"provider_id": provider.get("id"),
|
|
581
|
+
"prompt_id": prompt.get("id"),
|
|
582
|
+
"test_id": test.get("id"),
|
|
583
|
+
"assertion_type": failure.get("type"),
|
|
584
|
+
"expected": failure.get("expected"),
|
|
585
|
+
"actual": failure.get("actual", output),
|
|
586
|
+
"path": failure.get("path"),
|
|
587
|
+
"error": failure.get("error"),
|
|
588
|
+
}
|
|
589
|
+
for failure in failures
|
|
590
|
+
]
|
|
591
|
+
return {
|
|
592
|
+
"index": test_index,
|
|
593
|
+
"id": case_id,
|
|
594
|
+
"name": case_id,
|
|
595
|
+
"provider_id": provider.get("id"),
|
|
596
|
+
"provider_type": provider.get("type"),
|
|
597
|
+
"prompt_id": prompt.get("id"),
|
|
598
|
+
"test_id": test.get("id"),
|
|
599
|
+
"input": rendered_prompt,
|
|
600
|
+
"output": output,
|
|
601
|
+
"score": round(score, 4),
|
|
602
|
+
"passed": not failures,
|
|
603
|
+
"assertions": assertion_results,
|
|
604
|
+
"findings": findings,
|
|
605
|
+
"metrics": [
|
|
606
|
+
{
|
|
607
|
+
"name": "eval_assertions",
|
|
608
|
+
"score": round(score, 4),
|
|
609
|
+
"details": {"assertions": assertion_results, "findings": findings},
|
|
610
|
+
}
|
|
611
|
+
],
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def _provider_output(
|
|
616
|
+
*,
|
|
617
|
+
provider: Mapping[str, Any],
|
|
618
|
+
prompt: str,
|
|
619
|
+
variables: Mapping[str, Any],
|
|
620
|
+
test: Mapping[str, Any],
|
|
621
|
+
base_dir: Path,
|
|
622
|
+
) -> str:
|
|
623
|
+
provider_type = str(provider.get("type") or "echo").lower().replace("-", "_")
|
|
624
|
+
if provider_type == "echo":
|
|
625
|
+
return prompt
|
|
626
|
+
if provider_type == "scripted":
|
|
627
|
+
template = str(provider.get("response") or provider.get("output") or provider.get("template") or "")
|
|
628
|
+
if not template:
|
|
629
|
+
responses = _as_list(provider.get("responses"))
|
|
630
|
+
template = str(responses[0]) if responses else prompt
|
|
631
|
+
return _render_template(template, {**variables, "prompt": prompt, "input": prompt})
|
|
632
|
+
if provider_type in {"artifact", "artifact_json", "artifact_file"}:
|
|
633
|
+
return _artifact_provider_output(
|
|
634
|
+
provider=provider,
|
|
635
|
+
prompt=prompt,
|
|
636
|
+
variables=variables,
|
|
637
|
+
base_dir=base_dir,
|
|
638
|
+
)
|
|
639
|
+
if provider_type in {"python", "python_callable", "callable"}:
|
|
640
|
+
target = str(provider.get("target") or provider.get("callable") or "")
|
|
641
|
+
if not target:
|
|
642
|
+
raise ManifestError(f"provider `{provider.get('id')}` requires target")
|
|
643
|
+
callback = _load_callable(target, base_dir)
|
|
644
|
+
value = callback(prompt=prompt, vars=dict(variables), test=dict(test), provider=dict(provider))
|
|
645
|
+
if inspect.isawaitable(value):
|
|
646
|
+
value = asyncio.run(value)
|
|
647
|
+
return str(value)
|
|
648
|
+
if provider_type in {"litellm", "llm", "vertex", "vertex_ai", "gemini"}:
|
|
649
|
+
return _litellm_provider_output(
|
|
650
|
+
provider=provider,
|
|
651
|
+
prompt=prompt,
|
|
652
|
+
variables=variables,
|
|
653
|
+
provider_type=provider_type,
|
|
654
|
+
)
|
|
655
|
+
raise ManifestError(f"unsupported eval suite provider type: {provider_type}")
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def _litellm_provider_output(
|
|
659
|
+
*,
|
|
660
|
+
provider: Mapping[str, Any],
|
|
661
|
+
prompt: str,
|
|
662
|
+
variables: Mapping[str, Any],
|
|
663
|
+
provider_type: str,
|
|
664
|
+
) -> str:
|
|
665
|
+
"""Call a live LLM through litellm.
|
|
666
|
+
|
|
667
|
+
Routes any litellm-supported model. Use ``type: vertex`` (or ``gemini``)
|
|
668
|
+
with a bare ``model`` name to reach Vertex AI — authentication comes from
|
|
669
|
+
``GOOGLE_APPLICATION_CREDENTIALS`` and routing from the ``vertex_project`` /
|
|
670
|
+
``vertex_location`` provider fields (or the matching ``VERTEXAI_*`` env
|
|
671
|
+
vars). Use ``type: litellm`` with a fully-qualified model string
|
|
672
|
+
(``vertex_ai/gemini-2.5-flash``, ``gpt-4o-mini``, ``claude-3-5-sonnet``)
|
|
673
|
+
for any other provider.
|
|
674
|
+
"""
|
|
675
|
+
try:
|
|
676
|
+
import litellm
|
|
677
|
+
except Exception as exc: # pragma: no cover - import guard
|
|
678
|
+
raise ManifestError(
|
|
679
|
+
f"provider type `{provider_type}` requires litellm; reinstall "
|
|
680
|
+
"agent-learning-kit"
|
|
681
|
+
) from exc
|
|
682
|
+
|
|
683
|
+
model = str(provider.get("model") or "").strip()
|
|
684
|
+
if not model:
|
|
685
|
+
raise ManifestError(f"provider `{provider.get('id')}` requires a model")
|
|
686
|
+
if provider_type in {"vertex", "vertex_ai", "gemini"} and "/" not in model:
|
|
687
|
+
model = f"vertex_ai/{model}"
|
|
688
|
+
|
|
689
|
+
render_ctx = {**variables, "prompt": prompt, "input": prompt}
|
|
690
|
+
messages: List[Dict[str, Any]] = []
|
|
691
|
+
system_prompt = provider.get("system") or provider.get("system_prompt")
|
|
692
|
+
if system_prompt:
|
|
693
|
+
messages.append(
|
|
694
|
+
{"role": "system", "content": _render_template(str(system_prompt), render_ctx)}
|
|
695
|
+
)
|
|
696
|
+
messages.append({"role": "user", "content": prompt})
|
|
697
|
+
|
|
698
|
+
kwargs: Dict[str, Any] = dict(_as_dict(provider.get("params")))
|
|
699
|
+
for key in (
|
|
700
|
+
"vertex_project",
|
|
701
|
+
"vertex_location",
|
|
702
|
+
"vertex_credentials",
|
|
703
|
+
"temperature",
|
|
704
|
+
"max_tokens",
|
|
705
|
+
"top_p",
|
|
706
|
+
"api_base",
|
|
707
|
+
"api_key",
|
|
708
|
+
):
|
|
709
|
+
value = provider.get(key)
|
|
710
|
+
if value is not None and key not in kwargs:
|
|
711
|
+
kwargs[key] = value
|
|
712
|
+
|
|
713
|
+
litellm.drop_params = True
|
|
714
|
+
response = litellm.completion(model=model, messages=messages, **kwargs)
|
|
715
|
+
content = response.choices[0].message.content
|
|
716
|
+
return str(content or "")
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def _artifact_provider_output(
|
|
720
|
+
*,
|
|
721
|
+
provider: Mapping[str, Any],
|
|
722
|
+
prompt: str,
|
|
723
|
+
variables: Mapping[str, Any],
|
|
724
|
+
base_dir: Path,
|
|
725
|
+
) -> str:
|
|
726
|
+
raw_path = (
|
|
727
|
+
provider.get("path")
|
|
728
|
+
or provider.get("source")
|
|
729
|
+
or provider.get("artifact")
|
|
730
|
+
or variables.get("artifact_path")
|
|
731
|
+
or variables.get("artifact")
|
|
732
|
+
)
|
|
733
|
+
if not raw_path:
|
|
734
|
+
raise ManifestError(f"provider `{provider.get('id')}` requires artifact path")
|
|
735
|
+
rendered_path = _render_template(
|
|
736
|
+
str(raw_path),
|
|
737
|
+
{**variables, "prompt": prompt, "input": prompt},
|
|
738
|
+
)
|
|
739
|
+
artifact_path = Path(rendered_path).expanduser()
|
|
740
|
+
if not artifact_path.is_absolute():
|
|
741
|
+
artifact_path = base_dir / artifact_path
|
|
742
|
+
artifact_path = artifact_path.resolve()
|
|
743
|
+
artifact = _load_json_or_yaml(artifact_path)
|
|
744
|
+
fields = _artifact_fields(provider)
|
|
745
|
+
if not fields:
|
|
746
|
+
return json.dumps(artifact, indent=2, sort_keys=True, default=str)
|
|
747
|
+
extracted = {
|
|
748
|
+
label: _extract_artifact_path(artifact, path)
|
|
749
|
+
for label, path in fields
|
|
750
|
+
}
|
|
751
|
+
return json.dumps(
|
|
752
|
+
{
|
|
753
|
+
"artifact_path": str(artifact_path),
|
|
754
|
+
"fields": extracted,
|
|
755
|
+
},
|
|
756
|
+
indent=2,
|
|
757
|
+
sort_keys=True,
|
|
758
|
+
default=str,
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
def _artifact_fields(provider: Mapping[str, Any]) -> List[tuple[str, str]]:
|
|
763
|
+
raw_fields = (
|
|
764
|
+
provider.get("fields")
|
|
765
|
+
or provider.get("extract")
|
|
766
|
+
or provider.get("paths")
|
|
767
|
+
or provider.get("json_paths")
|
|
768
|
+
)
|
|
769
|
+
fields: List[tuple[str, str]] = []
|
|
770
|
+
for index, raw_field in enumerate(_as_list(raw_fields), start=1):
|
|
771
|
+
if isinstance(raw_field, str):
|
|
772
|
+
fields.append((raw_field, raw_field))
|
|
773
|
+
continue
|
|
774
|
+
item = _as_dict(raw_field)
|
|
775
|
+
path = str(item.get("path") or item.get("json_path") or item.get("field") or "")
|
|
776
|
+
if not path:
|
|
777
|
+
raise ManifestError(f"artifact field {index} requires path")
|
|
778
|
+
label = str(item.get("id") or item.get("name") or item.get("as") or path)
|
|
779
|
+
fields.append((label, path))
|
|
780
|
+
return fields
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def _extract_artifact_path(value: Any, path: str) -> Any:
|
|
784
|
+
current = value
|
|
785
|
+
for token in _artifact_path_tokens(path):
|
|
786
|
+
if isinstance(current, Mapping):
|
|
787
|
+
if token not in current:
|
|
788
|
+
raise ManifestError(f"artifact path `{path}` missing key `{token}`")
|
|
789
|
+
current = current[token]
|
|
790
|
+
elif isinstance(current, list):
|
|
791
|
+
try:
|
|
792
|
+
index = int(token)
|
|
793
|
+
except ValueError as exc:
|
|
794
|
+
raise ManifestError(
|
|
795
|
+
f"artifact path `{path}` expected list index, got `{token}`"
|
|
796
|
+
) from exc
|
|
797
|
+
try:
|
|
798
|
+
current = current[index]
|
|
799
|
+
except IndexError as exc:
|
|
800
|
+
raise ManifestError(
|
|
801
|
+
f"artifact path `{path}` index out of range: {index}"
|
|
802
|
+
) from exc
|
|
803
|
+
else:
|
|
804
|
+
raise ManifestError(f"artifact path `{path}` cannot traverse `{token}`")
|
|
805
|
+
return current
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def _artifact_path_tokens(path: str) -> List[str]:
|
|
809
|
+
normalized = path.strip()
|
|
810
|
+
if not normalized:
|
|
811
|
+
raise ManifestError("artifact path cannot be empty")
|
|
812
|
+
if normalized.startswith("$."):
|
|
813
|
+
normalized = normalized[2:]
|
|
814
|
+
elif normalized == "$":
|
|
815
|
+
return []
|
|
816
|
+
tokens: List[str] = []
|
|
817
|
+
for segment in normalized.split("."):
|
|
818
|
+
if not segment:
|
|
819
|
+
continue
|
|
820
|
+
while "[" in segment:
|
|
821
|
+
before, _, rest = segment.partition("[")
|
|
822
|
+
if before:
|
|
823
|
+
tokens.append(before)
|
|
824
|
+
index, marker, tail = rest.partition("]")
|
|
825
|
+
if not marker:
|
|
826
|
+
raise ManifestError(f"invalid artifact path segment `{segment}`")
|
|
827
|
+
tokens.append(index)
|
|
828
|
+
segment = tail
|
|
829
|
+
if segment:
|
|
830
|
+
tokens.append(segment)
|
|
831
|
+
return tokens
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def _evaluate_assertion(
|
|
835
|
+
assertion: Mapping[str, Any],
|
|
836
|
+
output: str,
|
|
837
|
+
variables: Optional[Mapping[str, Any]] = None,
|
|
838
|
+
) -> Dict[str, Any]:
|
|
839
|
+
assertion_type = str(assertion.get("type") or "contains").lower().replace("-", "_")
|
|
840
|
+
if assertion_type in _JSON_PATH_ASSERTIONS:
|
|
841
|
+
return _evaluate_json_path_assertion(assertion, output, assertion_type)
|
|
842
|
+
if assertion_type in _FI_EVAL_ASSERTIONS:
|
|
843
|
+
return _evaluate_fi_eval_assertion(assertion, output, variables or {})
|
|
844
|
+
expected = assertion.get("value")
|
|
845
|
+
text = str(output)
|
|
846
|
+
expected_text = str(expected)
|
|
847
|
+
if assertion_type == "contains":
|
|
848
|
+
passed = expected_text in text
|
|
849
|
+
elif assertion_type == "not_contains":
|
|
850
|
+
passed = expected_text not in text
|
|
851
|
+
elif assertion_type in {"equals", "equal", "is"}:
|
|
852
|
+
passed = text.strip() == expected_text.strip()
|
|
853
|
+
elif assertion_type in {"regex", "matches"}:
|
|
854
|
+
passed = re.search(expected_text, text, flags=re.MULTILINE) is not None
|
|
855
|
+
else:
|
|
856
|
+
raise ManifestError(f"unsupported assertion type: {assertion_type}")
|
|
857
|
+
return {
|
|
858
|
+
"type": assertion_type,
|
|
859
|
+
"expected": expected,
|
|
860
|
+
"actual": output,
|
|
861
|
+
"passed": bool(passed),
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
def _evaluate_fi_eval_assertion(
|
|
866
|
+
assertion: Mapping[str, Any],
|
|
867
|
+
output: str,
|
|
868
|
+
variables: Mapping[str, Any],
|
|
869
|
+
) -> Dict[str, Any]:
|
|
870
|
+
"""Score the case output with a hosted FutureAGI eval template.
|
|
871
|
+
|
|
872
|
+
Dispatches to ``fi.evals.evaluate`` on the platform (turing) engine by
|
|
873
|
+
default; credentials come from ``FI_API_KEY`` / ``FI_SECRET_KEY`` /
|
|
874
|
+
``FI_BASE_URL`` (or per-assertion overrides). Pass when the returned score
|
|
875
|
+
is >= ``threshold`` (default 0.5).
|
|
876
|
+
"""
|
|
877
|
+
eval_name = (
|
|
878
|
+
assertion.get("eval")
|
|
879
|
+
or assertion.get("metric")
|
|
880
|
+
or assertion.get("name")
|
|
881
|
+
or assertion.get("value")
|
|
882
|
+
)
|
|
883
|
+
if not eval_name:
|
|
884
|
+
raise ManifestError(
|
|
885
|
+
"fi_eval assertion requires an `eval` (hosted template name)."
|
|
886
|
+
)
|
|
887
|
+
threshold = float(assertion.get("threshold", 0.5))
|
|
888
|
+
engine = str(assertion.get("engine") or "turing").strip().lower()
|
|
889
|
+
model = assertion.get("model")
|
|
890
|
+
|
|
891
|
+
inputs: Dict[str, Any] = {"output": output}
|
|
892
|
+
for key in _FI_EVAL_INPUT_KEYS:
|
|
893
|
+
if key in assertion:
|
|
894
|
+
inputs[key] = _render_template(str(assertion[key]), variables)
|
|
895
|
+
extra_inputs = assertion.get("inputs")
|
|
896
|
+
if isinstance(extra_inputs, Mapping):
|
|
897
|
+
for key, val in extra_inputs.items():
|
|
898
|
+
inputs[str(key)] = (
|
|
899
|
+
_render_template(val, variables) if isinstance(val, str) else val
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
try:
|
|
903
|
+
from fi.evals import evaluate as _fi_evaluate
|
|
904
|
+
except Exception as exc: # pragma: no cover - optional dependency clarity
|
|
905
|
+
raise ManifestError(
|
|
906
|
+
"fi_eval assertion requires the FutureAGI evals engine (fi.evals). "
|
|
907
|
+
"Install the evals extra to score against platform templates."
|
|
908
|
+
) from exc
|
|
909
|
+
|
|
910
|
+
call_kwargs: Dict[str, Any] = dict(inputs)
|
|
911
|
+
if engine and engine != "auto":
|
|
912
|
+
call_kwargs["engine"] = engine
|
|
913
|
+
if model:
|
|
914
|
+
call_kwargs["model"] = model
|
|
915
|
+
for cred_key, cfg_key in (
|
|
916
|
+
("fi_api_key", "api_key"),
|
|
917
|
+
("fi_secret_key", "secret_key"),
|
|
918
|
+
("fi_base_url", "base_url"),
|
|
919
|
+
):
|
|
920
|
+
if assertion.get(cfg_key):
|
|
921
|
+
call_kwargs[cred_key] = assertion[cfg_key]
|
|
922
|
+
|
|
923
|
+
try:
|
|
924
|
+
result = _fi_evaluate(str(eval_name), **call_kwargs)
|
|
925
|
+
except Exception as exc:
|
|
926
|
+
return {
|
|
927
|
+
"type": "fi_eval",
|
|
928
|
+
"eval": eval_name,
|
|
929
|
+
"engine": engine,
|
|
930
|
+
"threshold": threshold,
|
|
931
|
+
"expected": f">= {threshold}",
|
|
932
|
+
"actual": None,
|
|
933
|
+
"passed": False,
|
|
934
|
+
"error": str(exc),
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
score = getattr(result, "score", None)
|
|
938
|
+
reason = getattr(result, "reason", "") or ""
|
|
939
|
+
try:
|
|
940
|
+
score_value = float(score)
|
|
941
|
+
except (TypeError, ValueError):
|
|
942
|
+
score_value = 0.0
|
|
943
|
+
return {
|
|
944
|
+
"type": "fi_eval",
|
|
945
|
+
"eval": eval_name,
|
|
946
|
+
"engine": engine,
|
|
947
|
+
"threshold": threshold,
|
|
948
|
+
"score": round(score_value, 4),
|
|
949
|
+
"reason": reason,
|
|
950
|
+
"expected": f">= {threshold}",
|
|
951
|
+
"actual": round(score_value, 4),
|
|
952
|
+
"passed": bool(score_value >= threshold),
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def evaluate_assertions(
|
|
957
|
+
output: str,
|
|
958
|
+
assertions: Sequence[Mapping[str, Any]],
|
|
959
|
+
*,
|
|
960
|
+
variables: Optional[Mapping[str, Any]] = None,
|
|
961
|
+
) -> Dict[str, Any]:
|
|
962
|
+
"""Score an output against a list of eval-suite assertions.
|
|
963
|
+
|
|
964
|
+
Public helper used by the generative optimizer bridge so candidate prompts
|
|
965
|
+
are scored against the suite's own assertions (including ``fi_eval``
|
|
966
|
+
platform templates). Returns pass-rate plus per-assertion detail.
|
|
967
|
+
"""
|
|
968
|
+
variables = dict(variables or {})
|
|
969
|
+
results = [
|
|
970
|
+
_evaluate_assertion(assertion, output, variables)
|
|
971
|
+
for assertion in (assertions or [])
|
|
972
|
+
]
|
|
973
|
+
if not results:
|
|
974
|
+
return {"score": 1.0, "passed": True, "results": []}
|
|
975
|
+
passed = sum(1 for item in results if item.get("passed"))
|
|
976
|
+
return {
|
|
977
|
+
"score": passed / len(results),
|
|
978
|
+
"passed": passed == len(results),
|
|
979
|
+
"results": results,
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
def _evaluate_json_path_assertion(
|
|
984
|
+
assertion: Mapping[str, Any],
|
|
985
|
+
output: str,
|
|
986
|
+
assertion_type: str,
|
|
987
|
+
) -> Dict[str, Any]:
|
|
988
|
+
path = str(
|
|
989
|
+
assertion.get("path")
|
|
990
|
+
or assertion.get("json_path")
|
|
991
|
+
or assertion.get("field")
|
|
992
|
+
or ""
|
|
993
|
+
)
|
|
994
|
+
expected = assertion.get("value")
|
|
995
|
+
result: Dict[str, Any] = {
|
|
996
|
+
"type": assertion_type,
|
|
997
|
+
"path": path,
|
|
998
|
+
"expected": True if assertion_type in _JSON_PATH_EXISTS_ASSERTIONS else expected,
|
|
999
|
+
"actual": None,
|
|
1000
|
+
"passed": False,
|
|
1001
|
+
}
|
|
1002
|
+
if not path:
|
|
1003
|
+
result["error"] = "json path assertion requires a path"
|
|
1004
|
+
return result
|
|
1005
|
+
try:
|
|
1006
|
+
document = json.loads(output)
|
|
1007
|
+
except json.JSONDecodeError as exc:
|
|
1008
|
+
result["error"] = f"output is not valid JSON: {exc.msg}"
|
|
1009
|
+
return result
|
|
1010
|
+
try:
|
|
1011
|
+
actual = _extract_artifact_path(document, path)
|
|
1012
|
+
except ManifestError as exc:
|
|
1013
|
+
result["error"] = str(exc)
|
|
1014
|
+
return result
|
|
1015
|
+
result["actual"] = actual
|
|
1016
|
+
|
|
1017
|
+
if assertion_type in _JSON_PATH_EXISTS_ASSERTIONS:
|
|
1018
|
+
result["passed"] = True
|
|
1019
|
+
elif assertion_type in _JSON_PATH_EQUALS_ASSERTIONS:
|
|
1020
|
+
result["passed"] = actual == expected
|
|
1021
|
+
elif assertion_type in _JSON_PATH_GTE_ASSERTIONS:
|
|
1022
|
+
passed, error = _json_path_numeric_compare(actual, expected, "gte")
|
|
1023
|
+
result["passed"] = passed
|
|
1024
|
+
if error:
|
|
1025
|
+
result["error"] = error
|
|
1026
|
+
elif assertion_type in _JSON_PATH_LTE_ASSERTIONS:
|
|
1027
|
+
passed, error = _json_path_numeric_compare(actual, expected, "lte")
|
|
1028
|
+
result["passed"] = passed
|
|
1029
|
+
if error:
|
|
1030
|
+
result["error"] = error
|
|
1031
|
+
elif assertion_type in _JSON_PATH_CONTAINS_ASSERTIONS:
|
|
1032
|
+
result["passed"] = _json_path_contains(actual, expected)
|
|
1033
|
+
elif assertion_type in _JSON_PATH_NOT_CONTAINS_ASSERTIONS:
|
|
1034
|
+
result["passed"] = not _json_path_contains(actual, expected)
|
|
1035
|
+
return result
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
def _json_path_numeric_compare(
|
|
1039
|
+
actual: Any,
|
|
1040
|
+
expected: Any,
|
|
1041
|
+
operator: str,
|
|
1042
|
+
) -> tuple[bool, str | None]:
|
|
1043
|
+
try:
|
|
1044
|
+
actual_number = float(actual)
|
|
1045
|
+
expected_number = float(expected)
|
|
1046
|
+
except (TypeError, ValueError):
|
|
1047
|
+
return (
|
|
1048
|
+
False,
|
|
1049
|
+
f"expected numeric JSON path values, got actual={actual!r} expected={expected!r}",
|
|
1050
|
+
)
|
|
1051
|
+
if operator == "gte":
|
|
1052
|
+
return actual_number >= expected_number, None
|
|
1053
|
+
return actual_number <= expected_number, None
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
def _json_path_contains(actual: Any, expected: Any) -> bool:
|
|
1057
|
+
if isinstance(actual, Mapping):
|
|
1058
|
+
return expected in actual or str(expected) in actual
|
|
1059
|
+
if isinstance(actual, (list, tuple, set)):
|
|
1060
|
+
return expected in actual
|
|
1061
|
+
return str(expected) in str(actual)
|
|
1062
|
+
|
|
1063
|
+
|
|
1064
|
+
def _suite_result(
|
|
1065
|
+
*,
|
|
1066
|
+
name: str,
|
|
1067
|
+
suite: Mapping[str, Any],
|
|
1068
|
+
cases: Sequence[Mapping[str, Any]],
|
|
1069
|
+
threshold: float,
|
|
1070
|
+
duration_seconds: float,
|
|
1071
|
+
dry_run: bool,
|
|
1072
|
+
) -> Dict[str, Any]:
|
|
1073
|
+
case_count = len(cases)
|
|
1074
|
+
passed_count = sum(1 for case in cases if case.get("passed"))
|
|
1075
|
+
assertion_count = sum(len(_as_list(case.get("assertions"))) for case in cases)
|
|
1076
|
+
failed_assertion_count = sum(
|
|
1077
|
+
1
|
|
1078
|
+
for case in cases
|
|
1079
|
+
for assertion in _as_list(case.get("assertions"))
|
|
1080
|
+
if not _as_dict(assertion).get("passed")
|
|
1081
|
+
)
|
|
1082
|
+
score = 1.0 if not assertion_count else (assertion_count - failed_assertion_count) / assertion_count
|
|
1083
|
+
passed = (score >= threshold) and (passed_count == case_count)
|
|
1084
|
+
if dry_run:
|
|
1085
|
+
passed = True
|
|
1086
|
+
return {
|
|
1087
|
+
"schema_version": CLI_SCHEMA_VERSION,
|
|
1088
|
+
"kind": EVAL_SUITE_SCHEMA_VERSION,
|
|
1089
|
+
"name": name,
|
|
1090
|
+
"status": "passed" if passed else "failed",
|
|
1091
|
+
"exit_code": 0 if passed else 1,
|
|
1092
|
+
"summary": {
|
|
1093
|
+
"score": round(score, 4),
|
|
1094
|
+
"threshold": threshold,
|
|
1095
|
+
"provider_count": len(_as_list(suite.get("providers"))),
|
|
1096
|
+
"prompt_count": len(_as_list(suite.get("prompts"))),
|
|
1097
|
+
"test_count": len(_as_list(suite.get("tests"))),
|
|
1098
|
+
"case_count": case_count,
|
|
1099
|
+
"passed_case_count": passed_count,
|
|
1100
|
+
"failed_case_count": case_count - passed_count,
|
|
1101
|
+
"assertion_count": assertion_count,
|
|
1102
|
+
"passed_assertion_count": assertion_count - failed_assertion_count,
|
|
1103
|
+
"failed_assertion_count": failed_assertion_count,
|
|
1104
|
+
"dry_run": dry_run,
|
|
1105
|
+
},
|
|
1106
|
+
"eval_suite": {
|
|
1107
|
+
"version": suite.get("version") or EVAL_SUITE_SCHEMA_VERSION,
|
|
1108
|
+
"providers": [
|
|
1109
|
+
{"id": provider.get("id"), "type": provider.get("type")}
|
|
1110
|
+
for provider in _as_list(suite.get("providers"))
|
|
1111
|
+
if isinstance(provider, Mapping)
|
|
1112
|
+
],
|
|
1113
|
+
"prompts": [
|
|
1114
|
+
{"id": prompt.get("id")}
|
|
1115
|
+
for prompt in _as_list(suite.get("prompts"))
|
|
1116
|
+
if isinstance(prompt, Mapping)
|
|
1117
|
+
],
|
|
1118
|
+
"tests": [
|
|
1119
|
+
{"id": test.get("id")}
|
|
1120
|
+
for test in _as_list(suite.get("tests"))
|
|
1121
|
+
if isinstance(test, Mapping)
|
|
1122
|
+
],
|
|
1123
|
+
"cases": list(cases),
|
|
1124
|
+
},
|
|
1125
|
+
"evaluation": {
|
|
1126
|
+
"passed": passed,
|
|
1127
|
+
"score": round(score, 4),
|
|
1128
|
+
"threshold": threshold,
|
|
1129
|
+
"cases": list(cases),
|
|
1130
|
+
"findings": [
|
|
1131
|
+
finding
|
|
1132
|
+
for case in cases
|
|
1133
|
+
for finding in _as_list(case.get("findings"))
|
|
1134
|
+
if isinstance(finding, Mapping)
|
|
1135
|
+
],
|
|
1136
|
+
},
|
|
1137
|
+
"duration_seconds": duration_seconds,
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
def _render_template(template: str, variables: Mapping[str, Any]) -> str:
|
|
1142
|
+
result = template
|
|
1143
|
+
for key, value in variables.items():
|
|
1144
|
+
result = result.replace("{{" + str(key) + "}}", str(value))
|
|
1145
|
+
result = result.replace("{{ " + str(key) + " }}", str(value))
|
|
1146
|
+
return result
|
|
1147
|
+
|
|
1148
|
+
|
|
1149
|
+
def _load_json_or_yaml(path: Path) -> Any:
|
|
1150
|
+
if not path.exists():
|
|
1151
|
+
raise ManifestError(f"eval suite file not found: {path}")
|
|
1152
|
+
if path.suffix.lower() in {".yaml", ".yml"}:
|
|
1153
|
+
try:
|
|
1154
|
+
import yaml # type: ignore
|
|
1155
|
+
except Exception as exc: # pragma: no cover - optional dependency clarity
|
|
1156
|
+
raise ManifestError("YAML eval suites require PyYAML; use JSON or install PyYAML.") from exc
|
|
1157
|
+
with path.open("r", encoding="utf-8") as handle:
|
|
1158
|
+
return yaml.safe_load(handle)
|
|
1159
|
+
with path.open("r", encoding="utf-8") as handle:
|
|
1160
|
+
return json.load(handle)
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
def _load_callable(target: str, base_dir: Path) -> Callable[..., Any]:
|
|
1164
|
+
module_name, _, function_name = target.partition(":")
|
|
1165
|
+
if not module_name or not function_name:
|
|
1166
|
+
raise ManifestError("python callable must use 'module:function' or 'path.py:function'")
|
|
1167
|
+
if module_name.endswith(".py") or "/" in module_name:
|
|
1168
|
+
module_path = Path(module_name)
|
|
1169
|
+
if not module_path.is_absolute():
|
|
1170
|
+
module_path = base_dir / module_path
|
|
1171
|
+
spec = importlib.util.spec_from_file_location(module_path.stem, module_path)
|
|
1172
|
+
if spec is None or spec.loader is None:
|
|
1173
|
+
raise ManifestError(f"cannot load python module: {module_path}")
|
|
1174
|
+
module = importlib.util.module_from_spec(spec)
|
|
1175
|
+
spec.loader.exec_module(module)
|
|
1176
|
+
else:
|
|
1177
|
+
module = importlib.import_module(module_name)
|
|
1178
|
+
callback = getattr(module, function_name, None)
|
|
1179
|
+
if not callable(callback):
|
|
1180
|
+
raise ManifestError(f"python callable not found: {target}")
|
|
1181
|
+
return callback
|
|
1182
|
+
|
|
1183
|
+
|
|
1184
|
+
def _merge_eval_suite_options(
|
|
1185
|
+
options: Optional[EvalSuiteOptions],
|
|
1186
|
+
*,
|
|
1187
|
+
name: Optional[str],
|
|
1188
|
+
threshold: Optional[float],
|
|
1189
|
+
dry_run: Optional[bool],
|
|
1190
|
+
) -> EvalSuiteOptions:
|
|
1191
|
+
opts = options or EvalSuiteOptions()
|
|
1192
|
+
return EvalSuiteOptions(
|
|
1193
|
+
name=opts.name if name is None else name,
|
|
1194
|
+
threshold=opts.threshold if threshold is None else threshold,
|
|
1195
|
+
dry_run=opts.dry_run if dry_run is None else dry_run,
|
|
1196
|
+
)
|
|
1197
|
+
|
|
1198
|
+
|
|
1199
|
+
def _merge_eval_suite_optimization_options(
|
|
1200
|
+
options: Optional[EvalSuiteOptimizationOptions],
|
|
1201
|
+
*,
|
|
1202
|
+
name: Optional[str],
|
|
1203
|
+
threshold: Optional[float],
|
|
1204
|
+
max_candidates: Optional[int],
|
|
1205
|
+
dry_run: Optional[bool],
|
|
1206
|
+
) -> EvalSuiteOptimizationOptions:
|
|
1207
|
+
opts = options or EvalSuiteOptimizationOptions()
|
|
1208
|
+
return EvalSuiteOptimizationOptions(
|
|
1209
|
+
name=opts.name if name is None else name,
|
|
1210
|
+
threshold=opts.threshold if threshold is None else threshold,
|
|
1211
|
+
max_candidates=opts.max_candidates if max_candidates is None else max_candidates,
|
|
1212
|
+
dry_run=opts.dry_run if dry_run is None else dry_run,
|
|
1213
|
+
)
|
|
1214
|
+
|
|
1215
|
+
|
|
1216
|
+
def _suite_file_like_path(path: str | Path) -> Path:
|
|
1217
|
+
resolved = Path(path).expanduser().resolve()
|
|
1218
|
+
if resolved.is_dir():
|
|
1219
|
+
return resolved / "eval_suite.json"
|
|
1220
|
+
return resolved
|
|
1221
|
+
|
|
1222
|
+
|
|
1223
|
+
def _eval_suite_descriptor(suite: Mapping[str, Any]) -> Dict[str, Any]:
|
|
1224
|
+
return {
|
|
1225
|
+
"version": suite.get("version") or EVAL_SUITE_SCHEMA_VERSION,
|
|
1226
|
+
"providers": [
|
|
1227
|
+
{"id": provider.get("id"), "type": provider.get("type")}
|
|
1228
|
+
for provider in _as_list(suite.get("providers"))
|
|
1229
|
+
if isinstance(provider, Mapping)
|
|
1230
|
+
],
|
|
1231
|
+
"prompts": [
|
|
1232
|
+
{"id": prompt.get("id")}
|
|
1233
|
+
for prompt in _as_list(suite.get("prompts"))
|
|
1234
|
+
if isinstance(prompt, Mapping)
|
|
1235
|
+
],
|
|
1236
|
+
"tests": [
|
|
1237
|
+
{"id": test.get("id")}
|
|
1238
|
+
for test in _as_list(suite.get("tests"))
|
|
1239
|
+
if isinstance(test, Mapping)
|
|
1240
|
+
],
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
|
|
1244
|
+
def _cli() -> Any:
|
|
1245
|
+
return importlib.import_module("fi.simulate.cli")
|
|
1246
|
+
|
|
1247
|
+
|
|
1248
|
+
def _as_list(value: Any) -> List[Any]:
|
|
1249
|
+
if value is None:
|
|
1250
|
+
return []
|
|
1251
|
+
if isinstance(value, list):
|
|
1252
|
+
return value
|
|
1253
|
+
if isinstance(value, tuple):
|
|
1254
|
+
return list(value)
|
|
1255
|
+
return [value]
|
|
1256
|
+
|
|
1257
|
+
|
|
1258
|
+
def _as_dict(value: Any) -> Dict[str, Any]:
|
|
1259
|
+
return dict(value) if isinstance(value, Mapping) else {}
|
|
1260
|
+
|
|
1261
|
+
|
|
1262
|
+
def _copy_mapping_sequence(
|
|
1263
|
+
values: Sequence[Mapping[str, Any]],
|
|
1264
|
+
*,
|
|
1265
|
+
field: str,
|
|
1266
|
+
) -> List[Dict[str, Any]]:
|
|
1267
|
+
if isinstance(values, (str, bytes)) or isinstance(values, Mapping):
|
|
1268
|
+
raise ValueError(f"{field} must be a sequence of mappings")
|
|
1269
|
+
copied = [copy.deepcopy(dict(value)) for value in values]
|
|
1270
|
+
if not copied:
|
|
1271
|
+
raise ValueError(f"{field} must contain at least one item")
|
|
1272
|
+
return copied
|
|
1273
|
+
|
|
1274
|
+
|
|
1275
|
+
__all__ = [
|
|
1276
|
+
"AGENT_LEARNING_EVAL_SUITE_SCHEMA_VERSION",
|
|
1277
|
+
"EVAL_SUITE_SCHEMA_VERSION",
|
|
1278
|
+
"EVAL_SUITE_OPTIMIZATION_SCHEMA_VERSION",
|
|
1279
|
+
"EvalSuiteOptimizationOptions",
|
|
1280
|
+
"EvalSuiteOptions",
|
|
1281
|
+
"build_eval_suite_manifest",
|
|
1282
|
+
"load_eval_suite_file",
|
|
1283
|
+
"optimize_eval_suite",
|
|
1284
|
+
"optimize_eval_suite_file",
|
|
1285
|
+
"run_eval_suite",
|
|
1286
|
+
"run_eval_suite_file",
|
|
1287
|
+
"write_eval_suite_file",
|
|
1288
|
+
]
|