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,252 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ import time
6
+
7
+ from livekit import rtc
8
+ from livekit.api import AccessToken, VideoGrants
9
+
10
+ from fi.simulate.simulation.bridge.audio import PCMResampler
11
+ from fi.simulate.simulation.bridge.connector import ProviderConnector
12
+
13
+ logger = logging.getLogger(__name__)
14
+ ROOM_SAMPLE_RATE = 48000
15
+ ROOM_CHANNELS = 1
16
+ TRACK_TIMEOUT_SECONDS = 30.0
17
+ WATCHDOG_TIMEOUT_SECONDS = 60.0
18
+ PROVIDER_READY_BUFFER_FRAMES = 3000
19
+ PROVIDER_AUDIO_TIMEOUT_SECONDS = 120.0
20
+ PROVIDER_QUEUE_MAX_CHUNKS = 200
21
+
22
+
23
+ class LiveKitAudioBridge:
24
+ def __init__(
25
+ self,
26
+ *,
27
+ url: str,
28
+ api_key: str,
29
+ api_secret: str,
30
+ room_name: str,
31
+ identity: str,
32
+ connector: ProviderConnector,
33
+ ) -> None:
34
+ self._url = url
35
+ self._api_key = api_key
36
+ self._api_secret = api_secret
37
+ self._room_name = room_name
38
+ self._identity = identity
39
+ self._connector = connector
40
+ self._room = rtc.Room()
41
+ self._track_future: asyncio.Future[rtc.RemoteAudioTrack] | None = None
42
+ self._room_disconnected: asyncio.Future[None] | None = None
43
+ self._audio_source: rtc.AudioSource | None = None
44
+ self._closed = False
45
+ self._close_lock = asyncio.Lock()
46
+ self._last_audio_at = time.monotonic()
47
+ self._last_provider_audio_at = time.monotonic()
48
+
49
+ @property
50
+ def call_id(self) -> str | None:
51
+ return self._connector.call_id
52
+
53
+ async def connect(self) -> None:
54
+ loop = asyncio.get_running_loop()
55
+ self._track_future = loop.create_future()
56
+ self._room_disconnected = loop.create_future()
57
+
58
+ @self._room.on("track_subscribed")
59
+ def _on_track(track, publication, participant) -> None:
60
+ if not isinstance(track, rtc.RemoteAudioTrack):
61
+ return
62
+ if self._track_future is None or self._track_future.done():
63
+ return
64
+ if publication.source != rtc.TrackSource.SOURCE_MICROPHONE:
65
+ return
66
+ if participant.kind == rtc.ParticipantKind.PARTICIPANT_KIND_SIP:
67
+ return
68
+ self._track_future.set_result(track)
69
+
70
+ @self._room.on("disconnected")
71
+ def _on_disconnected(*_args) -> None:
72
+ if self._room_disconnected and not self._room_disconnected.done():
73
+ self._room_disconnected.set_result(None)
74
+
75
+ token = (
76
+ AccessToken(self._api_key, self._api_secret)
77
+ .with_identity(self._identity)
78
+ .with_name("FutureAGI Web Bridge")
79
+ .with_kind("sip")
80
+ .with_grants(
81
+ VideoGrants(
82
+ room_join=True,
83
+ room=self._room_name,
84
+ can_publish=True,
85
+ can_subscribe=True,
86
+ )
87
+ )
88
+ .to_jwt()
89
+ )
90
+ try:
91
+ await self._room.connect(self._url, token)
92
+ self._latch_preexisting_track()
93
+ self._audio_source = rtc.AudioSource(ROOM_SAMPLE_RATE, ROOM_CHANNELS)
94
+ track = rtc.LocalAudioTrack.create_audio_track(
95
+ "bridge-audio", self._audio_source
96
+ )
97
+ await self._room.local_participant.publish_track(
98
+ track,
99
+ rtc.TrackPublishOptions(source=rtc.TrackSource.SOURCE_MICROPHONE),
100
+ )
101
+ await self._connector.connect()
102
+ except Exception:
103
+ await self.aclose()
104
+ raise
105
+
106
+ async def run(self) -> None:
107
+ tasks = {
108
+ asyncio.create_task(self._room_to_provider()),
109
+ asyncio.create_task(self._provider_to_room()),
110
+ asyncio.create_task(self._watchdog()),
111
+ asyncio.create_task(self._wait_for_room_disconnect()),
112
+ }
113
+ try:
114
+ done, pending = await asyncio.wait(
115
+ tasks, return_when=asyncio.FIRST_COMPLETED
116
+ )
117
+ for task in pending:
118
+ task.cancel()
119
+ await asyncio.gather(*pending, return_exceptions=True)
120
+ for task in done:
121
+ await task
122
+ finally:
123
+ await self.aclose()
124
+
125
+ async def aclose(self) -> None:
126
+ async with self._close_lock:
127
+ if self._closed:
128
+ return
129
+ self._closed = True
130
+ try:
131
+ await self._connector.disconnect()
132
+ finally:
133
+ await self._room.disconnect()
134
+
135
+ def _latch_preexisting_track(self) -> None:
136
+ if self._track_future is None or self._track_future.done():
137
+ return
138
+ for participant in self._room.remote_participants.values():
139
+ if participant.kind == rtc.ParticipantKind.PARTICIPANT_KIND_SIP:
140
+ continue
141
+ for publication in participant.track_publications.values():
142
+ if (
143
+ publication.track
144
+ and isinstance(publication.track, rtc.RemoteAudioTrack)
145
+ and publication.source == rtc.TrackSource.SOURCE_MICROPHONE
146
+ ):
147
+ self._track_future.set_result(publication.track)
148
+ return
149
+
150
+ async def _room_to_provider(self) -> None:
151
+ if self._track_future is None:
152
+ raise RuntimeError("bridge_not_connected")
153
+ silence = asyncio.create_task(self._send_silence_until_track())
154
+ try:
155
+ track = await asyncio.wait_for(
156
+ self._track_future, timeout=TRACK_TIMEOUT_SECONDS
157
+ )
158
+ finally:
159
+ silence.cancel()
160
+ await asyncio.gather(silence, return_exceptions=True)
161
+ buffered_frames: list[tuple[bytes, int]] = []
162
+ async for event in rtc.AudioStream(track):
163
+ frame = event.frame
164
+ self._last_audio_at = time.monotonic()
165
+ frame_data = frame.data.tobytes()
166
+ if not self._connector.is_agent_ready:
167
+ if len(buffered_frames) < PROVIDER_READY_BUFFER_FRAMES:
168
+ buffered_frames.append((frame_data, frame.sample_rate))
169
+ continue
170
+ for buffered_data, buffered_rate in buffered_frames:
171
+ await self._connector.send_audio(buffered_data, buffered_rate)
172
+ buffered_frames.clear()
173
+ await self._connector.send_audio(frame_data, frame.sample_rate)
174
+
175
+ async def _provider_to_room(self) -> None:
176
+ if self._audio_source is None:
177
+ raise RuntimeError("bridge_not_connected")
178
+ # The websocket reader must never block on room playback: a stalled
179
+ # ``capture_frame`` would stop close/hangup frames from being seen and
180
+ # keep a dead provider call alive. Live audio, so drop oldest when full.
181
+ queue: asyncio.Queue[tuple[bytes, int] | None] = asyncio.Queue(
182
+ maxsize=PROVIDER_QUEUE_MAX_CHUNKS
183
+ )
184
+
185
+ async def _pump() -> None:
186
+ try:
187
+ async for chunk in self._connector.recv_audio():
188
+ now = time.monotonic()
189
+ self._last_audio_at = now
190
+ self._last_provider_audio_at = now
191
+ if queue.full():
192
+ queue.get_nowait()
193
+ queue.put_nowait(chunk)
194
+ finally:
195
+ if queue.full():
196
+ queue.get_nowait()
197
+ queue.put_nowait(None)
198
+
199
+ pump = asyncio.create_task(_pump())
200
+ resamplers: dict[int, PCMResampler] = {}
201
+ try:
202
+ while True:
203
+ chunk = await queue.get()
204
+ if chunk is None:
205
+ break
206
+ pcm, sample_rate = chunk
207
+ if sample_rate != ROOM_SAMPLE_RATE:
208
+ resampler = resamplers.setdefault(
209
+ sample_rate,
210
+ PCMResampler(
211
+ from_rate=sample_rate,
212
+ to_rate=ROOM_SAMPLE_RATE,
213
+ channels=ROOM_CHANNELS,
214
+ ),
215
+ )
216
+ pcm = resampler.convert(pcm)
217
+ await self._audio_source.capture_frame(
218
+ rtc.AudioFrame(
219
+ data=pcm,
220
+ sample_rate=ROOM_SAMPLE_RATE,
221
+ num_channels=ROOM_CHANNELS,
222
+ samples_per_channel=len(pcm) // 2,
223
+ )
224
+ )
225
+ finally:
226
+ pump.cancel()
227
+ await asyncio.gather(pump, return_exceptions=True)
228
+
229
+ async def _send_silence_until_track(self) -> None:
230
+ frame = b"\x00" * int(16000 * 0.02 * 2)
231
+ while self._track_future is not None and not self._track_future.done():
232
+ await self._connector.send_audio(frame, 16000)
233
+ await asyncio.sleep(0.02)
234
+
235
+ async def _watchdog(self) -> None:
236
+ # ``_last_audio_at`` refreshes on simulator silence frames too, so it is
237
+ # blind to a dead provider; track provider-received audio separately.
238
+ while True:
239
+ await asyncio.sleep(5.0)
240
+ now = time.monotonic()
241
+ if now - self._last_audio_at > WATCHDOG_TIMEOUT_SECONDS:
242
+ raise RuntimeError("bridge_audio_watchdog_timeout")
243
+ if (
244
+ now - self._last_provider_audio_at
245
+ > PROVIDER_AUDIO_TIMEOUT_SECONDS
246
+ ):
247
+ raise RuntimeError("provider_audio_timeout")
248
+
249
+ async def _wait_for_room_disconnect(self) -> None:
250
+ if self._room_disconnected is None:
251
+ raise RuntimeError("bridge_not_connected")
252
+ await self._room_disconnected
@@ -0,0 +1,188 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ import os
6
+ from collections.abc import AsyncIterator
7
+
8
+ import aiohttp
9
+ from livekit import rtc
10
+
11
+ from fi.simulate.agent.definition import RetellTargetConfig
12
+ from fi.simulate.simulation.bridge.audio import PCMResampler
13
+ from fi.simulate.simulation.bridge.connector import ConnectorConfig, ProviderConnector
14
+
15
+ logger = logging.getLogger(__name__)
16
+ RETELL_SAMPLE_RATE = 48000
17
+
18
+
19
+ class RetellWebCallConnector(ProviderConnector):
20
+ def __init__(self, config: ConnectorConfig) -> None:
21
+ self._config = config
22
+ self._room: rtc.Room | None = None
23
+ self._audio_source: rtc.AudioSource | None = None
24
+ self._track_future: asyncio.Future[rtc.RemoteAudioTrack] | None = None
25
+ self._agent_disconnected = asyncio.Event()
26
+ self._agent_identity: str | None = None
27
+ self._agent_ready = asyncio.Event()
28
+ self._call_id: str | None = None
29
+ self._connected = False
30
+ self._resamplers: dict[int, PCMResampler] = {}
31
+
32
+ @classmethod
33
+ def from_target(cls, target: RetellTargetConfig) -> "RetellWebCallConnector":
34
+ api_key = os.environ.get(target.api_key_env, "").strip()
35
+ if not api_key:
36
+ raise ValueError("retell_webcall_config_missing: " + target.api_key_env)
37
+ return cls(
38
+ ConnectorConfig(
39
+ api_key=api_key,
40
+ assistant_id=target.agent_id,
41
+ api_url=str(target.api_url),
42
+ livekit_url=str(target.livekit_url),
43
+ )
44
+ )
45
+
46
+ @classmethod
47
+ def from_env(cls) -> "RetellWebCallConnector":
48
+ api_key = os.environ.get("RETELL_API_KEY", "").strip()
49
+ agent_id = os.environ.get("RETELL_AGENT_ID", "").strip()
50
+ missing = [
51
+ name
52
+ for name, value in (
53
+ ("RETELL_API_KEY", api_key),
54
+ ("RETELL_AGENT_ID", agent_id),
55
+ )
56
+ if not value
57
+ ]
58
+ if missing:
59
+ raise ValueError("retell_webcall_config_missing: " + ", ".join(missing))
60
+ return cls.from_target(
61
+ RetellTargetConfig(
62
+ agent_id=agent_id,
63
+ api_url=os.environ.get(
64
+ "RETELL_API_URL",
65
+ "https://api.retellai.com/v2/create-web-call",
66
+ ),
67
+ livekit_url=os.environ.get(
68
+ "RETELL_LIVEKIT_URL",
69
+ "wss://retell-ai-4ihahnq7.livekit.cloud",
70
+ ),
71
+ )
72
+ )
73
+
74
+ async def connect(self) -> None:
75
+ async with aiohttp.ClientSession() as session:
76
+ async with session.post(
77
+ self._config.api_url,
78
+ headers={"Authorization": f"Bearer {self._config.api_key}"},
79
+ json={"agent_id": self._config.assistant_id},
80
+ ) as response:
81
+ if response.status != 201:
82
+ raise RuntimeError(
83
+ f"retell_webcall_create_failed:{response.status}"
84
+ )
85
+ payload = await response.json()
86
+ access_token = (
87
+ payload.get("access_token") if isinstance(payload, dict) else None
88
+ )
89
+ call_id = payload.get("call_id") if isinstance(payload, dict) else None
90
+ if not isinstance(access_token, str) or not access_token.strip():
91
+ raise ValueError("retell_webcall_response_missing_access_token")
92
+ if not isinstance(call_id, str) or not call_id.strip():
93
+ raise ValueError("retell_webcall_response_missing_call_id")
94
+ self._call_id = call_id
95
+ self._room = rtc.Room()
96
+ self._track_future = asyncio.get_running_loop().create_future()
97
+
98
+ @self._room.on("track_subscribed")
99
+ def _on_track(track, _publication, participant) -> None:
100
+ if not isinstance(track, rtc.RemoteAudioTrack):
101
+ return
102
+ if self._track_future is None or self._track_future.done():
103
+ return
104
+ identity = getattr(participant, "identity", None)
105
+ if identity is not None:
106
+ self._agent_identity = str(identity)
107
+ self._track_future.set_result(track)
108
+ self._agent_ready.set()
109
+
110
+ @self._room.on("participant_disconnected")
111
+ def _on_participant_disconnected(participant) -> None:
112
+ identity = getattr(participant, "identity", None)
113
+ if self._agent_identity is None or str(identity) == self._agent_identity:
114
+ self._agent_disconnected.set()
115
+
116
+ @self._room.on("disconnected")
117
+ def _on_disconnected(*_args) -> None:
118
+ self._agent_disconnected.set()
119
+
120
+ try:
121
+ await self._room.connect(self._config.livekit_url, access_token)
122
+ self._audio_source = rtc.AudioSource(RETELL_SAMPLE_RATE, 1)
123
+ track = rtc.LocalAudioTrack.create_audio_track(
124
+ "bridge-audio", self._audio_source
125
+ )
126
+ await self._room.local_participant.publish_track(
127
+ track,
128
+ rtc.TrackPublishOptions(source=rtc.TrackSource.SOURCE_MICROPHONE),
129
+ )
130
+ self._connected = True
131
+ if self._track_future is None:
132
+ raise RuntimeError("retell_webcall_track_future_missing")
133
+ await asyncio.wait_for(asyncio.shield(self._track_future), timeout=30.0)
134
+ logger.info("retell_webcall_connected", extra={"call_id": call_id})
135
+ except asyncio.TimeoutError as exc:
136
+ await self.disconnect()
137
+ raise RuntimeError("retell_webcall_agent_track_timeout") from exc
138
+ except Exception:
139
+ await self.disconnect()
140
+ raise
141
+
142
+ async def send_audio(self, data: bytes, sample_rate: int) -> None:
143
+ if not self._audio_source or not self._connected:
144
+ return
145
+ if sample_rate != RETELL_SAMPLE_RATE:
146
+ resampler = self._resamplers.setdefault(
147
+ sample_rate,
148
+ PCMResampler(from_rate=sample_rate, to_rate=RETELL_SAMPLE_RATE),
149
+ )
150
+ data = resampler.convert(data)
151
+ await self._audio_source.capture_frame(
152
+ rtc.AudioFrame(
153
+ data=data,
154
+ sample_rate=RETELL_SAMPLE_RATE,
155
+ num_channels=1,
156
+ samples_per_channel=len(data) // 2,
157
+ )
158
+ )
159
+
160
+ async def recv_audio(self) -> AsyncIterator[tuple[bytes, int]]:
161
+ if self._track_future is None:
162
+ return
163
+ try:
164
+ track = await asyncio.wait_for(self._track_future, timeout=30.0)
165
+ except asyncio.TimeoutError as exc:
166
+ self._connected = False
167
+ raise RuntimeError("retell_webcall_agent_track_timeout") from exc
168
+ async for event in rtc.AudioStream(track):
169
+ frame = event.frame
170
+ yield frame.data.tobytes(), frame.sample_rate
171
+ self._connected = False
172
+
173
+ async def disconnect(self) -> None:
174
+ self._connected = False
175
+ if self._room:
176
+ await self._room.disconnect()
177
+
178
+ @property
179
+ def is_connected(self) -> bool:
180
+ return self._connected
181
+
182
+ @property
183
+ def is_agent_ready(self) -> bool:
184
+ return self._agent_ready.is_set()
185
+
186
+ @property
187
+ def call_id(self) -> str | None:
188
+ return self._call_id
@@ -0,0 +1,177 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ from collections.abc import AsyncIterator
7
+
8
+ import aiohttp
9
+
10
+ from fi.simulate.agent.definition import VapiTargetConfig
11
+ from fi.simulate.simulation.bridge.audio import PCMResampler
12
+ from fi.simulate.simulation.bridge.connector import ConnectorConfig, ProviderConnector
13
+
14
+ logger = logging.getLogger(__name__)
15
+ VAPI_SAMPLE_RATE = 16000
16
+
17
+
18
+ class VapiWebSocketConnector(ProviderConnector):
19
+ def __init__(self, config: ConnectorConfig) -> None:
20
+ self._config = config
21
+ self._session: aiohttp.ClientSession | None = None
22
+ self._ws: aiohttp.ClientWebSocketResponse | None = None
23
+ self._call_id: str | None = None
24
+ self._connected = False
25
+ self._resamplers: dict[int, PCMResampler] = {}
26
+
27
+ @classmethod
28
+ def from_target(
29
+ cls,
30
+ target: VapiTargetConfig,
31
+ *,
32
+ first_message_mode: str | None = None,
33
+ ) -> "VapiWebSocketConnector":
34
+ api_key = os.environ.get(target.api_key_env, "").strip()
35
+ if not api_key:
36
+ raise ValueError("vapi_websocket_config_missing: " + target.api_key_env)
37
+ return cls(
38
+ ConnectorConfig(
39
+ api_key=api_key,
40
+ assistant_id=target.assistant_id,
41
+ api_url=f"{str(target.api_base_url).rstrip('/')}/call",
42
+ first_message_mode=first_message_mode,
43
+ )
44
+ )
45
+
46
+ @classmethod
47
+ def from_env(cls) -> "VapiWebSocketConnector":
48
+ api_key = os.environ.get("VAPI_API_KEY", "").strip()
49
+ assistant_id = os.environ.get("VAPI_ASSISTANT_ID", "").strip()
50
+ missing = [
51
+ name
52
+ for name, value in (
53
+ ("VAPI_API_KEY", api_key),
54
+ ("VAPI_ASSISTANT_ID", assistant_id),
55
+ )
56
+ if not value
57
+ ]
58
+ if missing:
59
+ raise ValueError("vapi_websocket_config_missing: " + ", ".join(missing))
60
+ return cls.from_target(
61
+ VapiTargetConfig(
62
+ assistant_id=assistant_id,
63
+ api_base_url=os.environ.get("VAPI_API_BASE_URL", "https://api.vapi.ai"),
64
+ )
65
+ )
66
+
67
+ async def connect(self) -> None:
68
+ self._session = aiohttp.ClientSession()
69
+ try:
70
+ payload = {
71
+ "assistantId": self._config.assistant_id,
72
+ "transport": {
73
+ "provider": "vapi.websocket",
74
+ "audioFormat": {
75
+ "format": "pcm_s16le",
76
+ "container": "raw",
77
+ "sampleRate": VAPI_SAMPLE_RATE,
78
+ },
79
+ },
80
+ }
81
+ if self._config.first_message_mode:
82
+ payload["assistantOverrides"] = {
83
+ "firstMessageMode": self._config.first_message_mode,
84
+ }
85
+ async with self._session.post(
86
+ self._config.api_url,
87
+ headers={"Authorization": f"Bearer {self._config.api_key}"},
88
+ json=payload,
89
+ ) as response:
90
+ if response.status != 201:
91
+ raise RuntimeError(
92
+ f"vapi_websocket_call_create_failed:{response.status}"
93
+ )
94
+ payload = await response.json()
95
+ call_id = payload.get("id") if isinstance(payload, dict) else None
96
+ transport = payload.get("transport") if isinstance(payload, dict) else None
97
+ websocket_url = (
98
+ transport.get("websocketCallUrl")
99
+ if isinstance(transport, dict)
100
+ else None
101
+ )
102
+ if not isinstance(call_id, str) or not call_id.strip():
103
+ raise ValueError("vapi_websocket_response_missing_call_id")
104
+ if not isinstance(websocket_url, str) or not websocket_url.startswith(
105
+ ("ws://", "wss://")
106
+ ):
107
+ raise ValueError("vapi_websocket_response_missing_url")
108
+ self._call_id = call_id
109
+ self._ws = await self._session.ws_connect(websocket_url)
110
+ self._connected = True
111
+ logger.info("vapi_websocket_connected", extra={"call_id": call_id})
112
+ except Exception:
113
+ await self.disconnect()
114
+ raise
115
+
116
+ async def send_audio(self, data: bytes, sample_rate: int) -> None:
117
+ if not self._ws or self._ws.closed:
118
+ return
119
+ if sample_rate != VAPI_SAMPLE_RATE:
120
+ resampler = self._resamplers.setdefault(
121
+ sample_rate,
122
+ PCMResampler(from_rate=sample_rate, to_rate=VAPI_SAMPLE_RATE),
123
+ )
124
+ data = resampler.convert(data)
125
+ await self._ws.send_bytes(data)
126
+
127
+ async def recv_audio(self) -> AsyncIterator[tuple[bytes, int]]:
128
+ if not self._ws or self._ws.closed:
129
+ return
130
+ async for message in self._ws:
131
+ if message.type == aiohttp.WSMsgType.BINARY:
132
+ yield message.data, VAPI_SAMPLE_RATE
133
+ elif message.type == aiohttp.WSMsgType.TEXT:
134
+ if self._is_call_end_event(message.data):
135
+ logger.info(
136
+ "vapi_websocket_call_ended",
137
+ extra={"call_id": self._call_id},
138
+ )
139
+ break
140
+ elif message.type in {
141
+ aiohttp.WSMsgType.CLOSED,
142
+ aiohttp.WSMsgType.CLOSING,
143
+ aiohttp.WSMsgType.CLOSE,
144
+ aiohttp.WSMsgType.ERROR,
145
+ }:
146
+ break
147
+ self._connected = False
148
+
149
+ @staticmethod
150
+ def _is_call_end_event(payload: str) -> bool:
151
+ try:
152
+ data = json.loads(payload)
153
+ except ValueError:
154
+ return False
155
+ if not isinstance(data, dict):
156
+ return False
157
+ event_type = str(data.get("type") or "").lower()
158
+ if event_type in {"hangup", "call-ended", "end-of-call-report"}:
159
+ return True
160
+ return event_type == "status-update" and str(
161
+ data.get("status") or ""
162
+ ).lower() in {"ended", "ending"}
163
+
164
+ async def disconnect(self) -> None:
165
+ self._connected = False
166
+ if self._ws and not self._ws.closed:
167
+ await self._ws.close()
168
+ if self._session and not self._session.closed:
169
+ await self._session.close()
170
+
171
+ @property
172
+ def is_connected(self) -> bool:
173
+ return self._connected
174
+
175
+ @property
176
+ def call_id(self) -> str | None:
177
+ return self._call_id