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.
Files changed (642) hide show
  1. agent_learning_kit-0.1.0.dist-info/METADATA +381 -0
  2. agent_learning_kit-0.1.0.dist-info/RECORD +642 -0
  3. agent_learning_kit-0.1.0.dist-info/WHEEL +4 -0
  4. agent_learning_kit-0.1.0.dist-info/entry_points.txt +5 -0
  5. agent_learning_kit-0.1.0.dist-info/licenses/LICENSE +173 -0
  6. agent_learning_kit-0.1.0.dist-info/licenses/NOTICE +7 -0
  7. fi/__init__.py +5 -0
  8. fi/alk/__init__.py +57 -0
  9. fi/alk/_facade.py +31 -0
  10. fi/alk/_module_alias.py +68 -0
  11. fi/alk/_paths.py +14 -0
  12. fi/alk/_schema.py +522 -0
  13. fi/alk/actions.py +727 -0
  14. fi/alk/bench/__init__.py +517 -0
  15. fi/alk/bench/_codeexec.py +213 -0
  16. fi/alk/bench/_coding.py +215 -0
  17. fi/alk/bench/_docker.py +237 -0
  18. fi/alk/bench/_grader.py +286 -0
  19. fi/alk/bench/_pull.py +212 -0
  20. fi/alk/bench/_voice.py +147 -0
  21. fi/alk/capabilities.py +627 -0
  22. fi/alk/cli.py +6396 -0
  23. fi/alk/config.py +130 -0
  24. fi/alk/cua_loop.py +562 -0
  25. fi/alk/evals.py +2351 -0
  26. fi/alk/extensions.py +163 -0
  27. fi/alk/harness/ARCHITECTURE.md +231 -0
  28. fi/alk/harness/DESIGN.md +246 -0
  29. fi/alk/harness/ENVIRONMENT_CONFORMANCE.md +127 -0
  30. fi/alk/harness/HOW-IT-WORKS.md +297 -0
  31. fi/alk/harness/IMPLEMENTATION_AND_VALIDATION_STATUS.md +229 -0
  32. fi/alk/harness/README.md +417 -0
  33. fi/alk/harness/__init__.py +77 -0
  34. fi/alk/harness/__main__.py +3 -0
  35. fi/alk/harness/amend.py +312 -0
  36. fi/alk/harness/artifacts.py +319 -0
  37. fi/alk/harness/authoring_entrypoint.py +189 -0
  38. fi/alk/harness/authoring_runtime_validation.py +267 -0
  39. fi/alk/harness/backends/README.md +43 -0
  40. fi/alk/harness/backends/__init__.py +122 -0
  41. fi/alk/harness/backends/base.py +241 -0
  42. fi/alk/harness/backends/claude.py +211 -0
  43. fi/alk/harness/backends/files.py +182 -0
  44. fi/alk/harness/backends/vertex_gemini.py +457 -0
  45. fi/alk/harness/background_noise.py +95 -0
  46. fi/alk/harness/build.py +385 -0
  47. fi/alk/harness/bundle.py +593 -0
  48. fi/alk/harness/bundle_author_v2.py +1831 -0
  49. fi/alk/harness/bundle_v2.py +719 -0
  50. fi/alk/harness/call_runner.py +1440 -0
  51. fi/alk/harness/callback_http_adapter.py +111 -0
  52. fi/alk/harness/catalogue.py +287 -0
  53. fi/alk/harness/chat.py +428 -0
  54. fi/alk/harness/chat_call_runner.py +506 -0
  55. fi/alk/harness/checks.py +136 -0
  56. fi/alk/harness/cli.py +1354 -0
  57. fi/alk/harness/config.py +338 -0
  58. fi/alk/harness/contract.py +718 -0
  59. fi/alk/harness/credentials.py +674 -0
  60. fi/alk/harness/data/persona_vocabulary.json +111 -0
  61. fi/alk/harness/environment.py +99 -0
  62. fi/alk/harness/environment_plan.py +168 -0
  63. fi/alk/harness/events.py +125 -0
  64. fi/alk/harness/executor.py +304 -0
  65. fi/alk/harness/folder.py +234 -0
  66. fi/alk/harness/generated_runtime.py +815 -0
  67. fi/alk/harness/github.py +72 -0
  68. fi/alk/harness/hosted_authoring_entrypoint.py +183 -0
  69. fi/alk/harness/hosted_entrypoint.py +2402 -0
  70. fi/alk/harness/hosted_scheduler.py +2218 -0
  71. fi/alk/harness/job.py +426 -0
  72. fi/alk/harness/judge.py +184 -0
  73. fi/alk/harness/livekit_source.py +50 -0
  74. fi/alk/harness/livekit_tool_trace_bootstrap.py +71 -0
  75. fi/alk/harness/observability.py +208 -0
  76. fi/alk/harness/outbound.py +3252 -0
  77. fi/alk/harness/packaging.py +515 -0
  78. fi/alk/harness/persona_guides.py +157 -0
  79. fi/alk/harness/platform.py +692 -0
  80. fi/alk/harness/process_preflight.py +764 -0
  81. fi/alk/harness/process_runtime.py +5670 -0
  82. fi/alk/harness/prove.py +425 -0
  83. fi/alk/harness/provider_import.py +703 -0
  84. fi/alk/harness/provider_lifecycle.py +392 -0
  85. fi/alk/harness/provision.py +2896 -0
  86. fi/alk/harness/reception.py +147 -0
  87. fi/alk/harness/retell_chat_call_runner.py +373 -0
  88. fi/alk/harness/run/__init__.py +296 -0
  89. fi/alk/harness/run/alk.py +184 -0
  90. fi/alk/harness/run/call.py +162 -0
  91. fi/alk/harness/run/conversation.py +264 -0
  92. fi/alk/harness/run/data/voices_by_language_and_gender.json +693 -0
  93. fi/alk/harness/run/evidence.py +195 -0
  94. fi/alk/harness/run/grade.py +598 -0
  95. fi/alk/harness/run/live.py +297 -0
  96. fi/alk/harness/run/models.py +56 -0
  97. fi/alk/harness/run/platform_evals.py +227 -0
  98. fi/alk/harness/run/sdk_voice.py +130 -0
  99. fi/alk/harness/run/simulation.py +1209 -0
  100. fi/alk/harness/run/stage.py +91 -0
  101. fi/alk/harness/run/targets.py +508 -0
  102. fi/alk/harness/run/tools.py +601 -0
  103. fi/alk/harness/run/voice.py +340 -0
  104. fi/alk/harness/runtime.py +172 -0
  105. fi/alk/harness/sandbox_server.py +2011 -0
  106. fi/alk/harness/sandbox_worker.py +44 -0
  107. fi/alk/harness/scenario.py +1048 -0
  108. fi/alk/harness/scenario_source.py +879 -0
  109. fi/alk/harness/scenario_tools.py +1143 -0
  110. fi/alk/harness/scenarios.py +915 -0
  111. fi/alk/harness/secrets.py +168 -0
  112. fi/alk/harness/service_catalog.py +97 -0
  113. fi/alk/harness/session.py +391 -0
  114. fi/alk/harness/sessions.py +372 -0
  115. fi/alk/harness/simulator.py +76 -0
  116. fi/alk/harness/simulator_voice.py +928 -0
  117. fi/alk/harness/skills/build-environment/SKILL.md +538 -0
  118. fi/alk/harness/skills/harness.md +131 -0
  119. fi/alk/harness/skills/kinds/chat.md +48 -0
  120. fi/alk/harness/skills/kinds/voice-voicemail.md +63 -0
  121. fi/alk/harness/skills/kinds/voice.md +59 -0
  122. fi/alk/harness/skills/plan-suite/SKILL.md +103 -0
  123. fi/alk/harness/skills/provision-environment/SKILL.md +136 -0
  124. fi/alk/harness/skills/run-scenarios/SKILL.md +112 -0
  125. fi/alk/harness/skills/understand-agent/SKILL.md +251 -0
  126. fi/alk/harness/skills/write-scenarios/SKILL.md +606 -0
  127. fi/alk/harness/skills/write-scenarios/references/refusals.md +28 -0
  128. fi/alk/harness/skills/write-scenarios/references/world-api.md +92 -0
  129. fi/alk/harness/source_data_invariants.py +444 -0
  130. fi/alk/harness/source_tool_evidence.py +79 -0
  131. fi/alk/harness/sources.py +253 -0
  132. fi/alk/harness/spend.py +140 -0
  133. fi/alk/harness/tool_trace_proxy.py +104 -0
  134. fi/alk/harness/tools.py +1018 -0
  135. fi/alk/harness/understand.py +169 -0
  136. fi/alk/harness/voicemail_audio.py +74 -0
  137. fi/alk/harness/world/__init__.py +33 -0
  138. fi/alk/harness/world/errors.py +68 -0
  139. fi/alk/harness/world/expectations.py +91 -0
  140. fi/alk/harness/world/handle.py +538 -0
  141. fi/alk/harness/world/kinds.py +196 -0
  142. fi/alk/harness/world/mutate.py +186 -0
  143. fi/alk/harness/world/probe.py +413 -0
  144. fi/alk/harness/world/provision.py +511 -0
  145. fi/alk/harness/world/provisioned.py +191 -0
  146. fi/alk/harness/world/runtime.py +616 -0
  147. fi/alk/harness/world/snapshot.py +288 -0
  148. fi/alk/harness/world/stores/__init__.py +305 -0
  149. fi/alk/harness/world/stores/container.py +215 -0
  150. fi/alk/harness/world/stores/inprocess.py +346 -0
  151. fi/alk/harness/world/stores/postgres.py +481 -0
  152. fi/alk/harness/world/stores/prove.py +202 -0
  153. fi/alk/harness/world/stores/sqlite.py +245 -0
  154. fi/alk/harness/world/stores/written.py +182 -0
  155. fi/alk/harness/world/tools.py +1516 -0
  156. fi/alk/harness/world/workspace.py +144 -0
  157. fi/alk/image_loop.py +453 -0
  158. fi/alk/image_perturb.py +241 -0
  159. fi/alk/improve.py +274 -0
  160. fi/alk/live/__init__.py +154 -0
  161. fi/alk/live/_attribution.py +184 -0
  162. fi/alk/live/_capture.py +264 -0
  163. fi/alk/live/_codec.py +391 -0
  164. fi/alk/live/_contract.py +134 -0
  165. fi/alk/live/_loopback.py +316 -0
  166. fi/alk/live/_perturb.py +449 -0
  167. fi/alk/live/_runner.py +386 -0
  168. fi/alk/live/_stats.py +561 -0
  169. fi/alk/live/_transcript.py +240 -0
  170. fi/alk/live/_workers/__init__.py +9 -0
  171. fi/alk/live/_workers/a2a_worker.py +316 -0
  172. fi/alk/live/_workers/langgraph_worker.py +217 -0
  173. fi/alk/live/_workers/livekit_worker.py +207 -0
  174. fi/alk/live/_workers/mcp_loopback_server.py +46 -0
  175. fi/alk/live/_workers/mcp_worker.py +158 -0
  176. fi/alk/live/_workers/pipecat_worker.py +189 -0
  177. fi/alk/live/a2a_lane.py +138 -0
  178. fi/alk/live/langgraph_lane.py +339 -0
  179. fi/alk/live/livekit_lane.py +376 -0
  180. fi/alk/live/mcp_lane.py +172 -0
  181. fi/alk/live/pipecat_lane.py +341 -0
  182. fi/alk/live/voice_redteam.py +494 -0
  183. fi/alk/loss.py +306 -0
  184. fi/alk/optimize.py +36260 -0
  185. fi/alk/practice/__init__.py +51 -0
  186. fi/alk/practice/_assess.py +103 -0
  187. fi/alk/practice/_budget.py +81 -0
  188. fi/alk/practice/_calibrate.py +69 -0
  189. fi/alk/practice/_capstone.py +86 -0
  190. fi/alk/practice/_contract.py +91 -0
  191. fi/alk/practice/_diagnose.py +79 -0
  192. fi/alk/practice/_drill.py +196 -0
  193. fi/alk/practice/_experiment.py +720 -0
  194. fi/alk/practice/_schedule.py +102 -0
  195. fi/alk/practice/_store.py +194 -0
  196. fi/alk/practice/_trainer.py +245 -0
  197. fi/alk/practice/_update.py +125 -0
  198. fi/alk/redteam.py +2621 -0
  199. fi/alk/rewardhack.py +237 -0
  200. fi/alk/simulate.py +10351 -0
  201. fi/alk/studio/__init__.py +82 -0
  202. fi/alk/studio/_bias.py +314 -0
  203. fi/alk/studio/_calibration.py +522 -0
  204. fi/alk/studio/_coverage.py +262 -0
  205. fi/alk/studio/_download.py +665 -0
  206. fi/alk/studio/_fidelity_attack.py +114 -0
  207. fi/alk/studio/_generate.py +652 -0
  208. fi/alk/studio/_library.py +370 -0
  209. fi/alk/studio/_scan.py +134 -0
  210. fi/alk/studio/_upgrade.py +42 -0
  211. fi/alk/studio/_vendor.py +172 -0
  212. fi/alk/suite.py +4200 -0
  213. fi/alk/tasks.py +828 -0
  214. fi/alk/telemetry/__init__.py +149 -0
  215. fi/alk/telemetry/_contract.py +141 -0
  216. fi/alk/telemetry/_emit.py +182 -0
  217. fi/alk/telemetry/_ledger.py +296 -0
  218. fi/alk/telemetry/_queue.py +127 -0
  219. fi/alk/telemetry/_row.py +294 -0
  220. fi/alk/telemetry/_run.py +233 -0
  221. fi/alk/telemetry/_sync.py +193 -0
  222. fi/alk/telemetry/_url.py +119 -0
  223. fi/alk/trinity.py +49397 -0
  224. fi/alk/voice_loop.py +174 -0
  225. fi/api/__init__.py +1 -0
  226. fi/api/auth.py +137 -0
  227. fi/api/types.py +29 -0
  228. fi/cli/__init__.py +9 -0
  229. fi/cli/assertions/__init__.py +25 -0
  230. fi/cli/assertions/conditions.py +76 -0
  231. fi/cli/assertions/evaluator.py +286 -0
  232. fi/cli/assertions/exit_codes.py +20 -0
  233. fi/cli/assertions/parser.py +131 -0
  234. fi/cli/assertions/reporter.py +194 -0
  235. fi/cli/commands/__init__.py +9 -0
  236. fi/cli/commands/config.py +165 -0
  237. fi/cli/commands/export.py +208 -0
  238. fi/cli/commands/init.py +112 -0
  239. fi/cli/commands/list_cmd.py +213 -0
  240. fi/cli/commands/run.py +486 -0
  241. fi/cli/commands/validate.py +173 -0
  242. fi/cli/commands/view.py +424 -0
  243. fi/cli/config/__init__.py +6 -0
  244. fi/cli/config/defaults.py +206 -0
  245. fi/cli/config/loader.py +155 -0
  246. fi/cli/config/schema.py +174 -0
  247. fi/cli/main.py +78 -0
  248. fi/cli/output/__init__.py +6 -0
  249. fi/cli/output/formatters.py +106 -0
  250. fi/cli/output/reporters.py +46 -0
  251. fi/cli/storage/__init__.py +5 -0
  252. fi/cli/storage/run_history.py +249 -0
  253. fi/cli/utils/__init__.py +5 -0
  254. fi/cli/utils/console.py +44 -0
  255. fi/evals/__init__.py +131 -0
  256. fi/evals/autoeval/__init__.py +137 -0
  257. fi/evals/autoeval/analyzer.py +211 -0
  258. fi/evals/autoeval/config.py +244 -0
  259. fi/evals/autoeval/export.py +213 -0
  260. fi/evals/autoeval/interactive.py +283 -0
  261. fi/evals/autoeval/pipeline.py +625 -0
  262. fi/evals/autoeval/prompts.py +139 -0
  263. fi/evals/autoeval/recommender.py +242 -0
  264. fi/evals/autoeval/rules.py +589 -0
  265. fi/evals/autoeval/templates.py +299 -0
  266. fi/evals/autoeval/types.py +232 -0
  267. fi/evals/core/__init__.py +16 -0
  268. fi/evals/core/cloud_registry.py +184 -0
  269. fi/evals/core/engines.py +368 -0
  270. fi/evals/core/evaluate.py +319 -0
  271. fi/evals/core/judge_prompt.py +90 -0
  272. fi/evals/core/prompt_generator.py +83 -0
  273. fi/evals/core/registry.py +57 -0
  274. fi/evals/core/result.py +55 -0
  275. fi/evals/evaluator.py +721 -0
  276. fi/evals/execution.py +168 -0
  277. fi/evals/feedback/__init__.py +32 -0
  278. fi/evals/feedback/calibrator.py +160 -0
  279. fi/evals/feedback/collector.py +214 -0
  280. fi/evals/feedback/hooks.py +81 -0
  281. fi/evals/feedback/retriever.py +128 -0
  282. fi/evals/feedback/store.py +272 -0
  283. fi/evals/feedback/types.py +99 -0
  284. fi/evals/framework/README.md +79 -0
  285. fi/evals/framework/__init__.py +267 -0
  286. fi/evals/framework/backends/Dockerfile.eval-runner +33 -0
  287. fi/evals/framework/backends/__init__.py +99 -0
  288. fi/evals/framework/backends/_container.py +141 -0
  289. fi/evals/framework/backends/_utils.py +145 -0
  290. fi/evals/framework/backends/base.py +223 -0
  291. fi/evals/framework/backends/celery_backend.py +417 -0
  292. fi/evals/framework/backends/celery_worker.py +78 -0
  293. fi/evals/framework/backends/kubernetes_backend.py +665 -0
  294. fi/evals/framework/backends/ray_backend.py +521 -0
  295. fi/evals/framework/backends/temporal.py +350 -0
  296. fi/evals/framework/backends/temporal_worker.py +126 -0
  297. fi/evals/framework/backends/thread_pool.py +286 -0
  298. fi/evals/framework/context.py +258 -0
  299. fi/evals/framework/enrichment.py +306 -0
  300. fi/evals/framework/evals/__init__.py +68 -0
  301. fi/evals/framework/evals/agentic.py +399 -0
  302. fi/evals/framework/evals/builder.py +609 -0
  303. fi/evals/framework/evals/semantic.py +142 -0
  304. fi/evals/framework/evaluator.py +647 -0
  305. fi/evals/framework/evaluators/__init__.py +22 -0
  306. fi/evals/framework/evaluators/blocking.py +347 -0
  307. fi/evals/framework/evaluators/non_blocking.py +577 -0
  308. fi/evals/framework/propagation.py +421 -0
  309. fi/evals/framework/protocols.py +385 -0
  310. fi/evals/framework/registry.py +370 -0
  311. fi/evals/framework/resilience/__init__.py +150 -0
  312. fi/evals/framework/resilience/circuit_breaker.py +309 -0
  313. fi/evals/framework/resilience/degradation.py +355 -0
  314. fi/evals/framework/resilience/health.py +505 -0
  315. fi/evals/framework/resilience/rate_limiter.py +228 -0
  316. fi/evals/framework/resilience/retry.py +274 -0
  317. fi/evals/framework/resilience/types.py +288 -0
  318. fi/evals/framework/resilience/wrapper.py +433 -0
  319. fi/evals/framework/types.py +218 -0
  320. fi/evals/guardrails/README.md +915 -0
  321. fi/evals/guardrails/__init__.py +96 -0
  322. fi/evals/guardrails/backends/__init__.py +43 -0
  323. fi/evals/guardrails/backends/azure.py +361 -0
  324. fi/evals/guardrails/backends/base.py +88 -0
  325. fi/evals/guardrails/backends/generic_llm.py +163 -0
  326. fi/evals/guardrails/backends/granite.py +216 -0
  327. fi/evals/guardrails/backends/llamaguard.py +221 -0
  328. fi/evals/guardrails/backends/local_base.py +479 -0
  329. fi/evals/guardrails/backends/openai.py +365 -0
  330. fi/evals/guardrails/backends/qwen.py +170 -0
  331. fi/evals/guardrails/backends/shieldgemma.py +154 -0
  332. fi/evals/guardrails/backends/turing.py +235 -0
  333. fi/evals/guardrails/backends/vllm_client.py +321 -0
  334. fi/evals/guardrails/backends/wildguard.py +188 -0
  335. fi/evals/guardrails/base.py +888 -0
  336. fi/evals/guardrails/config.py +221 -0
  337. fi/evals/guardrails/discovery.py +243 -0
  338. fi/evals/guardrails/gateway.py +437 -0
  339. fi/evals/guardrails/registry.py +231 -0
  340. fi/evals/guardrails/scanners/__init__.py +127 -0
  341. fi/evals/guardrails/scanners/base.py +191 -0
  342. fi/evals/guardrails/scanners/code_injection.py +243 -0
  343. fi/evals/guardrails/scanners/eval_delegate.py +574 -0
  344. fi/evals/guardrails/scanners/invisible_chars.py +351 -0
  345. fi/evals/guardrails/scanners/jailbreak.py +412 -0
  346. fi/evals/guardrails/scanners/language.py +288 -0
  347. fi/evals/guardrails/scanners/pipeline.py +260 -0
  348. fi/evals/guardrails/scanners/regex.py +311 -0
  349. fi/evals/guardrails/scanners/secrets.py +274 -0
  350. fi/evals/guardrails/scanners/topics.py +649 -0
  351. fi/evals/guardrails/scanners/urls.py +341 -0
  352. fi/evals/guardrails/types.py +96 -0
  353. fi/evals/llm/__init__.py +3 -0
  354. fi/evals/llm/base_llm_provider.py +35 -0
  355. fi/evals/llm/providers/litellm.py +70 -0
  356. fi/evals/local/__init__.py +90 -0
  357. fi/evals/local/evaluator.py +690 -0
  358. fi/evals/local/execution_mode.py +121 -0
  359. fi/evals/local/llm.py +489 -0
  360. fi/evals/local/metrics/__init__.py +19 -0
  361. fi/evals/local/registry.py +360 -0
  362. fi/evals/manager.py +1018 -0
  363. fi/evals/manager_types.py +362 -0
  364. fi/evals/metrics/__init__.py +185 -0
  365. fi/evals/metrics/agents/__init__.py +74 -0
  366. fi/evals/metrics/agents/metrics.py +693 -0
  367. fi/evals/metrics/agents/report.py +36463 -0
  368. fi/evals/metrics/agents/types.py +160 -0
  369. fi/evals/metrics/base_llm_metric.py +111 -0
  370. fi/evals/metrics/base_metric.py +138 -0
  371. fi/evals/metrics/code_security/__init__.py +305 -0
  372. fi/evals/metrics/code_security/analyzer.py +985 -0
  373. fi/evals/metrics/code_security/benchmarks/__init__.py +73 -0
  374. fi/evals/metrics/code_security/benchmarks/builtin.py +750 -0
  375. fi/evals/metrics/code_security/benchmarks/loader.py +580 -0
  376. fi/evals/metrics/code_security/benchmarks/types.py +308 -0
  377. fi/evals/metrics/code_security/detectors/__init__.py +186 -0
  378. fi/evals/metrics/code_security/detectors/base.py +394 -0
  379. fi/evals/metrics/code_security/detectors/cryptography.py +345 -0
  380. fi/evals/metrics/code_security/detectors/injection.py +744 -0
  381. fi/evals/metrics/code_security/detectors/secrets.py +287 -0
  382. fi/evals/metrics/code_security/detectors/serialization.py +192 -0
  383. fi/evals/metrics/code_security/joint_metrics.py +588 -0
  384. fi/evals/metrics/code_security/judges/__init__.py +83 -0
  385. fi/evals/metrics/code_security/judges/base.py +238 -0
  386. fi/evals/metrics/code_security/judges/dual_judge.py +534 -0
  387. fi/evals/metrics/code_security/judges/llm_judge.py +301 -0
  388. fi/evals/metrics/code_security/judges/pattern_judge.py +515 -0
  389. fi/evals/metrics/code_security/metrics.py +388 -0
  390. fi/evals/metrics/code_security/modes/__init__.py +63 -0
  391. fi/evals/metrics/code_security/modes/adversarial.py +284 -0
  392. fi/evals/metrics/code_security/modes/autocomplete.py +198 -0
  393. fi/evals/metrics/code_security/modes/base.py +283 -0
  394. fi/evals/metrics/code_security/modes/instruct.py +253 -0
  395. fi/evals/metrics/code_security/modes/repair.py +230 -0
  396. fi/evals/metrics/code_security/reports/__init__.py +57 -0
  397. fi/evals/metrics/code_security/reports/generator.py +404 -0
  398. fi/evals/metrics/code_security/reports/leaderboard.py +509 -0
  399. fi/evals/metrics/code_security/types.py +534 -0
  400. fi/evals/metrics/function_calling/__init__.py +34 -0
  401. fi/evals/metrics/function_calling/metrics.py +573 -0
  402. fi/evals/metrics/function_calling/types.py +87 -0
  403. fi/evals/metrics/hallucination/__init__.py +54 -0
  404. fi/evals/metrics/hallucination/detector.py +149 -0
  405. fi/evals/metrics/hallucination/metrics.py +390 -0
  406. fi/evals/metrics/hallucination/nli.py +253 -0
  407. fi/evals/metrics/hallucination/sentinel.py +106 -0
  408. fi/evals/metrics/hallucination/types.py +132 -0
  409. fi/evals/metrics/heuristics/aggregation_metrics.py +85 -0
  410. fi/evals/metrics/heuristics/json_metrics.py +87 -0
  411. fi/evals/metrics/heuristics/similarity_metrics.py +375 -0
  412. fi/evals/metrics/heuristics/string_metrics.py +391 -0
  413. fi/evals/metrics/llm_as_judges/__init__.py +17 -0
  414. fi/evals/metrics/llm_as_judges/custom_judge/metric.py +112 -0
  415. fi/evals/metrics/llm_as_judges/custom_judge/prompts.py +26 -0
  416. fi/evals/metrics/llm_as_judges/types.py +48 -0
  417. fi/evals/metrics/rag/__init__.py +111 -0
  418. fi/evals/metrics/rag/advanced/__init__.py +14 -0
  419. fi/evals/metrics/rag/advanced/multi_hop.py +283 -0
  420. fi/evals/metrics/rag/advanced/source_attribution.py +344 -0
  421. fi/evals/metrics/rag/generation/__init__.py +17 -0
  422. fi/evals/metrics/rag/generation/answer_relevancy.py +176 -0
  423. fi/evals/metrics/rag/generation/context_utilization.py +245 -0
  424. fi/evals/metrics/rag/generation/faithfulness.py +241 -0
  425. fi/evals/metrics/rag/generation/groundedness.py +131 -0
  426. fi/evals/metrics/rag/rag_score.py +277 -0
  427. fi/evals/metrics/rag/retrieval/__init__.py +20 -0
  428. fi/evals/metrics/rag/retrieval/context_entity_recall.py +124 -0
  429. fi/evals/metrics/rag/retrieval/context_precision.py +158 -0
  430. fi/evals/metrics/rag/retrieval/context_recall.py +106 -0
  431. fi/evals/metrics/rag/retrieval/noise_sensitivity.py +163 -0
  432. fi/evals/metrics/rag/retrieval/ranking.py +261 -0
  433. fi/evals/metrics/rag/types.py +100 -0
  434. fi/evals/metrics/rag/utils/__init__.py +62 -0
  435. fi/evals/metrics/rag/utils/claims.py +189 -0
  436. fi/evals/metrics/rag/utils/entities.py +244 -0
  437. fi/evals/metrics/rag/utils/nli.py +92 -0
  438. fi/evals/metrics/rag/utils/similarity.py +345 -0
  439. fi/evals/metrics/structured/__init__.py +114 -0
  440. fi/evals/metrics/structured/field_completeness.py +313 -0
  441. fi/evals/metrics/structured/hierarchy_score.py +366 -0
  442. fi/evals/metrics/structured/json_validation.py +190 -0
  443. fi/evals/metrics/structured/schema_compliance.py +280 -0
  444. fi/evals/metrics/structured/structured_output_score.py +298 -0
  445. fi/evals/metrics/structured/types.py +108 -0
  446. fi/evals/metrics/structured/validators/__init__.py +30 -0
  447. fi/evals/metrics/structured/validators/base.py +189 -0
  448. fi/evals/metrics/structured/validators/json_validator.py +196 -0
  449. fi/evals/metrics/structured/validators/pydantic_validator.py +178 -0
  450. fi/evals/metrics/structured/validators/yaml_validator.py +248 -0
  451. fi/evals/otel/__init__.py +266 -0
  452. fi/evals/otel/config.py +400 -0
  453. fi/evals/otel/conventions.py +463 -0
  454. fi/evals/otel/enrichment.py +371 -0
  455. fi/evals/otel/instrumentors/__init__.py +140 -0
  456. fi/evals/otel/instrumentors/anthropic.py +517 -0
  457. fi/evals/otel/instrumentors/base.py +382 -0
  458. fi/evals/otel/instrumentors/openai.py +673 -0
  459. fi/evals/otel/processors/__init__.py +36 -0
  460. fi/evals/otel/processors/base.py +473 -0
  461. fi/evals/otel/processors/cost.py +445 -0
  462. fi/evals/otel/processors/evaluation.py +559 -0
  463. fi/evals/otel/processors/llm.py +462 -0
  464. fi/evals/otel/tracer.py +506 -0
  465. fi/evals/otel/types.py +232 -0
  466. fi/evals/otel_utils.py +23 -0
  467. fi/evals/protect.py +671 -0
  468. fi/evals/protect_input_adapter.py +154 -0
  469. fi/evals/streaming/__init__.py +88 -0
  470. fi/evals/streaming/buffer.py +213 -0
  471. fi/evals/streaming/evaluator.py +551 -0
  472. fi/evals/streaming/policy.py +307 -0
  473. fi/evals/streaming/scorers.py +368 -0
  474. fi/evals/streaming/types.py +238 -0
  475. fi/evals/templates.py +472 -0
  476. fi/evals/types.py +156 -0
  477. fi/opt/__init__.py +221 -0
  478. fi/opt/_objective_scoring.py +85 -0
  479. fi/opt/base/__init__.py +11 -0
  480. fi/opt/base/base_generator.py +33 -0
  481. fi/opt/base/base_mapper.py +26 -0
  482. fi/opt/base/base_optimizer.py +45 -0
  483. fi/opt/base/evaluator.py +211 -0
  484. fi/opt/components.py +3095 -0
  485. fi/opt/datamappers/__init__.py +3 -0
  486. fi/opt/datamappers/basic_mapper.py +40 -0
  487. fi/opt/deployment.py +1021 -0
  488. fi/opt/evidence.py +4332 -0
  489. fi/opt/generators/__init__.py +3 -0
  490. fi/opt/generators/litellm.py +66 -0
  491. fi/opt/integrations/__init__.py +23 -0
  492. fi/opt/integrations/generative_suite.py +410 -0
  493. fi/opt/integrations/simulate.py +1313 -0
  494. fi/opt/mutations.py +771 -0
  495. fi/opt/observability.py +4639 -0
  496. fi/opt/optimizer_trace.py +889 -0
  497. fi/opt/optimizers/__init__.py +80 -0
  498. fi/opt/optimizers/agent.py +331 -0
  499. fi/opt/optimizers/agent_bandit.py +392 -0
  500. fi/opt/optimizers/agent_curriculum.py +635 -0
  501. fi/opt/optimizers/agent_evolution.py +894 -0
  502. fi/opt/optimizers/agent_feedback.py +1863 -0
  503. fi/opt/optimizers/agent_pareto.py +547 -0
  504. fi/opt/optimizers/agent_social_memory.py +1113 -0
  505. fi/opt/optimizers/agent_tpe.py +321 -0
  506. fi/opt/optimizers/bayesian_search.py +449 -0
  507. fi/opt/optimizers/council.py +2075 -0
  508. fi/opt/optimizers/futureagi_replay.py +799 -0
  509. fi/opt/optimizers/gepa.py +322 -0
  510. fi/opt/optimizers/metaprompt.py +243 -0
  511. fi/opt/optimizers/promptwizard.py +417 -0
  512. fi/opt/optimizers/protegi.py +329 -0
  513. fi/opt/optimizers/random_search.py +224 -0
  514. fi/opt/research.py +518 -0
  515. fi/opt/simulation.py +260 -0
  516. fi/opt/targets.py +232 -0
  517. fi/opt/types.py +66 -0
  518. fi/opt/utils/__init__.py +4 -0
  519. fi/opt/utils/early_stopping.py +266 -0
  520. fi/opt/utils/setup_logging.py +82 -0
  521. fi/simulate/__init__.py +540 -0
  522. fi/simulate/_hashing.py +35 -0
  523. fi/simulate/_logging.py +10 -0
  524. fi/simulate/adapters.py +87 -0
  525. fi/simulate/agent/__init__.py +120 -0
  526. fi/simulate/agent/browser.py +658 -0
  527. fi/simulate/agent/definition.py +587 -0
  528. fi/simulate/agent/frameworks.py +3528 -0
  529. fi/simulate/agent/generic.py +8286 -0
  530. fi/simulate/agent/import_probe.py +227 -0
  531. fi/simulate/agent/memory.py +905 -0
  532. fi/simulate/agent/mocks.py +101 -0
  533. fi/simulate/agent/multi_agent.py +361 -0
  534. fi/simulate/agent/orchestration.py +903 -0
  535. fi/simulate/agent/realtime.py +665 -0
  536. fi/simulate/agent/wrapper.py +99 -0
  537. fi/simulate/agent/wrappers/__init__.py +18 -0
  538. fi/simulate/agent/wrappers/anthropic.py +62 -0
  539. fi/simulate/agent/wrappers/gemini.py +65 -0
  540. fi/simulate/agent/wrappers/http.py +404 -0
  541. fi/simulate/agent/wrappers/langchain.py +80 -0
  542. fi/simulate/agent/wrappers/openai.py +75 -0
  543. fi/simulate/agent/wrappers/websocket.py +326 -0
  544. fi/simulate/artifacts/__init__.py +11 -0
  545. fi/simulate/artifacts/manifest.py +62 -0
  546. fi/simulate/cli.py +20560 -0
  547. fi/simulate/endpoints/__init__.py +45 -0
  548. fi/simulate/endpoints/_http_actor.py +73 -0
  549. fi/simulate/endpoints/actor_sources.py +243 -0
  550. fi/simulate/endpoints/base.py +107 -0
  551. fi/simulate/endpoints/builtins.py +10 -0
  552. fi/simulate/endpoints/callable.py +95 -0
  553. fi/simulate/endpoints/http.py +76 -0
  554. fi/simulate/endpoints/livekit.py +138 -0
  555. fi/simulate/endpoints/originators.py +132 -0
  556. fi/simulate/endpoints/profiles.py +348 -0
  557. fi/simulate/endpoints/retell.py +633 -0
  558. fi/simulate/endpoints/vapi.py +205 -0
  559. fi/simulate/endpoints/websocket.py +76 -0
  560. fi/simulate/environment.py +33026 -0
  561. fi/simulate/environments/__init__.py +11 -0
  562. fi/simulate/environments/base.py +73 -0
  563. fi/simulate/environments/chat.py +697 -0
  564. fi/simulate/environments/voice.py +212 -0
  565. fi/simulate/evaluation/__init__.py +4 -0
  566. fi/simulate/evaluation/ai_eval.py +227 -0
  567. fi/simulate/evidence/__init__.py +35 -0
  568. fi/simulate/evidence/base.py +59 -0
  569. fi/simulate/evidence/caller_observed.py +50 -0
  570. fi/simulate/evidence/livekit_instrumentation.py +51 -0
  571. fi/simulate/evidence/livekit_room.py +50 -0
  572. fi/simulate/evidence/otel.py +49 -0
  573. fi/simulate/evidence/providers/__init__.py +24 -0
  574. fi/simulate/evidence/providers/base.py +61 -0
  575. fi/simulate/evidence/providers/retell.py +376 -0
  576. fi/simulate/evidence/providers/vapi.py +426 -0
  577. fi/simulate/hosted/__init__.py +32 -0
  578. fi/simulate/hosted/child_entrypoint.py +306 -0
  579. fi/simulate/hosted/job.py +150 -0
  580. fi/simulate/hosted/targets.py +53 -0
  581. fi/simulate/instrumentation/__init__.py +5 -0
  582. fi/simulate/instrumentation/livekit/__init__.py +122 -0
  583. fi/simulate/manifest.py +1033 -0
  584. fi/simulate/matrix_cli.py +165 -0
  585. fi/simulate/realtime/__init__.py +40 -0
  586. fi/simulate/realtime/events.py +107 -0
  587. fi/simulate/realtime/media.py +61 -0
  588. fi/simulate/realtime/session.py +91 -0
  589. fi/simulate/recording/__init__.py +5 -0
  590. fi/simulate/recording/room_recorder.py +326 -0
  591. fi/simulate/registry.py +185 -0
  592. fi/simulate/results/__init__.py +9 -0
  593. fi/simulate/results/base.py +18 -0
  594. fi/simulate/results/filesystem.py +71 -0
  595. fi/simulate/results/futureagi.py +1340 -0
  596. fi/simulate/runtime/__init__.py +85 -0
  597. fi/simulate/runtime/capabilities.py +40 -0
  598. fi/simulate/runtime/events.py +63 -0
  599. fi/simulate/runtime/failures.py +25 -0
  600. fi/simulate/runtime/ids.py +34 -0
  601. fi/simulate/runtime/plan.py +70 -0
  602. fi/simulate/runtime/planner.py +102 -0
  603. fi/simulate/runtime/report.py +174 -0
  604. fi/simulate/runtime/run.py +75 -0
  605. fi/simulate/runtime/runner.py +333 -0
  606. fi/simulate/runtime/spec.py +186 -0
  607. fi/simulate/simulation/__init__.py +30 -0
  608. fi/simulate/simulation/behavior_policy.py +425 -0
  609. fi/simulate/simulation/bridge/__init__.py +9 -0
  610. fi/simulate/simulation/bridge/audio.py +29 -0
  611. fi/simulate/simulation/bridge/connector.py +46 -0
  612. fi/simulate/simulation/bridge/livekit.py +252 -0
  613. fi/simulate/simulation/bridge/retell.py +188 -0
  614. fi/simulate/simulation/bridge/vapi.py +177 -0
  615. fi/simulate/simulation/contract.py +419 -0
  616. fi/simulate/simulation/engines/__init__.py +12 -0
  617. fi/simulate/simulation/engines/base.py +21 -0
  618. fi/simulate/simulation/engines/cloud.py +517 -0
  619. fi/simulate/simulation/engines/livekit.py +4167 -0
  620. fi/simulate/simulation/engines/local_text.py +89 -0
  621. fi/simulate/simulation/fidelity.py +374 -0
  622. fi/simulate/simulation/gemini_tts_stream.py +110 -0
  623. fi/simulate/simulation/generator.py +91 -0
  624. fi/simulate/simulation/goal_machine.py +185 -0
  625. fi/simulate/simulation/livekit_models.py +467 -0
  626. fi/simulate/simulation/matrix.py +170 -0
  627. fi/simulate/simulation/models.py +279 -0
  628. fi/simulate/simulation/runner.py +153 -0
  629. fi/simulate/simulation/synthetic.py +880 -0
  630. fi/simulate/simulation/voice_prompt.py +502 -0
  631. fi/simulate/simulator/__init__.py +55 -0
  632. fi/simulate/simulator/builtins.py +53 -0
  633. fi/simulate/suite.py +1288 -0
  634. fi/simulate/utils/routes.py +164 -0
  635. fi/simulate/voice.py +225 -0
  636. fi/simulate/voice_cli.py +182 -0
  637. fi/utils/__init__.py +1 -0
  638. fi/utils/constants.py +14 -0
  639. fi/utils/errors.py +200 -0
  640. fi/utils/executor.py +26 -0
  641. fi/utils/routes.py +119 -0
  642. 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})"