mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-21 17:25:26 -04:00
fix(sdk): subagents: update prompt and make fetching of last message more robust (#3406)
This commit is contained in:
@@ -12,7 +12,7 @@ from langchain.agents.middleware.types import AgentMiddleware, ContextT, ModelRe
|
||||
from langchain.agents.structured_output import ResponseFormat
|
||||
from langchain.tools import BaseTool, ToolRuntime
|
||||
from langchain_core.language_models import BaseChatModel
|
||||
from langchain_core.messages import HumanMessage, ToolMessage
|
||||
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
|
||||
from langchain_core.runnables import Runnable, RunnableConfig
|
||||
from langchain_core.tools import StructuredTool
|
||||
from langgraph.types import Command
|
||||
@@ -168,7 +168,10 @@ class CompiledSubAgent(TypedDict):
|
||||
"""
|
||||
|
||||
|
||||
DEFAULT_SUBAGENT_PROMPT = "In order to complete the objective that the user asks of you, you have access to a number of standard tools."
|
||||
DEFAULT_SUBAGENT_PROMPT = """In order to complete the objective that the user asks of you, you have access to a number of standard tools.
|
||||
|
||||
The calling agent only sees your final assistant message, not your intermediate work, tool results, or status tracking. Ensure your final
|
||||
response contains the complete answer."""
|
||||
|
||||
# State keys that are excluded when passing state to subagents and when returning
|
||||
# updates from subagents.
|
||||
@@ -430,8 +433,17 @@ def _build_task_tool( # noqa: C901, PLR0915
|
||||
else:
|
||||
content = json.dumps(structured)
|
||||
else:
|
||||
# Strip trailing whitespace to prevent API errors with Anthropic
|
||||
content = result["messages"][-1].text.rstrip() if result["messages"][-1].text else ""
|
||||
# Walk back to the last AIMessage with non-empty text. Anthropic
|
||||
# occasionally emits a trailing empty `end_turn` AIMessage after a
|
||||
# successful final tool call, which would otherwise be forwarded
|
||||
# as an empty ToolMessage.
|
||||
content = ""
|
||||
for msg in reversed(result["messages"]):
|
||||
if isinstance(msg, AIMessage):
|
||||
text = msg.text.rstrip() if msg.text else ""
|
||||
if text:
|
||||
content = text
|
||||
break
|
||||
|
||||
return Command(
|
||||
update={
|
||||
|
||||
@@ -1367,6 +1367,66 @@ class TestSubAgents:
|
||||
task_tool_message = tool_messages[0]
|
||||
assert task_tool_message.content == "Plain text result without structured response"
|
||||
|
||||
def test_fallback_skips_trailing_empty_ai_message(self) -> None:
|
||||
"""Skip a trailing empty AIMessage and use the last AIMessage with text.
|
||||
|
||||
Anthropic/Bedrock occasionally emits an empty `end_turn` AIMessage after
|
||||
a successful final tool call. The middleware should walk back to the
|
||||
prior AIMessage carrying the real answer instead of forwarding an empty
|
||||
ToolMessage.
|
||||
"""
|
||||
mock_subagent = RunnableLambda(
|
||||
lambda _: {
|
||||
"messages": [
|
||||
AIMessage(content="The real answer from the subagent."),
|
||||
AIMessage(content=""),
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
parent_chat_model = GenericFakeChatModel(
|
||||
messages=iter(
|
||||
[
|
||||
AIMessage(
|
||||
content="",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "task",
|
||||
"args": {
|
||||
"description": "Do work",
|
||||
"subagent_type": "worker",
|
||||
},
|
||||
"id": "call_trailing_empty",
|
||||
"type": "tool_call",
|
||||
}
|
||||
],
|
||||
),
|
||||
AIMessage(content="Done"),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
agent = create_deep_agent(
|
||||
model=parent_chat_model,
|
||||
checkpointer=InMemorySaver(),
|
||||
subagents=[
|
||||
CompiledSubAgent(
|
||||
name="worker",
|
||||
description="A worker agent",
|
||||
runnable=mock_subagent,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
result = agent.invoke(
|
||||
{"messages": [HumanMessage(content="Test")]},
|
||||
config={"configurable": {"thread_id": f"test-trailing-empty-{uuid.uuid4().hex}"}},
|
||||
)
|
||||
|
||||
tool_messages = [msg for msg in result["messages"] if msg.type == "tool"]
|
||||
assert len(tool_messages) == 1
|
||||
assert tool_messages[0].content == "The real answer from the subagent."
|
||||
|
||||
def test_subagent_streaming_emits_messages_and_updates_from_subgraph(self) -> None:
|
||||
"""Test end-to-end subagent streaming with `subgraphs=True`.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user