dv-pipecat-flows 0.0.22.dev2000__tar.gz → 0.0.22.dev2100__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.dev2000/src/dv_pipecat_flows.egg-info → dv_pipecat_flows-0.0.22.dev2100}/PKG-INFO +1 -1
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/engine_primitives_plan.md +1 -1
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100/src/dv_pipecat_flows.egg-info}/PKG-INFO +1 -1
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/src/pipecat_flows/actions.py +23 -1
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/src/pipecat_flows/flow_validator.py +8 -2
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/src/pipecat_flows/manager.py +117 -20
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/uv.lock +109 -1185
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/.agents/skills/loki-logs/SKILL.md +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/.agents/skills/loki-logs/query-reference.md +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/.claude/skills/loki-logs/SKILL.md +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/.claude/skills/loki-logs/query-reference.md +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/.gitattributes +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/.gitignore +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/.pre-commit-config.yaml +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/.python-version +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/.readthedocs.yaml +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/AGENTS.md +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/CHANGELOG.md +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/CLAUDE.md +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/CONTRIBUTING.md +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/LICENSE +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/MANIFEST.in +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/README.md +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/dev-requirements.txt +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/docker-compose.dev.yml +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/.eslintrc.json +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/.prettierrc +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/css/tailwind.css +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/examples/food_ordering.json +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/examples/movie_explorer.json +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/examples/patient_intake.json +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/examples/restaurant_reservation.json +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/examples/travel_planner.json +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/favicon.png +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/favicon.svg +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/index.html +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/js/editor/canvas.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/js/editor/editorState.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/js/editor/sidePanel.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/js/editor/toolbar.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/js/main.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/js/nodes/baseNode.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/js/nodes/endNode.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/js/nodes/flowNode.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/js/nodes/functionNode.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/js/nodes/index.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/js/nodes/mergeNode.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/js/nodes/startNode.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/js/types.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/js/utils/export.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/js/utils/helpers.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/js/utils/import.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/js/utils/validation.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/jsdoc.json +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/package-lock.json +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/package.json +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/postcss.config.cjs +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/public/favicon.png +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/public/favicon.svg +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/tailwind.config.cjs +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/vercel.json +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/editor/vite.config.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/env.example +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/images/food-ordering-flow.png +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/pipecat-flows.png +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/pipecat_upgrade.md +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/pyproject.toml +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/remote-asterisk-code/README.md +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/remote-asterisk-code/extensions.conf +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/remote-asterisk-code/rtp.conf +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/requirements.txt +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/scripts/check-pypi-package.py +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/scripts/fix-ruff.sh +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/scripts/pre-commit.sh +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/setup.cfg +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/src/dv_pipecat_flows.egg-info/SOURCES.txt +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/src/dv_pipecat_flows.egg-info/dependency_links.txt +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/src/dv_pipecat_flows.egg-info/requires.txt +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/src/dv_pipecat_flows.egg-info/top_level.txt +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/src/pipecat_flows/__init__.py +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/src/pipecat_flows/adapters.py +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/src/pipecat_flows/condition_evaluator.py +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/src/pipecat_flows/exceptions.py +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/src/pipecat_flows/processors/__init__.py +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/src/pipecat_flows/processors/router_mode_guard.py +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/src/pipecat_flows/processors/speak_interruption_guard.py +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/src/pipecat_flows/processors/user_turn_observer.py +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/src/pipecat_flows/router_mode.py +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/src/pipecat_flows/types.py +0 -0
{dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/engine_primitives_plan.md
RENAMED
|
@@ -48,7 +48,7 @@ Phase 1 of `../new_calling_agent_backend/deterministic_agents_plan.md` §6 maps
|
|
|
48
48
|
## 4. New files
|
|
49
49
|
|
|
50
50
|
- `src/pipecat_flows/condition_evaluator.py` — pure-function rule evaluator + variable resolver (no I/O, no manager refs).
|
|
51
|
-
- `src/pipecat_flows/flow_validator.py` — static graph validator invoked once
|
|
51
|
+
- `src/pipecat_flows/flow_validator.py` — static graph validator (skeleton; rule bodies + wiring land in PR-2/3/4). Intended to be invoked once from `FlowManager.initialize()`; will enforce primitive-combination rules and reserved keys. Not yet called at runtime.
|
|
52
52
|
- `src/pipecat_flows/router_mode.py` — schema builder, prompt builder, response parser, unclear-loop bookkeeping (keeps `manager.py` from ballooning further).
|
|
53
53
|
- `src/pipecat_flows/processors/router_mode_guard.py` — `FrameProcessor` subclass dropping `LLMTextFrame` etc. when `current_node_config.suppress_llm_text` is true. Owned by flows because the policy is per-node.
|
|
54
54
|
- `tests/conftest_primitives.py` — shared fixtures: `make_flow_manager(nodes, state=…, llm=FakeLLM())`, fake `LLMSwitcher`, fake adapter, frame-queue sink.
|
{dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/src/pipecat_flows/actions.py
RENAMED
|
@@ -46,6 +46,16 @@ if TYPE_CHECKING:
|
|
|
46
46
|
from pipecat_flows.manager import FlowManager
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
# Deadlock backstop for the "wait until ongoing actions finish" condition-wait.
|
|
50
|
+
# Normally `_ongoing_actions_finished_event` is set when the trailing action's
|
|
51
|
+
# frame (e.g. BotStoppedSpeakingFrame) arrives downstream. But a barge-in can
|
|
52
|
+
# flush that frame while the count stays >0, so the event is never set and the
|
|
53
|
+
# wait hangs forever (no `except` can rescue a never-returning await). This cap
|
|
54
|
+
# is generous on purpose — it must exceed any realistic TTS/function duration
|
|
55
|
+
# so it never truncates legitimate audio, only breaks a genuine hang.
|
|
56
|
+
ONGOING_ACTIONS_WAIT_TIMEOUT_S = 60.0
|
|
57
|
+
|
|
58
|
+
|
|
49
59
|
@dataclass
|
|
50
60
|
class FunctionActionFrame(ControlFrame):
|
|
51
61
|
"""Frame containing a function action to be executed.
|
|
@@ -364,7 +374,19 @@ class ActionManager:
|
|
|
364
374
|
pass
|
|
365
375
|
|
|
366
376
|
if needs_wait:
|
|
367
|
-
|
|
377
|
+
try:
|
|
378
|
+
await asyncio.wait_for(
|
|
379
|
+
self._ongoing_actions_finished_event.wait(),
|
|
380
|
+
timeout=ONGOING_ACTIONS_WAIT_TIMEOUT_S,
|
|
381
|
+
)
|
|
382
|
+
except asyncio.TimeoutError:
|
|
383
|
+
logger.warning(
|
|
384
|
+
"Timed out after %ss waiting for ongoing actions to finish "
|
|
385
|
+
"(previous=%s); the finished-event was never set — continuing "
|
|
386
|
+
"to avoid a permanent hang.",
|
|
387
|
+
ONGOING_ACTIONS_WAIT_TIMEOUT_S,
|
|
388
|
+
previous_action_type,
|
|
389
|
+
)
|
|
368
390
|
|
|
369
391
|
async def _handle_tts_action(self, action: dict) -> None:
|
|
370
392
|
"""Built-in handler for TTS actions.
|
|
@@ -6,8 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
"""Static validation of FlowConfigs that use the engine-primitive fields.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
NOTE: This module is a SKELETON and is NOT yet wired in. ``validate_flow_config``
|
|
10
|
+
is not called from ``FlowManager`` today and every rule body is a stub — there
|
|
11
|
+
is currently no active static validation at startup. The intended design is to
|
|
12
|
+
invoke it once from ``FlowManager.initialize()`` (before any node is set) so
|
|
13
|
+
that misconfigured graphs fail loudly at startup instead of mid-call, raising
|
|
14
|
+
``FlowInitializationError`` when ``result.errors`` is non-empty. That wiring,
|
|
15
|
+
and the rule bodies, land in PR-2/3/4 alongside the primitives they guard.
|
|
16
|
+
Until then, do not rely on this for any safety guarantee. Planned rules cover
|
|
11
17
|
the combinatorics of the four primitives we add together:
|
|
12
18
|
|
|
13
19
|
* auto_transition + post_actions (conflicting lifecycle)
|
{dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2100}/src/pipecat_flows/manager.py
RENAMED
|
@@ -185,7 +185,17 @@ class FlowManager:
|
|
|
185
185
|
self._last_router_pick: Optional[Dict[str, Any]] = None
|
|
186
186
|
self._context_aggregator = context_aggregator
|
|
187
187
|
self._pending_transition: Optional[Dict[str, Any]] = None
|
|
188
|
-
|
|
188
|
+
# Result ("success"/"failure") of the most recent function action, set
|
|
189
|
+
# by ActionManager and read by the dict-`auto_transition` resolver.
|
|
190
|
+
# Initialised to None and reset on node entry so a node with a
|
|
191
|
+
# success/failure dict that runs no function action this turn can't
|
|
192
|
+
# read a stale result from a previous node.
|
|
193
|
+
self._last_action_result: Optional[str] = None
|
|
194
|
+
# Strong references to detached fire-and-forget observability tasks.
|
|
195
|
+
# asyncio only holds weak refs to running tasks, so without this set a
|
|
196
|
+
# publish task can be GC'd mid-flight before its inner wait_for
|
|
197
|
+
# completes. Tasks discard themselves on completion (add_done_callback).
|
|
198
|
+
self._bg_tasks: set[asyncio.Task] = set()
|
|
189
199
|
self._context_strategy = context_strategy or ContextStrategyConfig(
|
|
190
200
|
strategy=ContextStrategy.APPEND
|
|
191
201
|
)
|
|
@@ -331,10 +341,13 @@ class FlowManager:
|
|
|
331
341
|
logger.warning(f"observability publish '{label}' failed: {exc!r}")
|
|
332
342
|
|
|
333
343
|
task = asyncio.create_task(_wrapped())
|
|
334
|
-
#
|
|
335
|
-
#
|
|
336
|
-
#
|
|
337
|
-
|
|
344
|
+
# Retain a strong reference so the loop's weak-ref bookkeeping can't GC
|
|
345
|
+
# this task before `_wrapped` finishes; the callback both discards the
|
|
346
|
+
# reference and acknowledges the result (suppressing the "Task
|
|
347
|
+
# exception was never retrieved" warning, though `_wrapped` already
|
|
348
|
+
# swallows it).
|
|
349
|
+
self._bg_tasks.add(task)
|
|
350
|
+
task.add_done_callback(self._bg_tasks.discard)
|
|
338
351
|
|
|
339
352
|
@property
|
|
340
353
|
def transport(self) -> Optional[BaseTransport]:
|
|
@@ -682,10 +695,6 @@ class FlowManager:
|
|
|
682
695
|
f"{'Transition-only function called for' if is_transition_only_function else 'Function handler completed for'} {name}"
|
|
683
696
|
)
|
|
684
697
|
|
|
685
|
-
# Store function result for potential extraction
|
|
686
|
-
if result and not is_transition_only_function:
|
|
687
|
-
self._pending_function_results[name] = result
|
|
688
|
-
|
|
689
698
|
# Determine if this is an edge function
|
|
690
699
|
is_edge_function = (
|
|
691
700
|
bool(next_node) or bool(transition_to) or bool(transition_callback)
|
|
@@ -1039,10 +1048,12 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
1039
1048
|
default_next=default_next,
|
|
1040
1049
|
)
|
|
1041
1050
|
|
|
1042
|
-
# Case 2: Action node with success/failure dict.
|
|
1051
|
+
# Case 2: Action node with success/failure dict. `_last_action_result`
|
|
1052
|
+
# is reset to None on node entry and set by this node's own function
|
|
1053
|
+
# action; None (no action ran) and "success" both take the success
|
|
1054
|
+
# branch — only an explicit "failure" routes to the failure target.
|
|
1043
1055
|
if isinstance(auto_transition, dict):
|
|
1044
|
-
|
|
1045
|
-
key = "failure" if result == "failure" else "success"
|
|
1056
|
+
key = "failure" if self._last_action_result == "failure" else "success"
|
|
1046
1057
|
return auto_transition.get(key) or auto_transition.get("success")
|
|
1047
1058
|
|
|
1048
1059
|
# Case 1: bare string (Speak / single-target Action).
|
|
@@ -1403,7 +1414,11 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
1403
1414
|
# override=Gemini) isn't supported by this path yet.
|
|
1404
1415
|
llm_service = self.state.get("interrupt_classifier_llm_service") or self._llm
|
|
1405
1416
|
if isinstance(llm_service, LLMSwitcher):
|
|
1406
|
-
|
|
1417
|
+
# LLMSwitcher exposes the live service via the `active_llm`
|
|
1418
|
+
# property (it proxies `self.strategy.active_service`). The
|
|
1419
|
+
# switcher itself has no `active_service` attribute — reading it
|
|
1420
|
+
# raises AttributeError. See pipecat/pipeline/llm_switcher.py.
|
|
1421
|
+
llm_service = llm_service.active_llm
|
|
1407
1422
|
|
|
1408
1423
|
valid_ids: List[str] = [RESPEAK_BRANCH_ID]
|
|
1409
1424
|
for r_id, brs in branches_by_source.items():
|
|
@@ -1450,6 +1465,16 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
1450
1465
|
)
|
|
1451
1466
|
return (natural_next or speak_id, [], False)
|
|
1452
1467
|
|
|
1468
|
+
# Allow-list extracted keys to the declared extract-spec names before
|
|
1469
|
+
# merging into shared `state`. The strict JSON schema constrains keys,
|
|
1470
|
+
# but every adapter falls back to schema-less json_object mode on
|
|
1471
|
+
# providers that reject strict schemas — on that path the model's keys
|
|
1472
|
+
# are unbounded. `state` is the same flat dict that holds live runtime
|
|
1473
|
+
# objects/secrets (task, tts_service_map, integration_credentials, ...
|
|
1474
|
+
# merged from flow_dependencies), so an unfiltered merge lets a
|
|
1475
|
+
# hallucinated reserved key clobber them.
|
|
1476
|
+
allowed_keys = {k for m in extract_by_source.values() for k in (m or {})}
|
|
1477
|
+
extracted = {k: v for k, v in extracted.items() if k in allowed_keys}
|
|
1453
1478
|
if extracted:
|
|
1454
1479
|
self.state.update(extracted)
|
|
1455
1480
|
# Mirror the normal-router behavior: surface what the classifier
|
|
@@ -1683,7 +1708,11 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
1683
1708
|
# override; otherwise the main LLM.
|
|
1684
1709
|
llm_service = self._llm
|
|
1685
1710
|
if isinstance(llm_service, LLMSwitcher):
|
|
1686
|
-
|
|
1711
|
+
# LLMSwitcher exposes the live service via the `active_llm`
|
|
1712
|
+
# property (it proxies `self.strategy.active_service`). The
|
|
1713
|
+
# switcher itself has no `active_service` attribute — reading it
|
|
1714
|
+
# raises AttributeError. See pipecat/pipeline/llm_switcher.py.
|
|
1715
|
+
llm_service = llm_service.active_llm
|
|
1687
1716
|
|
|
1688
1717
|
raw = await self._adapter.run_router_completion(
|
|
1689
1718
|
llm_service=llm_service,
|
|
@@ -1737,6 +1766,13 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
1737
1766
|
# source for extraction, and applying it would let stray values
|
|
1738
1767
|
# overwrite legitimate prior extractions. The re-prompt loop will
|
|
1739
1768
|
# capture the right reply on the next turn.
|
|
1769
|
+
# Allow-list to the declared `router_extract` names before merging —
|
|
1770
|
+
# the schema-less adapter fallback (see `parse_router_response`) can
|
|
1771
|
+
# emit arbitrary keys, and `state` holds live runtime objects/secrets
|
|
1772
|
+
# (task, tts_service_map, integration_credentials, ...) that a stray
|
|
1773
|
+
# reserved key would otherwise overwrite.
|
|
1774
|
+
allowed_keys = set((node_config.get("router_extract") or {}).keys())
|
|
1775
|
+
extracted = {k: v for k, v in extracted.items() if k in allowed_keys}
|
|
1740
1776
|
if extracted and branch_id != UNCLEAR_BRANCH_ID:
|
|
1741
1777
|
self.state.update(extracted)
|
|
1742
1778
|
logger.debug(
|
|
@@ -2014,6 +2050,20 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
2014
2050
|
config_key = f"{provider}_{model}"
|
|
2015
2051
|
target_service = llm_service_map.get(config_key)
|
|
2016
2052
|
if target_service:
|
|
2053
|
+
# Snapshot the baseline (un-overridden) LLM the first
|
|
2054
|
+
# time we override, so we can switch back when a later
|
|
2055
|
+
# node has no llm_override. The baseline is whatever was
|
|
2056
|
+
# active before any override fired.
|
|
2057
|
+
if previous_llm is None and "llm_baseline" not in self._previous_overrides:
|
|
2058
|
+
baseline = None
|
|
2059
|
+
active = self._llm
|
|
2060
|
+
if isinstance(active, LLMSwitcher):
|
|
2061
|
+
active = active.active_llm
|
|
2062
|
+
for key, svc in llm_service_map.items():
|
|
2063
|
+
if svc is active:
|
|
2064
|
+
baseline = key
|
|
2065
|
+
break
|
|
2066
|
+
self._previous_overrides["llm_baseline"] = baseline
|
|
2017
2067
|
await task.queue_frames(
|
|
2018
2068
|
[ManuallySwitchServiceFrame(service=target_service)]
|
|
2019
2069
|
)
|
|
@@ -2028,6 +2078,30 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
2028
2078
|
logger.warning(
|
|
2029
2079
|
f"Incomplete LLM override config for node {node_id}: {current_llm}"
|
|
2030
2080
|
)
|
|
2081
|
+
elif not current_llm and previous_llm and task and llm_service_map:
|
|
2082
|
+
# Exiting an llm_override node into one without an override:
|
|
2083
|
+
# restore the snapshotted baseline LLM. Without this the cheap
|
|
2084
|
+
# router model stays pinned for every subsequent generative
|
|
2085
|
+
# node (contract: "the previous LLM is restored on exit").
|
|
2086
|
+
baseline_key = self._previous_overrides.get("llm_baseline")
|
|
2087
|
+
baseline_service = (
|
|
2088
|
+
llm_service_map.get(baseline_key) if baseline_key else None
|
|
2089
|
+
)
|
|
2090
|
+
if baseline_service:
|
|
2091
|
+
await task.queue_frames(
|
|
2092
|
+
[ManuallySwitchServiceFrame(service=baseline_service)]
|
|
2093
|
+
)
|
|
2094
|
+
logger.debug(
|
|
2095
|
+
f"Restored baseline LLM '{baseline_key}' on exit from "
|
|
2096
|
+
f"override node (now at {node_id})"
|
|
2097
|
+
)
|
|
2098
|
+
else:
|
|
2099
|
+
logger.warning(
|
|
2100
|
+
f"Could not restore baseline LLM on exit from override "
|
|
2101
|
+
f"node {node_id}: baseline key '{baseline_key}' not in "
|
|
2102
|
+
f"llm_service_map"
|
|
2103
|
+
)
|
|
2104
|
+
self._previous_overrides.pop("llm_baseline", None)
|
|
2031
2105
|
self._previous_overrides["llm"] = current_llm
|
|
2032
2106
|
|
|
2033
2107
|
# Future: Handle other overrides (VAD, STT) here
|
|
@@ -2201,6 +2275,12 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
2201
2275
|
# Reset leaked action counts from interrupted actions in previous node
|
|
2202
2276
|
self._action_manager.reset_ongoing_actions()
|
|
2203
2277
|
|
|
2278
|
+
# Reset the per-node action result so a success/failure
|
|
2279
|
+
# `auto_transition` dict can't route on the PREVIOUS node's result
|
|
2280
|
+
# if this node runs no function action this turn. A function
|
|
2281
|
+
# pre-action below overwrites this before the resolver reads it.
|
|
2282
|
+
self._last_action_result = None
|
|
2283
|
+
|
|
2204
2284
|
# Clear any deferred post-actions from previous node
|
|
2205
2285
|
self._action_manager.clear_deferred_post_actions()
|
|
2206
2286
|
|
|
@@ -2438,12 +2518,29 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
2438
2518
|
router_target = node_config.get("router_exhausted_next")
|
|
2439
2519
|
if router_target:
|
|
2440
2520
|
await self._short_circuit_to(router_target)
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2521
|
+
return
|
|
2522
|
+
# No branch target AND no router_exhausted_next resolved
|
|
2523
|
+
# (reachable from a single misconfigured node: a branch with an
|
|
2524
|
+
# unset `next`, the unclear cap hit with no router_exhausted_next,
|
|
2525
|
+
# or under-cap with neither reprompt nor router_unclear_next).
|
|
2526
|
+
# Silently returning would strand the call: `suppress_llm_text`
|
|
2527
|
+
# is latched on this node so RouterModeGuard gates the generative
|
|
2528
|
+
# LLM, and the StopUserIdleProcessorFrame queued at entry is
|
|
2529
|
+
# never matched by a StartUserIdleProcessorFrame below — idle
|
|
2530
|
+
# recovery is disabled too → permanent dead-air with no escape.
|
|
2531
|
+
# Fail loudly instead: clear the suppression latch, re-enable
|
|
2532
|
+
# idle recovery so teardown isn't wedged, then raise.
|
|
2533
|
+
logger.error(
|
|
2534
|
+
f"Router-mode node {node_id} returned no target and no "
|
|
2535
|
+
f"router_exhausted_next configured; failing the transition "
|
|
2536
|
+
f"to avoid permanent dead-air"
|
|
2537
|
+
)
|
|
2538
|
+
self._current_node = None # release is_suppressing_llm_text() latch
|
|
2539
|
+
await self._task.queue_frame(StartUserIdleProcessorFrame())
|
|
2540
|
+
raise FlowError(
|
|
2541
|
+
f"Router-mode node {node_id} has no resolvable target and no "
|
|
2542
|
+
f"router_exhausted_next; this is a flow misconfiguration"
|
|
2543
|
+
)
|
|
2447
2544
|
|
|
2448
2545
|
# Register functions and prepare tools
|
|
2449
2546
|
tools: List[FlowsFunctionSchema | FlowsDirectFunctionWrapper] = []
|