dv-pipecat-flows 0.0.23.dev10__tar.gz → 0.0.23.dev11__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 → dv_pipecat_flows-0.0.23.dev11}/.gitignore +0 -2
  2. {dv_pipecat_flows-0.0.23.dev10/src/dv_pipecat_flows.egg-info → dv_pipecat_flows-0.0.23.dev11}/PKG-INFO +1 -1
  3. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11/src/dv_pipecat_flows.egg-info}/PKG-INFO +1 -1
  4. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/src/dv_pipecat_flows.egg-info/SOURCES.txt +0 -36
  5. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/src/dv_pipecat_flows.egg-info/scm_file_list.json +4 -67
  6. dv_pipecat_flows-0.0.23.dev11/src/dv_pipecat_flows.egg-info/scm_version.json +8 -0
  7. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/src/pipecat_flows/manager.py +84 -115
  8. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/src/pipecat_flows/processors/router_mode_guard.py +11 -0
  9. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/src/pipecat_flows/processors/user_turn_observer.py +32 -11
  10. dv_pipecat_flows-0.0.23.dev10/browser_copilot/.env.example +0 -13
  11. dv_pipecat_flows-0.0.23.dev10/browser_copilot/.gitignore +0 -5
  12. dv_pipecat_flows-0.0.23.dev10/browser_copilot/README.md +0 -71
  13. dv_pipecat_flows-0.0.23.dev10/browser_copilot/docs/ARCHITECTURE.md +0 -190
  14. dv_pipecat_flows-0.0.23.dev10/browser_copilot/docs/CODE_MAP.md +0 -254
  15. dv_pipecat_flows-0.0.23.dev10/browser_copilot/docs/CONFIG.md +0 -125
  16. dv_pipecat_flows-0.0.23.dev10/browser_copilot/docs/CONTRACTS.md +0 -189
  17. dv_pipecat_flows-0.0.23.dev10/browser_copilot/docs/COPILOT_TODO.md +0 -79
  18. dv_pipecat_flows-0.0.23.dev10/browser_copilot/docs/DISTRIBUTION.md +0 -73
  19. dv_pipecat_flows-0.0.23.dev10/browser_copilot/docs/HLD.md +0 -302
  20. dv_pipecat_flows-0.0.23.dev10/browser_copilot/docs/METHODS.md +0 -265
  21. dv_pipecat_flows-0.0.23.dev10/browser_copilot/docs/VOICE_E2E.md +0 -52
  22. dv_pipecat_flows-0.0.23.dev10/browser_copilot/eval/ANALYSIS_stagehand_vs_hybrid.md +0 -113
  23. dv_pipecat_flows-0.0.23.dev10/browser_copilot/eval/RESULTS.md +0 -107
  24. dv_pipecat_flows-0.0.23.dev10/browser_copilot/eval/_env.py +0 -44
  25. dv_pipecat_flows-0.0.23.dev10/browser_copilot/eval/bench.py +0 -480
  26. dv_pipecat_flows-0.0.23.dev10/browser_copilot/eval/bench_browseruse.py +0 -312
  27. dv_pipecat_flows-0.0.23.dev10/browser_copilot/eval/bench_stagehand.py +0 -283
  28. dv_pipecat_flows-0.0.23.dev10/browser_copilot/eval/inspect_browseruse.py +0 -115
  29. dv_pipecat_flows-0.0.23.dev10/browser_copilot/eval/inspect_browseruse_showcase.py +0 -115
  30. dv_pipecat_flows-0.0.23.dev10/browser_copilot/eval/inspect_stagehand_prompt.py +0 -112
  31. dv_pipecat_flows-0.0.23.dev10/browser_copilot/eval/pages/checkout.html +0 -29
  32. dv_pipecat_flows-0.0.23.dev10/browser_copilot/eval/pages/dashboard.html +0 -25
  33. dv_pipecat_flows-0.0.23.dev10/browser_copilot/eval/pages/signup.html +0 -23
  34. dv_pipecat_flows-0.0.23.dev10/browser_copilot/eval/results_browseruse.json +0 -159
  35. dv_pipecat_flows-0.0.23.dev10/browser_copilot/eval/results_perception.json +0 -446
  36. dv_pipecat_flows-0.0.23.dev10/browser_copilot/eval/results_stagehand.json +0 -166
  37. dv_pipecat_flows-0.0.23.dev10/browser_copilot/eval/tasks.py +0 -104
  38. dv_pipecat_flows-0.0.23.dev10/browser_copilot/eval/try_hybrid.py +0 -105
  39. dv_pipecat_flows-0.0.23.dev10/browser_copilot/eval/try_hybrid_multistep.py +0 -149
  40. dv_pipecat_flows-0.0.23.dev10/browser_copilot/eval/try_hybrid_oneshot.py +0 -133
  41. dv_pipecat_flows-0.0.23.dev10/browser_copilot/pyproject.toml +0 -27
  42. dv_pipecat_flows-0.0.23.dev10/browser_copilot/scripts/dev-stack.sh +0 -212
  43. dv_pipecat_flows-0.0.23.dev10/browser_copilot/uv.lock +0 -551
  44. dv_pipecat_flows-0.0.23.dev10/scripts/azure_latency_probe.py +0 -220
  45. dv_pipecat_flows-0.0.23.dev10/scripts/langfuse_export.py +0 -321
  46. dv_pipecat_flows-0.0.23.dev10/src/dv_pipecat_flows.egg-info/scm_version.json +0 -8
  47. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/.agents/skills/loki-logs/SKILL.md +0 -0
  48. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/.agents/skills/loki-logs/query-reference.md +0 -0
  49. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/.claude/skills/loki-logs/SKILL.md +0 -0
  50. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/.claude/skills/loki-logs/query-reference.md +0 -0
  51. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/.gitattributes +0 -0
  52. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/.pre-commit-config.yaml +0 -0
  53. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/.python-version +0 -0
  54. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/.readthedocs.yaml +0 -0
  55. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/AGENTS.md +0 -0
  56. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/CHANGELOG.md +0 -0
  57. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/CLAUDE.md +0 -0
  58. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/CONTRIBUTING.md +0 -0
  59. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/LICENSE +0 -0
  60. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/MANIFEST.in +0 -0
  61. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/README.md +0 -0
  62. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/dev-requirements.txt +0 -0
  63. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/docker-compose.dev.yml +0 -0
  64. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/.eslintrc.json +0 -0
  65. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/.prettierrc +0 -0
  66. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/css/tailwind.css +0 -0
  67. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/examples/food_ordering.json +0 -0
  68. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/examples/movie_explorer.json +0 -0
  69. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/examples/patient_intake.json +0 -0
  70. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/examples/restaurant_reservation.json +0 -0
  71. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/examples/travel_planner.json +0 -0
  72. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/favicon.png +0 -0
  73. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/favicon.svg +0 -0
  74. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/index.html +0 -0
  75. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/js/editor/canvas.js +0 -0
  76. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/js/editor/editorState.js +0 -0
  77. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/js/editor/sidePanel.js +0 -0
  78. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/js/editor/toolbar.js +0 -0
  79. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/js/main.js +0 -0
  80. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/js/nodes/baseNode.js +0 -0
  81. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/js/nodes/endNode.js +0 -0
  82. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/js/nodes/flowNode.js +0 -0
  83. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/js/nodes/functionNode.js +0 -0
  84. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/js/nodes/index.js +0 -0
  85. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/js/nodes/mergeNode.js +0 -0
  86. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/js/nodes/startNode.js +0 -0
  87. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/js/types.js +0 -0
  88. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/js/utils/export.js +0 -0
  89. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/js/utils/helpers.js +0 -0
  90. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/js/utils/import.js +0 -0
  91. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/js/utils/validation.js +0 -0
  92. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/jsdoc.json +0 -0
  93. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/package-lock.json +0 -0
  94. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/package.json +0 -0
  95. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/postcss.config.cjs +0 -0
  96. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/public/favicon.png +0 -0
  97. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/public/favicon.svg +0 -0
  98. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/tailwind.config.cjs +0 -0
  99. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/vercel.json +0 -0
  100. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/editor/vite.config.js +0 -0
  101. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/engine_primitives_plan.md +0 -0
  102. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/env.example +0 -0
  103. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/images/food-ordering-flow.png +0 -0
  104. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/pipecat-flows.png +0 -0
  105. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/pipecat_upgrade.md +0 -0
  106. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/pyproject.toml +0 -0
  107. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/remote-asterisk-code/README.md +0 -0
  108. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/remote-asterisk-code/extensions.conf +0 -0
  109. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/remote-asterisk-code/rtp.conf +0 -0
  110. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/requirements.txt +0 -0
  111. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/scripts/check-pypi-package.py +0 -0
  112. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/scripts/fix-ruff.sh +0 -0
  113. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/scripts/pre-commit.sh +0 -0
  114. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/setup.cfg +0 -0
  115. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/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.dev11}/src/dv_pipecat_flows.egg-info/requires.txt +0 -0
  117. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/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.dev11}/src/pipecat_flows/__init__.py +0 -0
  119. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/src/pipecat_flows/actions.py +0 -0
  120. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/src/pipecat_flows/adapters.py +0 -0
  121. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/src/pipecat_flows/condition_evaluator.py +0 -0
  122. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/src/pipecat_flows/exceptions.py +0 -0
  123. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/src/pipecat_flows/flow_validator.py +0 -0
  124. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/src/pipecat_flows/processors/__init__.py +0 -0
  125. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/src/pipecat_flows/processors/speak_interruption_guard.py +0 -0
  126. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/src/pipecat_flows/router_mode.py +0 -0
  127. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/src/pipecat_flows/types.py +0 -0
  128. {dv_pipecat_flows-0.0.23.dev10 → dv_pipecat_flows-0.0.23.dev11}/uv.lock +0 -0
@@ -187,5 +187,3 @@ MANIFEST
187
187
 
188
188
  # Asterisk configs
189
189
  remote-asterisk-code/pjsip.conf
190
- .venvringg1
191
- .venvringg2
@@ -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.dev11
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.dev11
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
@@ -23,40 +23,6 @@ uv.lock
23
23
  .agents/skills/loki-logs/query-reference.md
24
24
  .claude/skills/loki-logs/SKILL.md
25
25
  .claude/skills/loki-logs/query-reference.md
26
- browser_copilot/.env.example
27
- browser_copilot/.gitignore
28
- browser_copilot/README.md
29
- browser_copilot/pyproject.toml
30
- browser_copilot/uv.lock
31
- browser_copilot/docs/ARCHITECTURE.md
32
- browser_copilot/docs/CODE_MAP.md
33
- browser_copilot/docs/CONFIG.md
34
- browser_copilot/docs/CONTRACTS.md
35
- browser_copilot/docs/COPILOT_TODO.md
36
- browser_copilot/docs/DISTRIBUTION.md
37
- browser_copilot/docs/HLD.md
38
- browser_copilot/docs/METHODS.md
39
- browser_copilot/docs/VOICE_E2E.md
40
- browser_copilot/eval/ANALYSIS_stagehand_vs_hybrid.md
41
- browser_copilot/eval/RESULTS.md
42
- browser_copilot/eval/_env.py
43
- browser_copilot/eval/bench.py
44
- browser_copilot/eval/bench_browseruse.py
45
- browser_copilot/eval/bench_stagehand.py
46
- browser_copilot/eval/inspect_browseruse.py
47
- browser_copilot/eval/inspect_browseruse_showcase.py
48
- browser_copilot/eval/inspect_stagehand_prompt.py
49
- browser_copilot/eval/results_browseruse.json
50
- browser_copilot/eval/results_perception.json
51
- browser_copilot/eval/results_stagehand.json
52
- browser_copilot/eval/tasks.py
53
- browser_copilot/eval/try_hybrid.py
54
- browser_copilot/eval/try_hybrid_multistep.py
55
- browser_copilot/eval/try_hybrid_oneshot.py
56
- browser_copilot/eval/pages/checkout.html
57
- browser_copilot/eval/pages/dashboard.html
58
- browser_copilot/eval/pages/signup.html
59
- browser_copilot/scripts/dev-stack.sh
60
26
  editor/.eslintrc.json
61
27
  editor/.prettierrc
62
28
  editor/favicon.png
@@ -98,10 +64,8 @@ images/food-ordering-flow.png
98
64
  remote-asterisk-code/README.md
99
65
  remote-asterisk-code/extensions.conf
100
66
  remote-asterisk-code/rtp.conf
101
- scripts/azure_latency_probe.py
102
67
  scripts/check-pypi-package.py
103
68
  scripts/fix-ruff.sh
104
- scripts/langfuse_export.py
105
69
  scripts/pre-commit.sh
106
70
  src/dv_pipecat_flows.egg-info/PKG-INFO
107
71
  src/dv_pipecat_flows.egg-info/SOURCES.txt
@@ -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,
@@ -374,23 +374,6 @@ class FlowManager:
374
374
  Callable[[Dict[str, Any]], Awaitable[None]]
375
375
  ] = None
376
376
 
377
- # Optional hook for context that must SURVIVE a RESET* node transition.
378
- # A RESET* strategy replaces the whole message list, dropping anything an
379
- # external processor injected out-of-band (e.g. the browser co-pilot's
380
- # ambient "[Browser context]" message, appended to the aggregator on each
381
- # DOM update — not part of any node's messages). Given the freshly-built
382
- # replacement list, the provider returns it with its own sticky messages
383
- # re-applied: it both DROPS stale copies carried forward by
384
- # RESET_WITH_CONVERSATION and APPENDS the current one, so there is no
385
- # duplicate and no race (it rides the same update frame). Runs only on the
386
- # full-replace path; APPEND already retains the live message. Default None
387
- # (no-op) — non-copilot flows are unaffected.
388
- #
389
- # Signature: provider(messages: List[dict]) -> List[dict]
390
- self.sticky_context_provider: Optional[
391
- Callable[[List[dict]], List[dict]]
392
- ] = None
393
-
394
377
  # Wired in `initialize()` once the pipeline is up. Tracks the timestamp
395
378
  # of the latest UserStoppedSpeakingFrame so router-mode nodes can gate
396
379
  # their LLM classification on "user has actually replied" instead of
@@ -1872,24 +1855,6 @@ In all of these cases, you can provide a `name` in your new node's config for de
1872
1855
  label="jump",
1873
1856
  )
1874
1857
 
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
-
1893
1858
  async def _run_router_node(
1894
1859
  self, node_id: str, node_config: Dict[str, Any]
1895
1860
  ) -> Optional[str]:
@@ -1923,19 +1888,10 @@ In all of these cases, you can provide a `name` in your new node's config for de
1923
1888
  # `_run_router_node` captures a fresh entry_ts — the next iteration
1924
1889
  # correctly waits for a NEW user reply instead of reclassifying the
1925
1890
  # 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.
1936
1891
  observer = self._user_turn_observer
1937
1892
 
1938
- if observer is not None:
1893
+ timeout_s = float(node_config.get("router_wait_for_user_timeout", 15.0))
1894
+ if observer is not None and timeout_s > 0:
1939
1895
  entry_ts = time.monotonic()
1940
1896
  if observer.last_user_stopped_ts >= entry_ts:
1941
1897
  logger.debug(
@@ -1956,41 +1912,28 @@ In all of these cases, you can provide a `name` in your new node's config for de
1956
1912
  )
1957
1913
  else:
1958
1914
  logger.debug(
1959
- f"Router '{node_id}': waiting for "
1960
- f"UserStoppedSpeakingFrame (idle processor armed)"
1915
+ f"Router '{node_id}': waiting up to {timeout_s}s for "
1916
+ f"UserStoppedSpeakingFrame"
1961
1917
  )
1962
- await self._task.queue_frame(StartUserIdleProcessorFrame())
1963
1918
  try:
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()
1919
+ await asyncio.wait_for(
1920
+ observer.event.wait(), timeout=timeout_s
1921
+ )
1922
+ logger.debug(
1923
+ f"Router '{node_id}': user reply received after "
1924
+ f"{time.monotonic() - entry_ts:.2f}s"
1925
+ )
1926
+ except asyncio.TimeoutError:
1927
+ # User stayed silent. Don't raise — let the router
1928
+ # LLM classify whatever context is available; it
1929
+ # will most likely return UNCLEAR, which the
1930
+ # existing pick_branch_target machinery routes to
1931
+ # reprompt or exhausted_node.
1932
+ logger.info(
1933
+ f"Router '{node_id}': no user reply within "
1934
+ f"{timeout_s}s; proceeding (will likely route "
1935
+ f"to unclear/exhausted)"
1989
1936
  )
1990
- logger.debug(
1991
- f"Router '{node_id}': user reply received after "
1992
- f"{time.monotonic() - entry_ts:.2f}s"
1993
- )
1994
1937
 
1995
1938
  branches = node_config.get("router_branches") or []
1996
1939
  extract = node_config.get("router_extract") or {}
@@ -2689,6 +2632,7 @@ In all of these cases, you can provide a `name` in your new node's config for de
2689
2632
  """
2690
2633
  self._collect_readback = "" # grouped, language-matched read-back for the confirm
2691
2634
  self._collect_reply = "" # LLM free reply for a conversational (non-digit) turn
2635
+ self._collect_repeat = False # LLM-decided: caller asked to hear the digits back
2692
2636
  context_messages = [
2693
2637
  {"role": "user", "content": turn}
2694
2638
  for turn in turns
@@ -2732,6 +2676,14 @@ In all of these cases, you can provide a `name` in your new node's config for de
2732
2676
  "the number; make no promises or commitments."
2733
2677
  ),
2734
2678
  },
2679
+ "repeat_request": {
2680
+ "type": "boolean",
2681
+ "description": (
2682
+ "true ONLY if the caller's latest message asks to hear back the "
2683
+ "digits captured so far; false when they are offering to dictate "
2684
+ "again themselves."
2685
+ ),
2686
+ },
2735
2687
  },
2736
2688
  )
2737
2689
  try:
@@ -2749,6 +2701,11 @@ In all of these cases, you can provide a `name` in your new node's config for de
2749
2701
  "empty and put a SHORT on-topic reply in `reply`: answer in one "
2750
2702
  "sentence, then ask again for the number — stay on topic, make no "
2751
2703
  "promises.\n"
2704
+ "Set `repeat_request` to true ONLY when the caller's latest message "
2705
+ "asks to hear back what has been captured so far ('repeat that', "
2706
+ "'what do you have?', 'दोबारा बोलिए'). An offer to dictate again "
2707
+ "('मैं फिर से बताती हूँ', 'I'll tell you again') is NOT a repeat "
2708
+ "request.\n"
2752
2709
  "Digits: read each spoken number-word in the order spoken and join its "
2753
2710
  "digits left to right — a digit-word adds one digit, 'double X' adds "
2754
2711
  "XX, 'triple X' adds XXX (in any language, incl. डबल/ट्रिपल), and a lone "
@@ -2778,6 +2735,7 @@ In all of these cases, you can provide a `name` in your new node's config for de
2778
2735
  reply = (extracted or {}).get("reply")
2779
2736
  self._collect_readback = readback.strip() if isinstance(readback, str) else ""
2780
2737
  self._collect_reply = reply.strip() if isinstance(reply, str) else ""
2738
+ self._collect_repeat = bool((extracted or {}).get("repeat_request"))
2781
2739
  return re.sub(r"\D", "", value) if isinstance(value, str) else ""
2782
2740
 
2783
2741
  async def _reextract_if_corrected(
@@ -2939,9 +2897,10 @@ In all of these cases, you can provide a `name` in your new node's config for de
2939
2897
  # the CORRECTED number at once — instead of asking "what should it
2940
2898
  # be?" and making them repeat what they just said. The idempotent
2941
2899
  # 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``.
2900
+ # 8072341303. Only a plain rejection with no new digits falls
2901
+ # through to the re-ask — and starts FRESH (start_ts advanced), so
2902
+ # the rejected number's turns can't concatenate into the caller's
2903
+ # re-dictation (live call 2a3f6513). Bounded by ``corrections``.
2945
2904
  while True:
2946
2905
  confirmed = await self._confirm_collected(
2947
2906
  node_id, getattr(self, "_collect_readback", "") or " ".join(buffer)
@@ -2951,6 +2910,10 @@ In all of these cases, you can provide a `name` in your new node's config for de
2951
2910
  corrections += 1
2952
2911
  if corrections >= max_attempts:
2953
2912
  break
2913
+ # Snapshot the fresh-attempt boundary BEFORE the re-extract LLM
2914
+ # call: a caller who starts re-dictating during that await must
2915
+ # land INSIDE the new window, not be timed out of it.
2916
+ fresh_start = time.monotonic()
2954
2917
  corrected = await self._reextract_if_corrected(
2955
2918
  node_id, "phone", start_ts, normalized,
2956
2919
  country_default=country_default,
@@ -2958,7 +2921,9 @@ In all of these cases, you can provide a `name` in your new node's config for de
2958
2921
  if corrected is not None:
2959
2922
  buffer, normalized = corrected
2960
2923
  continue # re-read the corrected number, no "what should it be?"
2924
+ start_ts = fresh_start
2961
2925
  buffer = ""
2926
+ echoed = ""
2962
2927
  await self._task.queue_frame(
2963
2928
  TTSSpeakFrame(text=collect_prompt(language, "restart_denied"))
2964
2929
  )
@@ -2987,8 +2952,7 @@ In all of these cases, you can provide a `name` in your new node's config for de
2987
2952
  # correction during the echo phase ("no, the 3rd is 5": 7765 → 7755)
2988
2953
  # changes the value but not the length, so a length check would mis-read
2989
2954
  # 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]):
2955
+ if buffer and getattr(self, "_collect_repeat", False):
2992
2956
  # Re-reads bypass the miss counter, so the absolute deadline is the
2993
2957
  # only bound — without it a caller repeating "say it again" loops
2994
2958
  # forever (a turn lands every cycle, never silence, never a miss).
@@ -3092,7 +3056,9 @@ In all of these cases, you can provide a `name` in your new node's config for de
3092
3056
  if normalized:
3093
3057
  # Same as phone: a "no, it's X" correction re-extracts over the turns
3094
3058
  # 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?".
3059
+ # plain rejection with no new digits asks "what should it be?" —
3060
+ # and starts FRESH (start_ts advanced) so the rejected code's turns
3061
+ # can't concatenate into the re-dictation (live call 2a3f6513).
3096
3062
  while True:
3097
3063
  confirmed = await self._confirm_collected(
3098
3064
  node_id, getattr(self, "_collect_readback", "") or " ".join(normalized)
@@ -3102,6 +3068,8 @@ In all of these cases, you can provide a `name` in your new node's config for de
3102
3068
  corrections += 1
3103
3069
  if corrections >= max_attempts:
3104
3070
  break
3071
+ # Boundary BEFORE the re-extract await (same race as phone above).
3072
+ fresh_start = time.monotonic()
3105
3073
  corrected = await self._reextract_if_corrected(
3106
3074
  node_id, collect_type, start_ts, normalized,
3107
3075
  what="numeric code", num_digits=target,
@@ -3109,7 +3077,9 @@ In all of these cases, you can provide a `name` in your new node's config for de
3109
3077
  if corrected is not None:
3110
3078
  buffer, normalized = corrected
3111
3079
  continue # re-read the corrected code, no "what should it be?"
3080
+ start_ts = fresh_start
3112
3081
  buffer = ""
3082
+ echoed = ""
3113
3083
  await self._task.queue_frame(
3114
3084
  TTSSpeakFrame(text=collect_prompt(language, "restart_denied"))
3115
3085
  )
@@ -3135,8 +3105,7 @@ In all of these cases, you can provide a `name` in your new node's config for de
3135
3105
  # asked to hear it back, RE-READ the digits-so-far (no miss); else re-ask
3136
3106
  # and count a miss. Compare by VALUE not length so a same-length
3137
3107
  # 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]):
3108
+ if buffer and getattr(self, "_collect_repeat", False):
3140
3109
  # Re-reads bypass the miss counter, so the absolute deadline is the
3141
3110
  # only bound — without it a caller repeating "say it again" loops
3142
3111
  # forever (a turn lands every cycle, never silence, never a miss).
@@ -3196,7 +3165,8 @@ In all of these cases, you can provide a `name` in your new node's config for de
3196
3165
  # knows WHAT is being confirmed. Works in any language.
3197
3166
  answer = await self._extract_spoken_value(
3198
3167
  node_id,
3199
- "yes if the caller agrees the value is correct, no if they say it is wrong",
3168
+ "yes if the caller agrees the value is correct, no if they say it is "
3169
+ "wrong, repeat if they ask to hear the value read back again",
3200
3170
  timeout_s=10.0,
3201
3171
  system_prompt=(
3202
3172
  "The assistant just read a value back to the caller and asked them "
@@ -3205,13 +3175,16 @@ In all of these cases, you can provide a `name` in your new node's config for de
3205
3175
  "confirmed it is correct. If they point out ANY mistake or give a "
3206
3176
  "different value — even while sounding agreeable (e.g. 'no, the "
3207
3177
  "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."
3178
+ "is NOT confirmed; answer 'no'. Answer 'repeat' ONLY if they ask "
3179
+ "to HEAR the value again ('repeat that', 'दोबारा बोलिए') — a "
3180
+ "caller saying it is wrong and offering to dictate it again "
3181
+ "('नहीं, मैं फिर से बताती हूँ') means 'no', not 'repeat'. Answer "
3182
+ "'yes' only if they accept it exactly as read."
3210
3183
  ),
3211
3184
  context_prefix=[
3212
3185
  {"role": "assistant", "content": f"I have {readback}. Is that correct?"}
3213
3186
  ],
3214
- enum_values=["yes", "no"],
3187
+ enum_values=["yes", "no", "repeat"],
3215
3188
  )
3216
3189
  # Hard guard: a genuine confirmation never contains digits, a correction
3217
3190
  # ("no, the last is 5 not 3") always does. If the reply has any digit,
@@ -3224,17 +3197,18 @@ In all of these cases, you can provide a `name` in your new node's config for de
3224
3197
  else ""
3225
3198
  )
3226
3199
  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
3200
  if has_digit:
3236
3201
  return False
3237
3202
  text = (answer or "").strip().lower()
3203
+ # A request to hear it again (LLM-decided — "मैं फिर से बताती हूँ" /
3204
+ # "I'll tell you again" classifies as 'no', not 'repeat'; the old
3205
+ # substring match looped on that false positive, live call 2a3f6513)
3206
+ # is NOT a yes or a no — re-read the value and DON'T burn a confirm
3207
+ # attempt (bounded so a caller stuck on "repeat" can't loop forever).
3208
+ if text.startswith("repeat"):
3209
+ repeats += 1
3210
+ if repeats <= 3:
3211
+ continue
3238
3212
  if text.startswith("yes"):
3239
3213
  return True
3240
3214
  if text.startswith("no"):
@@ -4405,22 +4379,17 @@ In all of these cases, you can provide a `name` in your new node's config for de
4405
4379
 
4406
4380
  # For the first node or any RESET* strategy, replace the whole
4407
4381
  # context; APPEND adds to it.
4408
- is_replace = self._current_node is None or update_config.strategy in [
4409
- ContextStrategy.RESET,
4410
- ContextStrategy.RESET_WITH_SUMMARY,
4411
- ContextStrategy.RESET_WITH_CONVERSATION,
4412
- ]
4413
-
4414
- # Re-apply sticky context that a replace would otherwise drop (e.g. the
4415
- # co-pilot's [Browser context]). Only on replace — APPEND keeps the
4416
- # live message already. In the SAME frame, so no ordering race.
4417
- if is_replace and self.sticky_context_provider is not None:
4418
- try:
4419
- messages = self.sticky_context_provider(messages)
4420
- except Exception as exc: # never let a provider break a transition
4421
- logger.warning(f"sticky_context_provider failed: {exc}")
4422
-
4423
- frame_type = LLMMessagesUpdateFrame if is_replace else LLMMessagesAppendFrame
4382
+ frame_type = (
4383
+ LLMMessagesUpdateFrame
4384
+ if self._current_node is None
4385
+ or update_config.strategy
4386
+ in [
4387
+ ContextStrategy.RESET,
4388
+ ContextStrategy.RESET_WITH_SUMMARY,
4389
+ ContextStrategy.RESET_WITH_CONVERSATION,
4390
+ ]
4391
+ else LLMMessagesAppendFrame
4392
+ )
4424
4393
 
4425
4394
  await self._task.queue_frames(
4426
4395
  [frame_type(messages=messages), LLMSetToolsFrame(tools=functions)]