nvidia-nat-agno 1.2.0__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/meta/pypi.md ADDED
@@ -0,0 +1,25 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3
+ SPDX-License-Identifier: Apache-2.0
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
16
+ -->
17
+
18
+ ![NVIDIA NeMo Agent Toolkit](https://media.githubusercontent.com/media/NVIDIA/NeMo-Agent-Toolkit/refs/heads/main/docs/source/_static/banner.png "NeMo Agent toolkit banner image")
19
+
20
+ # NVIDIA NeMo Agent Toolkit Subpackage
21
+
22
+ <!-- Note: "Agno" is the official product name despite Vale spelling checker warnings -->
23
+ This is a subpackage for `Agno` integration in NeMo Agent toolkit.
24
+
25
+ For more information about the NVIDIA NeMo Agent toolkit, please visit the [NeMo Agent toolkit GitHub Repo](https://github.com/NVIDIA/NeMo-Agent-Toolkit).
File without changes
@@ -0,0 +1,87 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import os
17
+
18
+ from nat.builder.builder import Builder
19
+ from nat.builder.framework_enum import LLMFrameworkEnum
20
+ from nat.cli.register_workflow import register_llm_client
21
+ from nat.data_models.retry_mixin import RetryMixin
22
+ from nat.llm.nim_llm import NIMModelConfig
23
+ from nat.llm.openai_llm import OpenAIModelConfig
24
+ from nat.utils.exception_handlers.automatic_retries import patch_with_retry
25
+
26
+
27
+ @register_llm_client(config_type=NIMModelConfig, wrapper_type=LLMFrameworkEnum.AGNO)
28
+ async def nim_agno(llm_config: NIMModelConfig, builder: Builder):
29
+
30
+ from agno.models.nvidia import Nvidia
31
+
32
+ config_obj = {
33
+ **llm_config.model_dump(exclude={"type", "model_name"}, by_alias=True),
34
+ "id": f"{llm_config.model_name}",
35
+ }
36
+
37
+ # Because Agno uses a different environment variable for the API key, we need to set it here manually
38
+ if ("api_key" not in config_obj or config_obj["api_key"] is None):
39
+
40
+ if ("NVIDIA_API_KEY" in os.environ):
41
+ # Dont need to do anything. User has already set the correct key
42
+ pass
43
+ else:
44
+ nvidai_api_key = os.getenv("NVIDIA_API_KEY")
45
+
46
+ if (nvidai_api_key is not None):
47
+ # Transfer the key to the correct environment variable
48
+ os.environ["NVIDIA_API_KEY"] = nvidai_api_key
49
+
50
+ # Create Nvidia instance with conditional base_url
51
+ kwargs = {"id": config_obj.get("id")}
52
+ if "base_url" in config_obj and config_obj.get("base_url") is not None:
53
+ kwargs["base_url"] = config_obj.get("base_url")
54
+
55
+ client = Nvidia(**kwargs) # type: ignore[arg-type]
56
+
57
+ if isinstance(client, RetryMixin):
58
+
59
+ client = patch_with_retry(client,
60
+ retries=llm_config.num_retries,
61
+ retry_codes=llm_config.retry_on_status_codes,
62
+ retry_on_messages=llm_config.retry_on_errors)
63
+
64
+ yield client
65
+
66
+
67
+ @register_llm_client(config_type=OpenAIModelConfig, wrapper_type=LLMFrameworkEnum.AGNO)
68
+ async def openai_agno(llm_config: OpenAIModelConfig, builder: Builder):
69
+
70
+ from agno.models.openai import OpenAIChat
71
+
72
+ # Use model_dump to get the proper field values with correct types
73
+ kwargs = llm_config.model_dump(exclude={"type"}, by_alias=True)
74
+
75
+ # AGNO uses 'id' instead of 'model' for the model name
76
+ if "model" in kwargs:
77
+ kwargs["id"] = kwargs.pop("model")
78
+
79
+ client = OpenAIChat(**kwargs)
80
+
81
+ if isinstance(llm_config, RetryMixin):
82
+ client = patch_with_retry(client,
83
+ retries=llm_config.num_retries,
84
+ retry_codes=llm_config.retry_on_status_codes,
85
+ retry_on_messages=llm_config.retry_on_errors)
86
+
87
+ yield client
@@ -0,0 +1,22 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ # pylint: disable=unused-import
17
+ # flake8: noqa
18
+ # isort:skip_file
19
+
20
+ from . import llm
21
+ from . import tool_wrapper
22
+ from .tools import register
@@ -0,0 +1,366 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import asyncio
17
+ import json
18
+ import logging
19
+ import textwrap
20
+ import traceback
21
+ from typing import Any
22
+ from typing import Awaitable
23
+ from typing import Callable
24
+ from typing import List
25
+
26
+ from agno.tools import tool
27
+
28
+ from nat.builder.builder import Builder
29
+ from nat.builder.framework_enum import LLMFrameworkEnum
30
+ from nat.builder.function import Function
31
+ from nat.cli.register_workflow import register_tool_wrapper
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ # Add a module-level dictionary to track tool call counts for each tool
36
+ _tool_call_counters = {}
37
+ _MAX_EMPTY_CALLS = 1 # Maximum number of empty/metadata-only calls before signaling a problem
38
+ # For better UX, stop after just 1 empty call for search tools
39
+
40
+ # Dictionary to track which tools have already handled an initialization call
41
+ _tool_initialization_done = {}
42
+
43
+
44
+ async def process_result(result: Any, name: str) -> str:
45
+ """
46
+ Process the result from a function to ensure it's in the expected format.
47
+ This function guarantees that the output will be a properly formatted string,
48
+ suitable for consumption by language models like OpenAI's API.
49
+
50
+ Parameters
51
+ ----------
52
+ result : Any
53
+ The result to process
54
+ name : str
55
+ The name of the tool (for logging)
56
+
57
+ Returns
58
+ -------
59
+ str: The processed result as a properly formatted string
60
+ """
61
+ logger.debug(f"{name} processing result of type {type(result)}")
62
+
63
+ # Handle None or empty results
64
+ if result is None:
65
+ logger.warning(f"{name} returned None, converting to empty string")
66
+ return ""
67
+
68
+ # If the result is already a string, validate and return it
69
+ if isinstance(result, str):
70
+ logger.debug(f"{name} returning string result directly")
71
+ # Ensure result is not empty
72
+ if not result.strip():
73
+ return f"The {name} tool completed successfully but returned an empty result."
74
+ return result
75
+
76
+ # Handle Agno Agent.arun response objects
77
+ if hasattr(result, 'content'):
78
+ logger.debug(f"{name} returning result.content")
79
+ content = result.content
80
+ # Make sure content is a string
81
+ if not isinstance(content, str):
82
+ logger.debug(f"{name} result.content is not a string, converting")
83
+ content = str(content)
84
+ return content
85
+
86
+ # Handle OpenAI style responses
87
+ if hasattr(result, 'choices') and len(result.choices) > 0:
88
+ if hasattr(result.choices[0], 'message') and hasattr(result.choices[0].message, 'content'):
89
+ logger.debug(f"{name} returning result.choices[0].message.content")
90
+ return str(result.choices[0].message.content)
91
+ elif hasattr(result.choices[0], 'text'):
92
+ logger.debug(f"{name} returning result.choices[0].text")
93
+ return str(result.choices[0].text)
94
+
95
+ # Handle list of dictionaries by converting to a formatted string
96
+ if isinstance(result, list):
97
+ logger.debug(f"{name} converting list to string")
98
+ if len(result) == 0:
99
+ return f"The {name} tool returned an empty list."
100
+
101
+ if all(isinstance(item, dict) for item in result):
102
+ logger.debug(f"{name} converting list of dictionaries to string")
103
+ formatted_result = ""
104
+ for i, item in enumerate(result, 1):
105
+ formatted_result += f"Result {i}:\n"
106
+ for k, v in item.items():
107
+ formatted_result += f" {k}: {v}\n"
108
+ formatted_result += "\n"
109
+ return formatted_result
110
+ else:
111
+ # For other lists, convert to a simple list format
112
+ formatted_result = "Results:\n\n"
113
+ for i, item in enumerate(result, 1):
114
+ formatted_result += f"{i}. {str(item)}\n"
115
+ return formatted_result
116
+
117
+ # Handle dictionaries
118
+ if isinstance(result, dict):
119
+ logger.debug(f"{name} converting dictionary to string")
120
+ try:
121
+ # Try to format as JSON for readability
122
+ return json.dumps(result, indent=2)
123
+ except (TypeError, OverflowError):
124
+ # Fallback to manual formatting if JSON fails
125
+ formatted_result = "Result:\n\n"
126
+ for k, v in result.items():
127
+ formatted_result += f"{k}: {v}\n"
128
+ return formatted_result
129
+
130
+ # For all other types, convert to string
131
+ logger.debug(f"{name} converting {type(result)} to string")
132
+ return str(result)
133
+
134
+
135
+ def execute_agno_tool(name: str,
136
+ coroutine_fn: Callable[..., Awaitable[Any]],
137
+ required_fields: List[str],
138
+ loop: asyncio.AbstractEventLoop,
139
+ **kwargs: Any) -> Any:
140
+ """
141
+ Execute an Agno tool with the given parameters.
142
+
143
+ Parameters
144
+ ----------
145
+ name : str
146
+ The name of the tool
147
+ coroutine_fn : Callable
148
+ The async function to invoke
149
+ required_fields : List[str]
150
+ List of required fields for validation
151
+ loop : asyncio.AbstractEventLoop
152
+ The event loop to use for async execution
153
+ **kwargs : Any
154
+ The arguments to pass to the function
155
+
156
+ Returns
157
+ -------
158
+ The result of the function execution as a string
159
+ """
160
+ global _tool_call_counters, _tool_initialization_done
161
+
162
+ try:
163
+ logger.debug(f"Running {name} with kwargs: {kwargs}")
164
+
165
+ # Initialize counter for this tool if it doesn't exist
166
+ if name not in _tool_call_counters:
167
+ _tool_call_counters[name] = 0
168
+
169
+ # Track if this tool has already been initialized
170
+ if name not in _tool_initialization_done:
171
+ _tool_initialization_done[name] = False
172
+
173
+ # Filter out any known reserved keywords or metadata fields that might cause issues
174
+ # These are typically added by frameworks and not meant for the function itself
175
+ reserved_keywords = {'type', '_type', 'model_config', 'model_fields', 'model_dump', 'model_dump_json'}
176
+ filtered_kwargs = {k: v for k, v in kwargs.items() if k not in reserved_keywords}
177
+
178
+ # Check if we're only receiving metadata fields (potential infinite loop indicator)
179
+ only_metadata = len(filtered_kwargs) == 0 and len(kwargs) > 0
180
+
181
+ # Check if this is a search api tool with empty query
182
+ is_search_api = name.lower().endswith("_api_tool")
183
+ has_empty_query = "query" in filtered_kwargs and (not filtered_kwargs["query"]
184
+ or filtered_kwargs["query"].strip() == "")
185
+
186
+ # Log if we filtered anything
187
+ filtered_keys = set(kwargs.keys()) - set(filtered_kwargs.keys())
188
+ if filtered_keys:
189
+ logger.debug(f"Filtered reserved keywords from kwargs: {filtered_keys}")
190
+
191
+ # IMPORTANT: Special handling for SerpApi and other search API calls
192
+ if is_search_api and (only_metadata or has_empty_query):
193
+ # If this is the first time this tool is called with empty query, allow it for initialization
194
+ if not _tool_initialization_done[name]:
195
+ logger.info(f"First-time initialization call for {name}")
196
+ _tool_initialization_done[name] = True
197
+ else:
198
+ # If we've already initialized this tool, prevent repeated empty calls
199
+ logger.error(f"Tool {name} called with empty query after initialization. Blocking repeated calls.")
200
+ return f"ERROR: Tool {name} requires a valid query. Provide a specific search term to continue."
201
+
202
+ # IMPORTANT: Safeguard for infinite loops
203
+ # If we're only getting metadata fields and no actual parameters repeatedly
204
+ if only_metadata:
205
+ _tool_call_counters[name] += 1
206
+ logger.warning(
207
+ f"Tool {name} called with only metadata fields (call {_tool_call_counters[name]}/{_MAX_EMPTY_CALLS})")
208
+
209
+ # Break potential infinite loops after too many metadata-only calls
210
+ if _tool_call_counters[name] >= _MAX_EMPTY_CALLS:
211
+ logger.error(
212
+ f"Detected potential infinite loop for tool {name} - received {_tool_call_counters[name]} calls")
213
+ _tool_call_counters[name] = 0 # Reset counter
214
+ return f"ERROR: Tool {name} appears to be in a loop. Provide parameters when calling this tool."
215
+ else:
216
+ # Reset counter when we get actual parameters
217
+ _tool_call_counters[name] = 0
218
+
219
+ # Fix for the 'kwargs' wrapper issue - unwrap if needed
220
+ if len(filtered_kwargs) == 1 and 'kwargs' in filtered_kwargs and isinstance(filtered_kwargs['kwargs'], dict):
221
+ logger.debug("Detected wrapped kwargs, unwrapping")
222
+ # If input is {'kwargs': {'actual': 'params'}}, we need to unwrap it
223
+ unwrapped_kwargs = filtered_kwargs['kwargs']
224
+
225
+ # Also filter the unwrapped kwargs
226
+ unwrapped_kwargs = {k: v for k, v in unwrapped_kwargs.items() if k not in reserved_keywords}
227
+
228
+ # Check if we're missing required fields and try to recover
229
+ for field in required_fields:
230
+ if field not in unwrapped_kwargs:
231
+ logger.warning(f"Missing required field '{field}' in unwrapped kwargs: {unwrapped_kwargs}")
232
+ # Try to build a query from all the provided values if query is required
233
+ if field == 'query' and len(unwrapped_kwargs) > 0:
234
+ # Simple fallback for search tools - cobble together a query string
235
+ query_parts = []
236
+ for k, v in unwrapped_kwargs.items():
237
+ query_parts.append(f"{k}: {v}")
238
+ unwrapped_kwargs['query'] = " ".join(query_parts)
239
+ logger.info(f"Built fallback query: {unwrapped_kwargs['query']}")
240
+
241
+ filtered_kwargs = unwrapped_kwargs
242
+
243
+ # Special handling for initialization calls - these are often empty or partial
244
+ is_initialization = len(filtered_kwargs) == 0
245
+
246
+ # Further validation to ensure all required fields are present
247
+ # If this looks like an initialization call, we'll be more lenient
248
+ missing_fields = []
249
+ for field in required_fields:
250
+ if field not in filtered_kwargs:
251
+ missing_fields.append(field)
252
+ logger.warning(f"Missing field '{field}' in kwargs: {filtered_kwargs}")
253
+
254
+ # Special handling for search tools - query can be optional during initialization
255
+ if not is_initialization and missing_fields and "query" in missing_fields and name.lower().endswith(
256
+ "_api_tool"):
257
+ logger.info(f"Tool {name} was called without a 'query' parameter, treating as initialization")
258
+ is_initialization = True
259
+
260
+ # Only enforce required fields for non-initialization calls
261
+ if not is_initialization and missing_fields:
262
+ if "query" in missing_fields:
263
+ # Add a specific message for missing query
264
+ raise ValueError(f"Missing required parameter 'query'. The tool {name} requires a search query.")
265
+ else:
266
+ missing_fields_str = ", ".join([f"'{f}'" for f in missing_fields])
267
+ raise ValueError(f"Missing required parameters: {missing_fields_str} for {name}.")
268
+
269
+ logger.debug(f"Invoking function with parameters: {filtered_kwargs}")
270
+
271
+ # Try different calling styles to handle both positional and keyword arguments
272
+ try:
273
+ # First try calling with kwargs directly - this works for functions that use **kwargs
274
+ future = asyncio.run_coroutine_threadsafe(coroutine_fn(**filtered_kwargs), loop)
275
+ result = future.result(timeout=120) # 2-minute timeout
276
+ except TypeError as e:
277
+ if "missing 1 required positional argument: 'input_obj'" in str(e):
278
+ # If we get a specific error about missing positional arg, try passing as positional
279
+ logger.debug(f"Retrying with positional argument style for {name}")
280
+ future = asyncio.run_coroutine_threadsafe(coroutine_fn(filtered_kwargs), loop)
281
+ result = future.result(timeout=120) # 2-minute timeout
282
+ else:
283
+ # For other TypeError errors, reraise
284
+ raise
285
+
286
+ # Always process the result to ensure proper formatting, regardless of type
287
+ process_future = asyncio.run_coroutine_threadsafe(process_result(result, name), loop)
288
+ return process_future.result(timeout=30) # 30-second timeout for processing
289
+
290
+ except Exception as e:
291
+ logger.exception(f"Error executing Agno tool {name}: {e}")
292
+ error_traceback = traceback.format_exc()
293
+ logger.error(f"Exception traceback: {error_traceback}")
294
+ raise
295
+
296
+
297
+ @register_tool_wrapper(wrapper_type=LLMFrameworkEnum.AGNO)
298
+ def agno_tool_wrapper(name: str, fn: Function, builder: Builder):
299
+ """
300
+ Wraps a NAT Function to be usable as an Agno tool.
301
+
302
+ This wrapper handles the conversion of async NAT functions to
303
+ the format expected by Agno tools. It properly handles input schema,
304
+ descriptions, and async invocation.
305
+
306
+ Parameters
307
+ ----------
308
+ name : str
309
+ The name of the tool
310
+ fn : Function
311
+ The NAT Function to wrap
312
+ builder : Builder
313
+ The builder instance
314
+
315
+ Returns
316
+ -------
317
+ A callable that can be used as an Agno tool
318
+ """
319
+ # Ensure input schema is present
320
+ assert fn.input_schema is not None, "Tool must have input schema"
321
+
322
+ # Get the event loop for running async functions
323
+ try:
324
+ loop = asyncio.get_running_loop()
325
+ except RuntimeError:
326
+ # If there's no running event loop, create a new one
327
+ loop = asyncio.new_event_loop()
328
+ asyncio.set_event_loop(loop)
329
+
330
+ # Get the async function to invoke
331
+ coroutine_fn = fn.acall_invoke
332
+
333
+ # Extract metadata for the tool
334
+ description = fn.description or ""
335
+ if description:
336
+ description = textwrap.dedent(description).strip()
337
+
338
+ # Input schema handling from LangChain-style
339
+ required_fields = []
340
+ if fn.input_schema is not None:
341
+ try:
342
+ schema_json = fn.input_schema.model_json_schema()
343
+ required_fields = schema_json.get("required", [])
344
+ # Add schema description to the tool description if available
345
+ schema_desc = schema_json.get("description")
346
+ if schema_desc and schema_desc not in description:
347
+ description = f"{description}\n\nArguments: {schema_desc}"
348
+ except Exception as e:
349
+ logger.warning(f"Error extracting JSON schema from input_schema: {e}")
350
+
351
+ # Create a function specific to this tool with proper closure variables
352
+ def tool_sync_wrapper(**kwargs: Any) -> Any:
353
+ """Synchronous implementation of the tool function."""
354
+ return execute_agno_tool(name, coroutine_fn, required_fields, loop, **kwargs)
355
+
356
+ # Prepare the documentation for the tool
357
+ if description:
358
+ tool_sync_wrapper.__doc__ = description
359
+
360
+ # Set the function name
361
+ tool_sync_wrapper.__name__ = name
362
+
363
+ # Apply the tool decorator and return it
364
+ decorated_tool = tool(name=name, description=description)(tool_sync_wrapper)
365
+
366
+ return decorated_tool
File without changes
@@ -0,0 +1,20 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ # pylint: disable=unused-import
17
+ # flake8: noqa
18
+ # isort:skip_file
19
+
20
+ from . import serp_api_tool
@@ -0,0 +1,115 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import logging
17
+ import os
18
+
19
+ from pydantic import Field
20
+
21
+ from nat.builder.builder import Builder
22
+ from nat.builder.framework_enum import LLMFrameworkEnum
23
+ from nat.builder.function_info import FunctionInfo
24
+ from nat.cli.register_workflow import register_function
25
+ from nat.data_models.function import FunctionBaseConfig
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ class SerpApiToolConfig(FunctionBaseConfig, name="serp_api_tool"):
31
+ """
32
+ Tool that retrieves search results from the web using SerpAPI.
33
+ Requires a SERP_API_KEY.
34
+ """
35
+ api_key: str | None = Field(default=None, description="The API key for the SerpAPI service.")
36
+ max_results: int = Field(default=5, description="The maximum number of results to return.")
37
+
38
+
39
+ @register_function(config_type=SerpApiToolConfig, framework_wrappers=[LLMFrameworkEnum.AGNO])
40
+ async def serp_api_tool(tool_config: SerpApiToolConfig, builder: Builder):
41
+ """Create a SerpAPI search tool for use with Agno.
42
+
43
+ This creates a search function that uses SerpAPI to search the web.
44
+
45
+ Args:
46
+ tool_config (SerpApiToolConfig): Configuration for the SerpAPI tool.
47
+ builder (Builder): The NAT builder instance.
48
+
49
+ Returns:
50
+ FunctionInfo: A FunctionInfo object wrapping the SerpAPI search functionality.
51
+ """
52
+ import json
53
+
54
+ from agno.tools.serpapi import SerpApiTools
55
+
56
+ if (not tool_config.api_key):
57
+ tool_config.api_key = os.getenv("SERP_API_KEY")
58
+
59
+ if not tool_config.api_key:
60
+ raise ValueError(
61
+ "API token must be provided in the configuration or in the environment variable `SERP_API_KEY`")
62
+
63
+ # Create the SerpAPI tools instance
64
+ search_tool = SerpApiTools(api_key=tool_config.api_key)
65
+
66
+ # Simple search function with a single string parameter
67
+ async def _serp_api_search(query: str) -> str:
68
+ """
69
+ Search the web using SerpAPI.
70
+
71
+ Args:
72
+ query (str): The search query to perform. If empty, returns initialization message.
73
+
74
+ Returns:
75
+ str: Formatted search results or initialization message.
76
+ """
77
+
78
+ if not query or query.strip() == "":
79
+ exception_msg = "Search query cannot be empty. Please provide a specific search term to continue."
80
+ logger.warning(exception_msg)
81
+ return exception_msg
82
+
83
+ logger.info("Searching SerpAPI with query: '%s', max_results: %s", query, tool_config.max_results)
84
+
85
+ try:
86
+ # Perform the search
87
+ raw_all_results: str = search_tool.search_google(query=query, num_results=tool_config.max_results)
88
+ all_results: dict = json.loads(raw_all_results)
89
+ search_results = all_results.get('search_results', [])
90
+
91
+ logger.info("SerpAPI returned %s results", len(search_results))
92
+
93
+ # Format the results as a string
94
+ formatted_results = []
95
+ for result in search_results:
96
+ title = result.get('title', 'No Title')
97
+ link = result.get('link', 'No Link')
98
+ snippet = result.get('snippet', 'No Snippet')
99
+
100
+ formatted_result = f'<Document href="{link}"/>\n'
101
+ formatted_result += f'# {title}\n\n'
102
+ formatted_result += f'{snippet}\n'
103
+ formatted_result += '</Document>'
104
+ formatted_results.append(formatted_result)
105
+
106
+ return "\n\n---\n\n".join(formatted_results)
107
+ except Exception as e:
108
+ logger.exception("Error searching with SerpAPI: %s", e)
109
+ return f"Error performing search: {str(e)}"
110
+
111
+ fn_info = FunctionInfo.from_fn(
112
+ _serp_api_search,
113
+ description="""This tool searches the web using SerpAPI and returns relevant results for the given query.""")
114
+
115
+ yield fn_info
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.4
2
+ Name: nvidia-nat-agno
3
+ Version: 1.2.0
4
+ Summary: Subpackage for Agno integration in NeMo Agent toolkit
5
+ Keywords: ai,rag,agents
6
+ Classifier: Programming Language :: Python
7
+ Requires-Python: <3.13,>=3.11
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: nvidia-nat==v1.2.0
10
+ Requires-Dist: agno~=1.2.3
11
+ Requires-Dist: openai~=1.66
12
+ Requires-Dist: google-search-results~=2.4.2
13
+
14
+ <!--
15
+ SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
16
+ SPDX-License-Identifier: Apache-2.0
17
+
18
+ Licensed under the Apache License, Version 2.0 (the "License");
19
+ you may not use this file except in compliance with the License.
20
+ You may obtain a copy of the License at
21
+
22
+ http://www.apache.org/licenses/LICENSE-2.0
23
+
24
+ Unless required by applicable law or agreed to in writing, software
25
+ distributed under the License is distributed on an "AS IS" BASIS,
26
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
27
+ See the License for the specific language governing permissions and
28
+ limitations under the License.
29
+ -->
30
+
31
+ ![NVIDIA NeMo Agent Toolkit](https://media.githubusercontent.com/media/NVIDIA/NeMo-Agent-Toolkit/refs/heads/main/docs/source/_static/banner.png "NeMo Agent toolkit banner image")
32
+
33
+ # NVIDIA NeMo Agent Toolkit Subpackage
34
+
35
+ <!-- Note: "Agno" is the official product name despite Vale spelling checker warnings -->
36
+ This is a subpackage for `Agno` integration in NeMo Agent toolkit.
37
+
38
+ For more information about the NVIDIA NeMo Agent toolkit, please visit the [NeMo Agent toolkit GitHub Repo](https://github.com/NVIDIA/NeMo-Agent-Toolkit).
@@ -0,0 +1,13 @@
1
+ nat/meta/pypi.md,sha256=tZD7hiOSYWgiAdddD1eIJ8T5ipZwEIjnd8ilgmasdmw,1198
2
+ nat/plugins/agno/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ nat/plugins/agno/llm.py,sha256=hvPu0-gkRasnWZY5kwHOiA6rZ1ZIGlnaH3nPPfqrFJ8,3434
4
+ nat/plugins/agno/register.py,sha256=6vC1TjMxo3igqTnEtVFgLEf_jgLYkBfBZxjwqxGng6w,820
5
+ nat/plugins/agno/tool_wrapper.py,sha256=uClYG-LvRj1OBTMwzMuE40cibLhqVRXSCiqniVQND2Y,15914
6
+ nat/plugins/agno/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ nat/plugins/agno/tools/register.py,sha256=OCmzR03CHmQHm34ZEascM1dRVh-ALMs2mafDcqLDz6s,775
8
+ nat/plugins/agno/tools/serp_api_tool.py,sha256=AJQH6-1iEUUrk_nzfZ3zZqutEKhJ_LMOUJi_iol65Sc,4442
9
+ nvidia_nat_agno-1.2.0.dist-info/METADATA,sha256=ZCu94sJKFSIJITS1Gi1kF7E35qvaM6RZaZakb7dirMM,1591
10
+ nvidia_nat_agno-1.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
+ nvidia_nat_agno-1.2.0.dist-info/entry_points.txt,sha256=qRhuHKj2WmdJkLpbVXpYkdtc2cZdG4LPlBsABG2ImVI,103
12
+ nvidia_nat_agno-1.2.0.dist-info/top_level.txt,sha256=8-CJ2cP6-f0ZReXe5Hzqp-5pvzzHz-5Ds5H2bGqh1-U,4
13
+ nvidia_nat_agno-1.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [nat.components]
2
+ nat_agno = nat.plugins.agno.register
3
+ nat_agno_tools = nat.plugins.agno.tools.register
@@ -0,0 +1 @@
1
+ nat