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
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Evaluation Span Processor.
|
|
3
|
+
|
|
4
|
+
Enriches LLM spans with evaluation scores by running
|
|
5
|
+
configured metrics against the prompt/completion pairs.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict, List, Optional, Callable
|
|
9
|
+
from concurrent.futures import ThreadPoolExecutor, Future
|
|
10
|
+
import logging
|
|
11
|
+
import time
|
|
12
|
+
import random
|
|
13
|
+
import hashlib
|
|
14
|
+
|
|
15
|
+
from .base import BaseSpanProcessor, OTEL_AVAILABLE
|
|
16
|
+
from ..conventions import (
|
|
17
|
+
GenAIAttributes,
|
|
18
|
+
create_evaluation_attributes,
|
|
19
|
+
)
|
|
20
|
+
from ..types import EvaluationResult
|
|
21
|
+
|
|
22
|
+
if OTEL_AVAILABLE:
|
|
23
|
+
from opentelemetry.sdk.trace import ReadableSpan
|
|
24
|
+
else:
|
|
25
|
+
ReadableSpan = Any
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Type alias for evaluator function
|
|
31
|
+
EvaluatorFn = Callable[[str, str, Optional[str]], List[EvaluationResult]]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class EvaluationSpanProcessor(BaseSpanProcessor):
|
|
35
|
+
"""
|
|
36
|
+
Processor that evaluates LLM spans and attaches scores.
|
|
37
|
+
|
|
38
|
+
This processor runs configured evaluation metrics against
|
|
39
|
+
the prompt/completion pairs in LLM spans and records the
|
|
40
|
+
evaluation results as span attributes or events.
|
|
41
|
+
|
|
42
|
+
Supports:
|
|
43
|
+
- Multiple evaluation metrics
|
|
44
|
+
- Sampling (evaluate only a fraction of spans)
|
|
45
|
+
- Async evaluation (non-blocking)
|
|
46
|
+
- Result caching
|
|
47
|
+
- Timeout handling
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
from fi.evals import evaluate
|
|
51
|
+
|
|
52
|
+
processor = EvaluationSpanProcessor(
|
|
53
|
+
metrics=["relevance", "coherence", "faithfulness"],
|
|
54
|
+
sample_rate=0.1, # Evaluate 10% of spans
|
|
55
|
+
async_evaluation=True,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Or with custom evaluator
|
|
59
|
+
processor = EvaluationSpanProcessor(
|
|
60
|
+
evaluator=my_custom_evaluator,
|
|
61
|
+
metrics=["custom_metric"],
|
|
62
|
+
)
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
metrics: Optional[List[str]] = None,
|
|
68
|
+
evaluator: Optional[EvaluatorFn] = None,
|
|
69
|
+
sample_rate: float = 1.0,
|
|
70
|
+
async_evaluation: bool = True,
|
|
71
|
+
timeout_ms: int = 5000,
|
|
72
|
+
cache_enabled: bool = True,
|
|
73
|
+
cache_ttl_seconds: int = 3600,
|
|
74
|
+
max_workers: int = 4,
|
|
75
|
+
evaluator_model: Optional[str] = None,
|
|
76
|
+
on_evaluation_complete: Optional[Callable[[str, List[EvaluationResult]], None]] = None,
|
|
77
|
+
enabled: bool = True,
|
|
78
|
+
):
|
|
79
|
+
"""
|
|
80
|
+
Initialize the evaluation processor.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
metrics: List of metrics to evaluate (e.g., ["relevance", "coherence"])
|
|
84
|
+
evaluator: Custom evaluator function, or None to use default
|
|
85
|
+
sample_rate: Fraction of spans to evaluate (0.0 to 1.0)
|
|
86
|
+
async_evaluation: Whether to evaluate asynchronously
|
|
87
|
+
timeout_ms: Timeout for evaluation in milliseconds
|
|
88
|
+
cache_enabled: Whether to cache evaluation results
|
|
89
|
+
cache_ttl_seconds: Cache TTL in seconds
|
|
90
|
+
max_workers: Max threads for async evaluation
|
|
91
|
+
evaluator_model: Model to use for LLM-based evaluation
|
|
92
|
+
on_evaluation_complete: Callback when evaluation completes
|
|
93
|
+
enabled: Whether processor is active
|
|
94
|
+
"""
|
|
95
|
+
super().__init__(enabled=enabled)
|
|
96
|
+
self._metrics = metrics or ["relevance", "coherence"]
|
|
97
|
+
self._evaluator = evaluator
|
|
98
|
+
self._sample_rate = max(0.0, min(1.0, sample_rate))
|
|
99
|
+
self._async_evaluation = async_evaluation
|
|
100
|
+
self._timeout_ms = timeout_ms
|
|
101
|
+
self._cache_enabled = cache_enabled
|
|
102
|
+
self._cache_ttl_seconds = cache_ttl_seconds
|
|
103
|
+
self._max_workers = max_workers
|
|
104
|
+
self._evaluator_model = evaluator_model
|
|
105
|
+
self._on_evaluation_complete = on_evaluation_complete
|
|
106
|
+
|
|
107
|
+
# Internal state
|
|
108
|
+
self._executor: Optional[ThreadPoolExecutor] = None
|
|
109
|
+
self._cache: Dict[str, tuple[float, List[EvaluationResult]]] = {}
|
|
110
|
+
self._pending_evaluations: Dict[str, Future] = {}
|
|
111
|
+
|
|
112
|
+
def should_process(self, span: ReadableSpan) -> bool:
|
|
113
|
+
"""Determine if span should be evaluated."""
|
|
114
|
+
if not self.enabled:
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
# Check if it's an LLM span
|
|
118
|
+
attrs = self._get_attributes(span)
|
|
119
|
+
if not self._is_llm_span(attrs):
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
# Check sampling
|
|
123
|
+
if self._sample_rate < 1.0:
|
|
124
|
+
if random.random() > self._sample_rate:
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
def _is_llm_span(self, attrs: Dict[str, Any]) -> bool:
|
|
130
|
+
"""Check if this is an LLM span worth evaluating."""
|
|
131
|
+
# Has GenAI provider attribute
|
|
132
|
+
if GenAIAttributes.PROVIDER_NAME in attrs:
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
# Has model attribute
|
|
136
|
+
if GenAIAttributes.REQUEST_MODEL in attrs:
|
|
137
|
+
return True
|
|
138
|
+
|
|
139
|
+
# Has output messages
|
|
140
|
+
if GenAIAttributes.OUTPUT_MESSAGES in attrs:
|
|
141
|
+
return True
|
|
142
|
+
|
|
143
|
+
# Check for common patterns
|
|
144
|
+
for key in attrs:
|
|
145
|
+
if any(pattern in str(key).lower() for pattern in ["llm", "gen_ai", "completion"]):
|
|
146
|
+
return True
|
|
147
|
+
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
def _get_attributes(self, span: ReadableSpan) -> Dict[str, Any]:
|
|
151
|
+
"""Get span attributes safely."""
|
|
152
|
+
try:
|
|
153
|
+
return dict(span.attributes or {})
|
|
154
|
+
except Exception:
|
|
155
|
+
return {}
|
|
156
|
+
|
|
157
|
+
def _get_span_id(self, span: ReadableSpan) -> str:
|
|
158
|
+
"""Get unique span identifier."""
|
|
159
|
+
try:
|
|
160
|
+
ctx = span.get_span_context()
|
|
161
|
+
return f"{ctx.trace_id:032x}:{ctx.span_id:016x}"
|
|
162
|
+
except Exception:
|
|
163
|
+
return str(id(span))
|
|
164
|
+
|
|
165
|
+
def on_end(self, span: ReadableSpan) -> None:
|
|
166
|
+
"""Evaluate the span and record results."""
|
|
167
|
+
if not self.should_process(span):
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
attrs = self._get_attributes(span)
|
|
172
|
+
span_id = self._get_span_id(span)
|
|
173
|
+
|
|
174
|
+
# Extract content for evaluation
|
|
175
|
+
prompt = self._extract_prompt(attrs)
|
|
176
|
+
completion = self._extract_completion(attrs)
|
|
177
|
+
model = attrs.get(GenAIAttributes.REQUEST_MODEL)
|
|
178
|
+
|
|
179
|
+
if not prompt or not completion:
|
|
180
|
+
logger.debug(f"Skipping evaluation for span {span_id}: no content")
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
# Check cache
|
|
184
|
+
cache_key = self._compute_cache_key(prompt, completion, self._metrics)
|
|
185
|
+
cached_results = self._get_cached_results(cache_key)
|
|
186
|
+
if cached_results is not None:
|
|
187
|
+
logger.debug(f"Using cached evaluation for span {span_id}")
|
|
188
|
+
self._handle_results(span_id, cached_results)
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
# Run evaluation
|
|
192
|
+
if self._async_evaluation:
|
|
193
|
+
self._evaluate_async(span_id, prompt, completion, model, cache_key)
|
|
194
|
+
else:
|
|
195
|
+
results = self._evaluate_sync(prompt, completion, model)
|
|
196
|
+
self._cache_results(cache_key, results)
|
|
197
|
+
self._handle_results(span_id, results)
|
|
198
|
+
|
|
199
|
+
except Exception as e:
|
|
200
|
+
logger.warning(f"Evaluation processing error: {e}")
|
|
201
|
+
|
|
202
|
+
def _extract_prompt(self, attrs: Dict[str, Any]) -> Optional[str]:
|
|
203
|
+
"""Extract prompt from span attributes."""
|
|
204
|
+
for key in [
|
|
205
|
+
GenAIAttributes.INPUT_MESSAGES,
|
|
206
|
+
"llm.prompt",
|
|
207
|
+
"prompt",
|
|
208
|
+
"input",
|
|
209
|
+
]:
|
|
210
|
+
if key in attrs:
|
|
211
|
+
return str(attrs[key])
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
def _extract_completion(self, attrs: Dict[str, Any]) -> Optional[str]:
|
|
215
|
+
"""Extract completion from span attributes."""
|
|
216
|
+
for key in [
|
|
217
|
+
GenAIAttributes.OUTPUT_MESSAGES,
|
|
218
|
+
"llm.completion",
|
|
219
|
+
"completion",
|
|
220
|
+
"output",
|
|
221
|
+
"response",
|
|
222
|
+
]:
|
|
223
|
+
if key in attrs:
|
|
224
|
+
return str(attrs[key])
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
def _evaluate_sync(
|
|
228
|
+
self,
|
|
229
|
+
prompt: str,
|
|
230
|
+
completion: str,
|
|
231
|
+
model: Optional[str] = None,
|
|
232
|
+
) -> List[EvaluationResult]:
|
|
233
|
+
"""Run evaluation synchronously."""
|
|
234
|
+
start_time = time.time()
|
|
235
|
+
results: List[EvaluationResult] = []
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
if self._evaluator:
|
|
239
|
+
# Use custom evaluator
|
|
240
|
+
results = self._evaluator(prompt, completion, model)
|
|
241
|
+
else:
|
|
242
|
+
# Use default evaluator (fi.evals)
|
|
243
|
+
results = self._run_default_evaluator(prompt, completion, model)
|
|
244
|
+
|
|
245
|
+
# Add latency to results
|
|
246
|
+
latency_ms = (time.time() - start_time) * 1000
|
|
247
|
+
for result in results:
|
|
248
|
+
if result.latency_ms is None:
|
|
249
|
+
result.latency_ms = latency_ms / len(results)
|
|
250
|
+
|
|
251
|
+
except Exception as e:
|
|
252
|
+
logger.warning(f"Evaluation failed: {e}")
|
|
253
|
+
# Return error result
|
|
254
|
+
results = [
|
|
255
|
+
EvaluationResult(
|
|
256
|
+
metric="error",
|
|
257
|
+
score=0.0,
|
|
258
|
+
reason=str(e),
|
|
259
|
+
latency_ms=(time.time() - start_time) * 1000,
|
|
260
|
+
)
|
|
261
|
+
]
|
|
262
|
+
|
|
263
|
+
return results
|
|
264
|
+
|
|
265
|
+
def _run_default_evaluator(
|
|
266
|
+
self,
|
|
267
|
+
prompt: str,
|
|
268
|
+
completion: str,
|
|
269
|
+
model: Optional[str] = None,
|
|
270
|
+
) -> List[EvaluationResult]:
|
|
271
|
+
"""Run the default fi.evals evaluator."""
|
|
272
|
+
results: List[EvaluationResult] = []
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
# Import here to avoid circular deps
|
|
276
|
+
from fi.evals import Evaluator
|
|
277
|
+
|
|
278
|
+
evaluator = Evaluator()
|
|
279
|
+
|
|
280
|
+
for metric in self._metrics:
|
|
281
|
+
start = time.time()
|
|
282
|
+
try:
|
|
283
|
+
# Try to evaluate with the metric
|
|
284
|
+
eval_result = evaluator.evaluate(
|
|
285
|
+
metric=metric,
|
|
286
|
+
input=prompt,
|
|
287
|
+
output=completion,
|
|
288
|
+
model=self._evaluator_model,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
results.append(EvaluationResult(
|
|
292
|
+
metric=metric,
|
|
293
|
+
score=float(eval_result.get("score", 0.0)),
|
|
294
|
+
reason=eval_result.get("reason"),
|
|
295
|
+
latency_ms=(time.time() - start) * 1000,
|
|
296
|
+
))
|
|
297
|
+
|
|
298
|
+
except Exception as e:
|
|
299
|
+
logger.warning(f"Metric '{metric}' evaluation failed: {e}")
|
|
300
|
+
results.append(EvaluationResult(
|
|
301
|
+
metric=metric,
|
|
302
|
+
score=0.0,
|
|
303
|
+
reason=f"Evaluation error: {e}",
|
|
304
|
+
latency_ms=(time.time() - start) * 1000,
|
|
305
|
+
))
|
|
306
|
+
|
|
307
|
+
except ImportError:
|
|
308
|
+
logger.warning("fi.evals not available, skipping default evaluation")
|
|
309
|
+
|
|
310
|
+
return results
|
|
311
|
+
|
|
312
|
+
def _evaluate_async(
|
|
313
|
+
self,
|
|
314
|
+
span_id: str,
|
|
315
|
+
prompt: str,
|
|
316
|
+
completion: str,
|
|
317
|
+
model: Optional[str],
|
|
318
|
+
cache_key: str,
|
|
319
|
+
) -> None:
|
|
320
|
+
"""Run evaluation asynchronously."""
|
|
321
|
+
if self._executor is None:
|
|
322
|
+
self._executor = ThreadPoolExecutor(max_workers=self._max_workers)
|
|
323
|
+
|
|
324
|
+
def evaluate_task():
|
|
325
|
+
results = self._evaluate_sync(prompt, completion, model)
|
|
326
|
+
self._cache_results(cache_key, results)
|
|
327
|
+
self._handle_results(span_id, results)
|
|
328
|
+
return results
|
|
329
|
+
|
|
330
|
+
future = self._executor.submit(evaluate_task)
|
|
331
|
+
self._pending_evaluations[span_id] = future
|
|
332
|
+
|
|
333
|
+
def _handle_results(
|
|
334
|
+
self,
|
|
335
|
+
span_id: str,
|
|
336
|
+
results: List[EvaluationResult],
|
|
337
|
+
) -> None:
|
|
338
|
+
"""Handle evaluation results."""
|
|
339
|
+
# Log results
|
|
340
|
+
for result in results:
|
|
341
|
+
logger.info(
|
|
342
|
+
f"Evaluation [{span_id}] {result.metric}: "
|
|
343
|
+
f"score={result.score:.3f}"
|
|
344
|
+
+ (f" reason={result.reason[:50]}..." if result.reason else "")
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# Call callback if configured
|
|
348
|
+
if self._on_evaluation_complete:
|
|
349
|
+
try:
|
|
350
|
+
self._on_evaluation_complete(span_id, results)
|
|
351
|
+
except Exception as e:
|
|
352
|
+
logger.warning(f"Evaluation callback error: {e}")
|
|
353
|
+
|
|
354
|
+
# Clean up pending
|
|
355
|
+
self._pending_evaluations.pop(span_id, None)
|
|
356
|
+
|
|
357
|
+
def _compute_cache_key(
|
|
358
|
+
self,
|
|
359
|
+
prompt: str,
|
|
360
|
+
completion: str,
|
|
361
|
+
metrics: List[str],
|
|
362
|
+
) -> str:
|
|
363
|
+
"""Compute cache key for evaluation results."""
|
|
364
|
+
content = f"{prompt}|{completion}|{','.join(sorted(metrics))}"
|
|
365
|
+
return hashlib.sha256(content.encode()).hexdigest()[:32]
|
|
366
|
+
|
|
367
|
+
def _get_cached_results(self, cache_key: str) -> Optional[List[EvaluationResult]]:
|
|
368
|
+
"""Get cached results if valid."""
|
|
369
|
+
if not self._cache_enabled:
|
|
370
|
+
return None
|
|
371
|
+
|
|
372
|
+
if cache_key not in self._cache:
|
|
373
|
+
return None
|
|
374
|
+
|
|
375
|
+
timestamp, results = self._cache[cache_key]
|
|
376
|
+
if time.time() - timestamp > self._cache_ttl_seconds:
|
|
377
|
+
# Expired
|
|
378
|
+
del self._cache[cache_key]
|
|
379
|
+
return None
|
|
380
|
+
|
|
381
|
+
return results
|
|
382
|
+
|
|
383
|
+
def _cache_results(self, cache_key: str, results: List[EvaluationResult]) -> None:
|
|
384
|
+
"""Cache evaluation results."""
|
|
385
|
+
if not self._cache_enabled:
|
|
386
|
+
return
|
|
387
|
+
|
|
388
|
+
self._cache[cache_key] = (time.time(), results)
|
|
389
|
+
|
|
390
|
+
# Cleanup old entries if cache is too large
|
|
391
|
+
if len(self._cache) > 10000:
|
|
392
|
+
self._cleanup_cache()
|
|
393
|
+
|
|
394
|
+
def _cleanup_cache(self) -> None:
|
|
395
|
+
"""Remove expired cache entries."""
|
|
396
|
+
current_time = time.time()
|
|
397
|
+
expired = [
|
|
398
|
+
key for key, (ts, _) in self._cache.items()
|
|
399
|
+
if current_time - ts > self._cache_ttl_seconds
|
|
400
|
+
]
|
|
401
|
+
for key in expired:
|
|
402
|
+
del self._cache[key]
|
|
403
|
+
|
|
404
|
+
def get_evaluation_attributes(
|
|
405
|
+
self,
|
|
406
|
+
results: List[EvaluationResult],
|
|
407
|
+
) -> Dict[str, Any]:
|
|
408
|
+
"""
|
|
409
|
+
Convert evaluation results to span attributes.
|
|
410
|
+
|
|
411
|
+
Uses gen_ai.evaluation.* namespace.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
results: List of evaluation results
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Dictionary of OTEL-compliant attributes
|
|
418
|
+
"""
|
|
419
|
+
attrs: Dict[str, Any] = {}
|
|
420
|
+
|
|
421
|
+
for result in results:
|
|
422
|
+
# Dual-write via helper
|
|
423
|
+
result_attrs = create_evaluation_attributes(
|
|
424
|
+
metric=result.metric,
|
|
425
|
+
score=result.score,
|
|
426
|
+
reason=result.reason,
|
|
427
|
+
latency_ms=result.latency_ms,
|
|
428
|
+
)
|
|
429
|
+
attrs.update(result_attrs)
|
|
430
|
+
|
|
431
|
+
return attrs
|
|
432
|
+
|
|
433
|
+
def shutdown(self) -> None:
|
|
434
|
+
"""Shutdown the processor."""
|
|
435
|
+
super().shutdown()
|
|
436
|
+
|
|
437
|
+
# Wait for pending evaluations
|
|
438
|
+
for span_id, future in list(self._pending_evaluations.items()):
|
|
439
|
+
try:
|
|
440
|
+
future.result(timeout=self._timeout_ms / 1000)
|
|
441
|
+
except Exception as e:
|
|
442
|
+
logger.warning(f"Pending evaluation {span_id} failed: {e}")
|
|
443
|
+
|
|
444
|
+
self._pending_evaluations.clear()
|
|
445
|
+
|
|
446
|
+
# Shutdown executor
|
|
447
|
+
if self._executor:
|
|
448
|
+
self._executor.shutdown(wait=True)
|
|
449
|
+
self._executor = None
|
|
450
|
+
|
|
451
|
+
# Clear cache
|
|
452
|
+
self._cache.clear()
|
|
453
|
+
|
|
454
|
+
def force_flush(self, timeout_millis: int = 30000) -> bool:
|
|
455
|
+
"""Wait for pending evaluations."""
|
|
456
|
+
success = True
|
|
457
|
+
per_eval_timeout = timeout_millis / 1000 / max(len(self._pending_evaluations), 1)
|
|
458
|
+
|
|
459
|
+
for span_id, future in list(self._pending_evaluations.items()):
|
|
460
|
+
try:
|
|
461
|
+
future.result(timeout=per_eval_timeout)
|
|
462
|
+
except Exception:
|
|
463
|
+
success = False
|
|
464
|
+
|
|
465
|
+
return success
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
class BatchEvaluationProcessor(EvaluationSpanProcessor):
|
|
469
|
+
"""
|
|
470
|
+
Evaluation processor that batches evaluations for efficiency.
|
|
471
|
+
|
|
472
|
+
Collects spans and evaluates them in batches rather than
|
|
473
|
+
one at a time, which can be more efficient for some evaluators.
|
|
474
|
+
|
|
475
|
+
Example:
|
|
476
|
+
processor = BatchEvaluationProcessor(
|
|
477
|
+
batch_size=10,
|
|
478
|
+
batch_timeout_ms=1000,
|
|
479
|
+
)
|
|
480
|
+
"""
|
|
481
|
+
|
|
482
|
+
def __init__(
|
|
483
|
+
self,
|
|
484
|
+
batch_size: int = 10,
|
|
485
|
+
batch_timeout_ms: int = 1000,
|
|
486
|
+
**kwargs,
|
|
487
|
+
):
|
|
488
|
+
"""
|
|
489
|
+
Initialize batch evaluation processor.
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
batch_size: Maximum batch size
|
|
493
|
+
batch_timeout_ms: Max time to wait for batch
|
|
494
|
+
**kwargs: Additional args for EvaluationSpanProcessor
|
|
495
|
+
"""
|
|
496
|
+
super().__init__(**kwargs)
|
|
497
|
+
self._batch_size = batch_size
|
|
498
|
+
self._batch_timeout_ms = batch_timeout_ms
|
|
499
|
+
self._batch: List[tuple[str, str, str, Optional[str]]] = []
|
|
500
|
+
self._last_batch_time = time.time()
|
|
501
|
+
|
|
502
|
+
def on_end(self, span: ReadableSpan) -> None:
|
|
503
|
+
"""Add span to batch for evaluation."""
|
|
504
|
+
if not self.should_process(span):
|
|
505
|
+
return
|
|
506
|
+
|
|
507
|
+
try:
|
|
508
|
+
attrs = self._get_attributes(span)
|
|
509
|
+
span_id = self._get_span_id(span)
|
|
510
|
+
prompt = self._extract_prompt(attrs)
|
|
511
|
+
completion = self._extract_completion(attrs)
|
|
512
|
+
model = attrs.get(GenAIAttributes.REQUEST_MODEL)
|
|
513
|
+
|
|
514
|
+
if not prompt or not completion:
|
|
515
|
+
return
|
|
516
|
+
|
|
517
|
+
# Add to batch
|
|
518
|
+
self._batch.append((span_id, prompt, completion, model))
|
|
519
|
+
|
|
520
|
+
# Check if we should process batch
|
|
521
|
+
if len(self._batch) >= self._batch_size:
|
|
522
|
+
self._process_batch()
|
|
523
|
+
elif (time.time() - self._last_batch_time) * 1000 >= self._batch_timeout_ms:
|
|
524
|
+
self._process_batch()
|
|
525
|
+
|
|
526
|
+
except Exception as e:
|
|
527
|
+
logger.warning(f"Batch evaluation error: {e}")
|
|
528
|
+
|
|
529
|
+
def _process_batch(self) -> None:
|
|
530
|
+
"""Process the accumulated batch."""
|
|
531
|
+
if not self._batch:
|
|
532
|
+
return
|
|
533
|
+
|
|
534
|
+
batch = self._batch
|
|
535
|
+
self._batch = []
|
|
536
|
+
self._last_batch_time = time.time()
|
|
537
|
+
|
|
538
|
+
# Evaluate each item in the batch
|
|
539
|
+
for span_id, prompt, completion, model in batch:
|
|
540
|
+
cache_key = self._compute_cache_key(prompt, completion, self._metrics)
|
|
541
|
+
cached = self._get_cached_results(cache_key)
|
|
542
|
+
|
|
543
|
+
if cached is not None:
|
|
544
|
+
self._handle_results(span_id, cached)
|
|
545
|
+
else:
|
|
546
|
+
results = self._evaluate_sync(prompt, completion, model)
|
|
547
|
+
self._cache_results(cache_key, results)
|
|
548
|
+
self._handle_results(span_id, results)
|
|
549
|
+
|
|
550
|
+
def shutdown(self) -> None:
|
|
551
|
+
"""Process remaining batch and shutdown."""
|
|
552
|
+
self._process_batch()
|
|
553
|
+
super().shutdown()
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
__all__ = [
|
|
557
|
+
"EvaluationSpanProcessor",
|
|
558
|
+
"BatchEvaluationProcessor",
|
|
559
|
+
]
|