mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-03 06:12:14 +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 os
|
||||
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.runnables import RunnableConfig
|
||||
@@ -63,6 +63,15 @@ def handoff_after_clarification(
|
||||
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:
|
||||
"""
|
||||
Check if clarification is needed based on current state.
|
||||
@@ -524,12 +533,12 @@ def coordinator_node(
|
||||
messages.append(
|
||||
{
|
||||
"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
|
||||
tools = [handoff_to_planner]
|
||||
# Bind both handoff_to_planner and direct_response tools
|
||||
tools = [handoff_to_planner, direct_response]
|
||||
response = (
|
||||
get_llm_by_type(AGENT_LLM_MAP["coordinator"])
|
||||
.bind_tools(tools)
|
||||
@@ -556,11 +565,24 @@ def coordinator_node(
|
||||
if tool_args.get("research_topic"):
|
||||
research_topic = tool_args.get("research_topic")
|
||||
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:
|
||||
logger.error(f"Error processing tool calls: {e}")
|
||||
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)
|
||||
# ============================================================
|
||||
@@ -735,16 +757,19 @@ def coordinator_node(
|
||||
logger.error(f"Error processing tool calls: {e}")
|
||||
goto = "planner"
|
||||
else:
|
||||
# No tool calls detected - fallback to planner instead of ending
|
||||
logger.warning(
|
||||
"LLM didn't call any tools. This may indicate tool calling issues with the model. "
|
||||
"Falling back to planner to ensure research proceeds."
|
||||
)
|
||||
# Log full response for debugging
|
||||
logger.debug(f"Coordinator response content: {response.content}")
|
||||
logger.debug(f"Coordinator response object: {response}")
|
||||
# Fallback to planner to ensure workflow continues
|
||||
goto = "planner"
|
||||
# No tool calls detected
|
||||
if enable_clarification:
|
||||
# BRANCH 2: Fallback to planner to ensure research proceeds
|
||||
logger.warning(
|
||||
"LLM didn't call any tools. This may indicate tool calling issues with the model. "
|
||||
"Falling back to planner to ensure research proceeds."
|
||||
)
|
||||
logger.debug(f"Coordinator response content: {response.content}")
|
||||
logger.debug(f"Coordinator response object: {response}")
|
||||
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)
|
||||
if goto == "planner" and state.get("enable_background_investigation"):
|
||||
|
||||
@@ -39,9 +39,9 @@ Your primary responsibilities are:
|
||||
# Execution Rules
|
||||
|
||||
- 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):
|
||||
- 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:
|
||||
- Respond in plain text with an appropriate question
|
||||
- **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
|
||||
- Maximum 3 clarification rounds, then use `handoff_after_clarification()` tool
|
||||
- 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
|
||||
|
||||
**CRITICAL**: You MUST call one of the available tools for research requests. This is mandatory:
|
||||
- Do NOT respond to research questions without calling a tool
|
||||
- For research questions, ALWAYS use either `handoff_to_planner()` or `handoff_after_clarification()`
|
||||
**CRITICAL**: You MUST call one of the available tools. This is mandatory:
|
||||
- For greetings or small talk: use `direct_response()` tool
|
||||
- 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
|
||||
- Never skip tool calling even if you think you can answer the question directly
|
||||
- Responding with text alone for research requests will cause the workflow to fail
|
||||
- Never respond with text alone - always call a tool
|
||||
|
||||
# Clarification Process (When Enabled)
|
||||
|
||||
|
||||
@@ -39,9 +39,9 @@ CURRENT_TIME: {{ CURRENT_TIME }}
|
||||
# 执行规则
|
||||
|
||||
- 如果输入是简单的问候或闲聊(第1类):
|
||||
- 用适当的问候用纯文本回应
|
||||
- 调用`direct_response()`工具,传入你的问候消息
|
||||
- 如果输入涉及安全/道德风险(第2类):
|
||||
- 用纯文本礼貌地拒绝
|
||||
- 调用`direct_response()`工具,传入礼貌的拒绝消息
|
||||
- 如果你需要向用户询问更多背景信息:
|
||||
- 用纯文本进行适当的提问
|
||||
- **对于模糊或过于宽泛的研究问题**:提出澄清问题以缩小范围
|
||||
@@ -53,12 +53,12 @@ CURRENT_TIME: {{ CURRENT_TIME }}
|
||||
|
||||
# 工具调用要求
|
||||
|
||||
**关键**:你必须为研究请求调用可用工具之一。这是强制性的:
|
||||
- 不要在没有调用工具的情况下响应研究问题
|
||||
- 对于研究问题,始终使用`handoff_to_planner()`或`handoff_after_clarification()`
|
||||
**关键**:你必须调用可用工具之一。这是强制性的:
|
||||
- 对于问候或闲聊:使用`direct_response()`工具
|
||||
- 对于礼貌拒绝:使用`direct_response()`工具
|
||||
- 对于研究问题:使用`handoff_to_planner()`或`handoff_after_clarification()`工具
|
||||
- 工具调用是确保工作流程正确进行的必需条件
|
||||
- 即使你认为可以直接回答问题,也不要跳过工具调用
|
||||
- 仅用文本响应研究请求会导致工作流程失败
|
||||
- 不要仅用纯文本响应 - 始终调用工具
|
||||
|
||||
# 澄清过程(启用时)
|
||||
|
||||
|
||||
@@ -772,7 +772,8 @@ def test_coordinator_node_no_tool_calls(
|
||||
patch_handoff_to_planner,
|
||||
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 (
|
||||
patch("src.graph.nodes.AGENT_LLM_MAP", {"coordinator": "basic"}),
|
||||
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
|
||||
|
||||
result = coordinator_node(mock_state_coordinator, MagicMock())
|
||||
# Should fallback to planner instead of __end__ to ensure workflow continues
|
||||
assert result.goto == "planner"
|
||||
# With direct_response tool available, no tool calls means end workflow
|
||||
assert result.goto == "__end__"
|
||||
assert result.update["locale"] == "en-US"
|
||||
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")
|
||||
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 = MagicMock()
|
||||
mock_response = MagicMock()
|
||||
@@ -1736,9 +1737,11 @@ def test_coordinator_tools_with_clarification_disabled(mock_get_llm):
|
||||
assert mock_llm.bind_tools.called
|
||||
bound_tools = mock_llm.bind_tools.call_args[0][0]
|
||||
|
||||
# Should bind only 1 tool when clarification is disabled
|
||||
assert len(bound_tools) == 1
|
||||
assert bound_tools[0].name == "handoff_to_planner"
|
||||
# Should bind 2 tools when clarification is disabled: handoff_to_planner and direct_response
|
||||
assert len(bound_tools) == 2
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user