PraisonAI 3.0.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 (393) hide show
  1. praisonai/__init__.py +54 -0
  2. praisonai/__main__.py +15 -0
  3. praisonai/acp/__init__.py +54 -0
  4. praisonai/acp/config.py +159 -0
  5. praisonai/acp/server.py +587 -0
  6. praisonai/acp/session.py +219 -0
  7. praisonai/adapters/__init__.py +50 -0
  8. praisonai/adapters/readers.py +395 -0
  9. praisonai/adapters/rerankers.py +315 -0
  10. praisonai/adapters/retrievers.py +394 -0
  11. praisonai/adapters/vector_stores.py +409 -0
  12. praisonai/agent_scheduler.py +337 -0
  13. praisonai/agents_generator.py +903 -0
  14. praisonai/api/call.py +292 -0
  15. praisonai/auto.py +1197 -0
  16. praisonai/capabilities/__init__.py +275 -0
  17. praisonai/capabilities/a2a.py +140 -0
  18. praisonai/capabilities/assistants.py +283 -0
  19. praisonai/capabilities/audio.py +320 -0
  20. praisonai/capabilities/batches.py +469 -0
  21. praisonai/capabilities/completions.py +336 -0
  22. praisonai/capabilities/container_files.py +155 -0
  23. praisonai/capabilities/containers.py +93 -0
  24. praisonai/capabilities/embeddings.py +158 -0
  25. praisonai/capabilities/files.py +467 -0
  26. praisonai/capabilities/fine_tuning.py +293 -0
  27. praisonai/capabilities/guardrails.py +182 -0
  28. praisonai/capabilities/images.py +330 -0
  29. praisonai/capabilities/mcp.py +190 -0
  30. praisonai/capabilities/messages.py +270 -0
  31. praisonai/capabilities/moderations.py +154 -0
  32. praisonai/capabilities/ocr.py +217 -0
  33. praisonai/capabilities/passthrough.py +204 -0
  34. praisonai/capabilities/rag.py +207 -0
  35. praisonai/capabilities/realtime.py +160 -0
  36. praisonai/capabilities/rerank.py +165 -0
  37. praisonai/capabilities/responses.py +266 -0
  38. praisonai/capabilities/search.py +109 -0
  39. praisonai/capabilities/skills.py +133 -0
  40. praisonai/capabilities/vector_store_files.py +334 -0
  41. praisonai/capabilities/vector_stores.py +304 -0
  42. praisonai/capabilities/videos.py +141 -0
  43. praisonai/chainlit_ui.py +304 -0
  44. praisonai/chat/__init__.py +106 -0
  45. praisonai/chat/app.py +125 -0
  46. praisonai/cli/__init__.py +26 -0
  47. praisonai/cli/app.py +213 -0
  48. praisonai/cli/commands/__init__.py +75 -0
  49. praisonai/cli/commands/acp.py +70 -0
  50. praisonai/cli/commands/completion.py +333 -0
  51. praisonai/cli/commands/config.py +166 -0
  52. praisonai/cli/commands/debug.py +142 -0
  53. praisonai/cli/commands/diag.py +55 -0
  54. praisonai/cli/commands/doctor.py +166 -0
  55. praisonai/cli/commands/environment.py +179 -0
  56. praisonai/cli/commands/lsp.py +112 -0
  57. praisonai/cli/commands/mcp.py +210 -0
  58. praisonai/cli/commands/profile.py +457 -0
  59. praisonai/cli/commands/run.py +228 -0
  60. praisonai/cli/commands/schedule.py +150 -0
  61. praisonai/cli/commands/serve.py +97 -0
  62. praisonai/cli/commands/session.py +212 -0
  63. praisonai/cli/commands/traces.py +145 -0
  64. praisonai/cli/commands/version.py +101 -0
  65. praisonai/cli/configuration/__init__.py +18 -0
  66. praisonai/cli/configuration/loader.py +353 -0
  67. praisonai/cli/configuration/paths.py +114 -0
  68. praisonai/cli/configuration/schema.py +164 -0
  69. praisonai/cli/features/__init__.py +268 -0
  70. praisonai/cli/features/acp.py +236 -0
  71. praisonai/cli/features/action_orchestrator.py +546 -0
  72. praisonai/cli/features/agent_scheduler.py +773 -0
  73. praisonai/cli/features/agent_tools.py +474 -0
  74. praisonai/cli/features/agents.py +375 -0
  75. praisonai/cli/features/at_mentions.py +471 -0
  76. praisonai/cli/features/auto_memory.py +182 -0
  77. praisonai/cli/features/autonomy_mode.py +490 -0
  78. praisonai/cli/features/background.py +356 -0
  79. praisonai/cli/features/base.py +168 -0
  80. praisonai/cli/features/capabilities.py +1326 -0
  81. praisonai/cli/features/checkpoints.py +338 -0
  82. praisonai/cli/features/code_intelligence.py +652 -0
  83. praisonai/cli/features/compaction.py +294 -0
  84. praisonai/cli/features/compare.py +534 -0
  85. praisonai/cli/features/cost_tracker.py +514 -0
  86. praisonai/cli/features/debug.py +810 -0
  87. praisonai/cli/features/deploy.py +517 -0
  88. praisonai/cli/features/diag.py +289 -0
  89. praisonai/cli/features/doctor/__init__.py +63 -0
  90. praisonai/cli/features/doctor/checks/__init__.py +24 -0
  91. praisonai/cli/features/doctor/checks/acp_checks.py +240 -0
  92. praisonai/cli/features/doctor/checks/config_checks.py +366 -0
  93. praisonai/cli/features/doctor/checks/db_checks.py +366 -0
  94. praisonai/cli/features/doctor/checks/env_checks.py +543 -0
  95. praisonai/cli/features/doctor/checks/lsp_checks.py +199 -0
  96. praisonai/cli/features/doctor/checks/mcp_checks.py +349 -0
  97. praisonai/cli/features/doctor/checks/memory_checks.py +268 -0
  98. praisonai/cli/features/doctor/checks/network_checks.py +251 -0
  99. praisonai/cli/features/doctor/checks/obs_checks.py +328 -0
  100. praisonai/cli/features/doctor/checks/performance_checks.py +235 -0
  101. praisonai/cli/features/doctor/checks/permissions_checks.py +259 -0
  102. praisonai/cli/features/doctor/checks/selftest_checks.py +322 -0
  103. praisonai/cli/features/doctor/checks/serve_checks.py +426 -0
  104. praisonai/cli/features/doctor/checks/skills_checks.py +231 -0
  105. praisonai/cli/features/doctor/checks/tools_checks.py +371 -0
  106. praisonai/cli/features/doctor/engine.py +266 -0
  107. praisonai/cli/features/doctor/formatters.py +310 -0
  108. praisonai/cli/features/doctor/handler.py +397 -0
  109. praisonai/cli/features/doctor/models.py +264 -0
  110. praisonai/cli/features/doctor/registry.py +239 -0
  111. praisonai/cli/features/endpoints.py +1019 -0
  112. praisonai/cli/features/eval.py +560 -0
  113. praisonai/cli/features/external_agents.py +231 -0
  114. praisonai/cli/features/fast_context.py +410 -0
  115. praisonai/cli/features/flow_display.py +566 -0
  116. praisonai/cli/features/git_integration.py +651 -0
  117. praisonai/cli/features/guardrail.py +171 -0
  118. praisonai/cli/features/handoff.py +185 -0
  119. praisonai/cli/features/hooks.py +583 -0
  120. praisonai/cli/features/image.py +384 -0
  121. praisonai/cli/features/interactive_runtime.py +585 -0
  122. praisonai/cli/features/interactive_tools.py +380 -0
  123. praisonai/cli/features/interactive_tui.py +603 -0
  124. praisonai/cli/features/jobs.py +632 -0
  125. praisonai/cli/features/knowledge.py +531 -0
  126. praisonai/cli/features/lite.py +244 -0
  127. praisonai/cli/features/lsp_cli.py +225 -0
  128. praisonai/cli/features/mcp.py +169 -0
  129. praisonai/cli/features/message_queue.py +587 -0
  130. praisonai/cli/features/metrics.py +211 -0
  131. praisonai/cli/features/n8n.py +673 -0
  132. praisonai/cli/features/observability.py +293 -0
  133. praisonai/cli/features/ollama.py +361 -0
  134. praisonai/cli/features/output_style.py +273 -0
  135. praisonai/cli/features/package.py +631 -0
  136. praisonai/cli/features/performance.py +308 -0
  137. praisonai/cli/features/persistence.py +636 -0
  138. praisonai/cli/features/profile.py +226 -0
  139. praisonai/cli/features/profiler/__init__.py +81 -0
  140. praisonai/cli/features/profiler/core.py +558 -0
  141. praisonai/cli/features/profiler/optimizations.py +652 -0
  142. praisonai/cli/features/profiler/suite.py +386 -0
  143. praisonai/cli/features/profiling.py +350 -0
  144. praisonai/cli/features/queue/__init__.py +73 -0
  145. praisonai/cli/features/queue/manager.py +395 -0
  146. praisonai/cli/features/queue/models.py +286 -0
  147. praisonai/cli/features/queue/persistence.py +564 -0
  148. praisonai/cli/features/queue/scheduler.py +484 -0
  149. praisonai/cli/features/queue/worker.py +372 -0
  150. praisonai/cli/features/recipe.py +1723 -0
  151. praisonai/cli/features/recipes.py +449 -0
  152. praisonai/cli/features/registry.py +229 -0
  153. praisonai/cli/features/repo_map.py +860 -0
  154. praisonai/cli/features/router.py +466 -0
  155. praisonai/cli/features/sandbox_executor.py +515 -0
  156. praisonai/cli/features/serve.py +829 -0
  157. praisonai/cli/features/session.py +222 -0
  158. praisonai/cli/features/skills.py +856 -0
  159. praisonai/cli/features/slash_commands.py +650 -0
  160. praisonai/cli/features/telemetry.py +179 -0
  161. praisonai/cli/features/templates.py +1384 -0
  162. praisonai/cli/features/thinking.py +305 -0
  163. praisonai/cli/features/todo.py +334 -0
  164. praisonai/cli/features/tools.py +680 -0
  165. praisonai/cli/features/tui/__init__.py +83 -0
  166. praisonai/cli/features/tui/app.py +580 -0
  167. praisonai/cli/features/tui/cli.py +566 -0
  168. praisonai/cli/features/tui/debug.py +511 -0
  169. praisonai/cli/features/tui/events.py +99 -0
  170. praisonai/cli/features/tui/mock_provider.py +328 -0
  171. praisonai/cli/features/tui/orchestrator.py +652 -0
  172. praisonai/cli/features/tui/screens/__init__.py +50 -0
  173. praisonai/cli/features/tui/screens/main.py +245 -0
  174. praisonai/cli/features/tui/screens/queue.py +174 -0
  175. praisonai/cli/features/tui/screens/session.py +124 -0
  176. praisonai/cli/features/tui/screens/settings.py +148 -0
  177. praisonai/cli/features/tui/widgets/__init__.py +56 -0
  178. praisonai/cli/features/tui/widgets/chat.py +261 -0
  179. praisonai/cli/features/tui/widgets/composer.py +224 -0
  180. praisonai/cli/features/tui/widgets/queue_panel.py +200 -0
  181. praisonai/cli/features/tui/widgets/status.py +167 -0
  182. praisonai/cli/features/tui/widgets/tool_panel.py +248 -0
  183. praisonai/cli/features/workflow.py +720 -0
  184. praisonai/cli/legacy.py +236 -0
  185. praisonai/cli/main.py +5559 -0
  186. praisonai/cli/schedule_cli.py +54 -0
  187. praisonai/cli/state/__init__.py +31 -0
  188. praisonai/cli/state/identifiers.py +161 -0
  189. praisonai/cli/state/sessions.py +313 -0
  190. praisonai/code/__init__.py +93 -0
  191. praisonai/code/agent_tools.py +344 -0
  192. praisonai/code/diff/__init__.py +21 -0
  193. praisonai/code/diff/diff_strategy.py +432 -0
  194. praisonai/code/tools/__init__.py +27 -0
  195. praisonai/code/tools/apply_diff.py +221 -0
  196. praisonai/code/tools/execute_command.py +275 -0
  197. praisonai/code/tools/list_files.py +274 -0
  198. praisonai/code/tools/read_file.py +206 -0
  199. praisonai/code/tools/search_replace.py +248 -0
  200. praisonai/code/tools/write_file.py +217 -0
  201. praisonai/code/utils/__init__.py +46 -0
  202. praisonai/code/utils/file_utils.py +307 -0
  203. praisonai/code/utils/ignore_utils.py +308 -0
  204. praisonai/code/utils/text_utils.py +276 -0
  205. praisonai/db/__init__.py +64 -0
  206. praisonai/db/adapter.py +531 -0
  207. praisonai/deploy/__init__.py +62 -0
  208. praisonai/deploy/api.py +231 -0
  209. praisonai/deploy/docker.py +454 -0
  210. praisonai/deploy/doctor.py +367 -0
  211. praisonai/deploy/main.py +327 -0
  212. praisonai/deploy/models.py +179 -0
  213. praisonai/deploy/providers/__init__.py +33 -0
  214. praisonai/deploy/providers/aws.py +331 -0
  215. praisonai/deploy/providers/azure.py +358 -0
  216. praisonai/deploy/providers/base.py +101 -0
  217. praisonai/deploy/providers/gcp.py +314 -0
  218. praisonai/deploy/schema.py +208 -0
  219. praisonai/deploy.py +185 -0
  220. praisonai/endpoints/__init__.py +53 -0
  221. praisonai/endpoints/a2u_server.py +410 -0
  222. praisonai/endpoints/discovery.py +165 -0
  223. praisonai/endpoints/providers/__init__.py +28 -0
  224. praisonai/endpoints/providers/a2a.py +253 -0
  225. praisonai/endpoints/providers/a2u.py +208 -0
  226. praisonai/endpoints/providers/agents_api.py +171 -0
  227. praisonai/endpoints/providers/base.py +231 -0
  228. praisonai/endpoints/providers/mcp.py +263 -0
  229. praisonai/endpoints/providers/recipe.py +206 -0
  230. praisonai/endpoints/providers/tools_mcp.py +150 -0
  231. praisonai/endpoints/registry.py +131 -0
  232. praisonai/endpoints/server.py +161 -0
  233. praisonai/inbuilt_tools/__init__.py +24 -0
  234. praisonai/inbuilt_tools/autogen_tools.py +117 -0
  235. praisonai/inc/__init__.py +2 -0
  236. praisonai/inc/config.py +96 -0
  237. praisonai/inc/models.py +155 -0
  238. praisonai/integrations/__init__.py +56 -0
  239. praisonai/integrations/base.py +303 -0
  240. praisonai/integrations/claude_code.py +270 -0
  241. praisonai/integrations/codex_cli.py +255 -0
  242. praisonai/integrations/cursor_cli.py +195 -0
  243. praisonai/integrations/gemini_cli.py +222 -0
  244. praisonai/jobs/__init__.py +67 -0
  245. praisonai/jobs/executor.py +425 -0
  246. praisonai/jobs/models.py +230 -0
  247. praisonai/jobs/router.py +314 -0
  248. praisonai/jobs/server.py +186 -0
  249. praisonai/jobs/store.py +203 -0
  250. praisonai/llm/__init__.py +66 -0
  251. praisonai/llm/registry.py +382 -0
  252. praisonai/mcp_server/__init__.py +152 -0
  253. praisonai/mcp_server/adapters/__init__.py +74 -0
  254. praisonai/mcp_server/adapters/agents.py +128 -0
  255. praisonai/mcp_server/adapters/capabilities.py +168 -0
  256. praisonai/mcp_server/adapters/cli_tools.py +568 -0
  257. praisonai/mcp_server/adapters/extended_capabilities.py +462 -0
  258. praisonai/mcp_server/adapters/knowledge.py +93 -0
  259. praisonai/mcp_server/adapters/memory.py +104 -0
  260. praisonai/mcp_server/adapters/prompts.py +306 -0
  261. praisonai/mcp_server/adapters/resources.py +124 -0
  262. praisonai/mcp_server/adapters/tools_bridge.py +280 -0
  263. praisonai/mcp_server/auth/__init__.py +48 -0
  264. praisonai/mcp_server/auth/api_key.py +291 -0
  265. praisonai/mcp_server/auth/oauth.py +460 -0
  266. praisonai/mcp_server/auth/oidc.py +289 -0
  267. praisonai/mcp_server/auth/scopes.py +260 -0
  268. praisonai/mcp_server/cli.py +852 -0
  269. praisonai/mcp_server/elicitation.py +445 -0
  270. praisonai/mcp_server/icons.py +302 -0
  271. praisonai/mcp_server/recipe_adapter.py +573 -0
  272. praisonai/mcp_server/recipe_cli.py +824 -0
  273. praisonai/mcp_server/registry.py +703 -0
  274. praisonai/mcp_server/sampling.py +422 -0
  275. praisonai/mcp_server/server.py +490 -0
  276. praisonai/mcp_server/tasks.py +443 -0
  277. praisonai/mcp_server/transports/__init__.py +18 -0
  278. praisonai/mcp_server/transports/http_stream.py +376 -0
  279. praisonai/mcp_server/transports/stdio.py +132 -0
  280. praisonai/persistence/__init__.py +84 -0
  281. praisonai/persistence/config.py +238 -0
  282. praisonai/persistence/conversation/__init__.py +25 -0
  283. praisonai/persistence/conversation/async_mysql.py +427 -0
  284. praisonai/persistence/conversation/async_postgres.py +410 -0
  285. praisonai/persistence/conversation/async_sqlite.py +371 -0
  286. praisonai/persistence/conversation/base.py +151 -0
  287. praisonai/persistence/conversation/json_store.py +250 -0
  288. praisonai/persistence/conversation/mysql.py +387 -0
  289. praisonai/persistence/conversation/postgres.py +401 -0
  290. praisonai/persistence/conversation/singlestore.py +240 -0
  291. praisonai/persistence/conversation/sqlite.py +341 -0
  292. praisonai/persistence/conversation/supabase.py +203 -0
  293. praisonai/persistence/conversation/surrealdb.py +287 -0
  294. praisonai/persistence/factory.py +301 -0
  295. praisonai/persistence/hooks/__init__.py +18 -0
  296. praisonai/persistence/hooks/agent_hooks.py +297 -0
  297. praisonai/persistence/knowledge/__init__.py +26 -0
  298. praisonai/persistence/knowledge/base.py +144 -0
  299. praisonai/persistence/knowledge/cassandra.py +232 -0
  300. praisonai/persistence/knowledge/chroma.py +295 -0
  301. praisonai/persistence/knowledge/clickhouse.py +242 -0
  302. praisonai/persistence/knowledge/cosmosdb_vector.py +438 -0
  303. praisonai/persistence/knowledge/couchbase.py +286 -0
  304. praisonai/persistence/knowledge/lancedb.py +216 -0
  305. praisonai/persistence/knowledge/langchain_adapter.py +291 -0
  306. praisonai/persistence/knowledge/lightrag_adapter.py +212 -0
  307. praisonai/persistence/knowledge/llamaindex_adapter.py +256 -0
  308. praisonai/persistence/knowledge/milvus.py +277 -0
  309. praisonai/persistence/knowledge/mongodb_vector.py +306 -0
  310. praisonai/persistence/knowledge/pgvector.py +335 -0
  311. praisonai/persistence/knowledge/pinecone.py +253 -0
  312. praisonai/persistence/knowledge/qdrant.py +301 -0
  313. praisonai/persistence/knowledge/redis_vector.py +291 -0
  314. praisonai/persistence/knowledge/singlestore_vector.py +299 -0
  315. praisonai/persistence/knowledge/surrealdb_vector.py +309 -0
  316. praisonai/persistence/knowledge/upstash_vector.py +266 -0
  317. praisonai/persistence/knowledge/weaviate.py +223 -0
  318. praisonai/persistence/migrations/__init__.py +10 -0
  319. praisonai/persistence/migrations/manager.py +251 -0
  320. praisonai/persistence/orchestrator.py +406 -0
  321. praisonai/persistence/state/__init__.py +21 -0
  322. praisonai/persistence/state/async_mongodb.py +200 -0
  323. praisonai/persistence/state/base.py +107 -0
  324. praisonai/persistence/state/dynamodb.py +226 -0
  325. praisonai/persistence/state/firestore.py +175 -0
  326. praisonai/persistence/state/gcs.py +155 -0
  327. praisonai/persistence/state/memory.py +245 -0
  328. praisonai/persistence/state/mongodb.py +158 -0
  329. praisonai/persistence/state/redis.py +190 -0
  330. praisonai/persistence/state/upstash.py +144 -0
  331. praisonai/persistence/tests/__init__.py +3 -0
  332. praisonai/persistence/tests/test_all_backends.py +633 -0
  333. praisonai/profiler.py +1214 -0
  334. praisonai/recipe/__init__.py +134 -0
  335. praisonai/recipe/bridge.py +278 -0
  336. praisonai/recipe/core.py +893 -0
  337. praisonai/recipe/exceptions.py +54 -0
  338. praisonai/recipe/history.py +402 -0
  339. praisonai/recipe/models.py +266 -0
  340. praisonai/recipe/operations.py +440 -0
  341. praisonai/recipe/policy.py +422 -0
  342. praisonai/recipe/registry.py +849 -0
  343. praisonai/recipe/runtime.py +214 -0
  344. praisonai/recipe/security.py +711 -0
  345. praisonai/recipe/serve.py +859 -0
  346. praisonai/recipe/server.py +613 -0
  347. praisonai/scheduler/__init__.py +45 -0
  348. praisonai/scheduler/agent_scheduler.py +552 -0
  349. praisonai/scheduler/base.py +124 -0
  350. praisonai/scheduler/daemon_manager.py +225 -0
  351. praisonai/scheduler/state_manager.py +155 -0
  352. praisonai/scheduler/yaml_loader.py +193 -0
  353. praisonai/scheduler.py +194 -0
  354. praisonai/setup/__init__.py +1 -0
  355. praisonai/setup/build.py +21 -0
  356. praisonai/setup/post_install.py +23 -0
  357. praisonai/setup/setup_conda_env.py +25 -0
  358. praisonai/setup.py +16 -0
  359. praisonai/templates/__init__.py +116 -0
  360. praisonai/templates/cache.py +364 -0
  361. praisonai/templates/dependency_checker.py +358 -0
  362. praisonai/templates/discovery.py +391 -0
  363. praisonai/templates/loader.py +564 -0
  364. praisonai/templates/registry.py +511 -0
  365. praisonai/templates/resolver.py +206 -0
  366. praisonai/templates/security.py +327 -0
  367. praisonai/templates/tool_override.py +498 -0
  368. praisonai/templates/tools_doctor.py +256 -0
  369. praisonai/test.py +105 -0
  370. praisonai/train.py +562 -0
  371. praisonai/train_vision.py +306 -0
  372. praisonai/ui/agents.py +824 -0
  373. praisonai/ui/callbacks.py +57 -0
  374. praisonai/ui/chainlit_compat.py +246 -0
  375. praisonai/ui/chat.py +532 -0
  376. praisonai/ui/code.py +717 -0
  377. praisonai/ui/colab.py +474 -0
  378. praisonai/ui/colab_chainlit.py +81 -0
  379. praisonai/ui/components/aicoder.py +284 -0
  380. praisonai/ui/context.py +283 -0
  381. praisonai/ui/database_config.py +56 -0
  382. praisonai/ui/db.py +294 -0
  383. praisonai/ui/realtime.py +488 -0
  384. praisonai/ui/realtimeclient/__init__.py +756 -0
  385. praisonai/ui/realtimeclient/tools.py +242 -0
  386. praisonai/ui/sql_alchemy.py +710 -0
  387. praisonai/upload_vision.py +140 -0
  388. praisonai/version.py +1 -0
  389. praisonai-3.0.0.dist-info/METADATA +3493 -0
  390. praisonai-3.0.0.dist-info/RECORD +393 -0
  391. praisonai-3.0.0.dist-info/WHEEL +5 -0
  392. praisonai-3.0.0.dist-info/entry_points.txt +4 -0
  393. praisonai-3.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,756 @@
1
+ # Derived from https://github.com/openai/openai-realtime-console. Will integrate with Chainlit when more mature.
2
+
3
+ import os
4
+ import asyncio
5
+ import inspect
6
+ import numpy as np
7
+ import json
8
+ import websockets
9
+ from websockets.exceptions import ConnectionClosed
10
+ from datetime import datetime
11
+ from collections import defaultdict
12
+ import base64
13
+
14
+ from chainlit.logger import logger
15
+ from chainlit.config import config
16
+
17
+
18
+ def float_to_16bit_pcm(float32_array):
19
+ """
20
+ Converts a numpy array of float32 amplitude data to a numpy array in int16 format.
21
+ :param float32_array: numpy array of float32
22
+ :return: numpy array of int16
23
+ """
24
+ int16_array = np.clip(float32_array, -1, 1) * 32767
25
+ return int16_array.astype(np.int16)
26
+
27
+ def base64_to_array_buffer(base64_string):
28
+ """
29
+ Converts a base64 string to a numpy array buffer.
30
+ :param base64_string: base64 encoded string
31
+ :return: numpy array of uint8
32
+ """
33
+ binary_data = base64.b64decode(base64_string)
34
+ return np.frombuffer(binary_data, dtype=np.uint8)
35
+
36
+ def array_buffer_to_base64(array_buffer):
37
+ """
38
+ Converts a numpy array buffer to a base64 string.
39
+ :param array_buffer: numpy array
40
+ :return: base64 encoded string
41
+ """
42
+ if array_buffer.dtype == np.float32:
43
+ array_buffer = float_to_16bit_pcm(array_buffer)
44
+ elif array_buffer.dtype == np.int16:
45
+ array_buffer = array_buffer.tobytes()
46
+ else:
47
+ array_buffer = array_buffer.tobytes()
48
+
49
+ return base64.b64encode(array_buffer).decode('utf-8')
50
+
51
+ def merge_int16_arrays(left, right):
52
+ """
53
+ Merge two numpy arrays of int16.
54
+ :param left: numpy array of int16
55
+ :param right: numpy array of int16
56
+ :return: merged numpy array of int16
57
+ """
58
+ if isinstance(left, np.ndarray) and left.dtype == np.int16 and isinstance(right, np.ndarray) and right.dtype == np.int16:
59
+ return np.concatenate((left, right))
60
+ else:
61
+ raise ValueError("Both items must be numpy arrays of int16")
62
+
63
+
64
+ class RealtimeEventHandler:
65
+ def __init__(self):
66
+ self.event_handlers = defaultdict(list)
67
+
68
+ def on(self, event_name, handler):
69
+ self.event_handlers[event_name].append(handler)
70
+
71
+ def clear_event_handlers(self):
72
+ self.event_handlers = defaultdict(list)
73
+
74
+ def dispatch(self, event_name, event):
75
+ for handler in self.event_handlers[event_name]:
76
+ if inspect.iscoroutinefunction(handler):
77
+ asyncio.create_task(handler(event))
78
+ else:
79
+ handler(event)
80
+
81
+ async def wait_for_next(self, event_name):
82
+ future = asyncio.Future()
83
+
84
+ def handler(event):
85
+ if not future.done():
86
+ future.set_result(event)
87
+
88
+ self.on(event_name, handler)
89
+ return await future
90
+
91
+
92
+ class RealtimeAPI(RealtimeEventHandler):
93
+ def __init__(self, url=None, api_key=None):
94
+ super().__init__()
95
+ self.default_url = 'wss://api.openai.com/v1/realtime'
96
+
97
+ # Support custom base URL from environment variable
98
+ base_url = os.getenv("OPENAI_BASE_URL")
99
+ if base_url:
100
+ # Convert HTTP/HTTPS base URL to WebSocket URL for realtime API
101
+ if base_url.startswith('https://'):
102
+ ws_url = base_url.replace('https://', 'wss://').rstrip('/') + '/realtime'
103
+ elif base_url.startswith('http://'):
104
+ ws_url = base_url.replace('http://', 'ws://').rstrip('/') + '/realtime'
105
+ else:
106
+ # Assume it's already a WebSocket URL
107
+ ws_url = base_url.rstrip('/') + '/realtime' if not base_url.endswith('/realtime') else base_url
108
+ self.url = url or ws_url
109
+ else:
110
+ self.url = url or self.default_url
111
+
112
+ self.api_key = api_key or os.getenv("OPENAI_API_KEY")
113
+ self.ws = None
114
+
115
+ def is_connected(self):
116
+ if self.ws is None:
117
+ return False
118
+ # Some websockets versions don't have a closed attribute
119
+ try:
120
+ return not self.ws.closed
121
+ except AttributeError:
122
+ # Fallback: check if websocket is still alive by checking state
123
+ try:
124
+ return hasattr(self.ws, 'state') and self.ws.state.name == 'OPEN'
125
+ except:
126
+ # Last fallback: assume connected if ws exists
127
+ return True
128
+
129
+ def log(self, *args):
130
+ logger.debug(f"[Websocket/{datetime.utcnow().isoformat()}]", *args)
131
+
132
+ async def connect(self, model='gpt-5-nano-realtime-preview-2024-12-17'):
133
+ if self.is_connected():
134
+ raise Exception("Already connected")
135
+
136
+ headers = {
137
+ 'Authorization': f'Bearer {self.api_key}',
138
+ 'OpenAI-Beta': 'realtime=v1'
139
+ }
140
+
141
+ # Try different header parameter names for compatibility
142
+ try:
143
+ self.ws = await websockets.connect(f"{self.url}?model={model}", additional_headers=headers)
144
+ except TypeError:
145
+ # Fallback to older websockets versions
146
+ try:
147
+ self.ws = await websockets.connect(f"{self.url}?model={model}", extra_headers=headers)
148
+ except TypeError:
149
+ # Last fallback - some versions might not support headers parameter
150
+ raise Exception("Websockets library version incompatible. Please update websockets to version 11.0 or higher.")
151
+
152
+ self.log(f"Connected to {self.url}")
153
+ asyncio.create_task(self._receive_messages())
154
+
155
+ async def _receive_messages(self):
156
+ try:
157
+ async for message in self.ws:
158
+ event = json.loads(message)
159
+ if event['type'] == "error":
160
+ logger.error(f"OpenAI Realtime API Error: {event}")
161
+ self.log("received:", event)
162
+ self.dispatch(f"server.{event['type']}", event)
163
+ self.dispatch("server.*", event)
164
+ except ConnectionClosed as e:
165
+ logger.info(f"WebSocket connection closed normally: {e}")
166
+ # Mark connection as closed
167
+ self.ws = None
168
+ # Dispatch disconnection event
169
+ self.dispatch("disconnected", {"reason": str(e)})
170
+ except Exception as e:
171
+ logger.warning(f"WebSocket receive loop ended: {e}")
172
+ # Mark connection as closed
173
+ self.ws = None
174
+ # Dispatch disconnection event
175
+ self.dispatch("disconnected", {"reason": str(e)})
176
+
177
+ async def send(self, event_name, data=None):
178
+ if not self.is_connected():
179
+ raise Exception("RealtimeAPI is not connected")
180
+ data = data or {}
181
+ if not isinstance(data, dict):
182
+ raise Exception("data must be a dictionary")
183
+ event = {
184
+ "event_id": self._generate_id("evt_"),
185
+ "type": event_name,
186
+ **data
187
+ }
188
+ self.dispatch(f"client.{event_name}", event)
189
+ self.dispatch("client.*", event)
190
+ self.log("sent:", event)
191
+
192
+ try:
193
+ await self.ws.send(json.dumps(event))
194
+ except ConnectionClosed as e:
195
+ logger.info(f"WebSocket connection closed during send: {e}")
196
+ # Mark connection as closed if send fails
197
+ self.ws = None
198
+ raise Exception(f"WebSocket connection lost: {e}")
199
+ except Exception as e:
200
+ logger.error(f"Failed to send WebSocket message: {e}")
201
+ # Mark connection as closed if send fails
202
+ self.ws = None
203
+ raise Exception(f"WebSocket connection lost: {e}")
204
+
205
+ def _generate_id(self, prefix):
206
+ return f"{prefix}{int(datetime.utcnow().timestamp() * 1000)}"
207
+
208
+ async def disconnect(self):
209
+ if self.ws:
210
+ try:
211
+ await self.ws.close()
212
+ logger.info(f"Disconnected from {self.url}")
213
+ except Exception as e:
214
+ logger.warning(f"Error during WebSocket close: {e}")
215
+ finally:
216
+ self.ws = None
217
+ self.log(f"WebSocket connection cleaned up")
218
+
219
+ class RealtimeConversation:
220
+ default_frequency = config.features.audio.sample_rate
221
+
222
+ EventProcessors = {
223
+ 'conversation.item.created': lambda self, event: self._process_item_created(event),
224
+ 'conversation.item.truncated': lambda self, event: self._process_item_truncated(event),
225
+ 'conversation.item.deleted': lambda self, event: self._process_item_deleted(event),
226
+ 'conversation.item.input_audio_transcription.completed': lambda self, event: self._process_input_audio_transcription_completed(event),
227
+ 'input_audio_buffer.speech_started': lambda self, event: self._process_speech_started(event),
228
+ 'input_audio_buffer.speech_stopped': lambda self, event, input_audio_buffer: self._process_speech_stopped(event, input_audio_buffer),
229
+ 'response.created': lambda self, event: self._process_response_created(event),
230
+ 'response.output_item.added': lambda self, event: self._process_output_item_added(event),
231
+ 'response.output_item.done': lambda self, event: self._process_output_item_done(event),
232
+ 'response.content_part.added': lambda self, event: self._process_content_part_added(event),
233
+ 'response.audio_transcript.delta': lambda self, event: self._process_audio_transcript_delta(event),
234
+ 'response.audio.delta': lambda self, event: self._process_audio_delta(event),
235
+ 'response.text.delta': lambda self, event: self._process_text_delta(event),
236
+ 'response.function_call_arguments.delta': lambda self, event: self._process_function_call_arguments_delta(event),
237
+ }
238
+
239
+ def __init__(self):
240
+ self.clear()
241
+
242
+ def clear(self):
243
+ self.item_lookup = {}
244
+ self.items = []
245
+ self.response_lookup = {}
246
+ self.responses = []
247
+ self.queued_speech_items = {}
248
+ self.queued_transcript_items = {}
249
+ self.queued_input_audio = None
250
+
251
+ def queue_input_audio(self, input_audio):
252
+ self.queued_input_audio = input_audio
253
+
254
+ def process_event(self, event, *args):
255
+ event_processor = self.EventProcessors.get(event['type'])
256
+ if not event_processor:
257
+ raise Exception(f"Missing conversation event processor for {event['type']}")
258
+ return event_processor(self, event, *args)
259
+
260
+ def get_item(self, id):
261
+ return self.item_lookup.get(id)
262
+
263
+ def get_items(self):
264
+ return self.items[:]
265
+
266
+ def _process_item_created(self, event):
267
+ item = event['item']
268
+ new_item = item.copy()
269
+ if new_item['id'] not in self.item_lookup:
270
+ self.item_lookup[new_item['id']] = new_item
271
+ self.items.append(new_item)
272
+ new_item['formatted'] = {
273
+ 'audio': [],
274
+ 'text': '',
275
+ 'transcript': ''
276
+ }
277
+ if new_item['id'] in self.queued_speech_items:
278
+ new_item['formatted']['audio'] = self.queued_speech_items[new_item['id']]['audio']
279
+ del self.queued_speech_items[new_item['id']]
280
+ if 'content' in new_item:
281
+ text_content = [c for c in new_item['content'] if c['type'] in ['text', 'input_text']]
282
+ for content in text_content:
283
+ new_item['formatted']['text'] += content['text']
284
+ if new_item['id'] in self.queued_transcript_items:
285
+ new_item['formatted']['transcript'] = self.queued_transcript_items[new_item['id']]['transcript']
286
+ del self.queued_transcript_items[new_item['id']]
287
+ if new_item['type'] == 'message':
288
+ if new_item['role'] == 'user':
289
+ new_item['status'] = 'completed'
290
+ if self.queued_input_audio:
291
+ new_item['formatted']['audio'] = self.queued_input_audio
292
+ self.queued_input_audio = None
293
+ else:
294
+ new_item['status'] = 'in_progress'
295
+ elif new_item['type'] == 'function_call':
296
+ new_item['formatted']['tool'] = {
297
+ 'type': 'function',
298
+ 'name': new_item['name'],
299
+ 'call_id': new_item['call_id'],
300
+ 'arguments': ''
301
+ }
302
+ new_item['status'] = 'in_progress'
303
+ elif new_item['type'] == 'function_call_output':
304
+ new_item['status'] = 'completed'
305
+ new_item['formatted']['output'] = new_item['output']
306
+ return new_item, None
307
+
308
+ def _process_item_truncated(self, event):
309
+ item_id = event['item_id']
310
+ audio_end_ms = event['audio_end_ms']
311
+ item = self.item_lookup.get(item_id)
312
+ if not item:
313
+ raise Exception(f'item.truncated: Item "{item_id}" not found')
314
+ end_index = (audio_end_ms * self.default_frequency) // 1000
315
+ item['formatted']['transcript'] = ''
316
+ item['formatted']['audio'] = item['formatted']['audio'][:end_index]
317
+ return item, None
318
+
319
+ def _process_item_deleted(self, event):
320
+ item_id = event['item_id']
321
+ item = self.item_lookup.get(item_id)
322
+ if not item:
323
+ raise Exception(f'item.deleted: Item "{item_id}" not found')
324
+ del self.item_lookup[item['id']]
325
+ self.items.remove(item)
326
+ return item, None
327
+
328
+ def _process_input_audio_transcription_completed(self, event):
329
+ item_id = event['item_id']
330
+ content_index = event['content_index']
331
+ transcript = event['transcript']
332
+ formatted_transcript = transcript or ' '
333
+ item = self.item_lookup.get(item_id)
334
+ if not item:
335
+ self.queued_transcript_items[item_id] = {'transcript': formatted_transcript}
336
+ return None, None
337
+ item['content'][content_index]['transcript'] = transcript
338
+ item['formatted']['transcript'] = formatted_transcript
339
+ return item, {'transcript': transcript}
340
+
341
+ def _process_speech_started(self, event):
342
+ item_id = event['item_id']
343
+ audio_start_ms = event['audio_start_ms']
344
+ self.queued_speech_items[item_id] = {'audio_start_ms': audio_start_ms}
345
+ return None, None
346
+
347
+ def _process_speech_stopped(self, event, input_audio_buffer):
348
+ item_id = event['item_id']
349
+ audio_end_ms = event['audio_end_ms']
350
+ speech = self.queued_speech_items[item_id]
351
+ speech['audio_end_ms'] = audio_end_ms
352
+ if input_audio_buffer:
353
+ start_index = (speech['audio_start_ms'] * self.default_frequency) // 1000
354
+ end_index = (speech['audio_end_ms'] * self.default_frequency) // 1000
355
+ speech['audio'] = input_audio_buffer[start_index:end_index]
356
+ return None, None
357
+
358
+ def _process_response_created(self, event):
359
+ response = event['response']
360
+ if response['id'] not in self.response_lookup:
361
+ self.response_lookup[response['id']] = response
362
+ self.responses.append(response)
363
+ return None, None
364
+
365
+ def _process_output_item_added(self, event):
366
+ response_id = event['response_id']
367
+ item = event['item']
368
+ response = self.response_lookup.get(response_id)
369
+ if not response:
370
+ raise Exception(f'response.output_item.added: Response "{response_id}" not found')
371
+ response['output'].append(item['id'])
372
+ return None, None
373
+
374
+ def _process_output_item_done(self, event):
375
+ item = event['item']
376
+ if not item:
377
+ raise Exception('response.output_item.done: Missing "item"')
378
+ found_item = self.item_lookup.get(item['id'])
379
+ if not found_item:
380
+ raise Exception(f'response.output_item.done: Item "{item["id"]}" not found')
381
+ found_item['status'] = item['status']
382
+ return found_item, None
383
+
384
+ def _process_content_part_added(self, event):
385
+ item_id = event['item_id']
386
+ part = event['part']
387
+ item = self.item_lookup.get(item_id)
388
+ if not item:
389
+ raise Exception(f'response.content_part.added: Item "{item_id}" not found')
390
+ item['content'].append(part)
391
+ return item, None
392
+
393
+ def _process_audio_transcript_delta(self, event):
394
+ item_id = event['item_id']
395
+ content_index = event['content_index']
396
+ delta = event['delta']
397
+ item = self.item_lookup.get(item_id)
398
+ if not item:
399
+ raise Exception(f'response.audio_transcript.delta: Item "{item_id}" not found')
400
+ item['content'][content_index]['transcript'] += delta
401
+ item['formatted']['transcript'] += delta
402
+ return item, {'transcript': delta}
403
+
404
+ def _process_audio_delta(self, event):
405
+ item_id = event['item_id']
406
+ content_index = event['content_index']
407
+ delta = event['delta']
408
+ item = self.item_lookup.get(item_id)
409
+ if not item:
410
+ logger.debug(f'response.audio.delta: Item "{item_id}" not found')
411
+ return None, None
412
+ array_buffer = base64_to_array_buffer(delta)
413
+ append_values = array_buffer.tobytes()
414
+ item['formatted']['audio'].append(append_values)
415
+ return item, {'audio': append_values}
416
+
417
+ def _process_text_delta(self, event):
418
+ item_id = event['item_id']
419
+ content_index = event['content_index']
420
+ delta = event['delta']
421
+ item = self.item_lookup.get(item_id)
422
+ if not item:
423
+ raise Exception(f'response.text.delta: Item "{item_id}" not found')
424
+ item['content'][content_index]['text'] += delta
425
+ item['formatted']['text'] += delta
426
+ return item, {'text': delta}
427
+
428
+ def _process_function_call_arguments_delta(self, event):
429
+ item_id = event['item_id']
430
+ delta = event['delta']
431
+ item = self.item_lookup.get(item_id)
432
+ if not item:
433
+ raise Exception(f'response.function_call_arguments.delta: Item "{item_id}" not found')
434
+ item['arguments'] += delta
435
+ item['formatted']['tool']['arguments'] += delta
436
+ return item, {'arguments': delta}
437
+
438
+
439
+ class RealtimeClient(RealtimeEventHandler):
440
+ def __init__(self, url=None, api_key=None):
441
+ super().__init__()
442
+ self.default_session_config = {
443
+ "modalities": ["text", "audio"],
444
+ "instructions": "System settings:\nTool use: enabled.\n\nInstructions:\n- You are an artificial intelligence agent responsible for helping test realtime voice capabilities\n- Please make sure to respond with a helpful voice via audio\n- Be kind, helpful, and curteous\n- It is okay to ask the user questions\n- Use tools and functions you have available liberally, it is part of the training apparatus\n- Be open to exploration and conversation\n- Remember: this is just for fun and testing!\n\nPersonality:\n- Be upbeat and genuine\n- Try speaking quickly as if excited\n",
445
+ "voice": "shimmer",
446
+ "input_audio_format": "pcm16",
447
+ "output_audio_format": "pcm16",
448
+ "input_audio_transcription": { "model": 'whisper-1' },
449
+ "turn_detection": { "type": 'server_vad' },
450
+ "tools": [],
451
+ "tool_choice": "auto",
452
+ "temperature": 0.8,
453
+ }
454
+ self.session_config = {}
455
+ self.transcription_models = [{"model": "whisper-1"}]
456
+ self.default_server_vad_config = {
457
+ "type": "server_vad",
458
+ "threshold": 0.5,
459
+ "prefix_padding_ms": 300,
460
+ "silence_duration_ms": 200,
461
+ }
462
+ self.realtime = RealtimeAPI(url, api_key)
463
+ self.conversation = RealtimeConversation()
464
+ self._reset_config()
465
+ self._add_api_event_handlers()
466
+
467
+ def _reset_config(self):
468
+ self.session_created = False
469
+ self.tools = {}
470
+ self.session_config = self.default_session_config.copy()
471
+ self.input_audio_buffer = bytearray()
472
+ return True
473
+
474
+ def _add_api_event_handlers(self):
475
+ self.realtime.on("client.*", self._log_event)
476
+ self.realtime.on("server.*", self._log_event)
477
+ self.realtime.on("server.session.created", self._on_session_created)
478
+ self.realtime.on("server.response.created", self._process_event)
479
+ self.realtime.on("server.response.output_item.added", self._process_event)
480
+ self.realtime.on("server.response.content_part.added", self._process_event)
481
+ self.realtime.on("server.input_audio_buffer.speech_started", self._on_speech_started)
482
+ self.realtime.on("server.input_audio_buffer.speech_stopped", self._on_speech_stopped)
483
+ self.realtime.on("server.conversation.item.created", self._on_item_created)
484
+ self.realtime.on("server.conversation.item.truncated", self._process_event)
485
+ self.realtime.on("server.conversation.item.deleted", self._process_event)
486
+ self.realtime.on("server.conversation.item.input_audio_transcription.completed", self._process_event)
487
+ self.realtime.on("server.response.audio_transcript.delta", self._process_event)
488
+ self.realtime.on("server.response.audio.delta", self._process_event)
489
+ self.realtime.on("server.response.text.delta", self._process_event)
490
+ self.realtime.on("server.response.function_call_arguments.delta", self._process_event)
491
+ self.realtime.on("server.response.output_item.done", self._on_output_item_done)
492
+
493
+ def _log_event(self, event):
494
+ realtime_event = {
495
+ "time": datetime.utcnow().isoformat(),
496
+ "source": "client" if event["type"].startswith("client.") else "server",
497
+ "event": event,
498
+ }
499
+ self.dispatch("realtime.event", realtime_event)
500
+
501
+ def _on_session_created(self, event):
502
+ try:
503
+ session_id = event.get('session', {}).get('id', 'unknown')
504
+ model = event.get('session', {}).get('model', 'unknown')
505
+ logger.info(f"OpenAI Realtime session created - ID: {session_id}, Model: {model}")
506
+ except Exception as e:
507
+ logger.warning(f"Error processing session created event: {e}")
508
+ logger.debug(f"Session event details: {event}")
509
+ self.session_created = True
510
+
511
+ def _process_event(self, event, *args):
512
+ item, delta = self.conversation.process_event(event, *args)
513
+ if item:
514
+ self.dispatch("conversation.updated", {"item": item, "delta": delta})
515
+ return item, delta
516
+
517
+ def _on_speech_started(self, event):
518
+ self._process_event(event)
519
+ self.dispatch("conversation.interrupted", event)
520
+
521
+ def _on_speech_stopped(self, event):
522
+ self._process_event(event, self.input_audio_buffer)
523
+
524
+ def _on_item_created(self, event):
525
+ item, delta = self._process_event(event)
526
+ self.dispatch("conversation.item.appended", {"item": item})
527
+ if item and item["status"] == "completed":
528
+ self.dispatch("conversation.item.completed", {"item": item})
529
+
530
+ async def _on_output_item_done(self, event):
531
+ item, delta = self._process_event(event)
532
+ if item and item["status"] == "completed":
533
+ self.dispatch("conversation.item.completed", {"item": item})
534
+ if item and item.get("formatted", {}).get("tool"):
535
+ await self._call_tool(item["formatted"]["tool"])
536
+
537
+ async def _call_tool(self, tool):
538
+ try:
539
+ json_arguments = json.loads(tool["arguments"])
540
+ tool_config = self.tools.get(tool["name"])
541
+ if not tool_config:
542
+ raise Exception(f'Tool "{tool["name"]}" has not been added')
543
+ result = await tool_config["handler"](**json_arguments)
544
+ await self.realtime.send("conversation.item.create", {
545
+ "item": {
546
+ "type": "function_call_output",
547
+ "call_id": tool["call_id"],
548
+ "output": json.dumps(result),
549
+ }
550
+ })
551
+ except Exception as e:
552
+ error_message = json.dumps({"error": str(e)})
553
+ logger.error(f"Tool call error: {error_message}")
554
+ await self.realtime.send("conversation.item.create", {
555
+ "item": {
556
+ "type": "function_call_output",
557
+ "call_id": tool["call_id"],
558
+ "output": error_message,
559
+ }
560
+ })
561
+ await self.create_response()
562
+
563
+ def is_connected(self):
564
+ return self.realtime.is_connected()
565
+
566
+ def reset(self):
567
+ self.disconnect()
568
+ self.realtime.clear_event_handlers()
569
+ self._reset_config()
570
+ self._add_api_event_handlers()
571
+ return True
572
+
573
+ async def connect(self, model=None):
574
+ if self.is_connected():
575
+ raise Exception("Already connected, use .disconnect() first")
576
+
577
+ # Use provided model, OPENAI_MODEL_NAME environment variable, or default
578
+ if model is None:
579
+ model = os.getenv("OPENAI_MODEL_NAME", 'gpt-5-nano-realtime-preview-2024-12-17')
580
+
581
+ await self.realtime.connect(model)
582
+ await self.update_session()
583
+ return True
584
+
585
+ async def wait_for_session_created(self):
586
+ if not self.is_connected():
587
+ raise Exception("Not connected, use .connect() first")
588
+ while not self.session_created:
589
+ await asyncio.sleep(0.001)
590
+ return True
591
+
592
+ async def disconnect(self):
593
+ self.session_created = False
594
+ self.conversation.clear()
595
+ if self.realtime.is_connected():
596
+ await self.realtime.disconnect()
597
+ logger.info("RealtimeClient disconnected")
598
+
599
+ def get_turn_detection_type(self):
600
+ return self.session_config.get("turn_detection", {}).get("type")
601
+
602
+ async def add_tool(self, definition, handler):
603
+ if not definition.get("name"):
604
+ raise Exception("Missing tool name in definition")
605
+ name = definition["name"]
606
+ if name in self.tools:
607
+ raise Exception(f'Tool "{name}" already added. Please use .removeTool("{name}") before trying to add again.')
608
+ if not callable(handler):
609
+ raise Exception(f'Tool "{name}" handler must be a function')
610
+ self.tools[name] = {"definition": definition, "handler": handler}
611
+ await self.update_session()
612
+ return self.tools[name]
613
+
614
+ def remove_tool(self, name):
615
+ if name not in self.tools:
616
+ raise Exception(f'Tool "{name}" does not exist, can not be removed.')
617
+ del self.tools[name]
618
+ return True
619
+
620
+ async def delete_item(self, id):
621
+ await self.realtime.send("conversation.item.delete", {"item_id": id})
622
+ return True
623
+
624
+ async def update_session(self, **kwargs):
625
+ self.session_config.update(kwargs)
626
+ use_tools = [
627
+ {**tool_definition, "type": "function"}
628
+ for tool_definition in self.session_config.get("tools", [])
629
+ ] + [
630
+ {**self.tools[key]["definition"], "type": "function"}
631
+ for key in self.tools
632
+ ]
633
+ session = {**self.session_config, "tools": use_tools}
634
+ logger.debug(f"Updating session: {session}")
635
+ if self.realtime.is_connected():
636
+ await self.realtime.send("session.update", {"session": session})
637
+ return True
638
+
639
+ async def create_conversation_item(self, item):
640
+ await self.realtime.send("conversation.item.create", {
641
+ "item": item
642
+ })
643
+
644
+ async def send_user_message_content(self, content=[]):
645
+ if content:
646
+ for c in content:
647
+ if c["type"] == "input_audio":
648
+ if isinstance(c["audio"], (bytes, bytearray)):
649
+ c["audio"] = array_buffer_to_base64(c["audio"])
650
+ await self.realtime.send("conversation.item.create", {
651
+ "item": {
652
+ "type": "message",
653
+ "role": "user",
654
+ "content": content,
655
+ }
656
+ })
657
+ await self.create_response()
658
+ return True
659
+
660
+ async def append_input_audio(self, array_buffer):
661
+ if not self.is_connected():
662
+ logger.warning("Cannot append audio: RealtimeClient is not connected")
663
+ return False
664
+
665
+ if len(array_buffer) > 0:
666
+ try:
667
+ await self.realtime.send("input_audio_buffer.append", {
668
+ "audio": array_buffer_to_base64(np.array(array_buffer)),
669
+ })
670
+ self.input_audio_buffer.extend(array_buffer)
671
+ except Exception as e:
672
+ logger.error(f"Failed to append input audio: {e}")
673
+ # Connection might be lost, mark as disconnected
674
+ if "connection" in str(e).lower() or "closed" in str(e).lower():
675
+ logger.warning("WebSocket connection appears to be lost. Audio input will be queued until reconnection.")
676
+ return False
677
+ return True
678
+
679
+ async def create_response(self):
680
+ if self.get_turn_detection_type() is None and len(self.input_audio_buffer) > 0:
681
+ await self.realtime.send("input_audio_buffer.commit")
682
+ self.conversation.queue_input_audio(self.input_audio_buffer)
683
+ self.input_audio_buffer = bytearray()
684
+ await self.realtime.send("response.create")
685
+ return True
686
+
687
+ async def cancel_response(self, id=None, sample_count=0):
688
+ if not id:
689
+ await self.realtime.send("response.cancel")
690
+ return {"item": None}
691
+ else:
692
+ item = self.conversation.get_item(id)
693
+ if not item:
694
+ raise Exception(f'Could not find item "{id}"')
695
+ if item["type"] != "message":
696
+ raise Exception('Can only cancelResponse messages with type "message"')
697
+ if item["role"] != "assistant":
698
+ raise Exception('Can only cancelResponse messages with role "assistant"')
699
+ await self.realtime.send("response.cancel")
700
+ audio_index = next((i for i, c in enumerate(item["content"]) if c["type"] == "audio"), -1)
701
+ if audio_index == -1:
702
+ raise Exception("Could not find audio on item to cancel")
703
+ await self.realtime.send("conversation.item.truncate", {
704
+ "item_id": id,
705
+ "content_index": audio_index,
706
+ "audio_end_ms": int((sample_count / self.conversation.default_frequency) * 1000),
707
+ })
708
+ return {"item": item}
709
+
710
+ async def wait_for_next_item(self):
711
+ event = await self.wait_for_next("conversation.item.appended")
712
+ return {"item": event["item"]}
713
+
714
+ async def wait_for_next_completed_item(self):
715
+ event = await self.wait_for_next("conversation.item.completed")
716
+ return {"item": event["item"]}
717
+
718
+ async def _send_chainlit_message(self, item):
719
+ import chainlit as cl
720
+
721
+ # Debug logging
722
+ logger.debug(f"Received item structure: {json.dumps({k: type(v).__name__ for k, v in item.items()}, indent=2)}")
723
+
724
+ if "type" in item and item["type"] == "function_call_output":
725
+ # Don't send function call outputs directly to Chainlit
726
+ logger.debug(f"Function call output received: {item.get('output', '')}")
727
+ elif "role" in item:
728
+ if item["role"] == "user":
729
+ content = item.get("formatted", {}).get("text", "") or item.get("formatted", {}).get("transcript", "")
730
+ if content:
731
+ await cl.Message(content=content, author="User").send()
732
+ elif item["role"] == "assistant":
733
+ content = item.get("formatted", {}).get("text", "") or item.get("formatted", {}).get("transcript", "")
734
+ if content:
735
+ await cl.Message(content=content, author="AI").send()
736
+ else:
737
+ logger.warning(f"Unhandled role: {item['role']}")
738
+ else:
739
+ # Handle items without a 'role' or 'type'
740
+ logger.debug(f"Unhandled item type:\n{json.dumps(item, indent=2)}")
741
+
742
+ # Additional debug logging
743
+ logger.debug(f"Processed Chainlit message for item: {item.get('id', 'unknown')}")
744
+
745
+ async def ensure_connected(self):
746
+ """Check connection health and attempt reconnection if needed"""
747
+ if not self.is_connected():
748
+ try:
749
+ logger.info("Attempting to reconnect to OpenAI Realtime API...")
750
+ model = os.getenv("OPENAI_MODEL_NAME", 'gpt-5-nano-realtime-preview-2024-12-17')
751
+ await self.connect(model)
752
+ return True
753
+ except Exception as e:
754
+ logger.error(f"Failed to reconnect: {e}")
755
+ return False
756
+ return True