diff --git a/backend/app/gateway/routers/suggestions.py b/backend/app/gateway/routers/suggestions.py index 39f7d250a..373a48b38 100644 --- a/backend/app/gateway/routers/suggestions.py +++ b/backend/app/gateway/routers/suggestions.py @@ -135,6 +135,8 @@ async def generate_suggestions( request: Request, config: AppConfig = Depends(get_config), ) -> SuggestionsResponse: + if not config.suggestions.enabled: + return SuggestionsResponse(suggestions=[]) if not body.messages: return SuggestionsResponse(suggestions=[]) diff --git a/backend/packages/harness/deerflow/config/app_config.py b/backend/packages/harness/deerflow/config/app_config.py index 5091b7d31..311824782 100644 --- a/backend/packages/harness/deerflow/config/app_config.py +++ b/backend/packages/harness/deerflow/config/app_config.py @@ -28,6 +28,7 @@ from deerflow.config.skill_evolution_config import SkillEvolutionConfig from deerflow.config.skills_config import SkillsConfig from deerflow.config.stream_bridge_config import StreamBridgeConfig, load_stream_bridge_config_from_dict from deerflow.config.subagents_config import SubagentsAppConfig, load_subagents_config_from_dict +from deerflow.config.suggestions_config import SuggestionsConfig from deerflow.config.summarization_config import SummarizationConfig, load_summarization_config_from_dict from deerflow.config.title_config import TitleConfig, load_title_config_from_dict from deerflow.config.token_usage_config import TokenUsageConfig @@ -116,6 +117,7 @@ class AppConfig(BaseModel): acp_agents: dict[str, ACPAgentConfig] = Field(default_factory=dict, description="ACP-compatible agent configuration") subagents: SubagentsAppConfig = Field(default_factory=SubagentsAppConfig, description="Subagent runtime configuration") guardrails: GuardrailsConfig = Field(default_factory=GuardrailsConfig, description="Guardrail middleware configuration") + suggestions: SuggestionsConfig = Field(default_factory=SuggestionsConfig, description="Follow-up suggestions configuration.") circuit_breaker: CircuitBreakerConfig = Field(default_factory=CircuitBreakerConfig, description="LLM circuit breaker configuration") channel_connections: ChannelConnectionsConfig = Field( default_factory=ChannelConnectionsConfig, diff --git a/backend/packages/harness/deerflow/config/suggestions_config.py b/backend/packages/harness/deerflow/config/suggestions_config.py new file mode 100644 index 000000000..a7b8817bd --- /dev/null +++ b/backend/packages/harness/deerflow/config/suggestions_config.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel, Field + + +class SuggestionsConfig(BaseModel): + """Configuration for automatic follow-up suggestions.""" + + enabled: bool = Field(default=True, description="Whether to enable follow-up question suggestions at the end of an AI response") diff --git a/backend/tests/test_suggestions_router.py b/backend/tests/test_suggestions_router.py index bd0a998ff..1a7531fde 100644 --- a/backend/tests/test_suggestions_router.py +++ b/backend/tests/test_suggestions_router.py @@ -74,7 +74,7 @@ def test_generate_suggestions_strips_inline_think_block(monkeypatch): fake_model.ainvoke = AsyncMock(return_value=MagicMock(content=content)) monkeypatch.setattr(suggestions, "create_chat_model", lambda **kwargs: fake_model) - result = asyncio.run(suggestions.generate_suggestions.__wrapped__("t1", req, request=None, config=SimpleNamespace())) + result = asyncio.run(suggestions.generate_suggestions.__wrapped__("t1", req, request=None, config=SimpleNamespace(suggestions=SimpleNamespace(enabled=True)))) assert result.suggestions == ["深度学习和机器学习的区别?", "常用框架有哪些?", "需要什么数学基础?"] @@ -103,7 +103,7 @@ def test_generate_suggestions_parses_and_limits(monkeypatch): # Bypass the require_permission decorator (which needs request + # thread_store) — these tests cover the parsing logic. - result = asyncio.run(suggestions.generate_suggestions.__wrapped__("t1", req, request=None, config=SimpleNamespace())) + result = asyncio.run(suggestions.generate_suggestions.__wrapped__("t1", req, request=None, config=SimpleNamespace(suggestions=SimpleNamespace(enabled=True)))) assert result.suggestions == ["Q1", "Q2", "Q3"] fake_model.ainvoke.assert_awaited_once() @@ -125,7 +125,7 @@ def test_generate_suggestions_parses_list_block_content(monkeypatch): # Bypass the require_permission decorator (which needs request + # thread_store) — these tests cover the parsing logic. - result = asyncio.run(suggestions.generate_suggestions.__wrapped__("t1", req, request=None, config=SimpleNamespace())) + result = asyncio.run(suggestions.generate_suggestions.__wrapped__("t1", req, request=None, config=SimpleNamespace(suggestions=SimpleNamespace(enabled=True)))) assert result.suggestions == ["Q1", "Q2"] fake_model.ainvoke.assert_awaited_once() @@ -147,7 +147,7 @@ def test_generate_suggestions_parses_output_text_block_content(monkeypatch): # Bypass the require_permission decorator (which needs request + # thread_store) — these tests cover the parsing logic. - result = asyncio.run(suggestions.generate_suggestions.__wrapped__("t1", req, request=None, config=SimpleNamespace())) + result = asyncio.run(suggestions.generate_suggestions.__wrapped__("t1", req, request=None, config=SimpleNamespace(suggestions=SimpleNamespace(enabled=True)))) assert result.suggestions == ["Q1", "Q2"] fake_model.ainvoke.assert_awaited_once() @@ -166,6 +166,29 @@ def test_generate_suggestions_returns_empty_on_model_error(monkeypatch): # Bypass the require_permission decorator (which needs request + # thread_store) — these tests cover the parsing logic. - result = asyncio.run(suggestions.generate_suggestions.__wrapped__("t1", req, request=None, config=SimpleNamespace())) + result = asyncio.run(suggestions.generate_suggestions.__wrapped__("t1", req, request=None, config=SimpleNamespace(suggestions=SimpleNamespace(enabled=True)))) assert result.suggestions == [] + + +def test_generate_suggestions_returns_empty_when_disabled(monkeypatch): + """Ensure suggestions are bypassed and returned an empty list when disabled in config.""" + req = suggestions.SuggestionsRequest( + messages=[ + suggestions.SuggestionMessage(role="user", content="Hi"), + suggestions.SuggestionMessage(role="assistant", content="Hello"), + ], + n=3, + model_name=None, + ) + + mock_config = SimpleNamespace(suggestions=SimpleNamespace(enabled=False)) + + fake_model = MagicMock() + fake_model.ainvoke = AsyncMock(side_effect=RuntimeError("Model should not be called.")) + monkeypatch.setattr(suggestions, "create_chat_model", lambda **kwargs: fake_model) + + result = asyncio.run(suggestions.generate_suggestions.__wrapped__("t1", req, request=None, config=mock_config)) + + assert result.suggestions == [] + fake_model.ainvoke.assert_not_called() diff --git a/config.example.yaml b/config.example.yaml index e20ce5443..107ee7166 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -15,7 +15,7 @@ # ============================================================================ # Bump this number when the config schema changes. # Run `make config-upgrade` to merge new fields into your local config.yaml. -config_version: 12 +config_version: 13 # ============================================================================ # Logging @@ -711,6 +711,16 @@ tool_output: # web_search: 8000 # bash: 20000 +# ============================================================================ +# Suggestions Configuration +# ============================================================================ +# Configure whether the agent automatically generates follow-up question +# suggestions at the end of each response. + +suggestions: + enabled: true + + # ============================================================================ # Loop Detection Configuration # ============================================================================