mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-12 23:19:36 +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.authz import require_permission
|
||||||
from app.gateway.deps import get_config
|
from app.gateway.deps import get_config
|
||||||
from deerflow.config.app_config import AppConfig
|
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
|
from deerflow.utils.oneshot_llm import run_oneshot_llm
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -22,7 +23,7 @@ class SuggestionMessage(BaseModel):
|
|||||||
|
|
||||||
class SuggestionsRequest(BaseModel):
|
class SuggestionsRequest(BaseModel):
|
||||||
messages: list[SuggestionMessage] = Field(..., description="Recent conversation messages")
|
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")
|
model_name: str | None = Field(default=None, description="Optional model override")
|
||||||
|
|
||||||
|
|
||||||
@ -32,6 +33,7 @@ class SuggestionsResponse(BaseModel):
|
|||||||
|
|
||||||
class SuggestionsConfigResponse(BaseModel):
|
class SuggestionsConfigResponse(BaseModel):
|
||||||
enabled: bool = Field(..., description="Whether follow-up suggestions are enabled globally")
|
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
|
_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()
|
return "\n".join(parts).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _configured_max_suggestions(config: AppConfig) -> int:
|
||||||
|
return getattr(config.suggestions, "max_suggestions", DEFAULT_MAX_SUGGESTIONS)
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/suggestions/config",
|
"/suggestions/config",
|
||||||
response_model=SuggestionsConfigResponse,
|
response_model=SuggestionsConfigResponse,
|
||||||
@ -85,7 +91,7 @@ def _format_conversation(messages: list[SuggestionMessage]) -> str:
|
|||||||
async def get_suggestions_config(
|
async def get_suggestions_config(
|
||||||
config: AppConfig = Depends(get_config),
|
config: AppConfig = Depends(get_config),
|
||||||
) -> SuggestionsConfigResponse:
|
) -> SuggestionsConfigResponse:
|
||||||
return SuggestionsConfigResponse(enabled=config.suggestions.enabled)
|
return SuggestionsConfigResponse(enabled=config.suggestions.enabled, max_suggestions=_configured_max_suggestions(config))
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
@ -106,7 +112,7 @@ async def generate_suggestions(
|
|||||||
if not body.messages:
|
if not body.messages:
|
||||||
return SuggestionsResponse(suggestions=[])
|
return SuggestionsResponse(suggestions=[])
|
||||||
|
|
||||||
n = body.n
|
n = min(body.n, _configured_max_suggestions(config))
|
||||||
conversation = _format_conversation(body.messages)
|
conversation = _format_conversation(body.messages)
|
||||||
if not conversation:
|
if not conversation:
|
||||||
return SuggestionsResponse(suggestions=[])
|
return SuggestionsResponse(suggestions=[])
|
||||||
|
|||||||
@ -1,7 +1,16 @@
|
|||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
DEFAULT_MAX_SUGGESTIONS = 3
|
||||||
|
MAX_SUGGESTIONS_LIMIT = 5
|
||||||
|
|
||||||
|
|
||||||
class SuggestionsConfig(BaseModel):
|
class SuggestionsConfig(BaseModel):
|
||||||
"""Configuration for automatic follow-up suggestions."""
|
"""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")
|
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"}
|
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):
|
def test_generate_suggestions_injects_deerflow_trace_metadata_when_langfuse_enabled(monkeypatch):
|
||||||
monkeypatch.setenv("LANGFUSE_TRACING", "true")
|
monkeypatch.setenv("LANGFUSE_TRACING", "true")
|
||||||
monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "pk-lf-test")
|
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))
|
mock_config_true = SimpleNamespace(suggestions=SimpleNamespace(enabled=True))
|
||||||
result_true = asyncio.run(suggestions.get_suggestions_config(config=mock_config_true))
|
result_true = asyncio.run(suggestions.get_suggestions_config(config=mock_config_true))
|
||||||
assert result_true.enabled is True
|
assert result_true.enabled is True
|
||||||
|
assert result_true.max_suggestions == 3
|
||||||
|
|
||||||
# Test when disabled
|
# 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))
|
result_false = asyncio.run(suggestions.get_suggestions_config(config=mock_config_false))
|
||||||
assert result_false.enabled is False
|
assert result_false.enabled is False
|
||||||
|
assert result_false.max_suggestions == 4
|
||||||
|
|||||||
@ -1050,6 +1050,7 @@ tool_output:
|
|||||||
|
|
||||||
suggestions:
|
suggestions:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
max_suggestions: 3
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
@ -81,6 +81,7 @@ import {
|
|||||||
type SidecarContext,
|
type SidecarContext,
|
||||||
} from "@/core/sidecar";
|
} from "@/core/sidecar";
|
||||||
import { useSkills } from "@/core/skills/hooks";
|
import { useSkills } from "@/core/skills/hooks";
|
||||||
|
import { DEFAULT_MAX_SUGGESTIONS } from "@/core/suggestions/api";
|
||||||
import { useSuggestionsConfig } from "@/core/suggestions/hooks";
|
import { useSuggestionsConfig } from "@/core/suggestions/hooks";
|
||||||
import type { AgentThreadContext, GoalState } from "@/core/threads";
|
import type { AgentThreadContext, GoalState } from "@/core/threads";
|
||||||
import { compactThreadContext } from "@/core/threads/api";
|
import { compactThreadContext } from "@/core/threads/api";
|
||||||
@ -398,6 +399,8 @@ export function InputBox({
|
|||||||
const { data: suggestionsConfig } = useSuggestionsConfig();
|
const { data: suggestionsConfig } = useSuggestionsConfig();
|
||||||
const suggestionsConfigLoaded = suggestionsConfig !== undefined;
|
const suggestionsConfigLoaded = suggestionsConfig !== undefined;
|
||||||
const suggestionsEnabled = suggestionsConfig?.enabled;
|
const suggestionsEnabled = suggestionsConfig?.enabled;
|
||||||
|
const maxFollowupSuggestions =
|
||||||
|
suggestionsConfig?.max_suggestions ?? DEFAULT_MAX_SUGGESTIONS;
|
||||||
const [followupsHidden, setFollowupsHidden] = useState(false);
|
const [followupsHidden, setFollowupsHidden] = useState(false);
|
||||||
const [followupsLoading, setFollowupsLoading] = useState(false);
|
const [followupsLoading, setFollowupsLoading] = useState(false);
|
||||||
const [polishingInput, setPolishingInput] = useState(false);
|
const [polishingInput, setPolishingInput] = useState(false);
|
||||||
@ -1998,7 +2001,7 @@ export function InputBox({
|
|||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
messages: recent,
|
messages: recent,
|
||||||
n: 3,
|
n: maxFollowupSuggestions,
|
||||||
model_name: context.model_name ?? undefined,
|
model_name: context.model_name ?? undefined,
|
||||||
}),
|
}),
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
@ -2013,7 +2016,7 @@ export function InputBox({
|
|||||||
const suggestions = (data.suggestions ?? [])
|
const suggestions = (data.suggestions ?? [])
|
||||||
.map((s) => (typeof s === "string" ? s.trim() : ""))
|
.map((s) => (typeof s === "string" ? s.trim() : ""))
|
||||||
.filter((s) => s.length > 0)
|
.filter((s) => s.length > 0)
|
||||||
.slice(0, 5);
|
.slice(0, maxFollowupSuggestions);
|
||||||
setFollowups(suggestions);
|
setFollowups(suggestions);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@ -2028,6 +2031,7 @@ export function InputBox({
|
|||||||
context.model_name,
|
context.model_name,
|
||||||
disabled,
|
disabled,
|
||||||
isMock,
|
isMock,
|
||||||
|
maxFollowupSuggestions,
|
||||||
status,
|
status,
|
||||||
suggestionsConfigLoaded,
|
suggestionsConfigLoaded,
|
||||||
suggestionsEnabled,
|
suggestionsEnabled,
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
import { fetch } from "@/core/api/fetcher";
|
import { fetch } from "@/core/api/fetcher";
|
||||||
import { getBackendBaseURL } from "@/core/config";
|
import { getBackendBaseURL } from "@/core/config";
|
||||||
|
|
||||||
|
export const DEFAULT_MAX_SUGGESTIONS = 3;
|
||||||
|
|
||||||
export interface SuggestionsConfigResponse {
|
export interface SuggestionsConfigResponse {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
max_suggestions: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadSuggestionsConfig(): Promise<SuggestionsConfigResponse> {
|
export async function loadSuggestionsConfig(): Promise<SuggestionsConfigResponse> {
|
||||||
@ -10,7 +13,7 @@ export async function loadSuggestionsConfig(): Promise<SuggestionsConfigResponse
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 404) {
|
if (response.status === 404) {
|
||||||
// Fallback to true if the backend is older
|
// Fallback to true if the backend is older
|
||||||
return { enabled: true };
|
return { enabled: true, max_suggestions: DEFAULT_MAX_SUGGESTIONS };
|
||||||
}
|
}
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to load suggestions config: ${response.statusText}`,
|
`Failed to load suggestions config: ${response.statusText}`,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user