Skip to content
Draft
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
13 changes: 9 additions & 4 deletions nemoguardrails/actions/llm/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import json
import logging
import re
from typing import TYPE_CHECKING, Any, Dict, List, NoReturn, Optional, Union, cast
Expand Down Expand Up @@ -134,6 +135,10 @@ async def _stream_llm_call(

llm_response_metadata_var.set(accumulated_provider_metadata or None)

if tool_calls:
payload = json.dumps({"tool_calls": [tc.to_dict() for tc in tool_calls]})
await handler.push_chunk(payload)

await handler.finish()

llm_call_info = llm_call_info_var.get()
Expand Down Expand Up @@ -532,10 +537,10 @@ def get_colang_history(
history += f"# The result was {return_value}\n"
elif event["type"] == "mask_prev_user_message":
utterance_to_replace = get_last_user_utterance(events[:idx])
# We replace the last user utterance that led to jailbreak rail trigger with a placeholder text
split_history = history.rsplit(utterance_to_replace, 1)
placeholder_text = "<<<This text is hidden because the assistant should not talk about this.>>>"
history = placeholder_text.join(split_history)
if utterance_to_replace:
split_history = history.rsplit(utterance_to_replace, 1)
placeholder_text = "<<<This text is hidden because the assistant should not talk about this.>>>"
history = placeholder_text.join(split_history)

elif colang_version == "2.x":
new_history: List[str] = []
Expand Down
200 changes: 200 additions & 0 deletions nemoguardrails/llm/anthropic_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Conversion utilities between Anthropic Messages API and NeMo ChatMessage formats.

Used by both the server endpoint (messages.py) and the native Anthropic model
adapter (anthropic_chat.py).
"""

import json
from typing import Any, Dict, List, Optional, Tuple, Union

from nemoguardrails.types import ChatMessage, Role, ToolCall, ToolCallFunction


def anthropic_content_to_text(content: Union[str, List[Dict[str, Any]]]) -> str:
if isinstance(content, str):
return content
parts = []
for block in content:
if isinstance(block, str):
parts.append(block)
elif isinstance(block, dict):
if block.get("type") == "text":
parts.append(block.get("text", ""))
return "\n".join(parts) if parts else ""


def anthropic_tool_use_to_openai(content_blocks: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
tool_calls = []
for block in content_blocks:
if isinstance(block, dict) and block.get("type") == "tool_use":
args = block.get("input", {})
tool_calls.append(
{
"id": block.get("id", ""),
"type": "function",
"function": {
"name": block.get("name", ""),
"arguments": json.dumps(args) if isinstance(args, dict) else str(args),
},
}
)
return tool_calls


def anthropic_to_nemo_messages(
system: Optional[Union[str, List[Dict[str, Any]]]] = None,
messages: Optional[List[Dict[str, Any]]] = None,
) -> List[ChatMessage]:
result: List[ChatMessage] = []

if system:
system_text = anthropic_content_to_text(system) if not isinstance(system, str) else system
result.append(ChatMessage(role=Role.SYSTEM, content=system_text))

for msg in messages or []:
role_str = msg.get("role", "user")
content = msg.get("content", "")

if role_str == "user":
if isinstance(content, list):
tool_results = [b for b in content if isinstance(b, dict) and b.get("type") == "tool_result"]
if tool_results:
text = anthropic_content_to_text(content)
if text:
result.append(ChatMessage(role=Role.USER, content=text))
for tr in tool_results:
tr_content = tr.get("content", "")
tr_text = (
anthropic_content_to_text(tr_content)
if isinstance(tr_content, list)
else str(tr_content)
if tr_content
else ""
)
result.append(
ChatMessage(role=Role.TOOL, content=tr_text, tool_call_id=tr.get("tool_use_id", ""))
)
else:
text = anthropic_content_to_text(content)
result.append(ChatMessage(role=Role.USER, content=text))
else:
text = anthropic_content_to_text(content)
result.append(ChatMessage(role=Role.USER, content=text))

elif role_str == "assistant":
if isinstance(content, list):
text_parts = []
tool_calls = []
thinking_blocks = []
for block in content:
if isinstance(block, dict):
if block.get("type") == "text":
text_parts.append(block.get("text", ""))
elif block.get("type") == "tool_use":
args = block.get("input", {})
tool_calls.append(
ToolCall(
id=block.get("id", ""),
type="function",
function=ToolCallFunction(
name=block.get("name", ""),
arguments=args if isinstance(args, dict) else {},
),
)
)
elif block.get("type") in ("thinking", "redacted_thinking"):
thinking_blocks.append(block)
result.append(
ChatMessage(
role=Role.ASSISTANT,
content="\n".join(text_parts) if text_parts else "",
tool_calls=tool_calls if tool_calls else None,
reasoning=thinking_blocks if thinking_blocks else None,
)
)
else:
result.append(ChatMessage(role=Role.ASSISTANT, content=str(content) if content else ""))

elif role_str == "tool_result":
tool_use_id = msg.get("tool_use_id", "")
text = anthropic_content_to_text(content)
result.append(ChatMessage(role=Role.TOOL, content=text, tool_call_id=tool_use_id))

return result


def nemo_to_anthropic_messages(
messages: List[ChatMessage],
) -> Tuple[Optional[str], List[Dict[str, Any]]]:
system_text: Optional[str] = None
anthropic_messages: List[Dict[str, Any]] = []

for msg in messages:
if msg.role == Role.SYSTEM:
system_text = msg.content if isinstance(msg.content, str) else anthropic_content_to_text(msg.content or "")
continue

if msg.role == Role.USER:
content: Union[str, List[Dict[str, Any]]]
if isinstance(msg.content, list):
content = msg.content
else:
content = str(msg.content) if msg.content else ""
anthropic_messages.append({"role": "user", "content": content})

elif msg.role == Role.ASSISTANT:
blocks: List[Dict[str, Any]] = []
if msg.reasoning:
if isinstance(msg.reasoning, list):
blocks.extend(msg.reasoning)
elif isinstance(msg.reasoning, str):
blocks.append({"type": "thinking", "thinking": msg.reasoning})
if msg.content:
text = str(msg.content) if isinstance(msg.content, str) else anthropic_content_to_text(msg.content)
if text:
blocks.append({"type": "text", "text": text})
if msg.tool_calls:
for tc in msg.tool_calls:
blocks.append(
{
"type": "tool_use",
"id": tc.id,
"name": tc.function.name,
"input": tc.function.arguments,
}
)
anthropic_messages.append(
{"role": "assistant", "content": blocks if blocks else [{"type": "text", "text": ""}]}
)

elif msg.role == Role.TOOL:
content_blocks: List[Dict[str, Any]] = [{"type": "text", "text": str(msg.content) if msg.content else ""}]
anthropic_messages.append(
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": msg.tool_call_id or "",
"content": content_blocks,
}
],
}
)

return system_text, anthropic_messages
Loading