mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-27 08:28:00 +00:00
* feat: add composer input polishing * Revert "Merge branch 'main' into feat/input-polish" This reverts commit 5b6ceccf0db3092bc62fde3b05e7816829601756, reversing changes made to 45fbc57fef5fa5fd878cf0176c37f3e3bc7ebef6. * Merge main into feat/input-polish * style(frontend): format input helper polish guard * fix(input-polish): address composer polish review findings Frontend - Add a cancel affordance to the in-flight polish status pill that calls abortInputPolishRequest(), so a slow/hung provider no longer hard-locks the composer for up to stream_chunk_timeout with a page reload (and draft loss) as the only escape. - Reset promptHistoryIndexRef/promptHistoryDraftRef when a rewrite is applied (and on undo), so a stale history-browse index can no longer let the next ArrowDown silently overwrite the polished draft. - Disable polishing while an open human-input card is present, matching the frontend/AGENTS.md rule that composer entry points defer to the card so card-reply metadata is preserved. - canPolishInput now reuses parseGoalCommand/parseCompactCommand instead of a third hardcoded reserved-command regex, and drops the phantom /help entry (no /help parser exists in the composer), so future builtins only need to be taught to the existing parsers. Backend - Extract the non-graph one-shot LLM path (build model + inject Langfuse metadata + system/user invoke + text extract) into deerflow.utils.oneshot_llm.run_oneshot_llm, shared by the input-polish and suggestions routers so tracing-metadata and invocation shape cannot drift between the two copies. - strip_think_blocks gains truncate_unclosed (default True, preserving the suggestions/goal JSON-prep behavior); input polish passes False so a draft that legitimately contains a literal <think> substring is no longer truncated into a partial rewrite or a spurious 503. - Validate the empty-check and max_chars boundary against the same stripped view of the draft that is sent to the model, so the user-facing length boundary and the model input can no longer disagree. Tests / docs - Backend: literal-<think> preservation, whitespace-only rejection, and normalized-length/model-input agreement cases; suggestions tests repoint the create_chat_model patch to the shared helper module. - Frontend: helper unit tests updated for the /help/reserved-command change; a new Playwright case covers cancelling an in-flight polish request. - backend/AGENTS.md documents the shared one-shot helper and the polish normalization/think-tag behavior. --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
73 lines
2.7 KiB
Python
73 lines
2.7 KiB
Python
"""Shared helper for one-shot, non-graph LLM text requests.
|
|
|
|
Several Gateway routes (input polishing, follow-up suggestions, and title-style
|
|
rewrites) do the same thing: build a chat model from config, attach Langfuse
|
|
trace metadata, invoke it once with a system + user message pair, and pull the
|
|
plain text back out of the response. Centralizing that sequence here keeps the
|
|
tracing-metadata fields and invocation shape from drifting between routers — a
|
|
fix to one (e.g. a new Langfuse field) now applies to all callers instead of
|
|
silently regressing in whichever copy was forgotten.
|
|
|
|
Response-text *cleaning* (think-block / code-fence stripping, JSON parsing) is
|
|
intentionally left to each caller because their post-processing differs; this
|
|
helper stops at the extracted raw text.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
from langchain_core.messages import HumanMessage, SystemMessage
|
|
|
|
from deerflow.config.app_config import AppConfig
|
|
from deerflow.models import create_chat_model
|
|
from deerflow.runtime.user_context import get_effective_user_id
|
|
from deerflow.tracing import inject_langfuse_metadata
|
|
from deerflow.utils.llm_text import extract_response_text
|
|
|
|
|
|
def _resolve_environment() -> str | None:
|
|
return os.environ.get("DEER_FLOW_ENV") or os.environ.get("ENVIRONMENT")
|
|
|
|
|
|
async def run_oneshot_llm(
|
|
*,
|
|
system_instruction: str,
|
|
user_content: str,
|
|
run_name: str,
|
|
app_config: AppConfig,
|
|
model_name: str | None = None,
|
|
thread_id: str | None = None,
|
|
) -> str:
|
|
"""Run a single non-graph system+user LLM turn and return the raw text.
|
|
|
|
Args:
|
|
system_instruction: System message content.
|
|
user_content: Human message content.
|
|
run_name: LangChain ``run_name`` and Langfuse ``assistant_id`` for the call.
|
|
app_config: Application config used to build the model.
|
|
model_name: Optional model override; ``None`` uses the default model.
|
|
thread_id: Optional thread id, forwarded to Langfuse for tracing only.
|
|
|
|
Returns:
|
|
The extracted plain-text content of the model response (uncleaned).
|
|
"""
|
|
model = create_chat_model(name=model_name, thinking_enabled=False, app_config=app_config)
|
|
invoke_config: dict = {"run_name": run_name}
|
|
inject_langfuse_metadata(
|
|
invoke_config,
|
|
thread_id=thread_id,
|
|
user_id=get_effective_user_id(),
|
|
assistant_id=run_name,
|
|
model_name=model_name,
|
|
environment=_resolve_environment(),
|
|
)
|
|
response = await model.ainvoke(
|
|
[
|
|
SystemMessage(content=system_instruction),
|
|
HumanMessage(content=user_content),
|
|
],
|
|
config=invoke_config,
|
|
)
|
|
return extract_response_text(response.content)
|