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,309 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Circuit breaker implementation for fault tolerance.
|
|
3
|
+
|
|
4
|
+
Prevents cascading failures by stopping requests to failing backends
|
|
5
|
+
and allowing them to recover.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
from collections import deque
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Callable, Deque, Optional, TypeVar
|
|
13
|
+
|
|
14
|
+
from .types import (
|
|
15
|
+
CircuitBreakerConfig,
|
|
16
|
+
CircuitOpenError,
|
|
17
|
+
CircuitState,
|
|
18
|
+
EventCallback,
|
|
19
|
+
ResilienceEvent,
|
|
20
|
+
ResilienceEventType,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
T = TypeVar("T")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class CircuitStats:
|
|
28
|
+
"""Statistics for circuit breaker."""
|
|
29
|
+
|
|
30
|
+
total_requests: int = 0
|
|
31
|
+
successful_requests: int = 0
|
|
32
|
+
failed_requests: int = 0
|
|
33
|
+
rejected_requests: int = 0
|
|
34
|
+
last_failure_time: Optional[float] = None
|
|
35
|
+
last_success_time: Optional[float] = None
|
|
36
|
+
state_changes: int = 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CircuitBreaker:
|
|
40
|
+
"""
|
|
41
|
+
Thread-safe circuit breaker implementation.
|
|
42
|
+
|
|
43
|
+
States:
|
|
44
|
+
- CLOSED: Normal operation, requests pass through
|
|
45
|
+
- OPEN: Failures exceeded threshold, requests rejected
|
|
46
|
+
- HALF_OPEN: Testing recovery, limited requests allowed
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
config = CircuitBreakerConfig(failure_threshold=5, timeout_seconds=30)
|
|
50
|
+
cb = CircuitBreaker("my_backend", config)
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
result = cb.execute(lambda: backend.call())
|
|
54
|
+
except CircuitOpenError:
|
|
55
|
+
# Handle circuit open
|
|
56
|
+
pass
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
name: str,
|
|
62
|
+
config: Optional[CircuitBreakerConfig] = None,
|
|
63
|
+
on_state_change: Optional[Callable[[CircuitState, CircuitState], None]] = None,
|
|
64
|
+
event_callback: Optional[EventCallback] = None,
|
|
65
|
+
):
|
|
66
|
+
"""
|
|
67
|
+
Initialize circuit breaker.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
name: Name for this circuit breaker (typically backend name)
|
|
71
|
+
config: Configuration options
|
|
72
|
+
on_state_change: Callback for state transitions
|
|
73
|
+
event_callback: Callback for resilience events
|
|
74
|
+
"""
|
|
75
|
+
self.name = name
|
|
76
|
+
self.config = config or CircuitBreakerConfig()
|
|
77
|
+
self.on_state_change = on_state_change
|
|
78
|
+
self.event_callback = event_callback
|
|
79
|
+
|
|
80
|
+
self._state = CircuitState.CLOSED
|
|
81
|
+
self._failure_count = 0
|
|
82
|
+
self._success_count = 0
|
|
83
|
+
self._half_open_requests = 0
|
|
84
|
+
self._last_failure_time: Optional[float] = None
|
|
85
|
+
self._lock = threading.RLock()
|
|
86
|
+
|
|
87
|
+
# Sliding window for failure rate calculation
|
|
88
|
+
self._results: Deque[bool] = deque(maxlen=self.config.window_size)
|
|
89
|
+
|
|
90
|
+
# Statistics
|
|
91
|
+
self.stats = CircuitStats()
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def state(self) -> CircuitState:
|
|
95
|
+
"""Get current circuit state (checks for automatic transitions)."""
|
|
96
|
+
with self._lock:
|
|
97
|
+
self._check_state_transition()
|
|
98
|
+
return self._state
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def is_closed(self) -> bool:
|
|
102
|
+
"""Check if circuit is closed (normal operation)."""
|
|
103
|
+
return self.state == CircuitState.CLOSED
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def is_open(self) -> bool:
|
|
107
|
+
"""Check if circuit is open (rejecting requests)."""
|
|
108
|
+
return self.state == CircuitState.OPEN
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def is_half_open(self) -> bool:
|
|
112
|
+
"""Check if circuit is half-open (testing recovery)."""
|
|
113
|
+
return self.state == CircuitState.HALF_OPEN
|
|
114
|
+
|
|
115
|
+
def execute(self, func: Callable[[], T]) -> T:
|
|
116
|
+
"""
|
|
117
|
+
Execute function through circuit breaker.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
func: Function to execute
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Function result
|
|
124
|
+
|
|
125
|
+
Raises:
|
|
126
|
+
CircuitOpenError: If circuit is open
|
|
127
|
+
"""
|
|
128
|
+
with self._lock:
|
|
129
|
+
self._check_state_transition()
|
|
130
|
+
|
|
131
|
+
if self._state == CircuitState.OPEN:
|
|
132
|
+
time_until_retry = self._time_until_retry()
|
|
133
|
+
self.stats.rejected_requests += 1
|
|
134
|
+
raise CircuitOpenError(self.name, time_until_retry)
|
|
135
|
+
|
|
136
|
+
if self._state == CircuitState.HALF_OPEN:
|
|
137
|
+
if self._half_open_requests >= self.config.half_open_max_requests:
|
|
138
|
+
raise CircuitOpenError(self.name, 0)
|
|
139
|
+
self._half_open_requests += 1
|
|
140
|
+
|
|
141
|
+
self.stats.total_requests += 1
|
|
142
|
+
|
|
143
|
+
# Execute outside lock to avoid blocking other threads
|
|
144
|
+
try:
|
|
145
|
+
result = func()
|
|
146
|
+
self._record_success()
|
|
147
|
+
return result
|
|
148
|
+
except Exception as e:
|
|
149
|
+
if type(e) not in self.config.excluded_exceptions:
|
|
150
|
+
self._record_failure()
|
|
151
|
+
raise
|
|
152
|
+
|
|
153
|
+
def _record_success(self) -> None:
|
|
154
|
+
"""Record a successful request."""
|
|
155
|
+
with self._lock:
|
|
156
|
+
self._results.append(True)
|
|
157
|
+
self._success_count += 1
|
|
158
|
+
self.stats.successful_requests += 1
|
|
159
|
+
self.stats.last_success_time = time.monotonic()
|
|
160
|
+
|
|
161
|
+
if self._state == CircuitState.HALF_OPEN:
|
|
162
|
+
if self._success_count >= self.config.success_threshold:
|
|
163
|
+
self._transition_to(CircuitState.CLOSED)
|
|
164
|
+
|
|
165
|
+
def _record_failure(self) -> None:
|
|
166
|
+
"""Record a failed request."""
|
|
167
|
+
with self._lock:
|
|
168
|
+
self._results.append(False)
|
|
169
|
+
self._failure_count += 1
|
|
170
|
+
self._last_failure_time = time.monotonic()
|
|
171
|
+
self.stats.failed_requests += 1
|
|
172
|
+
self.stats.last_failure_time = time.monotonic()
|
|
173
|
+
|
|
174
|
+
if self._state == CircuitState.HALF_OPEN:
|
|
175
|
+
# Any failure in half-open trips back to open
|
|
176
|
+
self._transition_to(CircuitState.OPEN)
|
|
177
|
+
elif self._state == CircuitState.CLOSED:
|
|
178
|
+
if self._should_trip():
|
|
179
|
+
self._transition_to(CircuitState.OPEN)
|
|
180
|
+
|
|
181
|
+
def _should_trip(self) -> bool:
|
|
182
|
+
"""Check if circuit should trip to OPEN."""
|
|
183
|
+
# Check absolute failure count
|
|
184
|
+
if self._failure_count >= self.config.failure_threshold:
|
|
185
|
+
return True
|
|
186
|
+
|
|
187
|
+
# Check failure rate if window is full
|
|
188
|
+
if len(self._results) >= self.config.window_size:
|
|
189
|
+
failures = sum(1 for r in self._results if not r)
|
|
190
|
+
failure_rate = failures / len(self._results)
|
|
191
|
+
if failure_rate >= self.config.failure_rate_threshold:
|
|
192
|
+
return True
|
|
193
|
+
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
def _check_state_transition(self) -> None:
|
|
197
|
+
"""Check for automatic state transitions (OPEN -> HALF_OPEN)."""
|
|
198
|
+
if self._state == CircuitState.OPEN:
|
|
199
|
+
if self._last_failure_time is not None:
|
|
200
|
+
elapsed = time.monotonic() - self._last_failure_time
|
|
201
|
+
if elapsed >= self.config.timeout_seconds:
|
|
202
|
+
self._transition_to(CircuitState.HALF_OPEN)
|
|
203
|
+
|
|
204
|
+
def _transition_to(self, new_state: CircuitState) -> None:
|
|
205
|
+
"""Transition to a new state."""
|
|
206
|
+
old_state = self._state
|
|
207
|
+
self._state = new_state
|
|
208
|
+
self.stats.state_changes += 1
|
|
209
|
+
|
|
210
|
+
# Reset counters based on new state
|
|
211
|
+
if new_state == CircuitState.CLOSED:
|
|
212
|
+
self._failure_count = 0
|
|
213
|
+
self._success_count = 0
|
|
214
|
+
self._results.clear()
|
|
215
|
+
elif new_state == CircuitState.HALF_OPEN:
|
|
216
|
+
self._success_count = 0
|
|
217
|
+
self._half_open_requests = 0
|
|
218
|
+
elif new_state == CircuitState.OPEN:
|
|
219
|
+
self._last_failure_time = time.monotonic()
|
|
220
|
+
|
|
221
|
+
# Callbacks
|
|
222
|
+
if self.on_state_change:
|
|
223
|
+
try:
|
|
224
|
+
self.on_state_change(old_state, new_state)
|
|
225
|
+
except Exception:
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
self._emit_event(old_state, new_state)
|
|
229
|
+
|
|
230
|
+
def _time_until_retry(self) -> float:
|
|
231
|
+
"""Get time until circuit will transition to half-open."""
|
|
232
|
+
if self._last_failure_time is None:
|
|
233
|
+
return 0
|
|
234
|
+
elapsed = time.monotonic() - self._last_failure_time
|
|
235
|
+
return max(0, self.config.timeout_seconds - elapsed)
|
|
236
|
+
|
|
237
|
+
def _emit_event(self, old_state: CircuitState, new_state: CircuitState) -> None:
|
|
238
|
+
"""Emit state change event."""
|
|
239
|
+
if not self.event_callback:
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
event_type_map = {
|
|
243
|
+
CircuitState.OPEN: ResilienceEventType.CIRCUIT_OPENED,
|
|
244
|
+
CircuitState.CLOSED: ResilienceEventType.CIRCUIT_CLOSED,
|
|
245
|
+
CircuitState.HALF_OPEN: ResilienceEventType.CIRCUIT_HALF_OPEN,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
event = ResilienceEvent(
|
|
249
|
+
event_type=event_type_map[new_state],
|
|
250
|
+
backend_name=self.name,
|
|
251
|
+
metadata={
|
|
252
|
+
"old_state": old_state.value,
|
|
253
|
+
"new_state": new_state.value,
|
|
254
|
+
"failure_count": self._failure_count,
|
|
255
|
+
},
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
self.event_callback(event)
|
|
260
|
+
except Exception:
|
|
261
|
+
pass
|
|
262
|
+
|
|
263
|
+
def reset(self) -> None:
|
|
264
|
+
"""Reset circuit breaker to closed state."""
|
|
265
|
+
with self._lock:
|
|
266
|
+
old_state = self._state
|
|
267
|
+
self._state = CircuitState.CLOSED
|
|
268
|
+
self._failure_count = 0
|
|
269
|
+
self._success_count = 0
|
|
270
|
+
self._half_open_requests = 0
|
|
271
|
+
self._results.clear()
|
|
272
|
+
self._last_failure_time = None
|
|
273
|
+
self.stats.state_changes += 1
|
|
274
|
+
|
|
275
|
+
if old_state != CircuitState.CLOSED:
|
|
276
|
+
self._emit_event(old_state, CircuitState.CLOSED)
|
|
277
|
+
if self.on_state_change:
|
|
278
|
+
try:
|
|
279
|
+
self.on_state_change(old_state, CircuitState.CLOSED)
|
|
280
|
+
except Exception:
|
|
281
|
+
pass
|
|
282
|
+
|
|
283
|
+
def force_open(self) -> None:
|
|
284
|
+
"""Force circuit to open state (for testing/maintenance)."""
|
|
285
|
+
with self._lock:
|
|
286
|
+
if self._state != CircuitState.OPEN:
|
|
287
|
+
self._transition_to(CircuitState.OPEN)
|
|
288
|
+
|
|
289
|
+
def get_failure_rate(self) -> float:
|
|
290
|
+
"""Get current failure rate from sliding window."""
|
|
291
|
+
with self._lock:
|
|
292
|
+
if not self._results:
|
|
293
|
+
return 0.0
|
|
294
|
+
failures = sum(1 for r in self._results if not r)
|
|
295
|
+
return failures / len(self._results)
|
|
296
|
+
|
|
297
|
+
def get_stats(self) -> dict:
|
|
298
|
+
"""Get circuit breaker statistics as dictionary."""
|
|
299
|
+
with self._lock:
|
|
300
|
+
return {
|
|
301
|
+
"state": self._state.value,
|
|
302
|
+
"total_requests": self.stats.total_requests,
|
|
303
|
+
"successful_requests": self.stats.successful_requests,
|
|
304
|
+
"failed_requests": self.stats.failed_requests,
|
|
305
|
+
"rejected_requests": self.stats.rejected_requests,
|
|
306
|
+
"state_changes": self.stats.state_changes,
|
|
307
|
+
"failure_rate": self.get_failure_rate(),
|
|
308
|
+
"failure_count": self._failure_count,
|
|
309
|
+
}
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Graceful degradation with fallback chains.
|
|
3
|
+
|
|
4
|
+
Provides fallback mechanisms when primary operations fail.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Callable, Generic, List, Optional, TypeVar
|
|
10
|
+
|
|
11
|
+
from .types import (
|
|
12
|
+
DegradationConfig,
|
|
13
|
+
EventCallback,
|
|
14
|
+
ResilienceEvent,
|
|
15
|
+
ResilienceEventType,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T")
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class FallbackStats:
|
|
24
|
+
"""Statistics for fallback handler."""
|
|
25
|
+
|
|
26
|
+
total_calls: int = 0
|
|
27
|
+
primary_success: int = 0
|
|
28
|
+
fallback_used: int = 0
|
|
29
|
+
all_failed: int = 0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class FallbackOption(Generic[T]):
|
|
34
|
+
"""
|
|
35
|
+
A fallback option with name and callable.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
name: Identifier for this fallback
|
|
39
|
+
func: Callable that returns the fallback value
|
|
40
|
+
condition: Optional condition to check before using this fallback
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
name: str
|
|
44
|
+
func: Callable[[], T]
|
|
45
|
+
condition: Optional[Callable[[Exception], bool]] = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class FallbackChain(Generic[T]):
|
|
49
|
+
"""
|
|
50
|
+
Fallback chain for graceful degradation.
|
|
51
|
+
|
|
52
|
+
Tries a series of fallbacks in order when the primary operation fails.
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
chain = FallbackChain("api_call")
|
|
56
|
+
chain.add_fallback("cache", lambda: get_from_cache())
|
|
57
|
+
chain.add_fallback("default", lambda: default_value)
|
|
58
|
+
|
|
59
|
+
result = chain.execute(lambda: api.call())
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
name: str,
|
|
65
|
+
config: Optional[DegradationConfig] = None,
|
|
66
|
+
event_callback: Optional[EventCallback] = None,
|
|
67
|
+
):
|
|
68
|
+
"""
|
|
69
|
+
Initialize fallback chain.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
name: Name for this fallback chain
|
|
73
|
+
config: Degradation configuration
|
|
74
|
+
event_callback: Callback for degradation events
|
|
75
|
+
"""
|
|
76
|
+
self.name = name
|
|
77
|
+
self.config = config or DegradationConfig()
|
|
78
|
+
self.event_callback = event_callback
|
|
79
|
+
|
|
80
|
+
self._fallbacks: List[FallbackOption[T]] = []
|
|
81
|
+
self.stats = FallbackStats()
|
|
82
|
+
|
|
83
|
+
def add_fallback(
|
|
84
|
+
self,
|
|
85
|
+
name: str,
|
|
86
|
+
func: Callable[[], T],
|
|
87
|
+
condition: Optional[Callable[[Exception], bool]] = None,
|
|
88
|
+
) -> "FallbackChain[T]":
|
|
89
|
+
"""
|
|
90
|
+
Add a fallback option to the chain.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
name: Name for this fallback
|
|
94
|
+
func: Callable that returns the fallback value
|
|
95
|
+
condition: Optional condition (receives exception, returns bool)
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Self for chaining
|
|
99
|
+
"""
|
|
100
|
+
self._fallbacks.append(FallbackOption(name=name, func=func, condition=condition))
|
|
101
|
+
return self
|
|
102
|
+
|
|
103
|
+
def execute(
|
|
104
|
+
self,
|
|
105
|
+
primary: Callable[[], T],
|
|
106
|
+
fallback_exceptions: Optional[set[type]] = None,
|
|
107
|
+
) -> T:
|
|
108
|
+
"""
|
|
109
|
+
Execute primary with fallbacks on failure.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
primary: Primary callable to execute
|
|
113
|
+
fallback_exceptions: Exception types that trigger fallback
|
|
114
|
+
(defaults to config.fallback_exceptions)
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Result from primary or a fallback
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
Exception: If primary and all fallbacks fail
|
|
121
|
+
"""
|
|
122
|
+
exceptions = fallback_exceptions or self.config.fallback_on_exceptions
|
|
123
|
+
self.stats.total_calls += 1
|
|
124
|
+
|
|
125
|
+
# Try primary
|
|
126
|
+
try:
|
|
127
|
+
result = primary()
|
|
128
|
+
self.stats.primary_success += 1
|
|
129
|
+
return result
|
|
130
|
+
except Exception as e:
|
|
131
|
+
if not self._should_fallback(e, exceptions):
|
|
132
|
+
raise
|
|
133
|
+
|
|
134
|
+
primary_error = e
|
|
135
|
+
|
|
136
|
+
# Try fallbacks in order
|
|
137
|
+
for fallback in self._fallbacks:
|
|
138
|
+
# Check condition if specified
|
|
139
|
+
if fallback.condition and not fallback.condition(primary_error):
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
result = fallback.func()
|
|
144
|
+
self.stats.fallback_used += 1
|
|
145
|
+
self._emit_fallback_event(fallback.name, primary_error)
|
|
146
|
+
return result
|
|
147
|
+
except Exception as fallback_error:
|
|
148
|
+
logger.debug(
|
|
149
|
+
f"Fallback '{fallback.name}' failed: {fallback_error}"
|
|
150
|
+
)
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
# All failed
|
|
154
|
+
self.stats.all_failed += 1
|
|
155
|
+
self._emit_all_failed_event(primary_error)
|
|
156
|
+
raise primary_error
|
|
157
|
+
|
|
158
|
+
def _should_fallback(
|
|
159
|
+
self, exception: Exception, exceptions: set[type]
|
|
160
|
+
) -> bool:
|
|
161
|
+
"""Check if exception should trigger fallback."""
|
|
162
|
+
if not exceptions:
|
|
163
|
+
return True # Fallback on all exceptions
|
|
164
|
+
|
|
165
|
+
for exc_type in exceptions:
|
|
166
|
+
if isinstance(exception, exc_type):
|
|
167
|
+
return True
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
def _emit_fallback_event(self, fallback_name: str, error: Exception) -> None:
|
|
171
|
+
"""Emit fallback used event."""
|
|
172
|
+
if not self.event_callback:
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
event = ResilienceEvent(
|
|
176
|
+
event_type=ResilienceEventType.FALLBACK_USED,
|
|
177
|
+
backend_name=self.name,
|
|
178
|
+
metadata={
|
|
179
|
+
"fallback_name": fallback_name,
|
|
180
|
+
"primary_error": str(error),
|
|
181
|
+
"error_type": type(error).__name__,
|
|
182
|
+
},
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
self.event_callback(event)
|
|
187
|
+
except Exception:
|
|
188
|
+
pass
|
|
189
|
+
|
|
190
|
+
def _emit_all_failed_event(self, error: Exception) -> None:
|
|
191
|
+
"""Emit all fallbacks failed event."""
|
|
192
|
+
if not self.event_callback:
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
event = ResilienceEvent(
|
|
196
|
+
event_type=ResilienceEventType.DEGRADATION_ACTIVE,
|
|
197
|
+
backend_name=self.name,
|
|
198
|
+
metadata={
|
|
199
|
+
"fallback_count": len(self._fallbacks),
|
|
200
|
+
"all_failed": True,
|
|
201
|
+
"last_error": str(error),
|
|
202
|
+
},
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
self.event_callback(event)
|
|
207
|
+
except Exception:
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
def get_stats(self) -> dict:
|
|
211
|
+
"""Get fallback chain statistics as dictionary."""
|
|
212
|
+
return {
|
|
213
|
+
"total_calls": self.stats.total_calls,
|
|
214
|
+
"primary_success": self.stats.primary_success,
|
|
215
|
+
"fallback_used": self.stats.fallback_used,
|
|
216
|
+
"all_failed": self.stats.all_failed,
|
|
217
|
+
"fallback_rate": (
|
|
218
|
+
self.stats.fallback_used / self.stats.total_calls
|
|
219
|
+
if self.stats.total_calls > 0
|
|
220
|
+
else 0.0
|
|
221
|
+
),
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class DegradationHandler:
|
|
226
|
+
"""
|
|
227
|
+
Handler for graceful degradation with primary/secondary pattern.
|
|
228
|
+
|
|
229
|
+
Manages a primary operation with a single fallback operation.
|
|
230
|
+
|
|
231
|
+
Example:
|
|
232
|
+
handler = DegradationHandler(
|
|
233
|
+
"api",
|
|
234
|
+
primary=lambda: api.call(),
|
|
235
|
+
fallback=lambda: cache.get(),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
result = handler.execute()
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
def __init__(
|
|
242
|
+
self,
|
|
243
|
+
name: str,
|
|
244
|
+
primary: Callable[[], T],
|
|
245
|
+
fallback: Callable[[], T],
|
|
246
|
+
config: Optional[DegradationConfig] = None,
|
|
247
|
+
event_callback: Optional[EventCallback] = None,
|
|
248
|
+
):
|
|
249
|
+
"""
|
|
250
|
+
Initialize degradation handler.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
name: Name for this handler
|
|
254
|
+
primary: Primary callable
|
|
255
|
+
fallback: Fallback callable
|
|
256
|
+
config: Degradation configuration
|
|
257
|
+
event_callback: Callback for events
|
|
258
|
+
"""
|
|
259
|
+
self.name = name
|
|
260
|
+
self._primary = primary
|
|
261
|
+
self._fallback = fallback
|
|
262
|
+
self.config = config or DegradationConfig()
|
|
263
|
+
self.event_callback = event_callback
|
|
264
|
+
|
|
265
|
+
self._chain: FallbackChain = FallbackChain(
|
|
266
|
+
name, config, event_callback
|
|
267
|
+
)
|
|
268
|
+
self._chain.add_fallback("fallback", fallback)
|
|
269
|
+
|
|
270
|
+
def execute(self) -> T:
|
|
271
|
+
"""
|
|
272
|
+
Execute primary with fallback on failure.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Result from primary or fallback
|
|
276
|
+
|
|
277
|
+
Raises:
|
|
278
|
+
Exception: If both primary and fallback fail
|
|
279
|
+
"""
|
|
280
|
+
return self._chain.execute(self._primary)
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def stats(self) -> FallbackStats:
|
|
284
|
+
"""Get statistics."""
|
|
285
|
+
return self._chain.stats
|
|
286
|
+
|
|
287
|
+
def get_stats(self) -> dict:
|
|
288
|
+
"""Get statistics as dictionary."""
|
|
289
|
+
return self._chain.get_stats()
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def with_fallback(
|
|
293
|
+
fallback_value: T,
|
|
294
|
+
exceptions: Optional[set[type]] = None,
|
|
295
|
+
name: Optional[str] = None,
|
|
296
|
+
) -> Callable[[Callable[..., T]], Callable[..., T]]:
|
|
297
|
+
"""
|
|
298
|
+
Decorator for adding a static fallback value.
|
|
299
|
+
|
|
300
|
+
Example:
|
|
301
|
+
@with_fallback(default_config)
|
|
302
|
+
def load_config():
|
|
303
|
+
return api.get_config()
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
def decorator(func: Callable[..., T]) -> Callable[..., T]:
|
|
307
|
+
handler_name = name or func.__name__
|
|
308
|
+
chain: FallbackChain = FallbackChain(handler_name)
|
|
309
|
+
chain.add_fallback("default", lambda: fallback_value)
|
|
310
|
+
|
|
311
|
+
def wrapper(*args, **kwargs) -> T:
|
|
312
|
+
return chain.execute(
|
|
313
|
+
lambda: func(*args, **kwargs),
|
|
314
|
+
fallback_exceptions=exceptions,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
wrapper.__name__ = func.__name__
|
|
318
|
+
wrapper.__doc__ = func.__doc__
|
|
319
|
+
wrapper._fallback_chain = chain # type: ignore
|
|
320
|
+
return wrapper
|
|
321
|
+
|
|
322
|
+
return decorator
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def with_fallback_func(
|
|
326
|
+
fallback_func: Callable[[], T],
|
|
327
|
+
exceptions: Optional[set[type]] = None,
|
|
328
|
+
name: Optional[str] = None,
|
|
329
|
+
) -> Callable[[Callable[..., T]], Callable[..., T]]:
|
|
330
|
+
"""
|
|
331
|
+
Decorator for adding a fallback function.
|
|
332
|
+
|
|
333
|
+
Example:
|
|
334
|
+
@with_fallback_func(lambda: load_from_cache())
|
|
335
|
+
def load_config():
|
|
336
|
+
return api.get_config()
|
|
337
|
+
"""
|
|
338
|
+
|
|
339
|
+
def decorator(func: Callable[..., T]) -> Callable[..., T]:
|
|
340
|
+
handler_name = name or func.__name__
|
|
341
|
+
chain: FallbackChain = FallbackChain(handler_name)
|
|
342
|
+
chain.add_fallback("fallback", fallback_func)
|
|
343
|
+
|
|
344
|
+
def wrapper(*args, **kwargs) -> T:
|
|
345
|
+
return chain.execute(
|
|
346
|
+
lambda: func(*args, **kwargs),
|
|
347
|
+
fallback_exceptions=exceptions,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
wrapper.__name__ = func.__name__
|
|
351
|
+
wrapper.__doc__ = func.__doc__
|
|
352
|
+
wrapper._fallback_chain = chain # type: ignore
|
|
353
|
+
return wrapper
|
|
354
|
+
|
|
355
|
+
return decorator
|