nvidia-nat 1.3.0rc3__py3-none-any.whl → 1.3.0rc5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
nat/agent/base.py CHANGED
@@ -102,11 +102,11 @@ class BaseAgent(ABC):
102
102
  AIMessage
103
103
  The LLM response
104
104
  """
105
- output_message = ""
105
+ output_message = []
106
106
  async for event in runnable.astream(inputs, config=config):
107
- output_message += event.content
107
+ output_message.append(event.content)
108
108
 
109
- return AIMessage(content=output_message)
109
+ return AIMessage(content="".join(output_message))
110
110
 
111
111
  async def _call_llm(self, llm: Runnable, inputs: dict[str, Any], config: RunnableConfig | None = None) -> AIMessage:
112
112
  """
@@ -162,7 +162,7 @@ async def react_agent_workflow(config: ReActAgentWorkflowConfig, builder: Builde
162
162
  return GlobalTypeConverter.get().convert(response, to_type=str)
163
163
  return response
164
164
  except Exception as ex:
165
- logger.exception("%s ReAct Agent failed with exception: %s", AGENT_LOG_PREFIX, str(ex))
166
- raise RuntimeError
165
+ logger.error("%s ReAct Agent failed with exception: %s", AGENT_LOG_PREFIX, str(ex))
166
+ raise
167
167
 
168
168
  yield FunctionInfo.from_fn(_response_fn, description=config.description)
@@ -157,12 +157,12 @@ async def build_reasoning_function(config: ReasoningFunctionConfig, builder: Bui
157
157
  prompt = prompt.to_string()
158
158
 
159
159
  # Get the reasoning output from the LLM
160
- reasoning_output = ""
160
+ reasoning_output = []
161
161
 
162
162
  async for chunk in llm.astream(prompt):
163
- reasoning_output += chunk.content
163
+ reasoning_output.append(chunk.content)
164
164
 
165
- reasoning_output = remove_r1_think_tags(reasoning_output)
165
+ reasoning_output = remove_r1_think_tags("".join(reasoning_output))
166
166
 
167
167
  output = await downstream_template.ainvoke(input={
168
168
  "input_text": input_text, "reasoning_output": reasoning_output
@@ -200,12 +200,12 @@ async def build_reasoning_function(config: ReasoningFunctionConfig, builder: Bui
200
200
  prompt = prompt.to_string()
201
201
 
202
202
  # Get the reasoning output from the LLM
203
- reasoning_output = ""
203
+ reasoning_output = []
204
204
 
205
205
  async for chunk in llm.astream(prompt):
206
- reasoning_output += chunk.content
206
+ reasoning_output.append(chunk.content)
207
207
 
208
- reasoning_output = remove_r1_think_tags(reasoning_output)
208
+ reasoning_output = remove_r1_think_tags("".join(reasoning_output))
209
209
 
210
210
  output = await downstream_template.ainvoke(input={
211
211
  "input_text": input_text, "reasoning_output": reasoning_output
@@ -169,7 +169,7 @@ async def rewoo_agent_workflow(config: ReWOOAgentWorkflowConfig, builder: Builde
169
169
  return GlobalTypeConverter.get().convert(response, to_type=str)
170
170
  return response
171
171
  except Exception as ex:
172
- logger.exception("ReWOO Agent failed with exception: %s", ex)
173
- raise RuntimeError
172
+ logger.error("ReWOO Agent failed with exception: %s", ex)
173
+ raise
174
174
 
175
175
  yield FunctionInfo.from_fn(_response_fn, description=config.description)
@@ -233,14 +233,10 @@ def create_tool_calling_agent_prompt(config: "ToolCallAgentWorkflowConfig") -> s
233
233
  """
234
234
  # the Tool Calling Agent prompt can be customized via config option system_prompt and additional_instructions.
235
235
 
236
- if config.system_prompt:
237
- prompt_str = config.system_prompt
238
- else:
239
- prompt_str = ""
240
-
241
- if config.additional_instructions:
242
- prompt_str += f" {config.additional_instructions}"
243
-
244
- if len(prompt_str) > 0:
245
- return prompt_str
236
+ prompt_strs = []
237
+ for msg in [config.system_prompt, config.additional_instructions]:
238
+ if msg is not None:
239
+ prompt_strs.append(msg)
240
+ if prompt_strs:
241
+ return " ".join(prompt_strs)
246
242
  return None
@@ -118,8 +118,8 @@ async def tool_calling_agent_workflow(config: ToolCallAgentWorkflowConfig, build
118
118
  output_message = state.messages[-1]
119
119
  return str(output_message.content)
120
120
  except Exception as ex:
121
- logger.exception("%s Tool Calling Agent failed with exception: %s", AGENT_LOG_PREFIX, ex)
122
- raise RuntimeError
121
+ logger.error("%s Tool Calling Agent failed with exception: %s", AGENT_LOG_PREFIX, ex)
122
+ raise
123
123
 
124
124
  try:
125
125
  yield FunctionInfo.from_fn(_response_fn, description=config.description)
nat/builder/function.py CHANGED
@@ -159,8 +159,7 @@ class Function(FunctionBase[InputT, StreamingOutputT, SingleOutputT], ABC):
159
159
 
160
160
  return result
161
161
  except Exception as e:
162
- err_msg = f"Error: {e}" if str(e).strip() else ""
163
- logger.error("Error with ainvoke in function with input: %s. %s", value, err_msg)
162
+ logger.error("Error with ainvoke in function with input: %s. Error: %s", value, e)
164
163
  raise
165
164
 
166
165
  @typing.final
@@ -121,7 +121,15 @@ class Message(BaseModel):
121
121
  role: UserMessageContentRoleType
122
122
 
123
123
 
124
- class ChatRequestOptionals(BaseModel):
124
+ class ChatRequest(BaseModel):
125
+ """
126
+ ChatRequest is a data model that represents a request to the NAT chat API.
127
+ Fully compatible with OpenAI Chat Completions API specification.
128
+ """
129
+
130
+ # Required fields
131
+ messages: typing.Annotated[list[Message], conlist(Message, min_length=1)]
132
+
125
133
  # Optional fields (OpenAI Chat Completions API compatible)
126
134
  model: str | None = Field(default=None, description="name of the model to use")
127
135
  frequency_penalty: float | None = Field(default=0.0,
@@ -145,17 +153,6 @@ class ChatRequestOptionals(BaseModel):
145
153
  tool_choice: str | dict[str, typing.Any] | None = Field(default=None, description="Controls which tool is called")
146
154
  parallel_tool_calls: bool | None = Field(default=True, description="Whether to enable parallel function calling")
147
155
  user: str | None = Field(default=None, description="Unique identifier representing end-user")
148
-
149
-
150
- class ChatRequest(ChatRequestOptionals):
151
- """
152
- ChatRequest is a data model that represents a request to the NAT chat API.
153
- Fully compatible with OpenAI Chat Completions API specification.
154
- """
155
-
156
- # Required fields
157
- messages: typing.Annotated[list[Message], conlist(Message, min_length=1)]
158
-
159
156
  model_config = ConfigDict(extra="allow",
160
157
  json_schema_extra={
161
158
  "example": {
@@ -197,39 +194,82 @@ class ChatRequest(ChatRequestOptionals):
197
194
  top_p=top_p)
198
195
 
199
196
 
200
- class ChatRequestOrMessage(ChatRequestOptionals):
197
+ class ChatRequestOrMessage(BaseModel):
201
198
  """
202
- ChatRequestOrMessage is a data model that represents either a conversation or a string input.
199
+ `ChatRequestOrMessage` is a data model that represents either a conversation or a string input.
203
200
  This is useful for functions that can handle either type of input.
204
201
 
205
- `messages` is compatible with the OpenAI Chat Completions API specification.
206
-
207
- `input_string` is a string input that can be used for functions that do not require a conversation.
208
- """
202
+ - `messages` is compatible with the OpenAI Chat Completions API specification.
203
+ - `input_message` is a string input that can be used for functions that do not require a conversation.
204
+
205
+ Note: When `messages` is provided, extra fields are allowed to enable lossless round-trip
206
+ conversion with ChatRequest. When `input_message` is provided, no extra fields are permitted.
207
+ """
208
+ model_config = ConfigDict(
209
+ extra="allow",
210
+ json_schema_extra={
211
+ "examples": [
212
+ {
213
+ "input_message": "What can you do?"
214
+ },
215
+ {
216
+ "messages": [{
217
+ "role": "user", "content": "What can you do?"
218
+ }],
219
+ "model": "nvidia/nemotron",
220
+ "temperature": 0.7
221
+ },
222
+ ],
223
+ "oneOf": [
224
+ {
225
+ "required": ["input_message"],
226
+ "properties": {
227
+ "input_message": {
228
+ "type": "string"
229
+ },
230
+ },
231
+ "additionalProperties": {
232
+ "not": True, "errorMessage": 'remove additional property ${0#}'
233
+ },
234
+ },
235
+ {
236
+ "required": ["messages"],
237
+ "properties": {
238
+ "messages": {
239
+ "type": "array"
240
+ },
241
+ },
242
+ "additionalProperties": True
243
+ },
244
+ ]
245
+ },
246
+ )
209
247
 
210
248
  messages: typing.Annotated[list[Message] | None, conlist(Message, min_length=1)] = Field(
211
- default=None, description="The conversation messages to process.")
249
+ default=None, description="A non-empty conversation of messages to process.")
212
250
 
213
- input_string: str | None = Field(default=None, alias="input_message", description="The input message to process.")
251
+ input_message: str | None = Field(
252
+ default=None,
253
+ description="A single input message to process. Useful for functions that do not require a conversation")
214
254
 
215
255
  @property
216
256
  def is_string(self) -> bool:
217
- return self.input_string is not None
257
+ return self.input_message is not None
218
258
 
219
259
  @property
220
260
  def is_conversation(self) -> bool:
221
261
  return self.messages is not None
222
262
 
223
263
  @model_validator(mode="after")
224
- def validate_messages_or_input_string(self):
225
- if self.messages is not None and self.input_string is not None:
226
- raise ValueError("Either messages or input_message/input_string must be provided, not both")
227
- if self.messages is None and self.input_string is None:
228
- raise ValueError("Either messages or input_message/input_string must be provided")
229
- if self.input_string is not None:
230
- extra_fields = self.model_dump(exclude={"input_string"}, exclude_none=True, exclude_unset=True)
264
+ def validate_model(self):
265
+ if self.messages is not None and self.input_message is not None:
266
+ raise ValueError("Either messages or input_message must be provided, not both")
267
+ if self.messages is None and self.input_message is None:
268
+ raise ValueError("Either messages or input_message must be provided")
269
+ if self.input_message is not None:
270
+ extra_fields = self.model_dump(exclude={"input_message"}, exclude_none=True, exclude_unset=True)
231
271
  if len(extra_fields) > 0:
232
- raise ValueError("no extra fields are permitted when input_message/input_string is provided")
272
+ raise ValueError("no extra fields are permitted when input_message is provided")
233
273
  return self
234
274
 
235
275
 
@@ -701,9 +741,9 @@ GlobalTypeConverter.register_converter(_string_to_nat_chat_request)
701
741
 
702
742
 
703
743
  def _chat_request_or_message_to_chat_request(data: ChatRequestOrMessage) -> ChatRequest:
704
- if data.input_string is not None:
705
- return _string_to_nat_chat_request(data.input_string)
706
- return ChatRequest(**data.model_dump(exclude={"input_string"}))
744
+ if data.input_message is not None:
745
+ return _string_to_nat_chat_request(data.input_message)
746
+ return ChatRequest(**data.model_dump(exclude={"input_message"}))
707
747
 
708
748
 
709
749
  GlobalTypeConverter.register_converter(_chat_request_or_message_to_chat_request)
@@ -717,7 +757,17 @@ GlobalTypeConverter.register_converter(_chat_request_to_chat_request_or_message)
717
757
 
718
758
 
719
759
  def _chat_request_or_message_to_string(data: ChatRequestOrMessage) -> str:
720
- return data.input_string or ""
760
+ if data.input_message is not None:
761
+ return data.input_message
762
+ # Extract content from last message in conversation
763
+ if data.messages is None:
764
+ return ""
765
+ content = data.messages[-1].content
766
+ if content is None:
767
+ return ""
768
+ if isinstance(content, str):
769
+ return content
770
+ return str(content)
721
771
 
722
772
 
723
773
  GlobalTypeConverter.register_converter(_chat_request_or_message_to_string)
@@ -51,7 +51,7 @@ class ThinkingMixin(
51
51
  Returns the system prompt to use for thinking.
52
52
  For NVIDIA Nemotron, returns "/think" if enabled, else "/no_think".
53
53
  For Llama Nemotron v1.5, returns "/think" if enabled, else "/no_think".
54
- For Llama Nemotron v1.0, returns "detailed thinking on" if enabled, else "detailed thinking off".
54
+ For Llama Nemotron v1.0 or v1.1, returns "detailed thinking on" if enabled, else "detailed thinking off".
55
55
  If thinking is not supported on the model, returns None.
56
56
 
57
57
  Returns:
@@ -72,7 +72,7 @@ class ThinkingMixin(
72
72
  return "/think" if self.thinking else "/no_think"
73
73
 
74
74
  if model.startswith("nvidia/llama"):
75
- if "v1-0" in model or "v1-1" in model:
75
+ if "v1-0" in model or "v1-1" in model or model.endswith("v1"):
76
76
  return f"detailed thinking {'on' if self.thinking else 'off'}"
77
77
 
78
78
  if "v1-5" in model:
@@ -1184,6 +1184,7 @@ class FastApiFrontEndPluginWorker(FastApiFrontEndPluginWorkerBase):
1184
1184
  "server": client.server_name,
1185
1185
  "transport": config.server.transport,
1186
1186
  "session_healthy": session_healthy,
1187
+ "protected": True if config.server.auth_provider is not None else False,
1187
1188
  "tools": tools_info,
1188
1189
  "total_tools": len(configured_short_names),
1189
1190
  "available_tools": available_count
@@ -1196,6 +1197,7 @@ class FastApiFrontEndPluginWorker(FastApiFrontEndPluginWorkerBase):
1196
1197
  "server": "unknown",
1197
1198
  "transport": config.server.transport if config.server else "unknown",
1198
1199
  "session_healthy": False,
1200
+ "protected": False,
1199
1201
  "error": str(e),
1200
1202
  "tools": [],
1201
1203
  "total_tools": 0,
@@ -1226,6 +1228,7 @@ class FastApiFrontEndPluginWorker(FastApiFrontEndPluginWorkerBase):
1226
1228
  "server": "streamable-http:http://localhost:9901/mcp",
1227
1229
  "transport": "streamable-http",
1228
1230
  "session_healthy": True,
1231
+ "protected": False,
1229
1232
  "tools": [{
1230
1233
  "name": "tool_a",
1231
1234
  "description": "Tool A description",
@@ -25,6 +25,7 @@ from pydantic import ValidationError
25
25
  from starlette.websockets import WebSocketDisconnect
26
26
 
27
27
  from nat.authentication.interfaces import FlowHandlerBase
28
+ from nat.data_models.api_server import ChatRequest
28
29
  from nat.data_models.api_server import ChatResponse
29
30
  from nat.data_models.api_server import ChatResponseChunk
30
31
  from nat.data_models.api_server import Error
@@ -33,6 +34,8 @@ from nat.data_models.api_server import ResponsePayloadOutput
33
34
  from nat.data_models.api_server import ResponseSerializable
34
35
  from nat.data_models.api_server import SystemResponseContent
35
36
  from nat.data_models.api_server import TextContent
37
+ from nat.data_models.api_server import UserMessageContentRoleType
38
+ from nat.data_models.api_server import UserMessages
36
39
  from nat.data_models.api_server import WebSocketMessageStatus
37
40
  from nat.data_models.api_server import WebSocketMessageType
38
41
  from nat.data_models.api_server import WebSocketSystemInteractionMessage
@@ -64,12 +67,12 @@ class WebSocketMessageHandler:
64
67
  self._running_workflow_task: asyncio.Task | None = None
65
68
  self._message_parent_id: str = "default_id"
66
69
  self._conversation_id: str | None = None
67
- self._workflow_schema_type: str = None
68
- self._user_interaction_response: asyncio.Future[HumanResponse] | None = None
70
+ self._workflow_schema_type: str | None = None
71
+ self._user_interaction_response: asyncio.Future[TextContent] | None = None
69
72
 
70
73
  self._flow_handler: FlowHandlerBase | None = None
71
74
 
72
- self._schema_output_mapping: dict[str, type[BaseModel] | None] = {
75
+ self._schema_output_mapping: dict[str, type[BaseModel] | type[None]] = {
73
76
  WorkflowSchemaType.GENERATE: self._session_manager.workflow.single_output_schema,
74
77
  WorkflowSchemaType.CHAT: ChatResponse,
75
78
  WorkflowSchemaType.CHAT_STREAM: ChatResponseChunk,
@@ -114,36 +117,58 @@ class WebSocketMessageHandler:
114
117
  pass
115
118
 
116
119
  elif (isinstance(validated_message, WebSocketUserInteractionResponseMessage)):
117
- user_content = await self.process_user_message_content(validated_message)
120
+ user_content = await self._process_websocket_user_interaction_response_message(validated_message)
121
+ assert self._user_interaction_response is not None
118
122
  self._user_interaction_response.set_result(user_content)
119
123
  except (asyncio.CancelledError, WebSocketDisconnect):
120
124
  # TODO: Handle the disconnect
121
125
  break
122
126
 
123
- async def process_user_message_content(
124
- self, user_content: WebSocketUserMessage | WebSocketUserInteractionResponseMessage) -> BaseModel | None:
127
+ def _extract_last_user_message_content(self, messages: list[UserMessages]) -> TextContent:
125
128
  """
126
- Processes the contents of a user message.
129
+ Extracts the last user's TextContent from a list of messages.
127
130
 
128
- :param user_content: Incoming content data model.
129
- :return: A validated Pydantic user content model or None if not found.
130
- """
131
+ Args:
132
+ messages: List of UserMessages.
131
133
 
132
- for user_message in user_content.content.messages[::-1]:
133
- if (user_message.role == "user"):
134
+ Returns:
135
+ TextContent object from the last user message.
134
136
 
137
+ Raises:
138
+ ValueError: If no user text content is found.
139
+ """
140
+ for user_message in messages[::-1]:
141
+ if user_message.role == UserMessageContentRoleType.USER:
135
142
  for attachment in user_message.content:
136
-
137
143
  if isinstance(attachment, TextContent):
138
144
  return attachment
145
+ raise ValueError("No user text content found in messages.")
146
+
147
+ async def _process_websocket_user_interaction_response_message(
148
+ self, user_content: WebSocketUserInteractionResponseMessage) -> TextContent:
149
+ """
150
+ Processes a WebSocketUserInteractionResponseMessage.
151
+ """
152
+ return self._extract_last_user_message_content(user_content.content.messages)
139
153
 
140
- return None
154
+ async def _process_websocket_user_message(self, user_content: WebSocketUserMessage) -> ChatRequest | str:
155
+ """
156
+ Processes a WebSocketUserMessage based on schema type.
157
+ """
158
+ if self._workflow_schema_type in [WorkflowSchemaType.CHAT, WorkflowSchemaType.CHAT_STREAM]:
159
+ return ChatRequest(**user_content.content.model_dump(include={"messages"}))
160
+
161
+ elif self._workflow_schema_type in [WorkflowSchemaType.GENERATE, WorkflowSchemaType.GENERATE_STREAM]:
162
+ return self._extract_last_user_message_content(user_content.content.messages).text
163
+
164
+ raise ValueError("Unsupported workflow schema type for WebSocketUserMessage")
141
165
 
142
166
  async def process_workflow_request(self, user_message_as_validated_type: WebSocketUserMessage) -> None:
143
167
  """
144
168
  Process user messages and routes them appropriately.
145
169
 
146
- :param user_message_as_validated_type: A WebSocketUserMessage Data Model instance.
170
+ Args:
171
+ user_message_as_validated_type (WebSocketUserMessage): The validated user message to process.
147
172
  """
148
173
 
149
174
  try:
@@ -151,18 +176,15 @@ class WebSocketMessageHandler:
151
176
  self._workflow_schema_type = user_message_as_validated_type.schema_type
152
177
  self._conversation_id = user_message_as_validated_type.conversation_id
153
178
 
154
- content: BaseModel | None = await self.process_user_message_content(user_message_as_validated_type)
155
-
156
- if content is None:
157
- raise ValueError(f"User message content could not be found: {user_message_as_validated_type}")
179
+ message_content: typing.Any = await self._process_websocket_user_message(user_message_as_validated_type)
158
180
 
159
- if isinstance(content, TextContent) and (self._running_workflow_task is None):
181
+ if (self._running_workflow_task is None):
160
182
 
161
- def _done_callback(task: asyncio.Task):
183
+ def _done_callback(_task: asyncio.Task):
162
184
  self._running_workflow_task = None
163
185
 
164
186
  self._running_workflow_task = asyncio.create_task(
165
- self._run_workflow(payload=content.text,
187
+ self._run_workflow(payload=message_content,
166
188
  user_message_id=self._message_parent_id,
167
189
  conversation_id=self._conversation_id,
168
190
  result_type=self._schema_output_mapping[self._workflow_schema_type],
@@ -180,13 +202,14 @@ class WebSocketMessageHandler:
180
202
  async def create_websocket_message(self,
181
203
  data_model: BaseModel,
182
204
  message_type: str | None = None,
183
- status: str = WebSocketMessageStatus.IN_PROGRESS) -> None:
205
+ status: WebSocketMessageStatus = WebSocketMessageStatus.IN_PROGRESS) -> None:
184
206
  """
185
207
  Creates a websocket message that will be ready for routing based on message type or data model.
186
208
 
187
- :param data_model: Message content model.
188
- :param message_type: Message content model.
189
- :param status: Message content model.
209
+ Args:
210
+ data_model (BaseModel): Message content model.
211
+ message_type (str | None): Message content model.
212
+ status (WebSocketMessageStatus): Message content model.
190
213
  """
191
214
  try:
192
215
  message: BaseModel | None = None
@@ -196,8 +219,8 @@ class WebSocketMessageHandler:
196
219
 
197
220
  message_schema: type[BaseModel] = await self._message_validator.get_message_schema_by_type(message_type)
198
221
 
199
- if 'id' in data_model.model_fields:
200
- message_id: str = data_model.id
222
+ if hasattr(data_model, 'id'):
223
+ message_id: str = str(getattr(data_model, 'id'))
201
224
  else:
202
225
  message_id = str(uuid.uuid4())
203
226
 
@@ -253,12 +276,15 @@ class WebSocketMessageHandler:
253
276
  Registered human interaction callback that processes human interactions and returns
254
277
  responses from websocket connection.
255
278
 
256
- :param prompt: Incoming interaction content data model.
257
- :return: A Text Content Base Pydantic model.
279
+ Args:
280
+ prompt: Incoming interaction content data model.
281
+
282
+ Returns:
283
+ A Text Content Base Pydantic model.
258
284
  """
259
285
 
260
286
  # First create a future from the loop for the human response
261
- human_response_future: asyncio.Future[HumanResponse] = asyncio.get_running_loop().create_future()
287
+ human_response_future: asyncio.Future[TextContent] = asyncio.get_running_loop().create_future()
262
288
 
263
289
  # Then add the future to the outstanding human prompts dictionary
264
290
  self._user_interaction_response = human_response_future
@@ -274,10 +300,10 @@ class WebSocketMessageHandler:
274
300
  return HumanResponseNotification()
275
301
 
276
302
  # Wait for the human response future to complete
277
- interaction_response: HumanResponse = await human_response_future
303
+ text_content: TextContent = await human_response_future
278
304
 
279
305
  interaction_response: HumanResponse = await self._message_validator.convert_text_content_to_human_response(
280
- interaction_response, prompt.content)
306
+ text_content, prompt.content)
281
307
 
282
308
  return interaction_response
283
309
 
@@ -293,13 +319,12 @@ class WebSocketMessageHandler:
293
319
  output_type: type | None = None) -> None:
294
320
 
295
321
  try:
296
- async with self._session_manager.session(
297
- user_message_id=user_message_id,
298
- conversation_id=conversation_id,
299
- http_connection=self._socket,
300
- user_input_callback=self.human_interaction_callback,
301
- user_authentication_callback=(self._flow_handler.authenticate
302
- if self._flow_handler else None)) as session:
322
+ auth_callback = self._flow_handler.authenticate if self._flow_handler else None
323
+ async with self._session_manager.session(user_message_id=user_message_id,
324
+ conversation_id=conversation_id,
325
+ http_connection=self._socket,
326
+ user_input_callback=self.human_interaction_callback,
327
+ user_authentication_callback=auth_callback) as session:
303
328
 
304
329
  async for value in generate_streaming_response(payload,
305
330
  session_manager=session,
@@ -240,8 +240,7 @@ class MessageValidator:
240
240
  thread_id: str = "default",
241
241
  parent_id: str = "default",
242
242
  conversation_id: str | None = None,
243
- content: SystemResponseContent
244
- | Error = SystemResponseContent(),
243
+ content: SystemResponseContent | Error = SystemResponseContent(),
245
244
  status: WebSocketMessageStatus = WebSocketMessageStatus.IN_PROGRESS,
246
245
  timestamp: str = str(datetime.datetime.now(datetime.UTC))
247
246
  ) -> WebSocketSystemResponseTokenMessage | None:
@@ -13,13 +13,17 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
 
16
+ import logging
16
17
  from typing import Literal
17
18
 
18
19
  from pydantic import Field
20
+ from pydantic import model_validator
19
21
 
20
22
  from nat.authentication.oauth2.oauth2_resource_server_config import OAuth2ResourceServerConfig
21
23
  from nat.data_models.front_end import FrontEndBaseConfig
22
24
 
25
+ logger = logging.getLogger(__name__)
26
+
23
27
 
24
28
  class MCPFrontEndConfig(FrontEndBaseConfig, name="mcp"):
25
29
  """MCP front end configuration.
@@ -43,3 +47,31 @@ class MCPFrontEndConfig(FrontEndBaseConfig, name="mcp"):
43
47
 
44
48
  server_auth: OAuth2ResourceServerConfig | None = Field(
45
49
  default=None, description=("OAuth 2.0 Resource Server configuration for token verification."))
50
+
51
+ @model_validator(mode="after")
52
+ def validate_security_configuration(self):
53
+ """Validate security configuration to prevent accidental misconfigurations."""
54
+ # Check if server is bound to a non-localhost interface without authentication
55
+ localhost_hosts = {"localhost", "127.0.0.1", "::1"}
56
+ if self.host not in localhost_hosts and self.server_auth is None:
57
+ logger.warning(
58
+ "MCP server is configured to bind to '%s' without authentication. "
59
+ "This may expose your server to unauthorized access. "
60
+ "Consider either: (1) binding to localhost for local-only access, "
61
+ "or (2) configuring server_auth for production deployments on public interfaces.",
62
+ self.host)
63
+
64
+ # Check if SSE transport is used (which doesn't support authentication)
65
+ if self.transport == "sse":
66
+ if self.server_auth is not None:
67
+ logger.warning("SSE transport does not support authentication. "
68
+ "The configured server_auth will be ignored. "
69
+ "For production use with authentication, use 'streamable-http' transport instead.")
70
+ elif self.host not in localhost_hosts:
71
+ logger.warning(
72
+ "SSE transport does not support authentication and is bound to '%s'. "
73
+ "This configuration is not recommended for production use. "
74
+ "For production deployments, use 'streamable-http' transport with server_auth configured.",
75
+ self.host)
76
+
77
+ return self
@@ -105,9 +105,12 @@ class MCPFrontEndPlugin(FrontEndBase[MCPFrontEndConfig]):
105
105
 
106
106
  # Start the MCP server with configurable transport
107
107
  # streamable-http is the default, but users can choose sse if preferred
108
- if self.front_end_config.transport == "sse":
109
- logger.info("Starting MCP server with SSE endpoint at /sse")
110
- await mcp.run_sse_async()
111
- else: # streamable-http
112
- logger.info("Starting MCP server with streamable-http endpoint at /mcp/")
113
- await mcp.run_streamable_http_async()
108
+ try:
109
+ if self.front_end_config.transport == "sse":
110
+ logger.info("Starting MCP server with SSE endpoint at /sse")
111
+ await mcp.run_sse_async()
112
+ else: # streamable-http
113
+ logger.info("Starting MCP server with streamable-http endpoint at /mcp/")
114
+ await mcp.run_streamable_http_async()
115
+ except KeyboardInterrupt:
116
+ logger.info("MCP server shutdown requested (Ctrl+C). Shutting down gracefully.")
nat/runtime/runner.py CHANGED
@@ -196,8 +196,7 @@ class Runner:
196
196
 
197
197
  return result
198
198
  except Exception as e:
199
- err_msg = f": {e}" if str(e).strip() else "."
200
- logger.error("Error running workflow%s", err_msg)
199
+ logger.error("Error running workflow: %s", e)
201
200
  event_stream = self._context_state.event_stream.get()
202
201
  if event_stream:
203
202
  event_stream.on_complete()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nvidia-nat
3
- Version: 1.3.0rc3
3
+ Version: 1.3.0rc5
4
4
  Summary: NVIDIA NeMo Agent toolkit
5
5
  Author: NVIDIA Corporation
6
6
  Maintainer: NVIDIA Corporation
@@ -17,7 +17,7 @@ Description-Content-Type: text/markdown
17
17
  License-File: LICENSE-3rd-party.txt
18
18
  License-File: LICENSE.md
19
19
  Requires-Dist: aioboto3>=11.0.0
20
- Requires-Dist: authlib~=1.5
20
+ Requires-Dist: authlib<2.0.0,>=1.6.5
21
21
  Requires-Dist: click~=8.1
22
22
  Requires-Dist: colorama~=0.4.6
23
23
  Requires-Dist: datasets~=4.0
@@ -103,7 +103,6 @@ Requires-Dist: nat_multi_frameworks; extra == "examples"
103
103
  Requires-Dist: nat_plot_charts; extra == "examples"
104
104
  Requires-Dist: nat_por_to_jiratickets; extra == "examples"
105
105
  Requires-Dist: nat_profiler_agent; extra == "examples"
106
- Requires-Dist: nat_redact_pii; extra == "examples"
107
106
  Requires-Dist: nat_router_agent; extra == "examples"
108
107
  Requires-Dist: nat_semantic_kernel_demo; extra == "examples"
109
108
  Requires-Dist: nat_sequential_executor; extra == "examples"
@@ -1,6 +1,6 @@
1
1
  aiq/__init__.py,sha256=qte-NlwgM990yEeyaRUxA4IBQq3PaEkQUCK3i95iwPw,2341
2
2
  nat/agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- nat/agent/base.py,sha256=8cXz9Py8sFs7rTF4V9d3uzZAYFVquuWL0RQSkVMdnb0,10155
3
+ nat/agent/base.py,sha256=Q6byRPl4mrfVFD3boKt5yftnnJ3ucH9l39FTt3QOQbg,10169
4
4
  nat/agent/dual_node.py,sha256=pfvXa1iLKtrNBHsh-tM5RWRmVe7QkyYhQNanOfWdJJs,2569
5
5
  nat/agent/register.py,sha256=rPhHDyqRBIKyon3HqhOmpAjqPP18Or6wDQb0wRUBIjQ,1032
6
6
  nat/agent/prompt_optimizer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -10,16 +10,16 @@ nat/agent/react_agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
10
10
  nat/agent/react_agent/agent.py,sha256=sWrg9WrglTKQQyG3EcjNm2JTEchCPEo9li-Po7TJKss,21294
11
11
  nat/agent/react_agent/output_parser.py,sha256=m7K6wRwtckBBpAHqOf3BZ9mqZLwrP13Kxz5fvNxbyZE,4219
12
12
  nat/agent/react_agent/prompt.py,sha256=N47JJrT6xwYQCv1jedHhlul2AE7EfKsSYfAbgJwWRew,1758
13
- nat/agent/react_agent/register.py,sha256=lpiso1tKq70ZYKbV9zXZegtXPLJNBaBrnG25R9hyA9Q,9008
13
+ nat/agent/react_agent/register.py,sha256=qkPaK6AvXjolL-q_Z3waVobXDz24GMfuqGqCn-2un2Q,8991
14
14
  nat/agent/reasoning_agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- nat/agent/reasoning_agent/reasoning_agent.py,sha256=k_0wEDqACQn1Rn1MAKxoXyqOKsthHCQ1gt990YYUqHU,9575
15
+ nat/agent/reasoning_agent/reasoning_agent.py,sha256=fFQtzvaBWtmr_B6S9KSkqAfyl1BdcOc9xkhnbO4O8Pk,9603
16
16
  nat/agent/rewoo_agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  nat/agent/rewoo_agent/agent.py,sha256=XXgVXY9xwkyxnr093KXUtfgyNxAQbyGAecoGqN5mMLY,26199
18
18
  nat/agent/rewoo_agent/prompt.py,sha256=B0JeL1xDX4VKcShlkkviEcAsOKAwzSlX8NcAQdmUUPw,3645
19
- nat/agent/rewoo_agent/register.py,sha256=s6D9W4x5jIkda8l67gj3A46aefk6KQPuZ4H-ZJkVAtY,9300
19
+ nat/agent/rewoo_agent/register.py,sha256=XArlOR37QOBtAvsdKJUjRok5qTmx39S2mJHSteOwU58,9283
20
20
  nat/agent/tool_calling_agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- nat/agent/tool_calling_agent/agent.py,sha256=4SIp29I56oznPRQu7B3HCoX53Ri3_o3BRRYNJjeBkF8,11006
22
- nat/agent/tool_calling_agent/register.py,sha256=h1Xfr1KPvQkslPg-NqdOMQAmx1PNFAIvvOC5bAIJtbE,7074
21
+ nat/agent/tool_calling_agent/agent.py,sha256=9CRQbFlcJ02WvuRojaWcRS8ISl38JlS18BUflApuoAw,10960
22
+ nat/agent/tool_calling_agent/register.py,sha256=OucceyELA2xZL3KdANWK9w12fnVP75eVbZgzOnmXHys,7057
23
23
  nat/authentication/__init__.py,sha256=Xs1JQ16L9btwreh4pdGKwskffAw1YFO48jKrU4ib_7c,685
24
24
  nat/authentication/interfaces.py,sha256=1J2CWEJ_n6CLA3_HD3XV28CSbyfxrPAHzr7Q4kKDFdc,3511
25
25
  nat/authentication/register.py,sha256=lFhswYUk9iZ53mq33fClR9UfjJPdjGIivGGNHQeWiYo,915
@@ -48,7 +48,7 @@ nat/builder/eval_builder.py,sha256=I-ScvupmorClYoVBIs_PhSsB7Xf9e2nGWe0rCZp3txo,6
48
48
  nat/builder/evaluator.py,sha256=xWHMND2vcAUkdFP7FU3jnVki1rUHeTa0-9saFh2hWKs,1162
49
49
  nat/builder/framework_enum.py,sha256=n7IaTQBxhFozIQqRMcX9kXntw28JhFzCj82jJ0C5tNU,901
50
50
  nat/builder/front_end.py,sha256=FCJ87NSshVVuTg8zZrq3YAr_u0RaYVZVcibnqlRFy-M,2173
51
- nat/builder/function.py,sha256=3h51TA0D6EQGWjHDsoxa_8ooQcZpk_-yAndk4oc5dGo,27790
51
+ nat/builder/function.py,sha256=eZZWLwhphgQTnPvbga8sGleX7HCP46usZPIegE7zFzs,27725
52
52
  nat/builder/function_base.py,sha256=0Eg8RtjWhEU3Yme0CVxcRutobA0Qo8-YHZLI6L2qAgM,13116
53
53
  nat/builder/function_info.py,sha256=7Rmrn-gOFrT2TIJklJwA_O-ycx_oimwZ0-qMYpbuZrU,25161
54
54
  nat/builder/intermediate_step_manager.py,sha256=iOuMLWTaES0J0XzaLxhTUqFvuoCAChJu3V69T43K0k0,7599
@@ -112,7 +112,7 @@ nat/control_flow/router_agent/prompt.py,sha256=fIAiNsAs1zXRAatButR76zSpHJNxSkXXK
112
112
  nat/control_flow/router_agent/register.py,sha256=4RGmS9sy-QtIMmvh8mfMcR1VqxFPLpG4RckWCIExh40,4144
113
113
  nat/data_models/__init__.py,sha256=Xs1JQ16L9btwreh4pdGKwskffAw1YFO48jKrU4ib_7c,685
114
114
  nat/data_models/agent.py,sha256=IwDyb9Zc3R4Zd5rFeqt7q0EQswczAl5focxV9KozIzs,1625
115
- nat/data_models/api_server.py,sha256=NWT1ChN2qaakD2DgyYCy_7MhfzvEBQX15qnUXnpCQmk,28883
115
+ nat/data_models/api_server.py,sha256=IowzLwxJqnSkUehCbK0WJp98hBZFXUQDd1cq8lr9PVs,30582
116
116
  nat/data_models/authentication.py,sha256=XPu9W8nh4XRSuxPv3HxO-FMQ_JtTEoK6Y02JwnzDwTg,8457
117
117
  nat/data_models/common.py,sha256=nXXfGrjpxebzBUa55mLdmzePLt7VFHvTAc6Znj3yEv0,5875
118
118
  nat/data_models/component.py,sha256=b_hXOA8Gm5UNvlFkAhsR6kEvf33ST50MKtr5kWf75Ao,1894
@@ -146,7 +146,7 @@ nat/data_models/streaming.py,sha256=sSqJqLqb70qyw69_4R9QC2RMbRw7UjTLPdo3FYBUGxE,
146
146
  nat/data_models/swe_bench_model.py,sha256=uZs-hLFuT1B5CiPFwFg1PHinDW8PHne8TBzu7tHFv_k,1718
147
147
  nat/data_models/telemetry_exporter.py,sha256=P7kqxIQnFVuvo_UFpH9QSB8fACy_0U2Uzkw_IfWXagE,998
148
148
  nat/data_models/temperature_mixin.py,sha256=LlpfWrWtDrPJfSKfNx5E0P3p5SNGZli7ACRRpmO0QqA,1628
149
- nat/data_models/thinking_mixin.py,sha256=lzAnUk5vyv1nTYG9ho4BD3U2NTVZ50gBysN62iGj2KM,3303
149
+ nat/data_models/thinking_mixin.py,sha256=VRDUJZ8XP_Vv0gW2FRZUf8O9-kVgNEdZCEZ8oEmHyMk,3335
150
150
  nat/data_models/top_p_mixin.py,sha256=mu0DLnCAiwNzpSFR8FOW4kQBUpodSrvUR4MsLrNtbgA,1599
151
151
  nat/data_models/ttc_strategy.py,sha256=tAkKWcyEBmBOOYtHMtQTgeCbHxFTk5SEkmFunNVnfyE,1114
152
152
  nat/embedder/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -242,12 +242,12 @@ nat/front_ends/fastapi/dask_client_mixin.py,sha256=N_tw4yxA7EKIFTKp5_C2ZksIZucWx
242
242
  nat/front_ends/fastapi/fastapi_front_end_config.py,sha256=BcuzrVlA5b7yYyQKNvQgEanDBtKEHdpC8TAd-O7lfF0,11992
243
243
  nat/front_ends/fastapi/fastapi_front_end_controller.py,sha256=ei-34KCMpyaeAgeAN4gVvSGFjewjjRhHZPN0FqAfhDY,2548
244
244
  nat/front_ends/fastapi/fastapi_front_end_plugin.py,sha256=e33YkMcLzvm4OUG34bhl-WYiBTqkR-_wJYKG4GODkGM,11169
245
- nat/front_ends/fastapi/fastapi_front_end_plugin_worker.py,sha256=yrUSjbo9ge7yZi4fcFOsVFhLL5zxSh8ftZtHAExfm_s,60342
245
+ nat/front_ends/fastapi/fastapi_front_end_plugin_worker.py,sha256=T6uslFdkHl_r0U54_7cRRKLnWYP2tTMcD7snx9Gv1xs,60547
246
246
  nat/front_ends/fastapi/intermediate_steps_subscriber.py,sha256=kbyWlBVpyvyQQjeUnFG9nsR4RaqqNkx567ZSVwwl2RU,3104
247
247
  nat/front_ends/fastapi/job_store.py,sha256=cWIBnIgRdkGL7qbBunEKzTYzdPp3l3QCDHMP-qTZJpc,22743
248
248
  nat/front_ends/fastapi/main.py,sha256=s8gXCy61rJjK1aywMRpgPvzlkMGsCS-kI_0EIy4JjBM,2445
249
- nat/front_ends/fastapi/message_handler.py,sha256=8pdA3K8hLCcR-ohHXYtLUgX1U2sYFzqgeslIszlQbRo,15181
250
- nat/front_ends/fastapi/message_validator.py,sha256=Opx9ZjaNUfS3MS6w25bq_h_XASY_i2prmQRlY_sn5xM,17614
249
+ nat/front_ends/fastapi/message_handler.py,sha256=9CH42D-Ti5cczvRTLVuvlfCV9LaTqFGJCny46zrrEnk,16584
250
+ nat/front_ends/fastapi/message_validator.py,sha256=9n-DvUNfqlTtPgcBMC684quD619XYSzjgPfe6f23LPQ,17606
251
251
  nat/front_ends/fastapi/register.py,sha256=rA12NPFgV9ZNHOEIgB7_SB6NytjRxgBTLo7fJ-73_HM,1153
252
252
  nat/front_ends/fastapi/response_helpers.py,sha256=MGE9E73sQSCYjsR5YXRga2qbl44hrTAPW2N5Ui3vXX0,9028
253
253
  nat/front_ends/fastapi/step_adaptor.py,sha256=J6UtoXL9De8bgAg93nE0ASLUHZbidWOfRiuFo-tyZgY,12412
@@ -259,8 +259,8 @@ nat/front_ends/fastapi/html_snippets/__init__.py,sha256=GUJrgGtpvyMUCjUBvR3faAdv
259
259
  nat/front_ends/fastapi/html_snippets/auth_code_grant_success.py,sha256=BNpWwzmA58UM0GK4kZXG4PHJy_5K9ihaVHu8SgCs5JA,1131
260
260
  nat/front_ends/mcp/__init__.py,sha256=Xs1JQ16L9btwreh4pdGKwskffAw1YFO48jKrU4ib_7c,685
261
261
  nat/front_ends/mcp/introspection_token_verifier.py,sha256=s7Q4Q6rWZJ0ZVujSxxpvVI6Bnhkg1LJQ3RLkvhzFIGE,2836
262
- nat/front_ends/mcp/mcp_front_end_config.py,sha256=fFbg800FDJfwv-nXc0jEPaNVtW4ApESmVRexr-werks,2313
263
- nat/front_ends/mcp/mcp_front_end_plugin.py,sha256=NiIIgApk1X2yAEwtG9tHaY6SexQMbZrd6Drs7uIJix8,5055
262
+ nat/front_ends/mcp/mcp_front_end_config.py,sha256=m6z5qSz8YGnFnfu8hRID69suvO1YT_L6sxy1Ki64Ufw,4042
263
+ nat/front_ends/mcp/mcp_front_end_plugin.py,sha256=4u_kpen_T-_Uh62V5M7dfW9KyzbqXI7tGBG4AxJXWm0,5231
264
264
  nat/front_ends/mcp/mcp_front_end_plugin_worker.py,sha256=jMclC0qEd910oTGCqd1kQ8WjP3WPdQKTl854-2bU_KI,10200
265
265
  nat/front_ends/mcp/register.py,sha256=3aJtgG5VaiqujoeU1-Eq7Hl5pWslIlIwGFU2ASLTXgM,1173
266
266
  nat/front_ends/mcp/tool_converter.py,sha256=jyH6tFKUDXSfRBKkv8WjvJsQt05zk3FJBTCwnIuUh5M,11547
@@ -406,7 +406,7 @@ nat/retriever/nemo_retriever/register.py,sha256=3XdrvEJzX2Zc8wpdm__4YYlEWBW-FK3t
406
406
  nat/retriever/nemo_retriever/retriever.py,sha256=gi3_qJFqE-iqRh3of_cmJg-SwzaQ3z24zA9LwY_MSLY,6930
407
407
  nat/runtime/__init__.py,sha256=Xs1JQ16L9btwreh4pdGKwskffAw1YFO48jKrU4ib_7c,685
408
408
  nat/runtime/loader.py,sha256=obUdAgZVYCPGC0R8u3wcoKFJzzSPQgJvrbU4OWygtog,7953
409
- nat/runtime/runner.py,sha256=sUF-zJMgqcFq4xRx8y5bxct2EzgiKbmFkvWkYxlDsQg,11798
409
+ nat/runtime/runner.py,sha256=qa_AqtmB8TUHX6nVJ0TLEYCKUsm2L99kq5O72AuL3yc,11736
410
410
  nat/runtime/session.py,sha256=E8RTbnAhPbY5KCoSfiHzOJksmBh7xWjsoX0BC7Rn1ck,9101
411
411
  nat/runtime/user_metadata.py,sha256=ce37NRYJWnMOWk6A7VAQ1GQztjMmkhMOq-uYf2gNCwo,3692
412
412
  nat/settings/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -470,10 +470,10 @@ nat/utils/reactive/base/observer_base.py,sha256=6BiQfx26EMumotJ3KoVcdmFBYR_fnAss
470
470
  nat/utils/reactive/base/subject_base.py,sha256=UQOxlkZTIeeyYmG5qLtDpNf_63Y7p-doEeUA08_R8ME,2521
471
471
  nat/utils/settings/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
472
472
  nat/utils/settings/global_settings.py,sha256=9JaO6pxKT_Pjw6rxJRsRlFCXdVKCl_xUKU2QHZQWWNM,7294
473
- nvidia_nat-1.3.0rc3.dist-info/licenses/LICENSE-3rd-party.txt,sha256=fOk5jMmCX9YoKWyYzTtfgl-SUy477audFC5hNY4oP7Q,284609
474
- nvidia_nat-1.3.0rc3.dist-info/licenses/LICENSE.md,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
475
- nvidia_nat-1.3.0rc3.dist-info/METADATA,sha256=qZ0sBY6rZTYae27D_pu8g_fy-S9T4lOeReHqaNXKNOE,10222
476
- nvidia_nat-1.3.0rc3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
477
- nvidia_nat-1.3.0rc3.dist-info/entry_points.txt,sha256=4jCqjyETMpyoWbCBf4GalZU8I_wbstpzwQNezdAVbbo,698
478
- nvidia_nat-1.3.0rc3.dist-info/top_level.txt,sha256=lgJWLkigiVZuZ_O1nxVnD_ziYBwgpE2OStdaCduMEGc,8
479
- nvidia_nat-1.3.0rc3.dist-info/RECORD,,
473
+ nvidia_nat-1.3.0rc5.dist-info/licenses/LICENSE-3rd-party.txt,sha256=fOk5jMmCX9YoKWyYzTtfgl-SUy477audFC5hNY4oP7Q,284609
474
+ nvidia_nat-1.3.0rc5.dist-info/licenses/LICENSE.md,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
475
+ nvidia_nat-1.3.0rc5.dist-info/METADATA,sha256=nm0UvCzWa259-7OnT21duAI9yl9EqwKs3lXaxup59OA,10180
476
+ nvidia_nat-1.3.0rc5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
477
+ nvidia_nat-1.3.0rc5.dist-info/entry_points.txt,sha256=4jCqjyETMpyoWbCBf4GalZU8I_wbstpzwQNezdAVbbo,698
478
+ nvidia_nat-1.3.0rc5.dist-info/top_level.txt,sha256=lgJWLkigiVZuZ_O1nxVnD_ziYBwgpE2OStdaCduMEGc,8
479
+ nvidia_nat-1.3.0rc5.dist-info/RECORD,,