feat: add conversational-output node for low-code conversational-agent loop#965
Open
maxduu wants to merge 4 commits into
Open
feat: add conversational-output node for low-code conversational-agent loop#965maxduu wants to merge 4 commits into
maxduu wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds an optional (flagged) intermediate LangGraph node for conversational agents to reliably extract structured custom output fields via a forced set_conversational_output tool call, keeping the main AGENT LLM focused on the conversational reply while ensuring schema compliance at termination.
Changes:
- Introduces
GENERATE_CONVERSATIONAL_OUTPUTnode + wiring/routing to run between AGENT and TERMINATE when conversational + flag enabled + custom output fields exist. - Adds utilities/factories (
config_without_streaming,has_custom_conversational_output_fields,build_conversational_output_args_schema,create_set_conversational_output_tool) and updates termination to merge extracted custom fields. - Adds/updates tests covering the new node, routing behavior, termination behavior, and helper utilities.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/agent/tools/test_utils.py | Adds unit tests for the new shared config_without_streaming helper. |
| tests/agent/tools/internal_tools/test_analyze_files_tool.py | Removes the old private _config_without_streaming tests after refactor to shared helper. |
| tests/agent/react/test_utils.py | Adds tests for has_custom_conversational_output_fields and build_conversational_output_args_schema. |
| tests/agent/react/test_terminate_node.py | Adds termination tests for extracting/validating conversational custom output fields. |
| tests/agent/react/test_router_conversational.py | Adds routing tests for the new conversational-output node branch. |
| tests/agent/react/test_flow_control_tools.py | Adds tests for the new create_set_conversational_output_tool factory. |
| tests/agent/react/test_create_agent.py | Adds topology tests ensuring the node is conditionally inserted by config/schema. |
| tests/agent/react/test_conversational_output_node.py | Adds tests for the new node (tool binding, TAG_NOSTREAM config, streaming disabled, instruction handling). |
| src/uipath_langchain/agent/tools/utils.py | Adds shared config_without_streaming helper used by multiple internal LLM calls. |
| src/uipath_langchain/agent/tools/internal_tools/analyze_files_tool.py | Refactors to use config_without_streaming and keeps internal LLM call non-streamed. |
| src/uipath_langchain/agent/react/utils.py | Adds helpers to detect custom conversational output fields and generate tool args schema. |
| src/uipath_langchain/agent/react/types.py | Adds new graph node enum value, new config flag, and extends flow-control tool list. |
| src/uipath_langchain/agent/react/tools/tools.py | Adds create_set_conversational_output_tool factory. |
| src/uipath_langchain/agent/react/terminate_node.py | Extracts/merges set_conversational_output args into conversational termination output and improves validation errors. |
| src/uipath_langchain/agent/react/router_conversational.py | Adds optional routing to GENERATE_CONVERSATIONAL_OUTPUT when enabled. |
| src/uipath_langchain/agent/react/conversational_output_node.py | Implements the new focused LLM extraction node (non-streamed, TAG_NOSTREAM, forced tool call). |
| src/uipath_langchain/agent/react/constants.py | Adds constant for the response-messages field name. |
| src/uipath_langchain/agent/react/agent.py | Conditionally inserts the new node and passes routing flag based on config + schema analysis. |
Comment on lines
+98
to
+100
| custom_output_fields: dict[str, Any] = ( | ||
| dict(set_output_call["args"]) if set_output_call is not None else {} | ||
| ) |
Comment on lines
+83
to
+86
| detail=( | ||
| "The language model returned an unexpected response type." | ||
| "If you are using a BYOM configuration, verify your model deployment.", | ||
| ), |
GENERATE_CONVERSATIONAL_OUTPUT node for reliable structured-output extraction
|
| # response payload — it carries no user-visible content, only the | ||
| # framework-internal set_conversational_output tool call. | ||
| new_messages = ( | ||
| state.messages[initial_count:-1] |
Contributor
There was a problem hiding this comment.
If the extraction call comes back with no set_conversational_output tool call, will the empty extraction message leak into uipath__agent_response_messages as a blank assistant turn?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Summary
Adds a dedicated graph node that runs after the regular loop for low-code conversational agent.
In the past, conversational agents terminated with just
uipath__agent_response_messagesonce an LLM-call produces no tool-calls. Now, if an output-schema is defined and conversational-outputs are enabled, the graph routes to a newGENERATE_CONVERSATIONAL_OUTPUTgraph node which calls the LLM to produce a structured output via a focused, forced tool call to `set_conversational_output`.Why separate tool-call rather than just give the regular agent loop the tool-call?
For the regular conversational-loop, every LLM-call is with "tool_choice: auto" meaning the LLM can decide to call/not-call tool(s). An original approach attempted to give the original loop's LLM calls the
set_conversational_outputtool and prompt it to call it with its last reply in its turn. However, this causes some issues:Instead, this change is purely additive - after the conversational loop (which has no context of the
set_conversational_outputtool, we make an LLM-call with a forced (tool_choice: any) call to generate the structured output. This means every model is guaranteed to call the tool and it decouples conversational quality from schema compliance — the main loop's LLM calls stay focused on the reply/tool-calls, and a second LLM call reliably fills in the routing/handoff fields.Note that this does have an effect on latency - however, a planned optimization (follow-up) is to ensure we emit
end_exchangeafter the last regular loop's LLM-call rather than after the entire graph.End-to-end example:
In addition to the original
uipath_agent_response_messagesoutput which is always implicit,outputSchemafields will be respected and the agent can output things like:agent.json:Output:
Changes
Graph structure (agent/react/)
AgentGraphNode.GENERATE_CONVERSATIONAL_OUTPUTenum value.conversational_output_node.py— invokes a focused LLM call with onlyset_conversational_outputbound (tool_choice="any",disable_streaming=True,TAG_NOSTREAMtag). Reusesstate.messagesfor full agent context and appends the framework instruction as a HumanMessage that never persists to state.agent.py— conditionally inserts the new node between AGENT and TERMINATE when its a conversational agent andhas_custom_conversational_output_fields(output_schema).router_conversational.py— routes AGENT-without-tool-calls to the new node (or straight to TERMINATE if the flag is off).Config
AgentGraphConfig.conversational_outputs_enabled: bool = False— the top-level feature flag. Defaults to False; existing callers unchanged.Terminate node
_handle_end_conversationalnow best-effort extracts the tool call's args fromstate.messages[-1]. If the tool call is absent, custom fields stay empty and Pydantic surfaces a clear per-field error at schema validation.Utilities
has_custom_conversational_output_fields+build_conversational_output_args_schemahelpers inutils.py(stripsuipath__agent_response_messagesfrom the LLM-fillable args schema).create_set_conversational_output_toolfactory inagent/react/tools/tools.py.config_without_streaminghelper inagent/tools/utils.py— refactored out ofanalyze_files_tool.pysince it's now used in two places.UIPATH_CONVERSATIONAL_AGENT_RESPONSE_MESSAGES_FIELDconstant.Tests: full topology + router + terminate + utils + node coverage added.
Related PRs
Part of a coordinated four-repo change. Each PR is independently reviewable, but they land together:
conversationalService.enableOutputsFPS flag onUiPathRuntimeContext.set_conversational_outputtool + generate-output prompt for conversational agents uipath-python#1785 — adds theset_conversational_outputflow-control tool + generate-output prompt primitives.GENERATE_CONVERSATIONAL_OUTPUTgraph node, router path, and terminate extraction (the main change).Test plan