mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-11-07 19:20:34 +00:00
fix(title): build the title model off the Gateway event loop (#7813)
Thread title generation runs inside the agent graph (aafter_model), and it built its configured model inline. create_chat_model can read local CLI credentials while it is constructed (Codex auth.json, Claude Code credential files), so the first turn of every new thread read them on the shared loop and other requests waited for it. Build the provider through run_assembly, the dedicated assembly pool the one-shot helpers, skill moderation, the goal evaluator and start_run already use. Inference still awaits on the loop, and the sync path is unchanged. A Blockbuster regression builds the real Codex provider: on main the gate rejects the credential read, the broad fallback swallows it, and the local fallback title replaces the model's title; with the change the provider is constructed on a worker thread and the model's title is kept.
This commit is contained in:
parent
1dd6d05627
commit
6f24bc7c36
@ -2573,9 +2573,9 @@ Backend response cleanup preserves unrelated tag names such as `<think-tank>` an
|
||||
The Web UI composer can polish draft input before sending. The rewrite runs as a short Gateway LLM request using the `input_polish` model configuration, keeps slash skill prefixes such as `/data-analysis`, and only replaces the local draft after the user clicks the polish button; it does not create a thread run or persist a message.
|
||||
|
||||
Input polishing, follow-up suggestions, background project-file summaries, the
|
||||
LLM skill scanner, and the `/goal` evaluator construct their model providers off
|
||||
the Gateway event loop, so local credential reads do not stall unrelated
|
||||
requests.
|
||||
LLM skill scanner, thread title generation, and the `/goal` evaluator construct
|
||||
their model providers off the Gateway event loop, so local credential reads do
|
||||
not stall unrelated requests.
|
||||
|
||||
When the agent asks for clarification, the Web UI shows the structured response card but keeps the normal composer available. Users can complete the card or send a free-form chat message to bypass it; that message closes the latest pending clarification and becomes the agent's next input. Accompanying answer text stays outside the execution steps panel, and answering the request keeps previously completed text in the conversation while the agent continues.
|
||||
|
||||
|
||||
@ -17,6 +17,7 @@ from deerflow.agents.middlewares.dynamic_context_middleware import is_dynamic_co
|
||||
from deerflow.agents.middlewares.pii_redaction_middleware import redact_texts
|
||||
from deerflow.config.title_config import get_title_config
|
||||
from deerflow.models import create_chat_model
|
||||
from deerflow.utils.assembly_io import run_assembly
|
||||
from deerflow.utils.messages import ORIGINAL_USER_CONTENT_KEY, get_original_user_content_text
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -346,7 +347,11 @@ class TitleMiddleware(AgentMiddleware[TitleMiddlewareState]):
|
||||
model_kwargs = {"thinking_enabled": False, "attach_tracing": False}
|
||||
if self._app_config is not None:
|
||||
model_kwargs["app_config"] = self._app_config
|
||||
model = create_chat_model(name=config.model_name, **model_kwargs)
|
||||
# Provider construction can read local CLI credentials (Codex
|
||||
# auth.json, Claude Code credential files). This hook runs on the
|
||||
# Gateway event loop, so build the provider on the assembly pool
|
||||
# (utils/AGENTS.md) instead of stalling every other request.
|
||||
model = await run_assembly(create_chat_model, name=config.model_name, **model_kwargs)
|
||||
invoke_config = self._get_runnable_config()
|
||||
|
||||
from deerflow_extension_api import SystemOperationKind
|
||||
|
||||
@ -121,19 +121,20 @@ Display summaries use `strip_leading_think_blocks` before limiting text;
|
||||
### Agent / Tool Assembly Off-Load
|
||||
|
||||
One-shot input-polish, follow-up-suggestion, and project-file-summary helpers,
|
||||
skill moderation (`scan_skill_content`) and the goal evaluator build providers
|
||||
through `run_assembly` before awaiting inference; the run worker calls its
|
||||
cached evaluator factory the same way. Provider construction can read local
|
||||
credentials; keep it off the event loop and the default executor while
|
||||
preserving request context. Cancellation during construction must not start
|
||||
inference. A scan without `app_config` (in-graph `skill_manage`) reads the live
|
||||
config in the same pool, and its fail-closed fallback reuses that config rather
|
||||
than reading it again on the loop. Other model construction is separate and
|
||||
not covered here: manual compaction (`compact_thread_context` builds the
|
||||
summarization middleware and its models inline) and, inside graph runs, title
|
||||
generation, summarization and subagent construction.
|
||||
Coverage: `tests/blocking_io/test_oneshot_llm_offloop.py` and
|
||||
`tests/blocking_io/test_system_model_offloop.py`.
|
||||
skill moderation (`scan_skill_content`), the goal evaluator, and thread title
|
||||
generation build providers through `run_assembly` before awaiting inference;
|
||||
the run worker calls its cached evaluator factory the same way. Provider
|
||||
construction can read local credentials; keep it off the event loop and the
|
||||
default executor while preserving request context. Cancellation during
|
||||
construction must not start inference. A scan without `app_config` (in-graph
|
||||
`skill_manage`) reads the live config in the same pool, and its fail-closed
|
||||
fallback reuses that config rather than reading it again on the loop. Other
|
||||
model construction is separate and not covered here: manual compaction
|
||||
(`compact_thread_context` builds the summarization middleware and its models
|
||||
inline) and, inside graph runs, summarization and subagent construction.
|
||||
Coverage: `tests/blocking_io/test_oneshot_llm_offloop.py`,
|
||||
`tests/blocking_io/test_system_model_offloop.py` and
|
||||
`tests/blocking_io/test_title_model_offloop.py`.
|
||||
|
||||
`start_run` also resolves the factory through `run_assembly` before admission:
|
||||
cold imports cannot occupy the loop or its default pool, and import errors
|
||||
|
||||
81
backend/tests/blocking_io/test_title_model_offloop.py
Normal file
81
backend/tests/blocking_io/test_title_model_offloop.py
Normal file
@ -0,0 +1,81 @@
|
||||
"""Title generation must build its provider off the Gateway event loop.
|
||||
|
||||
Inside a graph run, ``aafter_model`` builds the configured title model inline.
|
||||
``create_chat_model`` can read local CLI credentials (Codex ``auth.json``,
|
||||
Claude Code credential files) while it is constructed, so every other request
|
||||
on the shared loop waited for that read. The real Codex provider is built under
|
||||
the strict gate; tracing and inference stay offline.
|
||||
"""
|
||||
|
||||
import json
|
||||
import threading
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from langchain_core.messages import AIMessage, HumanMessage
|
||||
|
||||
from deerflow.agents.middlewares import title_middleware
|
||||
from deerflow.agents.middlewares.title_middleware import TitleMiddleware
|
||||
from deerflow.config.app_config import AppConfig
|
||||
from deerflow.models import factory
|
||||
from deerflow.models.openai_codex_provider import CodexChatModel
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
_TITLE = "Deployment plan"
|
||||
|
||||
|
||||
def _codex_config() -> AppConfig:
|
||||
return AppConfig.model_validate(
|
||||
{
|
||||
"sandbox": {"use": "deerflow.sandbox.local:LocalSandboxProvider"},
|
||||
"models": [{"name": "fixture-codex", "model": "fixture-model", "use": "deerflow.models.openai_codex_provider:CodexChatModel"}],
|
||||
"title": {"model_name": "fixture-codex"},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def codex(tmp_path, monkeypatch):
|
||||
"""Real Codex construction (credential file IO included); offline tracing and inference."""
|
||||
credentials = tmp_path / "auth.json"
|
||||
credentials.write_text(json.dumps({"access_token": "fixture-token", "account_id": "fixture-account"}), encoding="utf-8")
|
||||
monkeypatch.setenv("CODEX_AUTH_PATH", str(credentials))
|
||||
monkeypatch.setattr(factory, "build_tracing_callbacks", lambda **kwargs: [])
|
||||
state = SimpleNamespace(construction_thread=None, calls=[])
|
||||
|
||||
async def invoke(model, messages, *, config):
|
||||
state.calls.append((model, threading.get_ident()))
|
||||
return AIMessage(content=_TITLE)
|
||||
|
||||
monkeypatch.setattr(CodexChatModel, "ainvoke", invoke)
|
||||
return state
|
||||
|
||||
|
||||
async def test_title_model_is_built_off_the_event_loop(codex, monkeypatch):
|
||||
middleware = TitleMiddleware(app_config=_codex_config())
|
||||
real_create = title_middleware.create_chat_model
|
||||
|
||||
def recording_create(*args, **kwargs):
|
||||
codex.construction_thread = threading.get_ident()
|
||||
return real_create(*args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(title_middleware, "create_chat_model", recording_create)
|
||||
state = {
|
||||
"messages": [
|
||||
HumanMessage(content="Plan the eu-west-2 deployment.", id="user"),
|
||||
AIMessage(content="The plan is ready.", id="assistant"),
|
||||
]
|
||||
}
|
||||
|
||||
result = await middleware._agenerate_title_result(state)
|
||||
|
||||
# On the loop the gate rejects the credential read, the broad fallback in
|
||||
# _agenerate_title_result swallows it, and the local title replaces the
|
||||
# configured model's title.
|
||||
assert result == {"title": _TITLE}
|
||||
assert len(codex.calls) == 1
|
||||
assert codex.construction_thread is not None
|
||||
assert codex.construction_thread != threading.get_ident()
|
||||
# Inference itself still awaits on the loop once the provider exists.
|
||||
assert codex.calls[0][1] == threading.get_ident()
|
||||
Loading…
x
Reference in New Issue
Block a user