Skip to content

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

Open
maxduu wants to merge 4 commits into
mainfrom
convo-agent-output-tool-call-auto
Open

feat: add conversational-output node for low-code conversational-agent loop#965
maxduu wants to merge 4 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.

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 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_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 routing/handoff 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 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.",
),
@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
@sonarqubecloud

sonarqubecloud Bot commented Jul 2, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

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

See analysis details on SonarQube Cloud

# response payload — it carries no user-visible content, only the
# framework-internal set_conversational_output tool call.
new_messages = (
state.messages[initial_count:-1]

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.

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?

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