mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-23 16:35:59 +00:00
feat(trace):LangGraph -> lead_agent and set custom agent_name to run_name (#3101)
* feat(trace):LangGraph -> lead_agent and set user custom agent name to run_name * feat(trace):follow github copilot suggest * feat(trace):Refactor run_name resolution and improve test coverage
This commit is contained in:
@@ -32,6 +32,7 @@ from deerflow.runtime import (
|
|||||||
UnsupportedStrategyError,
|
UnsupportedStrategyError,
|
||||||
run_agent,
|
run_agent,
|
||||||
)
|
)
|
||||||
|
from deerflow.runtime.runs.naming import resolve_root_run_name
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -235,6 +236,7 @@ def build_run_config(
|
|||||||
target = config.setdefault("configurable", {})
|
target = config.setdefault("configurable", {})
|
||||||
if target is not None and "agent_name" not in target:
|
if target is not None and "agent_name" not in target:
|
||||||
target["agent_name"] = normalized
|
target["agent_name"] = normalized
|
||||||
|
config.setdefault("run_name", resolve_root_run_name(config, normalized))
|
||||||
if metadata:
|
if metadata:
|
||||||
config.setdefault("metadata", {}).update(metadata)
|
config.setdefault("metadata", {}).update(metadata)
|
||||||
return config
|
return config
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
"""Run naming helpers for LangChain/LangSmith tracing."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_root_run_name(config: Mapping[str, Any], assistant_id: str | None) -> str:
|
||||||
|
for container_name in ("context", "configurable"):
|
||||||
|
container = config.get(container_name)
|
||||||
|
if isinstance(container, Mapping):
|
||||||
|
agent_name = container.get("agent_name")
|
||||||
|
if isinstance(agent_name, str) and agent_name.strip():
|
||||||
|
return agent_name
|
||||||
|
return assistant_id or "lead_agent"
|
||||||
@@ -33,6 +33,7 @@ from deerflow.runtime.serialization import serialize
|
|||||||
from deerflow.runtime.stream_bridge import StreamBridge
|
from deerflow.runtime.stream_bridge import StreamBridge
|
||||||
|
|
||||||
from .manager import RunManager, RunRecord
|
from .manager import RunManager, RunRecord
|
||||||
|
from .naming import resolve_root_run_name
|
||||||
from .schemas import RunStatus
|
from .schemas import RunStatus
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -224,6 +225,9 @@ async def run_agent(
|
|||||||
if journal is not None:
|
if journal is not None:
|
||||||
config.setdefault("callbacks", []).append(journal)
|
config.setdefault("callbacks", []).append(journal)
|
||||||
|
|
||||||
|
# Resolve after runtime context installation so context/configurable reflect
|
||||||
|
# the agent name that this run will actually execute.
|
||||||
|
config.setdefault("run_name", resolve_root_run_name(config, record.assistant_id))
|
||||||
runnable_config = RunnableConfig(**config)
|
runnable_config = RunnableConfig(**config)
|
||||||
if ctx.app_config is not None and _agent_factory_supports_app_config(agent_factory):
|
if ctx.app_config is not None and _agent_factory_supports_app_config(agent_factory):
|
||||||
agent = agent_factory(config=runnable_config, app_config=ctx.app_config)
|
agent = agent_factory(config=runnable_config, app_config=ctx.app_config)
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ def test_build_run_config_custom_agent_injects_agent_name():
|
|||||||
|
|
||||||
config = build_run_config("thread-1", None, None, assistant_id="finalis")
|
config = build_run_config("thread-1", None, None, assistant_id="finalis")
|
||||||
assert config["configurable"]["agent_name"] == "finalis"
|
assert config["configurable"]["agent_name"] == "finalis"
|
||||||
|
assert config["run_name"] == "finalis"
|
||||||
|
|
||||||
|
|
||||||
def test_build_run_config_lead_agent_no_agent_name():
|
def test_build_run_config_lead_agent_no_agent_name():
|
||||||
@@ -122,6 +123,7 @@ def test_build_run_config_lead_agent_no_agent_name():
|
|||||||
|
|
||||||
config = build_run_config("thread-1", None, None, assistant_id="lead_agent")
|
config = build_run_config("thread-1", None, None, assistant_id="lead_agent")
|
||||||
assert "agent_name" not in config["configurable"]
|
assert "agent_name" not in config["configurable"]
|
||||||
|
assert "run_name" not in config
|
||||||
|
|
||||||
|
|
||||||
def test_build_run_config_none_assistant_id_no_agent_name():
|
def test_build_run_config_none_assistant_id_no_agent_name():
|
||||||
@@ -130,6 +132,7 @@ def test_build_run_config_none_assistant_id_no_agent_name():
|
|||||||
|
|
||||||
config = build_run_config("thread-1", None, None, assistant_id=None)
|
config = build_run_config("thread-1", None, None, assistant_id=None)
|
||||||
assert "agent_name" not in config["configurable"]
|
assert "agent_name" not in config["configurable"]
|
||||||
|
assert "run_name" not in config
|
||||||
|
|
||||||
|
|
||||||
def test_build_run_config_explicit_agent_name_not_overwritten():
|
def test_build_run_config_explicit_agent_name_not_overwritten():
|
||||||
@@ -143,6 +146,7 @@ def test_build_run_config_explicit_agent_name_not_overwritten():
|
|||||||
assistant_id="other-agent",
|
assistant_id="other-agent",
|
||||||
)
|
)
|
||||||
assert config["configurable"]["agent_name"] == "explicit-agent"
|
assert config["configurable"]["agent_name"] == "explicit-agent"
|
||||||
|
assert config["run_name"] == "explicit-agent"
|
||||||
|
|
||||||
|
|
||||||
def test_build_run_config_context_custom_agent_injects_agent_name():
|
def test_build_run_config_context_custom_agent_injects_agent_name():
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
from deerflow.runtime.runs.naming import resolve_root_run_name
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_root_run_name_from_context_agent_name():
|
||||||
|
assert resolve_root_run_name({"context": {"agent_name": "finalis"}}, "lead_agent") == "finalis"
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_root_run_name_from_configurable_agent_name():
|
||||||
|
assert resolve_root_run_name({"configurable": {"agent_name": "finalis"}}, "lead_agent") == "finalis"
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_root_run_name_falls_back_to_assistant_id():
|
||||||
|
assert resolve_root_run_name({}, "my-agent") == "my-agent"
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_root_run_name_falls_back_to_lead_agent():
|
||||||
|
assert resolve_root_run_name({}, None) == "lead_agent"
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_root_run_name_prefers_context_over_configurable():
|
||||||
|
config = {
|
||||||
|
"context": {"agent_name": "ctx-agent"},
|
||||||
|
"configurable": {"agent_name": "cfg-agent"},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert resolve_root_run_name(config, "lead_agent") == "ctx-agent"
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_root_run_name_ignores_blank_agent_name():
|
||||||
|
assert resolve_root_run_name({"context": {"agent_name": " "}}, "my-agent") == "my-agent"
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_root_run_name_ignores_non_string_agent_name():
|
||||||
|
assert resolve_root_run_name({"context": {"agent_name": None}}, "my-agent") == "my-agent"
|
||||||
@@ -95,6 +95,108 @@ async def test_run_agent_threads_explicit_app_config_into_config_only_factory():
|
|||||||
bridge.cleanup.assert_awaited_once_with(record.run_id, delay=60)
|
bridge.cleanup.assert_awaited_once_with(record.run_id, delay=60)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_run_agent_defaults_root_run_name_from_assistant_id():
|
||||||
|
run_manager = RunManager()
|
||||||
|
record = await run_manager.create("thread-1", assistant_id="lead_agent")
|
||||||
|
bridge = SimpleNamespace(
|
||||||
|
publish=AsyncMock(),
|
||||||
|
publish_end=AsyncMock(),
|
||||||
|
cleanup=AsyncMock(),
|
||||||
|
)
|
||||||
|
captured: dict[str, object] = {}
|
||||||
|
|
||||||
|
class DummyAgent:
|
||||||
|
async def astream(self, graph_input, config=None, stream_mode=None, subgraphs=False):
|
||||||
|
captured["astream_run_name"] = config["run_name"]
|
||||||
|
yield {"messages": []}
|
||||||
|
|
||||||
|
def factory(*, config):
|
||||||
|
captured["factory_run_name"] = config["run_name"]
|
||||||
|
return DummyAgent()
|
||||||
|
|
||||||
|
await run_agent(
|
||||||
|
bridge,
|
||||||
|
run_manager,
|
||||||
|
record,
|
||||||
|
ctx=RunContext(checkpointer=None),
|
||||||
|
agent_factory=factory,
|
||||||
|
graph_input={},
|
||||||
|
config={},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert captured["factory_run_name"] == "lead_agent"
|
||||||
|
assert captured["astream_run_name"] == "lead_agent"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_run_agent_defaults_root_run_name_from_context_agent_name():
|
||||||
|
run_manager = RunManager()
|
||||||
|
record = await run_manager.create("thread-1", assistant_id="lead_agent")
|
||||||
|
bridge = SimpleNamespace(
|
||||||
|
publish=AsyncMock(),
|
||||||
|
publish_end=AsyncMock(),
|
||||||
|
cleanup=AsyncMock(),
|
||||||
|
)
|
||||||
|
captured: dict[str, object] = {}
|
||||||
|
|
||||||
|
class DummyAgent:
|
||||||
|
async def astream(self, graph_input, config=None, stream_mode=None, subgraphs=False):
|
||||||
|
captured["astream_run_name"] = config["run_name"]
|
||||||
|
yield {"messages": []}
|
||||||
|
|
||||||
|
def factory(*, config):
|
||||||
|
captured["factory_run_name"] = config["run_name"]
|
||||||
|
return DummyAgent()
|
||||||
|
|
||||||
|
await run_agent(
|
||||||
|
bridge,
|
||||||
|
run_manager,
|
||||||
|
record,
|
||||||
|
ctx=RunContext(checkpointer=None),
|
||||||
|
agent_factory=factory,
|
||||||
|
graph_input={},
|
||||||
|
config={"context": {"agent_name": "finalis"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert captured["factory_run_name"] == "finalis"
|
||||||
|
assert captured["astream_run_name"] == "finalis"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_run_agent_defaults_root_run_name_from_configurable_agent_name():
|
||||||
|
run_manager = RunManager()
|
||||||
|
record = await run_manager.create("thread-1", assistant_id="lead_agent")
|
||||||
|
bridge = SimpleNamespace(
|
||||||
|
publish=AsyncMock(),
|
||||||
|
publish_end=AsyncMock(),
|
||||||
|
cleanup=AsyncMock(),
|
||||||
|
)
|
||||||
|
captured: dict[str, object] = {}
|
||||||
|
|
||||||
|
class DummyAgent:
|
||||||
|
async def astream(self, graph_input, config=None, stream_mode=None, subgraphs=False):
|
||||||
|
captured["astream_run_name"] = config["run_name"]
|
||||||
|
yield {"messages": []}
|
||||||
|
|
||||||
|
def factory(*, config):
|
||||||
|
captured["factory_run_name"] = config["run_name"]
|
||||||
|
return DummyAgent()
|
||||||
|
|
||||||
|
await run_agent(
|
||||||
|
bridge,
|
||||||
|
run_manager,
|
||||||
|
record,
|
||||||
|
ctx=RunContext(checkpointer=None),
|
||||||
|
agent_factory=factory,
|
||||||
|
graph_input={},
|
||||||
|
config={"configurable": {"agent_name": "finalis"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert captured["factory_run_name"] == "finalis"
|
||||||
|
assert captured["astream_run_name"] == "finalis"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_rollback_restores_snapshot_without_deleting_thread():
|
async def test_rollback_restores_snapshot_without_deleting_thread():
|
||||||
checkpointer = FakeCheckpointer(put_result={"configurable": {"thread_id": "thread-1", "checkpoint_ns": "", "checkpoint_id": "restored-1"}})
|
checkpointer = FakeCheckpointer(put_result={"configurable": {"thread_id": "thread-1", "checkpoint_ns": "", "checkpoint_id": "restored-1"}})
|
||||||
|
|||||||
Reference in New Issue
Block a user