dv-pipecat-flows 0.0.22.dev1489__tar.gz → 0.0.22.dev1491__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.
- {dv_pipecat_flows-0.0.22.dev1489/src/dv_pipecat_flows.egg-info → dv_pipecat_flows-0.0.22.dev1491}/PKG-INFO +1 -1
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491/src/dv_pipecat_flows.egg-info}/PKG-INFO +1 -1
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/src/pipecat_flows/manager.py +204 -56
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/.agents/skills/loki-logs/SKILL.md +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/.agents/skills/loki-logs/query-reference.md +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/.claude/skills/loki-logs/SKILL.md +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/.claude/skills/loki-logs/query-reference.md +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/.gitattributes +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/.gitignore +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/.pre-commit-config.yaml +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/.python-version +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/.readthedocs.yaml +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/AGENTS.md +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/CHANGELOG.md +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/CLAUDE.md +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/CONTRIBUTING.md +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/LICENSE +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/MANIFEST.in +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/README.md +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/dev-requirements.txt +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/docker-compose.dev.yml +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/.eslintrc.json +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/.prettierrc +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/css/tailwind.css +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/examples/food_ordering.json +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/examples/movie_explorer.json +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/examples/patient_intake.json +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/examples/restaurant_reservation.json +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/examples/travel_planner.json +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/favicon.png +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/favicon.svg +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/index.html +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/editor/canvas.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/editor/editorState.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/editor/sidePanel.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/editor/toolbar.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/main.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/nodes/baseNode.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/nodes/endNode.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/nodes/flowNode.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/nodes/functionNode.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/nodes/index.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/nodes/mergeNode.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/nodes/startNode.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/types.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/utils/export.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/utils/helpers.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/utils/import.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/utils/validation.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/jsdoc.json +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/package-lock.json +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/package.json +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/postcss.config.cjs +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/public/favicon.png +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/public/favicon.svg +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/tailwind.config.cjs +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/vercel.json +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/vite.config.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/engine_primitives_plan.md +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/env.example +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/images/food-ordering-flow.png +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/pipecat-flows.png +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/pipecat_upgrade.md +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/pyproject.toml +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/remote-asterisk-code/README.md +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/remote-asterisk-code/extensions.conf +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/remote-asterisk-code/rtp.conf +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/requirements.txt +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/scripts/check-pypi-package.py +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/scripts/fix-ruff.sh +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/scripts/pre-commit.sh +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/setup.cfg +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/src/dv_pipecat_flows.egg-info/SOURCES.txt +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/src/dv_pipecat_flows.egg-info/dependency_links.txt +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/src/dv_pipecat_flows.egg-info/requires.txt +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/src/dv_pipecat_flows.egg-info/top_level.txt +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/src/pipecat_flows/__init__.py +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/src/pipecat_flows/actions.py +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/src/pipecat_flows/adapters.py +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/src/pipecat_flows/condition_evaluator.py +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/src/pipecat_flows/exceptions.py +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/src/pipecat_flows/flow_validator.py +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/src/pipecat_flows/processors/__init__.py +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/src/pipecat_flows/processors/router_mode_guard.py +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/src/pipecat_flows/processors/speak_interruption_guard.py +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/src/pipecat_flows/processors/user_turn_observer.py +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/src/pipecat_flows/router_mode.py +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/src/pipecat_flows/types.py +0 -0
- {dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/uv.lock +0 -0
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/src/pipecat_flows/manager.py
RENAMED
|
@@ -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 normalize_collect_value
|
|
51
|
+
from utils.collect_normalizers import is_repeat_request, normalize_collect_value
|
|
52
52
|
from utils.collect_prompts import collect_prompt
|
|
53
53
|
from pipecat.frames.frames import (
|
|
54
54
|
DTMFUpdateSettingsFrame,
|
|
@@ -2149,6 +2149,8 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
2149
2149
|
system_prompt: Optional[str] = None,
|
|
2150
2150
|
context_prefix: Optional[List[Dict[str, Any]]] = None,
|
|
2151
2151
|
enum_values: Optional[List[str]] = None,
|
|
2152
|
+
since_ts: Optional[float] = None,
|
|
2153
|
+
with_reply: bool = False,
|
|
2152
2154
|
) -> Optional[str]:
|
|
2153
2155
|
"""Wait for the caller's spoken reply and extract a single value from it
|
|
2154
2156
|
via a constrained LLM call — the same primitive the LLM router uses
|
|
@@ -2158,9 +2160,12 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
2158
2160
|
``context_prefix`` lets a caller prepend grounding messages (e.g. the
|
|
2159
2161
|
read-back question the confirm is about) ahead of the caller's latest
|
|
2160
2162
|
turn, and ``system_prompt`` overrides the default extraction instruction.
|
|
2163
|
+
``since_ts`` reads a reply that ALREADY committed (e.g. the caller's
|
|
2164
|
+
rejection of a read-back) instead of waiting for a new turn — pass it
|
|
2165
|
+
with ``timeout_s=0`` to re-extract a correction from the denial.
|
|
2161
2166
|
"""
|
|
2162
2167
|
observer = self._user_turn_observer
|
|
2163
|
-
entry_ts = time.monotonic()
|
|
2168
|
+
entry_ts = since_ts if since_ts is not None else time.monotonic()
|
|
2164
2169
|
if observer is not None and timeout_s > 0:
|
|
2165
2170
|
# Wait for the caller's reply to COMMIT, and stay PATIENT while they're
|
|
2166
2171
|
# still mid-utterance, so we read their finished answer (e.g. a
|
|
@@ -2186,7 +2191,24 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
2186
2191
|
value_spec: Dict[str, Any] = {"type": "string", "description": description}
|
|
2187
2192
|
if enum_values:
|
|
2188
2193
|
value_spec = {"type": "enum", "values": list(enum_values), "description": description}
|
|
2189
|
-
|
|
2194
|
+
extract_spec: Dict[str, Any] = {"value": value_spec}
|
|
2195
|
+
if with_reply:
|
|
2196
|
+
# Same hybrid contract as the digit extractor: when the caller says
|
|
2197
|
+
# something conversational instead of the value, the model answers it
|
|
2198
|
+
# on-topic in `reply` (we speak that) and leaves `value` empty. The field
|
|
2199
|
+
# description below is the instruction — the system prompt stays untouched.
|
|
2200
|
+
self._collect_reply = ""
|
|
2201
|
+
extract_spec["reply"] = {
|
|
2202
|
+
"type": "string",
|
|
2203
|
+
"description": (
|
|
2204
|
+
"Empty whenever the caller gave the value. ONLY when they gave no "
|
|
2205
|
+
"value and instead said something conversational (a question, a "
|
|
2206
|
+
"hesitation, an objection), a SHORT reply in their language that "
|
|
2207
|
+
"answers in ONE sentence and then asks again for it. Stay on topic; "
|
|
2208
|
+
"for anything unrelated give a one-line redirect; make no promises."
|
|
2209
|
+
),
|
|
2210
|
+
}
|
|
2211
|
+
schema = build_router_response_schema([], extract_spec)
|
|
2190
2212
|
|
|
2191
2213
|
deadline = entry_ts + timeout_s if timeout_s > 0 else entry_ts
|
|
2192
2214
|
last_value: Optional[str] = None
|
|
@@ -2244,6 +2266,9 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
2244
2266
|
value = (extracted or {}).get("value")
|
|
2245
2267
|
if isinstance(value, str) and value.strip():
|
|
2246
2268
|
last_value = value
|
|
2269
|
+
if with_reply:
|
|
2270
|
+
reply_val = (extracted or {}).get("reply")
|
|
2271
|
+
self._collect_reply = reply_val.strip() if isinstance(reply_val, str) else ""
|
|
2247
2272
|
# No observer (unit tests) → single shot. Otherwise re-extract only if
|
|
2248
2273
|
# the caller said MORE during the call or is still speaking, and time
|
|
2249
2274
|
# remains.
|
|
@@ -2362,6 +2387,7 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
2362
2387
|
# can never trap the call in the node (the activity-aware wait below would
|
|
2363
2388
|
# otherwise reset forever).
|
|
2364
2389
|
_MAX_CAPTURE_SECONDS = 90.0
|
|
2390
|
+
_MAX_COLLECT_REPLIES = 5 # conversational (non-digit) LLM replies before falling back
|
|
2365
2391
|
|
|
2366
2392
|
def _caller_is_speaking(self) -> bool:
|
|
2367
2393
|
"""True while the caller has an IN-PROGRESS utterance — they started more
|
|
@@ -2410,6 +2436,8 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
2410
2436
|
Returns digits 0-9 only, or "" if no digits / on error (degrades to a
|
|
2411
2437
|
re-ask).
|
|
2412
2438
|
"""
|
|
2439
|
+
self._collect_readback = "" # grouped, language-matched read-back for the confirm
|
|
2440
|
+
self._collect_reply = "" # LLM free reply for a conversational (non-digit) turn
|
|
2413
2441
|
context_messages = [
|
|
2414
2442
|
{"role": "user", "content": turn}
|
|
2415
2443
|
for turn in turns
|
|
@@ -2422,39 +2450,67 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
2422
2450
|
llm_service = llm_service.active_llm
|
|
2423
2451
|
schema = build_router_response_schema(
|
|
2424
2452
|
[],
|
|
2425
|
-
{
|
|
2426
|
-
"
|
|
2427
|
-
|
|
2428
|
-
|
|
2453
|
+
{
|
|
2454
|
+
"value": {
|
|
2455
|
+
"type": "string",
|
|
2456
|
+
"description": (
|
|
2457
|
+
f"The {what} as digits 0-9 only, in the order spoken (no spaces or "
|
|
2458
|
+
"punctuation). Join each spoken number-word left to right — e.g. "
|
|
2459
|
+
"'double seven' → '77', 'nine two triple two' → 9,2,222 → '92222', "
|
|
2460
|
+
"'eight one three zero' → '8130'. Return an empty string if the "
|
|
2461
|
+
"caller has not given any digits yet (e.g. only said 'hello') — "
|
|
2462
|
+
"never invent or guess digits."
|
|
2463
|
+
),
|
|
2464
|
+
},
|
|
2465
|
+
"readback": {
|
|
2466
|
+
"type": "string",
|
|
2467
|
+
"description": (
|
|
2468
|
+
"A read-back of `value` for confirmation, in the caller's language, "
|
|
2469
|
+
"with repeated digits grouped — e.g. 8555 → 'eight triple five', "
|
|
2470
|
+
"77 → 'double seven'. Empty when `value` is empty."
|
|
2471
|
+
),
|
|
2472
|
+
},
|
|
2473
|
+
"reply": {
|
|
2474
|
+
"type": "string",
|
|
2475
|
+
"description": (
|
|
2476
|
+
"Empty whenever the caller gave digits. ONLY when they gave no "
|
|
2477
|
+
"digits and instead said something conversational (a question, a "
|
|
2478
|
+
"hesitation, an objection), a SHORT reply in their language that "
|
|
2479
|
+
"answers in ONE sentence and then asks again for the number. Stay "
|
|
2480
|
+
"on topic; for anything unrelated give a one-line redirect back to "
|
|
2481
|
+
"the number; make no promises or commitments."
|
|
2482
|
+
),
|
|
2483
|
+
},
|
|
2484
|
+
},
|
|
2429
2485
|
)
|
|
2430
2486
|
try:
|
|
2431
2487
|
raw = await self._timed_router_completion(
|
|
2432
2488
|
node_id,
|
|
2433
2489
|
llm_service=llm_service,
|
|
2434
2490
|
system_prompt=(
|
|
2435
|
-
f"The caller is
|
|
2436
|
-
"
|
|
2437
|
-
"
|
|
2438
|
-
|
|
2439
|
-
"
|
|
2440
|
-
"
|
|
2441
|
-
"
|
|
2442
|
-
"
|
|
2443
|
-
"
|
|
2444
|
-
"
|
|
2445
|
-
"
|
|
2446
|
-
"
|
|
2447
|
-
"
|
|
2448
|
-
"
|
|
2449
|
-
"
|
|
2450
|
-
"
|
|
2451
|
-
"
|
|
2452
|
-
"
|
|
2453
|
-
"
|
|
2454
|
-
"
|
|
2455
|
-
"'70823 41303' then '
|
|
2456
|
-
"'
|
|
2457
|
-
"
|
|
2491
|
+
f"The caller is giving you a {what} — by voice in ANY language, or by "
|
|
2492
|
+
"keypad (a message like 'User inputted: 5') — possibly across several "
|
|
2493
|
+
"messages. Read everything they have said and return the final number "
|
|
2494
|
+
"in `value` and `readback`, or leave both empty if no digits have been "
|
|
2495
|
+
"given yet.\n"
|
|
2496
|
+
"If the caller instead said something conversational (a question, a "
|
|
2497
|
+
"hesitation, an objection) and gave NO digits, leave value/readback "
|
|
2498
|
+
"empty and put a SHORT on-topic reply in `reply`: answer in one "
|
|
2499
|
+
"sentence, then ask again for the number — stay on topic, make no "
|
|
2500
|
+
"promises.\n"
|
|
2501
|
+
"Digits: read each spoken number-word in the order spoken and join its "
|
|
2502
|
+
"digits left to right — a digit-word adds one digit, 'double X' adds "
|
|
2503
|
+
"XX, 'triple X' adds XXX (in any language, incl. डबल/ट्रिपल), and a lone "
|
|
2504
|
+
"digit next to a double/triple word stays separate.\n"
|
|
2505
|
+
"Corrections: if the caller fixes or restates any part — 'no, the "
|
|
2506
|
+
"first/last part is X', 'the start/beginning is X' (शुरू/स्टार्टिंग), "
|
|
2507
|
+
"'the end is X' (एंड/लास्ट), 'where I said Y it's Z', or they repeat the "
|
|
2508
|
+
"whole number — apply it and output the single final number, keeping "
|
|
2509
|
+
"the un-corrected digits unchanged. When the position is vague, find "
|
|
2510
|
+
"the run of digits being replaced and swap only that run. Examples: "
|
|
2511
|
+
"'70823 41303' then 'no, the first part is 807' → '8072341303'; then "
|
|
2512
|
+
"'एंड में 5 है' → '7082341305'; then 'where I said 41 it's 14' → "
|
|
2513
|
+
"'7082314303'."
|
|
2458
2514
|
),
|
|
2459
2515
|
response_schema=schema,
|
|
2460
2516
|
context_messages=context_messages,
|
|
@@ -2467,6 +2523,10 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
2467
2523
|
return ""
|
|
2468
2524
|
_branch, extracted = parse_router_response(raw, [UNCLEAR_BRANCH_ID])
|
|
2469
2525
|
value = (extracted or {}).get("value")
|
|
2526
|
+
readback = (extracted or {}).get("readback")
|
|
2527
|
+
reply = (extracted or {}).get("reply")
|
|
2528
|
+
self._collect_readback = readback.strip() if isinstance(readback, str) else ""
|
|
2529
|
+
self._collect_reply = reply.strip() if isinstance(reply, str) else ""
|
|
2470
2530
|
return re.sub(r"\D", "", value) if isinstance(value, str) else ""
|
|
2471
2531
|
|
|
2472
2532
|
async def _reextract_if_corrected(
|
|
@@ -2582,6 +2642,7 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
2582
2642
|
buffer = ""
|
|
2583
2643
|
echoed = "" # bare digits already spoken back, so we echo only the NEW ones
|
|
2584
2644
|
misses = 0
|
|
2645
|
+
replies = 0 # conversational (non-digit) replies, bounded so chatter can't loop
|
|
2585
2646
|
corrections = 0 # confirm "no" rounds, bounded so a correction can't loop
|
|
2586
2647
|
deadline = time.monotonic() + self._MAX_CAPTURE_SECONDS
|
|
2587
2648
|
|
|
@@ -2631,7 +2692,9 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
2631
2692
|
# the ORIGINAL number. Only a plain rejection with no new digits
|
|
2632
2693
|
# falls through to the re-ask. Bounded by ``corrections``.
|
|
2633
2694
|
while True:
|
|
2634
|
-
confirmed = await self._confirm_collected(
|
|
2695
|
+
confirmed = await self._confirm_collected(
|
|
2696
|
+
node_id, getattr(self, "_collect_readback", "") or " ".join(buffer)
|
|
2697
|
+
)
|
|
2635
2698
|
if confirmed is True:
|
|
2636
2699
|
return normalized
|
|
2637
2700
|
corrections += 1
|
|
@@ -2666,12 +2729,33 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
2666
2729
|
continue
|
|
2667
2730
|
|
|
2668
2731
|
if buffer == previous:
|
|
2669
|
-
# A turn arrived but the number is UNCHANGED (filler / mis-hear
|
|
2670
|
-
#
|
|
2671
|
-
#
|
|
2672
|
-
#
|
|
2673
|
-
#
|
|
2674
|
-
#
|
|
2732
|
+
# A turn arrived but the number is UNCHANGED (filler / mis-hear / a
|
|
2733
|
+
# request to repeat). If the caller asked to hear it back, RE-READ the
|
|
2734
|
+
# digits-so-far (no miss); otherwise re-ask and count a miss so garbage
|
|
2735
|
+
# can't loop forever. Compare by VALUE, not length: a same-length
|
|
2736
|
+
# correction during the echo phase ("no, the 3rd is 5": 7765 → 7755)
|
|
2737
|
+
# changes the value but not the length, so a length check would mis-read
|
|
2738
|
+
# the fix as "no progress" and burn a miss instead of echoing it below.
|
|
2739
|
+
turns = self._collect_turns_since(start_ts)
|
|
2740
|
+
if buffer and turns and is_repeat_request(turns[-1]):
|
|
2741
|
+
# Re-reads bypass the miss counter, so the absolute deadline is the
|
|
2742
|
+
# only bound — without it a caller repeating "say it again" loops
|
|
2743
|
+
# forever (a turn lands every cycle, never silence, never a miss).
|
|
2744
|
+
if time.monotonic() >= deadline:
|
|
2745
|
+
break
|
|
2746
|
+
await self._task.queue_frame(TTSSpeakFrame(
|
|
2747
|
+
text=collect_prompt(language, "echo", digits=" ".join(buffer))
|
|
2748
|
+
))
|
|
2749
|
+
continue
|
|
2750
|
+
reply = getattr(self, "_collect_reply", "")
|
|
2751
|
+
if reply and replies < self._MAX_COLLECT_REPLIES and time.monotonic() < deadline:
|
|
2752
|
+
# The caller said something other than digits (a question / objection).
|
|
2753
|
+
# Speak the LLM's on-topic reply (it answers AND steers back to the
|
|
2754
|
+
# number) instead of the robotic nudge. Bounded + deadline-capped so it
|
|
2755
|
+
# can't crowd out give-up; a repeat request was already handled above.
|
|
2756
|
+
replies += 1
|
|
2757
|
+
await self._task.queue_frame(TTSSpeakFrame(text=reply))
|
|
2758
|
+
continue
|
|
2675
2759
|
misses += 1
|
|
2676
2760
|
if misses < max_attempts:
|
|
2677
2761
|
await _nudge(language)
|
|
@@ -2717,6 +2801,7 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
2717
2801
|
buffer = ""
|
|
2718
2802
|
echoed = "" # bare digits already spoken back, so we echo only the NEW ones
|
|
2719
2803
|
misses = 0
|
|
2804
|
+
replies = 0 # conversational (non-digit) replies, bounded so chatter can't loop
|
|
2720
2805
|
corrections = 0 # confirm "no" rounds, bounded so a correction can't loop
|
|
2721
2806
|
deadline = time.monotonic() + self._MAX_CAPTURE_SECONDS
|
|
2722
2807
|
# Keypad auto-flushes once the caller has keyed `target` digits.
|
|
@@ -2758,7 +2843,9 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
2758
2843
|
# we already have and re-reads the CORRECTED code at once; only a
|
|
2759
2844
|
# plain rejection with no new digits asks "what should it be?".
|
|
2760
2845
|
while True:
|
|
2761
|
-
confirmed = await self._confirm_collected(
|
|
2846
|
+
confirmed = await self._confirm_collected(
|
|
2847
|
+
node_id, getattr(self, "_collect_readback", "") or " ".join(normalized)
|
|
2848
|
+
)
|
|
2762
2849
|
if confirmed is True:
|
|
2763
2850
|
return normalized
|
|
2764
2851
|
corrections += 1
|
|
@@ -2793,9 +2880,30 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
2793
2880
|
continue
|
|
2794
2881
|
|
|
2795
2882
|
if buffer == previous:
|
|
2796
|
-
# Unchanged (filler / mis-hear
|
|
2797
|
-
#
|
|
2798
|
-
#
|
|
2883
|
+
# Unchanged (filler / mis-hear / a request to repeat). If the caller
|
|
2884
|
+
# asked to hear it back, RE-READ the digits-so-far (no miss); else re-ask
|
|
2885
|
+
# and count a miss. Compare by VALUE not length so a same-length
|
|
2886
|
+
# correction during the echo phase counts as progress (echoed below).
|
|
2887
|
+
turns = self._collect_turns_since(start_ts)
|
|
2888
|
+
if buffer and turns and is_repeat_request(turns[-1]):
|
|
2889
|
+
# Re-reads bypass the miss counter, so the absolute deadline is the
|
|
2890
|
+
# only bound — without it a caller repeating "say it again" loops
|
|
2891
|
+
# forever (a turn lands every cycle, never silence, never a miss).
|
|
2892
|
+
if time.monotonic() >= deadline:
|
|
2893
|
+
break
|
|
2894
|
+
await self._task.queue_frame(TTSSpeakFrame(
|
|
2895
|
+
text=collect_prompt(language, "echo", digits=" ".join(buffer))
|
|
2896
|
+
))
|
|
2897
|
+
continue
|
|
2898
|
+
reply = getattr(self, "_collect_reply", "")
|
|
2899
|
+
if reply and replies < self._MAX_COLLECT_REPLIES and time.monotonic() < deadline:
|
|
2900
|
+
# The caller said something other than digits (a question / objection).
|
|
2901
|
+
# Speak the LLM's on-topic reply (it answers AND steers back to the
|
|
2902
|
+
# number) instead of the robotic nudge. Bounded + deadline-capped so it
|
|
2903
|
+
# can't crowd out give-up; a repeat request was already handled above.
|
|
2904
|
+
replies += 1
|
|
2905
|
+
await self._task.queue_frame(TTSSpeakFrame(text=reply))
|
|
2906
|
+
continue
|
|
2799
2907
|
misses += 1
|
|
2800
2908
|
if misses < max_attempts:
|
|
2801
2909
|
await _nudge(language)
|
|
@@ -2825,7 +2933,9 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
2825
2933
|
reply ("जी" … "हाँ, सही है") can read as unclear, and we'd rather re-ask
|
|
2826
2934
|
"is that correct?" than treat a confirming caller as a rejection."""
|
|
2827
2935
|
observer = self._user_turn_observer
|
|
2828
|
-
|
|
2936
|
+
repeats = 0
|
|
2937
|
+
asked = 0
|
|
2938
|
+
while asked < max(1, attempts):
|
|
2829
2939
|
listen_ts = time.monotonic()
|
|
2830
2940
|
await self._task.queue_frame(
|
|
2831
2941
|
TTSSpeakFrame(text=collect_prompt(self._active_language(), "confirm", readback=readback))
|
|
@@ -2862,7 +2972,16 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
2862
2972
|
if observer is not None and hasattr(observer, "transcripts_since")
|
|
2863
2973
|
else ""
|
|
2864
2974
|
)
|
|
2865
|
-
|
|
2975
|
+
has_digit = any(ch.isdigit() for ch in reply)
|
|
2976
|
+
# A request to hear it again ("can you repeat?") is NOT a yes or a no —
|
|
2977
|
+
# re-read the value and DON'T burn a confirm attempt (bounded so a caller
|
|
2978
|
+
# stuck on "repeat" can't loop forever). A digit-carrying reply is a
|
|
2979
|
+
# correction, handled just below, so only a digit-free reply is a repeat.
|
|
2980
|
+
if not has_digit and is_repeat_request(reply):
|
|
2981
|
+
repeats += 1
|
|
2982
|
+
if repeats <= 3:
|
|
2983
|
+
continue
|
|
2984
|
+
if has_digit:
|
|
2866
2985
|
return False
|
|
2867
2986
|
text = (answer or "").strip().lower()
|
|
2868
2987
|
if text.startswith("yes"):
|
|
@@ -2870,6 +2989,7 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
2870
2989
|
if text.startswith("no"):
|
|
2871
2990
|
return False
|
|
2872
2991
|
# null / unclear → re-ask "is that correct?" (next loop iteration)
|
|
2992
|
+
asked += 1
|
|
2873
2993
|
return None
|
|
2874
2994
|
|
|
2875
2995
|
async def _collect_single_value(
|
|
@@ -2889,9 +3009,11 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
2889
3009
|
description = {
|
|
2890
3010
|
"email": "the email address the caller said, as a standard address (e.g. john.smith@gmail.com)",
|
|
2891
3011
|
}.get(collect_type, "the value the caller said")
|
|
2892
|
-
|
|
3012
|
+
attempt = 0
|
|
3013
|
+
replies = 0
|
|
3014
|
+
while attempt < max_attempts:
|
|
2893
3015
|
raw_value = await self._extract_spoken_value(
|
|
2894
|
-
node_id, description, timeout_s=15.0
|
|
3016
|
+
node_id, description, timeout_s=15.0, with_reply=True
|
|
2895
3017
|
)
|
|
2896
3018
|
if not raw_value and allow_keypad:
|
|
2897
3019
|
capture_timeout = float(keypad_dtmf.get("timeout", 8.0)) + 10.0
|
|
@@ -2901,27 +3023,53 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
2901
3023
|
if raw_value and self._caller_is_speaking():
|
|
2902
3024
|
if await self._await_user_turn(timeout_s=5.0):
|
|
2903
3025
|
raw_value = await self._extract_spoken_value(
|
|
2904
|
-
node_id, description, timeout_s=15.0
|
|
3026
|
+
node_id, description, timeout_s=15.0, with_reply=True
|
|
2905
3027
|
)
|
|
2906
3028
|
normalized = normalize_collect_value(
|
|
2907
3029
|
collect_type, raw_value, country_default=country_default
|
|
2908
3030
|
)
|
|
2909
3031
|
if normalized:
|
|
2910
|
-
# Read the value back and confirm before storing (phone-parity).
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
#
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
3032
|
+
# Read the value back and confirm before storing (phone-parity). On a
|
|
3033
|
+
# denial, re-extract from the rejection reply itself ("no, it's
|
|
3034
|
+
# jane@gmail.com") and re-confirm the fix directly, instead of making
|
|
3035
|
+
# the caller repeat the whole address. A plain "no" with no new value
|
|
3036
|
+
# falls through to "what should it be?" and the next attempt.
|
|
3037
|
+
while True:
|
|
3038
|
+
confirm_ts = time.monotonic()
|
|
3039
|
+
confirmed = await self._confirm_collected(node_id, normalized)
|
|
3040
|
+
if confirmed is True:
|
|
3041
|
+
return normalized
|
|
3042
|
+
corrected = normalize_collect_value(
|
|
3043
|
+
collect_type,
|
|
3044
|
+
await self._extract_spoken_value(
|
|
3045
|
+
node_id, description, timeout_s=0.0, since_ts=confirm_ts
|
|
3046
|
+
),
|
|
3047
|
+
country_default=country_default,
|
|
2918
3048
|
)
|
|
3049
|
+
if corrected and corrected != normalized:
|
|
3050
|
+
normalized = corrected
|
|
3051
|
+
continue # re-read the corrected value for confirmation
|
|
3052
|
+
if attempt < max_attempts - 1:
|
|
3053
|
+
await self._task.queue_frame(
|
|
3054
|
+
TTSSpeakFrame(text=collect_prompt(self._active_language(), "restart_denied"))
|
|
3055
|
+
)
|
|
3056
|
+
break
|
|
3057
|
+
attempt += 1
|
|
3058
|
+
continue
|
|
3059
|
+
# No valid value. If the caller said something conversational instead, speak
|
|
3060
|
+
# the LLM's on-topic reply (it answers AND steers back) WITHOUT burning an
|
|
3061
|
+
# attempt (bounded). Otherwise count the attempt and re-ask.
|
|
3062
|
+
reply = getattr(self, "_collect_reply", "")
|
|
3063
|
+
if reply and replies < self._MAX_COLLECT_REPLIES:
|
|
3064
|
+
replies += 1
|
|
3065
|
+
await self._task.queue_frame(TTSSpeakFrame(text=reply))
|
|
2919
3066
|
continue
|
|
3067
|
+
attempt += 1
|
|
2920
3068
|
logger.info(
|
|
2921
3069
|
f"Collect '{node_id}': no valid {collect_type} "
|
|
2922
|
-
f"(attempt {attempt
|
|
3070
|
+
f"(attempt {attempt}/{max_attempts})"
|
|
2923
3071
|
)
|
|
2924
|
-
if reask and attempt < max_attempts
|
|
3072
|
+
if reask and attempt < max_attempts:
|
|
2925
3073
|
await self._task.queue_frame(TTSSpeakFrame(text=reask))
|
|
2926
3074
|
return None
|
|
2927
3075
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/editor/canvas.js
RENAMED
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/editor/editorState.js
RENAMED
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/editor/sidePanel.js
RENAMED
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/editor/toolbar.js
RENAMED
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/nodes/baseNode.js
RENAMED
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/nodes/endNode.js
RENAMED
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/nodes/flowNode.js
RENAMED
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/nodes/functionNode.js
RENAMED
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/nodes/index.js
RENAMED
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/nodes/mergeNode.js
RENAMED
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/nodes/startNode.js
RENAMED
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/utils/export.js
RENAMED
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/utils/helpers.js
RENAMED
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/utils/import.js
RENAMED
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/js/utils/validation.js
RENAMED
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/package-lock.json
RENAMED
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/postcss.config.cjs
RENAMED
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/public/favicon.png
RENAMED
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/public/favicon.svg
RENAMED
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/editor/tailwind.config.cjs
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/engine_primitives_plan.md
RENAMED
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/images/food-ordering-flow.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/remote-asterisk-code/README.md
RENAMED
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/remote-asterisk-code/rtp.conf
RENAMED
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/scripts/check-pypi-package.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/src/pipecat_flows/__init__.py
RENAMED
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/src/pipecat_flows/actions.py
RENAMED
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/src/pipecat_flows/adapters.py
RENAMED
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/src/pipecat_flows/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/src/pipecat_flows/router_mode.py
RENAMED
|
File without changes
|
{dv_pipecat_flows-0.0.22.dev1489 → dv_pipecat_flows-0.0.22.dev1491}/src/pipecat_flows/types.py
RENAMED
|
File without changes
|
|
File without changes
|