dv-pipecat-flows 0.0.0.dev2786__tar.gz → 0.0.0.dev2828__tar.gz

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 (128) hide show
  1. {dv_pipecat_flows-0.0.0.dev2786/src/dv_pipecat_flows.egg-info → dv_pipecat_flows-0.0.0.dev2828}/PKG-INFO +1 -1
  2. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828/src/dv_pipecat_flows.egg-info}/PKG-INFO +1 -1
  3. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/src/dv_pipecat_flows.egg-info/scm_file_list.json +10 -0
  4. dv_pipecat_flows-0.0.0.dev2828/src/dv_pipecat_flows.egg-info/scm_version.json +8 -0
  5. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/src/pipecat_flows/manager.py +107 -42
  6. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/src/pipecat_flows/processors/router_mode_guard.py +11 -0
  7. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/src/pipecat_flows/processors/user_turn_observer.py +32 -11
  8. dv_pipecat_flows-0.0.0.dev2786/src/dv_pipecat_flows.egg-info/scm_version.json +0 -8
  9. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/.agents/skills/loki-logs/SKILL.md +0 -0
  10. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/.agents/skills/loki-logs/query-reference.md +0 -0
  11. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/.claude/skills/loki-logs/SKILL.md +0 -0
  12. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/.claude/skills/loki-logs/query-reference.md +0 -0
  13. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/.gitattributes +0 -0
  14. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/.gitignore +0 -0
  15. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/.pre-commit-config.yaml +0 -0
  16. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/.python-version +0 -0
  17. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/.readthedocs.yaml +0 -0
  18. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/AGENTS.md +0 -0
  19. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/CHANGELOG.md +0 -0
  20. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/CLAUDE.md +0 -0
  21. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/CONTRIBUTING.md +0 -0
  22. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/LICENSE +0 -0
  23. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/MANIFEST.in +0 -0
  24. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/README.md +0 -0
  25. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/.env.example +0 -0
  26. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/.gitignore +0 -0
  27. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/README.md +0 -0
  28. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/docs/ARCHITECTURE.md +0 -0
  29. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/docs/CODE_MAP.md +0 -0
  30. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/docs/CONFIG.md +0 -0
  31. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/docs/CONTRACTS.md +0 -0
  32. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/docs/COPILOT_TODO.md +0 -0
  33. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/docs/DISTRIBUTION.md +0 -0
  34. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/docs/HLD.md +0 -0
  35. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/docs/METHODS.md +0 -0
  36. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/docs/VOICE_E2E.md +0 -0
  37. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/eval/ANALYSIS_stagehand_vs_hybrid.md +0 -0
  38. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/eval/RESULTS.md +0 -0
  39. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/eval/_env.py +0 -0
  40. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/eval/bench.py +0 -0
  41. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/eval/bench_browseruse.py +0 -0
  42. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/eval/bench_stagehand.py +0 -0
  43. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/eval/inspect_browseruse.py +0 -0
  44. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/eval/inspect_browseruse_showcase.py +0 -0
  45. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/eval/inspect_stagehand_prompt.py +0 -0
  46. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/eval/pages/checkout.html +0 -0
  47. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/eval/pages/dashboard.html +0 -0
  48. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/eval/pages/signup.html +0 -0
  49. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/eval/results_browseruse.json +0 -0
  50. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/eval/results_perception.json +0 -0
  51. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/eval/results_stagehand.json +0 -0
  52. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/eval/tasks.py +0 -0
  53. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/eval/try_hybrid.py +0 -0
  54. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/eval/try_hybrid_multistep.py +0 -0
  55. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/eval/try_hybrid_oneshot.py +0 -0
  56. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/pyproject.toml +0 -0
  57. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/scripts/dev-stack.sh +0 -0
  58. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/browser_copilot/uv.lock +0 -0
  59. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/dev-requirements.txt +0 -0
  60. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/docker-compose.dev.yml +0 -0
  61. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/.eslintrc.json +0 -0
  62. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/.prettierrc +0 -0
  63. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/css/tailwind.css +0 -0
  64. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/examples/food_ordering.json +0 -0
  65. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/examples/movie_explorer.json +0 -0
  66. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/examples/patient_intake.json +0 -0
  67. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/examples/restaurant_reservation.json +0 -0
  68. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/examples/travel_planner.json +0 -0
  69. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/favicon.png +0 -0
  70. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/favicon.svg +0 -0
  71. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/index.html +0 -0
  72. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/js/editor/canvas.js +0 -0
  73. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/js/editor/editorState.js +0 -0
  74. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/js/editor/sidePanel.js +0 -0
  75. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/js/editor/toolbar.js +0 -0
  76. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/js/main.js +0 -0
  77. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/js/nodes/baseNode.js +0 -0
  78. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/js/nodes/endNode.js +0 -0
  79. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/js/nodes/flowNode.js +0 -0
  80. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/js/nodes/functionNode.js +0 -0
  81. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/js/nodes/index.js +0 -0
  82. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/js/nodes/mergeNode.js +0 -0
  83. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/js/nodes/startNode.js +0 -0
  84. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/js/types.js +0 -0
  85. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/js/utils/export.js +0 -0
  86. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/js/utils/helpers.js +0 -0
  87. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/js/utils/import.js +0 -0
  88. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/js/utils/validation.js +0 -0
  89. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/jsdoc.json +0 -0
  90. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/package-lock.json +0 -0
  91. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/package.json +0 -0
  92. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/postcss.config.cjs +0 -0
  93. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/public/favicon.png +0 -0
  94. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/public/favicon.svg +0 -0
  95. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/tailwind.config.cjs +0 -0
  96. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/vercel.json +0 -0
  97. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/editor/vite.config.js +0 -0
  98. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/engine_primitives_plan.md +0 -0
  99. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/env.example +0 -0
  100. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/images/food-ordering-flow.png +0 -0
  101. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/pipecat-flows.png +0 -0
  102. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/pipecat_upgrade.md +0 -0
  103. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/pyproject.toml +0 -0
  104. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/remote-asterisk-code/README.md +0 -0
  105. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/remote-asterisk-code/extensions.conf +0 -0
  106. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/remote-asterisk-code/rtp.conf +0 -0
  107. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/requirements.txt +0 -0
  108. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/scripts/azure_latency_probe.py +0 -0
  109. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/scripts/check-pypi-package.py +0 -0
  110. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/scripts/fix-ruff.sh +0 -0
  111. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/scripts/langfuse_export.py +0 -0
  112. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/scripts/pre-commit.sh +0 -0
  113. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/setup.cfg +0 -0
  114. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/src/dv_pipecat_flows.egg-info/SOURCES.txt +0 -0
  115. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/src/dv_pipecat_flows.egg-info/dependency_links.txt +0 -0
  116. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/src/dv_pipecat_flows.egg-info/requires.txt +0 -0
  117. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/src/dv_pipecat_flows.egg-info/top_level.txt +0 -0
  118. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/src/pipecat_flows/__init__.py +0 -0
  119. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/src/pipecat_flows/actions.py +0 -0
  120. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/src/pipecat_flows/adapters.py +0 -0
  121. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/src/pipecat_flows/condition_evaluator.py +0 -0
  122. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/src/pipecat_flows/exceptions.py +0 -0
  123. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/src/pipecat_flows/flow_validator.py +0 -0
  124. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/src/pipecat_flows/processors/__init__.py +0 -0
  125. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/src/pipecat_flows/processors/speak_interruption_guard.py +0 -0
  126. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/src/pipecat_flows/router_mode.py +0 -0
  127. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/src/pipecat_flows/types.py +0 -0
  128. {dv_pipecat_flows-0.0.0.dev2786 → dv_pipecat_flows-0.0.0.dev2828}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dv-pipecat-flows
3
- Version: 0.0.0.dev2786
3
+ Version: 0.0.0.dev2828
4
4
  Summary: Conversation Flow management for Pipecat AI applications
5
5
  License: BSD 2-Clause License
6
6
  Project-URL: Source, https://github.com/pipecat-ai/pipecat-flows
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dv-pipecat-flows
3
- Version: 0.0.0.dev2786
3
+ Version: 0.0.0.dev2828
4
4
  Summary: Conversation Flow management for Pipecat AI applications
5
5
  License: BSD 2-Clause License
6
6
  Project-URL: Source, https://github.com/pipecat-ai/pipecat-flows
@@ -333,6 +333,7 @@
333
333
  "ringg-bot/ringg_processors/realtime_audio_suppressor.py",
334
334
  "ringg-bot/ringg_processors/realtime_tts_bridge.py",
335
335
  "ringg-bot/ringg_processors/screen_auto_injector.py",
336
+ "ringg-bot/ringg_processors/tts_cache.py",
336
337
  "ringg-bot/ringg_processors/two_stage_user_idle_processor.py",
337
338
  "ringg-bot/ringg_processors/user_activity_reset_processor.py",
338
339
  "ringg-bot/scripts/delete_agent_cache.py",
@@ -359,6 +360,7 @@
359
360
  "ringg-bot/test_bot.py",
360
361
  "ringg-bot/tests/__init__.py",
361
362
  "ringg-bot/tests/conftest.py",
363
+ "ringg-bot/tests/test_abandoned_call_report.py",
362
364
  "ringg-bot/tests/test_action_call_variables.py",
363
365
  "ringg-bot/tests/test_app_metrics.py",
364
366
  "ringg-bot/tests/test_artifact_store.py",
@@ -381,8 +383,10 @@
381
383
  "ringg-bot/tests/test_dom_context_injector.py",
382
384
  "ringg-bot/tests/test_dropped_turn_regeneration.py",
383
385
  "ringg-bot/tests/test_end_call_schema.py",
386
+ "ringg-bot/tests/test_end_call_teardown.py",
384
387
  "ringg-bot/tests/test_env_config_asterisk_ami.py",
385
388
  "ringg-bot/tests/test_execute_plan.py",
389
+ "ringg-bot/tests/test_find_button_api_config.py",
386
390
  "ringg-bot/tests/test_flow_query_kb.py",
387
391
  "ringg-bot/tests/test_gcs_fetch.py",
388
392
  "ringg-bot/tests/test_http_client_diagnostics.py",
@@ -409,6 +413,7 @@
409
413
  "ringg-bot/tests/test_merge_parallel_tool_responses.py",
410
414
  "ringg-bot/tests/test_metrics_bridge.py",
411
415
  "ringg-bot/tests/test_metrics_collector_llm.py",
416
+ "ringg-bot/tests/test_no_bot_calls_are_failed.py",
412
417
  "ringg-bot/tests/test_on_call_api_pre_call_message.py",
413
418
  "ringg-bot/tests/test_on_call_jq_metadata.py",
414
419
  "ringg-bot/tests/test_on_call_sdk_context.py",
@@ -443,6 +448,7 @@
443
448
  "ringg-bot/tests/test_transcript_timestamp_format.py",
444
449
  "ringg-bot/tests/test_transfer_custom_vars.py",
445
450
  "ringg-bot/tests/test_transfer_summary_api_tool.py",
451
+ "ringg-bot/tests/test_tts_cache.py",
446
452
  "ringg-bot/tests/test_tts_currency.py",
447
453
  "ringg-bot/tests/test_variable_replacement.py",
448
454
  "ringg-bot/tests/test_wandb_llm.py",
@@ -554,6 +560,7 @@
554
560
  "ringg-bot/utils/tts.py",
555
561
  "ringg-bot/utils/twilio_async_client.py",
556
562
  "ringg-bot/utils/warning_messages.py",
563
+ "ringg-bot/utils/widget_transcript.py",
557
564
  "ringg-bot/utils/word_timestamp_logger.py",
558
565
  "ringg-bot/voice_services/asterisk/asterisk_service.py",
559
566
  "ringg-bot/voice_services/asterisk/router.py",
@@ -647,8 +654,11 @@
647
654
  "tests/test_openai_realtime_transcript_filter.py",
648
655
  "tests/test_response_formatters.py",
649
656
  "tests/test_router_extract_schema.py",
657
+ "tests/test_router_mode_guard.py",
650
658
  "tests/test_router_no_unclear.py",
659
+ "tests/test_router_wait_idle.py",
651
660
  "tests/test_tts_split_cache.py",
661
+ "tests/test_user_turn_observer.py",
652
662
  "tests/test_voicemail_detector.py",
653
663
  "uv.lock"
654
664
  ]
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.0.0.dev0",
3
+ "distance": 2828,
4
+ "node": "gf48d50520527927107c4f155d372cd8fdc6d77c2",
5
+ "dirty": false,
6
+ "branch": "dv-stage",
7
+ "node_date": "2026-08-12"
8
+ }
@@ -48,7 +48,7 @@ from typing import (
48
48
 
49
49
  from loguru import logger
50
50
  from utils.tracking_helpers import start_tracking, complete_tracking
51
- from utils.collect_normalizers import is_repeat_request, normalize_collect_value
51
+ from utils.collect_normalizers import normalize_collect_value
52
52
  from utils.collect_prompts import collect_prompt
53
53
  from pipecat.frames.frames import (
54
54
  DTMFUpdateSettingsFrame,
@@ -1872,6 +1872,24 @@ In all of these cases, you can provide a `name` in your new node's config for de
1872
1872
  label="jump",
1873
1873
  )
1874
1874
 
1875
+ def _last_context_role(self) -> Optional[str]:
1876
+ """Role of the last message in the live LLM context, or None when no
1877
+ context aggregator / empty context. Used by the router wait gate to
1878
+ tell a real committed user turn apart from system-generated context
1879
+ pushes (e.g. the idle warning's LLM-path append)."""
1880
+ if not self._context_aggregator:
1881
+ return None
1882
+ ctx = self._context_aggregator.user()._context
1883
+ messages = (
1884
+ ctx.get_messages() if hasattr(ctx, "get_messages") else getattr(ctx, "messages", [])
1885
+ )
1886
+ if not messages:
1887
+ return None
1888
+ last = messages[-1]
1889
+ if isinstance(last, dict):
1890
+ return last.get("role")
1891
+ return getattr(last, "role", None)
1892
+
1875
1893
  async def _run_router_node(
1876
1894
  self, node_id: str, node_config: Dict[str, Any]
1877
1895
  ) -> Optional[str]:
@@ -1905,10 +1923,19 @@ In all of these cases, you can provide a `name` in your new node's config for de
1905
1923
  # `_run_router_node` captures a fresh entry_ts — the next iteration
1906
1924
  # correctly waits for a NEW user reply instead of reclassifying the
1907
1925
  # stale one that triggered the previous UNCLEAR.
1926
+ #
1927
+ # Silence handling is delegated to the user-idle processor: we re-arm
1928
+ # it for the duration of the wait (it was stopped at `_set_node`
1929
+ # entry) and block until the user actually replies. If they never do,
1930
+ # the idle processor's warning → end stages fire and end the call —
1931
+ # the router never classifies stale silence. The idle warning's
1932
+ # fixed-phrase TTSSpeakFrame passes RouterModeGuard; its LLM-path
1933
+ # fallback (realtime/chat-only/unsupported languages) is dropped by
1934
+ # the guard, so that warning is silent but the end stage still ends
1935
+ # the call.
1908
1936
  observer = self._user_turn_observer
1909
1937
 
1910
- timeout_s = float(node_config.get("router_wait_for_user_timeout", 15.0))
1911
- if observer is not None and timeout_s > 0:
1938
+ if observer is not None:
1912
1939
  entry_ts = time.monotonic()
1913
1940
  if observer.last_user_stopped_ts >= entry_ts:
1914
1941
  logger.debug(
@@ -1929,28 +1956,41 @@ In all of these cases, you can provide a `name` in your new node's config for de
1929
1956
  )
1930
1957
  else:
1931
1958
  logger.debug(
1932
- f"Router '{node_id}': waiting up to {timeout_s}s for "
1933
- f"UserStoppedSpeakingFrame"
1959
+ f"Router '{node_id}': waiting for "
1960
+ f"UserStoppedSpeakingFrame (idle processor armed)"
1934
1961
  )
1962
+ await self._task.queue_frame(StartUserIdleProcessorFrame())
1935
1963
  try:
1936
- await asyncio.wait_for(
1937
- observer.event.wait(), timeout=timeout_s
1938
- )
1939
- logger.debug(
1940
- f"Router '{node_id}': user reply received after "
1941
- f"{time.monotonic() - entry_ts:.2f}s"
1942
- )
1943
- except asyncio.TimeoutError:
1944
- # User stayed silent. Don't raise — let the router
1945
- # LLM classify whatever context is available; it
1946
- # will most likely return UNCLEAR, which the
1947
- # existing pick_branch_target machinery routes to
1948
- # reprompt or exhausted_node.
1949
- logger.info(
1950
- f"Router '{node_id}': no user reply within "
1951
- f"{timeout_s}s; proceeding (will likely route "
1952
- f"to unclear/exhausted)"
1964
+ while True:
1965
+ await observer.event.wait()
1966
+ # The observer fires on EVERY OpenAILLMContextFrame,
1967
+ # and with idle armed there IS a competing source:
1968
+ # the idle warning's LLM path (chat-only sessions,
1969
+ # realtime LLMs) appends its prompt with
1970
+ # run_llm=True, which makes the user aggregator push
1971
+ # a context frame with NO new user turn. Accept the
1972
+ # wake only when the last context message is the
1973
+ # user's; otherwise keep waiting for the real reply
1974
+ # (or for idle end-stage to end the call).
1975
+ role = self._last_context_role()
1976
+ if role is None or role == "user":
1977
+ break
1978
+ logger.debug(
1979
+ f"Router '{node_id}': ignoring context push "
1980
+ f"with last role '{role}' (idle warning, not a "
1981
+ f"user reply); still waiting"
1982
+ )
1983
+ observer.event.clear()
1984
+ finally:
1985
+ # Restore the "idle stopped while the node is
1986
+ # processing" invariant for the classifier call.
1987
+ await self._task.queue_frame(
1988
+ StopUserIdleProcessorFrame()
1953
1989
  )
1990
+ logger.debug(
1991
+ f"Router '{node_id}': user reply received after "
1992
+ f"{time.monotonic() - entry_ts:.2f}s"
1993
+ )
1954
1994
 
1955
1995
  branches = node_config.get("router_branches") or []
1956
1996
  extract = node_config.get("router_extract") or {}
@@ -2649,6 +2689,7 @@ In all of these cases, you can provide a `name` in your new node's config for de
2649
2689
  """
2650
2690
  self._collect_readback = "" # grouped, language-matched read-back for the confirm
2651
2691
  self._collect_reply = "" # LLM free reply for a conversational (non-digit) turn
2692
+ self._collect_repeat = False # LLM-decided: caller asked to hear the digits back
2652
2693
  context_messages = [
2653
2694
  {"role": "user", "content": turn}
2654
2695
  for turn in turns
@@ -2692,6 +2733,14 @@ In all of these cases, you can provide a `name` in your new node's config for de
2692
2733
  "the number; make no promises or commitments."
2693
2734
  ),
2694
2735
  },
2736
+ "repeat_request": {
2737
+ "type": "boolean",
2738
+ "description": (
2739
+ "true ONLY if the caller's latest message asks to hear back the "
2740
+ "digits captured so far; false when they are offering to dictate "
2741
+ "again themselves."
2742
+ ),
2743
+ },
2695
2744
  },
2696
2745
  )
2697
2746
  try:
@@ -2709,6 +2758,11 @@ In all of these cases, you can provide a `name` in your new node's config for de
2709
2758
  "empty and put a SHORT on-topic reply in `reply`: answer in one "
2710
2759
  "sentence, then ask again for the number — stay on topic, make no "
2711
2760
  "promises.\n"
2761
+ "Set `repeat_request` to true ONLY when the caller's latest message "
2762
+ "asks to hear back what has been captured so far ('repeat that', "
2763
+ "'what do you have?', 'दोबारा बोलिए'). An offer to dictate again "
2764
+ "('मैं फिर से बताती हूँ', 'I'll tell you again') is NOT a repeat "
2765
+ "request.\n"
2712
2766
  "Digits: read each spoken number-word in the order spoken and join its "
2713
2767
  "digits left to right — a digit-word adds one digit, 'double X' adds "
2714
2768
  "XX, 'triple X' adds XXX (in any language, incl. डबल/ट्रिपल), and a lone "
@@ -2738,6 +2792,7 @@ In all of these cases, you can provide a `name` in your new node's config for de
2738
2792
  reply = (extracted or {}).get("reply")
2739
2793
  self._collect_readback = readback.strip() if isinstance(readback, str) else ""
2740
2794
  self._collect_reply = reply.strip() if isinstance(reply, str) else ""
2795
+ self._collect_repeat = bool((extracted or {}).get("repeat_request"))
2741
2796
  return re.sub(r"\D", "", value) if isinstance(value, str) else ""
2742
2797
 
2743
2798
  async def _reextract_if_corrected(
@@ -2899,9 +2954,10 @@ In all of these cases, you can provide a `name` in your new node's config for de
2899
2954
  # the CORRECTED number at once — instead of asking "what should it
2900
2955
  # be?" and making them repeat what they just said. The idempotent
2901
2956
  # extractor merges "no, the first part is 807" into 70823… →
2902
- # 8072341303. KEEP start_ts throughout so corrections resolve against
2903
- # the ORIGINAL number. Only a plain rejection with no new digits
2904
- # falls through to the re-ask. Bounded by ``corrections``.
2957
+ # 8072341303. Only a plain rejection with no new digits falls
2958
+ # through to the re-ask — and starts FRESH (start_ts advanced), so
2959
+ # the rejected number's turns can't concatenate into the caller's
2960
+ # re-dictation (live call 2a3f6513). Bounded by ``corrections``.
2905
2961
  while True:
2906
2962
  confirmed = await self._confirm_collected(
2907
2963
  node_id, getattr(self, "_collect_readback", "") or " ".join(buffer)
@@ -2918,7 +2974,9 @@ In all of these cases, you can provide a `name` in your new node's config for de
2918
2974
  if corrected is not None:
2919
2975
  buffer, normalized = corrected
2920
2976
  continue # re-read the corrected number, no "what should it be?"
2977
+ start_ts = time.monotonic()
2921
2978
  buffer = ""
2979
+ echoed = ""
2922
2980
  await self._task.queue_frame(
2923
2981
  TTSSpeakFrame(text=collect_prompt(language, "restart_denied"))
2924
2982
  )
@@ -2947,8 +3005,7 @@ In all of these cases, you can provide a `name` in your new node's config for de
2947
3005
  # correction during the echo phase ("no, the 3rd is 5": 7765 → 7755)
2948
3006
  # changes the value but not the length, so a length check would mis-read
2949
3007
  # the fix as "no progress" and burn a miss instead of echoing it below.
2950
- turns = self._collect_turns_since(start_ts)
2951
- if buffer and turns and is_repeat_request(turns[-1]):
3008
+ if buffer and getattr(self, "_collect_repeat", False):
2952
3009
  # Re-reads bypass the miss counter, so the absolute deadline is the
2953
3010
  # only bound — without it a caller repeating "say it again" loops
2954
3011
  # forever (a turn lands every cycle, never silence, never a miss).
@@ -3052,7 +3109,9 @@ In all of these cases, you can provide a `name` in your new node's config for de
3052
3109
  if normalized:
3053
3110
  # Same as phone: a "no, it's X" correction re-extracts over the turns
3054
3111
  # we already have and re-reads the CORRECTED code at once; only a
3055
- # plain rejection with no new digits asks "what should it be?".
3112
+ # plain rejection with no new digits asks "what should it be?" —
3113
+ # and starts FRESH (start_ts advanced) so the rejected code's turns
3114
+ # can't concatenate into the re-dictation (live call 2a3f6513).
3056
3115
  while True:
3057
3116
  confirmed = await self._confirm_collected(
3058
3117
  node_id, getattr(self, "_collect_readback", "") or " ".join(normalized)
@@ -3069,7 +3128,9 @@ In all of these cases, you can provide a `name` in your new node's config for de
3069
3128
  if corrected is not None:
3070
3129
  buffer, normalized = corrected
3071
3130
  continue # re-read the corrected code, no "what should it be?"
3131
+ start_ts = time.monotonic()
3072
3132
  buffer = ""
3133
+ echoed = ""
3073
3134
  await self._task.queue_frame(
3074
3135
  TTSSpeakFrame(text=collect_prompt(language, "restart_denied"))
3075
3136
  )
@@ -3095,8 +3156,7 @@ In all of these cases, you can provide a `name` in your new node's config for de
3095
3156
  # asked to hear it back, RE-READ the digits-so-far (no miss); else re-ask
3096
3157
  # and count a miss. Compare by VALUE not length so a same-length
3097
3158
  # correction during the echo phase counts as progress (echoed below).
3098
- turns = self._collect_turns_since(start_ts)
3099
- if buffer and turns and is_repeat_request(turns[-1]):
3159
+ if buffer and getattr(self, "_collect_repeat", False):
3100
3160
  # Re-reads bypass the miss counter, so the absolute deadline is the
3101
3161
  # only bound — without it a caller repeating "say it again" loops
3102
3162
  # forever (a turn lands every cycle, never silence, never a miss).
@@ -3156,7 +3216,8 @@ In all of these cases, you can provide a `name` in your new node's config for de
3156
3216
  # knows WHAT is being confirmed. Works in any language.
3157
3217
  answer = await self._extract_spoken_value(
3158
3218
  node_id,
3159
- "yes if the caller agrees the value is correct, no if they say it is wrong",
3219
+ "yes if the caller agrees the value is correct, no if they say it is "
3220
+ "wrong, repeat if they ask to hear the value read back again",
3160
3221
  timeout_s=10.0,
3161
3222
  system_prompt=(
3162
3223
  "The assistant just read a value back to the caller and asked them "
@@ -3165,13 +3226,16 @@ In all of these cases, you can provide a `name` in your new node's config for de
3165
3226
  "confirmed it is correct. If they point out ANY mistake or give a "
3166
3227
  "different value — even while sounding agreeable (e.g. 'no, the "
3167
3228
  "last one is 5 not 3', 'haan but change the first digit') — that "
3168
- "is NOT confirmed; answer 'no'. Answer 'yes' only if they accept "
3169
- "it exactly as read."
3229
+ "is NOT confirmed; answer 'no'. Answer 'repeat' ONLY if they ask "
3230
+ "to HEAR the value again ('repeat that', 'दोबारा बोलिए') — a "
3231
+ "caller saying it is wrong and offering to dictate it again "
3232
+ "('नहीं, मैं फिर से बताती हूँ') means 'no', not 'repeat'. Answer "
3233
+ "'yes' only if they accept it exactly as read."
3170
3234
  ),
3171
3235
  context_prefix=[
3172
3236
  {"role": "assistant", "content": f"I have {readback}. Is that correct?"}
3173
3237
  ],
3174
- enum_values=["yes", "no"],
3238
+ enum_values=["yes", "no", "repeat"],
3175
3239
  )
3176
3240
  # Hard guard: a genuine confirmation never contains digits, a correction
3177
3241
  # ("no, the last is 5 not 3") always does. If the reply has any digit,
@@ -3184,17 +3248,18 @@ In all of these cases, you can provide a `name` in your new node's config for de
3184
3248
  else ""
3185
3249
  )
3186
3250
  has_digit = any(ch.isdigit() for ch in reply)
3187
- # A request to hear it again ("can you repeat?") is NOT a yes or a no —
3188
- # re-read the value and DON'T burn a confirm attempt (bounded so a caller
3189
- # stuck on "repeat" can't loop forever). A digit-carrying reply is a
3190
- # correction, handled just below, so only a digit-free reply is a repeat.
3191
- if not has_digit and is_repeat_request(reply):
3192
- repeats += 1
3193
- if repeats <= 3:
3194
- continue
3195
3251
  if has_digit:
3196
3252
  return False
3197
3253
  text = (answer or "").strip().lower()
3254
+ # A request to hear it again (LLM-decided — "मैं फिर से बताती हूँ" /
3255
+ # "I'll tell you again" classifies as 'no', not 'repeat'; the old
3256
+ # substring match looped on that false positive, live call 2a3f6513)
3257
+ # is NOT a yes or a no — re-read the value and DON'T burn a confirm
3258
+ # attempt (bounded so a caller stuck on "repeat" can't loop forever).
3259
+ if text.startswith("repeat"):
3260
+ repeats += 1
3261
+ if repeats <= 3:
3262
+ continue
3198
3263
  if text.startswith("yes"):
3199
3264
  return True
3200
3265
  if text.startswith("no"):
@@ -48,6 +48,7 @@ from typing import Callable, Tuple, Type
48
48
 
49
49
  from pipecat.frames.frames import (
50
50
  Frame,
51
+ LLMContextFrame,
51
52
  LLMMessagesFrame,
52
53
  LLMRunFrame,
53
54
  LLMTextFrame,
@@ -63,6 +64,16 @@ logger = logging.getLogger(__name__)
63
64
  # race-free fix for the spurious-bot-text problem.
64
65
  _TRIGGER_FRAME_TYPES: Tuple[Type[Frame], ...] = (
65
66
  OpenAILLMContextFrame,
67
+ # Universal context frame. `LLMContextFrame` and `OpenAILLMContextFrame` are
68
+ # SIBLINGS (both extend Frame directly), so matching one does NOT match the
69
+ # other — both must be listed. The pipeline picks between them from a single
70
+ # switch in the bot: `context = LLMContext() if (_use_responses or
71
+ # _use_universal_for_google) else OpenAILLMContext()`. Omitting this one let
72
+ # the LLM fire during router-mode nodes on every Responses-API agent
73
+ # (gpt-5.4-mini/nano are forced onto Responses regardless of the
74
+ # `use_responses_api` toggle) and every google/gemma agent — the bot spoke
75
+ # spurious freeform text instead of routing silently.
76
+ LLMContextFrame,
66
77
  LLMMessagesFrame,
67
78
  LLMRunFrame,
68
79
  )
@@ -1,4 +1,5 @@
1
- """Observer that timestamps :class:`OpenAILLMContextFrame` so router-mode
1
+ """Observer that timestamps the user-turn-committed context frame
2
+ (:class:`OpenAILLMContextFrame` **or** :class:`LLMContextFrame`) so router-mode
2
3
  nodes can detect "user turn is now committed to context" without reaching
3
4
  into the user context aggregator's private state.
4
5
 
@@ -7,22 +8,27 @@ Why an observer, not a FrameProcessor:
7
8
  public ``PipelineTask.add_observer`` API.
8
9
  * We only need a passive timestamp + an ``asyncio.Event`` — no frame routing.
9
10
 
10
- Why ``OpenAILLMContextFrame`` and not ``UserStoppedSpeakingFrame``:
11
+ Why a context frame and not ``UserStoppedSpeakingFrame``:
11
12
  * ``UserStoppedSpeakingFrame`` is raw VAD — it fires the instant the mic
12
13
  detects silence, BEFORE the STT final lands and BEFORE the user-context
13
14
  aggregator appends the turn. Router gates that release on this signal
14
15
  classify against an empty/stale context.
15
- * ``OpenAILLMContextFrame`` is pushed by ``LLMUserContextAggregator``
16
+ * The context frame is pushed by ``LLMUserContextAggregator``
16
17
  (``llm_response._process_aggregation``) only AFTER the final transcript
17
18
  is appended to the LLM context. It is the canonical "user turn is now
18
19
  in the context the router will read" event.
19
20
  * Router-mode never queues ``LLMRunFrame`` for the router node itself, so
20
- the only ``OpenAILLMContextFrame`` produced during a router invocation
21
- comes from the aggregator's turn-complete path — no competing source.
21
+ the only context frame produced during a router invocation comes from
22
+ the aggregator's turn-complete path — no competing source.
23
+
24
+ BOTH context-frame flavours must be matched (see
25
+ ``_USER_TURN_COMMITTED_FRAME_TYPES``): the aggregator emits
26
+ ``LLMContextFrame`` on the Responses-API / google / gemma paths and
27
+ ``OpenAILLMContextFrame`` on the Chat Completions path.
22
28
 
23
29
  State exposed:
24
30
  * ``last_user_context_pushed_ts``: monotonic time of the latest
25
- ``OpenAILLMContextFrame`` (user turn committed to context).
31
+ user-turn context frame (user turn committed to context).
26
32
  * ``last_user_stopped_ts``: backwards-compatible alias kept in sync with
27
33
  ``last_user_context_pushed_ts`` so existing callers keep working.
28
34
  * ``event``: ``asyncio.Event`` set on every such frame; the consumer is
@@ -38,26 +44,41 @@ from typing import List, Tuple
38
44
 
39
45
  from pipecat.frames.frames import (
40
46
  InterimTranscriptionFrame,
47
+ LLMContextFrame,
41
48
  TranscriptionFrame,
42
49
  UserStartedSpeakingFrame,
43
50
  )
44
51
  from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContextFrame
45
52
  from pipecat.observers.base_observer import BaseObserver, FramePushed
46
53
 
54
+ # Both context-frame flavours mean the same thing here: "the user's turn is now
55
+ # committed to the context the router will read". Which one the aggregator emits
56
+ # is decided by one switch in the bot:
57
+ #
58
+ # context = LLMContext() if (_use_responses or _use_universal_for_google)
59
+ # else OpenAILLMContext()
60
+ #
61
+ # They are SIBLINGS (both extend Frame directly, neither subclasses the other),
62
+ # so matching one does NOT match the other. Keying on only OpenAILLMContextFrame
63
+ # left every Responses-API / google / gemma agent's router waiting the full
64
+ # ``router_wait_for_user_timeout`` (15s) after the caller had already replied.
65
+ _USER_TURN_COMMITTED_FRAME_TYPES = (OpenAILLMContextFrame, LLMContextFrame)
66
+
47
67
  # Bound the per-call transcript log so a very long call can't grow it without
48
68
  # limit; far more than any single Collect dictation needs.
49
69
  _MAX_TRANSCRIPT_LOG = 256
50
70
 
51
71
 
52
72
  class UserTurnObserver(BaseObserver):
53
- """Tracks the timestamp of the most recent ``OpenAILLMContextFrame``.
73
+ """Tracks the timestamp of the most recent user-turn context frame.
54
74
 
55
75
  Router-mode uses this to (a) detect a user reply already committed to
56
76
  context BEFORE the router was entered (barge-in during the preceding
57
77
  Speak's TTS) and (b) await the NEXT user reply when none is available.
58
78
 
59
- The observer is keyed on ``OpenAILLMContextFrame`` rather than the raw
60
- VAD ``UserStoppedSpeakingFrame`` because only the former guarantees the
79
+ The observer is keyed on the context frame (either flavour, see
80
+ ``_USER_TURN_COMMITTED_FRAME_TYPES``) rather than the raw VAD
81
+ ``UserStoppedSpeakingFrame`` because only the former guarantees the
61
82
  user's transcript has been appended to the LLM context the router reads.
62
83
  """
63
84
 
@@ -93,7 +114,7 @@ class UserTurnObserver(BaseObserver):
93
114
 
94
115
  @property
95
116
  def event(self) -> asyncio.Event:
96
- """The asyncio.Event set on each ``OpenAILLMContextFrame``.
117
+ """The asyncio.Event set on each user-turn context frame.
97
118
 
98
119
  Callers MUST ``.clear()`` before awaiting if they want to wait for the
99
120
  NEXT frame (vs. immediately returning on a stale set state).
@@ -101,7 +122,7 @@ class UserTurnObserver(BaseObserver):
101
122
  return self._event
102
123
 
103
124
  async def on_push_frame(self, data: FramePushed) -> None:
104
- if isinstance(data.frame, OpenAILLMContextFrame):
125
+ if isinstance(data.frame, _USER_TURN_COMMITTED_FRAME_TYPES):
105
126
  now = time.monotonic()
106
127
  self.last_user_context_pushed_ts = now
107
128
  self.last_user_stopped_ts = now
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "0.0.0.dev0",
3
- "distance": 2786,
4
- "node": "g4f35e028c126047efd7781c8674fba2a380c5e99",
5
- "dirty": false,
6
- "branch": "dv-stage",
7
- "node_date": "2026-08-10"
8
- }