Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from pydantic import BaseModel
from pydantic import Field

from nat.llm.utils.constants import LLMFinishReason
from nat.plugins.langchain.agent.base import AGENT_CALL_LOG_MESSAGE
from nat.plugins.langchain.agent.base import AGENT_LOG_PREFIX
from nat.plugins.langchain.agent.base import INPUT_SCHEMA_MESSAGE
Expand Down Expand Up @@ -309,6 +310,10 @@ async def agent_node(self, state: ReActGraphState, config: RunnableConfig | None
logger.debug("%s The agent's scratchpad (with tool result) was:\n%s",
AGENT_LOG_PREFIX,
agent_scratchpad)
# A truncated reply would be parsed as a complete answer or tool input (#2518)
if getattr(output_message, "response_metadata", {}).get("finish_reason") == LLMFinishReason.LENGTH:
raise RuntimeError("LLM output truncated (finish_reason='length'). "
f"Truncated output: {str(output_message.content)[:500]!r}")
try:
# check if the agent has the final answer yet
logger.debug("%s Successfully obtained agent response. Parsing agent's response", AGENT_LOG_PREFIX)
Expand Down
28 changes: 28 additions & 0 deletions packages/nvidia_nat_langchain/tests/agent/test_react.py
Original file line number Diff line number Diff line change
Expand Up @@ -2442,3 +2442,31 @@ async def on_llm_end(self, *args: typing.Any, **kwargs: typing.Any) -> None:
config={"callbacks": [CountLLMEnds()]})
assert CountLLMEnds.calls == 2
assert response.usage.total_tokens == 300


async def test_agent_node_raises_on_truncated_output(mock_react_agent_no_raise):
from unittest.mock import AsyncMock
from unittest.mock import patch

truncated = AIMessage(content="Thought: I know this.\nFinal Answer: LangSmith is a platform for",
response_metadata={"finish_reason": "length"})
with patch.object(mock_react_agent_no_raise, '_stream_llm', new_callable=AsyncMock) as mock_stream_llm:
mock_stream_llm.return_value = truncated
with pytest.raises(RuntimeError, match="finish_reason='length'"):
await mock_react_agent_no_raise.agent_node(ReActGraphState(messages=[HumanMessage(content="hi")]))


async def test_agent_node_raises_on_truncated_output_in_agentic_cycle(mock_react_agent_no_raise):
from unittest.mock import AsyncMock
from unittest.mock import patch

truncated = AIMessage(content="Thought: search more.\nAction: Tool A\nAction Input: {\"query\": \"Lang",
response_metadata={"finish_reason": "length"})
state = ReActGraphState(messages=[HumanMessage(content="hi")],
agent_scratchpad=[AgentAction(tool="Tool A", tool_input="hello", log="test")],
tool_responses=[ToolMessage(name="Tool A", tool_call_id="Tool A", content="result")])
with patch.object(mock_react_agent_no_raise, '_stream_llm', new_callable=AsyncMock) as mock_stream_llm:
mock_stream_llm.return_value = truncated
with pytest.raises(RuntimeError, match="finish_reason='length'"):
await mock_react_agent_no_raise.agent_node(state)
assert "agent_scratchpad" in mock_stream_llm.call_args.args[1]
Loading