dv-pipecat-flows 0.0.22.dev1516__tar.gz → 0.0.22.dev1700__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.dev1700/.python-version +1 -0
- {dv_pipecat_flows-0.0.22.dev1516/src/dv_pipecat_flows.egg-info → dv_pipecat_flows-0.0.22.dev1700}/PKG-INFO +1 -2
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/README.md +0 -1
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700/src/dv_pipecat_flows.egg-info}/PKG-INFO +1 -2
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/src/dv_pipecat_flows.egg-info/SOURCES.txt +0 -25
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/src/pipecat_flows/actions.py +106 -23
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/src/pipecat_flows/adapters.py +245 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/src/pipecat_flows/condition_evaluator.py +9 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/src/pipecat_flows/manager.py +122 -41
- dv_pipecat_flows-0.0.22.dev1516/.python-version +0 -1
- dv_pipecat_flows-0.0.22.dev1516/evals/kb/.gitignore +0 -2
- dv_pipecat_flows-0.0.22.dev1516/evals/kb/README.md +0 -213
- dv_pipecat_flows-0.0.22.dev1516/evals/kb/RESULTS.md +0 -133
- dv_pipecat_flows-0.0.22.dev1516/evals/kb/UNIFIED_KB_BENCHMARKS.md +0 -713
- dv_pipecat_flows-0.0.22.dev1516/evals/kb/chunkers.py +0 -179
- dv_pipecat_flows-0.0.22.dev1516/evals/kb/contracts.py +0 -128
- dv_pipecat_flows-0.0.22.dev1516/evals/kb/datagen/generate_corpus.py +0 -281
- dv_pipecat_flows-0.0.22.dev1516/evals/kb/datagen/generate_dataset.py +0 -413
- dv_pipecat_flows-0.0.22.dev1516/evals/kb/datagen/generate_hybrid.py +0 -181
- dv_pipecat_flows-0.0.22.dev1516/evals/kb/datagen/templates/insurance.yaml +0 -40
- dv_pipecat_flows-0.0.22.dev1516/evals/kb/datasets/.gitignore +0 -9
- dv_pipecat_flows-0.0.22.dev1516/evals/kb/datasets/v1_hybrid/manifest.json +0 -33
- dv_pipecat_flows-0.0.22.dev1516/evals/kb/datasets/v1_retrieval/manifest.json +0 -16
- dv_pipecat_flows-0.0.22.dev1516/evals/kb/datasets/v1_retrieval_long/manifest.json +0 -17
- dv_pipecat_flows-0.0.22.dev1516/evals/kb/datasets/v1_synthetic/manifest.json +0 -45
- dv_pipecat_flows-0.0.22.dev1516/evals/kb/runners/run_filter_pick.py +0 -515
- dv_pipecat_flows-0.0.22.dev1516/evals/kb/runners/run_filter_scaling.py +0 -286
- dv_pipecat_flows-0.0.22.dev1516/evals/kb/runners/run_hybrid.py +0 -205
- dv_pipecat_flows-0.0.22.dev1516/evals/kb/runners/run_latency.py +0 -232
- dv_pipecat_flows-0.0.22.dev1516/evals/kb/runners/run_rerank.py +0 -279
- dv_pipecat_flows-0.0.22.dev1516/evals/kb/runners/run_retrieval.py +0 -340
- dv_pipecat_flows-0.0.22.dev1516/evals/kb/service/API.md +0 -490
- dv_pipecat_flows-0.0.22.dev1516/evals/kb/service/__init__.py +0 -0
- dv_pipecat_flows-0.0.22.dev1516/evals/kb/service/app.py +0 -332
- dv_pipecat_flows-0.0.22.dev1516/evals/kb/service/jobs.py +0 -230
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/.agents/skills/loki-logs/SKILL.md +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/.agents/skills/loki-logs/query-reference.md +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/.claude/skills/loki-logs/SKILL.md +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/.claude/skills/loki-logs/query-reference.md +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/.gitattributes +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/.gitignore +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/.pre-commit-config.yaml +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/.readthedocs.yaml +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/AGENTS.md +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/CHANGELOG.md +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/CLAUDE.md +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/CONTRIBUTING.md +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/LICENSE +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/MANIFEST.in +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/dev-requirements.txt +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/docker-compose.dev.yml +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/.eslintrc.json +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/.prettierrc +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/css/tailwind.css +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/examples/food_ordering.json +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/examples/movie_explorer.json +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/examples/patient_intake.json +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/examples/restaurant_reservation.json +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/examples/travel_planner.json +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/favicon.png +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/favicon.svg +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/index.html +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/js/editor/canvas.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/js/editor/editorState.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/js/editor/sidePanel.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/js/editor/toolbar.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/js/main.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/js/nodes/baseNode.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/js/nodes/endNode.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/js/nodes/flowNode.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/js/nodes/functionNode.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/js/nodes/index.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/js/nodes/mergeNode.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/js/nodes/startNode.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/js/types.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/js/utils/export.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/js/utils/helpers.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/js/utils/import.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/js/utils/validation.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/jsdoc.json +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/package-lock.json +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/package.json +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/postcss.config.cjs +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/public/favicon.png +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/public/favicon.svg +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/tailwind.config.cjs +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/vercel.json +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/editor/vite.config.js +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/engine_primitives_plan.md +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/env.example +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/images/food-ordering-flow.png +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/pipecat-flows.png +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/pipecat_upgrade.md +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/pyproject.toml +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/remote-asterisk-code/README.md +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/remote-asterisk-code/extensions.conf +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/remote-asterisk-code/rtp.conf +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/requirements.txt +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/scripts/check-pypi-package.py +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/scripts/fix-ruff.sh +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/scripts/pre-commit.sh +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/setup.cfg +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/src/dv_pipecat_flows.egg-info/dependency_links.txt +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/src/dv_pipecat_flows.egg-info/requires.txt +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/src/dv_pipecat_flows.egg-info/top_level.txt +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/src/pipecat_flows/__init__.py +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/src/pipecat_flows/exceptions.py +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/src/pipecat_flows/flow_validator.py +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/src/pipecat_flows/processors/__init__.py +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/src/pipecat_flows/processors/router_mode_guard.py +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/src/pipecat_flows/processors/speak_interruption_guard.py +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/src/pipecat_flows/processors/user_turn_observer.py +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/src/pipecat_flows/router_mode.py +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/src/pipecat_flows/types.py +0 -0
- {dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/uv.lock +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dv-pipecat-flows
|
|
3
|
-
Version: 0.0.22.
|
|
3
|
+
Version: 0.0.22.dev1700
|
|
4
4
|
Summary: Conversation Flow management for Pipecat AI applications
|
|
5
5
|
License: BSD 2-Clause License
|
|
6
6
|
Project-URL: Source, https://github.com/pipecat-ai/pipecat-flows
|
|
@@ -309,4 +309,3 @@ We aim to review all contributions promptly and provide constructive feedback to
|
|
|
309
309
|
➡️ [Reach us on X](https://x.com/pipecat_ai)
|
|
310
310
|
|
|
311
311
|
|
|
312
|
-
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dv-pipecat-flows
|
|
3
|
-
Version: 0.0.22.
|
|
3
|
+
Version: 0.0.22.dev1700
|
|
4
4
|
Summary: Conversation Flow management for Pipecat AI applications
|
|
5
5
|
License: BSD 2-Clause License
|
|
6
6
|
Project-URL: Source, https://github.com/pipecat-ai/pipecat-flows
|
|
@@ -309,4 +309,3 @@ We aim to review all contributions promptly and provide constructive feedback to
|
|
|
309
309
|
➡️ [Reach us on X](https://x.com/pipecat_ai)
|
|
310
310
|
|
|
311
311
|
|
|
312
|
-
|
|
@@ -60,31 +60,6 @@ editor/js/utils/import.js
|
|
|
60
60
|
editor/js/utils/validation.js
|
|
61
61
|
editor/public/favicon.png
|
|
62
62
|
editor/public/favicon.svg
|
|
63
|
-
evals/kb/.gitignore
|
|
64
|
-
evals/kb/README.md
|
|
65
|
-
evals/kb/RESULTS.md
|
|
66
|
-
evals/kb/UNIFIED_KB_BENCHMARKS.md
|
|
67
|
-
evals/kb/chunkers.py
|
|
68
|
-
evals/kb/contracts.py
|
|
69
|
-
evals/kb/datagen/generate_corpus.py
|
|
70
|
-
evals/kb/datagen/generate_dataset.py
|
|
71
|
-
evals/kb/datagen/generate_hybrid.py
|
|
72
|
-
evals/kb/datagen/templates/insurance.yaml
|
|
73
|
-
evals/kb/datasets/.gitignore
|
|
74
|
-
evals/kb/datasets/v1_hybrid/manifest.json
|
|
75
|
-
evals/kb/datasets/v1_retrieval/manifest.json
|
|
76
|
-
evals/kb/datasets/v1_retrieval_long/manifest.json
|
|
77
|
-
evals/kb/datasets/v1_synthetic/manifest.json
|
|
78
|
-
evals/kb/runners/run_filter_pick.py
|
|
79
|
-
evals/kb/runners/run_filter_scaling.py
|
|
80
|
-
evals/kb/runners/run_hybrid.py
|
|
81
|
-
evals/kb/runners/run_latency.py
|
|
82
|
-
evals/kb/runners/run_rerank.py
|
|
83
|
-
evals/kb/runners/run_retrieval.py
|
|
84
|
-
evals/kb/service/API.md
|
|
85
|
-
evals/kb/service/__init__.py
|
|
86
|
-
evals/kb/service/app.py
|
|
87
|
-
evals/kb/service/jobs.py
|
|
88
63
|
images/food-ordering-flow.png
|
|
89
64
|
remote-asterisk-code/README.md
|
|
90
65
|
remote-asterisk-code/extensions.conf
|
{dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/src/pipecat_flows/actions.py
RENAMED
|
@@ -26,13 +26,14 @@ Actions are used to perform side effects during conversations, such as:
|
|
|
26
26
|
import asyncio
|
|
27
27
|
import inspect
|
|
28
28
|
from dataclasses import dataclass
|
|
29
|
-
from typing import TYPE_CHECKING, Callable, Dict, List, Optional
|
|
29
|
+
from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple
|
|
30
30
|
|
|
31
31
|
from loguru import logger
|
|
32
32
|
from pipecat.frames.frames import (
|
|
33
33
|
BotStoppedSpeakingFrame,
|
|
34
34
|
ControlFrame,
|
|
35
35
|
EndFrame,
|
|
36
|
+
InterruptionFrame,
|
|
36
37
|
ManuallySwitchServiceFrame,
|
|
37
38
|
TTSSpeakFrame,
|
|
38
39
|
)
|
|
@@ -97,6 +98,14 @@ class ActionManager:
|
|
|
97
98
|
self._ongoing_actions_count = 0
|
|
98
99
|
self._ongoing_actions_finished_event = asyncio.Event()
|
|
99
100
|
self._deferred_post_actions: List[ActionConfig] = []
|
|
101
|
+
# Tracks whether an InterruptionFrame has been observed since the
|
|
102
|
+
# last action increment. The BotStoppedSpeakingFrame "interruption
|
|
103
|
+
# recovery" branch must only fire when a real interruption actually
|
|
104
|
+
# happened — otherwise a stale BotStoppedSpeakingFrame from a prior
|
|
105
|
+
# speech (e.g. cached intro completing while the first Speak node
|
|
106
|
+
# is already starting) would zero a legitimate in-flight count and
|
|
107
|
+
# release _ongoing_actions_finished_event prematurely.
|
|
108
|
+
self._seen_interruption_since_increment = False
|
|
100
109
|
|
|
101
110
|
# Register built-in actions
|
|
102
111
|
self._register_action("tts_say", self._handle_tts_action)
|
|
@@ -105,7 +114,12 @@ class ActionManager:
|
|
|
105
114
|
|
|
106
115
|
# Add pipeline observation
|
|
107
116
|
task.set_reached_downstream_filter(
|
|
108
|
-
(
|
|
117
|
+
(
|
|
118
|
+
ActionFinishedFrame,
|
|
119
|
+
FunctionActionFrame,
|
|
120
|
+
BotStoppedSpeakingFrame,
|
|
121
|
+
InterruptionFrame,
|
|
122
|
+
)
|
|
109
123
|
)
|
|
110
124
|
|
|
111
125
|
@task.event_handler("on_frame_reached_downstream")
|
|
@@ -147,16 +161,39 @@ class ActionManager:
|
|
|
147
161
|
# Defensively reset on this frame so the engine doesn't
|
|
148
162
|
# deadlock; skip the deferred post-actions since the action
|
|
149
163
|
# didn't complete naturally.
|
|
150
|
-
|
|
164
|
+
#
|
|
165
|
+
# Recovery is gated on `_seen_interruption_since_increment`:
|
|
166
|
+
# count>0 alone is ambiguous — a stale BotStoppedSpeakingFrame
|
|
167
|
+
# from a prior speech (e.g. cached intro completing after a
|
|
168
|
+
# new Speak's increment) would otherwise zero a legitimate
|
|
169
|
+
# in-flight count and unblock the next _set_node early.
|
|
170
|
+
if (
|
|
171
|
+
self._ongoing_actions_count > 0
|
|
172
|
+
and self._seen_interruption_since_increment
|
|
173
|
+
):
|
|
151
174
|
logger.debug(
|
|
152
175
|
f"BotStoppedSpeaking with in-flight count="
|
|
153
|
-
f"{self._ongoing_actions_count}
|
|
176
|
+
f"{self._ongoing_actions_count} after InterruptionFrame; "
|
|
154
177
|
f"resetting to release _ongoing_actions_finished_event"
|
|
155
178
|
)
|
|
156
179
|
self._ongoing_actions_count = 0
|
|
157
180
|
self._ongoing_actions_finished_event.set()
|
|
181
|
+
self._seen_interruption_since_increment = False
|
|
182
|
+
elif self._ongoing_actions_count > 0:
|
|
183
|
+
# Stale frame from prior speech. Ignore — the real
|
|
184
|
+
# in-flight action's ActionFinishedFrame is still coming.
|
|
185
|
+
logger.debug(
|
|
186
|
+
f"BotStoppedSpeaking with in-flight count="
|
|
187
|
+
f"{self._ongoing_actions_count} but no InterruptionFrame "
|
|
188
|
+
f"observed since last increment; treating as stale, "
|
|
189
|
+
f"keeping counter intact"
|
|
190
|
+
)
|
|
158
191
|
else:
|
|
159
192
|
await self._execute_deferred_post_actions()
|
|
193
|
+
elif isinstance(frame, InterruptionFrame):
|
|
194
|
+
# Mark — the very next BotStoppedSpeakingFrame is the truncated
|
|
195
|
+
# speech's stop and should trigger the recovery branch above.
|
|
196
|
+
self._seen_interruption_since_increment = True
|
|
160
197
|
elif isinstance(frame, ActionFinishedFrame):
|
|
161
198
|
# Handle action finished
|
|
162
199
|
self._decrement_ongoing_actions_count()
|
|
@@ -368,7 +405,7 @@ class ActionManager:
|
|
|
368
405
|
To prevent the LLM from immediately responding after TTS in pre-actions,
|
|
369
406
|
set ``respond_immediately=false`` in the node configuration.
|
|
370
407
|
"""
|
|
371
|
-
text = self._pick_tts_variant(action)
|
|
408
|
+
text, text_raw, chosen_lang = self._pick_tts_variant(action)
|
|
372
409
|
if not text:
|
|
373
410
|
logger.error("TTS action missing 'text' / 'text_variants' field")
|
|
374
411
|
return
|
|
@@ -387,8 +424,17 @@ class ActionManager:
|
|
|
387
424
|
served_from_cache = False
|
|
388
425
|
if cache_handler is not None:
|
|
389
426
|
try:
|
|
390
|
-
|
|
391
|
-
|
|
427
|
+
# Fall back to text when no raw template was emitted —
|
|
428
|
+
# cache key derivation will treat the rendered text as
|
|
429
|
+
# its own template (lower hit rate but always correct).
|
|
430
|
+
effective_raw = text_raw or text
|
|
431
|
+
served_from_cache = bool(
|
|
432
|
+
await cache_handler(
|
|
433
|
+
text=text,
|
|
434
|
+
text_raw=effective_raw,
|
|
435
|
+
language=chosen_lang,
|
|
436
|
+
)
|
|
437
|
+
)
|
|
392
438
|
except Exception as e:
|
|
393
439
|
logger.error(f"tts_say cache handler raised, falling back to live TTS: {e}")
|
|
394
440
|
served_from_cache = False
|
|
@@ -408,24 +454,35 @@ class ActionManager:
|
|
|
408
454
|
self._decrement_ongoing_actions_count()
|
|
409
455
|
logger.error(f"TTS error: {e}")
|
|
410
456
|
|
|
411
|
-
def _pick_tts_variant(
|
|
412
|
-
|
|
457
|
+
def _pick_tts_variant(
|
|
458
|
+
self, action: dict
|
|
459
|
+
) -> Tuple[Optional[str], Optional[str], Optional[str]]:
|
|
460
|
+
"""Pick a variation string from ``text_variants``.
|
|
461
|
+
|
|
462
|
+
Returns ``(text, text_raw, chosen_lang)``:
|
|
463
|
+
* ``text`` — the picked variant (rendered string), or ``action["text"]``
|
|
464
|
+
when no variants are usable.
|
|
465
|
+
* ``text_raw`` — the matching unrendered template (with ``{{var}}``
|
|
466
|
+
markers intact) from ``action["text_raw_variants"][chosen_lang][idx]``
|
|
467
|
+
when present, else ``action["text_raw"]``, else mirrors ``text``.
|
|
468
|
+
Used by the split-cache to derive the stable prefix cache key.
|
|
469
|
+
* ``chosen_lang`` — the ISO key that won, or None when the legacy
|
|
470
|
+
``text`` fallback was taken. Threaded down to the cache_handler
|
|
471
|
+
so the cache key can be language-scoped (different scripts must
|
|
472
|
+
not share a cache slot).
|
|
413
473
|
|
|
414
474
|
Resolution order for the language key:
|
|
415
|
-
1. ``state["language"]`` (
|
|
416
|
-
auto-injected language extract).
|
|
475
|
+
1. ``state["language"]`` (set by Router-LLM auto-injected extract).
|
|
417
476
|
2. ``action["fallback_language"]`` (the Speak's primary ISO).
|
|
418
477
|
3. First key in ``text_variants``.
|
|
419
478
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
every variant index is picked at most once until the set has been
|
|
424
|
-
exhausted, at which point it resets and the cycle restarts.
|
|
479
|
+
Cycling state lives on :attr:`FlowManager._variant_usage` keyed by
|
|
480
|
+
``(speak_node_id, lang)`` — every variant index is picked at most
|
|
481
|
+
once until the set has been exhausted, at which point it resets.
|
|
425
482
|
"""
|
|
426
483
|
variants = action.get("text_variants")
|
|
427
484
|
if not isinstance(variants, dict) or not variants:
|
|
428
|
-
return action.get("text")
|
|
485
|
+
return action.get("text"), action.get("text_raw"), None
|
|
429
486
|
|
|
430
487
|
active = (self._flow_manager.state or {}).get("language") if isinstance(
|
|
431
488
|
self._flow_manager.state, dict
|
|
@@ -454,7 +511,19 @@ class ActionManager:
|
|
|
454
511
|
if not chosen_list:
|
|
455
512
|
# Variants dict is malformed (all keys empty); fall back to the
|
|
456
513
|
# forward-compat `text` field if the lowerer emitted one.
|
|
457
|
-
return action.get("text")
|
|
514
|
+
return action.get("text"), action.get("text_raw"), None
|
|
515
|
+
|
|
516
|
+
# Look up the parallel raw-template list emitted by the backend for
|
|
517
|
+
# this language. Same length as `chosen_list` at lowering time; if
|
|
518
|
+
# absent or short, the caller falls back to chosen_list itself
|
|
519
|
+
# (cache derivation will treat the rendered text as its own
|
|
520
|
+
# template — cache MAY miss but won't garble).
|
|
521
|
+
raw_variants = action.get("text_raw_variants")
|
|
522
|
+
raw_list: List[str] = []
|
|
523
|
+
if isinstance(raw_variants, dict):
|
|
524
|
+
cand_raw = raw_variants.get(chosen_lang or "")
|
|
525
|
+
if isinstance(cand_raw, list):
|
|
526
|
+
raw_list = [s for s in cand_raw if isinstance(s, str)]
|
|
458
527
|
|
|
459
528
|
speak_id = (
|
|
460
529
|
getattr(self._flow_manager, "_current_node", None)
|
|
@@ -465,6 +534,7 @@ class ActionManager:
|
|
|
465
534
|
usage = self._flow_manager._variant_usage
|
|
466
535
|
used = usage.get(usage_key, set())
|
|
467
536
|
# Pick the smallest index not yet used; reset the set when full.
|
|
537
|
+
picked_idx = 0
|
|
468
538
|
for idx in range(len(chosen_list)):
|
|
469
539
|
if idx not in used:
|
|
470
540
|
used = set(used) | {idx}
|
|
@@ -472,11 +542,20 @@ class ActionManager:
|
|
|
472
542
|
usage[usage_key] = set() # cycle complete; reset for next round
|
|
473
543
|
else:
|
|
474
544
|
usage[usage_key] = used
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
545
|
+
picked_idx = idx
|
|
546
|
+
break
|
|
547
|
+
else:
|
|
548
|
+
# Degenerate: usage was already saturated (race) — reset and pick 0.
|
|
549
|
+
usage[usage_key] = {0}
|
|
550
|
+
picked_idx = 0
|
|
551
|
+
|
|
552
|
+
picked_text = chosen_list[picked_idx]
|
|
553
|
+
picked_raw = (
|
|
554
|
+
raw_list[picked_idx]
|
|
555
|
+
if 0 <= picked_idx < len(raw_list)
|
|
556
|
+
else picked_text
|
|
557
|
+
)
|
|
558
|
+
return picked_text, picked_raw, chosen_lang
|
|
480
559
|
|
|
481
560
|
async def _maybe_switch_tts_service_for_action(self, action: dict) -> None:
|
|
482
561
|
"""Queue a :class:`ManuallySwitchServiceFrame` when the action carries
|
|
@@ -571,6 +650,9 @@ class ActionManager:
|
|
|
571
650
|
self._ongoing_actions_count += 1
|
|
572
651
|
if self._ongoing_actions_count == 1:
|
|
573
652
|
self._ongoing_actions_finished_event.clear()
|
|
653
|
+
# Reset the interruption flag — any prior InterruptionFrame applied to
|
|
654
|
+
# the previous speech, not this new one.
|
|
655
|
+
self._seen_interruption_since_increment = False
|
|
574
656
|
|
|
575
657
|
def _decrement_ongoing_actions_count(self) -> None:
|
|
576
658
|
"""Decrement the count of ongoing actions and set the finished event if this was the last action."""
|
|
@@ -590,3 +672,4 @@ class ActionManager:
|
|
|
590
672
|
)
|
|
591
673
|
self._ongoing_actions_count = 0
|
|
592
674
|
self._ongoing_actions_finished_event.set()
|
|
675
|
+
self._seen_interruption_since_increment = False
|
{dv_pipecat_flows-0.0.22.dev1516 → dv_pipecat_flows-0.0.22.dev1700}/src/pipecat_flows/adapters.py
RENAMED
|
@@ -283,6 +283,138 @@ class UniversalLLMAdapter(LLMAdapter):
|
|
|
283
283
|
be in the standard FlowsFunctionSchema format.
|
|
284
284
|
"""
|
|
285
285
|
|
|
286
|
+
async def run_router_completion(
|
|
287
|
+
self,
|
|
288
|
+
llm_service: Any,
|
|
289
|
+
system_prompt: str,
|
|
290
|
+
response_schema: Dict[str, Any],
|
|
291
|
+
context_messages: List[Dict[str, Any]],
|
|
292
|
+
*,
|
|
293
|
+
schema_name: str = "router_decision",
|
|
294
|
+
) -> Optional[str]:
|
|
295
|
+
"""Constrained completion for services using the universal LLMContext
|
|
296
|
+
(currently: ``OpenAIResponsesLLMService``).
|
|
297
|
+
|
|
298
|
+
Dispatches by underlying client surface:
|
|
299
|
+
* ``_client.responses`` (OpenAI Responses API) → ``responses.create``
|
|
300
|
+
with ``text.format = {type: json_schema, ...}``
|
|
301
|
+
* ``_client.chat.completions`` (any service exposing OpenAI's
|
|
302
|
+
chat-completions client — e.g. Azure when routed through universal
|
|
303
|
+
context) → falls back to OpenAI-style ``json_schema`` path.
|
|
304
|
+
|
|
305
|
+
Returns the raw JSON string, or None on failure.
|
|
306
|
+
"""
|
|
307
|
+
if not hasattr(llm_service, "_client"):
|
|
308
|
+
logger.warning(
|
|
309
|
+
"Universal router completion: llm_service has no _client; "
|
|
310
|
+
"cannot make direct completion call"
|
|
311
|
+
)
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
model_name = (
|
|
315
|
+
getattr(llm_service, "model_name", None)
|
|
316
|
+
or getattr(llm_service, "_model_name", None)
|
|
317
|
+
or getattr(llm_service, "_model", None)
|
|
318
|
+
)
|
|
319
|
+
if not model_name:
|
|
320
|
+
logger.warning(
|
|
321
|
+
f"Universal router completion: could not resolve model_name on "
|
|
322
|
+
f"{type(llm_service).__name__}; falling back to None."
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
client = llm_service._client
|
|
326
|
+
|
|
327
|
+
# Flatten context to a single input string for the Responses API. The
|
|
328
|
+
# Responses surface accepts a string for `input`; system_prompt goes
|
|
329
|
+
# into `instructions` separately so the model treats it as the
|
|
330
|
+
# authoritative directive (not just another user turn).
|
|
331
|
+
context_text = ""
|
|
332
|
+
if context_messages:
|
|
333
|
+
lines = []
|
|
334
|
+
for msg in context_messages:
|
|
335
|
+
role = msg.get("role", "user")
|
|
336
|
+
content = msg.get("content", "")
|
|
337
|
+
if isinstance(content, list):
|
|
338
|
+
content = "".join(
|
|
339
|
+
p.get("text", "") if isinstance(p, dict) else str(p)
|
|
340
|
+
for p in content
|
|
341
|
+
)
|
|
342
|
+
lines.append(f"{role}: {content}")
|
|
343
|
+
context_text = "\n".join(lines)
|
|
344
|
+
|
|
345
|
+
if hasattr(client, "responses"):
|
|
346
|
+
try:
|
|
347
|
+
response = await client.responses.create(
|
|
348
|
+
model=model_name,
|
|
349
|
+
input=context_text or " ",
|
|
350
|
+
instructions=system_prompt,
|
|
351
|
+
stream=False,
|
|
352
|
+
text={
|
|
353
|
+
"format": {
|
|
354
|
+
"type": "json_schema",
|
|
355
|
+
"name": schema_name,
|
|
356
|
+
"schema": response_schema,
|
|
357
|
+
"strict": True,
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
)
|
|
361
|
+
return getattr(response, "output_text", None)
|
|
362
|
+
except Exception as exc: # noqa: BLE001
|
|
363
|
+
logger.error(
|
|
364
|
+
f"Universal (Responses API) router completion failed: {exc!r}; "
|
|
365
|
+
f"falling back to json_object"
|
|
366
|
+
)
|
|
367
|
+
try:
|
|
368
|
+
response = await client.responses.create(
|
|
369
|
+
model=model_name,
|
|
370
|
+
input=context_text or " ",
|
|
371
|
+
instructions=system_prompt,
|
|
372
|
+
stream=False,
|
|
373
|
+
text={"format": {"type": "json_object"}},
|
|
374
|
+
)
|
|
375
|
+
return getattr(response, "output_text", None)
|
|
376
|
+
except Exception as exc2: # noqa: BLE001
|
|
377
|
+
logger.error(
|
|
378
|
+
f"Universal Responses fallback also failed: {exc2!r}"
|
|
379
|
+
)
|
|
380
|
+
return None
|
|
381
|
+
|
|
382
|
+
if hasattr(client, "chat") and hasattr(client.chat, "completions"):
|
|
383
|
+
# Service exposes the OpenAI chat-completions surface even though
|
|
384
|
+
# universal LLMContext is in use. Reuse the OpenAI-style path.
|
|
385
|
+
messages: List[Dict[str, Any]] = [
|
|
386
|
+
{"role": "system", "content": system_prompt}
|
|
387
|
+
]
|
|
388
|
+
if context_messages:
|
|
389
|
+
messages.extend(context_messages)
|
|
390
|
+
try:
|
|
391
|
+
response = await client.chat.completions.create(
|
|
392
|
+
model=model_name,
|
|
393
|
+
messages=messages,
|
|
394
|
+
temperature=0.1,
|
|
395
|
+
response_format={
|
|
396
|
+
"type": "json_schema",
|
|
397
|
+
"json_schema": {
|
|
398
|
+
"name": schema_name,
|
|
399
|
+
"schema": response_schema,
|
|
400
|
+
"strict": True,
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
)
|
|
404
|
+
return response.choices[0].message.content
|
|
405
|
+
except Exception as exc: # noqa: BLE001
|
|
406
|
+
logger.error(
|
|
407
|
+
f"Universal (chat-completions) router completion failed: {exc!r}"
|
|
408
|
+
)
|
|
409
|
+
return None
|
|
410
|
+
|
|
411
|
+
logger.warning(
|
|
412
|
+
f"Universal router completion: llm_service "
|
|
413
|
+
f"{type(llm_service).__name__} exposes neither responses nor "
|
|
414
|
+
f"chat.completions on its _client; no compatible structured-output path."
|
|
415
|
+
)
|
|
416
|
+
return None
|
|
417
|
+
|
|
286
418
|
def _get_function_name_from_dict(self, function_def: Dict[str, Any]) -> str:
|
|
287
419
|
raise RuntimeError(
|
|
288
420
|
"Provider-specific function definitions are not supported in flows using universal LLMContext. Use FlowsFunctionSchemas or direct functions instead."
|
|
@@ -591,6 +723,119 @@ class GeminiAdapter(LLMAdapter):
|
|
|
591
723
|
and Gemini's as needed.
|
|
592
724
|
"""
|
|
593
725
|
|
|
726
|
+
async def run_router_completion(
|
|
727
|
+
self,
|
|
728
|
+
llm_service: Any,
|
|
729
|
+
system_prompt: str,
|
|
730
|
+
response_schema: Dict[str, Any],
|
|
731
|
+
context_messages: List[Dict[str, Any]],
|
|
732
|
+
*,
|
|
733
|
+
schema_name: str = "router_decision",
|
|
734
|
+
) -> Optional[str]:
|
|
735
|
+
"""Gemini constrained completion via ``response_schema`` +
|
|
736
|
+
``response_mime_type='application/json'``.
|
|
737
|
+
|
|
738
|
+
Accesses ``llm_service._client`` (google-genai SDK) and
|
|
739
|
+
``llm_service._model_name``, makes a one-shot
|
|
740
|
+
``generate_content`` outside the streaming pipeline. Returns the
|
|
741
|
+
raw JSON string emitted by the model.
|
|
742
|
+
|
|
743
|
+
Falls back to plain JSON mode (no schema) if the strict
|
|
744
|
+
``response_schema`` path fails — Gemini's schema validator rejects
|
|
745
|
+
certain JSON Schema features that the universal router schema may
|
|
746
|
+
emit (e.g. nested ``oneOf`` / ``anyOf``).
|
|
747
|
+
"""
|
|
748
|
+
if not hasattr(llm_service, "_client"):
|
|
749
|
+
logger.warning(
|
|
750
|
+
"Gemini router completion: llm_service has no _client; "
|
|
751
|
+
"cannot make direct completion call"
|
|
752
|
+
)
|
|
753
|
+
return None
|
|
754
|
+
|
|
755
|
+
model_name = (
|
|
756
|
+
getattr(llm_service, "model_name", None)
|
|
757
|
+
or getattr(llm_service, "_model_name", None)
|
|
758
|
+
or getattr(llm_service, "_model", None)
|
|
759
|
+
)
|
|
760
|
+
if not model_name:
|
|
761
|
+
logger.warning(
|
|
762
|
+
f"Gemini router completion: could not resolve model_name on "
|
|
763
|
+
f"{type(llm_service).__name__}; falling back to None."
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
# google-genai's GenerateContentConfig only ships with the optional
|
|
767
|
+
# `google` extra. Import lazily so non-Gemini deploys aren't forced
|
|
768
|
+
# to install google-genai.
|
|
769
|
+
try:
|
|
770
|
+
from google.genai.types import GenerateContentConfig
|
|
771
|
+
except ImportError:
|
|
772
|
+
logger.error(
|
|
773
|
+
"Gemini router completion: google-genai not installed; "
|
|
774
|
+
"install pipecat-ai[google] to use Router-LLM with Gemini."
|
|
775
|
+
)
|
|
776
|
+
return None
|
|
777
|
+
|
|
778
|
+
# Flatten context into a single user-turn string. Gemini's `contents`
|
|
779
|
+
# accepts Content objects with role+parts, but for a one-shot
|
|
780
|
+
# constrained completion a flat prompt is simpler and avoids the
|
|
781
|
+
# role-alternation requirement (system_instruction handles the system
|
|
782
|
+
# prompt separately).
|
|
783
|
+
context_text = ""
|
|
784
|
+
if context_messages:
|
|
785
|
+
lines = []
|
|
786
|
+
for msg in context_messages:
|
|
787
|
+
role = msg.get("role", "user")
|
|
788
|
+
content = msg.get("content", "")
|
|
789
|
+
if isinstance(content, list):
|
|
790
|
+
content = "".join(
|
|
791
|
+
p.get("text", "") if isinstance(p, dict) else str(p)
|
|
792
|
+
for p in content
|
|
793
|
+
)
|
|
794
|
+
lines.append(f"{role}: {content}")
|
|
795
|
+
context_text = "\n".join(lines)
|
|
796
|
+
|
|
797
|
+
try:
|
|
798
|
+
config = GenerateContentConfig(
|
|
799
|
+
system_instruction=system_prompt,
|
|
800
|
+
response_mime_type="application/json",
|
|
801
|
+
response_schema=response_schema,
|
|
802
|
+
temperature=0.1,
|
|
803
|
+
)
|
|
804
|
+
response = await llm_service._client.aio.models.generate_content(
|
|
805
|
+
model=model_name,
|
|
806
|
+
contents=context_text or " ",
|
|
807
|
+
config=config,
|
|
808
|
+
)
|
|
809
|
+
if response.candidates and response.candidates[0].content:
|
|
810
|
+
for part in response.candidates[0].content.parts:
|
|
811
|
+
if part.text:
|
|
812
|
+
return part.text
|
|
813
|
+
return None
|
|
814
|
+
except Exception as exc: # noqa: BLE001
|
|
815
|
+
logger.error(
|
|
816
|
+
f"Gemini router completion failed: {exc!r}; "
|
|
817
|
+
f"falling back to JSON mode without schema"
|
|
818
|
+
)
|
|
819
|
+
try:
|
|
820
|
+
config = GenerateContentConfig(
|
|
821
|
+
system_instruction=system_prompt,
|
|
822
|
+
response_mime_type="application/json",
|
|
823
|
+
temperature=0.1,
|
|
824
|
+
)
|
|
825
|
+
response = await llm_service._client.aio.models.generate_content(
|
|
826
|
+
model=model_name,
|
|
827
|
+
contents=context_text or " ",
|
|
828
|
+
config=config,
|
|
829
|
+
)
|
|
830
|
+
if response.candidates and response.candidates[0].content:
|
|
831
|
+
for part in response.candidates[0].content.parts:
|
|
832
|
+
if part.text:
|
|
833
|
+
return part.text
|
|
834
|
+
return None
|
|
835
|
+
except Exception as exc2: # noqa: BLE001
|
|
836
|
+
logger.error(f"Gemini router completion fallback also failed: {exc2!r}")
|
|
837
|
+
return None
|
|
838
|
+
|
|
594
839
|
def _get_function_name_from_dict(self, function_def: Dict[str, Any]) -> str:
|
|
595
840
|
"""Extract function name from Gemini function definition.
|
|
596
841
|
|
|
@@ -25,6 +25,7 @@ Operator vocabulary (matches the FE's ConditionOperator set):
|
|
|
25
25
|
|
|
26
26
|
Variable reference forms (matches the rest of the codebase):
|
|
27
27
|
``{{name}}`` -> custom/trigger variable, looked up in ``state``
|
|
28
|
+
``[{name}]`` -> Router/Agent-extracted variable, looked up in ``state``
|
|
28
29
|
``((tool.field))`` -> pre-call tool output, looked up in ``tool_outputs``
|
|
29
30
|
(dotted path; first segment is the tool name,
|
|
30
31
|
remainder is jsonpath through its response)
|
|
@@ -59,6 +60,10 @@ _VALID_OPERATORS = frozenset({
|
|
|
59
60
|
|
|
60
61
|
_BRACED_VAR_RE = re.compile(r"^\s*\{\{\s*([A-Za-z_][A-Za-z0-9_.]*)\s*\}\}\s*$")
|
|
61
62
|
_PAREN_VAR_RE = re.compile(r"^\s*\(\(\s*([A-Za-z_][A-Za-z0-9_.]*)\s*\)\)\s*$")
|
|
63
|
+
# ``[{name}]`` is the dedicated Router/Agent-extracted-variable token (kept
|
|
64
|
+
# distinct from custom ``{{name}}`` on the FE). Resolves from ``state`` exactly
|
|
65
|
+
# like the braced and bare forms.
|
|
66
|
+
_BRACKET_BRACE_VAR_RE = re.compile(r"^\s*\[\{\s*([A-Za-z_][A-Za-z0-9_.]*)\s*\}\]\s*$")
|
|
62
67
|
|
|
63
68
|
|
|
64
69
|
def _walk_dotted(obj: Any, path: List[str]) -> Any:
|
|
@@ -119,6 +124,10 @@ def _resolve_variable(
|
|
|
119
124
|
return None
|
|
120
125
|
return _walk_dotted(tool_outputs, paren.group(1).split("."))
|
|
121
126
|
|
|
127
|
+
bracket_brace = _BRACKET_BRACE_VAR_RE.match(ref)
|
|
128
|
+
if bracket_brace:
|
|
129
|
+
return _walk_dotted(state, bracket_brace.group(1).split("."))
|
|
130
|
+
|
|
122
131
|
# Bare identifier — treat as state lookup, allowing dotted paths.
|
|
123
132
|
return _walk_dotted(state, ref.strip().split("."))
|
|
124
133
|
|