dv-pipecat-flows 0.0.22.dev2000__tar.gz → 0.0.22.dev2200__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.dev2200}/PKG-INFO +1 -1
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/engine_primitives_plan.md +1 -1
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200/src/dv_pipecat_flows.egg-info}/PKG-INFO +1 -1
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/src/pipecat_flows/actions.py +23 -1
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/src/pipecat_flows/flow_validator.py +8 -2
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/src/pipecat_flows/manager.py +172 -22
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/src/pipecat_flows/types.py +6 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/uv.lock +109 -1185
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/.agents/skills/loki-logs/SKILL.md +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/.agents/skills/loki-logs/query-reference.md +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/.claude/skills/loki-logs/SKILL.md +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/.claude/skills/loki-logs/query-reference.md +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/.gitattributes +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/.gitignore +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/.pre-commit-config.yaml +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/.python-version +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/.readthedocs.yaml +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/AGENTS.md +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/CHANGELOG.md +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/CLAUDE.md +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/CONTRIBUTING.md +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/LICENSE +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/MANIFEST.in +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/README.md +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/dev-requirements.txt +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/docker-compose.dev.yml +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/.eslintrc.json +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/.prettierrc +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/css/tailwind.css +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/examples/food_ordering.json +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/examples/movie_explorer.json +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/examples/patient_intake.json +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/examples/restaurant_reservation.json +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/examples/travel_planner.json +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/favicon.png +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/favicon.svg +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/index.html +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/js/editor/canvas.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/js/editor/editorState.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/js/editor/sidePanel.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/js/editor/toolbar.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/js/main.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/js/nodes/baseNode.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/js/nodes/endNode.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/js/nodes/flowNode.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/js/nodes/functionNode.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/js/nodes/index.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/js/nodes/mergeNode.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/js/nodes/startNode.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/js/types.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/js/utils/export.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/js/utils/helpers.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/js/utils/import.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/js/utils/validation.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/jsdoc.json +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/package-lock.json +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/package.json +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/postcss.config.cjs +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/public/favicon.png +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/public/favicon.svg +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/tailwind.config.cjs +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/vercel.json +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/editor/vite.config.js +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/env.example +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/images/food-ordering-flow.png +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/pipecat-flows.png +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/pipecat_upgrade.md +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/pyproject.toml +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/remote-asterisk-code/README.md +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/remote-asterisk-code/extensions.conf +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/remote-asterisk-code/rtp.conf +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/requirements.txt +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/scripts/check-pypi-package.py +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/scripts/fix-ruff.sh +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/scripts/pre-commit.sh +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/setup.cfg +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/src/dv_pipecat_flows.egg-info/SOURCES.txt +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/src/dv_pipecat_flows.egg-info/dependency_links.txt +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/src/dv_pipecat_flows.egg-info/requires.txt +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/src/dv_pipecat_flows.egg-info/top_level.txt +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/src/pipecat_flows/__init__.py +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/src/pipecat_flows/adapters.py +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/src/pipecat_flows/condition_evaluator.py +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/src/pipecat_flows/exceptions.py +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/src/pipecat_flows/processors/__init__.py +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/src/pipecat_flows/processors/router_mode_guard.py +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/src/pipecat_flows/processors/speak_interruption_guard.py +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/src/pipecat_flows/processors/user_turn_observer.py +0 -0
- {dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/src/pipecat_flows/router_mode.py +0 -0
{dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/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.dev2200}/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.dev2200}/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]:
|
|
@@ -609,6 +622,7 @@ class FlowManager:
|
|
|
609
622
|
handler: Optional[Callable | FlowsDirectFunctionWrapper],
|
|
610
623
|
transition_to: Optional[str],
|
|
611
624
|
transition_callback: Optional[Callable] = None,
|
|
625
|
+
state_update_keys: Optional[List[str]] = None,
|
|
612
626
|
) -> Callable:
|
|
613
627
|
"""Create a transition function for the given name and handler.
|
|
614
628
|
|
|
@@ -617,6 +631,14 @@ class FlowManager:
|
|
|
617
631
|
handler: Optional function to process data.
|
|
618
632
|
transition_to: Optional node to transition to (static flows).
|
|
619
633
|
transition_callback: Optional callback for dynamic transitions.
|
|
634
|
+
state_update_keys: Optional list of argument names — when the
|
|
635
|
+
LLM calls this function, the named keys from
|
|
636
|
+
``params.arguments`` are copied into ``flow_manager.state``
|
|
637
|
+
before the transition resolves. Used by backend wiring to
|
|
638
|
+
flatten ``agent_node → llm_router`` transitions and still
|
|
639
|
+
propagate the router's ``router_extract`` variables (e.g.
|
|
640
|
+
``language``) into state, preserving parity with
|
|
641
|
+
``_run_router_node``'s ``state.update(extracted)``.
|
|
620
642
|
|
|
621
643
|
Returns:
|
|
622
644
|
Async function that handles the tool invocation.
|
|
@@ -639,6 +661,26 @@ class FlowManager:
|
|
|
639
661
|
function_start_time = time.time()
|
|
640
662
|
try:
|
|
641
663
|
logger.debug(f"Function called: {name}")
|
|
664
|
+
# Flattened-router extract: copy LLM-supplied args into
|
|
665
|
+
# ``flow_manager.state`` BEFORE the transition resolves, so
|
|
666
|
+
# downstream nodes (e.g. Speak picking variant by
|
|
667
|
+
# ``state["language"]``) see the new values on their first
|
|
668
|
+
# ``_set_node`` after the transition. Mirrors the
|
|
669
|
+
# ``state.update(extracted)`` that ``_run_router_node`` does
|
|
670
|
+
# on the non-flattened path.
|
|
671
|
+
if state_update_keys and params.arguments:
|
|
672
|
+
extract_update = {
|
|
673
|
+
key: params.arguments[key]
|
|
674
|
+
for key in state_update_keys
|
|
675
|
+
if key in params.arguments
|
|
676
|
+
}
|
|
677
|
+
if extract_update:
|
|
678
|
+
self.state.update(extract_update)
|
|
679
|
+
logger.debug(
|
|
680
|
+
f"[FLATTENED_ROUTER_EXTRACT] state.update keys="
|
|
681
|
+
f"{list(extract_update.keys())} function={name}"
|
|
682
|
+
)
|
|
683
|
+
|
|
642
684
|
function_call_id = await start_tracking(
|
|
643
685
|
transcript_handler=self._transcript_handler,
|
|
644
686
|
tool_name=name,
|
|
@@ -682,10 +724,6 @@ class FlowManager:
|
|
|
682
724
|
f"{'Transition-only function called for' if is_transition_only_function else 'Function handler completed for'} {name}"
|
|
683
725
|
)
|
|
684
726
|
|
|
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
727
|
# Determine if this is an edge function
|
|
690
728
|
is_edge_function = (
|
|
691
729
|
bool(next_node) or bool(transition_to) or bool(transition_callback)
|
|
@@ -892,6 +930,7 @@ class FlowManager:
|
|
|
892
930
|
handler: Optional[Callable | FlowsDirectFunctionWrapper],
|
|
893
931
|
transition_to: Optional[str] = None,
|
|
894
932
|
transition_callback: Optional[Callable] = None,
|
|
933
|
+
state_update_keys: Optional[List[str]] = None,
|
|
895
934
|
) -> None:
|
|
896
935
|
"""Register a function with the LLM if not already registered.
|
|
897
936
|
|
|
@@ -915,7 +954,11 @@ class FlowManager:
|
|
|
915
954
|
|
|
916
955
|
# Create transition function
|
|
917
956
|
transition_func = await self._create_transition_func(
|
|
918
|
-
name,
|
|
957
|
+
name,
|
|
958
|
+
handler,
|
|
959
|
+
transition_to,
|
|
960
|
+
transition_callback,
|
|
961
|
+
state_update_keys=state_update_keys,
|
|
919
962
|
)
|
|
920
963
|
|
|
921
964
|
# Register function with LLM (or LLMSwitcher).
|
|
@@ -1039,10 +1082,12 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
1039
1082
|
default_next=default_next,
|
|
1040
1083
|
)
|
|
1041
1084
|
|
|
1042
|
-
# Case 2: Action node with success/failure dict.
|
|
1085
|
+
# Case 2: Action node with success/failure dict. `_last_action_result`
|
|
1086
|
+
# is reset to None on node entry and set by this node's own function
|
|
1087
|
+
# action; None (no action ran) and "success" both take the success
|
|
1088
|
+
# branch — only an explicit "failure" routes to the failure target.
|
|
1043
1089
|
if isinstance(auto_transition, dict):
|
|
1044
|
-
|
|
1045
|
-
key = "failure" if result == "failure" else "success"
|
|
1090
|
+
key = "failure" if self._last_action_result == "failure" else "success"
|
|
1046
1091
|
return auto_transition.get(key) or auto_transition.get("success")
|
|
1047
1092
|
|
|
1048
1093
|
# Case 1: bare string (Speak / single-target Action).
|
|
@@ -1403,7 +1448,11 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
1403
1448
|
# override=Gemini) isn't supported by this path yet.
|
|
1404
1449
|
llm_service = self.state.get("interrupt_classifier_llm_service") or self._llm
|
|
1405
1450
|
if isinstance(llm_service, LLMSwitcher):
|
|
1406
|
-
|
|
1451
|
+
# LLMSwitcher exposes the live service via the `active_llm`
|
|
1452
|
+
# property (it proxies `self.strategy.active_service`). The
|
|
1453
|
+
# switcher itself has no `active_service` attribute — reading it
|
|
1454
|
+
# raises AttributeError. See pipecat/pipeline/llm_switcher.py.
|
|
1455
|
+
llm_service = llm_service.active_llm
|
|
1407
1456
|
|
|
1408
1457
|
valid_ids: List[str] = [RESPEAK_BRANCH_ID]
|
|
1409
1458
|
for r_id, brs in branches_by_source.items():
|
|
@@ -1450,6 +1499,16 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
1450
1499
|
)
|
|
1451
1500
|
return (natural_next or speak_id, [], False)
|
|
1452
1501
|
|
|
1502
|
+
# Allow-list extracted keys to the declared extract-spec names before
|
|
1503
|
+
# merging into shared `state`. The strict JSON schema constrains keys,
|
|
1504
|
+
# but every adapter falls back to schema-less json_object mode on
|
|
1505
|
+
# providers that reject strict schemas — on that path the model's keys
|
|
1506
|
+
# are unbounded. `state` is the same flat dict that holds live runtime
|
|
1507
|
+
# objects/secrets (task, tts_service_map, integration_credentials, ...
|
|
1508
|
+
# merged from flow_dependencies), so an unfiltered merge lets a
|
|
1509
|
+
# hallucinated reserved key clobber them.
|
|
1510
|
+
allowed_keys = {k for m in extract_by_source.values() for k in (m or {})}
|
|
1511
|
+
extracted = {k: v for k, v in extracted.items() if k in allowed_keys}
|
|
1453
1512
|
if extracted:
|
|
1454
1513
|
self.state.update(extracted)
|
|
1455
1514
|
# Mirror the normal-router behavior: surface what the classifier
|
|
@@ -1683,7 +1742,11 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
1683
1742
|
# override; otherwise the main LLM.
|
|
1684
1743
|
llm_service = self._llm
|
|
1685
1744
|
if isinstance(llm_service, LLMSwitcher):
|
|
1686
|
-
|
|
1745
|
+
# LLMSwitcher exposes the live service via the `active_llm`
|
|
1746
|
+
# property (it proxies `self.strategy.active_service`). The
|
|
1747
|
+
# switcher itself has no `active_service` attribute — reading it
|
|
1748
|
+
# raises AttributeError. See pipecat/pipeline/llm_switcher.py.
|
|
1749
|
+
llm_service = llm_service.active_llm
|
|
1687
1750
|
|
|
1688
1751
|
raw = await self._adapter.run_router_completion(
|
|
1689
1752
|
llm_service=llm_service,
|
|
@@ -1737,6 +1800,13 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
1737
1800
|
# source for extraction, and applying it would let stray values
|
|
1738
1801
|
# overwrite legitimate prior extractions. The re-prompt loop will
|
|
1739
1802
|
# capture the right reply on the next turn.
|
|
1803
|
+
# Allow-list to the declared `router_extract` names before merging —
|
|
1804
|
+
# the schema-less adapter fallback (see `parse_router_response`) can
|
|
1805
|
+
# emit arbitrary keys, and `state` holds live runtime objects/secrets
|
|
1806
|
+
# (task, tts_service_map, integration_credentials, ...) that a stray
|
|
1807
|
+
# reserved key would otherwise overwrite.
|
|
1808
|
+
allowed_keys = set((node_config.get("router_extract") or {}).keys())
|
|
1809
|
+
extracted = {k: v for k, v in extracted.items() if k in allowed_keys}
|
|
1740
1810
|
if extracted and branch_id != UNCLEAR_BRANCH_ID:
|
|
1741
1811
|
self.state.update(extracted)
|
|
1742
1812
|
logger.debug(
|
|
@@ -2014,6 +2084,20 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
2014
2084
|
config_key = f"{provider}_{model}"
|
|
2015
2085
|
target_service = llm_service_map.get(config_key)
|
|
2016
2086
|
if target_service:
|
|
2087
|
+
# Snapshot the baseline (un-overridden) LLM the first
|
|
2088
|
+
# time we override, so we can switch back when a later
|
|
2089
|
+
# node has no llm_override. The baseline is whatever was
|
|
2090
|
+
# active before any override fired.
|
|
2091
|
+
if previous_llm is None and "llm_baseline" not in self._previous_overrides:
|
|
2092
|
+
baseline = None
|
|
2093
|
+
active = self._llm
|
|
2094
|
+
if isinstance(active, LLMSwitcher):
|
|
2095
|
+
active = active.active_llm
|
|
2096
|
+
for key, svc in llm_service_map.items():
|
|
2097
|
+
if svc is active:
|
|
2098
|
+
baseline = key
|
|
2099
|
+
break
|
|
2100
|
+
self._previous_overrides["llm_baseline"] = baseline
|
|
2017
2101
|
await task.queue_frames(
|
|
2018
2102
|
[ManuallySwitchServiceFrame(service=target_service)]
|
|
2019
2103
|
)
|
|
@@ -2028,6 +2112,30 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
2028
2112
|
logger.warning(
|
|
2029
2113
|
f"Incomplete LLM override config for node {node_id}: {current_llm}"
|
|
2030
2114
|
)
|
|
2115
|
+
elif not current_llm and previous_llm and task and llm_service_map:
|
|
2116
|
+
# Exiting an llm_override node into one without an override:
|
|
2117
|
+
# restore the snapshotted baseline LLM. Without this the cheap
|
|
2118
|
+
# router model stays pinned for every subsequent generative
|
|
2119
|
+
# node (contract: "the previous LLM is restored on exit").
|
|
2120
|
+
baseline_key = self._previous_overrides.get("llm_baseline")
|
|
2121
|
+
baseline_service = (
|
|
2122
|
+
llm_service_map.get(baseline_key) if baseline_key else None
|
|
2123
|
+
)
|
|
2124
|
+
if baseline_service:
|
|
2125
|
+
await task.queue_frames(
|
|
2126
|
+
[ManuallySwitchServiceFrame(service=baseline_service)]
|
|
2127
|
+
)
|
|
2128
|
+
logger.debug(
|
|
2129
|
+
f"Restored baseline LLM '{baseline_key}' on exit from "
|
|
2130
|
+
f"override node (now at {node_id})"
|
|
2131
|
+
)
|
|
2132
|
+
else:
|
|
2133
|
+
logger.warning(
|
|
2134
|
+
f"Could not restore baseline LLM on exit from override "
|
|
2135
|
+
f"node {node_id}: baseline key '{baseline_key}' not in "
|
|
2136
|
+
f"llm_service_map"
|
|
2137
|
+
)
|
|
2138
|
+
self._previous_overrides.pop("llm_baseline", None)
|
|
2031
2139
|
self._previous_overrides["llm"] = current_llm
|
|
2032
2140
|
|
|
2033
2141
|
# Future: Handle other overrides (VAD, STT) here
|
|
@@ -2201,6 +2309,12 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
2201
2309
|
# Reset leaked action counts from interrupted actions in previous node
|
|
2202
2310
|
self._action_manager.reset_ongoing_actions()
|
|
2203
2311
|
|
|
2312
|
+
# Reset the per-node action result so a success/failure
|
|
2313
|
+
# `auto_transition` dict can't route on the PREVIOUS node's result
|
|
2314
|
+
# if this node runs no function action this turn. A function
|
|
2315
|
+
# pre-action below overwrites this before the resolver reads it.
|
|
2316
|
+
self._last_action_result = None
|
|
2317
|
+
|
|
2204
2318
|
# Clear any deferred post-actions from previous node
|
|
2205
2319
|
self._action_manager.clear_deferred_post_actions()
|
|
2206
2320
|
|
|
@@ -2242,8 +2356,26 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
2242
2356
|
isinstance(a, dict) and a.get("type") == "tts_say"
|
|
2243
2357
|
for a in pre_actions
|
|
2244
2358
|
):
|
|
2359
|
+
# ``Event.wait()`` that's never set hangs forever — a bare
|
|
2360
|
+
# ``await`` here was a transition-deadlock vector when the
|
|
2361
|
+
# ``BotStoppedSpeakingFrame`` never reaches ActionManager
|
|
2362
|
+
# (cancelled pipeline, dropped frame, abruptly closed
|
|
2363
|
+
# transport). Mirror ``actions.py:_maybe_wait_for_ongoing_
|
|
2364
|
+
# actions_to_finish``'s 60s ``wait_for`` backstop so the
|
|
2365
|
+
# node still transitions instead of permanently parking.
|
|
2366
|
+
from pipecat_flows.actions import ONGOING_ACTIONS_WAIT_TIMEOUT_S
|
|
2245
2367
|
try:
|
|
2246
|
-
await
|
|
2368
|
+
await asyncio.wait_for(
|
|
2369
|
+
self._action_manager._ongoing_actions_finished_event.wait(),
|
|
2370
|
+
timeout=ONGOING_ACTIONS_WAIT_TIMEOUT_S,
|
|
2371
|
+
)
|
|
2372
|
+
except asyncio.TimeoutError:
|
|
2373
|
+
logger.warning(
|
|
2374
|
+
f"audio-wait on '{node_id}' timed out after "
|
|
2375
|
+
f"{ONGOING_ACTIONS_WAIT_TIMEOUT_S}s — "
|
|
2376
|
+
f"BotStoppedSpeakingFrame never arrived; continuing "
|
|
2377
|
+
f"with short-circuit to avoid a permanent hang"
|
|
2378
|
+
)
|
|
2247
2379
|
except Exception as wait_exc:
|
|
2248
2380
|
logger.warning(
|
|
2249
2381
|
f"audio-wait on '{node_id}' failed ({wait_exc!r}); "
|
|
@@ -2438,12 +2570,29 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
2438
2570
|
router_target = node_config.get("router_exhausted_next")
|
|
2439
2571
|
if router_target:
|
|
2440
2572
|
await self._short_circuit_to(router_target)
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2573
|
+
return
|
|
2574
|
+
# No branch target AND no router_exhausted_next resolved
|
|
2575
|
+
# (reachable from a single misconfigured node: a branch with an
|
|
2576
|
+
# unset `next`, the unclear cap hit with no router_exhausted_next,
|
|
2577
|
+
# or under-cap with neither reprompt nor router_unclear_next).
|
|
2578
|
+
# Silently returning would strand the call: `suppress_llm_text`
|
|
2579
|
+
# is latched on this node so RouterModeGuard gates the generative
|
|
2580
|
+
# LLM, and the StopUserIdleProcessorFrame queued at entry is
|
|
2581
|
+
# never matched by a StartUserIdleProcessorFrame below — idle
|
|
2582
|
+
# recovery is disabled too → permanent dead-air with no escape.
|
|
2583
|
+
# Fail loudly instead: clear the suppression latch, re-enable
|
|
2584
|
+
# idle recovery so teardown isn't wedged, then raise.
|
|
2585
|
+
logger.error(
|
|
2586
|
+
f"Router-mode node {node_id} returned no target and no "
|
|
2587
|
+
f"router_exhausted_next configured; failing the transition "
|
|
2588
|
+
f"to avoid permanent dead-air"
|
|
2589
|
+
)
|
|
2590
|
+
self._current_node = None # release is_suppressing_llm_text() latch
|
|
2591
|
+
await self._task.queue_frame(StartUserIdleProcessorFrame())
|
|
2592
|
+
raise FlowError(
|
|
2593
|
+
f"Router-mode node {node_id} has no resolvable target and no "
|
|
2594
|
+
f"router_exhausted_next; this is a flow misconfiguration"
|
|
2595
|
+
)
|
|
2447
2596
|
|
|
2448
2597
|
# Register functions and prepare tools
|
|
2449
2598
|
tools: List[FlowsFunctionSchema | FlowsDirectFunctionWrapper] = []
|
|
@@ -2461,6 +2610,7 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
2461
2610
|
handler=schema.handler,
|
|
2462
2611
|
transition_to=schema.transition_to,
|
|
2463
2612
|
transition_callback=schema.transition_callback,
|
|
2613
|
+
state_update_keys=schema.state_update_keys,
|
|
2464
2614
|
)
|
|
2465
2615
|
|
|
2466
2616
|
async def register_direct_function(func):
|
{dv_pipecat_flows-0.0.22.dev2000 → dv_pipecat_flows-0.0.22.dev2200}/src/pipecat_flows/types.py
RENAMED
|
@@ -273,6 +273,12 @@ class FlowsFunctionSchema:
|
|
|
273
273
|
handler: Optional[FunctionHandler] = None
|
|
274
274
|
transition_to: Optional[str] = None
|
|
275
275
|
transition_callback: Optional[Callable] = None
|
|
276
|
+
# When the LLM calls this function, copy these keys from
|
|
277
|
+
# ``params.arguments`` into ``flow_manager.state`` BEFORE the transition
|
|
278
|
+
# resolves. Used by backend wiring to flatten ``agent_node → llm_router``
|
|
279
|
+
# transitions and still propagate the router's ``router_extract``
|
|
280
|
+
# variables (e.g. ``language``) into state. Empty/None = no-op.
|
|
281
|
+
state_update_keys: Optional[List[str]] = None
|
|
276
282
|
|
|
277
283
|
def __post_init__(self):
|
|
278
284
|
"""Validate the schema configuration.
|