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,649 @@
1
+ """
2
+ Topic Restriction Scanner for Guardrails.
3
+
4
+ Restricts conversations to allowed topics and detects off-topic content.
5
+ Supports both keyword-based and semantic embedding-based detection.
6
+ """
7
+
8
+ import re
9
+ import time
10
+ from typing import Any, Dict, List, Optional, Set
11
+
12
+ from fi.evals.guardrails.scanners.base import (
13
+ BaseScanner,
14
+ ScanResult,
15
+ ScanMatch,
16
+ ScannerAction,
17
+ register_scanner,
18
+ )
19
+
20
+
21
+ # Topic descriptions for semantic embedding matching
22
+ TOPIC_DESCRIPTIONS: Dict[str, str] = {
23
+ "politics": "Political discussions about elections, voting, government, political parties, politicians, legislation, and policy debates",
24
+ "religion": "Religious discussions about faith, worship, God, spiritual beliefs, churches, mosques, temples, and religious texts",
25
+ "violence": "Violent content including weapons, attacks, murder, assault, warfare, fighting, and physical harm",
26
+ "drugs": "Drug-related content about narcotics, illegal substances, drug use, addiction, and trafficking",
27
+ "adult_content": "Adult or sexual content including pornography, explicit material, and NSFW topics",
28
+ "gambling": "Gambling discussions about casinos, betting, poker, lottery, and wagering",
29
+ "medical_advice": "Medical advice about diagnoses, treatments, medications, symptoms, and health conditions",
30
+ "financial_advice": "Financial advice about investments, stocks, trading, cryptocurrency, and portfolio management",
31
+ "legal_advice": "Legal advice about lawsuits, attorneys, court cases, litigation, and legal proceedings",
32
+ "customer_support": "Customer support topics like orders, shipping, refunds, account issues, and billing questions",
33
+ "product_info": "Product information about features, specifications, pricing, availability, and warranties",
34
+ "technical_support": "Technical support for software errors, bugs, installation, configuration, and troubleshooting",
35
+ "general_knowledge": "General knowledge questions about facts, history, science, geography, and explanations",
36
+ }
37
+
38
+ # Predefined topic keywords for common restrictions
39
+ TOPIC_KEYWORDS: Dict[str, Set[str]] = {
40
+ # Sensitive topics often restricted
41
+ "politics": {
42
+ "election", "vote", "democrat", "republican", "liberal", "conservative",
43
+ "president", "congress", "senate", "parliament", "politician", "government",
44
+ "left-wing", "right-wing", "campaign", "ballot", "trump", "biden", "party",
45
+ },
46
+ "religion": {
47
+ "god", "jesus", "allah", "buddha", "church", "mosque", "temple", "prayer",
48
+ "bible", "quran", "torah", "christian", "muslim", "jewish", "hindu",
49
+ "atheist", "agnostic", "faith", "worship", "salvation", "sin", "heaven", "hell",
50
+ },
51
+ "violence": {
52
+ "kill", "murder", "attack", "assault", "weapon", "gun", "bomb", "terrorist",
53
+ "violence", "violent", "hurt", "harm", "blood", "death", "dead", "shoot",
54
+ "stab", "fight", "war", "battle", "combat",
55
+ },
56
+ "drugs": {
57
+ "cocaine", "heroin", "marijuana", "cannabis", "weed", "meth", "lsd", "mdma",
58
+ "drug", "narcotic", "overdose", "addiction", "dealer", "cartel", "trafficking",
59
+ },
60
+ "adult_content": {
61
+ "sex", "porn", "nude", "naked", "erotic", "explicit", "xxx", "nsfw",
62
+ "fetish", "intimate", "sexual", "genitals",
63
+ },
64
+ "gambling": {
65
+ "casino", "bet", "betting", "gamble", "gambling", "poker", "blackjack",
66
+ "slot", "lottery", "wager", "odds", "bookie", "sportsbook",
67
+ },
68
+ "medical_advice": {
69
+ "diagnosis", "treatment", "medication", "prescription", "dosage", "symptom",
70
+ "disease", "illness", "cure", "therapy", "doctor", "patient",
71
+ },
72
+ "financial_advice": {
73
+ "invest", "investment", "stock", "bond", "portfolio", "trading", "forex",
74
+ "crypto", "bitcoin", "dividend", "retirement", "pension",
75
+ },
76
+ "legal_advice": {
77
+ "lawsuit", "attorney", "lawyer", "court", "judge", "verdict", "settlement",
78
+ "litigation", "defendant", "plaintiff", "legal", "illegal",
79
+ },
80
+
81
+ # Common allowed topics
82
+ "customer_support": {
83
+ "order", "shipping", "delivery", "refund", "return", "exchange", "tracking",
84
+ "account", "password", "login", "subscription", "billing", "payment",
85
+ "help", "support", "issue", "problem", "question",
86
+ },
87
+ "product_info": {
88
+ "product", "feature", "specification", "price", "availability", "warranty",
89
+ "size", "color", "model", "version", "compatible",
90
+ },
91
+ "technical_support": {
92
+ "error", "bug", "crash", "install", "update", "download", "configure",
93
+ "setup", "troubleshoot", "debug", "fix", "issue", "problem",
94
+ },
95
+ "general_knowledge": {
96
+ "what", "how", "why", "when", "where", "who", "explain", "describe",
97
+ "define", "meaning", "history", "science", "math", "geography",
98
+ },
99
+ }
100
+
101
+
102
+ @register_scanner("topics")
103
+ class TopicRestrictionScanner(BaseScanner):
104
+ """
105
+ Scanner for topic restriction and off-topic detection.
106
+
107
+ Supports two detection modes:
108
+ - Keyword-based: Fast pattern matching (default)
109
+ - Semantic: Embedding-based similarity matching
110
+
111
+ Restricts conversations to allowed topics or blocks denied topics.
112
+
113
+ Usage:
114
+ # Keyword-based (fast, no dependencies)
115
+ scanner = TopicRestrictionScanner(
116
+ allowed_topics=["customer_support", "product_info"],
117
+ )
118
+
119
+ # Semantic embedding-based (requires sentence-transformers)
120
+ scanner = TopicRestrictionScanner(
121
+ denied_topics=["politics", "religion"],
122
+ use_embeddings=True,
123
+ )
124
+
125
+ # Hybrid mode - combines both approaches
126
+ scanner = TopicRestrictionScanner(
127
+ allowed_topics=["customer_support"],
128
+ use_embeddings=True,
129
+ combine_scores=True,
130
+ )
131
+
132
+ # Custom topic descriptions for semantic matching
133
+ scanner = TopicRestrictionScanner(
134
+ custom_topic_descriptions={
135
+ "insurance_claims": "Insurance claim processing, policy coverage, claim status",
136
+ },
137
+ allowed_topics=["insurance_claims"],
138
+ use_embeddings=True,
139
+ )
140
+
141
+ result = scanner.scan("Who should I vote for in the election?")
142
+ if not result.passed:
143
+ print(f"Off-topic: {result.metadata.get('detected_topics')}")
144
+ """
145
+
146
+ name = "topics"
147
+ category = "topic_restriction"
148
+ description = "Restricts conversations to allowed topics"
149
+ default_action = ScannerAction.FLAG
150
+
151
+ # Default embedding model
152
+ DEFAULT_EMBEDDING_MODEL = "all-MiniLM-L6-v2"
153
+
154
+ def __init__(
155
+ self,
156
+ action: Optional[ScannerAction] = None,
157
+ enabled: bool = True,
158
+ threshold: float = 0.5,
159
+ allowed_topics: Optional[List[str]] = None,
160
+ denied_topics: Optional[List[str]] = None,
161
+ custom_topics: Optional[Dict[str, Set[str]]] = None,
162
+ custom_topic_descriptions: Optional[Dict[str, str]] = None,
163
+ min_keyword_matches: int = 2,
164
+ case_sensitive: bool = False,
165
+ use_embeddings: bool = False,
166
+ embedding_model: Optional[str] = None,
167
+ combine_scores: bool = True,
168
+ embedding_weight: float = 0.6,
169
+ keyword_weight: float = 0.4,
170
+ semantic_threshold: float = 0.5,
171
+ device: Optional[str] = None,
172
+ ):
173
+ """
174
+ Initialize topic restriction scanner.
175
+
176
+ Args:
177
+ action: Action on detection
178
+ enabled: Whether scanner is enabled
179
+ threshold: Confidence threshold (based on keyword match ratio)
180
+ allowed_topics: List of allowed topic names (whitelist mode)
181
+ denied_topics: List of denied topic names (blacklist mode)
182
+ custom_topics: Custom topic definitions {topic_name: {keywords}}
183
+ custom_topic_descriptions: Custom descriptions for semantic matching
184
+ min_keyword_matches: Minimum keyword matches to detect a topic
185
+ case_sensitive: Whether matching is case-sensitive
186
+ use_embeddings: Enable semantic embedding-based detection
187
+ embedding_model: Model name for embeddings (default: all-MiniLM-L6-v2)
188
+ combine_scores: Combine keyword and embedding scores (hybrid mode)
189
+ embedding_weight: Weight for embedding score in combined mode
190
+ keyword_weight: Weight for keyword score in combined mode
191
+ semantic_threshold: Similarity threshold for semantic matching
192
+ device: Device for embedding model ('cpu', 'cuda', 'mps', or None)
193
+ """
194
+ super().__init__(action, enabled)
195
+ self.threshold = threshold
196
+ self.allowed_topics = set(allowed_topics) if allowed_topics else None
197
+ self.denied_topics = set(denied_topics) if denied_topics else None
198
+ self.min_keyword_matches = min_keyword_matches
199
+ self.case_sensitive = case_sensitive
200
+
201
+ # Embedding settings
202
+ self.use_embeddings = use_embeddings
203
+ self.embedding_model_name = embedding_model or self.DEFAULT_EMBEDDING_MODEL
204
+ self.combine_scores = combine_scores
205
+ self.embedding_weight = embedding_weight
206
+ self.keyword_weight = keyword_weight
207
+ self.semantic_threshold = semantic_threshold
208
+ self.device = device
209
+
210
+ # Build topic dictionaries
211
+ self.topics = TOPIC_KEYWORDS.copy()
212
+ if custom_topics:
213
+ self.topics.update(custom_topics)
214
+
215
+ self.topic_descriptions = TOPIC_DESCRIPTIONS.copy()
216
+ if custom_topic_descriptions:
217
+ self.topic_descriptions.update(custom_topic_descriptions)
218
+
219
+ # Compile patterns for keyword matching
220
+ self._topic_patterns: Dict[str, List[re.Pattern]] = {}
221
+ for topic, keywords in self.topics.items():
222
+ flags = 0 if case_sensitive else re.IGNORECASE
223
+ patterns = [
224
+ re.compile(r'\b' + re.escape(kw) + r'\b', flags)
225
+ for kw in keywords
226
+ ]
227
+ self._topic_patterns[topic] = patterns
228
+
229
+ # Lazy-loaded embedding components
230
+ self._embedding_model: Optional[Any] = None
231
+ self._topic_embeddings: Optional[Dict[str, Any]] = None
232
+ self._embeddings_available = False
233
+ self._embeddings_load_error: Optional[str] = None
234
+
235
+ # Pre-load embeddings if requested
236
+ if use_embeddings:
237
+ self._load_embedding_model()
238
+
239
+ def _load_embedding_model(self) -> bool:
240
+ """
241
+ Lazy load the embedding model and compute topic embeddings.
242
+
243
+ Returns:
244
+ True if model loaded successfully, False otherwise
245
+ """
246
+ if self._embedding_model is not None:
247
+ return self._embeddings_available
248
+
249
+ try:
250
+ from sentence_transformers import SentenceTransformer
251
+
252
+ # Determine device
253
+ device = self.device
254
+ if device is None:
255
+ try:
256
+ import torch
257
+ if torch.cuda.is_available():
258
+ device = "cuda"
259
+ elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
260
+ device = "mps"
261
+ else:
262
+ device = "cpu"
263
+ except ImportError:
264
+ device = "cpu"
265
+
266
+ # Load embedding model
267
+ self._embedding_model = SentenceTransformer(
268
+ self.embedding_model_name,
269
+ device=device,
270
+ )
271
+ self._device = device
272
+
273
+ # Pre-compute topic embeddings
274
+ self._compute_topic_embeddings()
275
+
276
+ self._embeddings_available = True
277
+ return True
278
+
279
+ except ImportError as e:
280
+ self._embeddings_load_error = f"sentence-transformers not installed: {e}"
281
+ self._embeddings_available = False
282
+ return False
283
+ except Exception as e:
284
+ self._embeddings_load_error = f"Failed to load embedding model: {e}"
285
+ self._embeddings_available = False
286
+ return False
287
+
288
+ def _compute_topic_embeddings(self) -> None:
289
+ """Pre-compute embeddings for all topic descriptions."""
290
+ if self._embedding_model is None:
291
+ return
292
+
293
+ self._topic_embeddings = {}
294
+
295
+ # Determine which topics to compute embeddings for
296
+ relevant_topics = set()
297
+ if self.allowed_topics:
298
+ relevant_topics.update(self.allowed_topics)
299
+ if self.denied_topics:
300
+ relevant_topics.update(self.denied_topics)
301
+ if not relevant_topics:
302
+ # Compute for all topics if no specific restriction
303
+ relevant_topics = set(self.topic_descriptions.keys())
304
+
305
+ for topic in relevant_topics:
306
+ if topic in self.topic_descriptions:
307
+ description = self.topic_descriptions[topic]
308
+ else:
309
+ # Fallback to keywords as description
310
+ keywords = self.topics.get(topic, set())
311
+ description = " ".join(keywords) if keywords else topic
312
+
313
+ embedding = self._embedding_model.encode(
314
+ description,
315
+ convert_to_tensor=True,
316
+ normalize_embeddings=True,
317
+ )
318
+ self._topic_embeddings[topic] = embedding
319
+
320
+ def _semantic_similarity(
321
+ self, content: str
322
+ ) -> Dict[str, Dict[str, Any]]:
323
+ """
324
+ Compute semantic similarity between content and topic descriptions.
325
+
326
+ Args:
327
+ content: Text to analyze
328
+
329
+ Returns:
330
+ Dict of {topic: {similarity, confidence}}
331
+ """
332
+ if not self._embeddings_available or self._embedding_model is None:
333
+ return {}
334
+
335
+ try:
336
+ from sentence_transformers import util
337
+
338
+ # Encode content
339
+ content_embedding = self._embedding_model.encode(
340
+ content,
341
+ convert_to_tensor=True,
342
+ normalize_embeddings=True,
343
+ )
344
+
345
+ results = {}
346
+ for topic, topic_embedding in self._topic_embeddings.items():
347
+ # Compute cosine similarity
348
+ similarity = util.cos_sim(content_embedding, topic_embedding).item()
349
+
350
+ # Normalize to 0-1 range (cosine similarity can be negative)
351
+ confidence = max(0.0, (similarity + 1.0) / 2.0)
352
+
353
+ results[topic] = {
354
+ "similarity": similarity,
355
+ "confidence": confidence,
356
+ "method": "semantic",
357
+ }
358
+
359
+ return results
360
+
361
+ except Exception:
362
+ # Return empty on error
363
+ return {}
364
+
365
+ def _detect_topics_keywords(self, text: str) -> Dict[str, Dict]:
366
+ """
367
+ Detect topics in text based on keyword matching.
368
+
369
+ Returns:
370
+ Dict of {topic: {count, keywords, confidence, method}}
371
+ """
372
+ detected = {}
373
+
374
+ for topic, patterns in self._topic_patterns.items():
375
+ matched_keywords = []
376
+ for pattern in patterns:
377
+ matches = pattern.findall(text)
378
+ if matches:
379
+ matched_keywords.extend(matches)
380
+
381
+ if len(matched_keywords) >= self.min_keyword_matches:
382
+ # Calculate confidence based on keyword density
383
+ total_keywords = len(self.topics[topic])
384
+ unique_matches = len(set(kw.lower() for kw in matched_keywords))
385
+ confidence = min(unique_matches / total_keywords * 2, 1.0)
386
+
387
+ detected[topic] = {
388
+ "count": len(matched_keywords),
389
+ "unique_count": unique_matches,
390
+ "keywords": list(set(matched_keywords)),
391
+ "confidence": confidence,
392
+ "method": "keyword",
393
+ }
394
+
395
+ return detected
396
+
397
+ def _detect_topics(self, text: str) -> Dict[str, Dict]:
398
+ """
399
+ Detect topics using configured method(s).
400
+
401
+ Returns:
402
+ Dict of {topic: {confidence, method, ...}}
403
+ """
404
+ # Get keyword-based detection
405
+ keyword_results = self._detect_topics_keywords(text)
406
+
407
+ # Get semantic detection if enabled
408
+ semantic_results = {}
409
+ if self.use_embeddings and self._embeddings_available:
410
+ semantic_results = self._semantic_similarity(text)
411
+
412
+ # Combine results
413
+ if not self.use_embeddings or not self._embeddings_available:
414
+ return keyword_results
415
+
416
+ if not self.combine_scores:
417
+ # Semantic-only mode (filter by threshold)
418
+ return {
419
+ topic: info
420
+ for topic, info in semantic_results.items()
421
+ if info["confidence"] >= self.semantic_threshold
422
+ }
423
+
424
+ # Hybrid mode: combine scores
425
+ all_topics = set(keyword_results.keys()) | set(semantic_results.keys())
426
+ combined = {}
427
+
428
+ for topic in all_topics:
429
+ kw_info = keyword_results.get(topic, {})
430
+ sem_info = semantic_results.get(topic, {})
431
+
432
+ kw_conf = kw_info.get("confidence", 0.0)
433
+ sem_conf = sem_info.get("confidence", 0.0)
434
+
435
+ # Weighted combination
436
+ combined_confidence = (
437
+ self.keyword_weight * kw_conf +
438
+ self.embedding_weight * sem_conf
439
+ )
440
+
441
+ combined[topic] = {
442
+ "confidence": combined_confidence,
443
+ "keyword_confidence": kw_conf,
444
+ "semantic_confidence": sem_conf,
445
+ "method": "hybrid",
446
+ }
447
+
448
+ # Include keyword details if available
449
+ if kw_info:
450
+ combined[topic]["keywords"] = kw_info.get("keywords", [])
451
+ combined[topic]["count"] = kw_info.get("count", 0)
452
+
453
+ # Include semantic similarity if available
454
+ if sem_info:
455
+ combined[topic]["similarity"] = sem_info.get("similarity", 0.0)
456
+
457
+ return combined
458
+
459
+ def scan(self, content: str, context: Optional[str] = None) -> ScanResult:
460
+ """
461
+ Scan content for topic violations.
462
+
463
+ Uses keyword matching, semantic similarity, or both depending on config.
464
+
465
+ Args:
466
+ content: Content to scan
467
+ context: Optional context
468
+
469
+ Returns:
470
+ ScanResult with topic detection details
471
+ """
472
+ start = time.perf_counter()
473
+ matches = []
474
+ issues = []
475
+ metadata: Dict[str, Any] = {}
476
+
477
+ # Record detection mode
478
+ if self.use_embeddings and self._embeddings_available:
479
+ metadata["detection_mode"] = "hybrid" if self.combine_scores else "semantic"
480
+ metadata["embedding_model"] = self.embedding_model_name
481
+ else:
482
+ metadata["detection_mode"] = "keyword"
483
+ if self.use_embeddings and not self._embeddings_available:
484
+ metadata["embedding_error"] = self._embeddings_load_error
485
+
486
+ # Detect topics
487
+ detected_topics = self._detect_topics(content)
488
+
489
+ # Also check context if provided
490
+ if context:
491
+ context_topics = self._detect_topics(context)
492
+ for topic, info in context_topics.items():
493
+ if topic in detected_topics:
494
+ # Merge: take max confidence
495
+ if info["confidence"] > detected_topics[topic]["confidence"]:
496
+ detected_topics[topic] = info
497
+ else:
498
+ detected_topics[topic] = info
499
+
500
+ # Check topic restrictions
501
+ violation = False
502
+
503
+ # Whitelist mode: only allowed topics are permitted
504
+ if self.allowed_topics:
505
+ for topic, info in detected_topics.items():
506
+ if info["confidence"] >= self.threshold:
507
+ if topic not in self.allowed_topics:
508
+ matches.append(ScanMatch(
509
+ pattern_name="off_topic",
510
+ matched_text=f"Topic: {topic}",
511
+ start=0,
512
+ end=len(content),
513
+ confidence=info["confidence"],
514
+ metadata={
515
+ "topic": topic,
516
+ "keywords": info.get("keywords", []),
517
+ "method": info.get("method", "unknown"),
518
+ },
519
+ ))
520
+ issues.append(f"Off-topic: {topic}")
521
+ violation = True
522
+
523
+ # Also flag if no allowed topic was detected
524
+ allowed_detected = any(
525
+ topic in self.allowed_topics and info["confidence"] >= self.threshold
526
+ for topic, info in detected_topics.items()
527
+ )
528
+ if not allowed_detected and not violation and detected_topics:
529
+ # Content doesn't match any allowed topic
530
+ matches.append(ScanMatch(
531
+ pattern_name="no_allowed_topic",
532
+ matched_text="No allowed topic detected",
533
+ start=0,
534
+ end=len(content),
535
+ confidence=0.6,
536
+ ))
537
+ issues.append("No allowed topic detected")
538
+ violation = True
539
+
540
+ # Blacklist mode: denied topics are blocked
541
+ if self.denied_topics:
542
+ for topic, info in detected_topics.items():
543
+ if topic in self.denied_topics and info["confidence"] >= self.threshold:
544
+ matches.append(ScanMatch(
545
+ pattern_name="denied_topic",
546
+ matched_text=f"Topic: {topic}",
547
+ start=0,
548
+ end=len(content),
549
+ confidence=info["confidence"],
550
+ metadata={
551
+ "topic": topic,
552
+ "keywords": info.get("keywords", []),
553
+ "method": info.get("method", "unknown"),
554
+ },
555
+ ))
556
+ issues.append(f"Denied topic: {topic}")
557
+ violation = True
558
+
559
+ latency = (time.perf_counter() - start) * 1000
560
+
561
+ # Determine result
562
+ max_confidence = max([m.confidence for m in matches], default=0.0)
563
+
564
+ metadata["detected_topics"] = {
565
+ k: v for k, v in detected_topics.items()
566
+ if v["confidence"] >= self.threshold
567
+ }
568
+
569
+ if violation:
570
+ return self._create_result(
571
+ passed=False,
572
+ matches=matches,
573
+ score=max_confidence,
574
+ reason="; ".join(issues),
575
+ latency_ms=latency,
576
+ metadata=metadata,
577
+ )
578
+
579
+ return self._create_result(
580
+ passed=True,
581
+ matches=[],
582
+ score=0.0,
583
+ reason="Content is on-topic",
584
+ latency_ms=latency,
585
+ metadata=metadata,
586
+ )
587
+
588
+ @classmethod
589
+ def with_embeddings(
590
+ cls,
591
+ allowed_topics: Optional[List[str]] = None,
592
+ denied_topics: Optional[List[str]] = None,
593
+ embedding_model: Optional[str] = None,
594
+ threshold: float = 0.5,
595
+ **kwargs,
596
+ ) -> "TopicRestrictionScanner":
597
+ """
598
+ Factory method to create an embedding-enabled topic scanner.
599
+
600
+ Args:
601
+ allowed_topics: Allowed topic list
602
+ denied_topics: Denied topic list
603
+ embedding_model: Model to use (defaults to all-MiniLM-L6-v2)
604
+ threshold: Detection threshold
605
+ **kwargs: Additional arguments passed to __init__
606
+
607
+ Returns:
608
+ Configured TopicRestrictionScanner with embeddings enabled
609
+ """
610
+ return cls(
611
+ allowed_topics=allowed_topics,
612
+ denied_topics=denied_topics,
613
+ use_embeddings=True,
614
+ embedding_model=embedding_model,
615
+ threshold=threshold,
616
+ **kwargs,
617
+ )
618
+
619
+ @classmethod
620
+ def semantic_only(
621
+ cls,
622
+ allowed_topics: Optional[List[str]] = None,
623
+ denied_topics: Optional[List[str]] = None,
624
+ embedding_model: Optional[str] = None,
625
+ threshold: float = 0.5,
626
+ **kwargs,
627
+ ) -> "TopicRestrictionScanner":
628
+ """
629
+ Factory method to create a semantic-only topic scanner (no keywords).
630
+
631
+ Args:
632
+ allowed_topics: Allowed topic list
633
+ denied_topics: Denied topic list
634
+ embedding_model: Model to use
635
+ threshold: Detection threshold
636
+ **kwargs: Additional arguments
637
+
638
+ Returns:
639
+ Configured TopicRestrictionScanner using semantic-only detection
640
+ """
641
+ return cls(
642
+ allowed_topics=allowed_topics,
643
+ denied_topics=denied_topics,
644
+ use_embeddings=True,
645
+ combine_scores=False,
646
+ embedding_model=embedding_model,
647
+ threshold=threshold,
648
+ **kwargs,
649
+ )