mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-19 12:24:46 +08:00
fix: handle greetings without triggering research workflow (#755)
* fix: handle greetings without triggering research workflow (#733) * test: update tests for direct_response tool behavior * fix: address Copilot review comments for coordinator_node - Extract locale from direct_response tool_args - Fix import sorting (ruff I001) * fix: remove locale extraction from tool_args in direct_response Use locale from state instead of tool_args to avoid potential side effects. The locale is already properly passed from frontend via state. * fix: only fallback to planner when clarification is enabled In legacy mode (BRANCH 1), no tool calls should end the workflow gracefully instead of falling back to planner. This fixes the test_coordinator_node_no_tool_calls integration test. --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
@@ -5,7 +5,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import Any, Annotated, Literal
|
from typing import Annotated, Any, Literal
|
||||||
|
|
||||||
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
|
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
|
||||||
from langchain_core.runnables import RunnableConfig
|
from langchain_core.runnables import RunnableConfig
|
||||||
@@ -63,6 +63,15 @@ def handoff_after_clarification(
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def direct_response(
|
||||||
|
message: Annotated[str, "The response message to send directly to user."],
|
||||||
|
locale: Annotated[str, "The user's detected language locale (e.g., en-US, zh-CN)."],
|
||||||
|
):
|
||||||
|
"""Respond directly to user for greetings, small talk, or polite rejections. Do NOT use this for research questions - use handoff_to_planner instead."""
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def needs_clarification(state: dict) -> bool:
|
def needs_clarification(state: dict) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if clarification is needed based on current state.
|
Check if clarification is needed based on current state.
|
||||||
@@ -524,12 +533,12 @@ def coordinator_node(
|
|||||||
messages.append(
|
messages.append(
|
||||||
{
|
{
|
||||||
"role": "system",
|
"role": "system",
|
||||||
"content": "CRITICAL: Clarification is DISABLED. You MUST immediately call handoff_to_planner tool with the user's query as-is. Do NOT ask questions or mention needing more information.",
|
"content": "Clarification is DISABLED. For research questions, use handoff_to_planner. For greetings or small talk, use direct_response. Do NOT ask clarifying questions.",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Only bind handoff_to_planner tool
|
# Bind both handoff_to_planner and direct_response tools
|
||||||
tools = [handoff_to_planner]
|
tools = [handoff_to_planner, direct_response]
|
||||||
response = (
|
response = (
|
||||||
get_llm_by_type(AGENT_LLM_MAP["coordinator"])
|
get_llm_by_type(AGENT_LLM_MAP["coordinator"])
|
||||||
.bind_tools(tools)
|
.bind_tools(tools)
|
||||||
@@ -556,11 +565,24 @@ def coordinator_node(
|
|||||||
if tool_args.get("research_topic"):
|
if tool_args.get("research_topic"):
|
||||||
research_topic = tool_args.get("research_topic")
|
research_topic = tool_args.get("research_topic")
|
||||||
break
|
break
|
||||||
|
elif tool_name == "direct_response":
|
||||||
|
logger.info("Direct response to user (greeting/small talk)")
|
||||||
|
goto = "__end__"
|
||||||
|
# Append direct message to messages list instead of overwriting response
|
||||||
|
if tool_args.get("message"):
|
||||||
|
messages.append(AIMessage(content=tool_args.get("message"), name="coordinator"))
|
||||||
|
break
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error processing tool calls: {e}")
|
logger.error(f"Error processing tool calls: {e}")
|
||||||
goto = "planner"
|
goto = "planner"
|
||||||
|
|
||||||
|
# Do not return early - let code flow to unified return logic below
|
||||||
|
# Set clarification variables for legacy mode
|
||||||
|
clarification_rounds = 0
|
||||||
|
clarification_history = []
|
||||||
|
clarified_topic = research_topic
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# BRANCH 2: Clarification ENABLED (New Feature)
|
# BRANCH 2: Clarification ENABLED (New Feature)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -735,16 +757,19 @@ def coordinator_node(
|
|||||||
logger.error(f"Error processing tool calls: {e}")
|
logger.error(f"Error processing tool calls: {e}")
|
||||||
goto = "planner"
|
goto = "planner"
|
||||||
else:
|
else:
|
||||||
# No tool calls detected - fallback to planner instead of ending
|
# No tool calls detected
|
||||||
logger.warning(
|
if enable_clarification:
|
||||||
"LLM didn't call any tools. This may indicate tool calling issues with the model. "
|
# BRANCH 2: Fallback to planner to ensure research proceeds
|
||||||
"Falling back to planner to ensure research proceeds."
|
logger.warning(
|
||||||
)
|
"LLM didn't call any tools. This may indicate tool calling issues with the model. "
|
||||||
# Log full response for debugging
|
"Falling back to planner to ensure research proceeds."
|
||||||
logger.debug(f"Coordinator response content: {response.content}")
|
)
|
||||||
logger.debug(f"Coordinator response object: {response}")
|
logger.debug(f"Coordinator response content: {response.content}")
|
||||||
# Fallback to planner to ensure workflow continues
|
logger.debug(f"Coordinator response object: {response}")
|
||||||
goto = "planner"
|
goto = "planner"
|
||||||
|
else:
|
||||||
|
# BRANCH 1: No tool calls means end workflow gracefully (e.g., greeting handled)
|
||||||
|
logger.info("No tool calls in legacy mode - ending workflow gracefully")
|
||||||
|
|
||||||
# Apply background_investigation routing if enabled (unified logic)
|
# Apply background_investigation routing if enabled (unified logic)
|
||||||
if goto == "planner" and state.get("enable_background_investigation"):
|
if goto == "planner" and state.get("enable_background_investigation"):
|
||||||
|
|||||||
@@ -39,9 +39,9 @@ Your primary responsibilities are:
|
|||||||
# Execution Rules
|
# Execution Rules
|
||||||
|
|
||||||
- If the input is a simple greeting or small talk (category 1):
|
- If the input is a simple greeting or small talk (category 1):
|
||||||
- Respond in plain text with an appropriate greeting
|
- Call `direct_response()` tool with your greeting message
|
||||||
- If the input poses a security/moral risk (category 2):
|
- If the input poses a security/moral risk (category 2):
|
||||||
- Respond in plain text with a polite rejection
|
- Call `direct_response()` tool with a polite rejection message
|
||||||
- If you need to ask user for more context:
|
- If you need to ask user for more context:
|
||||||
- Respond in plain text with an appropriate question
|
- Respond in plain text with an appropriate question
|
||||||
- **For vague or overly broad research questions**: Ask clarifying questions to narrow down the scope
|
- **For vague or overly broad research questions**: Ask clarifying questions to narrow down the scope
|
||||||
@@ -49,16 +49,16 @@ Your primary responsibilities are:
|
|||||||
- Ask about: specific applications, aspects, timeframe, geographic scope, or target audience
|
- Ask about: specific applications, aspects, timeframe, geographic scope, or target audience
|
||||||
- Maximum 3 clarification rounds, then use `handoff_after_clarification()` tool
|
- Maximum 3 clarification rounds, then use `handoff_after_clarification()` tool
|
||||||
- For all other inputs (category 3 - which includes most questions):
|
- For all other inputs (category 3 - which includes most questions):
|
||||||
- call `handoff_to_planner()` tool to handoff to planner for research without ANY thoughts.
|
- Call `handoff_to_planner()` tool to handoff to planner for research without ANY thoughts.
|
||||||
|
|
||||||
# Tool Calling Requirements
|
# Tool Calling Requirements
|
||||||
|
|
||||||
**CRITICAL**: You MUST call one of the available tools for research requests. This is mandatory:
|
**CRITICAL**: You MUST call one of the available tools. This is mandatory:
|
||||||
- Do NOT respond to research questions without calling a tool
|
- For greetings or small talk: use `direct_response()` tool
|
||||||
- For research questions, ALWAYS use either `handoff_to_planner()` or `handoff_after_clarification()`
|
- For polite rejections: use `direct_response()` tool
|
||||||
|
- For research questions: use `handoff_to_planner()` or `handoff_after_clarification()` tool
|
||||||
- Tool calling is required to ensure the workflow proceeds correctly
|
- Tool calling is required to ensure the workflow proceeds correctly
|
||||||
- Never skip tool calling even if you think you can answer the question directly
|
- Never respond with text alone - always call a tool
|
||||||
- Responding with text alone for research requests will cause the workflow to fail
|
|
||||||
|
|
||||||
# Clarification Process (When Enabled)
|
# Clarification Process (When Enabled)
|
||||||
|
|
||||||
|
|||||||
@@ -39,9 +39,9 @@ CURRENT_TIME: {{ CURRENT_TIME }}
|
|||||||
# 执行规则
|
# 执行规则
|
||||||
|
|
||||||
- 如果输入是简单的问候或闲聊(第1类):
|
- 如果输入是简单的问候或闲聊(第1类):
|
||||||
- 用适当的问候用纯文本回应
|
- 调用`direct_response()`工具,传入你的问候消息
|
||||||
- 如果输入涉及安全/道德风险(第2类):
|
- 如果输入涉及安全/道德风险(第2类):
|
||||||
- 用纯文本礼貌地拒绝
|
- 调用`direct_response()`工具,传入礼貌的拒绝消息
|
||||||
- 如果你需要向用户询问更多背景信息:
|
- 如果你需要向用户询问更多背景信息:
|
||||||
- 用纯文本进行适当的提问
|
- 用纯文本进行适当的提问
|
||||||
- **对于模糊或过于宽泛的研究问题**:提出澄清问题以缩小范围
|
- **对于模糊或过于宽泛的研究问题**:提出澄清问题以缩小范围
|
||||||
@@ -53,12 +53,12 @@ CURRENT_TIME: {{ CURRENT_TIME }}
|
|||||||
|
|
||||||
# 工具调用要求
|
# 工具调用要求
|
||||||
|
|
||||||
**关键**:你必须为研究请求调用可用工具之一。这是强制性的:
|
**关键**:你必须调用可用工具之一。这是强制性的:
|
||||||
- 不要在没有调用工具的情况下响应研究问题
|
- 对于问候或闲聊:使用`direct_response()`工具
|
||||||
- 对于研究问题,始终使用`handoff_to_planner()`或`handoff_after_clarification()`
|
- 对于礼貌拒绝:使用`direct_response()`工具
|
||||||
|
- 对于研究问题:使用`handoff_to_planner()`或`handoff_after_clarification()`工具
|
||||||
- 工具调用是确保工作流程正确进行的必需条件
|
- 工具调用是确保工作流程正确进行的必需条件
|
||||||
- 即使你认为可以直接回答问题,也不要跳过工具调用
|
- 不要仅用纯文本响应 - 始终调用工具
|
||||||
- 仅用文本响应研究请求会导致工作流程失败
|
|
||||||
|
|
||||||
# 澄清过程(启用时)
|
# 澄清过程(启用时)
|
||||||
|
|
||||||
|
|||||||
@@ -772,7 +772,8 @@ def test_coordinator_node_no_tool_calls(
|
|||||||
patch_handoff_to_planner,
|
patch_handoff_to_planner,
|
||||||
patch_logger,
|
patch_logger,
|
||||||
):
|
):
|
||||||
# No tool calls, should fallback to planner (fix for issue #535)
|
# No tool calls when clarification disabled - should end workflow (fix for issue #733)
|
||||||
|
# When LLM doesn't call any tools in BRANCH 1, workflow ends gracefully
|
||||||
with (
|
with (
|
||||||
patch("src.graph.nodes.AGENT_LLM_MAP", {"coordinator": "basic"}),
|
patch("src.graph.nodes.AGENT_LLM_MAP", {"coordinator": "basic"}),
|
||||||
patch("src.graph.nodes.get_llm_by_type") as mock_get_llm,
|
patch("src.graph.nodes.get_llm_by_type") as mock_get_llm,
|
||||||
@@ -783,8 +784,8 @@ def test_coordinator_node_no_tool_calls(
|
|||||||
mock_get_llm.return_value = mock_llm
|
mock_get_llm.return_value = mock_llm
|
||||||
|
|
||||||
result = coordinator_node(mock_state_coordinator, MagicMock())
|
result = coordinator_node(mock_state_coordinator, MagicMock())
|
||||||
# Should fallback to planner instead of __end__ to ensure workflow continues
|
# With direct_response tool available, no tool calls means end workflow
|
||||||
assert result.goto == "planner"
|
assert result.goto == "__end__"
|
||||||
assert result.update["locale"] == "en-US"
|
assert result.update["locale"] == "en-US"
|
||||||
assert result.update["resources"] == ["resource1", "resource2"]
|
assert result.update["resources"] == ["resource1", "resource2"]
|
||||||
|
|
||||||
@@ -1704,7 +1705,7 @@ def test_coordinator_tools_with_clarification_enabled(mock_get_llm):
|
|||||||
|
|
||||||
@patch("src.graph.nodes.get_llm_by_type")
|
@patch("src.graph.nodes.get_llm_by_type")
|
||||||
def test_coordinator_tools_with_clarification_disabled(mock_get_llm):
|
def test_coordinator_tools_with_clarification_disabled(mock_get_llm):
|
||||||
"""Test that coordinator binds only one tool when clarification is disabled."""
|
"""Test that coordinator binds two tools when clarification is disabled (fix for issue #733)."""
|
||||||
# Mock LLM response with tool call
|
# Mock LLM response with tool call
|
||||||
mock_llm = MagicMock()
|
mock_llm = MagicMock()
|
||||||
mock_response = MagicMock()
|
mock_response = MagicMock()
|
||||||
@@ -1736,9 +1737,11 @@ def test_coordinator_tools_with_clarification_disabled(mock_get_llm):
|
|||||||
assert mock_llm.bind_tools.called
|
assert mock_llm.bind_tools.called
|
||||||
bound_tools = mock_llm.bind_tools.call_args[0][0]
|
bound_tools = mock_llm.bind_tools.call_args[0][0]
|
||||||
|
|
||||||
# Should bind only 1 tool when clarification is disabled
|
# Should bind 2 tools when clarification is disabled: handoff_to_planner and direct_response
|
||||||
assert len(bound_tools) == 1
|
assert len(bound_tools) == 2
|
||||||
assert bound_tools[0].name == "handoff_to_planner"
|
tool_names = {tool.name for tool in bound_tools}
|
||||||
|
assert "handoff_to_planner" in tool_names
|
||||||
|
assert "direct_response" in tool_names
|
||||||
|
|
||||||
|
|
||||||
@patch("src.graph.nodes.get_llm_by_type")
|
@patch("src.graph.nodes.get_llm_by_type")
|
||||||
|
|||||||
Reference in New Issue
Block a user