dv-pipecat-flows 0.0.0.dev2087__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.
Files changed (115) hide show
  1. {dv_pipecat_flows-0.0.0.dev2087/src/dv_pipecat_flows.egg-info → dv_pipecat_flows-0.0.0.dev2093}/PKG-INFO +1 -1
  2. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093/src/dv_pipecat_flows.egg-info}/PKG-INFO +1 -1
  3. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/src/dv_pipecat_flows.egg-info/SOURCES.txt +0 -1
  4. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/adapters.py +4 -5
  5. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/manager.py +80 -85
  6. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/router_mode.py +4 -85
  7. dv_pipecat_flows-0.0.0.dev2087/src/pipecat_flows/data_extractor.py +0 -249
  8. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/.agents/skills/loki-logs/SKILL.md +0 -0
  9. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/.agents/skills/loki-logs/query-reference.md +0 -0
  10. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/.claude/skills/loki-logs/SKILL.md +0 -0
  11. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/.claude/skills/loki-logs/query-reference.md +0 -0
  12. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/.gitattributes +0 -0
  13. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/.gitignore +0 -0
  14. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/.pre-commit-config.yaml +0 -0
  15. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/.python-version +0 -0
  16. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/.readthedocs.yaml +0 -0
  17. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/AGENTS.md +0 -0
  18. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/CHANGELOG.md +0 -0
  19. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/CLAUDE.md +0 -0
  20. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/CONTRIBUTING.md +0 -0
  21. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/LICENSE +0 -0
  22. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/MANIFEST.in +0 -0
  23. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/README.md +0 -0
  24. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/dev-requirements.txt +0 -0
  25. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/docker-compose.dev.yml +0 -0
  26. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/.eslintrc.json +0 -0
  27. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/.prettierrc +0 -0
  28. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/css/tailwind.css +0 -0
  29. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/examples/food_ordering.json +0 -0
  30. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/examples/movie_explorer.json +0 -0
  31. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/examples/patient_intake.json +0 -0
  32. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/examples/restaurant_reservation.json +0 -0
  33. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/examples/travel_planner.json +0 -0
  34. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/favicon.png +0 -0
  35. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/favicon.svg +0 -0
  36. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/index.html +0 -0
  37. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/editor/canvas.js +0 -0
  38. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/editor/editorState.js +0 -0
  39. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/editor/sidePanel.js +0 -0
  40. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/editor/toolbar.js +0 -0
  41. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/main.js +0 -0
  42. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/nodes/baseNode.js +0 -0
  43. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/nodes/endNode.js +0 -0
  44. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/nodes/flowNode.js +0 -0
  45. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/nodes/functionNode.js +0 -0
  46. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/nodes/index.js +0 -0
  47. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/nodes/mergeNode.js +0 -0
  48. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/nodes/startNode.js +0 -0
  49. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/types.js +0 -0
  50. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/utils/export.js +0 -0
  51. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/utils/helpers.js +0 -0
  52. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/utils/import.js +0 -0
  53. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/js/utils/validation.js +0 -0
  54. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/jsdoc.json +0 -0
  55. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/package-lock.json +0 -0
  56. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/package.json +0 -0
  57. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/postcss.config.cjs +0 -0
  58. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/public/favicon.png +0 -0
  59. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/public/favicon.svg +0 -0
  60. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/tailwind.config.cjs +0 -0
  61. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/vercel.json +0 -0
  62. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/editor/vite.config.js +0 -0
  63. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/engine_primitives_plan.md +0 -0
  64. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/env.example +0 -0
  65. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/.gitignore +0 -0
  66. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/README.md +0 -0
  67. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/RESULTS.md +0 -0
  68. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/UNIFIED_KB_BENCHMARKS.md +0 -0
  69. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/chunkers.py +0 -0
  70. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/contracts.py +0 -0
  71. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/datagen/generate_corpus.py +0 -0
  72. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/datagen/generate_dataset.py +0 -0
  73. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/datagen/generate_hybrid.py +0 -0
  74. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/datagen/templates/insurance.yaml +0 -0
  75. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/datasets/.gitignore +0 -0
  76. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/datasets/v1_hybrid/manifest.json +0 -0
  77. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/datasets/v1_retrieval/manifest.json +0 -0
  78. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/datasets/v1_retrieval_long/manifest.json +0 -0
  79. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/datasets/v1_synthetic/manifest.json +0 -0
  80. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/runners/run_filter_pick.py +0 -0
  81. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/runners/run_filter_scaling.py +0 -0
  82. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/runners/run_hybrid.py +0 -0
  83. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/runners/run_latency.py +0 -0
  84. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/runners/run_rerank.py +0 -0
  85. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/runners/run_retrieval.py +0 -0
  86. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/service/API.md +0 -0
  87. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/service/__init__.py +0 -0
  88. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/service/app.py +0 -0
  89. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/evals/kb/service/jobs.py +0 -0
  90. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/images/food-ordering-flow.png +0 -0
  91. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/pipecat-flows.png +0 -0
  92. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/pipecat_upgrade.md +0 -0
  93. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/pyproject.toml +0 -0
  94. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/remote-asterisk-code/README.md +0 -0
  95. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/remote-asterisk-code/extensions.conf +0 -0
  96. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/remote-asterisk-code/rtp.conf +0 -0
  97. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/requirements.txt +0 -0
  98. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/scripts/check-pypi-package.py +0 -0
  99. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/scripts/fix-ruff.sh +0 -0
  100. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/scripts/pre-commit.sh +0 -0
  101. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/setup.cfg +0 -0
  102. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/src/dv_pipecat_flows.egg-info/dependency_links.txt +0 -0
  103. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/src/dv_pipecat_flows.egg-info/requires.txt +0 -0
  104. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/src/dv_pipecat_flows.egg-info/top_level.txt +0 -0
  105. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/__init__.py +0 -0
  106. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/actions.py +0 -0
  107. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/condition_evaluator.py +0 -0
  108. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/exceptions.py +0 -0
  109. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/flow_validator.py +0 -0
  110. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/processors/__init__.py +0 -0
  111. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/processors/router_mode_guard.py +0 -0
  112. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/processors/speak_interruption_guard.py +0 -0
  113. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/processors/user_turn_observer.py +0 -0
  114. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/src/pipecat_flows/types.py +0 -0
  115. {dv_pipecat_flows-0.0.0.dev2087 → dv_pipecat_flows-0.0.0.dev2093}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dv-pipecat-flows
3
- Version: 0.0.0.dev2087
3
+ Version: 0.0.0.dev2093
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dv-pipecat-flows
3
- Version: 0.0.0.dev2087
3
+ Version: 0.0.0.dev2093
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
@@ -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
@@ -176,9 +176,9 @@ class LLMAdapter:
176
176
 
177
177
  Args:
178
178
  llm_service: The underlying LLM service (exposes ``_client`` +
179
- ``_model``, same convention as ``DataExtractor`` uses).
180
- system_prompt: System message text (built by
181
- :func:`render_router_system_prompt`).
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
- Mirrors the direct-client pattern already used by ``DataExtractor``:
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
@@ -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 ("speak", "action"):
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
- "transfer",
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
- # Temporary diagnostic answers "did router_branch_descriptions
1256
- # actually reach the engine, or is the lowerer/pydantic layer still
1257
- # silently dropping it?" Remove after verifying.
1258
- logger.debug(
1259
- f"Router '{node_id}' descriptions present={bool(branch_descriptions)} "
1260
- f"keys={list(branch_descriptions.keys())} "
1261
- f"sample={next(iter(branch_descriptions.items()), None)}"
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:
@@ -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