Entropic 2.3.8
Local-first agentic inference engine
Loading...
Searching...
No Matches
nemotron3_adapter.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
15#include "nemotron3_adapter.h"
16
18
19#include <nlohmann/json.hpp>
20
21#include <regex>
22#include <sstream>
23
24namespace entropic {
25
26namespace {
27auto logger = entropic::log::get("inference.adapter.nemotron3");
28
30constexpr const char* TOOL_RESULT_SUFFIX =
31 "Continue. Batch multiple tool calls in one response when possible.";
32
39std::string generate_uuid() {
40 static std::atomic<uint64_t> counter{0};
41 return "tc-" + std::to_string(counter.fetch_add(1, std::memory_order_relaxed));
42}
43
44} // anonymous namespace
45
46// ── Tool call parsing ──────────────────────────────────────
47
60ParseResult Nemotron3Adapter::parse_tool_calls(const std::string& content) const {
61 ParseResult result;
62
63 auto calls = parse_dsml_function_calls(content);
64 if (calls.empty()) {
65 calls = parse_xml_function_calls(content);
66 }
67 if (calls.empty()) {
68 calls = parse_tagged_tool_calls(content);
69 }
70
71 result.tool_calls = std::move(calls);
72 result.cleaned_content = clean_content(content);
73 return result;
74}
75
76// ── DSML invoke parsing (gh#70, native Nemotron 3 format) ──
77
90std::vector<ToolCall> Nemotron3Adapter::parse_dsml_function_calls(
91 const std::string& content) const
92{
93 std::vector<ToolCall> calls;
94 // Custom `re` delimiter: the pattern contains `)"` sequences (from
95 // the `name="..."` capture) that would terminate a default R"(...)"
96 // literal early.
97 std::regex invoke_pattern(
98 R"re(<|DSML|invoke name="([^"]+)">([\s\S]*?)</|DSML|invoke>)re");
99
100 auto begin = std::sregex_iterator(content.begin(), content.end(), invoke_pattern);
101 auto end = std::sregex_iterator();
102
103 for (auto it = begin; it != end; ++it) {
104 ToolCall tc;
105 tc.id = generate_uuid();
106 tc.name = (*it)[1].str();
107 tc.arguments = extract_dsml_parameters((*it)[2].str());
108 calls.push_back(std::move(tc));
109
110 logger->info("Parsed DSML invoke: {}", calls.back().name);
111 }
112 return calls;
113}
114
126std::unordered_map<std::string, std::string> Nemotron3Adapter::extract_dsml_parameters(
127 const std::string& invoke_body) const
128{
129 std::unordered_map<std::string, std::string> arguments;
130 // Custom `re` delimiter — same `)"`-in-pattern reason as the
131 // invoke regex above.
132 std::regex param_pattern(
133 R"re(<|DSML|parameter name="([^"]+)"\s+(?:string|int|bool|value)="([^"]*)"\s*/>)re");
134
135 auto begin = std::sregex_iterator(invoke_body.begin(), invoke_body.end(), param_pattern);
136 auto end = std::sregex_iterator();
137
138 for (auto it = begin; it != end; ++it) {
139 arguments[(*it)[1].str()] = (*it)[2].str();
140 }
141 return arguments;
142}
143
151std::vector<ToolCall> Nemotron3Adapter::parse_xml_function_calls(
152 const std::string& content) const
153{
154 std::vector<ToolCall> calls;
155 std::regex func_pattern(R"(<function=([^>]+)>([\s\S]*?)</function>)");
156
157 auto begin = std::sregex_iterator(content.begin(), content.end(), func_pattern);
158 auto end = std::sregex_iterator();
159
160 for (auto it = begin; it != end; ++it) {
161 std::string func_name = (*it)[1].str();
162 std::string func_body = (*it)[2].str();
163
164 auto ns = func_name.find_first_not_of(" \t");
165 auto ne = func_name.find_last_not_of(" \t");
166 if (ns != std::string::npos) {
167 func_name = func_name.substr(ns, ne - ns + 1);
168 }
169
170 ToolCall tc;
171 tc.id = generate_uuid();
172 tc.name = func_name;
173 tc.arguments = extract_xml_parameters(func_body);
174 calls.push_back(std::move(tc));
175
176 logger->info("Parsed XML tool call: {}", func_name);
177 }
178 return calls;
179}
180
188std::unordered_map<std::string, std::string> Nemotron3Adapter::extract_xml_parameters(
189 const std::string& func_body) const
190{
191 std::string body = func_body;
192 auto nested = body.find("<function=");
193 if (nested != std::string::npos) {
194 logger->warn("Truncating function body at nested <function= tag");
195 body = body.substr(0, nested);
196 }
197
198 std::unordered_map<std::string, std::string> arguments;
199 std::regex param_pattern(R"(<parameter=([^>]+)>([\s\S]*?)</parameter>)");
200
201 auto begin = std::sregex_iterator(body.begin(), body.end(), param_pattern);
202 auto end = std::sregex_iterator();
203
204 for (auto it = begin; it != end; ++it) {
205 std::string key = (*it)[1].str();
206 std::string value = (*it)[2].str();
207
208 auto ks = key.find_first_not_of(" \t\n\r");
209 auto ke = key.find_last_not_of(" \t\n\r");
210 if (ks != std::string::npos) key = key.substr(ks, ke - ks + 1);
211
212 auto vs = value.find_first_not_of(" \t\n\r");
213 auto ve = value.find_last_not_of(" \t\n\r");
214 if (vs != std::string::npos) value = value.substr(vs, ve - vs + 1);
215
216 if (key.empty() || value.empty()) {
217 logger->warn("Skipping empty XML parameter: key='{}' value='{}'", key, value);
218 continue;
219 }
220 arguments[key] = value;
221 }
222 return arguments;
223}
224
225// ── Tool result formatting ─────────────────────────────────
226
236 const ToolCall& /*tool_call*/,
237 const std::string& result) const
238{
239 Message msg;
240 msg.role = "user";
241 msg.content = "<tool_response>\n" + result +
242 "\n</tool_response>\n\n" + TOOL_RESULT_SUFFIX;
243 return msg;
244}
245
246// ── Tool definition formatting ─────────────────────────────
247
255static nlohmann::json build_tool_defs(
256 const std::vector<std::string>& tool_jsons)
257{
258 nlohmann::json tool_defs = nlohmann::json::array();
259 for (const auto& json_str : tool_jsons) {
260 try {
261 auto j = nlohmann::json::parse(json_str);
262 tool_defs.push_back({
263 {"type", "function"},
264 {"function", {
265 {"name", j.value("name", "unknown")},
266 {"description", j.value("description", "")},
267 {"parameters", j.value("inputSchema",
268 nlohmann::json::object())}
269 }}
270 });
271 } catch (...) {
272 continue;
273 }
274 }
275 return tool_defs;
276}
277
291 const std::vector<std::string>& tool_jsons) const
292{
293 std::ostringstream out;
294 out << "# Tools\n\n"
295 << "You may call one or more functions to assist with the user query.\n"
296 << "Put your final answer OUTSIDE of any tool calls.\n\n"
297 << "Here are the available tools:\n"
298 << "<tools>\n"
299 << build_tool_defs(tool_jsons).dump(2) << "\n"
300 << "</tools>\n\n"
301 << "For each function call, emit a DSML invoke block:\n"
302 << "<|DSML|function_calls>\n"
303 << "<|DSML|invoke name=\"example.tool\">\n"
304 << "<|DSML|parameter name=\"param_name\" string=\"value\"/>\n"
305 << "</|DSML|invoke>\n"
306 << "</|DSML|function_calls>";
307 return out.str();
308}
309
310// ── Content cleaning ───────────────────────────────────────
311
328std::string Nemotron3Adapter::clean_content(const std::string& content) const {
329 std::string cleaned = std::regex_replace(content,
330 std::regex(R"(<tool_call>\s*[\s\S]*?\s*</tool_call>)"), "");
331 cleaned = std::regex_replace(cleaned,
332 std::regex(R"(<function=[^>]+>[\s\S]*?</function>)"), "");
333 cleaned = std::regex_replace(cleaned,
334 std::regex(R"(<|DSML|function_calls>[\s\S]*?</|DSML|function_calls>)"), "");
335 cleaned = std::regex_replace(cleaned,
336 std::regex(R"(<|DSML|invoke[\s\S]*?</|DSML|invoke>)"), "");
337 cleaned = std::regex_replace(cleaned,
338 std::regex(R"(</?|[^>]*>)"), "");
339 cleaned = strip_think_blocks(cleaned);
340 return cleaned;
341}
342
343} // namespace entropic
std::vector< ToolCall > parse_tagged_tool_calls(const std::string &content) const
Parse <tool_call>JSON</tool_call> tagged blocks.
std::string strip_think_blocks(const std::string &content) const
Strip all <think>...</think> blocks from content.
std::string format_tools(const std::vector< std::string > &tool_jsons) const override
Format tools as a <tools> JSON array, then teach DSML invoke.
Message format_tool_result(const ToolCall &tool_call, const std::string &result) const override
Wrap tool result in <tool_response> tags.
ParseResult parse_tool_calls(const std::string &content) const override
Parse DSML invoke calls; fall back to qwen XML, then tagged JSON.
spdlog initialization and logger access.
ENTROPIC_EXPORT std::shared_ptr< spdlog::logger > get(const std::string &name)
Get or create a named logger.
Definition logging.cpp:211
Activate model on GPU (WARM → ACTIVE).
std::string generate_uuid()
Generate a UUID v4 string.
Definition backend.cpp:840
static nlohmann::json build_tool_defs(const std::vector< std::string > &tool_jsons)
Build the OpenAI-function <tools> JSON array for injection.
Nemotron 3 chat adapter (v2.1.9, gh#47).
A message in a conversation.
Definition message.h:35
std::string content
Message text content (always populated)
Definition message.h:37
std::string role
Message role.
Definition message.h:36
Parsed tool call result: cleaned content + extracted calls.
std::string cleaned_content
Content with tool calls removed.
std::vector< ToolCall > tool_calls
Extracted tool calls.
A tool call request parsed from model output.
Definition tool_call.h:31
std::unordered_map< std::string, std::string > arguments
Tool arguments as string key-value pairs.
Definition tool_call.h:34
std::string id
Unique call ID (UUID)
Definition tool_call.h:32
std::string name
Tool name (e.g. "filesystem.read_file")
Definition tool_call.h:33