mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-28 17:06:05 +00:00
feat(suggestions): configure follow-up suggestion count (#4533)
This commit is contained in:
parent
919caf7c83
commit
0a9ce5d7e3
@ -8,6 +8,7 @@ import deerflow.utils.llm_text as llm_text
|
||||
from app.gateway.authz import require_permission
|
||||
from app.gateway.deps import get_config
|
||||
from deerflow.config.app_config import AppConfig
|
||||
from deerflow.config.suggestions_config import DEFAULT_MAX_SUGGESTIONS, MAX_SUGGESTIONS_LIMIT
|
||||
from deerflow.utils.oneshot_llm import run_oneshot_llm
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -22,7 +23,7 @@ class SuggestionMessage(BaseModel):
|
||||
|
||||
class SuggestionsRequest(BaseModel):
|
||||
messages: list[SuggestionMessage] = Field(..., description="Recent conversation messages")
|
||||
n: int = Field(default=3, ge=1, le=5, description="Number of suggestions to generate")
|
||||
n: int = Field(default=DEFAULT_MAX_SUGGESTIONS, ge=1, le=MAX_SUGGESTIONS_LIMIT, description="Number of suggestions to generate")
|
||||
model_name: str | None = Field(default=None, description="Optional model override")
|
||||
|
||||
|
||||
@ -32,6 +33,7 @@ class SuggestionsResponse(BaseModel):
|
||||
|
||||
class SuggestionsConfigResponse(BaseModel):
|
||||
enabled: bool = Field(..., description="Whether follow-up suggestions are enabled globally")
|
||||
max_suggestions: int = Field(..., ge=1, le=MAX_SUGGESTIONS_LIMIT, description="Maximum number of follow-up suggestions to generate")
|
||||
|
||||
|
||||
_strip_markdown_code_fence = llm_text.strip_markdown_code_fence
|
||||
@ -76,6 +78,10 @@ def _format_conversation(messages: list[SuggestionMessage]) -> str:
|
||||
return "\n".join(parts).strip()
|
||||
|
||||
|
||||
def _configured_max_suggestions(config: AppConfig) -> int:
|
||||
return getattr(config.suggestions, "max_suggestions", DEFAULT_MAX_SUGGESTIONS)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/suggestions/config",
|
||||
response_model=SuggestionsConfigResponse,
|
||||
@ -85,7 +91,7 @@ def _format_conversation(messages: list[SuggestionMessage]) -> str:
|
||||
async def get_suggestions_config(
|
||||
config: AppConfig = Depends(get_config),
|
||||
) -> SuggestionsConfigResponse:
|
||||
return SuggestionsConfigResponse(enabled=config.suggestions.enabled)
|
||||
return SuggestionsConfigResponse(enabled=config.suggestions.enabled, max_suggestions=_configured_max_suggestions(config))
|
||||
|
||||
|
||||
@router.post(
|
||||
@ -106,7 +112,7 @@ async def generate_suggestions(
|
||||
if not body.messages:
|
||||
return SuggestionsResponse(suggestions=[])
|
||||
|
||||
n = body.n
|
||||
n = min(body.n, _configured_max_suggestions(config))
|
||||
conversation = _format_conversation(body.messages)
|
||||
if not conversation:
|
||||
return SuggestionsResponse(suggestions=[])
|
||||
|
||||
@ -1,7 +1,16 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
DEFAULT_MAX_SUGGESTIONS = 3
|
||||
MAX_SUGGESTIONS_LIMIT = 5
|
||||
|
||||
|
||||
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")
|
||||
max_suggestions: int = Field(
|
||||
default=DEFAULT_MAX_SUGGESTIONS,
|
||||
ge=1,
|
||||
le=MAX_SUGGESTIONS_LIMIT,
|
||||
description="Maximum number of follow-up suggestions to generate.",
|
||||
)
|
||||
|
||||
@ -125,6 +125,31 @@ def test_generate_suggestions_parses_and_limits(monkeypatch):
|
||||
assert fake_model.ainvoke.await_args.kwargs["config"] == {"run_name": "suggest_agent"}
|
||||
|
||||
|
||||
def test_generate_suggestions_respects_configured_max(monkeypatch):
|
||||
req = suggestions.SuggestionsRequest(
|
||||
messages=[
|
||||
suggestions.SuggestionMessage(role="user", content="Hi"),
|
||||
suggestions.SuggestionMessage(role="assistant", content="Hello"),
|
||||
],
|
||||
n=4,
|
||||
model_name=None,
|
||||
)
|
||||
fake_model = MagicMock()
|
||||
fake_model.ainvoke = AsyncMock(return_value=MagicMock(content='["Q1", "Q2", "Q3", "Q4"]'))
|
||||
monkeypatch.setattr(oneshot_llm, "create_chat_model", lambda **kwargs: fake_model)
|
||||
|
||||
result = asyncio.run(
|
||||
suggestions.generate_suggestions.__wrapped__(
|
||||
"t1",
|
||||
req,
|
||||
request=None,
|
||||
config=SimpleNamespace(suggestions=SimpleNamespace(enabled=True, max_suggestions=2)),
|
||||
)
|
||||
)
|
||||
|
||||
assert result.suggestions == ["Q1", "Q2"]
|
||||
|
||||
|
||||
def test_generate_suggestions_injects_deerflow_trace_metadata_when_langfuse_enabled(monkeypatch):
|
||||
monkeypatch.setenv("LANGFUSE_TRACING", "true")
|
||||
monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "pk-lf-test")
|
||||
@ -248,8 +273,10 @@ def test_get_suggestions_config():
|
||||
mock_config_true = SimpleNamespace(suggestions=SimpleNamespace(enabled=True))
|
||||
result_true = asyncio.run(suggestions.get_suggestions_config(config=mock_config_true))
|
||||
assert result_true.enabled is True
|
||||
assert result_true.max_suggestions == 3
|
||||
|
||||
# Test when disabled
|
||||
mock_config_false = SimpleNamespace(suggestions=SimpleNamespace(enabled=False))
|
||||
mock_config_false = SimpleNamespace(suggestions=SimpleNamespace(enabled=False, max_suggestions=4))
|
||||
result_false = asyncio.run(suggestions.get_suggestions_config(config=mock_config_false))
|
||||
assert result_false.enabled is False
|
||||
assert result_false.max_suggestions == 4
|
||||
|
||||
@ -1050,6 +1050,7 @@ tool_output:
|
||||
|
||||
suggestions:
|
||||
enabled: true
|
||||
max_suggestions: 3
|
||||
|
||||
|
||||
# ============================================================================
|
||||
|
||||
@ -81,6 +81,7 @@ import {
|
||||
type SidecarContext,
|
||||
} from "@/core/sidecar";
|
||||
import { useSkills } from "@/core/skills/hooks";
|
||||
import { DEFAULT_MAX_SUGGESTIONS } from "@/core/suggestions/api";
|
||||
import { useSuggestionsConfig } from "@/core/suggestions/hooks";
|
||||
import type { AgentThreadContext, GoalState } from "@/core/threads";
|
||||
import { compactThreadContext } from "@/core/threads/api";
|
||||
@ -398,6 +399,8 @@ export function InputBox({
|
||||
const { data: suggestionsConfig } = useSuggestionsConfig();
|
||||
const suggestionsConfigLoaded = suggestionsConfig !== undefined;
|
||||
const suggestionsEnabled = suggestionsConfig?.enabled;
|
||||
const maxFollowupSuggestions =
|
||||
suggestionsConfig?.max_suggestions ?? DEFAULT_MAX_SUGGESTIONS;
|
||||
const [followupsHidden, setFollowupsHidden] = useState(false);
|
||||
const [followupsLoading, setFollowupsLoading] = useState(false);
|
||||
const [polishingInput, setPolishingInput] = useState(false);
|
||||
@ -1998,7 +2001,7 @@ export function InputBox({
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
messages: recent,
|
||||
n: 3,
|
||||
n: maxFollowupSuggestions,
|
||||
model_name: context.model_name ?? undefined,
|
||||
}),
|
||||
signal: controller.signal,
|
||||
@ -2013,7 +2016,7 @@ export function InputBox({
|
||||
const suggestions = (data.suggestions ?? [])
|
||||
.map((s) => (typeof s === "string" ? s.trim() : ""))
|
||||
.filter((s) => s.length > 0)
|
||||
.slice(0, 5);
|
||||
.slice(0, maxFollowupSuggestions);
|
||||
setFollowups(suggestions);
|
||||
})
|
||||
.catch(() => {
|
||||
@ -2028,6 +2031,7 @@ export function InputBox({
|
||||
context.model_name,
|
||||
disabled,
|
||||
isMock,
|
||||
maxFollowupSuggestions,
|
||||
status,
|
||||
suggestionsConfigLoaded,
|
||||
suggestionsEnabled,
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import { fetch } from "@/core/api/fetcher";
|
||||
import { getBackendBaseURL } from "@/core/config";
|
||||
|
||||
export const DEFAULT_MAX_SUGGESTIONS = 3;
|
||||
|
||||
export interface SuggestionsConfigResponse {
|
||||
enabled: boolean;
|
||||
max_suggestions: number;
|
||||
}
|
||||
|
||||
export async function loadSuggestionsConfig(): Promise<SuggestionsConfigResponse> {
|
||||
@ -10,7 +13,7 @@ export async function loadSuggestionsConfig(): Promise<SuggestionsConfigResponse
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
// Fallback to true if the backend is older
|
||||
return { enabled: true };
|
||||
return { enabled: true, max_suggestions: DEFAULT_MAX_SUGGESTIONS };
|
||||
}
|
||||
throw new Error(
|
||||
`Failed to load suggestions config: ${response.statusText}`,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user