dv-pipecat-flows 0.0.0.dev2072__tar.gz → 0.0.0.dev2093__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.0.dev2072/src/dv_pipecat_flows.egg-info → dv_pipecat_flows-0.0.0.dev2093}/PKG-INFO +1 -1
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093/src/dv_pipecat_flows.egg-info}/PKG-INFO +1 -1
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/src/dv_pipecat_flows.egg-info/SOURCES.txt +0 -1
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/actions.py +29 -10
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/adapters.py +4 -5
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/manager.py +80 -85
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/router_mode.py +4 -85
- dv_pipecat_flows-0.0.0.dev2072/src/pipecat_flows/data_extractor.py +0 -249
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/.agents/skills/loki-logs/SKILL.md +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/.agents/skills/loki-logs/query-reference.md +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/.claude/skills/loki-logs/SKILL.md +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/.claude/skills/loki-logs/query-reference.md +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/.gitattributes +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/.gitignore +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/.pre-commit-config.yaml +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/.python-version +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/.readthedocs.yaml +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/AGENTS.md +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/CHANGELOG.md +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/CLAUDE.md +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/CONTRIBUTING.md +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/LICENSE +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/MANIFEST.in +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/README.md +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/dev-requirements.txt +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/docker-compose.dev.yml +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/.eslintrc.json +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/.prettierrc +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/css/tailwind.css +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/examples/food_ordering.json +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/examples/movie_explorer.json +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/examples/patient_intake.json +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/examples/restaurant_reservation.json +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/examples/travel_planner.json +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/favicon.png +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/favicon.svg +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/index.html +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/editor/canvas.js +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/editor/editorState.js +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/editor/sidePanel.js +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/editor/toolbar.js +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/main.js +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/nodes/baseNode.js +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/nodes/endNode.js +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/nodes/flowNode.js +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/nodes/functionNode.js +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/nodes/index.js +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/nodes/mergeNode.js +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/nodes/startNode.js +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/types.js +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/utils/export.js +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/utils/helpers.js +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/utils/import.js +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/utils/validation.js +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/jsdoc.json +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/package-lock.json +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/package.json +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/postcss.config.cjs +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/public/favicon.png +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/public/favicon.svg +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/tailwind.config.cjs +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/vercel.json +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/vite.config.js +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/engine_primitives_plan.md +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/env.example +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/.gitignore +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/README.md +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/RESULTS.md +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/UNIFIED_KB_BENCHMARKS.md +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/chunkers.py +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/contracts.py +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/datagen/generate_corpus.py +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/datagen/generate_dataset.py +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/datagen/generate_hybrid.py +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/datagen/templates/insurance.yaml +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/datasets/.gitignore +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/datasets/v1_hybrid/manifest.json +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/datasets/v1_retrieval/manifest.json +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/datasets/v1_retrieval_long/manifest.json +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/datasets/v1_synthetic/manifest.json +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/runners/run_filter_pick.py +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/runners/run_filter_scaling.py +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/runners/run_hybrid.py +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/runners/run_latency.py +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/runners/run_rerank.py +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/runners/run_retrieval.py +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/service/API.md +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/service/__init__.py +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/service/app.py +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/service/jobs.py +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/images/food-ordering-flow.png +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/pipecat-flows.png +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/pipecat_upgrade.md +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/pyproject.toml +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/remote-asterisk-code/README.md +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/remote-asterisk-code/extensions.conf +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/remote-asterisk-code/rtp.conf +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/requirements.txt +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/scripts/check-pypi-package.py +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/scripts/fix-ruff.sh +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/scripts/pre-commit.sh +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/setup.cfg +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/src/dv_pipecat_flows.egg-info/dependency_links.txt +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/src/dv_pipecat_flows.egg-info/requires.txt +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/src/dv_pipecat_flows.egg-info/top_level.txt +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/__init__.py +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/condition_evaluator.py +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/exceptions.py +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/flow_validator.py +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/processors/__init__.py +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/processors/router_mode_guard.py +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/processors/speak_interruption_guard.py +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/processors/user_turn_observer.py +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/types.py +0 -0
- {dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/uv.lock +0 -0
|
@@ -101,7 +101,6 @@ src/pipecat_flows/__init__.py
|
|
|
101
101
|
src/pipecat_flows/actions.py
|
|
102
102
|
src/pipecat_flows/adapters.py
|
|
103
103
|
src/pipecat_flows/condition_evaluator.py
|
|
104
|
-
src/pipecat_flows/data_extractor.py
|
|
105
104
|
src/pipecat_flows/exceptions.py
|
|
106
105
|
src/pipecat_flows/flow_validator.py
|
|
107
106
|
src/pipecat_flows/manager.py
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/actions.py
RENAMED
|
@@ -312,11 +312,16 @@ class ActionManager:
|
|
|
312
312
|
|
|
313
313
|
Args:
|
|
314
314
|
action: Action configuration containing:
|
|
315
|
-
- text: Text to speak (required)
|
|
315
|
+
- text: Text to speak (required, resolved)
|
|
316
|
+
- text_raw: Raw template with {{var}} markers (optional)
|
|
316
317
|
- use_cache: Use caching for TTS audio (optional, default: False)
|
|
317
318
|
|
|
318
|
-
|
|
319
|
-
flow_manager.state['say_with_cache_handler']
|
|
319
|
+
Caching is opt-in via dependency injection: when use_cache=True and
|
|
320
|
+
flow_manager.state['say_with_cache_handler'] is set, the handler is
|
|
321
|
+
invoked as `await handler(text=..., text_raw=...) -> bool`. Returning
|
|
322
|
+
True means the handler served the audio downstream (and pushed an
|
|
323
|
+
ActionFinishedFrame downstream itself). Returning False (or raising)
|
|
324
|
+
falls back to live TTS via TTSSpeakFrame.
|
|
320
325
|
|
|
321
326
|
To prevent the LLM from immediately responding after TTS in pre-actions,
|
|
322
327
|
set respond_immediately=false in the node configuration.
|
|
@@ -327,18 +332,32 @@ class ActionManager:
|
|
|
327
332
|
return
|
|
328
333
|
|
|
329
334
|
use_cache = action.get("use_cache", False)
|
|
335
|
+
cache_handler = (
|
|
336
|
+
self._flow_manager.state.get("say_with_cache_handler") if use_cache else None
|
|
337
|
+
)
|
|
330
338
|
|
|
331
339
|
try:
|
|
332
|
-
# For now, always use the standard TTS path to avoid pipeline issues
|
|
333
|
-
# Caching can be implemented at the TTS service level instead
|
|
334
|
-
|
|
335
340
|
# Mark that we're starting the action
|
|
336
341
|
self._increment_ongoing_actions_count()
|
|
337
342
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
343
|
+
served_from_cache = False
|
|
344
|
+
if cache_handler is not None:
|
|
345
|
+
try:
|
|
346
|
+
text_raw = action.get("text_raw") or text
|
|
347
|
+
served_from_cache = bool(await cache_handler(text=text, text_raw=text_raw))
|
|
348
|
+
except Exception as e:
|
|
349
|
+
logger.error(f"tts_say cache handler raised, falling back to live TTS: {e}")
|
|
350
|
+
served_from_cache = False
|
|
351
|
+
|
|
352
|
+
if not served_from_cache:
|
|
353
|
+
# Live TTS path
|
|
354
|
+
await self._task.queue_frame(TTSSpeakFrame(text=text))
|
|
355
|
+
|
|
356
|
+
# Queue frame marking the end of the action. Even on the cached
|
|
357
|
+
# path this is safe: the frame is queued at the source and has to
|
|
358
|
+
# traverse the entire pipeline to reach our downstream filter,
|
|
359
|
+
# while the cached audio was pushed downstream of the TTS service
|
|
360
|
+
# and reaches the wire sooner.
|
|
342
361
|
await self._task.queue_frame(ActionFinishedFrame())
|
|
343
362
|
|
|
344
363
|
except Exception as e:
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/adapters.py
RENAMED
|
@@ -176,9 +176,9 @@ class LLMAdapter:
|
|
|
176
176
|
|
|
177
177
|
Args:
|
|
178
178
|
llm_service: The underlying LLM service (exposes ``_client`` +
|
|
179
|
-
``_model
|
|
180
|
-
system_prompt: System message text (
|
|
181
|
-
|
|
179
|
+
``_model``).
|
|
180
|
+
system_prompt: System message text (authored by the backend
|
|
181
|
+
lowerer and shipped via ``task_messages[0].content``).
|
|
182
182
|
response_schema: JSON Schema constraining the output (built by
|
|
183
183
|
:func:`build_router_response_schema`).
|
|
184
184
|
context_messages: Recent conversation turns to include as user
|
|
@@ -374,8 +374,7 @@ class OpenAIAdapter(LLMAdapter):
|
|
|
374
374
|
"""OpenAI / Azure constrained completion via ``response_format``
|
|
375
375
|
``json_schema``. Returns the raw JSON string emitted by the model.
|
|
376
376
|
|
|
377
|
-
|
|
378
|
-
accesses ``llm_service._client`` and ``llm_service._model``, makes a
|
|
377
|
+
Accesses ``llm_service._client`` and ``llm_service._model``, makes a
|
|
379
378
|
one-shot ``chat.completions.create`` outside the streaming pipeline.
|
|
380
379
|
|
|
381
380
|
Uses ``strict: True`` so OpenAI enforces the schema server-side
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/manager.py
RENAMED
|
@@ -68,7 +68,6 @@ from pipecat.transports.base_transport import BaseTransport
|
|
|
68
68
|
from pipecat_flows.actions import ActionError, ActionManager
|
|
69
69
|
from pipecat_flows.adapters import create_adapter
|
|
70
70
|
from pipecat_flows.condition_evaluator import evaluate_conditions
|
|
71
|
-
from pipecat_flows.data_extractor import DataExtractor
|
|
72
71
|
from pipecat_flows.exceptions import (
|
|
73
72
|
AutoTransitionDepthError,
|
|
74
73
|
FlowError,
|
|
@@ -84,7 +83,6 @@ from pipecat_flows.router_mode import (
|
|
|
84
83
|
build_router_response_schema,
|
|
85
84
|
parse_router_response,
|
|
86
85
|
pick_branch_target,
|
|
87
|
-
render_router_system_prompt,
|
|
88
86
|
)
|
|
89
87
|
from pipecat_flows.types import (
|
|
90
88
|
ActionConfig,
|
|
@@ -175,8 +173,12 @@ class FlowManager:
|
|
|
175
173
|
self._llm = llm
|
|
176
174
|
self._action_manager = ActionManager(task, flow_manager=self)
|
|
177
175
|
self._adapter = create_adapter(llm, context_aggregator)
|
|
178
|
-
self._data_extractor = DataExtractor()
|
|
179
176
|
self._initialized = False
|
|
177
|
+
# Stashed by _run_router_node right after the LLM picks a branch and
|
|
178
|
+
# read by _set_node when it tool-logs the transition. Lets the router
|
|
179
|
+
# rationale + prompt_sha1 land in the transition tool-log args. Cleared
|
|
180
|
+
# by _set_node after read so it can't leak into an unrelated next hop.
|
|
181
|
+
self._last_router_pick: Optional[Dict[str, Any]] = None
|
|
180
182
|
self._context_aggregator = context_aggregator
|
|
181
183
|
self._pending_transition: Optional[Dict[str, Any]] = None
|
|
182
184
|
self._pending_function_results: Dict[str, Dict[str, Any]] = {} # Store function results for extraction
|
|
@@ -912,69 +914,6 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
912
914
|
)
|
|
913
915
|
await self._set_node(node_id, node_config)
|
|
914
916
|
|
|
915
|
-
async def _process_data_extraction(self, node_id: str, node_config: NodeConfig) -> None:
|
|
916
|
-
"""Process data extraction for the current node.
|
|
917
|
-
|
|
918
|
-
Args:
|
|
919
|
-
node_id: The current node ID
|
|
920
|
-
node_config: The node configuration containing extraction settings
|
|
921
|
-
"""
|
|
922
|
-
# Get extraction config from runtime config or node config
|
|
923
|
-
runtime_config = self.state.get("nodes_runtime_config", {})
|
|
924
|
-
node_runtime = runtime_config.get(node_id, {})
|
|
925
|
-
extraction_config = node_runtime.get("data_extraction", [])
|
|
926
|
-
|
|
927
|
-
# Also check node_config directly if not in runtime
|
|
928
|
-
if not extraction_config and "data_extraction" in node_config:
|
|
929
|
-
extraction_config = node_config["data_extraction"]
|
|
930
|
-
# Convert to dict format if needed
|
|
931
|
-
if extraction_config and hasattr(extraction_config[0], "model_dump"):
|
|
932
|
-
extraction_config = [field.model_dump() for field in extraction_config]
|
|
933
|
-
|
|
934
|
-
if not extraction_config:
|
|
935
|
-
return
|
|
936
|
-
|
|
937
|
-
logger.debug(f"Processing data extraction for node {node_id}: {len(extraction_config)} fields")
|
|
938
|
-
|
|
939
|
-
try:
|
|
940
|
-
# Get conversation context for conversation-type extractions
|
|
941
|
-
conversation_context = None
|
|
942
|
-
has_conversation_fields = any(
|
|
943
|
-
f.get("extraction_type") == "conversation" for f in extraction_config
|
|
944
|
-
)
|
|
945
|
-
if has_conversation_fields:
|
|
946
|
-
try:
|
|
947
|
-
conversation_context = self.get_current_context()
|
|
948
|
-
except Exception as e:
|
|
949
|
-
logger.warning(f"Could not get conversation context for extraction: {e}")
|
|
950
|
-
|
|
951
|
-
# Process all extractions
|
|
952
|
-
extracted_data = await self._data_extractor.process_node_extractions(
|
|
953
|
-
node_name=node_id,
|
|
954
|
-
extraction_config=extraction_config,
|
|
955
|
-
function_results=self._pending_function_results.copy() if self._pending_function_results else None,
|
|
956
|
-
conversation_context=conversation_context,
|
|
957
|
-
llm_service=self._llm if has_conversation_fields else None,
|
|
958
|
-
)
|
|
959
|
-
|
|
960
|
-
# Store extracted data in state
|
|
961
|
-
if extracted_data:
|
|
962
|
-
if "extracted_data" not in self.state:
|
|
963
|
-
self.state["extracted_data"] = {}
|
|
964
|
-
if node_id not in self.state["extracted_data"]:
|
|
965
|
-
self.state["extracted_data"][node_id] = {}
|
|
966
|
-
|
|
967
|
-
self.state["extracted_data"][node_id].update(extracted_data)
|
|
968
|
-
logger.info(f"Stored {len(extracted_data)} extracted fields for node {node_id}")
|
|
969
|
-
|
|
970
|
-
# Clear pending function results after extraction
|
|
971
|
-
self._pending_function_results.clear()
|
|
972
|
-
|
|
973
|
-
except Exception as e:
|
|
974
|
-
logger.error(f"Error during data extraction for node {node_id}: {e}")
|
|
975
|
-
# Clear pending results even on error
|
|
976
|
-
self._pending_function_results.clear()
|
|
977
|
-
|
|
978
917
|
# ------------------------------------------------------------------
|
|
979
918
|
# Pure-code dispatch primitives (auto_transition + condition_evaluator).
|
|
980
919
|
# Used by Speak / Action / Condition-Router to skip the LLM round-trip.
|
|
@@ -1144,11 +1083,11 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
1144
1083
|
if not self._context_aggregator or target_node_config is None:
|
|
1145
1084
|
return False
|
|
1146
1085
|
target_kind = target_node_config.get("type")
|
|
1147
|
-
if target_kind in ("
|
|
1086
|
+
if target_kind in ("speak_node", "action_node"):
|
|
1148
1087
|
return False
|
|
1149
1088
|
is_terminal = bool(target_node_config.get("terminal")) or target_kind in (
|
|
1150
1089
|
"end_node",
|
|
1151
|
-
"
|
|
1090
|
+
"transfer_node",
|
|
1152
1091
|
)
|
|
1153
1092
|
is_cond_router = target_node_config.get("auto_transition") == "__condition__"
|
|
1154
1093
|
user_agg = self._context_aggregator.user()
|
|
@@ -1250,24 +1189,24 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
1250
1189
|
|
|
1251
1190
|
branches = node_config.get("router_branches") or []
|
|
1252
1191
|
extract = node_config.get("router_extract") or {}
|
|
1253
|
-
branch_descriptions = node_config.get("router_branch_descriptions") or {}
|
|
1254
1192
|
|
|
1255
|
-
#
|
|
1256
|
-
#
|
|
1257
|
-
#
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1193
|
+
# Backend lowerer is the single source of truth for the router prompt.
|
|
1194
|
+
# It writes the rendered prompt into task_messages[0].content at publish
|
|
1195
|
+
# time; we read it verbatim here. No runtime re-rendering — keeps prompt
|
|
1196
|
+
# authoring in the backend repo and avoids drift between lowering-time
|
|
1197
|
+
# and runtime prompts.
|
|
1198
|
+
tm = node_config.get("task_messages") or []
|
|
1199
|
+
system_prompt = (
|
|
1200
|
+
tm[0].get("content", "") if tm and isinstance(tm[0], dict) else ""
|
|
1262
1201
|
)
|
|
1202
|
+
if not system_prompt:
|
|
1203
|
+
raise RouterParseError(
|
|
1204
|
+
f"router node '{node_id}' has no rendered system prompt in "
|
|
1205
|
+
f"task_messages[0].content — re-publish the agent so the "
|
|
1206
|
+
f"lowerer fills it"
|
|
1207
|
+
)
|
|
1263
1208
|
|
|
1264
1209
|
schema = build_router_response_schema(branches, extract)
|
|
1265
|
-
system_prompt = render_router_system_prompt(
|
|
1266
|
-
branches, extract, branch_descriptions
|
|
1267
|
-
)
|
|
1268
|
-
logger.debug(
|
|
1269
|
-
f"Router '{node_id}' rendered system_prompt ({len(system_prompt)} chars):\n{system_prompt}"
|
|
1270
|
-
)
|
|
1271
1210
|
valid_branch_ids = [b["id"] for b in branches if isinstance(b, dict) and b.get("id")]
|
|
1272
1211
|
valid_branch_ids.append(UNCLEAR_BRANCH_ID)
|
|
1273
1212
|
|
|
@@ -1340,6 +1279,13 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
1340
1279
|
f"Router '{node_id}' picked branch_id='{branch_id}' "
|
|
1341
1280
|
f"rationale={rationale!r} prompt_sha1={prompt_hash}"
|
|
1342
1281
|
)
|
|
1282
|
+
# Hand off to the next _set_node so it can surface this in the
|
|
1283
|
+
# transition tool-log args. Cleared by _set_node after read.
|
|
1284
|
+
self._last_router_pick = {
|
|
1285
|
+
"branch_id": branch_id,
|
|
1286
|
+
"rationale": rationale,
|
|
1287
|
+
"prompt_sha1": prompt_hash,
|
|
1288
|
+
}
|
|
1343
1289
|
|
|
1344
1290
|
# Write extracted variables into shared state so downstream nodes
|
|
1345
1291
|
# (Condition routers, Speak template substitutions) can read them.
|
|
@@ -1595,6 +1541,32 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
1595
1541
|
if not self._initialized:
|
|
1596
1542
|
raise FlowTransitionError(f"{self.__class__.__name__} must be initialized first")
|
|
1597
1543
|
|
|
1544
|
+
# Tool-log every transition so operators see the graph traversal on
|
|
1545
|
+
# the same timeline as on-call API tool calls. Capture state BEFORE
|
|
1546
|
+
# any of the node-setup logic runs so previous_node_id reflects the
|
|
1547
|
+
# actual outgoing node (legacy path resets _current_node only at L1796
|
|
1548
|
+
# of the success arm; v2 paths early-latch at L1546).
|
|
1549
|
+
transition_start_time = time.time()
|
|
1550
|
+
previous_node_id = self._current_node
|
|
1551
|
+
router_pick = self._last_router_pick
|
|
1552
|
+
self._last_router_pick = None
|
|
1553
|
+
transition_tool_name = f"transition_to_{node_id}"
|
|
1554
|
+
transition_args: Dict[str, Any] = {
|
|
1555
|
+
"from_node_id": previous_node_id,
|
|
1556
|
+
"to_node_id": node_id,
|
|
1557
|
+
}
|
|
1558
|
+
if router_pick:
|
|
1559
|
+
transition_args.update(router_pick)
|
|
1560
|
+
transition_call_id = await start_tracking(
|
|
1561
|
+
transcript_handler=getattr(self, "_transcript_handler", None),
|
|
1562
|
+
tool_name=transition_tool_name,
|
|
1563
|
+
args=transition_args,
|
|
1564
|
+
tool_phase="on_call",
|
|
1565
|
+
tool_type="transition",
|
|
1566
|
+
current_node=previous_node_id,
|
|
1567
|
+
logger=logger,
|
|
1568
|
+
)
|
|
1569
|
+
|
|
1598
1570
|
try:
|
|
1599
1571
|
# Clear any pending transition state when starting a new node
|
|
1600
1572
|
# This ensures clean state regardless of how we arrived here:
|
|
@@ -1715,9 +1687,6 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
1715
1687
|
# Apply node-level overrides if any
|
|
1716
1688
|
await self._apply_node_overrides(node_id)
|
|
1717
1689
|
|
|
1718
|
-
# Process data extraction for the node (function results from previous node)
|
|
1719
|
-
await self._process_data_extraction(node_id, node_config)
|
|
1720
|
-
|
|
1721
1690
|
# Pure-code short-circuit: if this node is a Speak / Action /
|
|
1722
1691
|
# Condition-Router (has `auto_transition`), skip tools+context+
|
|
1723
1692
|
# LLMRunFrame entirely and recurse directly into the target node.
|
|
@@ -1886,6 +1855,19 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
1886
1855
|
# node_entered observer is now emitted at the *top* of _set_node
|
|
1887
1856
|
# so short-circuit paths (auto_transition / router-mode) get it too.
|
|
1888
1857
|
|
|
1858
|
+
await complete_tracking(
|
|
1859
|
+
transcript_handler=getattr(self, "_transcript_handler", None),
|
|
1860
|
+
function_call_id=transition_call_id,
|
|
1861
|
+
tool_name=transition_tool_name,
|
|
1862
|
+
function_start_time=transition_start_time,
|
|
1863
|
+
response_data={
|
|
1864
|
+
"to_node_id": node_id,
|
|
1865
|
+
"to_node_label": node_config.get("label"),
|
|
1866
|
+
},
|
|
1867
|
+
status_code=200,
|
|
1868
|
+
logger=logger,
|
|
1869
|
+
)
|
|
1870
|
+
|
|
1889
1871
|
except Exception as e:
|
|
1890
1872
|
# If setup crashed inside an uninterruptible node, the guard is
|
|
1891
1873
|
# still latched on ``_current_node`` — clear it so the pipeline
|
|
@@ -1896,6 +1878,19 @@ In all of these cases, you can provide a `name` in your new node's config for de
|
|
|
1896
1878
|
except Exception:
|
|
1897
1879
|
pass
|
|
1898
1880
|
logger.error(f"Error setting node {node_id}: {str(e)}")
|
|
1881
|
+
try:
|
|
1882
|
+
await complete_tracking(
|
|
1883
|
+
transcript_handler=getattr(self, "_transcript_handler", None),
|
|
1884
|
+
function_call_id=transition_call_id,
|
|
1885
|
+
tool_name=transition_tool_name,
|
|
1886
|
+
function_start_time=transition_start_time,
|
|
1887
|
+
response_data=None,
|
|
1888
|
+
status_code=500,
|
|
1889
|
+
error_details={"error": str(e), "type": type(e).__name__},
|
|
1890
|
+
logger=logger,
|
|
1891
|
+
)
|
|
1892
|
+
except Exception as track_exc:
|
|
1893
|
+
logger.warning(f"complete_tracking failed for {transition_tool_name}: {track_exc!r}")
|
|
1899
1894
|
raise FlowError(f"Failed to set node {node_id}: {str(e)}") from e
|
|
1900
1895
|
|
|
1901
1896
|
def _schedule_deferred_post_actions(self, post_actions: List[ActionConfig]) -> None:
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/router_mode.py
RENAMED
|
@@ -16,12 +16,15 @@ writes extracted variables into ``flow_manager.state``, and recurses into
|
|
|
16
16
|
|
|
17
17
|
This module is the pure data-shaping layer:
|
|
18
18
|
* :func:`build_router_response_schema` — JSON Schema for the constrained call
|
|
19
|
-
* :func:`render_router_system_prompt` — system message text for the call
|
|
20
19
|
* :func:`parse_router_response` — extracts ``{branch, extracted}`` from a
|
|
21
20
|
provider response (string/dict/tool-call), with defensive fallbacks
|
|
22
21
|
* :func:`pick_branch_target` — resolves the parsed branch_id to a target
|
|
23
22
|
node name (and handles the unclear sentinel + attempt counter)
|
|
24
23
|
|
|
24
|
+
The router system prompt itself is authored by the backend lowerer
|
|
25
|
+
(``new_calling_agent_backend/agent/commons/flow_lowering.py``) and shipped
|
|
26
|
+
verbatim in ``task_messages[0].content``; the engine reads it directly.
|
|
27
|
+
|
|
25
28
|
The actual LLM call orchestration lives in ``manager.py:_run_router_node`` —
|
|
26
29
|
this module provides the helpers it composes.
|
|
27
30
|
"""
|
|
@@ -135,90 +138,6 @@ def build_router_response_schema(
|
|
|
135
138
|
}
|
|
136
139
|
|
|
137
140
|
|
|
138
|
-
def render_router_system_prompt(
|
|
139
|
-
branches: List[RouterBranch],
|
|
140
|
-
extract: Optional[Dict[str, ExtractSpec]] = None,
|
|
141
|
-
branch_descriptions: Optional[Dict[str, str]] = None,
|
|
142
|
-
) -> str:
|
|
143
|
-
"""Build the router's system message.
|
|
144
|
-
|
|
145
|
-
Kept short on purpose — every token here is paid on every router call (or
|
|
146
|
-
cached, if prompt-caching is wired). Lists each branch with its description
|
|
147
|
-
so the LLM knows when to pick which; appends the extract-vars block when
|
|
148
|
-
the node asks for any.
|
|
149
|
-
|
|
150
|
-
Args:
|
|
151
|
-
branches: NodeConfig ``router_branches`` list (carries id + next).
|
|
152
|
-
extract: NodeConfig ``router_extract`` map.
|
|
153
|
-
branch_descriptions: Optional override map ``{branch_id: description}``.
|
|
154
|
-
The backend lowerer pulls descriptions from the matching edges'
|
|
155
|
-
``condition`` field, not from the branches list itself, so this
|
|
156
|
-
argument is how the orchestrator passes them in.
|
|
157
|
-
|
|
158
|
-
Returns:
|
|
159
|
-
System-prompt string. Engine appends the recent conversation turns
|
|
160
|
-
as a follow-on user message (see ``router_context_window``).
|
|
161
|
-
"""
|
|
162
|
-
descriptions = branch_descriptions or {}
|
|
163
|
-
branch_lines: List[str] = []
|
|
164
|
-
for br in branches or []:
|
|
165
|
-
if not isinstance(br, dict):
|
|
166
|
-
continue
|
|
167
|
-
bid = br.get("id")
|
|
168
|
-
if not bid:
|
|
169
|
-
continue
|
|
170
|
-
desc = descriptions.get(bid) or "(no description)"
|
|
171
|
-
branch_lines.append(f"- {bid}: {desc}")
|
|
172
|
-
branch_lines.append(
|
|
173
|
-
f"- {UNCLEAR_BRANCH_ID}: pick this when no branch above clearly fits "
|
|
174
|
-
f"what the user said"
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
extract_block = ""
|
|
178
|
-
if extract:
|
|
179
|
-
extract_rows: List[str] = []
|
|
180
|
-
for name, spec in extract.items():
|
|
181
|
-
if not isinstance(spec, dict):
|
|
182
|
-
extract_rows.append(f"- {name} (string)")
|
|
183
|
-
continue
|
|
184
|
-
t = spec.get("type", "string")
|
|
185
|
-
desc = spec.get("description", "")
|
|
186
|
-
tail = f" ({t}) — {desc}" if desc else f" ({t})"
|
|
187
|
-
extract_rows.append(f"- {name}{tail}")
|
|
188
|
-
extract_block = (
|
|
189
|
-
"\n\n## Also extract these variables (use null if the user didn't say):\n"
|
|
190
|
-
+ "\n".join(extract_rows)
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
reasoning_block = (
|
|
194
|
-
"\n\n## How to reason\n"
|
|
195
|
-
"Read the user's MOST RECENT message in full before picking. When it "
|
|
196
|
-
"contains BOTH a negation (\"no\", \"not\", \"don't\", \"won't\", "
|
|
197
|
-
"\"can't\", \"nahi\", \"nahin\") AND a positive-sounding action "
|
|
198
|
-
"statement — in either order — the negation scopes over the whole "
|
|
199
|
-
"message. Do NOT pick a positive branch just because one sub-clause "
|
|
200
|
-
"matches its description. Pick a negative-sense branch if one fits; "
|
|
201
|
-
"otherwise __unclear__.\n\n"
|
|
202
|
-
"Example: \"no I don't think so I'll be able to pay\" — negation at "
|
|
203
|
-
"the start scopes over \"I'll be able to pay\". Pick a "
|
|
204
|
-
"\"cannot pay\"-style branch if present, else __unclear__.\n\n"
|
|
205
|
-
"## Strong default\n"
|
|
206
|
-
"If the message is ambiguous or self-contradictory and you can't "
|
|
207
|
-
"defend your pick by quoting the user, prefer __unclear__. A reprompt "
|
|
208
|
-
"costs one turn; a confident misroute costs the call."
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
return (
|
|
212
|
-
"You are a branch-routing classifier for a conversational agent. Read "
|
|
213
|
-
"the conversation so far and pick the one branch_id that best matches "
|
|
214
|
-
"what the user said. Emit JSON with `rationale` first (1-2 sentences "
|
|
215
|
-
"citing the user's words), then `branch`, then `extracted`.\n\n"
|
|
216
|
-
"## Branches\n" + "\n".join(branch_lines)
|
|
217
|
-
+ extract_block
|
|
218
|
-
+ reasoning_block
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
|
|
222
141
|
def parse_router_response(
|
|
223
142
|
raw: Any,
|
|
224
143
|
valid_branch_ids: List[str],
|
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
#
|
|
2
|
-
# Copyright (c) 2024, Daily
|
|
3
|
-
#
|
|
4
|
-
# SPDX-License-Identifier: BSD 2-Clause License
|
|
5
|
-
#
|
|
6
|
-
"""Data extraction system for Pipecat Flows.
|
|
7
|
-
|
|
8
|
-
This module provides functionality to extract and store data at the node level
|
|
9
|
-
from either function responses or conversation context, making it available
|
|
10
|
-
to downstream nodes through the flow manager's state.
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
import json
|
|
14
|
-
from typing import Any, Dict, List, Optional
|
|
15
|
-
|
|
16
|
-
from loguru import logger
|
|
17
|
-
from pipecat.frames.frames import LLMMessagesAppendFrame
|
|
18
|
-
from pipecat.processors.aggregators.llm_context import LLMContext
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class DataExtractor:
|
|
22
|
-
"""Handles extraction of data from function responses and conversations."""
|
|
23
|
-
|
|
24
|
-
def __init__(self):
|
|
25
|
-
"""Initialize the data extractor."""
|
|
26
|
-
self._extraction_cache = {}
|
|
27
|
-
|
|
28
|
-
async def extract_from_function_response(
|
|
29
|
-
self,
|
|
30
|
-
function_name: str,
|
|
31
|
-
response: Dict[str, Any],
|
|
32
|
-
extraction_fields: List[Dict[str, Any]],
|
|
33
|
-
) -> Dict[str, Any]:
|
|
34
|
-
"""Store complete function response for specified fields.
|
|
35
|
-
|
|
36
|
-
Args:
|
|
37
|
-
function_name: Name of the function that produced the response.
|
|
38
|
-
response: The complete response from the function.
|
|
39
|
-
extraction_fields: List of extraction field configurations.
|
|
40
|
-
|
|
41
|
-
Returns:
|
|
42
|
-
Dictionary mapping field keys to extracted values.
|
|
43
|
-
"""
|
|
44
|
-
extracted_data = {}
|
|
45
|
-
|
|
46
|
-
for field in extraction_fields:
|
|
47
|
-
if field.get("extraction_type") != "function_response":
|
|
48
|
-
continue
|
|
49
|
-
|
|
50
|
-
if field.get("source_function") != function_name:
|
|
51
|
-
continue
|
|
52
|
-
|
|
53
|
-
key = field.get("key")
|
|
54
|
-
if not key:
|
|
55
|
-
logger.warning(f"Extraction field missing 'key' for function {function_name}")
|
|
56
|
-
continue
|
|
57
|
-
|
|
58
|
-
# Store the complete response
|
|
59
|
-
extracted_data[key] = response
|
|
60
|
-
logger.debug(f"Extracted function response for key '{key}' from {function_name}")
|
|
61
|
-
|
|
62
|
-
return extracted_data
|
|
63
|
-
|
|
64
|
-
async def extract_from_conversation(
|
|
65
|
-
self,
|
|
66
|
-
context: List[Dict[str, Any]],
|
|
67
|
-
extraction_fields: List[Dict[str, Any]],
|
|
68
|
-
llm_service: Any,
|
|
69
|
-
) -> Dict[str, Any]:
|
|
70
|
-
"""Use LLM to extract data from conversation context.
|
|
71
|
-
|
|
72
|
-
Args:
|
|
73
|
-
context: The conversation context (list of messages).
|
|
74
|
-
extraction_fields: List of extraction field configurations.
|
|
75
|
-
llm_service: The LLM service to use for extraction.
|
|
76
|
-
|
|
77
|
-
Returns:
|
|
78
|
-
Dictionary mapping field keys to extracted values.
|
|
79
|
-
"""
|
|
80
|
-
extracted_data = {}
|
|
81
|
-
|
|
82
|
-
# Filter for conversation-type extractions
|
|
83
|
-
conversation_fields = [
|
|
84
|
-
f for f in extraction_fields if f.get("extraction_type") == "conversation"
|
|
85
|
-
]
|
|
86
|
-
|
|
87
|
-
if not conversation_fields:
|
|
88
|
-
return extracted_data
|
|
89
|
-
|
|
90
|
-
# Build extraction prompt
|
|
91
|
-
extraction_prompt = self._build_extraction_prompt(context, conversation_fields)
|
|
92
|
-
|
|
93
|
-
try:
|
|
94
|
-
# Create a temporary context for extraction
|
|
95
|
-
extraction_messages = [
|
|
96
|
-
{
|
|
97
|
-
"role": "system",
|
|
98
|
-
"content": "You are a data extraction assistant. Extract the requested information from the conversation and return it as JSON.",
|
|
99
|
-
},
|
|
100
|
-
{"role": "user", "content": extraction_prompt},
|
|
101
|
-
]
|
|
102
|
-
|
|
103
|
-
# Use LLM to extract data
|
|
104
|
-
# Note: This assumes the LLM service has a method to get completions
|
|
105
|
-
# You may need to adjust based on your LLM service implementation
|
|
106
|
-
response = await self._get_llm_extraction(llm_service, extraction_messages)
|
|
107
|
-
|
|
108
|
-
if response:
|
|
109
|
-
try:
|
|
110
|
-
# Parse the JSON response
|
|
111
|
-
extracted = json.loads(response)
|
|
112
|
-
for field in conversation_fields:
|
|
113
|
-
key = field.get("key")
|
|
114
|
-
if key and key in extracted:
|
|
115
|
-
extracted_data[key] = extracted[key]
|
|
116
|
-
logger.debug(f"Extracted conversation data for key '{key}'")
|
|
117
|
-
except json.JSONDecodeError as e:
|
|
118
|
-
logger.error(f"Failed to parse LLM extraction response: {e}")
|
|
119
|
-
# Try to extract raw values as fallback
|
|
120
|
-
for field in conversation_fields:
|
|
121
|
-
key = field.get("key")
|
|
122
|
-
if key:
|
|
123
|
-
extracted_data[key] = None
|
|
124
|
-
|
|
125
|
-
except Exception as e:
|
|
126
|
-
logger.error(f"Error during conversation extraction: {e}")
|
|
127
|
-
# Set all fields to None on error
|
|
128
|
-
for field in conversation_fields:
|
|
129
|
-
key = field.get("key")
|
|
130
|
-
if key:
|
|
131
|
-
extracted_data[key] = None
|
|
132
|
-
|
|
133
|
-
return extracted_data
|
|
134
|
-
|
|
135
|
-
def _build_extraction_prompt(
|
|
136
|
-
self, context: List[Dict[str, Any]], fields: List[Dict[str, Any]]
|
|
137
|
-
) -> str:
|
|
138
|
-
"""Build a prompt for LLM extraction.
|
|
139
|
-
|
|
140
|
-
Args:
|
|
141
|
-
context: The conversation context.
|
|
142
|
-
fields: Fields to extract.
|
|
143
|
-
|
|
144
|
-
Returns:
|
|
145
|
-
The extraction prompt string.
|
|
146
|
-
"""
|
|
147
|
-
# Format the conversation
|
|
148
|
-
conversation_text = "\n".join(
|
|
149
|
-
[f"{msg.get('role', 'unknown')}: {msg.get('content', '')}" for msg in context[-10:]]
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
# Build field descriptions
|
|
153
|
-
field_descriptions = []
|
|
154
|
-
for field in fields:
|
|
155
|
-
key = field.get("key")
|
|
156
|
-
description = field.get("description", f"Extract {key}")
|
|
157
|
-
field_descriptions.append(f"- {key}: {description}")
|
|
158
|
-
|
|
159
|
-
prompt = f"""Given the following conversation:
|
|
160
|
-
|
|
161
|
-
{conversation_text}
|
|
162
|
-
|
|
163
|
-
Please extract the following information and return it as a JSON object:
|
|
164
|
-
{chr(10).join(field_descriptions)}
|
|
165
|
-
|
|
166
|
-
Return only valid JSON with the extracted values. If a value cannot be determined, use null.
|
|
167
|
-
"""
|
|
168
|
-
return prompt
|
|
169
|
-
|
|
170
|
-
async def _get_llm_extraction(
|
|
171
|
-
self, llm_service: Any, messages: List[Dict[str, Any]]
|
|
172
|
-
) -> Optional[str]:
|
|
173
|
-
"""Get extraction response from LLM.
|
|
174
|
-
|
|
175
|
-
This is a placeholder that needs to be adapted based on the specific
|
|
176
|
-
LLM service implementation.
|
|
177
|
-
|
|
178
|
-
Args:
|
|
179
|
-
llm_service: The LLM service instance.
|
|
180
|
-
messages: Messages to send to the LLM.
|
|
181
|
-
|
|
182
|
-
Returns:
|
|
183
|
-
The LLM response as a string, or None if failed.
|
|
184
|
-
"""
|
|
185
|
-
try:
|
|
186
|
-
# This implementation depends on your LLM service
|
|
187
|
-
# For OpenAI-style services:
|
|
188
|
-
if hasattr(llm_service, "_client"):
|
|
189
|
-
# Assuming OpenAI-style client
|
|
190
|
-
response = await llm_service._client.chat.completions.create(
|
|
191
|
-
model=llm_service._model,
|
|
192
|
-
messages=messages,
|
|
193
|
-
temperature=0.1, # Low temperature for consistent extraction
|
|
194
|
-
response_format={"type": "json_object"}, # Request JSON format
|
|
195
|
-
)
|
|
196
|
-
return response.choices[0].message.content
|
|
197
|
-
else:
|
|
198
|
-
logger.warning("LLM service does not support extraction, returning None")
|
|
199
|
-
return None
|
|
200
|
-
except Exception as e:
|
|
201
|
-
logger.error(f"Error calling LLM for extraction: {e}")
|
|
202
|
-
return None
|
|
203
|
-
|
|
204
|
-
async def process_node_extractions(
|
|
205
|
-
self,
|
|
206
|
-
node_name: str,
|
|
207
|
-
extraction_config: List[Dict[str, Any]],
|
|
208
|
-
function_results: Optional[Dict[str, Dict[str, Any]]] = None,
|
|
209
|
-
conversation_context: Optional[List[Dict[str, Any]]] = None,
|
|
210
|
-
llm_service: Optional[Any] = None,
|
|
211
|
-
) -> Dict[str, Any]:
|
|
212
|
-
"""Process all data extractions for a node.
|
|
213
|
-
|
|
214
|
-
Args:
|
|
215
|
-
node_name: Name of the current node.
|
|
216
|
-
extraction_config: List of extraction field configurations.
|
|
217
|
-
function_results: Map of function names to their results.
|
|
218
|
-
conversation_context: The conversation context.
|
|
219
|
-
llm_service: The LLM service for conversation extraction.
|
|
220
|
-
|
|
221
|
-
Returns:
|
|
222
|
-
Dictionary with all extracted data for the node.
|
|
223
|
-
"""
|
|
224
|
-
if not extraction_config:
|
|
225
|
-
return {}
|
|
226
|
-
|
|
227
|
-
all_extracted = {}
|
|
228
|
-
|
|
229
|
-
# Process function response extractions
|
|
230
|
-
if function_results:
|
|
231
|
-
for func_name, result in function_results.items():
|
|
232
|
-
func_extracted = await self.extract_from_function_response(
|
|
233
|
-
func_name, result, extraction_config
|
|
234
|
-
)
|
|
235
|
-
all_extracted.update(func_extracted)
|
|
236
|
-
|
|
237
|
-
# Process conversation extractions
|
|
238
|
-
if conversation_context and llm_service:
|
|
239
|
-
conv_extracted = await self.extract_from_conversation(
|
|
240
|
-
conversation_context, extraction_config, llm_service
|
|
241
|
-
)
|
|
242
|
-
all_extracted.update(conv_extracted)
|
|
243
|
-
|
|
244
|
-
if all_extracted:
|
|
245
|
-
logger.info(
|
|
246
|
-
f"Extracted {len(all_extracted)} data fields for node '{node_name}': {list(all_extracted.keys())}"
|
|
247
|
-
)
|
|
248
|
-
|
|
249
|
-
return all_extracted
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/.agents/skills/loki-logs/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/.claude/skills/loki-logs/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/examples/food_ordering.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/editor/canvas.js
RENAMED
|
File without changes
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/editor/editorState.js
RENAMED
|
File without changes
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/editor/sidePanel.js
RENAMED
|
File without changes
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/editor/toolbar.js
RENAMED
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/nodes/baseNode.js
RENAMED
|
File without changes
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/nodes/endNode.js
RENAMED
|
File without changes
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/nodes/flowNode.js
RENAMED
|
File without changes
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/nodes/functionNode.js
RENAMED
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/nodes/mergeNode.js
RENAMED
|
File without changes
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/nodes/startNode.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/utils/helpers.js
RENAMED
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/utils/validation.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/editor/tailwind.config.cjs
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/UNIFIED_KB_BENCHMARKS.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/datasets/.gitignore
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/runners/run_hybrid.py
RENAMED
|
File without changes
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/runners/run_latency.py
RENAMED
|
File without changes
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/runners/run_rerank.py
RENAMED
|
File without changes
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/runners/run_retrieval.py
RENAMED
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/service/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/images/food-ordering-flow.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/remote-asterisk-code/README.md
RENAMED
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/remote-asterisk-code/rtp.conf
RENAMED
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/scripts/check-pypi-package.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dv_pipecat_flows-0.0.0.dev2072 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/types.py
RENAMED
|
File without changes
|
|
File without changes
|