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
fi/simulate/suite.py ADDED
@@ -0,0 +1,1288 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import copy
5
+ import importlib
6
+ import importlib.util
7
+ import inspect
8
+ import json
9
+ import re
10
+ import time
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+ from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence
14
+
15
+ from .manifest import CLI_SCHEMA_VERSION, ManifestError
16
+
17
+
18
+ EVAL_SUITE_SCHEMA_VERSION = "agent-simulate.eval.v1"
19
+ AGENT_LEARNING_EVAL_SUITE_SCHEMA_VERSION = "agent-learning.eval.v1"
20
+ EVAL_SUITE_OPTIMIZATION_SCHEMA_VERSION = "agent-learning.eval-optimization.v1"
21
+
22
+ _JSON_PATH_EQUALS_ASSERTIONS = {
23
+ "json_path_equals",
24
+ "json_path_equal",
25
+ "json_equals",
26
+ "path_equals",
27
+ "path_equal",
28
+ }
29
+ _JSON_PATH_EXISTS_ASSERTIONS = {
30
+ "json_path_exists",
31
+ "json_exists",
32
+ "path_exists",
33
+ }
34
+ _JSON_PATH_GTE_ASSERTIONS = {
35
+ "json_path_gte",
36
+ "json_path_ge",
37
+ "json_gte",
38
+ "path_gte",
39
+ "greater_than_or_equal",
40
+ }
41
+ _JSON_PATH_LTE_ASSERTIONS = {
42
+ "json_path_lte",
43
+ "json_path_le",
44
+ "json_lte",
45
+ "path_lte",
46
+ "less_than_or_equal",
47
+ }
48
+ _JSON_PATH_CONTAINS_ASSERTIONS = {
49
+ "json_path_contains",
50
+ "json_contains",
51
+ "path_contains",
52
+ }
53
+ _JSON_PATH_NOT_CONTAINS_ASSERTIONS = {
54
+ "json_path_not_contains",
55
+ "json_not_contains",
56
+ "path_not_contains",
57
+ }
58
+ _JSON_PATH_ASSERTIONS = (
59
+ _JSON_PATH_EQUALS_ASSERTIONS
60
+ | _JSON_PATH_EXISTS_ASSERTIONS
61
+ | _JSON_PATH_GTE_ASSERTIONS
62
+ | _JSON_PATH_LTE_ASSERTIONS
63
+ | _JSON_PATH_CONTAINS_ASSERTIONS
64
+ | _JSON_PATH_NOT_CONTAINS_ASSERTIONS
65
+ )
66
+
67
+ # Assertion tokens that score the case output with a hosted FutureAGI eval
68
+ # template via ``fi.evals.evaluate`` (platform/turing engine by default,
69
+ # using FI_API_KEY / FI_SECRET_KEY / FI_BASE_URL). Pass = score >= threshold.
70
+ _FI_EVAL_ASSERTIONS = {
71
+ "fi_eval",
72
+ "fi_evals",
73
+ "platform_eval",
74
+ "platform",
75
+ "turing",
76
+ }
77
+
78
+ # Extra eval-input keys an ``fi_eval`` assertion may template from the case
79
+ # vars before forwarding to the platform (``output`` is always the case output).
80
+ _FI_EVAL_INPUT_KEYS = (
81
+ "input",
82
+ "context",
83
+ "expected",
84
+ "query",
85
+ "reference",
86
+ "instructions",
87
+ "prompt",
88
+ "criteria",
89
+ )
90
+
91
+ # Generative (LLM prompt-rewriting) optimizer tokens. These run the real
92
+ # optimizers in ``fi.opt.optimizers`` through the generative eval-suite bridge
93
+ # rather than the deterministic agent/target search backends.
94
+ _GENERATIVE_OPTIMIZER_TOKENS = {
95
+ "gepa": "gepa",
96
+ "protegi": "protegi",
97
+ "pro_te_gi": "protegi",
98
+ "metaprompt": "metaprompt",
99
+ "meta_prompt": "metaprompt",
100
+ "promptwizard": "promptwizard",
101
+ "prompt_wizard": "promptwizard",
102
+ "random_search": "random_search",
103
+ "random": "random_search",
104
+ "bayesian_search": "bayesian_search",
105
+ "bayesian": "bayesian_search",
106
+ "bayes": "bayesian_search",
107
+ }
108
+
109
+
110
+ def _generative_optimizer_token(optimizer_cfg: Any) -> Optional[str]:
111
+ """Return the canonical generative optimizer token, or None for Family A."""
112
+ if not isinstance(optimizer_cfg, Mapping):
113
+ return None
114
+ raw = (
115
+ optimizer_cfg.get("algorithm")
116
+ or optimizer_cfg.get("type")
117
+ or optimizer_cfg.get("name")
118
+ or optimizer_cfg.get("strategy")
119
+ )
120
+ if not raw:
121
+ return None
122
+ norm = str(raw).strip().lower().replace("-", "_").replace(" ", "_")
123
+ return _GENERATIVE_OPTIMIZER_TOKENS.get(norm)
124
+
125
+
126
+ @dataclass(frozen=True)
127
+ class EvalSuiteOptions:
128
+ name: Optional[str] = None
129
+ threshold: Optional[float] = None
130
+ dry_run: bool = False
131
+
132
+
133
+ @dataclass(frozen=True)
134
+ class EvalSuiteOptimizationOptions:
135
+ name: Optional[str] = None
136
+ threshold: Optional[float] = None
137
+ max_candidates: Optional[int] = None
138
+ dry_run: bool = False
139
+
140
+
141
+ def build_eval_suite_manifest(
142
+ *,
143
+ name: str,
144
+ providers: Optional[Sequence[Mapping[str, Any]]] = None,
145
+ prompts: Optional[Sequence[Mapping[str, Any]]] = None,
146
+ tests: Optional[Sequence[Mapping[str, Any]]] = None,
147
+ threshold: float = 1.0,
148
+ outputs: Optional[Mapping[str, Any]] = None,
149
+ metadata: Optional[Mapping[str, Any]] = None,
150
+ version: str = AGENT_LEARNING_EVAL_SUITE_SCHEMA_VERSION,
151
+ ) -> Dict[str, Any]:
152
+ """Build a promptfoo-style eval suite manifest from SDK data."""
153
+
154
+ if not name:
155
+ raise ValueError("name is required")
156
+ provider_values = _copy_mapping_sequence(
157
+ providers
158
+ if providers is not None
159
+ else (
160
+ {
161
+ "id": "echo",
162
+ "type": "echo",
163
+ },
164
+ ),
165
+ field="providers",
166
+ )
167
+ prompt_values = _copy_mapping_sequence(
168
+ prompts
169
+ if prompts is not None
170
+ else (
171
+ {
172
+ "id": "support-policy-question",
173
+ "template": "{{question}}",
174
+ },
175
+ ),
176
+ field="prompts",
177
+ )
178
+ test_values = _copy_mapping_sequence(
179
+ tests
180
+ if tests is not None
181
+ else (
182
+ {
183
+ "id": "policy-grounding",
184
+ "vars": {"question": "Where is the refund policy?"},
185
+ "assert": [{"type": "contains", "value": "policy"}],
186
+ },
187
+ ),
188
+ field="tests",
189
+ )
190
+ manifest: Dict[str, Any] = {
191
+ "version": str(version),
192
+ "name": str(name),
193
+ "threshold": float(threshold),
194
+ "providers": provider_values,
195
+ "prompts": prompt_values,
196
+ "tests": test_values,
197
+ }
198
+ if outputs:
199
+ manifest["outputs"] = copy.deepcopy(dict(outputs))
200
+ if metadata:
201
+ manifest["metadata"] = copy.deepcopy(dict(metadata))
202
+ return manifest
203
+
204
+
205
+ def write_eval_suite_file(suite: Mapping[str, Any], path: str | Path) -> Path:
206
+ """Write an eval suite manifest as formatted JSON and return the path."""
207
+
208
+ suite_path = Path(path).expanduser().resolve()
209
+ suite_path.parent.mkdir(parents=True, exist_ok=True)
210
+ suite_path.write_text(
211
+ json.dumps(dict(suite), indent=2, sort_keys=True, default=str) + "\n",
212
+ encoding="utf-8",
213
+ )
214
+ return suite_path
215
+
216
+
217
+ def load_eval_suite_file(path: str | Path) -> Dict[str, Any]:
218
+ suite_path = Path(path).expanduser().resolve()
219
+ suite = _load_json_or_yaml(suite_path)
220
+ if not isinstance(suite, Mapping):
221
+ raise ManifestError("eval suite root must be an object")
222
+ return _prepare_eval_suite(dict(suite), base_dir=suite_path.parent)
223
+
224
+
225
+ def run_eval_suite_file(
226
+ path: str | Path,
227
+ *,
228
+ options: Optional[EvalSuiteOptions] = None,
229
+ name: Optional[str] = None,
230
+ threshold: Optional[float] = None,
231
+ dry_run: Optional[bool] = None,
232
+ ) -> Dict[str, Any]:
233
+ suite_path = Path(path).expanduser().resolve()
234
+ suite = load_eval_suite_file(suite_path)
235
+ return run_eval_suite(
236
+ suite,
237
+ suite_path=suite_path,
238
+ options=_merge_eval_suite_options(
239
+ options,
240
+ name=name,
241
+ threshold=threshold,
242
+ dry_run=dry_run,
243
+ ),
244
+ )
245
+
246
+
247
+ def optimize_eval_suite_file(
248
+ path: str | Path,
249
+ *,
250
+ options: Optional[EvalSuiteOptimizationOptions] = None,
251
+ name: Optional[str] = None,
252
+ threshold: Optional[float] = None,
253
+ max_candidates: Optional[int] = None,
254
+ dry_run: Optional[bool] = None,
255
+ ) -> Dict[str, Any]:
256
+ """Load and optimize a promptfoo-style eval suite with Agent Learning."""
257
+
258
+ suite_path = Path(path).expanduser().resolve()
259
+ suite = load_eval_suite_file(suite_path)
260
+ return optimize_eval_suite(
261
+ suite,
262
+ suite_path=suite_path,
263
+ options=_merge_eval_suite_optimization_options(
264
+ options,
265
+ name=name,
266
+ threshold=threshold,
267
+ max_candidates=max_candidates,
268
+ dry_run=dry_run,
269
+ ),
270
+ )
271
+
272
+
273
+ def optimize_eval_suite(
274
+ suite: Mapping[str, Any],
275
+ *,
276
+ suite_path: str | Path = ".",
277
+ options: Optional[EvalSuiteOptimizationOptions] = None,
278
+ name: Optional[str] = None,
279
+ threshold: Optional[float] = None,
280
+ max_candidates: Optional[int] = None,
281
+ dry_run: Optional[bool] = None,
282
+ ) -> Dict[str, Any]:
283
+ """Optimize an in-memory eval suite and return a unified artifact payload."""
284
+
285
+ started = time.time()
286
+ opts = _merge_eval_suite_optimization_options(
287
+ options,
288
+ name=name,
289
+ threshold=threshold,
290
+ max_candidates=max_candidates,
291
+ dry_run=dry_run,
292
+ )
293
+ suite_path = _suite_file_like_path(suite_path)
294
+ runtime_suite = copy.deepcopy(dict(suite))
295
+ if opts.name:
296
+ runtime_suite["name"] = opts.name
297
+ if opts.threshold is not None:
298
+ runtime_suite.setdefault("optimization", {})["threshold"] = opts.threshold
299
+ if opts.max_candidates is not None:
300
+ runtime_suite.setdefault("optimization", {}).setdefault(
301
+ "optimizer", {}
302
+ )["max_candidates"] = opts.max_candidates
303
+
304
+ prepared = _prepare_eval_suite(runtime_suite, base_dir=suite_path.parent)
305
+ cli = _cli()
306
+ optimization = cli._optimization_config(prepared)
307
+ optimizer_config = cli._optimizer_config(optimization)
308
+
309
+ # Generative (LLM prompt-rewriting) optimizers run through their own bridge
310
+ # and don't need a deterministic search-space target, so route them before
311
+ # `_target_config` (which requires one).
312
+ generative_token = _generative_optimizer_token(optimization.get("optimizer"))
313
+ if generative_token and not opts.dry_run:
314
+ try:
315
+ from fi.opt.integrations.generative_suite import (
316
+ optimize_eval_suite_generative,
317
+ )
318
+ except Exception as exc: # pragma: no cover - optional dependency clarity
319
+ raise ManifestError(
320
+ "Agent Learning Kit generative optimizer engine is required for "
321
+ f"the `{generative_token}` optimizer."
322
+ ) from exc
323
+ payload = optimize_eval_suite_generative(
324
+ prepared,
325
+ suite_path=suite_path,
326
+ name=str(prepared.get("name") or suite_path.stem),
327
+ token=generative_token,
328
+ optimizer_config=dict(optimizer_config or {}),
329
+ threshold=float(optimization.get("threshold", 0.5)),
330
+ started=started,
331
+ )
332
+ payload["eval_suite"] = _eval_suite_descriptor(prepared)
333
+ payload.setdefault("summary", {})
334
+ payload["summary"]["provider_count"] = len(_as_list(prepared.get("providers")))
335
+ payload["summary"]["prompt_count"] = len(_as_list(prepared.get("prompts")))
336
+ payload["summary"]["test_count"] = len(_as_list(prepared.get("tests")))
337
+ return payload
338
+
339
+ target_config = cli._target_config(optimization)
340
+ if opts.dry_run:
341
+ return {
342
+ "schema_version": CLI_SCHEMA_VERSION,
343
+ "kind": EVAL_SUITE_OPTIMIZATION_SCHEMA_VERSION,
344
+ "name": str(prepared.get("name") or suite_path.stem),
345
+ "status": "passed",
346
+ "exit_code": 0,
347
+ "dry_run": True,
348
+ "summary": {
349
+ "provider_count": len(_as_list(prepared.get("providers"))),
350
+ "prompt_count": len(_as_list(prepared.get("prompts"))),
351
+ "test_count": len(_as_list(prepared.get("tests"))),
352
+ "search_path_count": len(target_config.get("search_space", {})),
353
+ "max_candidates": optimizer_config.get("max_candidates"),
354
+ },
355
+ "eval_suite": _eval_suite_descriptor(prepared),
356
+ "duration_seconds": round(time.time() - started, 4),
357
+ }
358
+
359
+ try:
360
+ from fi.opt import problem_from_eval_suite
361
+ except Exception as exc: # pragma: no cover - optional dependency clarity
362
+ raise ManifestError(
363
+ "Agent Learning Kit optimizer engine is required for eval-suite optimization."
364
+ ) from exc
365
+
366
+ problem = problem_from_eval_suite(
367
+ prepared,
368
+ suite_path=suite_path,
369
+ name=str(prepared.get("name") or suite_path.stem),
370
+ )
371
+ optimization_result = problem.optimize()
372
+ payload = cli._optimization_result(
373
+ manifest=prepared,
374
+ manifest_path=suite_path,
375
+ optimization_result=optimization_result,
376
+ threshold=float(optimization.get("threshold", 1.0)),
377
+ duration_seconds=round(time.time() - started, 4),
378
+ )
379
+ payload["kind"] = EVAL_SUITE_OPTIMIZATION_SCHEMA_VERSION
380
+ payload["eval_suite"] = _eval_suite_descriptor(prepared)
381
+ payload["summary"]["provider_count"] = len(_as_list(prepared.get("providers")))
382
+ payload["summary"]["prompt_count"] = len(_as_list(prepared.get("prompts")))
383
+ payload["summary"]["test_count"] = len(_as_list(prepared.get("tests")))
384
+ payload["optimization"]["source"] = "eval_suite"
385
+ if "manifest_optimization" in payload["optimization"]:
386
+ artifact = copy.deepcopy(payload["optimization"]["manifest_optimization"])
387
+ artifact["kind"] = "eval_suite_optimization"
388
+ artifact["source"] = "eval_suite"
389
+ payload["optimization"]["eval_suite_optimization"] = artifact
390
+ return payload
391
+
392
+
393
+ def run_eval_suite(
394
+ suite: Mapping[str, Any],
395
+ *,
396
+ suite_path: str | Path = ".",
397
+ options: Optional[EvalSuiteOptions] = None,
398
+ ) -> Dict[str, Any]:
399
+ started = time.time()
400
+ opts = options or EvalSuiteOptions()
401
+ base_dir = Path(suite_path).expanduser().resolve().parent
402
+ prepared = _prepare_eval_suite(dict(suite), base_dir=base_dir)
403
+ name = str(opts.name or prepared.get("name") or "agent-learning-eval")
404
+ threshold = float(opts.threshold if opts.threshold is not None else prepared.get("threshold", 1.0))
405
+ if opts.dry_run:
406
+ return _suite_result(
407
+ name=name,
408
+ suite=prepared,
409
+ cases=[],
410
+ threshold=threshold,
411
+ duration_seconds=round(time.time() - started, 4),
412
+ dry_run=True,
413
+ )
414
+
415
+ cases: List[Dict[str, Any]] = []
416
+ providers = [_as_dict(provider) for provider in _as_list(prepared.get("providers"))]
417
+ prompts = [_as_dict(prompt) for prompt in _as_list(prepared.get("prompts"))]
418
+ tests = [_as_dict(test) for test in _as_list(prepared.get("tests"))]
419
+ for provider in providers:
420
+ for prompt in prompts:
421
+ for test_index, test in enumerate(tests, start=1):
422
+ cases.append(
423
+ _run_eval_case(
424
+ provider=provider,
425
+ prompt=prompt,
426
+ test=test,
427
+ test_index=test_index,
428
+ base_dir=base_dir,
429
+ )
430
+ )
431
+ return _suite_result(
432
+ name=name,
433
+ suite=prepared,
434
+ cases=cases,
435
+ threshold=threshold,
436
+ duration_seconds=round(time.time() - started, 4),
437
+ dry_run=False,
438
+ )
439
+
440
+
441
+ def _prepare_eval_suite(suite: Dict[str, Any], *, base_dir: Path) -> Dict[str, Any]:
442
+ providers = [_as_dict(item) for item in _as_list(suite.get("providers") or suite.get("provider"))]
443
+ prompts = [_as_dict(item) for item in _as_list(suite.get("prompts") or suite.get("prompt"))]
444
+ tests = _suite_tests(suite, base_dir=base_dir)
445
+ if not providers:
446
+ raise ManifestError("eval suite requires at least one provider")
447
+ if not prompts:
448
+ raise ManifestError("eval suite requires at least one prompt")
449
+ if not tests:
450
+ raise ManifestError("eval suite requires at least one test")
451
+ suite["providers"] = [_normalize_provider(item, index) for index, item in enumerate(providers, start=1)]
452
+ suite["prompts"] = [_normalize_prompt(item, index) for index, item in enumerate(prompts, start=1)]
453
+ suite["tests"] = [_normalize_test(item, index) for index, item in enumerate(tests, start=1)]
454
+ suite.pop("tests_file", None)
455
+ suite.pop("data_file", None)
456
+ suite.pop("data", None)
457
+ suite.setdefault("version", EVAL_SUITE_SCHEMA_VERSION)
458
+ return suite
459
+
460
+
461
+ def _suite_tests(suite: Mapping[str, Any], *, base_dir: Path) -> List[Dict[str, Any]]:
462
+ tests_value = suite.get("tests")
463
+ tests_file = suite.get("tests_file") or suite.get("data") or suite.get("data_file")
464
+ records: List[Dict[str, Any]] = []
465
+ if isinstance(tests_value, str):
466
+ records.extend(_load_test_records(base_dir / tests_value))
467
+ else:
468
+ records.extend(_as_dict(item) for item in _as_list(tests_value))
469
+ for path in _as_list(tests_file):
470
+ records.extend(_load_test_records(base_dir / str(path)))
471
+ return records
472
+
473
+
474
+ def _load_test_records(path: Path) -> List[Dict[str, Any]]:
475
+ source = path.expanduser().resolve()
476
+ if not source.exists():
477
+ raise ManifestError(f"eval suite tests file not found: {source}")
478
+ if source.suffix.lower() == ".jsonl":
479
+ records = []
480
+ for line_number, line in enumerate(source.read_text(encoding="utf-8").splitlines(), start=1):
481
+ if not line.strip():
482
+ continue
483
+ try:
484
+ item = json.loads(line)
485
+ except json.JSONDecodeError as exc:
486
+ raise ManifestError(f"invalid JSONL in {source}:{line_number}") from exc
487
+ records.append(_as_dict(item))
488
+ return records
489
+ data = _load_json_or_yaml(source)
490
+ if isinstance(data, Mapping) and "tests" in data:
491
+ return [_as_dict(item) for item in _as_list(data.get("tests"))]
492
+ return [_as_dict(item) for item in _as_list(data)]
493
+
494
+
495
+ def _normalize_provider(provider: Mapping[str, Any], index: int) -> Dict[str, Any]:
496
+ item = dict(provider)
497
+ item["id"] = str(item.get("id") or item.get("name") or f"provider_{index}")
498
+ item["type"] = str(item.get("type") or item.get("kind") or "echo")
499
+ return item
500
+
501
+
502
+ def _normalize_prompt(prompt: Mapping[str, Any], index: int) -> Dict[str, Any]:
503
+ item = dict(prompt)
504
+ item["id"] = str(item.get("id") or item.get("name") or f"prompt_{index}")
505
+ item["template"] = str(item.get("template") or item.get("content") or item.get("prompt") or "")
506
+ if not item["template"]:
507
+ raise ManifestError(f"prompt `{item['id']}` requires a template")
508
+ return item
509
+
510
+
511
+ def _normalize_test(test: Mapping[str, Any], index: int) -> Dict[str, Any]:
512
+ item = dict(test)
513
+ item["id"] = str(item.get("id") or item.get("name") or f"test_{index}")
514
+ item["vars"] = _as_dict(item.get("vars") or item.get("variables"))
515
+ assertions = _as_list(item.get("assert") or item.get("assertions") or item.get("checks"))
516
+ item["assertions"] = [_normalize_assertion(assertion, item["id"], offset) for offset, assertion in enumerate(assertions, start=1)]
517
+ if not item["assertions"]:
518
+ raise ManifestError(f"test `{item['id']}` requires at least one assertion")
519
+ return item
520
+
521
+
522
+ def _normalize_assertion(assertion: Any, test_id: str, index: int) -> Dict[str, Any]:
523
+ if isinstance(assertion, str):
524
+ item = {"type": "contains", "value": assertion}
525
+ else:
526
+ item = _as_dict(assertion)
527
+ item["type"] = str(item.get("type") or item.get("kind") or "contains").lower().replace("-", "_")
528
+ if "path" not in item:
529
+ for alias in ("json_path", "field"):
530
+ if alias in item:
531
+ item["path"] = item.get(alias)
532
+ break
533
+ if "value" not in item and "expected" in item:
534
+ item["value"] = item.get("expected")
535
+ assertion_type = str(item["type"])
536
+ if assertion_type in _JSON_PATH_ASSERTIONS and not item.get("path"):
537
+ raise ManifestError(f"assertion {index} in test `{test_id}` requires a path")
538
+ if assertion_type in _FI_EVAL_ASSERTIONS:
539
+ if not (item.get("eval") or item.get("metric") or item.get("name") or item.get("value")):
540
+ raise ManifestError(
541
+ f"assertion {index} in test `{test_id}` requires an `eval` "
542
+ "(hosted template name)"
543
+ )
544
+ return item
545
+ requires_value = assertion_type not in _JSON_PATH_EXISTS_ASSERTIONS
546
+ if requires_value and "value" not in item:
547
+ raise ManifestError(f"assertion {index} in test `{test_id}` requires a value")
548
+ return item
549
+
550
+
551
+ def _run_eval_case(
552
+ *,
553
+ provider: Mapping[str, Any],
554
+ prompt: Mapping[str, Any],
555
+ test: Mapping[str, Any],
556
+ test_index: int,
557
+ base_dir: Path,
558
+ ) -> Dict[str, Any]:
559
+ variables = _as_dict(test.get("vars"))
560
+ rendered_prompt = _render_template(str(prompt.get("template") or ""), variables)
561
+ output = _provider_output(
562
+ provider=provider,
563
+ prompt=rendered_prompt,
564
+ variables=variables,
565
+ test=test,
566
+ base_dir=base_dir,
567
+ )
568
+ assertion_results = [
569
+ _evaluate_assertion(assertion, output, variables)
570
+ for assertion in _as_list(test.get("assertions"))
571
+ ]
572
+ failures = [item for item in assertion_results if not item.get("passed")]
573
+ case_id = f"{provider.get('id')}::{prompt.get('id')}::{test.get('id')}"
574
+ score = 1.0 if not assertion_results else (len(assertion_results) - len(failures)) / len(assertion_results)
575
+ findings = [
576
+ {
577
+ "type": "eval_assertion_failed",
578
+ "severity": "high",
579
+ "case_id": case_id,
580
+ "provider_id": provider.get("id"),
581
+ "prompt_id": prompt.get("id"),
582
+ "test_id": test.get("id"),
583
+ "assertion_type": failure.get("type"),
584
+ "expected": failure.get("expected"),
585
+ "actual": failure.get("actual", output),
586
+ "path": failure.get("path"),
587
+ "error": failure.get("error"),
588
+ }
589
+ for failure in failures
590
+ ]
591
+ return {
592
+ "index": test_index,
593
+ "id": case_id,
594
+ "name": case_id,
595
+ "provider_id": provider.get("id"),
596
+ "provider_type": provider.get("type"),
597
+ "prompt_id": prompt.get("id"),
598
+ "test_id": test.get("id"),
599
+ "input": rendered_prompt,
600
+ "output": output,
601
+ "score": round(score, 4),
602
+ "passed": not failures,
603
+ "assertions": assertion_results,
604
+ "findings": findings,
605
+ "metrics": [
606
+ {
607
+ "name": "eval_assertions",
608
+ "score": round(score, 4),
609
+ "details": {"assertions": assertion_results, "findings": findings},
610
+ }
611
+ ],
612
+ }
613
+
614
+
615
+ def _provider_output(
616
+ *,
617
+ provider: Mapping[str, Any],
618
+ prompt: str,
619
+ variables: Mapping[str, Any],
620
+ test: Mapping[str, Any],
621
+ base_dir: Path,
622
+ ) -> str:
623
+ provider_type = str(provider.get("type") or "echo").lower().replace("-", "_")
624
+ if provider_type == "echo":
625
+ return prompt
626
+ if provider_type == "scripted":
627
+ template = str(provider.get("response") or provider.get("output") or provider.get("template") or "")
628
+ if not template:
629
+ responses = _as_list(provider.get("responses"))
630
+ template = str(responses[0]) if responses else prompt
631
+ return _render_template(template, {**variables, "prompt": prompt, "input": prompt})
632
+ if provider_type in {"artifact", "artifact_json", "artifact_file"}:
633
+ return _artifact_provider_output(
634
+ provider=provider,
635
+ prompt=prompt,
636
+ variables=variables,
637
+ base_dir=base_dir,
638
+ )
639
+ if provider_type in {"python", "python_callable", "callable"}:
640
+ target = str(provider.get("target") or provider.get("callable") or "")
641
+ if not target:
642
+ raise ManifestError(f"provider `{provider.get('id')}` requires target")
643
+ callback = _load_callable(target, base_dir)
644
+ value = callback(prompt=prompt, vars=dict(variables), test=dict(test), provider=dict(provider))
645
+ if inspect.isawaitable(value):
646
+ value = asyncio.run(value)
647
+ return str(value)
648
+ if provider_type in {"litellm", "llm", "vertex", "vertex_ai", "gemini"}:
649
+ return _litellm_provider_output(
650
+ provider=provider,
651
+ prompt=prompt,
652
+ variables=variables,
653
+ provider_type=provider_type,
654
+ )
655
+ raise ManifestError(f"unsupported eval suite provider type: {provider_type}")
656
+
657
+
658
+ def _litellm_provider_output(
659
+ *,
660
+ provider: Mapping[str, Any],
661
+ prompt: str,
662
+ variables: Mapping[str, Any],
663
+ provider_type: str,
664
+ ) -> str:
665
+ """Call a live LLM through litellm.
666
+
667
+ Routes any litellm-supported model. Use ``type: vertex`` (or ``gemini``)
668
+ with a bare ``model`` name to reach Vertex AI — authentication comes from
669
+ ``GOOGLE_APPLICATION_CREDENTIALS`` and routing from the ``vertex_project`` /
670
+ ``vertex_location`` provider fields (or the matching ``VERTEXAI_*`` env
671
+ vars). Use ``type: litellm`` with a fully-qualified model string
672
+ (``vertex_ai/gemini-2.5-flash``, ``gpt-4o-mini``, ``claude-3-5-sonnet``)
673
+ for any other provider.
674
+ """
675
+ try:
676
+ import litellm
677
+ except Exception as exc: # pragma: no cover - import guard
678
+ raise ManifestError(
679
+ f"provider type `{provider_type}` requires litellm; reinstall "
680
+ "agent-learning-kit"
681
+ ) from exc
682
+
683
+ model = str(provider.get("model") or "").strip()
684
+ if not model:
685
+ raise ManifestError(f"provider `{provider.get('id')}` requires a model")
686
+ if provider_type in {"vertex", "vertex_ai", "gemini"} and "/" not in model:
687
+ model = f"vertex_ai/{model}"
688
+
689
+ render_ctx = {**variables, "prompt": prompt, "input": prompt}
690
+ messages: List[Dict[str, Any]] = []
691
+ system_prompt = provider.get("system") or provider.get("system_prompt")
692
+ if system_prompt:
693
+ messages.append(
694
+ {"role": "system", "content": _render_template(str(system_prompt), render_ctx)}
695
+ )
696
+ messages.append({"role": "user", "content": prompt})
697
+
698
+ kwargs: Dict[str, Any] = dict(_as_dict(provider.get("params")))
699
+ for key in (
700
+ "vertex_project",
701
+ "vertex_location",
702
+ "vertex_credentials",
703
+ "temperature",
704
+ "max_tokens",
705
+ "top_p",
706
+ "api_base",
707
+ "api_key",
708
+ ):
709
+ value = provider.get(key)
710
+ if value is not None and key not in kwargs:
711
+ kwargs[key] = value
712
+
713
+ litellm.drop_params = True
714
+ response = litellm.completion(model=model, messages=messages, **kwargs)
715
+ content = response.choices[0].message.content
716
+ return str(content or "")
717
+
718
+
719
+ def _artifact_provider_output(
720
+ *,
721
+ provider: Mapping[str, Any],
722
+ prompt: str,
723
+ variables: Mapping[str, Any],
724
+ base_dir: Path,
725
+ ) -> str:
726
+ raw_path = (
727
+ provider.get("path")
728
+ or provider.get("source")
729
+ or provider.get("artifact")
730
+ or variables.get("artifact_path")
731
+ or variables.get("artifact")
732
+ )
733
+ if not raw_path:
734
+ raise ManifestError(f"provider `{provider.get('id')}` requires artifact path")
735
+ rendered_path = _render_template(
736
+ str(raw_path),
737
+ {**variables, "prompt": prompt, "input": prompt},
738
+ )
739
+ artifact_path = Path(rendered_path).expanduser()
740
+ if not artifact_path.is_absolute():
741
+ artifact_path = base_dir / artifact_path
742
+ artifact_path = artifact_path.resolve()
743
+ artifact = _load_json_or_yaml(artifact_path)
744
+ fields = _artifact_fields(provider)
745
+ if not fields:
746
+ return json.dumps(artifact, indent=2, sort_keys=True, default=str)
747
+ extracted = {
748
+ label: _extract_artifact_path(artifact, path)
749
+ for label, path in fields
750
+ }
751
+ return json.dumps(
752
+ {
753
+ "artifact_path": str(artifact_path),
754
+ "fields": extracted,
755
+ },
756
+ indent=2,
757
+ sort_keys=True,
758
+ default=str,
759
+ )
760
+
761
+
762
+ def _artifact_fields(provider: Mapping[str, Any]) -> List[tuple[str, str]]:
763
+ raw_fields = (
764
+ provider.get("fields")
765
+ or provider.get("extract")
766
+ or provider.get("paths")
767
+ or provider.get("json_paths")
768
+ )
769
+ fields: List[tuple[str, str]] = []
770
+ for index, raw_field in enumerate(_as_list(raw_fields), start=1):
771
+ if isinstance(raw_field, str):
772
+ fields.append((raw_field, raw_field))
773
+ continue
774
+ item = _as_dict(raw_field)
775
+ path = str(item.get("path") or item.get("json_path") or item.get("field") or "")
776
+ if not path:
777
+ raise ManifestError(f"artifact field {index} requires path")
778
+ label = str(item.get("id") or item.get("name") or item.get("as") or path)
779
+ fields.append((label, path))
780
+ return fields
781
+
782
+
783
+ def _extract_artifact_path(value: Any, path: str) -> Any:
784
+ current = value
785
+ for token in _artifact_path_tokens(path):
786
+ if isinstance(current, Mapping):
787
+ if token not in current:
788
+ raise ManifestError(f"artifact path `{path}` missing key `{token}`")
789
+ current = current[token]
790
+ elif isinstance(current, list):
791
+ try:
792
+ index = int(token)
793
+ except ValueError as exc:
794
+ raise ManifestError(
795
+ f"artifact path `{path}` expected list index, got `{token}`"
796
+ ) from exc
797
+ try:
798
+ current = current[index]
799
+ except IndexError as exc:
800
+ raise ManifestError(
801
+ f"artifact path `{path}` index out of range: {index}"
802
+ ) from exc
803
+ else:
804
+ raise ManifestError(f"artifact path `{path}` cannot traverse `{token}`")
805
+ return current
806
+
807
+
808
+ def _artifact_path_tokens(path: str) -> List[str]:
809
+ normalized = path.strip()
810
+ if not normalized:
811
+ raise ManifestError("artifact path cannot be empty")
812
+ if normalized.startswith("$."):
813
+ normalized = normalized[2:]
814
+ elif normalized == "$":
815
+ return []
816
+ tokens: List[str] = []
817
+ for segment in normalized.split("."):
818
+ if not segment:
819
+ continue
820
+ while "[" in segment:
821
+ before, _, rest = segment.partition("[")
822
+ if before:
823
+ tokens.append(before)
824
+ index, marker, tail = rest.partition("]")
825
+ if not marker:
826
+ raise ManifestError(f"invalid artifact path segment `{segment}`")
827
+ tokens.append(index)
828
+ segment = tail
829
+ if segment:
830
+ tokens.append(segment)
831
+ return tokens
832
+
833
+
834
+ def _evaluate_assertion(
835
+ assertion: Mapping[str, Any],
836
+ output: str,
837
+ variables: Optional[Mapping[str, Any]] = None,
838
+ ) -> Dict[str, Any]:
839
+ assertion_type = str(assertion.get("type") or "contains").lower().replace("-", "_")
840
+ if assertion_type in _JSON_PATH_ASSERTIONS:
841
+ return _evaluate_json_path_assertion(assertion, output, assertion_type)
842
+ if assertion_type in _FI_EVAL_ASSERTIONS:
843
+ return _evaluate_fi_eval_assertion(assertion, output, variables or {})
844
+ expected = assertion.get("value")
845
+ text = str(output)
846
+ expected_text = str(expected)
847
+ if assertion_type == "contains":
848
+ passed = expected_text in text
849
+ elif assertion_type == "not_contains":
850
+ passed = expected_text not in text
851
+ elif assertion_type in {"equals", "equal", "is"}:
852
+ passed = text.strip() == expected_text.strip()
853
+ elif assertion_type in {"regex", "matches"}:
854
+ passed = re.search(expected_text, text, flags=re.MULTILINE) is not None
855
+ else:
856
+ raise ManifestError(f"unsupported assertion type: {assertion_type}")
857
+ return {
858
+ "type": assertion_type,
859
+ "expected": expected,
860
+ "actual": output,
861
+ "passed": bool(passed),
862
+ }
863
+
864
+
865
+ def _evaluate_fi_eval_assertion(
866
+ assertion: Mapping[str, Any],
867
+ output: str,
868
+ variables: Mapping[str, Any],
869
+ ) -> Dict[str, Any]:
870
+ """Score the case output with a hosted FutureAGI eval template.
871
+
872
+ Dispatches to ``fi.evals.evaluate`` on the platform (turing) engine by
873
+ default; credentials come from ``FI_API_KEY`` / ``FI_SECRET_KEY`` /
874
+ ``FI_BASE_URL`` (or per-assertion overrides). Pass when the returned score
875
+ is >= ``threshold`` (default 0.5).
876
+ """
877
+ eval_name = (
878
+ assertion.get("eval")
879
+ or assertion.get("metric")
880
+ or assertion.get("name")
881
+ or assertion.get("value")
882
+ )
883
+ if not eval_name:
884
+ raise ManifestError(
885
+ "fi_eval assertion requires an `eval` (hosted template name)."
886
+ )
887
+ threshold = float(assertion.get("threshold", 0.5))
888
+ engine = str(assertion.get("engine") or "turing").strip().lower()
889
+ model = assertion.get("model")
890
+
891
+ inputs: Dict[str, Any] = {"output": output}
892
+ for key in _FI_EVAL_INPUT_KEYS:
893
+ if key in assertion:
894
+ inputs[key] = _render_template(str(assertion[key]), variables)
895
+ extra_inputs = assertion.get("inputs")
896
+ if isinstance(extra_inputs, Mapping):
897
+ for key, val in extra_inputs.items():
898
+ inputs[str(key)] = (
899
+ _render_template(val, variables) if isinstance(val, str) else val
900
+ )
901
+
902
+ try:
903
+ from fi.evals import evaluate as _fi_evaluate
904
+ except Exception as exc: # pragma: no cover - optional dependency clarity
905
+ raise ManifestError(
906
+ "fi_eval assertion requires the FutureAGI evals engine (fi.evals). "
907
+ "Install the evals extra to score against platform templates."
908
+ ) from exc
909
+
910
+ call_kwargs: Dict[str, Any] = dict(inputs)
911
+ if engine and engine != "auto":
912
+ call_kwargs["engine"] = engine
913
+ if model:
914
+ call_kwargs["model"] = model
915
+ for cred_key, cfg_key in (
916
+ ("fi_api_key", "api_key"),
917
+ ("fi_secret_key", "secret_key"),
918
+ ("fi_base_url", "base_url"),
919
+ ):
920
+ if assertion.get(cfg_key):
921
+ call_kwargs[cred_key] = assertion[cfg_key]
922
+
923
+ try:
924
+ result = _fi_evaluate(str(eval_name), **call_kwargs)
925
+ except Exception as exc:
926
+ return {
927
+ "type": "fi_eval",
928
+ "eval": eval_name,
929
+ "engine": engine,
930
+ "threshold": threshold,
931
+ "expected": f">= {threshold}",
932
+ "actual": None,
933
+ "passed": False,
934
+ "error": str(exc),
935
+ }
936
+
937
+ score = getattr(result, "score", None)
938
+ reason = getattr(result, "reason", "") or ""
939
+ try:
940
+ score_value = float(score)
941
+ except (TypeError, ValueError):
942
+ score_value = 0.0
943
+ return {
944
+ "type": "fi_eval",
945
+ "eval": eval_name,
946
+ "engine": engine,
947
+ "threshold": threshold,
948
+ "score": round(score_value, 4),
949
+ "reason": reason,
950
+ "expected": f">= {threshold}",
951
+ "actual": round(score_value, 4),
952
+ "passed": bool(score_value >= threshold),
953
+ }
954
+
955
+
956
+ def evaluate_assertions(
957
+ output: str,
958
+ assertions: Sequence[Mapping[str, Any]],
959
+ *,
960
+ variables: Optional[Mapping[str, Any]] = None,
961
+ ) -> Dict[str, Any]:
962
+ """Score an output against a list of eval-suite assertions.
963
+
964
+ Public helper used by the generative optimizer bridge so candidate prompts
965
+ are scored against the suite's own assertions (including ``fi_eval``
966
+ platform templates). Returns pass-rate plus per-assertion detail.
967
+ """
968
+ variables = dict(variables or {})
969
+ results = [
970
+ _evaluate_assertion(assertion, output, variables)
971
+ for assertion in (assertions or [])
972
+ ]
973
+ if not results:
974
+ return {"score": 1.0, "passed": True, "results": []}
975
+ passed = sum(1 for item in results if item.get("passed"))
976
+ return {
977
+ "score": passed / len(results),
978
+ "passed": passed == len(results),
979
+ "results": results,
980
+ }
981
+
982
+
983
+ def _evaluate_json_path_assertion(
984
+ assertion: Mapping[str, Any],
985
+ output: str,
986
+ assertion_type: str,
987
+ ) -> Dict[str, Any]:
988
+ path = str(
989
+ assertion.get("path")
990
+ or assertion.get("json_path")
991
+ or assertion.get("field")
992
+ or ""
993
+ )
994
+ expected = assertion.get("value")
995
+ result: Dict[str, Any] = {
996
+ "type": assertion_type,
997
+ "path": path,
998
+ "expected": True if assertion_type in _JSON_PATH_EXISTS_ASSERTIONS else expected,
999
+ "actual": None,
1000
+ "passed": False,
1001
+ }
1002
+ if not path:
1003
+ result["error"] = "json path assertion requires a path"
1004
+ return result
1005
+ try:
1006
+ document = json.loads(output)
1007
+ except json.JSONDecodeError as exc:
1008
+ result["error"] = f"output is not valid JSON: {exc.msg}"
1009
+ return result
1010
+ try:
1011
+ actual = _extract_artifact_path(document, path)
1012
+ except ManifestError as exc:
1013
+ result["error"] = str(exc)
1014
+ return result
1015
+ result["actual"] = actual
1016
+
1017
+ if assertion_type in _JSON_PATH_EXISTS_ASSERTIONS:
1018
+ result["passed"] = True
1019
+ elif assertion_type in _JSON_PATH_EQUALS_ASSERTIONS:
1020
+ result["passed"] = actual == expected
1021
+ elif assertion_type in _JSON_PATH_GTE_ASSERTIONS:
1022
+ passed, error = _json_path_numeric_compare(actual, expected, "gte")
1023
+ result["passed"] = passed
1024
+ if error:
1025
+ result["error"] = error
1026
+ elif assertion_type in _JSON_PATH_LTE_ASSERTIONS:
1027
+ passed, error = _json_path_numeric_compare(actual, expected, "lte")
1028
+ result["passed"] = passed
1029
+ if error:
1030
+ result["error"] = error
1031
+ elif assertion_type in _JSON_PATH_CONTAINS_ASSERTIONS:
1032
+ result["passed"] = _json_path_contains(actual, expected)
1033
+ elif assertion_type in _JSON_PATH_NOT_CONTAINS_ASSERTIONS:
1034
+ result["passed"] = not _json_path_contains(actual, expected)
1035
+ return result
1036
+
1037
+
1038
+ def _json_path_numeric_compare(
1039
+ actual: Any,
1040
+ expected: Any,
1041
+ operator: str,
1042
+ ) -> tuple[bool, str | None]:
1043
+ try:
1044
+ actual_number = float(actual)
1045
+ expected_number = float(expected)
1046
+ except (TypeError, ValueError):
1047
+ return (
1048
+ False,
1049
+ f"expected numeric JSON path values, got actual={actual!r} expected={expected!r}",
1050
+ )
1051
+ if operator == "gte":
1052
+ return actual_number >= expected_number, None
1053
+ return actual_number <= expected_number, None
1054
+
1055
+
1056
+ def _json_path_contains(actual: Any, expected: Any) -> bool:
1057
+ if isinstance(actual, Mapping):
1058
+ return expected in actual or str(expected) in actual
1059
+ if isinstance(actual, (list, tuple, set)):
1060
+ return expected in actual
1061
+ return str(expected) in str(actual)
1062
+
1063
+
1064
+ def _suite_result(
1065
+ *,
1066
+ name: str,
1067
+ suite: Mapping[str, Any],
1068
+ cases: Sequence[Mapping[str, Any]],
1069
+ threshold: float,
1070
+ duration_seconds: float,
1071
+ dry_run: bool,
1072
+ ) -> Dict[str, Any]:
1073
+ case_count = len(cases)
1074
+ passed_count = sum(1 for case in cases if case.get("passed"))
1075
+ assertion_count = sum(len(_as_list(case.get("assertions"))) for case in cases)
1076
+ failed_assertion_count = sum(
1077
+ 1
1078
+ for case in cases
1079
+ for assertion in _as_list(case.get("assertions"))
1080
+ if not _as_dict(assertion).get("passed")
1081
+ )
1082
+ score = 1.0 if not assertion_count else (assertion_count - failed_assertion_count) / assertion_count
1083
+ passed = (score >= threshold) and (passed_count == case_count)
1084
+ if dry_run:
1085
+ passed = True
1086
+ return {
1087
+ "schema_version": CLI_SCHEMA_VERSION,
1088
+ "kind": EVAL_SUITE_SCHEMA_VERSION,
1089
+ "name": name,
1090
+ "status": "passed" if passed else "failed",
1091
+ "exit_code": 0 if passed else 1,
1092
+ "summary": {
1093
+ "score": round(score, 4),
1094
+ "threshold": threshold,
1095
+ "provider_count": len(_as_list(suite.get("providers"))),
1096
+ "prompt_count": len(_as_list(suite.get("prompts"))),
1097
+ "test_count": len(_as_list(suite.get("tests"))),
1098
+ "case_count": case_count,
1099
+ "passed_case_count": passed_count,
1100
+ "failed_case_count": case_count - passed_count,
1101
+ "assertion_count": assertion_count,
1102
+ "passed_assertion_count": assertion_count - failed_assertion_count,
1103
+ "failed_assertion_count": failed_assertion_count,
1104
+ "dry_run": dry_run,
1105
+ },
1106
+ "eval_suite": {
1107
+ "version": suite.get("version") or EVAL_SUITE_SCHEMA_VERSION,
1108
+ "providers": [
1109
+ {"id": provider.get("id"), "type": provider.get("type")}
1110
+ for provider in _as_list(suite.get("providers"))
1111
+ if isinstance(provider, Mapping)
1112
+ ],
1113
+ "prompts": [
1114
+ {"id": prompt.get("id")}
1115
+ for prompt in _as_list(suite.get("prompts"))
1116
+ if isinstance(prompt, Mapping)
1117
+ ],
1118
+ "tests": [
1119
+ {"id": test.get("id")}
1120
+ for test in _as_list(suite.get("tests"))
1121
+ if isinstance(test, Mapping)
1122
+ ],
1123
+ "cases": list(cases),
1124
+ },
1125
+ "evaluation": {
1126
+ "passed": passed,
1127
+ "score": round(score, 4),
1128
+ "threshold": threshold,
1129
+ "cases": list(cases),
1130
+ "findings": [
1131
+ finding
1132
+ for case in cases
1133
+ for finding in _as_list(case.get("findings"))
1134
+ if isinstance(finding, Mapping)
1135
+ ],
1136
+ },
1137
+ "duration_seconds": duration_seconds,
1138
+ }
1139
+
1140
+
1141
+ def _render_template(template: str, variables: Mapping[str, Any]) -> str:
1142
+ result = template
1143
+ for key, value in variables.items():
1144
+ result = result.replace("{{" + str(key) + "}}", str(value))
1145
+ result = result.replace("{{ " + str(key) + " }}", str(value))
1146
+ return result
1147
+
1148
+
1149
+ def _load_json_or_yaml(path: Path) -> Any:
1150
+ if not path.exists():
1151
+ raise ManifestError(f"eval suite file not found: {path}")
1152
+ if path.suffix.lower() in {".yaml", ".yml"}:
1153
+ try:
1154
+ import yaml # type: ignore
1155
+ except Exception as exc: # pragma: no cover - optional dependency clarity
1156
+ raise ManifestError("YAML eval suites require PyYAML; use JSON or install PyYAML.") from exc
1157
+ with path.open("r", encoding="utf-8") as handle:
1158
+ return yaml.safe_load(handle)
1159
+ with path.open("r", encoding="utf-8") as handle:
1160
+ return json.load(handle)
1161
+
1162
+
1163
+ def _load_callable(target: str, base_dir: Path) -> Callable[..., Any]:
1164
+ module_name, _, function_name = target.partition(":")
1165
+ if not module_name or not function_name:
1166
+ raise ManifestError("python callable must use 'module:function' or 'path.py:function'")
1167
+ if module_name.endswith(".py") or "/" in module_name:
1168
+ module_path = Path(module_name)
1169
+ if not module_path.is_absolute():
1170
+ module_path = base_dir / module_path
1171
+ spec = importlib.util.spec_from_file_location(module_path.stem, module_path)
1172
+ if spec is None or spec.loader is None:
1173
+ raise ManifestError(f"cannot load python module: {module_path}")
1174
+ module = importlib.util.module_from_spec(spec)
1175
+ spec.loader.exec_module(module)
1176
+ else:
1177
+ module = importlib.import_module(module_name)
1178
+ callback = getattr(module, function_name, None)
1179
+ if not callable(callback):
1180
+ raise ManifestError(f"python callable not found: {target}")
1181
+ return callback
1182
+
1183
+
1184
+ def _merge_eval_suite_options(
1185
+ options: Optional[EvalSuiteOptions],
1186
+ *,
1187
+ name: Optional[str],
1188
+ threshold: Optional[float],
1189
+ dry_run: Optional[bool],
1190
+ ) -> EvalSuiteOptions:
1191
+ opts = options or EvalSuiteOptions()
1192
+ return EvalSuiteOptions(
1193
+ name=opts.name if name is None else name,
1194
+ threshold=opts.threshold if threshold is None else threshold,
1195
+ dry_run=opts.dry_run if dry_run is None else dry_run,
1196
+ )
1197
+
1198
+
1199
+ def _merge_eval_suite_optimization_options(
1200
+ options: Optional[EvalSuiteOptimizationOptions],
1201
+ *,
1202
+ name: Optional[str],
1203
+ threshold: Optional[float],
1204
+ max_candidates: Optional[int],
1205
+ dry_run: Optional[bool],
1206
+ ) -> EvalSuiteOptimizationOptions:
1207
+ opts = options or EvalSuiteOptimizationOptions()
1208
+ return EvalSuiteOptimizationOptions(
1209
+ name=opts.name if name is None else name,
1210
+ threshold=opts.threshold if threshold is None else threshold,
1211
+ max_candidates=opts.max_candidates if max_candidates is None else max_candidates,
1212
+ dry_run=opts.dry_run if dry_run is None else dry_run,
1213
+ )
1214
+
1215
+
1216
+ def _suite_file_like_path(path: str | Path) -> Path:
1217
+ resolved = Path(path).expanduser().resolve()
1218
+ if resolved.is_dir():
1219
+ return resolved / "eval_suite.json"
1220
+ return resolved
1221
+
1222
+
1223
+ def _eval_suite_descriptor(suite: Mapping[str, Any]) -> Dict[str, Any]:
1224
+ return {
1225
+ "version": suite.get("version") or EVAL_SUITE_SCHEMA_VERSION,
1226
+ "providers": [
1227
+ {"id": provider.get("id"), "type": provider.get("type")}
1228
+ for provider in _as_list(suite.get("providers"))
1229
+ if isinstance(provider, Mapping)
1230
+ ],
1231
+ "prompts": [
1232
+ {"id": prompt.get("id")}
1233
+ for prompt in _as_list(suite.get("prompts"))
1234
+ if isinstance(prompt, Mapping)
1235
+ ],
1236
+ "tests": [
1237
+ {"id": test.get("id")}
1238
+ for test in _as_list(suite.get("tests"))
1239
+ if isinstance(test, Mapping)
1240
+ ],
1241
+ }
1242
+
1243
+
1244
+ def _cli() -> Any:
1245
+ return importlib.import_module("fi.simulate.cli")
1246
+
1247
+
1248
+ def _as_list(value: Any) -> List[Any]:
1249
+ if value is None:
1250
+ return []
1251
+ if isinstance(value, list):
1252
+ return value
1253
+ if isinstance(value, tuple):
1254
+ return list(value)
1255
+ return [value]
1256
+
1257
+
1258
+ def _as_dict(value: Any) -> Dict[str, Any]:
1259
+ return dict(value) if isinstance(value, Mapping) else {}
1260
+
1261
+
1262
+ def _copy_mapping_sequence(
1263
+ values: Sequence[Mapping[str, Any]],
1264
+ *,
1265
+ field: str,
1266
+ ) -> List[Dict[str, Any]]:
1267
+ if isinstance(values, (str, bytes)) or isinstance(values, Mapping):
1268
+ raise ValueError(f"{field} must be a sequence of mappings")
1269
+ copied = [copy.deepcopy(dict(value)) for value in values]
1270
+ if not copied:
1271
+ raise ValueError(f"{field} must contain at least one item")
1272
+ return copied
1273
+
1274
+
1275
+ __all__ = [
1276
+ "AGENT_LEARNING_EVAL_SUITE_SCHEMA_VERSION",
1277
+ "EVAL_SUITE_SCHEMA_VERSION",
1278
+ "EVAL_SUITE_OPTIMIZATION_SCHEMA_VERSION",
1279
+ "EvalSuiteOptimizationOptions",
1280
+ "EvalSuiteOptions",
1281
+ "build_eval_suite_manifest",
1282
+ "load_eval_suite_file",
1283
+ "optimize_eval_suite",
1284
+ "optimize_eval_suite_file",
1285
+ "run_eval_suite",
1286
+ "run_eval_suite_file",
1287
+ "write_eval_suite_file",
1288
+ ]