dv-pipecat-flows 0.0.22.dev2100__tar.gz → 0.0.22.dev2207__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.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/.gitignore +7 -0
  2. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/AGENTS.md +18 -0
  3. {dv_pipecat_flows-0.0.22.dev2100/src/dv_pipecat_flows.egg-info → dv_pipecat_flows-0.0.22.dev2207}/PKG-INFO +1 -1
  4. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/.env.example +13 -0
  5. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/.gitignore +5 -0
  6. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/README.md +71 -0
  7. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/docs/ARCHITECTURE.md +190 -0
  8. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/docs/CODE_MAP.md +254 -0
  9. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/docs/CONFIG.md +125 -0
  10. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/docs/CONTRACTS.md +189 -0
  11. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/docs/COPILOT_TODO.md +79 -0
  12. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/docs/DISTRIBUTION.md +73 -0
  13. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/docs/HLD.md +302 -0
  14. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/docs/METHODS.md +265 -0
  15. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/docs/VOICE_E2E.md +52 -0
  16. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/eval/ANALYSIS_stagehand_vs_hybrid.md +113 -0
  17. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/eval/RESULTS.md +107 -0
  18. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/eval/_env.py +44 -0
  19. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/eval/bench.py +480 -0
  20. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/eval/bench_browseruse.py +312 -0
  21. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/eval/bench_stagehand.py +283 -0
  22. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/eval/inspect_browseruse.py +115 -0
  23. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/eval/inspect_browseruse_showcase.py +115 -0
  24. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/eval/inspect_stagehand_prompt.py +112 -0
  25. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/eval/pages/checkout.html +29 -0
  26. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/eval/pages/dashboard.html +25 -0
  27. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/eval/pages/signup.html +23 -0
  28. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/eval/results_browseruse.json +159 -0
  29. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/eval/results_perception.json +446 -0
  30. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/eval/results_stagehand.json +166 -0
  31. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/eval/tasks.py +104 -0
  32. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/eval/try_hybrid.py +105 -0
  33. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/eval/try_hybrid_multistep.py +149 -0
  34. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/eval/try_hybrid_oneshot.py +133 -0
  35. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/pyproject.toml +27 -0
  36. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/scripts/dev-stack.sh +212 -0
  37. dv_pipecat_flows-0.0.22.dev2207/browser_copilot/uv.lock +551 -0
  38. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/pyproject.toml +1 -1
  39. dv_pipecat_flows-0.0.22.dev2207/remote-asterisk-code/README.md +9 -0
  40. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/remote-asterisk-code/extensions.conf +7 -0
  41. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/remote-asterisk-code/rtp.conf +6 -0
  42. dv_pipecat_flows-0.0.22.dev2207/scripts/azure_latency_probe.py +220 -0
  43. dv_pipecat_flows-0.0.22.dev2207/scripts/langfuse_export.py +321 -0
  44. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207/src/dv_pipecat_flows.egg-info}/PKG-INFO +1 -1
  45. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/src/dv_pipecat_flows.egg-info/SOURCES.txt +38 -0
  46. dv_pipecat_flows-0.0.22.dev2207/src/dv_pipecat_flows.egg-info/scm_file_list.json +659 -0
  47. dv_pipecat_flows-0.0.22.dev2207/src/dv_pipecat_flows.egg-info/scm_version.json +8 -0
  48. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/src/pipecat_flows/actions.py +88 -0
  49. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/src/pipecat_flows/adapters.py +33 -0
  50. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/src/pipecat_flows/condition_evaluator.py +225 -5
  51. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/src/pipecat_flows/manager.py +1705 -43
  52. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/src/pipecat_flows/processors/user_turn_observer.py +46 -1
  53. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/src/pipecat_flows/router_mode.py +46 -5
  54. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/src/pipecat_flows/types.py +42 -2
  55. dv_pipecat_flows-0.0.22.dev2100/remote-asterisk-code/README.md +0 -0
  56. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/.agents/skills/loki-logs/SKILL.md +0 -0
  57. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/.agents/skills/loki-logs/query-reference.md +0 -0
  58. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/.claude/skills/loki-logs/SKILL.md +0 -0
  59. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/.claude/skills/loki-logs/query-reference.md +0 -0
  60. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/.gitattributes +0 -0
  61. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/.pre-commit-config.yaml +0 -0
  62. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/.python-version +0 -0
  63. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/.readthedocs.yaml +0 -0
  64. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/CHANGELOG.md +0 -0
  65. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/CLAUDE.md +0 -0
  66. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/CONTRIBUTING.md +0 -0
  67. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/LICENSE +0 -0
  68. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/MANIFEST.in +0 -0
  69. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/README.md +0 -0
  70. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/dev-requirements.txt +0 -0
  71. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/docker-compose.dev.yml +0 -0
  72. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/.eslintrc.json +0 -0
  73. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/.prettierrc +0 -0
  74. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/css/tailwind.css +0 -0
  75. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/examples/food_ordering.json +0 -0
  76. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/examples/movie_explorer.json +0 -0
  77. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/examples/patient_intake.json +0 -0
  78. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/examples/restaurant_reservation.json +0 -0
  79. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/examples/travel_planner.json +0 -0
  80. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/favicon.png +0 -0
  81. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/favicon.svg +0 -0
  82. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/index.html +0 -0
  83. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/js/editor/canvas.js +0 -0
  84. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/js/editor/editorState.js +0 -0
  85. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/js/editor/sidePanel.js +0 -0
  86. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/js/editor/toolbar.js +0 -0
  87. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/js/main.js +0 -0
  88. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/js/nodes/baseNode.js +0 -0
  89. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/js/nodes/endNode.js +0 -0
  90. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/js/nodes/flowNode.js +0 -0
  91. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/js/nodes/functionNode.js +0 -0
  92. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/js/nodes/index.js +0 -0
  93. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/js/nodes/mergeNode.js +0 -0
  94. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/js/nodes/startNode.js +0 -0
  95. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/js/types.js +0 -0
  96. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/js/utils/export.js +0 -0
  97. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/js/utils/helpers.js +0 -0
  98. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/js/utils/import.js +0 -0
  99. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/js/utils/validation.js +0 -0
  100. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/jsdoc.json +0 -0
  101. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/package-lock.json +0 -0
  102. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/package.json +0 -0
  103. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/postcss.config.cjs +0 -0
  104. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/public/favicon.png +0 -0
  105. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/public/favicon.svg +0 -0
  106. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/tailwind.config.cjs +0 -0
  107. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/vercel.json +0 -0
  108. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/editor/vite.config.js +0 -0
  109. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/engine_primitives_plan.md +0 -0
  110. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/env.example +0 -0
  111. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/images/food-ordering-flow.png +0 -0
  112. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/pipecat-flows.png +0 -0
  113. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/pipecat_upgrade.md +0 -0
  114. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/requirements.txt +0 -0
  115. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/scripts/check-pypi-package.py +0 -0
  116. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/scripts/fix-ruff.sh +0 -0
  117. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/scripts/pre-commit.sh +0 -0
  118. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/setup.cfg +0 -0
  119. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/src/dv_pipecat_flows.egg-info/dependency_links.txt +0 -0
  120. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/src/dv_pipecat_flows.egg-info/requires.txt +0 -0
  121. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/src/dv_pipecat_flows.egg-info/top_level.txt +0 -0
  122. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/src/pipecat_flows/__init__.py +0 -0
  123. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/src/pipecat_flows/exceptions.py +0 -0
  124. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/src/pipecat_flows/flow_validator.py +0 -0
  125. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/src/pipecat_flows/processors/__init__.py +0 -0
  126. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/src/pipecat_flows/processors/router_mode_guard.py +0 -0
  127. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/src/pipecat_flows/processors/speak_interruption_guard.py +0 -0
  128. {dv_pipecat_flows-0.0.22.dev2100 → dv_pipecat_flows-0.0.22.dev2207}/uv.lock +0 -0
@@ -71,6 +71,11 @@ npm-debug.log*
71
71
  *.njsproj
72
72
  *.sln
73
73
  *.sw?
74
+ ringg-bot/eval/out/*
75
+ ringg-bot/eval/inputs/*
76
+ ringg-bot/eval/report/*
77
+ ringg-bot/eval/samples/*
78
+ ringg-bot/eval/comparison_analysis/*
74
79
 
75
80
  # Auto-generated docs
76
81
  docs/api
@@ -182,3 +187,5 @@ MANIFEST
182
187
 
183
188
  # Asterisk configs
184
189
  remote-asterisk-code/pjsip.conf
190
+ .venvringg1
191
+ .venvringg2
@@ -125,6 +125,23 @@ Example `call_config`:
125
125
  - `tests/test_manager.py`, `tests/test_actions.py`, etc., cover FlowManager and ActionManager edge cases (context resets, action ordering, function registration). Extend these tests when adding new features.
126
126
  - Logging: see the dedicated section below for Loguru usage and the custom logger configuration.
127
127
 
128
+ ## Metrics & Observability
129
+
130
+ - **SDK**: `ringg-telemetry` (metrics-only facade over OpenTelemetry). Initialised once per process in the FastAPI lifespan — `ringg-bot/server.py` — gated on `ENABLE_METRICS`. Export path is OTLP/gRPC → the in-cluster `otel-gateway` → **Google Managed Prometheus**. Tracing is a *separate* stack (Langfuse, `ringg-bot/utils/tracing.py`); the two are unrelated.
131
+ - **Declare instruments in one place**: the registry block near the bottom of `ringg-bot/utils/metrics_collector.py`. Module-level handles (`ringg_metrics.histogram/counter/gauge`), created at import, resolved lazily. Keep the cardinality justification in a comment next to each — the `labels=(...)` tuple is a hard allowlist, and the SDK **silently drops** any label not in it.
132
+ - **Record from a lazily-importing, swallow-all helper** when the call site is on a per-turn hot path — see `_record_redis_latency` in `ringg-bot/ringg_processors/llm_cache.py` and `_record_router_metrics` in `ringg-bot/utils/language_detection.py`. Telemetry must never raise into a live call, and the import must not drag the metrics stack into eval scripts.
133
+ - **Names**: the SDK prefixes everything with `ringg.` and GMP rewrites it for PromQL — `ringg.language.router.duration` + `unit="s"` becomes `ringg_language_router_duration_seconds_{bucket,count,sum}`; a counter with `unit="1"` becomes `..._total`. A non-standard unit gets appended as a suffix, so use `"1"` even for money (put "USD" in the description).
134
+ - **Dashboards as code**: `k8s/monitoring/dashboards/` — one JSON per dashboard plus a README with the `gcloud monitoring dashboards create/update` commands and the assigned resource ids.
135
+ - **Tests**: `ringg_telemetry.testing.install_test_provider()` / `uninstall_test_provider()` route handles to an in-memory reader. See `ringg-bot/tests/test_metrics_bridge.py` and `tests/test_language_router_metrics.py`.
136
+
137
+ ## Data Residency
138
+
139
+ - Some regions forbid their call data leaving regional storage. The single definition of that rule is `is_data_residency_restricted(region)` in `ringg-bot/utils/artifact_store.py` (currently `{"ksa"}`). **Consult it before any new write to the long-lived artifact bucket** — `RUNTIME_CONFIG_BUCKET` is US-located.
140
+ - Restricted regions **skip** the write entirely: there is no fallback bucket and no re-routing. Applies to the runtime-config snapshot, the tool-call snapshot and the language-router samples (`utils/generic_functions/cleanup.py`, `utils/language_sample_store.py`). The data still reaches the region's own backend via the `call_completion` POST, and transcripts/recordings still go to the regional bucket via `utils/transcript._get_gcs_bucket`.
141
+ - `region` is a **per-call** value (`RunConfig.region`, set by the backend), not a pod env var. On the two artifact uploaders it is keyword-only and *required* precisely so a new call site cannot become a violation by omission.
142
+ - Skipping the upload must not skip local cleanup: `LOCAL_STORAGE_DIR` is the pod's `/tmp` and the pod outlives thousands of calls, so anything buffered on disk still has to be unlinked on the skip path.
143
+ - Where feasible, gate at *capture* time too, not just upload time — e.g. `bot.py` passes `collect_samples=not is_data_residency_restricted(_region)` so raw user utterances are never written to `/tmp` for a restricted call.
144
+
128
145
  ## Logging
129
146
 
130
147
  - We use Loguru for all application logging. Do not use the Python `logging` module in app code; always `from loguru import logger`.
@@ -399,6 +416,7 @@ curl -X POST https://api.ringg.ai/call \
399
416
 
400
417
  - `provider_metadata.asterisk_channel_id`, `provider_metadata.asterisk_sip_provider` — set by the bridge for Asterisk calls.
401
418
  - `provider_metadata.transfer.type`, `provider_metadata.transfer.sip_domain` — may be injected from dialplan variables (`TRANSFER_TYPE`, `TRANSFER_SIP_DOMAIN`) or ARI variables for outbound calls. Agents typically do not need to set these.
419
+ - `call_config.transfer.include_call_numbers` — when true, `dialplan_context` transfers add this call's `to_number` / `from_number` to the SIP MESSAGE body. Defaults off.
402
420
 
403
421
  ## Next Steps
404
422
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dv-pipecat-flows
3
- Version: 0.0.22.dev2100
3
+ Version: 0.0.22.dev2207
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
@@ -0,0 +1,13 @@
1
+ # Browser Copilot eval harness — environment template (copy to .env, fill in)
2
+ # NO real secrets in this file.
3
+ #
4
+ # Only the eval/ benchmark harness reads this file. It needs Azure OpenAI creds
5
+ # (COPILOT_AZURE_API_KEY / COPILOT_AZURE_ENDPOINT — see eval/_env.py). The model
6
+ # and API version are pinned in eval/bench.py; the values below just document
7
+ # what the harness runs against.
8
+
9
+ # --- LLM provider (Azure OpenAI) — used by the eval harness ---
10
+ COPILOT_AZURE_ENDPOINT=https://<your-azure-openai>.openai.azure.com
11
+ COPILOT_AZURE_API_KEY=<azure-openai-api-key>
12
+ COPILOT_AZURE_API_VERSION=2024-10-21
13
+ COPILOT_MODEL=gpt-4.1-mini
@@ -0,0 +1,5 @@
1
+ .venv/
2
+ .env
3
+ __pycache__/
4
+ *.pyc
5
+ .venv-stagehand/
@@ -0,0 +1,71 @@
1
+ # Browser Copilot (beta)
2
+
3
+ An embeddable, screen-aware AI copilot for web apps. The agent **sees the current
4
+ page** (structured DOM perception — no screenshots, no screen share), **answers
5
+ questions about it**, and **acts on it** (highlight / click / type / …) under a
6
+ configurable confirmation policy.
7
+
8
+ **One brain, two I/O modes.** Chat and voice are BOTH LiveKit sessions of the
9
+ real Ringg agent (pipecat runtime) — they differ only in I/O: **voice** =
10
+ mic + captions (`media_type=audio`), **chat** = typed text over `lk.chat`
11
+ (`media_type=text`). Both drive the widget's `cp_action` executor. There is no
12
+ separate chat server anymore (the old gpt-4.1-mini `/plan` orchestrator was
13
+ retired — see `docs/METHODS.md`); this package now holds the **eval** harness +
14
+ **docs** only.
15
+
16
+ ```mermaid
17
+ flowchart LR
18
+ W["Widget<br/>perceive (values redacted) · execute · confirm<br/>chat⇄voice toggle"]
19
+ B["Voice runtime (pipecat)<br/>DomContextInjector · browser_actions"]
20
+
21
+ W <-->|"dom_summary ↓ · cp_action ↑ (both modes)"| B
22
+ W -->|"audio | text"| B
23
+ ```
24
+
25
+ Full diagrams (component graph, chat/voice sequences, enablement):
26
+ [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
27
+
28
+ ## Layout
29
+
30
+ ```
31
+ eval/ benchmark harness + fixture pages + committed baseline results
32
+ docs/ METHODS (all approaches + numbers), ARCHITECTURE, CONTRACTS, CONFIG,
33
+ DISTRIBUTION, VOICE_E2E, CODE_MAP (the system file-by-file, all 4 repos)
34
+ scripts/ dev-stack.sh — one-shot local stack (backend/runtime/ngrok/frontend)
35
+ ```
36
+
37
+ The copilot BRAIN is the real agent in the pipecat runtime
38
+ (`../ringg-bot/`) — this package no longer ships a server. The FE widget lives
39
+ in **`dv-frontend-v2/apps/agents-cdn`** (`src/copilot/`, branch
40
+ `feat/browser-copilot`) — the embeddable CDN widget that already owns the
41
+ LiveKit room + chat/voice UI; activation is backend-driven (`copilot_enabled`
42
+ in the webcall response — no widget env). The runtime bridge lives in
43
+ `ringg-bot/ringg_processors/` + `utils/flow_tools/browser_actions.py`.
44
+ (The earlier `fe-ringg-v2-app` widget was removed — one FE home.)
45
+
46
+ ## Run
47
+
48
+ **The whole local stack in one go** (backend + voice runtime + ngrok +
49
+ frontend, with branch/env/Krisp preflight):
50
+
51
+ ```bash
52
+ scripts/dev-stack.sh up # start everything (idempotent, 4 services)
53
+ scripts/dev-stack.sh status # health + branch matrix
54
+ scripts/dev-stack.sh logs runtime # tail a service (runtime|frontend|ngrok)
55
+ scripts/dev-stack.sh down # stop everything it started
56
+ ```
57
+
58
+ ## Eval (the numbers gate)
59
+
60
+ Any change to perception/prompts must re-run the benchmark before docs cite numbers:
61
+
62
+ ```bash
63
+ uv run python eval/bench.py # hybrid v1/v2 + screenshot, QA + action funnel
64
+ ```
65
+
66
+ Results land in `eval/results_*.json`; summaries in `eval/RESULTS.md` and `docs/METHODS.md`.
67
+
68
+ The FE widget lives in `dv-frontend-v2/apps/agents-cdn` (`src/copilot/`,
69
+ branch `feat/browser-copilot`; e2e in `packages/tests` — `pnpm --filter tests
70
+ test:copilot`); the voice runtime bridge lives in `ringg-bot/ringg_processors/`
71
+ on this repo's copilot branches.
@@ -0,0 +1,190 @@
1
+ # Architecture (as-built, beta)
2
+
3
+ Original HLD (design rationale, roadmap phases): [HLD.md](HLD.md). This doc is
4
+ the shorter as-built picture. Contracts: [CONTRACTS.md](CONTRACTS.md) · modes:
5
+ [CONFIG.md](CONFIG.md) · method evidence: [METHODS.md](METHODS.md).
6
+
7
+ ## The whole system at a glance
8
+
9
+ ```mermaid
10
+ flowchart TB
11
+ subgraph BROWSER["🖥️ Browser — the user's page"]
12
+ UI["Widget UI<br/>mini chat · chat⇄voice toggle · confirm bar"]
13
+ PERC["Perception (src/copilot/perception.ts)<br/>data-cp tagging · text/rows/headings ·<br/>state flags · VALUES NEVER LEAVE"]
14
+ EVT["Events (events.ts)<br/>focus / typing-length / selection / click"]
15
+ EXEC["Executor (executor.ts)<br/>runCpAction: click · type(native setter) ·<br/>select · scroll · highlight — confirm bar"]
16
+ end
17
+
18
+ subgraph VOICE["🎙️ The agent — one brain, two I/O modes"]
19
+ BE["backend /calling/webcall<br/>copilot_support: tools + copilot_config"]
20
+ LK[("LiveKit room<br/>audio | text (lk.chat) + RPC")]
21
+ subgraph BOTS["pipecat runtime (ringg-bot)"]
22
+ BWF["bot_with_flows.py<br/>multi-node"]
23
+ BOT["bot.py<br/>single-node"]
24
+ INJ["DomContextInjector<br/>ONE [Browser context] msg"]
25
+ TOOLS["browser_actions tools<br/>guardrails + act-then-wait"]
26
+ end
27
+ end
28
+
29
+ UI -->|"startWebCall (media_type: audio | text)"| BE --> LK
30
+ PERC -->|receive_dom_summary RPC| INJ
31
+ EVT -->|receive_user_event RPC| INJ
32
+ BWF --- INJ
33
+ BOT --- INJ
34
+ BWF --- TOOLS
35
+ BOT --- TOOLS
36
+ TOOLS -->|cp_action RPC| EXEC
37
+ EXEC -->|"{ok, result}"| TOOLS
38
+ ```
39
+
40
+ ## Two planes
41
+
42
+ ```
43
+ ┌─────────────────────── BROWSER (the user's page) ────────────────────────┐
44
+ │ dv-frontend-v2 agents-cdn widget (src/copilot/) │
45
+ │ • Perception: id-contract collector (data-cp registry, label/value │
46
+ │ pairing, tables, viewport, redaction) + MutationObserver + SPA hooks │
47
+ │ • Execution: runCpAction — full human-action vocabulary + confirm bar │
48
+ │ • UI: floating mini chat (ChatWindowFrame) — chat⇄voice toggle; both │
49
+ │ are LiveKit sessions of the agent (text vs audio I/O) │
50
+ └───────────────────────────────────▲──────────────────────────────────────┘
51
+ startWebCall(media_type) │ receive_dom_summary ↓ · cp_action ↑
52
+ ▼
53
+ ┌────────────── VOICE RUNTIME (ringg-bot) ──────────────┐
54
+ │ LiveKit bot = the REAL agent (STT/TTS or lk.chat text)│
55
+ │ DomContextInjector ← receive_dom_summary RPC; │
56
+ │ browser_actions flow tools → cp_action RPC; captions │
57
+ │ via native publish_transcription. bot.py (single-node)│
58
+ │ + bot_with_flows.py (multi-node) share the tools. │
59
+ └───────────────────────────────────────────────────────┘
60
+ ```
61
+
62
+ ## The loop (both modes)
63
+
64
+ **perceive → the agent decides → guardrail → execute → ack → re-perceive**
65
+
66
+ - Perception is *structured text*, never pixels: the collector emits addressable
67
+ elements (`[cp:e11] textbox: Coupon code`) with values REDACTED in-browser,
68
+ pushed to the bot as `receive_dom_summary` → ONE `[Browser context]` message.
69
+ - The agent references those `[cp:…]` handles in a `cp_action` tool call.
70
+ - Guardrails on the bot (`copilot_config`: origin allowlist / kill-switch /
71
+ high_risk_confirm) + the widget's one-tap confirm bar.
72
+ - Every execution returns `{ok, result}` over the RPC — the agent learns what
73
+ happened; mutating actions then WAIT (act-then-wait) for the fresh page.
74
+
75
+ ### Chat turn (media_type = text)
76
+
77
+ ```mermaid
78
+ sequenceDiagram
79
+ autonumber
80
+ actor U as User
81
+ participant W as Widget
82
+ participant LK as LiveKit (lk.chat)
83
+ participant B as Bot (pipecat)
84
+ participant LLM as Agent LLM
85
+
86
+ Note over W,B: on connect + every page change
87
+ W->>LK: receive_dom_summary (DomSummary v2)
88
+ LK->>B: → DomContextInjector → [Browser context]
89
+ U->>W: type "open Campaigns"
90
+ W->>LK: lk.chat send
91
+ LK->>B: typed message
92
+ B->>LLM: [Browser context] + chat history
93
+ LLM-->>B: tool call click_element(cp="e12", say="Opening Campaigns")
94
+ B->>B: guardrails: click is confirm-exempt → auto-run<br/>(only a destructive type would set require_confirm)
95
+ B->>LK: cp_action RPC (require_confirm:false)
96
+ LK->>W: → executor.runCpAction
97
+ W->>W: runs click directly (no confirm bar)
98
+ W-->>B: {ok:true}
99
+ B->>W: lk.chat: "Opening Campaigns — take a look."
100
+ Note over B: act-then-wait: no follow-up completion; next page pushes a fresh summary
101
+ ```
102
+
103
+ ### Voice turn (media_type = audio)
104
+
105
+ ```mermaid
106
+ sequenceDiagram
107
+ autonumber
108
+ actor U as User
109
+ participant W as Widget
110
+ participant LK as LiveKit
111
+ participant B as Bot (pipecat)
112
+ participant LLM as Agent LLM
113
+
114
+ Note over W,B: on connect + every page change
115
+ W->>LK: receive_dom_summary (DomSummary v2)
116
+ LK->>B: → DomContextInjector
117
+ B->>B: render ONE "[Browser context]" msg<br/>(actions+flags · page content · focused · viewport)
118
+
119
+ U->>LK: 🎤 "open Campaigns"
120
+ LK->>B: audio → STT
121
+ B->>LLM: context (incl. [Browser context]) + transcript
122
+ LLM-->>B: tool call click_element(cp="e12", say="Opening Campaigns")
123
+ B->>B: guardrails: kill-switch · origin allowlist ·<br/>click is confirm-exempt → auto-run
124
+ B->>LK: cp_action RPC {action, target, require_confirm:false}
125
+ LK->>W: → executor
126
+ W->>W: runs click directly (no confirm bar)
127
+ W-->>B: {ok: true}
128
+ B->>U: 🔊 speaks "say" ack
129
+ Note over B: act-then-wait: _suppress_followup_llm →<br/>NO follow-up completion (page is changing)
130
+ W->>B: fresh receive_dom_summary (new page)
131
+ U->>B: next utterance sees the NEW [Browser context]
132
+ ```
133
+
134
+ ### Enablement (who turns the copilot on)
135
+
136
+ ```mermaid
137
+ flowchart LR
138
+ CFG["agent_versions.call_config<br/>copilot_config {enabled, high_risk_confirm}"]
139
+ BE["backend copilot_support.py"]
140
+ MN["multi-node: inject 8 tools into<br/>every flow node's predefined_tools"]
141
+ SN["single-node: bot.py<br/>register_single_node_browser_tools"]
142
+ RESP["webcall response<br/>copilot_enabled: true"]
143
+ WID["widget activates<br/>(DOM push + cp_action handler)"]
144
+
145
+ CFG --> BE
146
+ BE -->|flow_config| MN
147
+ CFG -->|call_config forwarded| SN
148
+ BE --> RESP --> WID
149
+ MN --> BOT1["bot_with_flows.py"]
150
+ SN --> BOT2["bot.py"]
151
+ ```
152
+
153
+ ## Voice specifics
154
+
155
+ - Captions: `LiveKitCaptionObserver` already publishes user STT + agent speech
156
+ as native LiveKit transcription for every audio call — the widget renders
157
+ them with the app's existing `WebcallChatUI` (`useTrackTranscription`).
158
+ - DOM freshness: widget pushes `receive_dom_summary` on connect and on
159
+ debounced mutations, `receive_page_change` on SPA routes; the injector keeps
160
+ exactly ONE `[Browser context]` message in the LLM context.
161
+ - Actions: the agent's `browser_actions` flow tools (attached per agent config)
162
+ call `cp_action`; the widget executes under the same confirm policy; the
163
+ runtime's `copilot_config` (allowed_origins / high_risk_confirm / disabled)
164
+ is the server-side guardrail.
165
+
166
+ ## Related: the runtime's OTHER DOM path
167
+
168
+ `cp_action` (this copilot) is not the only DOM mechanism — widget authors have
169
+ a separate one-way `execute_dom_action` path (preconfigured `action_id` →
170
+ host-page custom event). They are complementary by design; boundary + table:
171
+ [CONTRACTS.md → "Two DOM paths"](CONTRACTS.md#two-dom-paths--complementary-by-design-do-not-consolidate).
172
+
173
+ ## Security invariants
174
+
175
+ 1. Input **values never leave the browser** (masked flags only; server rejects
176
+ `value` keys).
177
+ 2. Mutating actions are **origin-allowlisted** (runtime
178
+ `copilot_config.allowed_origins`) and **policy-gated** on both sides.
179
+ 3. Widget activation is domain-gated (agent `whitelisted_domains` for voice;
180
+ CORS + Origin check for chat).
181
+ 4. Kill switches: `copilot_config.disabled` (agent) / `COPILOT_DISABLED`
182
+ (runtime env).
183
+
184
+ ## Repos & branches (beta)
185
+
186
+ | repo | branch | contents |
187
+ |---|---|---|
188
+ | dv-pipecat-flows | `feat/browser-copilot-beta` | this package (eval/docs) + runtime `copilot_config` field |
189
+ | dv-pipecat-flows (runtime) | `feat/browser-copilot` lineage | copilot bridge: contract, injector, RPC bridge, browser_actions |
190
+ | dv-frontend-v2 | `feat/browser-copilot` | widget: apps/agents-cdn/src/copilot (perception/executor/hook/confirm) + packages/tests copilot e2e |
@@ -0,0 +1,254 @@
1
+ # CODE_MAP — the browser copilot, file by file, across four repos
2
+
3
+ One agent brain (the pipecat bot) + one embeddable widget. Chat and voice are both
4
+ LiveKit webcalls (`media_type` text|audio) of the same agent. The widget perceives the
5
+ host page (hybrid-v2 DOM walk, `data-cp` handles), pushes a DomSummary v2 over LiveKit
6
+ RPC; the bot keeps exactly ONE `[Browser context]` message in the LLM context; the LLM
7
+ calls browser_actions tools; the bot RPCs `cp_action` back; the widget executes the DOM
8
+ action and answers `{ok, result, error}`. Activation is backend-driven: the webcall
9
+ response's `copilot_enabled` comes from the agent's `call_config.copilot_config.enabled`.
10
+
11
+ Repo/branch matrix: `dv-frontend-v2 @ feat/browser-copilot` (widget),
12
+ `fe-ringg-v2-app @ feat/browser-copilot-widget-beta` (dashboard embed),
13
+ `new_calling_agent_backend @ feat/browser-copilot` (webcall mint),
14
+ `dv-pipecat-flows @ feat/browser-copilot-beta` (runtime brain, `ringg-bot/`).
15
+
16
+ Quick index:
17
+
18
+ | Concern | File |
19
+ |---|---|
20
+ | Wire types/constants (TS) | `dv-frontend-v2/apps/agents-cdn/src/copilot/contract.ts` |
21
+ | Perception (DOM walk, handles) | `…/src/copilot/perception.ts` |
22
+ | User-event tracking | `…/src/copilot/events.ts` |
23
+ | Action execution | `…/src/copilot/executor.ts` |
24
+ | Widget↔bot stitch (hook) | `…/src/copilot/use-copilot.ts` |
25
+ | Confirm UI | `…/src/copilot/ActionConfirmBar.tsx` |
26
+ | Webcall start + Room + wiring | `…/src/App.tsx` (Room born in `AppWrapper.tsx`) |
27
+ | Embed entry / env / harness | `…/src/main.tsx`, `…/src/lib/env-helpers.ts`, `…/copilot-test.html` |
28
+ | Dashboard embed | `fe-ringg-v2-app/components/RinggWidgetEmbed.tsx` |
29
+ | Copilot enablement policy | `new_calling_agent_backend/calling/copilot_support.py` |
30
+ | Webcall mint | `new_calling_agent_backend/calling/router.py` → `calling/service.py` |
31
+ | Webcall auth | `new_calling_agent_backend/middleware/auth_middleware.py` |
32
+ | Wire constants (bot) + scrub | `ringg-bot/ringg_processors/copilot_contract.py` |
33
+ | RPC bridge | `ringg-bot/ringg_processors/livekit_rpc_bridge.py` |
34
+ | [Browser context] injector | `ringg-bot/ringg_processors/dom_context_injector.py` |
35
+ | Tools + guardrails | `ringg-bot/utils/flow_tools/browser_actions.py` |
36
+ | Bot wiring | `ringg-bot/bot_with_flows.py` (flows) / `ringg-bot/bot.py` (single-node) |
37
+ | Local stack | `browser_copilot/scripts/dev-stack.sh` |
38
+
39
+ ## Wire diagram (files on every hop)
40
+
41
+ ```
42
+ HOST PAGE (customer site / copilot-test.html / fe-ringg-v2-app dashboard)
43
+ └─ RinggWidgetEmbed.tsx / <script> loader ──► window.loadAgent() [main.tsx]
44
+ └─ AppWrapper.tsx (new Room) ──► App.tsx
45
+
46
+ App.tsx ── POST {backend}/calling/webcall (getAuthHeaders, env-helpers.ts) ──► router.py
47
+ router.py /webcall ── get_webcall_identity [auth_middleware.py] ──► service.py
48
+ service.py handle_initiate_webcall_request
49
+ ├─ origin_allowed_for_webcall / resolve_copilot_enabled /
50
+ │ build_copilot_session_config / enable_browser_copilot_tools_in_flow
51
+ │ [copilot_support.py]
52
+ └─ session → bot (agent dispatch, or POST /webcall/livekit/start when
53
+ WEBCALL_LIVEKIT_VIA_BOT=1) ──► {user_token, call_id, copilot_enabled} ──► App.tsx
54
+
55
+ App.tsx room.connect(user_token) ◄── LiveKit room ──► bot_with_flows.py / bot.py
56
+ (identity "user") (identity "agent")
57
+
58
+ PERCEPTION widget ──► bot
59
+ use-copilot.ts (MutationObserver/scroll/popstate + cheapHash)
60
+ └─ pushSummary → buildDomSummary [perception.ts]
61
+ → performRpc "receive_dynamic_data" {receive_dom_summary | receive_user_event
62
+ [events.ts] | receive_page_change}
63
+ ──► livekit_rpc_bridge.py _on_receive_dynamic_data → copilot_sink
64
+ ──► dom_context_injector.py absorb → _sync_to_context → ONE "[Browser context]"
65
+
66
+ ACTION bot ──► widget
67
+ LLM tool call → browser_actions.py handler → _run (guardrails)
68
+ → livekit_rpc_bridge.py send_dom_action → performRpc "cp_action"
69
+ ──► use-copilot.ts handler (confirm gate → ActionConfirmBar.tsx)
70
+ ──► executor.ts runCpAction (resolveTarget → click/type/select/…)
71
+ ──► {ok,result,error} back ──► _run: say-ack (_speak, scrub_handles
72
+ [copilot_contract.py]) + _suppress_followup_llm (act-then-wait)
73
+
74
+ CHAT TEXT bot ──► widget: send_chat_text (topic "lk.chat", scrub_handles)
75
+ ```
76
+
77
+ ## Cross-repo sync points (change one side, check the other)
78
+
79
+ - `contract.ts` ↔ `copilot_contract.py` — RPC names, component types, action kinds,
80
+ identities. Mapping table + invariants: `docs/CONTRACTS.md`.
81
+ - `perception.ts:buildDomSummary()` ↔ `dom_context_injector.py:_render_summary()` —
82
+ the DomSummary v2 shape. All v2 keys optional; legacy `headings` kept for old injectors.
83
+ - `copilot_support.py:BROWSER_COPILOT_ACTION_TOOLS` ↔
84
+ `browser_actions.py:BROWSER_ACTION_SCHEMAS` — the 9 tool names the backend injects
85
+ must resolve to schemas the bot registers.
86
+ - Timeouts: widget `AUTO_DENY_MS=15s` (use-copilot.ts) must stay under the bot's
87
+ `send_dom_action` 20s RPC timeout (livekit_rpc_bridge.py) — a stuck confirm bar
88
+ auto-denies before the bot's RPC gives up.
89
+ - Identities: widget token minted as `"user"`, bot joins as `"agent"` — hardcoded on
90
+ both sides (`USER_IDENTITY` in livekit_rpc_bridge.py / `AGENT_IDENTITY` in contract.ts;
91
+ App.tsx discovers the agent by `identity.includes("agent")`).
92
+
93
+ ## A. Widget — `dv-frontend-v2/apps/agents-cdn/` (the deep end)
94
+
95
+ ### `src/copilot/contract.ts`
96
+ - TypeScript mirror of `ringg-bot/ringg_processors/copilot_contract.py`; kept in lock-step by hand.
97
+ - Transport constants: `AGENT_IDENTITY="agent"`, `RPC_CP_ACTION="cp_action"`, `RPC_RECEIVE_DYNAMIC_DATA="receive_dynamic_data"`, component types `CT_DOM_SUMMARY="receive_dom_summary"`, `CT_USER_EVENT`, `CT_PAGE_CHANGE`.
98
+ - Types: `PageState`/`ElementState` (chat-era addressable view, still feeds `find_elements`), `UserEvent`, `CpAction`/`CpTarget`/`CpResult`, `DomSummary` + `DomSummaryAction`/`DomSummaryContent` (v2: `content`, `focused`, `viewport`, `truncated`, omit-if-default flags `filled/disabled/sensitive/off/options`; legacy `headings` kept for old injectors).
99
+ - Load-bearing redaction invariant in the types themselves: an element NEVER carries an input `value` — only `value_masked` / `sensitive` flags.
100
+
101
+ ### `src/copilot/perception.ts`
102
+ - How the copilot SEES the page — production port of the benchmarked hybrid-v2 collector (`browser_copilot/eval/bench.py::COLLECTOR_JS`).
103
+ - `CP_ATTR="data-cp"`, `UI_MARKER="data-copilot-ui"` (the widget's own subtree is never perceived); `INTERACTIVE_SEL` covers buttons/links/inputs/selects + ARIA roles incl. combobox/listbox/option.
104
+ - `collectTagged()` — the single tagging walk: clears stale tags, then (1) interactive elements → `e1..eN` handles stamped as `data-cp`, (2) table/grid rows → one `t*` line each (cap 40), (3) headings/small text containers → `t*` lines.
105
+ - Helpers: `roleOf` (explicit role → tag map → input-type map), `nameOf` (aria-label → label → aria-labelledby → placeholder → title → text → name, ≤90 chars), `optionsOf` (native `<select>` choices, ≤12, skips placeholders), `isSensitive` (password type, `cc-*` autocomplete, `SENSITIVE_RE` on name/id/testid), `isFilled` (drives `value_masked` — the flag, never the value).
106
+ - `prioritize()` — budget buckets: in-viewport interactive > off-viewport interactive > in-viewport text > rest (stable within rank).
107
+ - `collectPageState()` → `PageState` (≤300 elements + `recent_events` from events.ts) — consumed locally by `findElements`/`cheapHash`.
108
+ - `buildDomSummary()` → `DomSummary` v2 (≤120 actions, ≤70 content lines — keeps the RPC under LiveKit's ~15KiB cap), forms grouped under their `<form>`, landmarks, `focused`, `viewport`, `truncated`. Shape MUST match `DomContextInjector._render_summary`.
109
+ - `resolveById(id)` — live `[data-cp="…"]` lookup; the handle lives in the DOM, so resolution is always fresh.
110
+
111
+ ### `src/copilot/events.ts`
112
+ - Behavioral perception: what the user is DOING (structure says what exists). Pure module (no React) — shared by the hook and the e2e bundle.
113
+ - Ring buffer of 10 `UserEvent`s (oldest→newest), consumed by `collectPageState()`; `onUserEvent(cb)` push-subscribes (the voice session forwards each event to the bot).
114
+ - `startEventTracking()` wires capture-phase listeners: `focusin` (focus), `input` (typing, 400ms debounce — records `text_len` ONLY, never the text), `selectionchange` (300ms debounce, the one kind that carries text: visible page content the user highlighted, ≤200 chars), `click`. Everything inside `[data-copilot-ui]` is ignored; targets resolve to the nearest `data-cp` ancestor via `targetIdOf`.
115
+ - `stopEventTracking()` removes listeners and clears the ring.
116
+
117
+ ### `src/copilot/executor.ts`
118
+ - The agent's hands: `runCpAction(action, ctx)` runs a parsed `CpAction`, always resolving to `{ok, result, error}` (never throws).
119
+ - `resolveTarget()` chain: explicit `targetId` → `target.cp` → `target.ref` (shares the cp namespace; the bot no longer advertises `ref` but stray ones still resolve) → CSS `selector` → visible `text` (substring match over tagged + button/link candidates) → `role`(+name).
120
+ - `normalizeCp()` maps the wire action to an op; `click`/`type`/`select`/`highlight`/`read_region` require a resolved target ("target not found" otherwise); `scroll` only requires one when the bot asked for an element scroll — else `scrollPage(direction, amount)` (default step 0.8×viewport). `screenshot` → "unsupported in beta widget".
121
+ - `select`'s value comes from `params.value` (the bot's `select_option.value` schema), falling back to `params.text`.
122
+ - `typeInto`/`setNativeValue`: writes via the native value setter + dispatched `input`/`change` events so React controlled inputs actually update (a plain `.value=` is ignored by React).
123
+ - `selectOption()` (async): native `<select>` by value/text; else custom dropdown — click the trigger (`[role=combobox]`/`[aria-haspopup]`/button), then `waitForOption(want, 1200ms)` polls the WHOLE document every 40ms (Radix/shadcn portal the listbox to `<body>`), clicks the match, or re-clicks the trigger to close if none found.
124
+ - `findElements(query, role, limit)` — query-scored (exact id/name=3, contains=2), capped ≤50, returns `{matches:[{cp,role,name}], total_considered}`; never the full page state (RPC size). `read_dom` returns `buildDomSummary()`; `read_region` returns the target's `innerText` ≤2000 chars; `highlight` = smooth scroll + 3px outline for 2s.
125
+
126
+ ### `src/copilot/use-copilot.ts` — THE stitch point
127
+ - `useCopilot({room, agentIdentity, enabled, hostEl, onLog})` — App owns the Room; this hook adds the copilot on top when `enabled` (webcall said `copilot_enabled` && call connected). Stamps `hostEl` with `UI_MARKER` on mount, removes on teardown.
128
+ - Bot→widget: registers `room.localParticipant.registerRpcMethod("cp_action", handler)`. Handler parses the `CpAction`; if `params.require_confirm` → `askConfirm()` surfaces a `pending` (rendered by ActionConfirmBar), auto-DENIES after `AUTO_DENY_MS=15s` (beats the bot's 20s RPC timeout); denial returns `{ok:false, error:"user denied"}`.
129
+ - Otherwise → `runCpAction()`; the result JSON is the RPC response. Confirmation POLICY is the bot's (the widget just honors the flag; the confirm label is `target.text ?? target.cp ?? action`). Every action is logged to the widget's SystemLog via `onLog`.
130
+ - Widget→bot: `pushSummary()` — `buildDomSummary()`; if the URL changed since the last push, first sends `CT_PAGE_CHANGE {url,title}`, then `CT_DOM_SUMMARY` via `performRpc("receive_dynamic_data")` to the agent identity. Guards `room.state === Connected`. `pushSummaryWithRetry()` — up to 6 attempts, backoff 700ms×n, for the agent-connect race (the bot registers its handler a beat after `ParticipantConnected`).
131
+ - Perception loop: debounced (400ms) MutationObserver on `document.body` (childList + subtree + attrs `aria-label/disabled/aria-checked/value/hidden`), ignoring its own `data-cp` churn and the widget subtree; `scroll`/`resize` throttled 500ms; `popstate`/`hashchange` for pure history moves (pushState apps also mutate the DOM, which the observer catches).
132
+ - `notify()` dedupes with `cheapHash()` = `route|elementCount|firstId|lastId|focused|scroll_y` before pushing — cheap enough to run on every debounce tick.
133
+ - First summary fires when the agent participant appears (`RoomEvent.ParticipantConnected`, or immediately if already present). `onUserEvent` subscription forwards each tracked event as `CT_USER_EVENT` (mapped by `toRuntimeEvent`: `target_id`→`target_ref`, typing carries `text_len` only; fire-and-forget).
134
+ - Teardown: unregister RPC, disconnect observer/listeners, stop tracking, resolve any pending confirm with `false` (never leave the bot's RPC hanging), reset hash/url.
135
+
136
+ ### `src/copilot/ActionConfirmBar.tsx`
137
+ - One-tap confirm for a `require_confirm` action; themed via `useWidgetTheme`, inline-styled (embeds in arbitrary host pages).
138
+ - Verb map (`click`→"Click", `type`→"Type into", …); for `type` shows the text to be typed, else the target label. Buttons carry `data-ringg="copilot-confirm-approve"` / `"copilot-confirm-deny"`; the bar is `data-ringg="copilot-confirm-bar"`.
139
+ - Purely presentational — approve/deny call the hook's `confirm()`/`deny()`, which resolve the pending promise inside the `cp_action` RPC handler.
140
+
141
+ ### `src/App.tsx` (copilot wiring only)
142
+ - Owns the webcall + Room lifecycle (Room itself is created in `AppWrapper.tsx` and provided via `RoomContext`). `handleCallStart(mediaType)` → `fetch(`${getBackendUrl(mode)}/calling/webcall`)` with `{agent_id, custom_args_values, media_type}` and `getAuthHeaders(authorization, xApiKey)`; then `setCopilotEnabled(data?.copilot_enabled === true)` and `room.connect(getLivekitUrl(mode), data.user_token)` (mic enabled for audio).
143
+ - `agentParticipant` = first remote participant whose identity includes `"agent"`; `copilotHostEl` = `#ringg_ai_container` (the hook stamps it `data-copilot-ui`).
144
+ - `useCopilot({room, agentIdentity: agentParticipant?.identity, enabled: copilotEnabled && isCalling, hostEl, onLog: pushSystemLog})` → `{pending, confirm, deny}`.
145
+ - Renders the confirm overlay when `pending`: fixed, bottom 96 / right 24, `zIndex 2147483000`, wrapping `<ActionConfirmBar/>` (`data-ringg="copilot-confirm-overlay"`).
146
+ - `handleCallEnd()` resets `copilotEnabled` to false (the hook effect tears everything down).
147
+
148
+ ### `src/main.tsx`
149
+ - The embed entry. Dev mode: auto-creates `#ringg_ai_container` and mounts `AppWrapper` with `DEV_APP_CONFIG` (env `VITE_X_API_KEY` / `VITE_DEV_AGENT_ID`).
150
+ - Prod bundle: exposes `window.loadAgent(config)` — reuses an existing `#ringg_ai_container` (integrator-positioned) or appends one to `<body>`, then renders `AppWrapper config`.
151
+ - Also injects the brand `@font-face` by resolving the woff2 next to the loaded `dv-agent.es.js` (classic-script-safe; no `import.meta`).
152
+
153
+ ### `src/lib/env-helpers.ts`
154
+ - `getBackendUrl(mode)` / `getLivekitUrl(mode)`: `dev|stage|prod` → `VITE_BACKEND_URL_{DEV,STAGE,PROD}` / `VITE_LIVEKIT_SERVER_URL_*` (baked at build time).
155
+ - `getAuthHeaders(authorization, xApiKey)`: exactly one header — `Authorization` wins over `X-API-KEY`; dev builds fall back to `VITE_AUTHORIZATION` / `VITE_X_API_KEY`.
156
+
157
+ ### `copilot-test.html`
158
+ - Standalone host-page harness ("Acme Corp"): loads the LOCAL build (`./dist/dv-agent.es.js` + `style.css`), then `window.loadAgent({mode:"dev", agentId, xApiKey, defaultTab:"audio"})` (agent id + workspace key via inputs or `?agentId=…&key=…`).
159
+ - Test surface: a sign-in form (email, password → flagged `sensitive`, plan `<select>`), and Subscribe / Contact sales / Delete account buttons wired to visible result text — demonstrates click auto-run vs the type-only confirm policy.
160
+
161
+ ### e2e — `packages/tests/tests/copilot/` + `packages/tests/copilot.playwright.config.ts`
162
+ - `bundle.ts` — `buildCopilotBundle()` concatenates the REAL `contract/perception/events/executor` sources, strips module syntax, transpiles with the TypeScript compiler API, and exposes `window.__cp` — specs exercise shipped code, not a re-implementation.
163
+ - 3 specs: `copilot-perception.spec.ts` (grounding/roles/ids resolve), `copilot-events.spec.ts` (typed text NEVER captured — length only), `copilot-summary-v2.spec.ts` (v2 content with resolvable `t*` handles).
164
+ - Fixtures: static pages at `packages/tests/tests/fixtures/copilot-pages/{checkout,dashboard,signup}.html`, loaded over `file://` — no app server, no auth (`copilot.playwright.config.ts` deliberately omits the root config's webServer). Run: `pnpm --filter tests test:copilot`.
165
+
166
+ ## B. Host-page embed — `fe-ringg-v2-app` (@ feat/browser-copilot-widget-beta)
167
+
168
+ ### `components/RinggWidgetEmbed.tsx`
169
+ - Embeds the agents-cdn widget into the dashboard EXACTLY like a customer site: appends `style.css` + `dv-agent.es.js` from `NEXT_PUBLIC_RINGG_WIDGET_URL` (default `/ringg-widget`, synced from the widget build), then `window.loadAgent({mode, agentId, xApiKey | authorization, defaultTab:"audio"})`.
170
+ - Env-gated: `NEXT_PUBLIC_RINGG_WIDGET_ENABLED`, `_AGENT_ID`, `_API_KEY`, `_MODE`. Auth: workspace `X-API-KEY` if set, else the logged-in user's `Bearer` token (`useAuth`). Renders `null`; mounted once in `app/layout.tsx` (`<RinggWidgetEmbed />`).
171
+ - All copilot brain/UI live in the widget — this file is ONLY the embed; activation stays backend-driven (`copilot_enabled`).
172
+
173
+ ## C. Backend — `new_calling_agent_backend` (@ feat/browser-copilot)
174
+
175
+ ### `calling/copilot_support.py`
176
+ - `extract_copilot_config(agent_version)` — reads the RAW `call_config` dict (not the pydantic model, so unknown-key stripping elsewhere can't eat it) → `copilot_config` or `{}`.
177
+ - `resolve_copilot_enabled()` — per-AGENT capability: `call_config.copilot_config.enabled == true` (no workspace switch). `build_copilot_session_config()` → `{enabled, high_risk_confirm}` handed to the bot session.
178
+ - `origin_allowed_for_webcall(origin, whitelist)` — same rule for pages and extensions: origin must be in the agent's `whitelisted_domains` (no `chrome-extension://` bypass).
179
+ - `BROWSER_COPILOT_ACTION_TOOLS` — the 8 canonical names (read_dom, find_elements, read_region, click_element, type_text, select_option, scroll_page, highlight_element), sync'd with the bot's `BROWSER_ACTION_SCHEMAS`. `enable_browser_copilot_tools_in_flow(flow_config)` appends `{"name": …}` to every flow node's `predefined_tools` (in place, idempotent).
180
+
181
+ ### webcall flow — `calling/router.py` → `calling/service.py`
182
+ - `router.py` `POST /webcall` (router prefix `/calling`, mounted under `/ca/api/v0` in `main.py`) — requires an `Origin` header, authenticates via `Depends(get_webcall_identity)`, delegates to `handle_initiate_webcall_request(payload, current_user, db, origin)` in `service.py`.
183
+ - `service.py`: agent + version lookup → `origin_allowed_for_webcall` (403 if not whitelisted) → `initiate_call_helper`. In the `runtime == "livekit"` branch: `copilot_enabled = resolve_copilot_enabled(agent_version)`; `build_copilot_session_config` is written into the session's `call_config.copilot_config`; when enabled, `enable_browser_copilot_tools_in_flow(flow_config)` expands the flag into concrete tools on every node.
184
+ - Session delivery: `WEBCALL_LIVEKIT_VIA_BOT=1` (local dev) proxies the fully-assembled session to the bot's `POST {PIPECAT_URL}/webcall/livekit/start` (bot creates the room + mints tokens); otherwise `create_explicit_agent_dispatch` + a locally-minted user token. Response: `{user_token, call_id, enabled_slash_commands, copilot_enabled}`. Every other runtime branch hard-sets `copilot_enabled: False` (copilot is LiveKit-webcall-only).
185
+
186
+ ### `middleware/auth_middleware.py`
187
+ - `get_webcall_identity` → `resolve_webcall_identity`: (1) `Authorization: Bearer pk_live_*` — the per-agent webcall public key, binds to exactly one agent (`auth_method="webcall_pk"`; can't reach admin endpoints, can't set callbacks); (2) user JWT via header or HttpOnly cookie (dashboard test calls); (3) deprecated fallback: `X-API-KEY` → `fetch_user_from_db` → `Workspace.api_key` lookup (`auth_method="api_key"`, logs a deprecation warning). Fails closed.
188
+
189
+ ## D. Runtime brain — `dv-pipecat-flows/ringg-bot/` (@ feat/browser-copilot-beta)
190
+
191
+ ### `ringg_processors/copilot_contract.py`
192
+ - Bot-side constants: identities `AGENT_IDENTITY="agent"` / `USER_IDENTITY="user"`; `RPC_CP_ACTION`; inbound component types (`CT_DOM_SUMMARY`, `CT_USER_EVENT`, `CT_PAGE_CHANGE`, plus legacy `CT_DOM_SNAPSHOT`/`CT_SCREENSHOT`) collected in `COPILOT_INBOUND_TYPES`; the `ACTION_*` names; `MUTATING_ACTIONS = {click, type, select, scroll}`.
193
+ - `scrub_handles(text)` — TTS/chat hygiene: strips `[cp:e48]`-style and bare `e48/t11/f3` tokens from user-facing text, tidies the gaps, never returns empty. Backstop for the paths we control (chat relay + spoken acks); the streamed voice narration relies on the prompt.
194
+
195
+ ### `ringg_processors/livekit_rpc_bridge.py`
196
+ - `LiveKitRPCBridge(room, task, on_call_tools_config, bot_logger, copilot_sink)` — owns the LiveKit RPC surface for one webcall; node-agnostic (both bots).
197
+ - `register_handlers()` registers `receive_dynamic_data` on `room.local_participant`. `_on_receive_dynamic_data`: copilot component types → `copilot_sink(component_type, component_data)` WITHOUT injecting a user turn (ambient observations); widget interactions (calendar/form/quick-reply/blocks) → tool API + `_inject_user_turn` (`LLMMessagesAppendFrame(run_llm=True)`).
198
+ - `send_dom_action(action, params, timeout=20s)` — `perform_rpc(destination="user", method="cp_action")`; logs the full round-trip latency on every path (`grep cp_action`); ALWAYS returns `{ok, result, error}` (failures/timeouts become `ok:False`, never an exception).
199
+ - `send_chat_text(text)` — assistant replies over the `lk.chat` text stream (text-mode webcalls), through `scrub_handles`. Plus non-copilot senders: `send_dynamic_data`, `send_blocks_payload` (`ringg.blocks` stream), `send_source_urls`, `publish_caption`.
200
+
201
+ ### `ringg_processors/dom_context_injector.py`
202
+ - Absorbs copilot perception and keeps EXACTLY ONE `[Browser context]` user message in the LLM context (marker `_CTX_MARKER`); not a FrameProcessor — input arrives out-of-band over RPC, so it mutates the shared context via the user aggregator (`set_flow_manager` for flows / `set_context_aggregator` for single-node).
203
+ - `absorb(component_type, data)`: `CT_DOM_SUMMARY` → store + `_sync_to_context`; `CT_PAGE_CHANGE` → keep url/title, DROP stale summary; `CT_USER_EVENT` → `_update_activity` ("typing in element e9 (12 chars)" — selection changes re-sync); legacy `CT_DOM_SNAPSHOT` supported.
204
+ - `_render_summary()` — v2 renderer: Viewport line (px + scroll %), `Focused: [cp:…]`, on-screen actions (`[cp:e7] button "Subscribe" [filled] [sensitive] (disabled)` + `— options: …` suffix for dropdowns), "Off-screen (scroll_page to reach)" (≤20), Forms, "Page content" (`# heading` / `row:` / text lines with `[cp:tN]`, ≤60), Regions, legacy flat headings only when no v2 content, and the `interactive_count` + `find_elements` hint (+ "(list truncated)").
205
+ - Legacy (pre-v2) summaries render byte-identical to before (`_action_flags` is empty for them); a raw input `value` key is never rendered on the summary path.
206
+ - `_build_message_text()` = marker + URL/Title + summary + "User is currently: …"; hard tail-truncate at 12,000 chars (hostile summaries can't blow the context). `_sync_to_context()` filters old `[Browser context]` messages and appends the fresh one.
207
+ - Also holds `_url` — read by `browser_actions._current_url` for the mutating-action origin guardrail.
208
+ - Auto-continue chain: `arm_continue` (mutating action) → next LLM step fires when the fresh summary lands (`_maybe_continue`, capped `MAX_CHAIN`). Stale-summary watchdog (`CONTINUE_FALLBACK_S=3.5`): if no summary lands (a DOM change the digest can't see, e.g. an option toggle), fire the continue anyway with a `[Browser context]` re-check note (`_stale_note`) so the chain can't starve. `_action_flags` renders `[checked]`/`[unchecked]` (selection state) alongside `[filled]`/`[sensitive]`; long `— options: …` lists truncate with `…`.
209
+
210
+ ### `utils/flow_tools/browser_actions.py`
211
+ - The LLM-callable hands: 9 handlers (`read_dom`, `find_elements`, `read_region`, `click_element`, `type_text`, `select_option`, `scroll_page`, `highlight_element`, `execute_plan`) + their `FlowsFunctionSchema`s in `BROWSER_ACTION_SCHEMAS`. Targeting: `cp` handle is the advertised, preferred key (`ref` deliberately NOT advertised — one handle namespace; strays still forwarded); `selector`/`text`/`role` fallbacks. Mutating schemas take an optional `say` ack.
212
+ - `_run_step(flow_manager, action, params) -> (ok, body)` — the shared guardrails, in order: bridge present (`state["livekit_rpc"]`) → kill-switch `copilot_config.disabled` → for `MUTATING_ACTIONS`, `_origin_allowed(injector._url, cfg.allowed_origins)` (empty allowlist = allow all) → confirm policy → `bridge.send_dom_action`. `_run(...)` is the single-action wrapper (adds `say`, suppression, `arm_continue`).
213
+ - `execute_plan({steps, say})` — one LLM call runs an ordered batch of steps on the CURRENT view via `_run_step` each (`MAX_PLAN_STEPS=8`, `PLAN_BATCH_BUDGET_S=60`), stops at the first failure/denial, then issues one `read_dom` and folds the fresh page into `[Browser context]` (`injector.absorb(CT_DOM_SUMMARY, …)`), returning ActionResult-v1-shaped `results` + a compact `page` digest. NO suppression sentinel / no `arm_continue` → the follow-up LLM runs and plans the next batch off the real page. `disarm_continue()`s at start (a batch supersedes the passive chain). Confirm-eligible steps use the blocking confirm bar mid-batch.
214
+ - Confirm policy: `_CONFIRM_EXEMPT_ACTIONS = {click, scroll, select}` ALWAYS auto-run (product decision), even destructive-named, even under `high_risk_confirm`. Only `type` can confirm: `require_confirm=True` when `high_risk_confirm` OR `_is_destructive`.
215
+ - `_is_destructive`: `_target_name` from `target.text`, else the cp handle looked up in the injector's live summary (actions/content/form fields); matched against the `_DESTRUCTIVE` lexicon (submit/pay/delete/cancel subscription/publish/…); fail-open when no name resolves (destructive intents are named explicitly).
216
+ - Act-then-wait: successful MUTATING actions return `_suppress_followup_llm: True` (flows manager → `run_llm=False`; single-node adapter translates) — the fresh `[Browser context]` arrives async, so an immediate completion could only act on the STALE page. The optional `say` ack is spoken out-of-band via `_speak` (scrubbed; `state["_speak"]` shim or `task.queue_frame(TTSSpeakFrame)`) so the bot isn't silent. Failures never suppress.
217
+ - Single-node adapter: `resolve_copilot_config(call_config)` (env fallbacks `COPILOT_ALLOWED_ORIGINS` / `COPILOT_HIGH_RISK_CONFIRM` / `COPILOT_DISABLED`); `register_single_node_browser_tools(...)` registers the same 9 schemas on bot.py when `copilot_config.enabled`, wrapping each flows handler in a `SimpleNamespace(state=…)` shim — same `_run` guardrails, zero duplication; suppression becomes `FunctionCallResultProperties(run_llm=False)`.
218
+
219
+ ### bot wiring (copilot-relevant only)
220
+ - `bot_with_flows.py` — LiveKit channel, `on_connected`: builds `DomContextInjector` (+`set_flow_manager`), resolves `copilot_cfg` from `call_config.copilot_config` with the same env fallbacks, then `LiveKitRPCBridge(room, task, …, copilot_sink=dom_injector.absorb)` + `register_handlers()`; stores `dom_injector` / `copilot_config` / `livekit_rpc` in `flow_manager.state` (read by `_run`). Tools arrive pre-injected in the flow nodes (backend's `enable_browser_copilot_tools_in_flow`) and resolve to the `BROWSER_ACTION_SCHEMAS` handlers. Text mode: `transcript_handler.on_message_added = _relay_chat_reply` → `bridge.send_chat_text` (the widget's chat rendering path).
221
+ - `bot.py` — single-node: `copilot_state_ref: dict` created before tool build; `register_single_node_browser_tools(call_config=…, state_ref=copilot_state_ref, …)` at tool-registration time (LiveKit channel only); `on_connected` fills the ref with `livekit_rpc` + `dom_injector` (`DomContextInjector.set_context_aggregator(context_aggregator.user())`) — handlers read them lazily through the shim.
222
+
223
+ ### tests
224
+ - `tests/test_browser_action_suppression.py` — act-then-wait (mutating success suppresses; reads/highlight/failures never), kill-switch, origin allowlist, click/scroll/select NEVER confirm, destructive `type` confirms by default, `high_risk_confirm` forwarding, `scrub_handles` + say-ack scrubbing, destructive-name lookup via the injector summary, single-node registration (9 tools; skipped when disabled) and adapter `run_llm` translation.
225
+ - `tests/test_execute_plan.py` — ordered batch runs then re-perceives, stop-on-failure / denied-confirm / invalid-step, per-step origin & confirm guardrails, kill-switch skips read_dom, `MAX_PLAN_STEPS` truncation, one scrubbed `say`, read_dom-failure `page_error`, single-node adapter runs the LLM (no suppression), `_step_to_action` mapping.
226
+ - `tests/test_dom_context_injector.py` — legacy summary renders golden-identical, v2 flags/content/focused/viewport, dropdown options line + long-list truncation, `[checked]`/`[unchecked]` state, hostile `value` key never rendered, content/message caps, activity line, auto-continue chain + stale-summary watchdog (fires on no-summary, cancelled by summary/disarm, one-shot, respects `MAX_CHAIN`).
227
+
228
+ ## E. Ops
229
+
230
+ ### `browser_copilot/scripts/dev-stack.sh`
231
+ - One-shot local stack, `up|down|status|logs [svc]` — 4 services: **backend** (new_calling_agent_backend, :8082, `uv run python main.py` against the dockerized deps), **runtime** (ringg-bot via `ringg-bot/.venv/bin/python server.py`, :8765), **ngrok** (tunnels :8765 to the reserved `PIPECAT_URL` domain so the backend reaches the local bot), **frontend** (the host page with the embedded widget: builds the agents-cdn bundle from dv-frontend-v2, syncs it via `sync:widget`, serves on :3000).
232
+ - Preflight: branch matrix warning (the 5 expected branches), Docker/ngrok present, Krisp model is real (not an LFS pointer stub — deaf-bot trap), backend `WEBCALL_LIVEKIT_VIA_BOT=1`, widget/app env vars. Logs + pids in `/tmp/copilot-stack`.
233
+
234
+ ### `fe-ringg-v2-app/package.json` → `sync:widget`
235
+ - `rm -rf public/ringg-widget && mkdir -p … && cp -r ../dv-frontend-v2/apps/agents-cdn/dist/. public/ringg-widget/` — copies the built widget bundle into the app's public assets, where `RinggWidgetEmbed` loads it from.
236
+
237
+ ## Trace 1 — voice: "click Subscribe"
238
+
239
+ 1. Mic → LiveKit room (widget joined as `user` via `App.tsx handleCallStart`; bot as `agent`) → pipecat STT transcribes the utterance.
240
+ 2. The LLM context already holds one `[Browser context]` message (from `dom_context_injector.py:_sync_to_context`) listing `[cp:e7] button "Subscribe"`. The LLM calls `click_element({cp:"e7", say:"Done — subscribed."})`.
241
+ 3. `browser_actions.py:click_element` → `_run(ACTION_CLICK, {target:{cp:"e7"}}, say)`: bridge present; kill-switch off; click is mutating → origin check `_origin_allowed(injector._url, copilot_config.allowed_origins)`; click ∈ `_CONFIRM_EXEMPT_ACTIONS` → never `require_confirm`.
242
+ 4. `livekit_rpc_bridge.py:send_dom_action("click", …)` → `perform_rpc(destination="user", method="cp_action", timeout=20s)`, latency-timed.
243
+ 5. Widget: the `use-copilot.ts` `cp_action` handler parses the `CpAction`; no `require_confirm` → straight to `executor.ts:runCpAction`.
244
+ 6. `executor.ts:normalizeCp` → `resolveTarget` → `perception.ts:resolveById("e7")` → op `click` → `highlight(el)` + `el.click()` → handler returns `JSON.stringify({ok:true, result:null, error:null})` as the RPC response (and logs "Co-pilot: click" via `onLog`).
245
+ 7. Bot: `send_dom_action` parses `{ok:true}` → `_run` success path: `_speak(flow_manager, "Done — subscribed.")` (through `copilot_contract.py:scrub_handles`, as a `TTSSpeakFrame` / single-node `params.llm.push_frame`) then returns the result with `_suppress_followup_llm: True` → NO follow-up LLM completion. The turn ends (act-then-wait).
246
+ 8. The click changed the page → widget `MutationObserver` fires → `schedule()` (400ms debounce) → `cheapHash()` differs → `pushSummary()` → `receive_dynamic_data(CT_DOM_SUMMARY)` → `LiveKitRPCBridge._on_receive_dynamic_data` → `DomContextInjector.absorb` → the ONE `[Browser context]` message is replaced. The user's next turn reasons over the FRESH page.
247
+
248
+ ## Trace 2 — the page changes
249
+
250
+ 1. The host app re-renders (SPA navigation, dialog opens, row added) → `use-copilot.ts` MutationObserver callback; records caused by our own `data-cp` tagging or inside `[data-copilot-ui]` are skipped → `schedule()` (400ms debounce). Scroll/resize (500ms throttle) and `popstate`/`hashchange` feed the same `schedule()`.
251
+ 2. `notify()` → `cheapHash()` (runs `perception.ts:collectPageState`) = `route_key|count|firstId|lastId|focused|scroll_y`; identical to the last push → dropped; different → `pushSummary()`.
252
+ 3. `pushSummary()` → `perception.ts:buildDomSummary()` (fresh tagging walk, new dense `e*/t*` ids). If `summary.url` differs from the last push it first RPCs `CT_PAGE_CHANGE {url,title}`; then `performRpc("receive_dynamic_data", {component_type:"receive_dom_summary", component_data: summary})` to the agent identity.
253
+ 4. Bot: `livekit_rpc_bridge.py:_on_receive_dynamic_data` → component_type ∈ `COPILOT_INBOUND_TYPES` → `copilot_sink` (NO user turn injected — ambient observation) → returns `"ok"`.
254
+ 5. `dom_context_injector.py:absorb(CT_DOM_SUMMARY)` stores the summary (a preceding `CT_PAGE_CHANGE` had dropped the stale one) → `_sync_to_context()` → `_build_message_text()` (`[Browser context]` + URL/Title + `_render_summary()` + activity line, ≤12k chars) → replaces the single copilot message in the shared `OpenAILLMContext`. The next LLM completion sees the new page.