dv-pipecat-flows 0.0.23.dev10__tar.gz → 0.0.23.dev12__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.23.dev10/src/dv_pipecat_flows.egg-info → dv_pipecat_flows-0.0.23.dev12}/PKG-INFO +1 -1
  2. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12/src/dv_pipecat_flows.egg-info}/PKG-INFO +1 -1
  3. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/src/dv_pipecat_flows.egg-info/scm_file_list.json +4 -67
  4. dv_pipecat_flows-0.0.23.dev12/src/dv_pipecat_flows.egg-info/scm_version.json +8 -0
  5. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/src/pipecat_flows/manager.py +52 -21
  6. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/src/pipecat_flows/processors/router_mode_guard.py +11 -0
  7. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/src/pipecat_flows/processors/user_turn_observer.py +32 -11
  8. dv_pipecat_flows-0.0.23.dev10/src/dv_pipecat_flows.egg-info/scm_version.json +0 -8
  9. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/.agents/skills/loki-logs/SKILL.md +0 -0
  10. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/.agents/skills/loki-logs/query-reference.md +0 -0
  11. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/.claude/skills/loki-logs/SKILL.md +0 -0
  12. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/.claude/skills/loki-logs/query-reference.md +0 -0
  13. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/.gitattributes +0 -0
  14. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/.gitignore +0 -0
  15. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/.pre-commit-config.yaml +0 -0
  16. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/.python-version +0 -0
  17. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/.readthedocs.yaml +0 -0
  18. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/AGENTS.md +0 -0
  19. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/CHANGELOG.md +0 -0
  20. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/CLAUDE.md +0 -0
  21. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/CONTRIBUTING.md +0 -0
  22. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/LICENSE +0 -0
  23. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/MANIFEST.in +0 -0
  24. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/README.md +0 -0
  25. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/.env.example +0 -0
  26. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/.gitignore +0 -0
  27. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/README.md +0 -0
  28. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/docs/ARCHITECTURE.md +0 -0
  29. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/docs/CODE_MAP.md +0 -0
  30. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/docs/CONFIG.md +0 -0
  31. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/docs/CONTRACTS.md +0 -0
  32. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/docs/COPILOT_TODO.md +0 -0
  33. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/docs/DISTRIBUTION.md +0 -0
  34. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/docs/HLD.md +0 -0
  35. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/docs/METHODS.md +0 -0
  36. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/docs/VOICE_E2E.md +0 -0
  37. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/eval/ANALYSIS_stagehand_vs_hybrid.md +0 -0
  38. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/eval/RESULTS.md +0 -0
  39. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/eval/_env.py +0 -0
  40. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/eval/bench.py +0 -0
  41. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/eval/bench_browseruse.py +0 -0
  42. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/eval/bench_stagehand.py +0 -0
  43. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/eval/inspect_browseruse.py +0 -0
  44. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/eval/inspect_browseruse_showcase.py +0 -0
  45. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/eval/inspect_stagehand_prompt.py +0 -0
  46. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/eval/pages/checkout.html +0 -0
  47. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/eval/pages/dashboard.html +0 -0
  48. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/eval/pages/signup.html +0 -0
  49. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/eval/results_browseruse.json +0 -0
  50. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/eval/results_perception.json +0 -0
  51. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/eval/results_stagehand.json +0 -0
  52. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/eval/tasks.py +0 -0
  53. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/eval/try_hybrid.py +0 -0
  54. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/eval/try_hybrid_multistep.py +0 -0
  55. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/eval/try_hybrid_oneshot.py +0 -0
  56. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/pyproject.toml +0 -0
  57. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/scripts/dev-stack.sh +0 -0
  58. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/browser_copilot/uv.lock +0 -0
  59. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/dev-requirements.txt +0 -0
  60. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/docker-compose.dev.yml +0 -0
  61. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/.eslintrc.json +0 -0
  62. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/.prettierrc +0 -0
  63. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/css/tailwind.css +0 -0
  64. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/examples/food_ordering.json +0 -0
  65. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/examples/movie_explorer.json +0 -0
  66. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/examples/patient_intake.json +0 -0
  67. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/examples/restaurant_reservation.json +0 -0
  68. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/examples/travel_planner.json +0 -0
  69. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/favicon.png +0 -0
  70. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/favicon.svg +0 -0
  71. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/index.html +0 -0
  72. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/js/editor/canvas.js +0 -0
  73. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/js/editor/editorState.js +0 -0
  74. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/js/editor/sidePanel.js +0 -0
  75. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/js/editor/toolbar.js +0 -0
  76. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/js/main.js +0 -0
  77. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/js/nodes/baseNode.js +0 -0
  78. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/js/nodes/endNode.js +0 -0
  79. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/js/nodes/flowNode.js +0 -0
  80. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/js/nodes/functionNode.js +0 -0
  81. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/js/nodes/index.js +0 -0
  82. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/js/nodes/mergeNode.js +0 -0
  83. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/js/nodes/startNode.js +0 -0
  84. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/js/types.js +0 -0
  85. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/js/utils/export.js +0 -0
  86. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/js/utils/helpers.js +0 -0
  87. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/js/utils/import.js +0 -0
  88. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/js/utils/validation.js +0 -0
  89. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/jsdoc.json +0 -0
  90. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/package-lock.json +0 -0
  91. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/package.json +0 -0
  92. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/postcss.config.cjs +0 -0
  93. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/public/favicon.png +0 -0
  94. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/public/favicon.svg +0 -0
  95. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/tailwind.config.cjs +0 -0
  96. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/vercel.json +0 -0
  97. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/editor/vite.config.js +0 -0
  98. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/engine_primitives_plan.md +0 -0
  99. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/env.example +0 -0
  100. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/images/food-ordering-flow.png +0 -0
  101. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/pipecat-flows.png +0 -0
  102. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/pipecat_upgrade.md +0 -0
  103. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/pyproject.toml +0 -0
  104. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/remote-asterisk-code/README.md +0 -0
  105. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/remote-asterisk-code/extensions.conf +0 -0
  106. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/remote-asterisk-code/rtp.conf +0 -0
  107. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/requirements.txt +0 -0
  108. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/scripts/azure_latency_probe.py +0 -0
  109. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/scripts/check-pypi-package.py +0 -0
  110. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/scripts/fix-ruff.sh +0 -0
  111. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/scripts/langfuse_export.py +0 -0
  112. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/scripts/pre-commit.sh +0 -0
  113. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/setup.cfg +0 -0
  114. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/src/dv_pipecat_flows.egg-info/SOURCES.txt +0 -0
  115. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/src/dv_pipecat_flows.egg-info/dependency_links.txt +0 -0
  116. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/src/dv_pipecat_flows.egg-info/requires.txt +0 -0
  117. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/src/dv_pipecat_flows.egg-info/top_level.txt +0 -0
  118. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/src/pipecat_flows/__init__.py +0 -0
  119. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/src/pipecat_flows/actions.py +0 -0
  120. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/src/pipecat_flows/adapters.py +0 -0
  121. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/src/pipecat_flows/condition_evaluator.py +0 -0
  122. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/src/pipecat_flows/exceptions.py +0 -0
  123. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/src/pipecat_flows/flow_validator.py +0 -0
  124. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/src/pipecat_flows/processors/__init__.py +0 -0
  125. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/src/pipecat_flows/processors/speak_interruption_guard.py +0 -0
  126. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/src/pipecat_flows/router_mode.py +0 -0
  127. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/src/pipecat_flows/types.py +0 -0
  128. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev12}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dv-pipecat-flows
3
- Version: 0.0.23.dev10
3
+ Version: 0.0.23.dev12
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.23.dev10
3
+ Version: 0.0.23.dev12
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
@@ -40,40 +40,6 @@
40
40
  "LICENSE",
41
41
  "MANIFEST.in",
42
42
  "README.md",
43
- "browser_copilot/.env.example",
44
- "browser_copilot/.gitignore",
45
- "browser_copilot/README.md",
46
- "browser_copilot/docs/ARCHITECTURE.md",
47
- "browser_copilot/docs/CODE_MAP.md",
48
- "browser_copilot/docs/CONFIG.md",
49
- "browser_copilot/docs/CONTRACTS.md",
50
- "browser_copilot/docs/COPILOT_TODO.md",
51
- "browser_copilot/docs/DISTRIBUTION.md",
52
- "browser_copilot/docs/HLD.md",
53
- "browser_copilot/docs/METHODS.md",
54
- "browser_copilot/docs/VOICE_E2E.md",
55
- "browser_copilot/eval/ANALYSIS_stagehand_vs_hybrid.md",
56
- "browser_copilot/eval/RESULTS.md",
57
- "browser_copilot/eval/_env.py",
58
- "browser_copilot/eval/bench.py",
59
- "browser_copilot/eval/bench_browseruse.py",
60
- "browser_copilot/eval/bench_stagehand.py",
61
- "browser_copilot/eval/inspect_browseruse.py",
62
- "browser_copilot/eval/inspect_browseruse_showcase.py",
63
- "browser_copilot/eval/inspect_stagehand_prompt.py",
64
- "browser_copilot/eval/pages/checkout.html",
65
- "browser_copilot/eval/pages/dashboard.html",
66
- "browser_copilot/eval/pages/signup.html",
67
- "browser_copilot/eval/results_browseruse.json",
68
- "browser_copilot/eval/results_perception.json",
69
- "browser_copilot/eval/results_stagehand.json",
70
- "browser_copilot/eval/tasks.py",
71
- "browser_copilot/eval/try_hybrid.py",
72
- "browser_copilot/eval/try_hybrid_multistep.py",
73
- "browser_copilot/eval/try_hybrid_oneshot.py",
74
- "browser_copilot/pyproject.toml",
75
- "browser_copilot/scripts/dev-stack.sh",
76
- "browser_copilot/uv.lock",
77
43
  "dev-requirements.txt",
78
44
  "docker-compose.dev.yml",
79
45
  "docs/ASTERISK_CHAN_WEBSOCKET_MIGRATION.md",
@@ -86,7 +52,6 @@
86
52
  "docs/Makefile",
87
53
  "docs/OPENAI_RESPONSES_API_MIGRATION.md",
88
54
  "docs/blockkit-agents-cdn-integration.md",
89
- "docs/browser-copilot-rpc-contract.md",
90
55
  "docs/build-docs.sh",
91
56
  "docs/conf.py",
92
57
  "docs/examples/pb_fintech_call_transfer_config.json",
@@ -191,13 +156,6 @@
191
156
  "k8s/dv-pipecat/values-stage-asia.yaml",
192
157
  "k8s/dv-pipecat/values-stage.yaml",
193
158
  "k8s/dv-pipecat/values.yaml",
194
- "k8s/langfuse/.gitignore",
195
- "k8s/langfuse/.helmignore",
196
- "k8s/langfuse/Chart.yaml",
197
- "k8s/langfuse/README.md",
198
- "k8s/langfuse/internal-lb.yaml",
199
- "k8s/langfuse/managed-cert.yaml",
200
- "k8s/langfuse/values.yaml",
201
159
  "k8s/livekit/Chart.yaml",
202
160
  "k8s/livekit/README.md",
203
161
  "k8s/livekit/templates/_helpers.tpl",
@@ -220,15 +178,12 @@
220
178
  "remote-asterisk-code/extensions.conf",
221
179
  "remote-asterisk-code/rtp.conf",
222
180
  "requirements.txt",
223
- "ringg-bot/BROWSER_COPILOT_EVALUATION.md",
224
181
  "ringg-bot/DEPLOYMENT.md",
225
182
  "ringg-bot/Dockerfile",
226
- "ringg-bot/LIVE_MEETING_AGENT.md",
227
183
  "ringg-bot/PIPECAT_ARCHITECTURE.md",
228
184
  "ringg-bot/PIPELINE_TESTING.md",
229
185
  "ringg-bot/README.md",
230
186
  "ringg-bot/assets/background/office_ambience_8k_mono.wav",
231
- "ringg-bot/assets/tool_call_music_default.mp3",
232
187
  "ringg-bot/backchannel.py",
233
188
  "ringg-bot/bot.py",
234
189
  "ringg-bot/bot_with_flows.py",
@@ -320,11 +275,8 @@
320
275
  "ringg-bot/replay/tool_sandbox.py",
321
276
  "ringg-bot/replay/tool_sandbox_single_node.py",
322
277
  "ringg-bot/requirements.txt",
323
- "ringg-bot/ringg_processors/copilot_contract.py",
324
- "ringg-bot/ringg_processors/dom_context_injector.py",
325
278
  "ringg-bot/ringg_processors/end_of_call_processor.py",
326
279
  "ringg-bot/ringg_processors/function_call_filler_processor.py",
327
- "ringg-bot/ringg_processors/function_call_music_processor.py",
328
280
  "ringg-bot/ringg_processors/language_steering_processor.py",
329
281
  "ringg-bot/ringg_processors/livekit_caption_observer.py",
330
282
  "ringg-bot/ringg_processors/livekit_rpc_bridge.py",
@@ -370,29 +322,23 @@
370
322
  "ringg-bot/tests/test_asteriskv2_transfer_numbers.py",
371
323
  "ringg-bot/tests/test_asteriskv2_user_to_user.py",
372
324
  "ringg-bot/tests/test_azure_region_routing.py",
373
- "ringg-bot/tests/test_browser_action_suppression.py",
374
325
  "ringg-bot/tests/test_call_transfer_strategies.py",
375
326
  "ringg-bot/tests/test_chat_idle_activity_frame.py",
376
327
  "ringg-bot/tests/test_chat_user_idle_processor.py",
377
328
  "ringg-bot/tests/test_connection_watchdog.py",
378
- "ringg-bot/tests/test_copilot_log_redaction.py",
379
329
  "ringg-bot/tests/test_custom_vars_pipeline_parity.py",
380
330
  "ringg-bot/tests/test_decimal_safe_aggregator.py",
381
331
  "ringg-bot/tests/test_direct_sim_tool_records.py",
382
- "ringg-bot/tests/test_dom_context_injector.py",
383
332
  "ringg-bot/tests/test_dropped_turn_regeneration.py",
384
333
  "ringg-bot/tests/test_end_call_schema.py",
334
+ "ringg-bot/tests/test_end_call_teardown.py",
385
335
  "ringg-bot/tests/test_env_config_asterisk_ami.py",
386
- "ringg-bot/tests/test_execute_plan.py",
387
- "ringg-bot/tests/test_find_button_api_config.py",
388
336
  "ringg-bot/tests/test_flow_query_kb.py",
389
337
  "ringg-bot/tests/test_gcs_fetch.py",
390
338
  "ringg-bot/tests/test_http_client_diagnostics.py",
391
339
  "ringg-bot/tests/test_http_client_stall_diagnostics.py",
392
340
  "ringg-bot/tests/test_inbound_call_data_and_custom_vars.py",
393
- "ringg-bot/tests/test_inbound_gate_context_handoff.py",
394
341
  "ringg-bot/tests/test_inbound_registration_race.py",
395
- "ringg-bot/tests/test_inbound_resolve.py",
396
342
  "ringg-bot/tests/test_job_reporting.py",
397
343
  "ringg-bot/tests/test_kb_planner.py",
398
344
  "ringg-bot/tests/test_language_map.py",
@@ -411,19 +357,17 @@
411
357
  "ringg-bot/tests/test_merge_parallel_tool_responses.py",
412
358
  "ringg-bot/tests/test_metrics_bridge.py",
413
359
  "ringg-bot/tests/test_metrics_collector_llm.py",
360
+ "ringg-bot/tests/test_no_bot_calls_are_failed.py",
414
361
  "ringg-bot/tests/test_on_call_api_pre_call_message.py",
415
362
  "ringg-bot/tests/test_on_call_jq_metadata.py",
416
363
  "ringg-bot/tests/test_on_call_sdk_context.py",
417
364
  "ringg-bot/tests/test_ozonetel_inbound.py",
418
365
  "ringg-bot/tests/test_ozonetel_transfer.py",
419
- "ringg-bot/tests/test_rag_filter_operators.py",
420
- "ringg-bot/tests/test_rag_filter_property_names.py",
421
366
  "ringg-bot/tests/test_realtime_external_tts.py",
422
367
  "ringg-bot/tests/test_recording_flush.py",
423
368
  "ringg-bot/tests/test_recording_stereo.py",
424
369
  "ringg-bot/tests/test_regression_monitor_sync.py",
425
370
  "ringg-bot/tests/test_replay_kb_flag.py",
426
- "ringg-bot/tests/test_runtime_config_language_normalization.py",
427
371
  "ringg-bot/tests/test_runtime_config_upload.py",
428
372
  "ringg-bot/tests/test_sdk_executor_flatten.py",
429
373
  "ringg-bot/tests/test_sdk_tool_schema.py",
@@ -438,13 +382,11 @@
438
382
  "ringg-bot/tests/test_simulate_batch.py",
439
383
  "ringg-bot/tests/test_simulate_progress.py",
440
384
  "ringg-bot/tests/test_summary_dispatcher.py",
441
- "ringg-bot/tests/test_tool_call_music.py",
442
385
  "ringg-bot/tests/test_tool_calls_upload.py",
443
386
  "ringg-bot/tests/test_tool_executor_context.py",
444
387
  "ringg-bot/tests/test_traffic_split.py",
445
388
  "ringg-bot/tests/test_transcript_timestamp_format.py",
446
389
  "ringg-bot/tests/test_transfer_custom_vars.py",
447
- "ringg-bot/tests/test_transfer_summary_api_tool.py",
448
390
  "ringg-bot/tests/test_tts_currency.py",
449
391
  "ringg-bot/tests/test_variable_replacement.py",
450
392
  "ringg-bot/tests/test_wandb_llm.py",
@@ -485,7 +427,6 @@
485
427
  "ringg-bot/utils/decimal_safe_aggregator.py",
486
428
  "ringg-bot/utils/deepgram_warmup.py",
487
429
  "ringg-bot/utils/flow_tools/__init__.py",
488
- "ringg-bot/utils/flow_tools/browser_actions.py",
489
430
  "ringg-bot/utils/flow_tools/call_transfer.py",
490
431
  "ringg-bot/utils/flow_tools/collect_long_input.py",
491
432
  "ringg-bot/utils/flow_tools/dtmf_output.py",
@@ -556,12 +497,10 @@
556
497
  "ringg-bot/utils/tts.py",
557
498
  "ringg-bot/utils/twilio_async_client.py",
558
499
  "ringg-bot/utils/warning_messages.py",
559
- "ringg-bot/utils/widget_transcript.py",
560
500
  "ringg-bot/utils/word_timestamp_logger.py",
561
501
  "ringg-bot/voice_services/asterisk/asterisk_service.py",
562
502
  "ringg-bot/voice_services/asterisk/router.py",
563
503
  "ringg-bot/voice_services/asteriskv2/asterisk_v2_service.py",
564
- "ringg-bot/voice_services/asteriskv2/inbound_resolve.py",
565
504
  "ringg-bot/voice_services/asteriskv2/provider_transforms.py",
566
505
  "ringg-bot/voice_services/asteriskv2/router.py",
567
506
  "ringg-bot/voice_services/asteriskv2/websocket_handler.py",
@@ -605,10 +544,8 @@
605
544
  "ringg-bot/voicemail_detector.py",
606
545
  "ringg-bot/websocket/router.py",
607
546
  "ringg-bot/websocket/websocket_service.py",
608
- "scripts/azure_latency_probe.py",
609
547
  "scripts/check-pypi-package.py",
610
548
  "scripts/fix-ruff.sh",
611
- "scripts/langfuse_export.py",
612
549
  "scripts/pre-commit.sh",
613
550
  "src/pipecat_flows/__init__.py",
614
551
  "src/pipecat_flows/actions.py",
@@ -645,14 +582,14 @@
645
582
  "tests/test_hamsa_stt_config.py",
646
583
  "tests/test_helpers.py",
647
584
  "tests/test_keypad_menu_node.py",
648
- "tests/test_livekit_chat_flush.py",
649
585
  "tests/test_manager.py",
650
586
  "tests/test_openai_realtime_transcript_filter.py",
651
587
  "tests/test_response_formatters.py",
652
588
  "tests/test_router_extract_schema.py",
589
+ "tests/test_router_mode_guard.py",
653
590
  "tests/test_router_no_unclear.py",
654
- "tests/test_router_wait_idle.py",
655
591
  "tests/test_tts_split_cache.py",
592
+ "tests/test_user_turn_observer.py",
656
593
  "tests/test_voicemail_detector.py",
657
594
  "uv.lock"
658
595
  ]
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.0.21",
3
+ "distance": 1862,
4
+ "node": "g300a493f33163fbfe115dc7a972b469319383c67",
5
+ "dirty": true,
6
+ "branch": "fix/dtdc",
7
+ "node_date": "2026-08-11"
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,
@@ -2689,6 +2689,7 @@ In all of these cases, you can provide a `name` in your new node's config for de
2689
2689
  """
2690
2690
  self._collect_readback = "" # grouped, language-matched read-back for the confirm
2691
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
2692
2693
  context_messages = [
2693
2694
  {"role": "user", "content": turn}
2694
2695
  for turn in turns
@@ -2732,6 +2733,14 @@ In all of these cases, you can provide a `name` in your new node's config for de
2732
2733
  "the number; make no promises or commitments."
2733
2734
  ),
2734
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
+ },
2735
2744
  },
2736
2745
  )
2737
2746
  try:
@@ -2749,6 +2758,11 @@ In all of these cases, you can provide a `name` in your new node's config for de
2749
2758
  "empty and put a SHORT on-topic reply in `reply`: answer in one "
2750
2759
  "sentence, then ask again for the number — stay on topic, make no "
2751
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"
2752
2766
  "Digits: read each spoken number-word in the order spoken and join its "
2753
2767
  "digits left to right — a digit-word adds one digit, 'double X' adds "
2754
2768
  "XX, 'triple X' adds XXX (in any language, incl. डबल/ट्रिपल), and a lone "
@@ -2778,6 +2792,7 @@ In all of these cases, you can provide a `name` in your new node's config for de
2778
2792
  reply = (extracted or {}).get("reply")
2779
2793
  self._collect_readback = readback.strip() if isinstance(readback, str) else ""
2780
2794
  self._collect_reply = reply.strip() if isinstance(reply, str) else ""
2795
+ self._collect_repeat = bool((extracted or {}).get("repeat_request"))
2781
2796
  return re.sub(r"\D", "", value) if isinstance(value, str) else ""
2782
2797
 
2783
2798
  async def _reextract_if_corrected(
@@ -2939,9 +2954,10 @@ In all of these cases, you can provide a `name` in your new node's config for de
2939
2954
  # the CORRECTED number at once — instead of asking "what should it
2940
2955
  # be?" and making them repeat what they just said. The idempotent
2941
2956
  # extractor merges "no, the first part is 807" into 70823… →
2942
- # 8072341303. KEEP start_ts throughout so corrections resolve against
2943
- # the ORIGINAL number. Only a plain rejection with no new digits
2944
- # 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``.
2945
2961
  while True:
2946
2962
  confirmed = await self._confirm_collected(
2947
2963
  node_id, getattr(self, "_collect_readback", "") or " ".join(buffer)
@@ -2951,6 +2967,10 @@ In all of these cases, you can provide a `name` in your new node's config for de
2951
2967
  corrections += 1
2952
2968
  if corrections >= max_attempts:
2953
2969
  break
2970
+ # Snapshot the fresh-attempt boundary BEFORE the re-extract LLM
2971
+ # call: a caller who starts re-dictating during that await must
2972
+ # land INSIDE the new window, not be timed out of it.
2973
+ fresh_start = time.monotonic()
2954
2974
  corrected = await self._reextract_if_corrected(
2955
2975
  node_id, "phone", start_ts, normalized,
2956
2976
  country_default=country_default,
@@ -2958,7 +2978,9 @@ In all of these cases, you can provide a `name` in your new node's config for de
2958
2978
  if corrected is not None:
2959
2979
  buffer, normalized = corrected
2960
2980
  continue # re-read the corrected number, no "what should it be?"
2981
+ start_ts = fresh_start
2961
2982
  buffer = ""
2983
+ echoed = ""
2962
2984
  await self._task.queue_frame(
2963
2985
  TTSSpeakFrame(text=collect_prompt(language, "restart_denied"))
2964
2986
  )
@@ -2987,8 +3009,7 @@ In all of these cases, you can provide a `name` in your new node's config for de
2987
3009
  # correction during the echo phase ("no, the 3rd is 5": 7765 → 7755)
2988
3010
  # changes the value but not the length, so a length check would mis-read
2989
3011
  # the fix as "no progress" and burn a miss instead of echoing it below.
2990
- turns = self._collect_turns_since(start_ts)
2991
- if buffer and turns and is_repeat_request(turns[-1]):
3012
+ if buffer and getattr(self, "_collect_repeat", False):
2992
3013
  # Re-reads bypass the miss counter, so the absolute deadline is the
2993
3014
  # only bound — without it a caller repeating "say it again" loops
2994
3015
  # forever (a turn lands every cycle, never silence, never a miss).
@@ -3092,7 +3113,9 @@ In all of these cases, you can provide a `name` in your new node's config for de
3092
3113
  if normalized:
3093
3114
  # Same as phone: a "no, it's X" correction re-extracts over the turns
3094
3115
  # we already have and re-reads the CORRECTED code at once; only a
3095
- # plain rejection with no new digits asks "what should it be?".
3116
+ # plain rejection with no new digits asks "what should it be?" —
3117
+ # and starts FRESH (start_ts advanced) so the rejected code's turns
3118
+ # can't concatenate into the re-dictation (live call 2a3f6513).
3096
3119
  while True:
3097
3120
  confirmed = await self._confirm_collected(
3098
3121
  node_id, getattr(self, "_collect_readback", "") or " ".join(normalized)
@@ -3102,6 +3125,8 @@ In all of these cases, you can provide a `name` in your new node's config for de
3102
3125
  corrections += 1
3103
3126
  if corrections >= max_attempts:
3104
3127
  break
3128
+ # Boundary BEFORE the re-extract await (same race as phone above).
3129
+ fresh_start = time.monotonic()
3105
3130
  corrected = await self._reextract_if_corrected(
3106
3131
  node_id, collect_type, start_ts, normalized,
3107
3132
  what="numeric code", num_digits=target,
@@ -3109,7 +3134,9 @@ In all of these cases, you can provide a `name` in your new node's config for de
3109
3134
  if corrected is not None:
3110
3135
  buffer, normalized = corrected
3111
3136
  continue # re-read the corrected code, no "what should it be?"
3137
+ start_ts = fresh_start
3112
3138
  buffer = ""
3139
+ echoed = ""
3113
3140
  await self._task.queue_frame(
3114
3141
  TTSSpeakFrame(text=collect_prompt(language, "restart_denied"))
3115
3142
  )
@@ -3135,8 +3162,7 @@ In all of these cases, you can provide a `name` in your new node's config for de
3135
3162
  # asked to hear it back, RE-READ the digits-so-far (no miss); else re-ask
3136
3163
  # and count a miss. Compare by VALUE not length so a same-length
3137
3164
  # correction during the echo phase counts as progress (echoed below).
3138
- turns = self._collect_turns_since(start_ts)
3139
- if buffer and turns and is_repeat_request(turns[-1]):
3165
+ if buffer and getattr(self, "_collect_repeat", False):
3140
3166
  # Re-reads bypass the miss counter, so the absolute deadline is the
3141
3167
  # only bound — without it a caller repeating "say it again" loops
3142
3168
  # forever (a turn lands every cycle, never silence, never a miss).
@@ -3196,7 +3222,8 @@ In all of these cases, you can provide a `name` in your new node's config for de
3196
3222
  # knows WHAT is being confirmed. Works in any language.
3197
3223
  answer = await self._extract_spoken_value(
3198
3224
  node_id,
3199
- "yes if the caller agrees the value is correct, no if they say it is wrong",
3225
+ "yes if the caller agrees the value is correct, no if they say it is "
3226
+ "wrong, repeat if they ask to hear the value read back again",
3200
3227
  timeout_s=10.0,
3201
3228
  system_prompt=(
3202
3229
  "The assistant just read a value back to the caller and asked them "
@@ -3205,13 +3232,16 @@ In all of these cases, you can provide a `name` in your new node's config for de
3205
3232
  "confirmed it is correct. If they point out ANY mistake or give a "
3206
3233
  "different value — even while sounding agreeable (e.g. 'no, the "
3207
3234
  "last one is 5 not 3', 'haan but change the first digit') — that "
3208
- "is NOT confirmed; answer 'no'. Answer 'yes' only if they accept "
3209
- "it exactly as read."
3235
+ "is NOT confirmed; answer 'no'. Answer 'repeat' ONLY if they ask "
3236
+ "to HEAR the value again ('repeat that', 'दोबारा बोलिए') — a "
3237
+ "caller saying it is wrong and offering to dictate it again "
3238
+ "('नहीं, मैं फिर से बताती हूँ') means 'no', not 'repeat'. Answer "
3239
+ "'yes' only if they accept it exactly as read."
3210
3240
  ),
3211
3241
  context_prefix=[
3212
3242
  {"role": "assistant", "content": f"I have {readback}. Is that correct?"}
3213
3243
  ],
3214
- enum_values=["yes", "no"],
3244
+ enum_values=["yes", "no", "repeat"],
3215
3245
  )
3216
3246
  # Hard guard: a genuine confirmation never contains digits, a correction
3217
3247
  # ("no, the last is 5 not 3") always does. If the reply has any digit,
@@ -3224,17 +3254,18 @@ In all of these cases, you can provide a `name` in your new node's config for de
3224
3254
  else ""
3225
3255
  )
3226
3256
  has_digit = any(ch.isdigit() for ch in reply)
3227
- # A request to hear it again ("can you repeat?") is NOT a yes or a no —
3228
- # re-read the value and DON'T burn a confirm attempt (bounded so a caller
3229
- # stuck on "repeat" can't loop forever). A digit-carrying reply is a
3230
- # correction, handled just below, so only a digit-free reply is a repeat.
3231
- if not has_digit and is_repeat_request(reply):
3232
- repeats += 1
3233
- if repeats <= 3:
3234
- continue
3235
3257
  if has_digit:
3236
3258
  return False
3237
3259
  text = (answer or "").strip().lower()
3260
+ # A request to hear it again (LLM-decided — "मैं फिर से बताती हूँ" /
3261
+ # "I'll tell you again" classifies as 'no', not 'repeat'; the old
3262
+ # substring match looped on that false positive, live call 2a3f6513)
3263
+ # is NOT a yes or a no — re-read the value and DON'T burn a confirm
3264
+ # attempt (bounded so a caller stuck on "repeat" can't loop forever).
3265
+ if text.startswith("repeat"):
3266
+ repeats += 1
3267
+ if repeats <= 3:
3268
+ continue
3238
3269
  if text.startswith("yes"):
3239
3270
  return True
3240
3271
  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.21",
3
- "distance": 2207,
4
- "node": "gc326546704e7e1a6ee5805d1a16c7803d01dd4ae",
5
- "dirty": false,
6
- "branch": "dv-stage",
7
- "node_date": "2026-08-12"
8
- }