Skip to content

Commit ea85f78

Browse files
authored
feat(llm): add LangChain 1.x content blocks support for reasoning and tool calls (#1496)
1 parent 0da4b14 commit ea85f78

File tree

2 files changed

+370
-102
lines changed

2 files changed

+370
-102
lines changed

nemoguardrails/actions/llm/utils.py

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -243,15 +243,18 @@ def _convert_messages_to_langchain_format(prompt: List[dict]) -> List:
243243
def _store_reasoning_traces(response) -> None:
244244
"""Store reasoning traces from response in context variable.
245245
246-
Extracts reasoning content from response.additional_kwargs["reasoning_content"]
247-
if available. Otherwise, falls back to extracting from <think> tags in the
248-
response content (and removes the tags from content).
246+
Tries multiple extraction methods in order of preference:
247+
1. content_blocks with type="reasoning" (LangChain v1 standard)
248+
2. additional_kwargs["reasoning_content"] (provider-specific)
249+
3. <think> tags in content (legacy fallback)
249250
250251
Args:
251252
response: The LLM response object
252253
"""
254+
reasoning_content = _extract_reasoning_from_content_blocks(response)
253255

254-
reasoning_content = _extract_reasoning_content(response)
256+
if not reasoning_content:
257+
reasoning_content = _extract_reasoning_from_additional_kwargs(response)
255258

256259
if not reasoning_content:
257260
# Some LLM providers (e.g., certain NVIDIA models) embed reasoning in <think> tags
@@ -263,14 +266,27 @@ def _store_reasoning_traces(response) -> None:
263266
reasoning_trace_var.set(reasoning_content)
264267

265268

266-
def _extract_reasoning_content(response):
269+
def _extract_reasoning_from_content_blocks(response) -> Optional[str]:
270+
"""Extract reasoning from content_blocks with type='reasoning'.
271+
272+
This is the LangChain v1 standard for structured content blocks.
273+
"""
274+
if hasattr(response, "content_blocks"):
275+
for block in response.content_blocks:
276+
if block.get("type") == "reasoning":
277+
return block.get("reasoning")
278+
return None
279+
280+
281+
def _extract_reasoning_from_additional_kwargs(response) -> Optional[str]:
282+
"""Extract reasoning from additional_kwargs['reasoning_content'].
283+
284+
This is used by some providers for backward compatibility.
285+
"""
267286
if hasattr(response, "additional_kwargs"):
268287
additional_kwargs = response.additional_kwargs
269-
if (
270-
isinstance(additional_kwargs, dict)
271-
and "reasoning_content" in additional_kwargs
272-
):
273-
return additional_kwargs["reasoning_content"]
288+
if isinstance(additional_kwargs, dict):
289+
return additional_kwargs.get("reasoning_content")
274290
return None
275291

276292

@@ -317,10 +333,26 @@ def _extract_and_remove_think_tags(response) -> Optional[str]:
317333

318334
def _store_tool_calls(response) -> None:
319335
"""Extract and store tool calls from response in context."""
320-
tool_calls = getattr(response, "tool_calls", None)
336+
tool_calls = _extract_tool_calls_from_content_blocks(response)
337+
if not tool_calls:
338+
tool_calls = _extract_tool_calls_from_attribute(response)
321339
tool_calls_var.set(tool_calls)
322340

323341

342+
def _extract_tool_calls_from_content_blocks(response) -> List | None:
343+
if hasattr(response, "content_blocks"):
344+
tool_calls = []
345+
for block in response.content_blocks:
346+
if block.get("type") == "tool_call":
347+
tool_calls.append(block)
348+
return tool_calls if tool_calls else None
349+
return None
350+
351+
352+
def _extract_tool_calls_from_attribute(response) -> List | None:
353+
return getattr(response, "tool_calls", None)
354+
355+
324356
def _store_response_metadata(response) -> None:
325357
"""Store response metadata excluding content for metadata preservation.
326358

0 commit comments

Comments
 (0)