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,888 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main Guardrails Class.
|
|
3
|
+
|
|
4
|
+
Provides the primary interface for content screening with support for:
|
|
5
|
+
- Multiple models (Turing, local models, third-party APIs)
|
|
6
|
+
- Input, output, and retrieval rails
|
|
7
|
+
- Ensemble mode with configurable aggregation
|
|
8
|
+
- Async and sync APIs
|
|
9
|
+
- Fast scanner pipeline for quick threat detection
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import functools
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
import time
|
|
17
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed, TimeoutError as FuturesTimeoutError
|
|
18
|
+
from typing import Any, Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
from fi.evals.guardrails.config import (
|
|
21
|
+
GuardrailsConfig,
|
|
22
|
+
GuardrailModel,
|
|
23
|
+
RailType,
|
|
24
|
+
AggregationStrategy,
|
|
25
|
+
)
|
|
26
|
+
from fi.evals.guardrails.types import GuardrailResult, GuardrailsResponse
|
|
27
|
+
from fi.evals.guardrails.backends.base import BaseBackend
|
|
28
|
+
from fi.evals.guardrails.scanners import ScannerPipeline
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _trace_guardrail(fn):
|
|
34
|
+
"""Decorator that wraps guardrail screen methods with OTEL spans."""
|
|
35
|
+
@functools.wraps(fn)
|
|
36
|
+
def wrapper(self, content, *args, **kwargs):
|
|
37
|
+
try:
|
|
38
|
+
from fi.evals.otel.enrichment import is_auto_enrichment_enabled
|
|
39
|
+
if not is_auto_enrichment_enabled():
|
|
40
|
+
raise ImportError
|
|
41
|
+
from fi.evals.otel.conventions import (
|
|
42
|
+
GenAIAttributes,
|
|
43
|
+
GuardrailAttributes,
|
|
44
|
+
)
|
|
45
|
+
from opentelemetry import trace as _trace
|
|
46
|
+
except ImportError:
|
|
47
|
+
return fn(self, content, *args, **kwargs)
|
|
48
|
+
|
|
49
|
+
tracer = _trace.get_tracer("fi.evals.guardrails")
|
|
50
|
+
rail_type = args[0] if args else kwargs.get("rail_type", RailType.INPUT)
|
|
51
|
+
span_name = f"guardrail.screen_{rail_type.value}"
|
|
52
|
+
|
|
53
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
54
|
+
span.set_attribute(GenAIAttributes.SPAN_KIND, "GUARDRAIL")
|
|
55
|
+
span.set_attribute(GuardrailAttributes.GEN_AI_NAME, span_name)
|
|
56
|
+
span.set_attribute(GuardrailAttributes.GEN_AI_TYPE, rail_type.value)
|
|
57
|
+
span.set_attribute(GenAIAttributes.INPUT_MESSAGES, content)
|
|
58
|
+
|
|
59
|
+
response = fn(self, content, *args, **kwargs)
|
|
60
|
+
|
|
61
|
+
result_str = "allow" if response.passed else "block"
|
|
62
|
+
span.set_attribute(GuardrailAttributes.GEN_AI_RESULT, result_str)
|
|
63
|
+
if response.blocked_categories:
|
|
64
|
+
span.set_attribute(
|
|
65
|
+
GuardrailAttributes.GEN_AI_CATEGORIES,
|
|
66
|
+
str(response.blocked_categories),
|
|
67
|
+
)
|
|
68
|
+
if response.total_latency_ms:
|
|
69
|
+
span.set_attribute(GuardrailAttributes.LATENCY_MS, response.total_latency_ms)
|
|
70
|
+
|
|
71
|
+
return response
|
|
72
|
+
return wrapper
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Guardrails:
|
|
76
|
+
"""
|
|
77
|
+
Comprehensive guardrails system supporting multiple backends.
|
|
78
|
+
|
|
79
|
+
Extends the existing Protect system with:
|
|
80
|
+
- Local model support (Qwen3Guard, Granite Guardian, etc.)
|
|
81
|
+
- Turing model integration
|
|
82
|
+
- Ensemble mode with configurable aggregation
|
|
83
|
+
- Input, output, and retrieval rails
|
|
84
|
+
- Async and sync APIs
|
|
85
|
+
|
|
86
|
+
Usage:
|
|
87
|
+
# Quick start with defaults (uses Turing Flash)
|
|
88
|
+
guardrails = Guardrails()
|
|
89
|
+
result = guardrails.screen_input("user message")
|
|
90
|
+
|
|
91
|
+
# Advanced: Ensemble with multiple models
|
|
92
|
+
guardrails = Guardrails(
|
|
93
|
+
config=GuardrailsConfig(
|
|
94
|
+
models=[
|
|
95
|
+
GuardrailModel.TURING_FLASH,
|
|
96
|
+
GuardrailModel.QWEN3GUARD_8B,
|
|
97
|
+
],
|
|
98
|
+
aggregation=AggregationStrategy.MAJORITY,
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def __init__(
|
|
104
|
+
self,
|
|
105
|
+
config: Optional[GuardrailsConfig] = None,
|
|
106
|
+
fi_api_key: Optional[str] = None,
|
|
107
|
+
fi_secret_key: Optional[str] = None,
|
|
108
|
+
fi_base_url: Optional[str] = None,
|
|
109
|
+
):
|
|
110
|
+
"""
|
|
111
|
+
Initialize the Guardrails system.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
config: Configuration for the guardrails system
|
|
115
|
+
fi_api_key: FutureAGI API key (for Turing models)
|
|
116
|
+
fi_secret_key: FutureAGI secret key
|
|
117
|
+
fi_base_url: Base URL for FutureAGI API
|
|
118
|
+
"""
|
|
119
|
+
self.config = config or GuardrailsConfig()
|
|
120
|
+
self._fi_api_key = fi_api_key
|
|
121
|
+
self._fi_secret_key = fi_secret_key
|
|
122
|
+
self._fi_base_url = fi_base_url
|
|
123
|
+
self.backends: Dict[GuardrailModel, BaseBackend] = {}
|
|
124
|
+
self.scanner_pipeline: Optional[ScannerPipeline] = None
|
|
125
|
+
|
|
126
|
+
self._load_backends()
|
|
127
|
+
self._load_scanners()
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def discover_backends(cls) -> List[GuardrailModel]:
|
|
131
|
+
"""
|
|
132
|
+
Discover available backends based on environment.
|
|
133
|
+
|
|
134
|
+
Checks for API keys, VLLM servers, and GPU availability.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
List of available GuardrailModel values
|
|
138
|
+
|
|
139
|
+
Usage:
|
|
140
|
+
available = Guardrails.discover_backends()
|
|
141
|
+
print(f"Available: {[m.value for m in available]}")
|
|
142
|
+
"""
|
|
143
|
+
from fi.evals.guardrails.discovery import discover_backends
|
|
144
|
+
return discover_backends()
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def get_backend_details(cls) -> Dict[str, Dict]:
|
|
148
|
+
"""
|
|
149
|
+
Get detailed information about all backends.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Dict mapping model names to availability details
|
|
153
|
+
|
|
154
|
+
Usage:
|
|
155
|
+
details = Guardrails.get_backend_details()
|
|
156
|
+
for model, info in details.items():
|
|
157
|
+
print(f"{model}: {info['status']} - {info['reason']}")
|
|
158
|
+
"""
|
|
159
|
+
from fi.evals.guardrails.discovery import get_backend_details
|
|
160
|
+
return get_backend_details()
|
|
161
|
+
|
|
162
|
+
def _load_backends(self):
|
|
163
|
+
"""Initialize model backends based on configuration."""
|
|
164
|
+
for model in self.config.models:
|
|
165
|
+
self.backends[model] = self._create_backend(model)
|
|
166
|
+
|
|
167
|
+
def _load_scanners(self):
|
|
168
|
+
"""Initialize scanner pipeline based on configuration."""
|
|
169
|
+
scanner_config = self.config.scanners
|
|
170
|
+
if scanner_config is None or not scanner_config.enabled:
|
|
171
|
+
self.scanner_pipeline = None
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
# Build scanner pipeline from config
|
|
175
|
+
from fi.evals.guardrails.scanners import (
|
|
176
|
+
JailbreakScanner,
|
|
177
|
+
CodeInjectionScanner,
|
|
178
|
+
SecretsScanner,
|
|
179
|
+
MaliciousURLScanner,
|
|
180
|
+
InvisibleCharScanner,
|
|
181
|
+
LanguageScanner,
|
|
182
|
+
TopicRestrictionScanner,
|
|
183
|
+
RegexScanner,
|
|
184
|
+
RegexPattern,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
scanners = []
|
|
188
|
+
|
|
189
|
+
if scanner_config.jailbreak:
|
|
190
|
+
scanners.append(JailbreakScanner(
|
|
191
|
+
threshold=scanner_config.jailbreak_threshold
|
|
192
|
+
))
|
|
193
|
+
|
|
194
|
+
if scanner_config.code_injection:
|
|
195
|
+
scanners.append(CodeInjectionScanner(
|
|
196
|
+
threshold=scanner_config.code_injection_threshold
|
|
197
|
+
))
|
|
198
|
+
|
|
199
|
+
if scanner_config.secrets:
|
|
200
|
+
scanners.append(SecretsScanner(
|
|
201
|
+
threshold=scanner_config.secrets_threshold
|
|
202
|
+
))
|
|
203
|
+
|
|
204
|
+
if scanner_config.urls:
|
|
205
|
+
scanners.append(MaliciousURLScanner(
|
|
206
|
+
threshold=scanner_config.urls_threshold
|
|
207
|
+
))
|
|
208
|
+
|
|
209
|
+
if scanner_config.invisible_chars:
|
|
210
|
+
scanners.append(InvisibleCharScanner())
|
|
211
|
+
|
|
212
|
+
if scanner_config.language:
|
|
213
|
+
scanners.append(LanguageScanner(
|
|
214
|
+
allowed_languages=scanner_config.language.allowed,
|
|
215
|
+
blocked_languages=scanner_config.language.blocked,
|
|
216
|
+
allowed_scripts=scanner_config.language.allowed_scripts,
|
|
217
|
+
))
|
|
218
|
+
|
|
219
|
+
if scanner_config.topics:
|
|
220
|
+
scanners.append(TopicRestrictionScanner(
|
|
221
|
+
allowed_topics=scanner_config.topics.allowed,
|
|
222
|
+
denied_topics=scanner_config.topics.denied,
|
|
223
|
+
custom_topics=scanner_config.topics.custom_topics,
|
|
224
|
+
min_keyword_matches=scanner_config.topics.min_keyword_matches,
|
|
225
|
+
))
|
|
226
|
+
|
|
227
|
+
if scanner_config.regex_patterns or scanner_config.predefined_patterns:
|
|
228
|
+
custom_patterns = []
|
|
229
|
+
for pattern_config in scanner_config.regex_patterns:
|
|
230
|
+
custom_patterns.append(RegexPattern(
|
|
231
|
+
name=pattern_config.name,
|
|
232
|
+
pattern=pattern_config.pattern,
|
|
233
|
+
confidence=pattern_config.confidence,
|
|
234
|
+
description=pattern_config.description,
|
|
235
|
+
))
|
|
236
|
+
scanners.append(RegexScanner(
|
|
237
|
+
patterns=scanner_config.predefined_patterns,
|
|
238
|
+
custom_patterns=custom_patterns if custom_patterns else None,
|
|
239
|
+
))
|
|
240
|
+
|
|
241
|
+
if scanners:
|
|
242
|
+
self.scanner_pipeline = ScannerPipeline(
|
|
243
|
+
scanners=scanners,
|
|
244
|
+
parallel=scanner_config.parallel,
|
|
245
|
+
fail_fast=scanner_config.fail_fast,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
def _create_backend(self, model: GuardrailModel) -> BaseBackend:
|
|
249
|
+
"""
|
|
250
|
+
Create a backend for the specified model.
|
|
251
|
+
|
|
252
|
+
Uses the model registry to find and instantiate the correct backend.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
model: The model to create a backend for
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Backend instance
|
|
259
|
+
|
|
260
|
+
Raises:
|
|
261
|
+
ValueError: If model not found in registry
|
|
262
|
+
RuntimeError: If backend cannot be instantiated
|
|
263
|
+
"""
|
|
264
|
+
from fi.evals.guardrails.registry import get_model_info, get_backend_class
|
|
265
|
+
|
|
266
|
+
info = get_model_info(model)
|
|
267
|
+
if not info:
|
|
268
|
+
raise ValueError(f"Model {model.value} not found in registry")
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
backend_class = get_backend_class(model)
|
|
272
|
+
except ImportError as e:
|
|
273
|
+
raise RuntimeError(
|
|
274
|
+
f"Failed to import backend for {model.value}: {e}. "
|
|
275
|
+
"Make sure required dependencies are installed."
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Build kwargs based on model type
|
|
279
|
+
kwargs = {"model": model}
|
|
280
|
+
|
|
281
|
+
if info.model_type == "api":
|
|
282
|
+
if model.value.startswith("turing"):
|
|
283
|
+
kwargs.update({
|
|
284
|
+
"fi_api_key": self._fi_api_key,
|
|
285
|
+
"fi_secret_key": self._fi_secret_key,
|
|
286
|
+
"fi_base_url": self._fi_base_url,
|
|
287
|
+
})
|
|
288
|
+
elif model.value == "openai-moderation":
|
|
289
|
+
kwargs["api_key"] = os.environ.get("OPENAI_API_KEY")
|
|
290
|
+
elif model.value == "azure-content-safety":
|
|
291
|
+
kwargs["endpoint"] = os.environ.get("AZURE_CONTENT_SAFETY_ENDPOINT")
|
|
292
|
+
kwargs["api_key"] = os.environ.get("AZURE_CONTENT_SAFETY_KEY")
|
|
293
|
+
|
|
294
|
+
elif info.model_type == "local":
|
|
295
|
+
# Check for VLLM server URL
|
|
296
|
+
env_var = f"VLLM_{model.value.upper().replace('-', '_')}_URL"
|
|
297
|
+
vllm_url = os.environ.get(env_var) or os.environ.get("VLLM_SERVER_URL")
|
|
298
|
+
if vllm_url:
|
|
299
|
+
kwargs["vllm_url"] = vllm_url
|
|
300
|
+
|
|
301
|
+
# HuggingFace token for gated models
|
|
302
|
+
hf_token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
|
|
303
|
+
if hf_token:
|
|
304
|
+
kwargs["hf_token"] = hf_token
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
return backend_class(**kwargs)
|
|
308
|
+
except Exception as e:
|
|
309
|
+
raise RuntimeError(f"Failed to create backend for {model.value}: {e}")
|
|
310
|
+
|
|
311
|
+
# =========================================================================
|
|
312
|
+
# Input Rails - Screen user input before LLM
|
|
313
|
+
# =========================================================================
|
|
314
|
+
|
|
315
|
+
def screen_input(
|
|
316
|
+
self,
|
|
317
|
+
content: str,
|
|
318
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
319
|
+
) -> GuardrailsResponse:
|
|
320
|
+
"""
|
|
321
|
+
Screen user input BEFORE sending to LLM.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
content: User input to screen
|
|
325
|
+
metadata: Optional context (user_id, session_id, etc.)
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
GuardrailsResponse with pass/fail and details
|
|
329
|
+
"""
|
|
330
|
+
return self._screen_sync(content, RailType.INPUT, metadata=metadata)
|
|
331
|
+
|
|
332
|
+
async def screen_input_async(
|
|
333
|
+
self,
|
|
334
|
+
content: str,
|
|
335
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
336
|
+
) -> GuardrailsResponse:
|
|
337
|
+
"""
|
|
338
|
+
Async version of screen_input.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
content: User input to screen
|
|
342
|
+
metadata: Optional context
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
GuardrailsResponse with pass/fail and details
|
|
346
|
+
"""
|
|
347
|
+
return await self._screen_async(content, RailType.INPUT, metadata=metadata)
|
|
348
|
+
|
|
349
|
+
# =========================================================================
|
|
350
|
+
# Output Rails - Screen LLM response before user
|
|
351
|
+
# =========================================================================
|
|
352
|
+
|
|
353
|
+
def screen_output(
|
|
354
|
+
self,
|
|
355
|
+
content: str,
|
|
356
|
+
context: Optional[str] = None,
|
|
357
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
358
|
+
) -> GuardrailsResponse:
|
|
359
|
+
"""
|
|
360
|
+
Screen LLM output BEFORE sending to user.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
content: LLM response to screen
|
|
364
|
+
context: Optional context (for hallucination check)
|
|
365
|
+
metadata: Optional metadata
|
|
366
|
+
"""
|
|
367
|
+
return self._screen_sync(content, RailType.OUTPUT, context=context, metadata=metadata)
|
|
368
|
+
|
|
369
|
+
async def screen_output_async(
|
|
370
|
+
self,
|
|
371
|
+
content: str,
|
|
372
|
+
context: Optional[str] = None,
|
|
373
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
374
|
+
) -> GuardrailsResponse:
|
|
375
|
+
"""Async version of screen_output."""
|
|
376
|
+
return await self._screen_async(content, RailType.OUTPUT, context=context, metadata=metadata)
|
|
377
|
+
|
|
378
|
+
# =========================================================================
|
|
379
|
+
# Retrieval Rails - Screen RAG chunks
|
|
380
|
+
# =========================================================================
|
|
381
|
+
|
|
382
|
+
def screen_retrieval(
|
|
383
|
+
self,
|
|
384
|
+
chunks: List[str],
|
|
385
|
+
query: Optional[str] = None,
|
|
386
|
+
) -> List[GuardrailsResponse]:
|
|
387
|
+
"""
|
|
388
|
+
Screen retrieved chunks in RAG pipeline.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
chunks: Retrieved document chunks
|
|
392
|
+
query: Original user query
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
List of responses, one per chunk
|
|
396
|
+
"""
|
|
397
|
+
results = []
|
|
398
|
+
for chunk in chunks:
|
|
399
|
+
result = self._screen_sync(
|
|
400
|
+
chunk,
|
|
401
|
+
RailType.RETRIEVAL,
|
|
402
|
+
metadata={"query": query} if query else None,
|
|
403
|
+
)
|
|
404
|
+
results.append(result)
|
|
405
|
+
return results
|
|
406
|
+
|
|
407
|
+
async def screen_retrieval_async(
|
|
408
|
+
self,
|
|
409
|
+
chunks: List[str],
|
|
410
|
+
query: Optional[str] = None,
|
|
411
|
+
) -> List[GuardrailsResponse]:
|
|
412
|
+
"""Async version of screen_retrieval."""
|
|
413
|
+
tasks = [
|
|
414
|
+
self._screen_async(
|
|
415
|
+
chunk,
|
|
416
|
+
RailType.RETRIEVAL,
|
|
417
|
+
metadata={"query": query} if query else None,
|
|
418
|
+
)
|
|
419
|
+
for chunk in chunks
|
|
420
|
+
]
|
|
421
|
+
return await asyncio.gather(*tasks)
|
|
422
|
+
|
|
423
|
+
# =========================================================================
|
|
424
|
+
# Batch Processing
|
|
425
|
+
# =========================================================================
|
|
426
|
+
|
|
427
|
+
async def screen_batch_async(
|
|
428
|
+
self,
|
|
429
|
+
contents: List[str],
|
|
430
|
+
rail_type: RailType = RailType.INPUT,
|
|
431
|
+
) -> List[GuardrailsResponse]:
|
|
432
|
+
"""Process multiple inputs in parallel."""
|
|
433
|
+
tasks = [self._screen_async(c, rail_type) for c in contents]
|
|
434
|
+
return await asyncio.gather(*tasks)
|
|
435
|
+
|
|
436
|
+
# =========================================================================
|
|
437
|
+
# Internal Implementation
|
|
438
|
+
# =========================================================================
|
|
439
|
+
|
|
440
|
+
@_trace_guardrail
|
|
441
|
+
def _screen_sync(
|
|
442
|
+
self,
|
|
443
|
+
content: str,
|
|
444
|
+
rail_type: RailType,
|
|
445
|
+
context: Optional[str] = None,
|
|
446
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
447
|
+
) -> GuardrailsResponse:
|
|
448
|
+
"""
|
|
449
|
+
Synchronous screening using scanners and backends.
|
|
450
|
+
|
|
451
|
+
Flow:
|
|
452
|
+
1. Run fast scanners first (<10ms each)
|
|
453
|
+
2. If scanners block, optionally skip slow model backends
|
|
454
|
+
3. Run model backends
|
|
455
|
+
4. Aggregate all results
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
content: Content to screen
|
|
459
|
+
rail_type: Type of rail
|
|
460
|
+
context: Optional context
|
|
461
|
+
metadata: Optional metadata
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
Aggregated GuardrailsResponse
|
|
465
|
+
"""
|
|
466
|
+
start_time = time.time()
|
|
467
|
+
|
|
468
|
+
# Handle empty content
|
|
469
|
+
if not content or not content.strip():
|
|
470
|
+
return GuardrailsResponse.create_passed(
|
|
471
|
+
content=content,
|
|
472
|
+
latency_ms=(time.time() - start_time) * 1000,
|
|
473
|
+
models_used=[m.value for m in self.backends.keys()],
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
all_results: List[GuardrailResult] = []
|
|
477
|
+
errors: List[str] = []
|
|
478
|
+
scanner_blocked = False
|
|
479
|
+
|
|
480
|
+
# Step 1: Run fast scanners first
|
|
481
|
+
if self.scanner_pipeline:
|
|
482
|
+
try:
|
|
483
|
+
pipeline_result = self.scanner_pipeline.scan(content, context)
|
|
484
|
+
|
|
485
|
+
# Convert scanner results to GuardrailResults
|
|
486
|
+
for scan_result in pipeline_result.results:
|
|
487
|
+
guardrail_result = GuardrailResult(
|
|
488
|
+
category=scan_result.category,
|
|
489
|
+
score=scan_result.score,
|
|
490
|
+
passed=scan_result.passed,
|
|
491
|
+
model=f"scanner:{scan_result.scanner_name}",
|
|
492
|
+
reason=scan_result.reason,
|
|
493
|
+
latency_ms=scan_result.latency_ms,
|
|
494
|
+
)
|
|
495
|
+
all_results.append(guardrail_result)
|
|
496
|
+
|
|
497
|
+
scanner_blocked = not pipeline_result.passed
|
|
498
|
+
|
|
499
|
+
# If fail_fast is enabled and scanners blocked, skip model backends
|
|
500
|
+
if scanner_blocked and self.config.scanners and self.config.scanners.fail_fast:
|
|
501
|
+
elapsed_ms = (time.time() - start_time) * 1000
|
|
502
|
+
return self._aggregate_results(content, all_results, elapsed_ms)
|
|
503
|
+
|
|
504
|
+
except Exception as e:
|
|
505
|
+
errors.append(f"scanner_pipeline: {str(e)}")
|
|
506
|
+
|
|
507
|
+
# Step 2: Run model backends
|
|
508
|
+
use_weighted_early_exit = (
|
|
509
|
+
self.config.aggregation == AggregationStrategy.WEIGHTED
|
|
510
|
+
and len(self.backends) > 1
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
if self.config.parallel and len(self.backends) > 1:
|
|
514
|
+
# Run backends in parallel
|
|
515
|
+
with ThreadPoolExecutor(max_workers=self.config.max_workers) as executor:
|
|
516
|
+
future_to_model = {
|
|
517
|
+
executor.submit(
|
|
518
|
+
backend.classify,
|
|
519
|
+
content,
|
|
520
|
+
rail_type,
|
|
521
|
+
context,
|
|
522
|
+
metadata,
|
|
523
|
+
): model
|
|
524
|
+
for model, backend in self.backends.items()
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
timeout_seconds = self.config.timeout_ms / 1000.0
|
|
528
|
+
weights = self.config.model_weights
|
|
529
|
+
total_weight = sum(
|
|
530
|
+
weights.get(m.value, 1.0) for m in self.backends
|
|
531
|
+
)
|
|
532
|
+
failed_weight = 0.0
|
|
533
|
+
passed_weight = 0.0
|
|
534
|
+
|
|
535
|
+
try:
|
|
536
|
+
for future in as_completed(future_to_model, timeout=timeout_seconds):
|
|
537
|
+
model = future_to_model[future]
|
|
538
|
+
try:
|
|
539
|
+
results = future.result()
|
|
540
|
+
all_results.extend(results)
|
|
541
|
+
|
|
542
|
+
# Early exit for WEIGHTED: if accumulated weight
|
|
543
|
+
# already decides the outcome, cancel remaining futures
|
|
544
|
+
if use_weighted_early_exit:
|
|
545
|
+
w = weights.get(model.value, 1.0)
|
|
546
|
+
has_fail = any(not r.passed for r in results)
|
|
547
|
+
if has_fail:
|
|
548
|
+
failed_weight += w
|
|
549
|
+
else:
|
|
550
|
+
passed_weight += w
|
|
551
|
+
threshold = self.config.weighted_threshold
|
|
552
|
+
if failed_weight > threshold * total_weight:
|
|
553
|
+
# Enough weight to block — cancel the rest
|
|
554
|
+
for f in future_to_model:
|
|
555
|
+
f.cancel()
|
|
556
|
+
break
|
|
557
|
+
if passed_weight >= (1 - threshold) * total_weight:
|
|
558
|
+
# Remaining models can't tip the balance — pass early
|
|
559
|
+
for f in future_to_model:
|
|
560
|
+
f.cancel()
|
|
561
|
+
break
|
|
562
|
+
|
|
563
|
+
except Exception as e:
|
|
564
|
+
errors.append(f"{model.value}: {str(e)}")
|
|
565
|
+
except FuturesTimeoutError:
|
|
566
|
+
errors.append("Timeout waiting for backends")
|
|
567
|
+
else:
|
|
568
|
+
# Run backends sequentially
|
|
569
|
+
weights = self.config.model_weights
|
|
570
|
+
total_weight = sum(
|
|
571
|
+
weights.get(m.value, 1.0) for m in self.backends
|
|
572
|
+
)
|
|
573
|
+
failed_weight = 0.0
|
|
574
|
+
passed_weight = 0.0
|
|
575
|
+
|
|
576
|
+
for model, backend in self.backends.items():
|
|
577
|
+
try:
|
|
578
|
+
results = backend.classify(content, rail_type, context, metadata)
|
|
579
|
+
all_results.extend(results)
|
|
580
|
+
|
|
581
|
+
# Early exit for WEIGHTED sequential
|
|
582
|
+
if use_weighted_early_exit:
|
|
583
|
+
w = weights.get(model.value, 1.0)
|
|
584
|
+
has_fail = any(not r.passed for r in results)
|
|
585
|
+
if has_fail:
|
|
586
|
+
failed_weight += w
|
|
587
|
+
else:
|
|
588
|
+
passed_weight += w
|
|
589
|
+
threshold = self.config.weighted_threshold
|
|
590
|
+
if failed_weight > threshold * total_weight:
|
|
591
|
+
break
|
|
592
|
+
if passed_weight >= (1 - threshold) * total_weight:
|
|
593
|
+
break
|
|
594
|
+
|
|
595
|
+
except Exception as e:
|
|
596
|
+
errors.append(f"{model.value}: {str(e)}")
|
|
597
|
+
|
|
598
|
+
elapsed_ms = (time.time() - start_time) * 1000
|
|
599
|
+
|
|
600
|
+
# Handle errors
|
|
601
|
+
if not all_results and errors:
|
|
602
|
+
return GuardrailsResponse.create_error(
|
|
603
|
+
content=content,
|
|
604
|
+
error="; ".join(errors),
|
|
605
|
+
fail_open=self.config.fail_open,
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
# Aggregate results
|
|
609
|
+
return self._aggregate_results(content, all_results, elapsed_ms)
|
|
610
|
+
|
|
611
|
+
async def _screen_async(
|
|
612
|
+
self,
|
|
613
|
+
content: str,
|
|
614
|
+
rail_type: RailType,
|
|
615
|
+
context: Optional[str] = None,
|
|
616
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
617
|
+
) -> GuardrailsResponse:
|
|
618
|
+
"""
|
|
619
|
+
Asynchronous screening using scanners and backends.
|
|
620
|
+
|
|
621
|
+
Flow:
|
|
622
|
+
1. Run fast scanners first (<10ms each)
|
|
623
|
+
2. If scanners block, optionally skip slow model backends
|
|
624
|
+
3. Run model backends in parallel
|
|
625
|
+
4. Aggregate all results
|
|
626
|
+
|
|
627
|
+
Args:
|
|
628
|
+
content: Content to screen
|
|
629
|
+
rail_type: Type of rail
|
|
630
|
+
context: Optional context
|
|
631
|
+
metadata: Optional metadata
|
|
632
|
+
|
|
633
|
+
Returns:
|
|
634
|
+
Aggregated GuardrailsResponse
|
|
635
|
+
"""
|
|
636
|
+
start_time = time.time()
|
|
637
|
+
|
|
638
|
+
# Handle empty content
|
|
639
|
+
if not content or not content.strip():
|
|
640
|
+
return GuardrailsResponse.create_passed(
|
|
641
|
+
content=content,
|
|
642
|
+
latency_ms=(time.time() - start_time) * 1000,
|
|
643
|
+
models_used=[m.value for m in self.backends.keys()],
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
all_results: List[GuardrailResult] = []
|
|
647
|
+
scanner_blocked = False
|
|
648
|
+
|
|
649
|
+
# Step 1: Run fast scanners first (sync, as they're already fast)
|
|
650
|
+
if self.scanner_pipeline:
|
|
651
|
+
try:
|
|
652
|
+
pipeline_result = await self.scanner_pipeline.scan_async(content, context)
|
|
653
|
+
|
|
654
|
+
# Convert scanner results to GuardrailResults
|
|
655
|
+
for scan_result in pipeline_result.results:
|
|
656
|
+
guardrail_result = GuardrailResult(
|
|
657
|
+
category=scan_result.category,
|
|
658
|
+
score=scan_result.score,
|
|
659
|
+
passed=scan_result.passed,
|
|
660
|
+
model=f"scanner:{scan_result.scanner_name}",
|
|
661
|
+
reason=scan_result.reason,
|
|
662
|
+
latency_ms=scan_result.latency_ms,
|
|
663
|
+
)
|
|
664
|
+
all_results.append(guardrail_result)
|
|
665
|
+
|
|
666
|
+
scanner_blocked = not pipeline_result.passed
|
|
667
|
+
|
|
668
|
+
# If fail_fast is enabled and scanners blocked, skip model backends
|
|
669
|
+
if scanner_blocked and self.config.scanners and self.config.scanners.fail_fast:
|
|
670
|
+
elapsed_ms = (time.time() - start_time) * 1000
|
|
671
|
+
return self._aggregate_results(content, all_results, elapsed_ms)
|
|
672
|
+
|
|
673
|
+
except Exception:
|
|
674
|
+
pass # Continue with model backends even if scanners fail
|
|
675
|
+
|
|
676
|
+
# Step 2: Run all backends in parallel
|
|
677
|
+
tasks = [
|
|
678
|
+
backend.classify_async(content, rail_type, context, metadata)
|
|
679
|
+
for backend in self.backends.values()
|
|
680
|
+
]
|
|
681
|
+
|
|
682
|
+
try:
|
|
683
|
+
timeout_seconds = self.config.timeout_ms / 1000.0
|
|
684
|
+
results_list = await asyncio.wait_for(
|
|
685
|
+
asyncio.gather(*tasks, return_exceptions=True),
|
|
686
|
+
timeout=timeout_seconds,
|
|
687
|
+
)
|
|
688
|
+
except asyncio.TimeoutError:
|
|
689
|
+
if self.config.fail_open:
|
|
690
|
+
return GuardrailsResponse.create_passed(
|
|
691
|
+
content=content,
|
|
692
|
+
latency_ms=(time.time() - start_time) * 1000,
|
|
693
|
+
models_used=[m.value for m in self.backends.keys()],
|
|
694
|
+
)
|
|
695
|
+
else:
|
|
696
|
+
return GuardrailsResponse.create_error(
|
|
697
|
+
content=content,
|
|
698
|
+
error="Timeout waiting for backends",
|
|
699
|
+
fail_open=False,
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
elapsed_ms = (time.time() - start_time) * 1000
|
|
703
|
+
|
|
704
|
+
# Flatten backend results
|
|
705
|
+
for result in results_list:
|
|
706
|
+
if isinstance(result, Exception):
|
|
707
|
+
continue
|
|
708
|
+
all_results.extend(result)
|
|
709
|
+
|
|
710
|
+
return self._aggregate_results(content, all_results, elapsed_ms)
|
|
711
|
+
|
|
712
|
+
def _aggregate_results(
|
|
713
|
+
self,
|
|
714
|
+
content: str,
|
|
715
|
+
results: List[GuardrailResult],
|
|
716
|
+
elapsed_ms: float,
|
|
717
|
+
) -> GuardrailsResponse:
|
|
718
|
+
"""
|
|
719
|
+
Aggregate results from multiple backends using configured strategy.
|
|
720
|
+
|
|
721
|
+
Args:
|
|
722
|
+
content: Original content
|
|
723
|
+
results: Results from all backends
|
|
724
|
+
elapsed_ms: Total elapsed time
|
|
725
|
+
|
|
726
|
+
Returns:
|
|
727
|
+
Aggregated GuardrailsResponse
|
|
728
|
+
"""
|
|
729
|
+
models_used = [m.value for m in self.backends.keys()]
|
|
730
|
+
|
|
731
|
+
if not results:
|
|
732
|
+
return GuardrailsResponse.create_passed(
|
|
733
|
+
content=content,
|
|
734
|
+
latency_ms=elapsed_ms,
|
|
735
|
+
models_used=models_used,
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
# Check for error results - if all results are errors, fail closed (unless fail_open)
|
|
739
|
+
error_results = [r for r in results if r.category == "error"]
|
|
740
|
+
non_error_results = [r for r in results if r.category != "error"]
|
|
741
|
+
|
|
742
|
+
if not non_error_results:
|
|
743
|
+
# All results are errors
|
|
744
|
+
if self.config.fail_open:
|
|
745
|
+
return GuardrailsResponse.create_passed(
|
|
746
|
+
content=content,
|
|
747
|
+
latency_ms=elapsed_ms,
|
|
748
|
+
models_used=models_used,
|
|
749
|
+
results=results,
|
|
750
|
+
)
|
|
751
|
+
else:
|
|
752
|
+
error_msg = "; ".join(r.reason or "Unknown error" for r in error_results)
|
|
753
|
+
return GuardrailsResponse(
|
|
754
|
+
passed=False,
|
|
755
|
+
results=results,
|
|
756
|
+
blocked_categories=["error"],
|
|
757
|
+
original_content=content,
|
|
758
|
+
total_latency_ms=elapsed_ms,
|
|
759
|
+
models_used=models_used,
|
|
760
|
+
error=error_msg,
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
# Apply category thresholds
|
|
764
|
+
processed_results = self._apply_thresholds(results)
|
|
765
|
+
|
|
766
|
+
# Count total distinct models/scanners that produced results.
|
|
767
|
+
# Models that didn't flag a category are implicit "pass" votes.
|
|
768
|
+
all_model_names = set(r.model for r in processed_results)
|
|
769
|
+
total_voters = len(all_model_names)
|
|
770
|
+
|
|
771
|
+
# Group results by category
|
|
772
|
+
category_results: Dict[str, List[GuardrailResult]] = {}
|
|
773
|
+
for result in processed_results:
|
|
774
|
+
if result.category not in category_results:
|
|
775
|
+
category_results[result.category] = []
|
|
776
|
+
category_results[result.category].append(result)
|
|
777
|
+
|
|
778
|
+
# Apply aggregation strategy
|
|
779
|
+
blocked_categories: List[str] = []
|
|
780
|
+
flagged_categories: List[str] = []
|
|
781
|
+
|
|
782
|
+
for category, cat_results in category_results.items():
|
|
783
|
+
if category in ("safe", "empty", "error"):
|
|
784
|
+
continue
|
|
785
|
+
|
|
786
|
+
should_block = self._should_block(cat_results, total_voters)
|
|
787
|
+
|
|
788
|
+
if should_block:
|
|
789
|
+
category_config = self.config.categories.get(category)
|
|
790
|
+
if category_config:
|
|
791
|
+
if category_config.action == "block":
|
|
792
|
+
blocked_categories.append(category)
|
|
793
|
+
elif category_config.action == "flag":
|
|
794
|
+
flagged_categories.append(category)
|
|
795
|
+
else:
|
|
796
|
+
blocked_categories.append(category)
|
|
797
|
+
|
|
798
|
+
# Create response
|
|
799
|
+
passed = len(blocked_categories) == 0
|
|
800
|
+
models_used = [m.value for m in self.backends.keys()]
|
|
801
|
+
|
|
802
|
+
return GuardrailsResponse(
|
|
803
|
+
passed=passed,
|
|
804
|
+
results=processed_results,
|
|
805
|
+
blocked_categories=blocked_categories,
|
|
806
|
+
flagged_categories=flagged_categories,
|
|
807
|
+
original_content=content,
|
|
808
|
+
total_latency_ms=elapsed_ms,
|
|
809
|
+
models_used=models_used,
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
def _apply_thresholds(
|
|
813
|
+
self,
|
|
814
|
+
results: List[GuardrailResult],
|
|
815
|
+
) -> List[GuardrailResult]:
|
|
816
|
+
"""
|
|
817
|
+
Apply category-specific thresholds to results.
|
|
818
|
+
|
|
819
|
+
Args:
|
|
820
|
+
results: Raw results from backends
|
|
821
|
+
|
|
822
|
+
Returns:
|
|
823
|
+
Results with thresholds applied
|
|
824
|
+
"""
|
|
825
|
+
processed = []
|
|
826
|
+
for result in results:
|
|
827
|
+
category_config = self.config.categories.get(result.category)
|
|
828
|
+
if category_config and category_config.enabled:
|
|
829
|
+
if result.score >= category_config.threshold:
|
|
830
|
+
result.passed = False
|
|
831
|
+
result.action = category_config.action
|
|
832
|
+
else:
|
|
833
|
+
result.passed = True
|
|
834
|
+
result.action = "pass"
|
|
835
|
+
processed.append(result)
|
|
836
|
+
return processed
|
|
837
|
+
|
|
838
|
+
def _should_block(self, cat_results: List[GuardrailResult], total_voters: int = 0) -> bool:
|
|
839
|
+
"""
|
|
840
|
+
Determine if content should be blocked based on aggregation strategy.
|
|
841
|
+
|
|
842
|
+
Args:
|
|
843
|
+
cat_results: Results for a single category from all models.
|
|
844
|
+
total_voters: Total distinct models/scanners that ran.
|
|
845
|
+
Models that didn't flag this category are implicit passes.
|
|
846
|
+
|
|
847
|
+
Returns:
|
|
848
|
+
True if content should be blocked
|
|
849
|
+
"""
|
|
850
|
+
if not cat_results:
|
|
851
|
+
return False
|
|
852
|
+
|
|
853
|
+
failed_count = sum(1 for r in cat_results if not r.passed)
|
|
854
|
+
# Use total_voters (all models that ran) as denominator, not just
|
|
855
|
+
# models that flagged this specific category. Models that returned
|
|
856
|
+
# "safe" instead are implicit passes for any category they didn't flag.
|
|
857
|
+
total_count = max(total_voters, len(cat_results))
|
|
858
|
+
strategy = self.config.aggregation
|
|
859
|
+
|
|
860
|
+
if strategy == AggregationStrategy.ANY:
|
|
861
|
+
return failed_count > 0
|
|
862
|
+
elif strategy == AggregationStrategy.ALL:
|
|
863
|
+
return failed_count == total_count
|
|
864
|
+
elif strategy == AggregationStrategy.MAJORITY:
|
|
865
|
+
return failed_count > total_count / 2
|
|
866
|
+
elif strategy == AggregationStrategy.WEIGHTED:
|
|
867
|
+
weights = self.config.model_weights
|
|
868
|
+
# Total weight includes ALL models that ran (from backends dict),
|
|
869
|
+
# not just those that flagged this specific category.
|
|
870
|
+
total_weight = sum(
|
|
871
|
+
weights.get(m.value, 1.0) for m in self.backends
|
|
872
|
+
)
|
|
873
|
+
if total_weight == 0:
|
|
874
|
+
# Fallback: just use weights from cat_results
|
|
875
|
+
total_weight = sum(weights.get(r.model, 1.0) for r in cat_results)
|
|
876
|
+
if total_weight == 0:
|
|
877
|
+
return False
|
|
878
|
+
failed_weight = 0.0
|
|
879
|
+
for r in cat_results:
|
|
880
|
+
if not r.passed:
|
|
881
|
+
failed_weight += weights.get(r.model, 1.0)
|
|
882
|
+
return failed_weight > self.config.weighted_threshold * total_weight
|
|
883
|
+
else:
|
|
884
|
+
return failed_count > 0
|
|
885
|
+
|
|
886
|
+
def __repr__(self) -> str:
|
|
887
|
+
models = [m.value for m in self.config.models]
|
|
888
|
+
return f"Guardrails(models={models}, aggregation={self.config.aggregation.value})"
|