Skip to content

feat: add conversational-output node for low-code conversational-agent loop#965

Merged
maxduu merged 13 commits into
mainfrom
convo-agent-output-tool-call-auto
Jul 8, 2026
Merged

feat: add conversational-output node for low-code conversational-agent loop#965
maxduu merged 13 commits into
mainfrom
convo-agent-output-tool-call-auto

Conversation

@maxduu

@maxduu maxduu commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

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_messages once an LLM-call produces no tool-calls. Now, if an output-schema is defined and conversational-outputs are enabled, the graph routes to a new GENERATE_CONVERSATIONAL_OUTPUT graph node which calls the LLM to produce a structured output via a focused, forced tool call to set_conversational_output.

Why a separate tool-call rather than just give the regular agent loop the set_conversational_output tool?

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_output tool and prompt it to call it with its last reply in its turn. However, this causes some issues:

  • GPT models oftentimes don't call the tool
  • The model needs to focus on both the conversation response and calling the tool, meaning possible negative effects on the response quality.

Instead, this change is purely additive - after the conversational loop (which has no context of the set_conversational_output tool, 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_exchange after 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_messages output which is always implicit, outputSchema fields will be respected and the agent can output things like:
agent.json:

"messages": [
        {
            "role": "system",
            "content": "You are an agentic assistant that searches the web and can analyze file attachments. If the user asks questions related to service desk tickets, route to the service-desk agent. If the user mentions that they want to speak to a human, route to a human.",
          ...
    ],
    "inputSchema": {
        "type": "object",
        "properties": {}
    },
    "outputSchema": {
        "type": "object",
        "properties": {
            "should_handoff": {
                "type": "boolean",
                "description": "Whether the conversation should be handed off to a different agent."
            },
            "handoff_target": {
                "type": "string",
                "description": "If should_handoff is true, this field indicates the target to handoff to. Either \"human\" or \"service_desk_agent\"."
            }
        }
    },

Output:

● END
output
├── should_handoff: True
├── handoff_target: human
└── uipath__agent_response_messages (list, 1 items)
    └── #0 (dict)
        ├── role: assistant
        ├── contentParts (list, 1 items)
        │   └── #0 (dict)
        │       ├── mimeType: text/markdown
        │       ├── data (dict)
        │       │   └── inline: I will connect you to a human representative. Please wait while I transfer your request.
        │       └── citations (list, 0 items)
        └── toolCalls (list, 0 items) 

Changes

Graph structure (agent/react/)

  • New AgentGraphNode.GENERATE_CONVERSATIONAL_OUTPUT enum value.
  • New conversational_output_node.py — invokes a focused LLM call with only set_conversational_output bound (tool_choice="any", disable_streaming=True, TAG_NOSTREAM tag). Reuses state.messages for 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 and has_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

  • New AgentGraphConfig.conversational_outputs_enabled: bool = False — the top-level feature flag. Defaults to False; existing callers unchanged.

Terminate node

  • _handle_end_conversational now best-effort extracts the tool call's args from state.messages[-1]. If the tool call is absent, custom fields stay empty and Pydantic surfaces a clear per-field error at schema validation.
  • The new node's AIMessage is sliced off before conversion so it doesn't leak into the response payload.

Utilities

  • New has_custom_conversational_output_fields + build_conversational_output_args_schema helpers in utils.py (strips uipath__agent_response_messages from the LLM-fillable args schema).
  • New create_set_conversational_output_tool factory in agent/react/tools/tools.py.
  • New shared config_without_streaming helper in agent/tools/utils.py — refactored out of analyze_files_tool.py since it's now used in two places.
  • New UIPATH_CONVERSATIONAL_AGENT_RESPONSE_MESSAGES_FIELD constant.

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:

Test plan

  • `uv run pytest tests/agent tests/runtime tests/cli` (1567 tests pass)
  • `just lint` clean
  • Manual end-to-end against `convo-agent-web-search-file-attachments`

Copilot AI review requested due to automatic review settings July 2, 2026 05:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_OUTPUT node + 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 thread src/uipath_langchain/agent/react/terminate_node.py Outdated
Comment thread src/uipath_langchain/agent/react/conversational_output_node.py
@maxduu maxduu changed the title feat: GENERATE_CONVERSATIONAL_OUTPUT node for reliable structured-output extraction feat: add conversational-output node for low-code conversational-agent loop Jul 2, 2026
Comment thread src/uipath_langchain/agent/react/terminate_node.py Outdated
@maxduu maxduu force-pushed the convo-agent-output-tool-call-auto branch from 72aa3cc to 3e13af0 Compare July 7, 2026 19:23

payload_handler.check_stop_reason(response)

return {"messages": [response]}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@maxduu maxduu Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@sonarqubecloud

sonarqubecloud Bot commented Jul 8, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
86.4% Coverage on New Code (required ≥ 90%)

See analysis details on SonarQube Cloud

@maxduu maxduu merged commit 7e562f7 into main Jul 8, 2026
44 of 45 checks passed
@maxduu maxduu deleted the convo-agent-output-tool-call-auto branch July 8, 2026 16:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants