feat: add conversational-output node for low-code conversational-agent loop#965
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. |
GENERATE_CONVERSATIONAL_OUTPUT node for reliable structured-output extraction72aa3cc to
3e13af0
Compare
|
|
||
| payload_handler.check_stop_reason(response) | ||
|
|
||
| return {"messages": [response]} |
There was a problem hiding this comment.
Do we have a ToolMessage result added for this? Since it's returned into graph state, will it stay in the conversation history and get replayed on the next turn as a tool call with no matching result? Would that be an issue?
There was a problem hiding this comment.
So this works the same way as the end_execution tool, where the LLM adds the AIMessage with the tool-call inputs. The tool itself is never executed since we immediately route to the TERMINATE node, so there's no ToolMessage result.
To ensure this AIMessage is not added to chat-history, we use config_without_streaming here - that makes it so it doesn't stream any events to CAS and thus won't be added to the conversation-history. This is the same pattern used for the analyze-attachments tool which also makes an LLM-call that isn't streamed to CAS.
Finally, we ensure the AIMessage isn't part of the uipath__agent_response_messages by parsing it out here with new_conversation_messages = state.messages[initial_count:-1].
So in summary - no ToolMessage since the tool is never executed (route directly to TERMINATE node) and then the AIMessage is never streamed to CAS, and additionally not part of the output because we extract it out.
|


Summary
Adds a dedicated graph node that runs after the regular loop for low-code conversational agent when structured-outputs are defined.
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 toset_conversational_output.Why a separate tool-call rather than just give the regular agent loop the
set_conversational_outputtool?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 structured output 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_outputflow-control tool + generate-output prompt primitives.GENERATE_CONVERSATIONAL_OUTPUTgraph node, router path, and terminate extraction (the main change).Test plan