From ed9de49fbac46dfab196a09743241e521390917e Mon Sep 17 00:00:00 2001 From: Eric Evans <194135482+ericevans-nv@users.noreply.github.com> Date: Thu, 9 Oct 2025 15:28:59 -0500 Subject: [PATCH 01/15] Adding RAG lib integration stubs. Signed-off-by: Eric Evans <194135482+ericevans-nv@users.noreply.github.com> --- packages/nvidia_nat_rag_lib/pyproject.toml | 44 +++ .../nvidia_nat_rag_lib/src/nat/meta/pypi.md | 23 ++ .../src/nat/plugins/rag_lib/__init__.py | 0 .../src/nat/plugins/rag_lib/client.py | 306 ++++++++++++++++++ .../src/nat/plugins/rag_lib/register.py | 21 ++ .../nvidia_nat_rag_lib/tests/test_config.yml | 56 ++++ .../tests/test_rag_lib_function.py | 136 ++++++++ pyproject.toml | 2 + 8 files changed, 588 insertions(+) create mode 100644 packages/nvidia_nat_rag_lib/pyproject.toml create mode 100644 packages/nvidia_nat_rag_lib/src/nat/meta/pypi.md create mode 100644 packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/__init__.py create mode 100644 packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/client.py create mode 100644 packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/register.py create mode 100644 packages/nvidia_nat_rag_lib/tests/test_config.yml create mode 100644 packages/nvidia_nat_rag_lib/tests/test_rag_lib_function.py diff --git a/packages/nvidia_nat_rag_lib/pyproject.toml b/packages/nvidia_nat_rag_lib/pyproject.toml new file mode 100644 index 0000000000..d2c70af61d --- /dev/null +++ b/packages/nvidia_nat_rag_lib/pyproject.toml @@ -0,0 +1,44 @@ +[build-system] +build-backend = "setuptools.build_meta" +requires = ["setuptools >= 64", "setuptools-scm>=8"] + + +[tool.setuptools.packages.find] +where = ["src"] +include = ["nat.*"] + + +[tool.setuptools_scm] +git_describe_command = ["git", "describe", "--long", "--first-parent"] +root = "../.." + + +[project] +name = "nvidia-nat-rag-lib" +dynamic = ["version"] +dependencies = [ + "nvidia-nat~=1.4", + "nvidia-rag>=2.2.2", +] + +requires-python = ">=3.11,<3.14" +description = "Subpackage for NVIDIA RAG library integration in NeMo Agent Toolkit" +readme = "src/nat/meta/pypi.md" +keywords = ["ai", "rag", "agents", "retrieval"] +classifiers = [ + "Programming Language :: Python", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] + +[tool.uv] +config-settings = { editable_mode = "compat" } + + +[tool.uv.sources] +nvidia-nat = { workspace = true } + + +[project.entry-points.'nat.components'] +nat_rag_lib = "nat.plugins.rag_lib.register" diff --git a/packages/nvidia_nat_rag_lib/src/nat/meta/pypi.md b/packages/nvidia_nat_rag_lib/src/nat/meta/pypi.md new file mode 100644 index 0000000000..bea33ba21f --- /dev/null +++ b/packages/nvidia_nat_rag_lib/src/nat/meta/pypi.md @@ -0,0 +1,23 @@ +# NVIDIA RAG Library Integration + +This package provides seamless integration between the NVIDIA RAG Library and the NeMo Agent Toolkit, allowing developers to easily incorporate retrieval-augmented generation capabilities into their agent workflows. + +## Features + +- Direct access to NVIDIA RAG Library functionality +- Configurable environment setup and prerequisites verification +- Retry logic for robust operation +- Support for various deployment modes (on-premises, hosted, mixed) +- Integration with NAT's component reference system + +## Installation + +Install as part of the NeMo Agent Toolkit: + +```bash +pip install "nvidia-nat[rag-lib]" +``` + +## Usage + +Configure the RAG library in your workflow YAML file and use it as a function in your agent setup. diff --git a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/__init__.py b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/client.py b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/client.py new file mode 100644 index 0000000000..58e397f871 --- /dev/null +++ b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/client.py @@ -0,0 +1,306 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025, 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. + +import asyncio +import logging +import os +from pathlib import Path +from typing import Literal + +from pydantic import Field +from pydantic import HttpUrl + +from nat.builder.builder import Builder +from nat.cli.register_workflow import register_function +from nat.data_models.component_ref import EmbedderRef +from nat.data_models.component_ref import LLMRef +from nat.data_models.function import FunctionBaseConfig +from nat.data_models.retry_mixin import RetryMixin + +logger = logging.getLogger(__name__) + + +class NvidiaRAGLibConfig(FunctionBaseConfig, RetryMixin, name="nvidia_rag_lib"): + """Configuration for NVIDIA RAG Library integration. + + This configuration manages the setup and instantiation of the NVIDIA RAG library, + providing retrieval-augmented generation capabilities including document ingestion, + vector search, reranking, and response generation. The configuration handles + environment setup, model references, deployment modes, and operational parameters. + """ + + # Core RAG configuration + vdb_endpoint: HttpUrl = Field(default=HttpUrl("http://localhost:19530"), description="Vector database endpoint URL") + reranker_top_k: int = Field(default=10, description="Number of top results to rerank") + vdb_top_k: int = Field(default=100, description="Number of top results from vector database") + collection_names: list[str] | None = Field(default=None, description="List of collection names to use for queries") + + # Document processing + chunk_size: int = Field(default=512, description="Size of document chunks for processing") + chunk_overlap: int = Field(default=150, description="Overlap between document chunks") + generate_summary: bool = Field(default=False, description="Whether to generate document summaries") + blocking_upload: bool = Field(default=False, description="Whether to use blocking upload for documents") + + # Infrastructure + vectorstore_gpu_device_id: str | None = Field(default="0", description="GPU device ID for vector store") + model_directory: str | None = Field(default="~/.cache/model-cache", description="Directory for model cache") + + # Model configuration (NAT component references) + llm_name: LLMRef | None = Field(default=None, description="Reference to the LLM to use for responses") + embedder_name: EmbedderRef | None = Field(default=None, + description="Reference to the embedder to use for embeddings") + ranking_modelname: str | None = Field(default=None, description="Name of the ranking model") + + # Service endpoints + app_embeddings_serverurl: str | None = Field(default="", description="Embeddings service URL") + app_llm_serverurl: str | None = Field(default="", description="LLM service URL") + app_ranking_serverurl: str | None = Field(default=None, description="Ranking service URL") + + # Deployment and operational + deployment_mode: Literal["on_prem", "hosted", "mixed"] = Field(default="hosted", + description="Deployment mode for the RAG system") + timeout: float | None = Field(default=60.0, description="Timeout for operations in seconds") + + # Setup and management options + env_library_path: str | None = Field(default=None, description="Path to .env_library file for environment setup") + use_accuracy_profile: bool = Field(default=False, description="Load accuracy profile settings") + use_perf_profile: bool = Field(default=False, description="Load performance profile settings") + verify_prerequisites: bool = Field(default=True, description="Verify prerequisites before initialization") + health_check_dependencies: bool = Field(default=True, description="Perform health check on dependent services") + health_check_timeout: float = Field(default=30.0, description="Timeout in seconds for health check operations") + + +async def _load_env_library(config: NvidiaRAGLibConfig) -> None: + """Load environment variables from a specified .env_library file. + + This function loads environment variables from an external environment + library file if specified in the configuration. This allows for + centralized environment management across multiple RAG deployments. + + Args: + config: Configuration containing the path to the environment library file + """ + if not config.env_library_path: + return + + env_path = Path(config.env_library_path).expanduser() + if not env_path.exists(): + logger.warning("Environment library file not found: %s", env_path) + return + + try: + from dotenv import load_dotenv + load_dotenv(env_path) + logger.info("Loaded environment library: %s", env_path) + except ImportError: + logger.warning("python-dotenv not available, skipping .env_library loading") + + +async def _setup_environment_variables(config: NvidiaRAGLibConfig, builder: Builder) -> None: + """Configure environment variables required by the NVIDIA RAG library. + + This function sets up all necessary environment variables that the NVIDIA RAG + library expects, including vector database endpoints, model configurations, + document processing parameters, and infrastructure settings. Model names are + dynamically extracted from NAT component references when available. + + Args: + config: Configuration containing RAG parameters and component references + builder: NAT builder instance for accessing LLM and embedder configurations + """ + # Core configuration + if config.vdb_endpoint: + os.environ["VDB_ENDPOINT"] = str(config.vdb_endpoint) + + os.environ["RERANKER_TOP_K"] = str(config.reranker_top_k) + os.environ["VDB_TOP_K"] = str(config.vdb_top_k) + + if config.collection_names: + os.environ["COLLECTION_NAMES"] = ",".join(config.collection_names) + + # Document processing + os.environ["CHUNK_SIZE"] = str(config.chunk_size) + os.environ["CHUNK_OVERLAP"] = str(config.chunk_overlap) + os.environ["GENERATE_SUMMARY"] = str(config.generate_summary).lower() + os.environ["BLOCKING_UPLOAD"] = str(config.blocking_upload).lower() + + # Infrastructure + if config.vectorstore_gpu_device_id is not None: + os.environ["VECTORSTORE_GPU_DEVICE_ID"] = config.vectorstore_gpu_device_id + + if config.model_directory: + model_dir = Path(config.model_directory).expanduser() + os.environ["MODEL_DIRECTORY"] = str(model_dir) + + # Model names from NAT component references + if config.llm_name: + try: + llm_config = builder.get_llm_config(config.llm_name) + model_name = getattr(llm_config, 'model_name', None) + if model_name: + os.environ["APP_LLM_MODELNAME"] = str(model_name) + logger.debug("Set APP_LLM_MODELNAME from LLM reference: %s", model_name) + except Exception as e: + logger.warning("Failed to get LLM config for %s: %s", config.llm_name, e) + + if config.embedder_name: + try: + embedder_config = builder.get_embedder_config(config.embedder_name) + model_name = getattr(embedder_config, 'model_name', None) + if model_name: + os.environ["APP_EMBEDDINGS_MODELNAME"] = str(model_name) + logger.debug("Set APP_EMBEDDINGS_MODELNAME from embedder reference: %s", model_name) + except Exception as e: + logger.warning("Failed to get embedder config for %s: %s", config.embedder_name, e) + + if config.ranking_modelname: + os.environ["APP_RANKING_MODELNAME"] = config.ranking_modelname + + # Service URLs + if config.app_embeddings_serverurl is not None: + os.environ["APP_EMBEDDINGS_SERVERURL"] = config.app_embeddings_serverurl + if config.app_llm_serverurl is not None: + os.environ["APP_LLM_SERVERURL"] = config.app_llm_serverurl + if config.app_ranking_serverurl is not None: + os.environ["APP_RANKING_SERVERURL"] = config.app_ranking_serverurl + + # Deployment mode + os.environ["DEPLOYMENT_MODE"] = config.deployment_mode + + +async def _load_profiles(config: NvidiaRAGLibConfig) -> None: + """Load accuracy and performance optimization profiles. + + This function loads predefined environment configurations for accuracy + or performance optimization. These profiles contain environment variable + settings that optimize the RAG system for specific use cases. + + Args: + config: Configuration specifying which profiles to load + """ + if config.use_accuracy_profile: + accuracy_profile_path = Path("accuracy_profile.env") + if accuracy_profile_path.exists(): + try: + from dotenv import load_dotenv + load_dotenv(accuracy_profile_path) + logger.info("Loaded accuracy profile") + except ImportError: + logger.warning("python-dotenv not available, skipping accuracy profile") + else: + logger.warning("Accuracy profile file not found: %s", accuracy_profile_path) + + if config.use_perf_profile: + perf_profile_path = Path("perf_profile.env") + if perf_profile_path.exists(): + try: + from dotenv import load_dotenv + load_dotenv(perf_profile_path) + logger.info("Loaded performance profile") + except ImportError: + logger.warning("python-dotenv not available, skipping performance profile") + else: + logger.warning("Performance profile file not found: %s", perf_profile_path) + + +def _verify_prerequisites(config: NvidiaRAGLibConfig) -> None: + """Verify that all required prerequisites are met before initialization. + + This function checks that necessary directories exist, dependencies are + available, and the system is properly configured for RAG operations. + + Args: + config: Configuration containing prerequisite specifications + """ + # Check model directory + if config.model_directory: + model_dir = Path(config.model_directory).expanduser() + if not model_dir.exists(): + logger.warning("Model directory does not exist: %s", model_dir) + + +@register_function(config_type=NvidiaRAGLibConfig) +async def nvidia_rag_lib(config: NvidiaRAGLibConfig, builder: Builder): + """ + Initialize and configure the NVIDIA RAG library client. + + This function orchestrates the complete setup process for the NVIDIA RAG library, + including environment configuration, profile loading, prerequisite verification, + and service health checks. It yields a query function that provides access to + the fully configured RAG system for retrieval-augmented generation operations. + + Args: + config: Configuration parameters for the NVIDIA RAG library + builder: NAT builder instance for accessing other components + + Yields: + NvidiaRAG: Fully configured NVIDIA RAG library client instance + + Raises: + ImportError: If the nvidia-rag library is not installed + RuntimeError: If required prerequisites are not met + """ + logger.info("Starting NVIDIA RAG setup...") + + try: + # Step 1: Load .env_library if available + if config.env_library_path: + logger.debug("Loading environment library: %s", config.env_library_path) + await _load_env_library(config) + + # Step 2: Set up environment variables + logger.debug("Setting up environment variables...") + await _setup_environment_variables(config, builder) + + # Step 3: Load profiles if requested + if config.use_accuracy_profile or config.use_perf_profile: + logger.debug("Loading performance profiles...") + await _load_profiles(config) + + # Step 4: Verify prerequisites + if config.verify_prerequisites: + logger.debug("Verifying prerequisites...") + _verify_prerequisites(config) + + # Step 5: Import and instantiate the NVIDIA RAG library + logger.debug("Importing NVIDIA RAG library...") + from nvidia_rag import NvidiaRAG + + rag = NvidiaRAG() + + # Step 6: Health check if requested + if config.health_check_dependencies: + logger.debug("Performing health check...") + try: + health_status = await asyncio.wait_for(rag.health(check_dependencies=True), + timeout=config.health_check_timeout) + logger.info("Health check completed: %s", health_status) + except TimeoutError: + logger.warning("Health check timed out after %ss, but continuing", config.health_check_timeout) + except Exception as e: + logger.warning("Health check failed, but continuing: %s", e) + + # Yield the RAG instance + yield rag + + except ImportError as e: + logger.error("nvidia_rag library not available. Install with: pip install nvidia-rag. Error: %s", e) + raise ImportError("nvidia-rag is required for this function. " + "Follow installation steps: pip install nvidia-rag --force-reinstall") from e + except Exception as e: + logger.error("Failed to set up NVIDIA RAG: %s", e) + raise + finally: + pass diff --git a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/register.py b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/register.py new file mode 100644 index 0000000000..2be2d28d29 --- /dev/null +++ b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/register.py @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025, 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. + +# flake8: noqa +# isort:skip_file + +# Import any providers which need to be automatically registered here + +from . import client diff --git a/packages/nvidia_nat_rag_lib/tests/test_config.yml b/packages/nvidia_nat_rag_lib/tests/test_config.yml new file mode 100644 index 0000000000..dd87f3ca00 --- /dev/null +++ b/packages/nvidia_nat_rag_lib/tests/test_config.yml @@ -0,0 +1,56 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Test configuration for NVIDIA RAG Library integration +# This configuration is used as a pytest fixture for testing + +llms: + test_llm: + _type: nim + model_name: meta/llama-3.1-8b-instruct + temperature: 0.0 + max_tokens: 512 + +embedders: + test_embedder: + _type: nim + model_name: nvidia/nv-embedqa-e5-v5 + truncate: "END" + +functions: + rag_client: + _type: nvidia_rag_lib + # Core configuration + vdb_endpoint: "http://localhost:19530" + reranker_top_k: 5 + vdb_top_k: 50 + collection_names: ["test_collection"] + + # Document processing + chunk_size: 256 + chunk_overlap: 50 + generate_summary: false + blocking_upload: false + + # Model references + llm_name: test_llm + embedder_name: test_embedder + + # Infrastructure + vectorstore_gpu_device_id: "0" + model_directory: "/tmp/test-model-cache" + + # Service configuration + deployment_mode: "hosted" + timeout: 30.0 + + # Setup options (disabled for testing) + verify_prerequisites: false + health_check_dependencies: false + health_check_timeout: 10.0 + +workflow: + _type: react_agent + llm_name: test_llm + tool_names: [rag_client] + verbose: false diff --git a/packages/nvidia_nat_rag_lib/tests/test_rag_lib_function.py b/packages/nvidia_nat_rag_lib/tests/test_rag_lib_function.py new file mode 100644 index 0000000000..760550067d --- /dev/null +++ b/packages/nvidia_nat_rag_lib/tests/test_rag_lib_function.py @@ -0,0 +1,136 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025, 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. +""" +Test suite for the NVIDIA RAG Library integration. + +This module contains tests for the NVIDIA RAG Library function configuration, +registration, and basic functionality verification. +""" + +from pathlib import Path + +import pytest +import yaml + +from nat.data_models.config import Config + + +@pytest.fixture +def test_config(): + """Pytest fixture that loads the test configuration.""" + config_path = Path(__file__).parent / "test_config.yml" + + with open(config_path, encoding='utf-8') as f: + config_dict = yaml.safe_load(f) + + return Config.model_validate(config_dict) + + +class TestNvidiaRAGLib: + """Test suite for the NVIDIA RAG Library function.""" + + def test_function_registration(self): + """Test that the RAG function is properly registered with NAT.""" + from nat.cli.type_registry import GlobalTypeRegistry + from nat.plugins.rag_lib.client import NvidiaRAGLibConfig + registry = GlobalTypeRegistry.get() + + # Check if our function is registered + try: + function_info = registry.get_function(NvidiaRAGLibConfig) + assert function_info is not None + assert function_info.config_type == NvidiaRAGLibConfig + except KeyError: + pytest.fail("NvidiaRAGLibConfig function not properly registered") + + @pytest.mark.asyncio + async def test_rag_library_acquisition(self, test_config): + """Test acquiring the RAG library. + + Simple test that tries to acquire the RAG library. + """ + from nat.builder.workflow_builder import WorkflowBuilder + + try: + async with WorkflowBuilder.from_config(test_config) as builder: + # Simply acquire the RAG library function + rag_function = await builder.get_function("rag_client") + + # Verify we got the function + assert rag_function is not None + print("RAG library acquired successfully") + + except ImportError as e: + if "nvidia-rag" in str(e): + pytest.fail(f"nvidia-rag library not available: {e}") + else: + raise + + @pytest.mark.asyncio + async def test_rag_search_functionality(self, test_config): + """Test RAG library search functionality after successful acquisition. + + This test demonstrates how to use the RAG library's search capabilities + including citation parsing. It doesn't need to work (no vector DB running) + but shows the proper setup for RAG search operations. + """ + from nat.builder.workflow_builder import WorkflowBuilder + + def parse_search_citations(citations): + """Parse search citations into formatted document strings.""" + parsed_docs = [] + + for idx, citation in enumerate(citations.results): + # If using pydantic models, citation fields may be attributes, not dict keys + content = getattr(citation, 'content', '') + doc_name = getattr(citation, 'document_name', f'Citation {idx+1}') + parsed_document = f'\n{content}\n' + parsed_docs.append(parsed_document) + + # combine parsed documents into a single string + internal_search_docs = "\n\n---\n\n".join(parsed_docs) + return internal_search_docs + + try: + async with WorkflowBuilder.from_config(test_config) as builder: + # Acquire the RAG library function + rag_function = await builder.get_function("rag_client") + assert rag_function is not None + + try: + # Demonstrate search configuration matching our config + collection_names = test_config.functions["rag_client"].collection_names + reranker_top_k = test_config.functions["rag_client"].reranker_top_k + vdb_top_k = test_config.functions["rag_client"].vdb_top_k + + search_results = rag_function.search( + query="test query", + collection_names=collection_names, + reranker_top_k=reranker_top_k, + vdb_top_k=vdb_top_k, + ) + parsed_docs = parse_search_citations(search_results) + + # Assert if data was returned from parsed_docs + assert parsed_docs is not None + + except Exception as e: + print(f"RAG search failed as expected (no vector DB): {e}") + + except ImportError as e: + if "nvidia-rag" in str(e): + pytest.fail(f"nvidia-rag library not available: {e}") + else: + raise diff --git a/pyproject.toml b/pyproject.toml index d1e025b14a..9b1c7a7252 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,6 +81,7 @@ mem0ai = ["nvidia-nat-mem0ai"] opentelemetry = ["nvidia-nat-opentelemetry"] phoenix = ["nvidia-nat-phoenix"] profiling = ["nvidia-nat-profiling"] # meta-package +rag-lib = ["nvidia-nat-rag-lib"] ragaai = ["nvidia-nat-ragaai"] mysql = ["nvidia-nat-mysql"] redis = ["nvidia-nat-redis"] @@ -171,6 +172,7 @@ nvidia-nat-mysql = { workspace = true } nvidia-nat-opentelemetry = { workspace = true } nvidia-nat-phoenix = { workspace = true } nvidia-nat-profiling = { workspace = true } +nvidia-nat-rag-lib = { workspace = true } nvidia-nat-ragaai = { workspace = true } nvidia-nat-redis = { workspace = true } nvidia-nat-s3 = { workspace = true } From 229028b7b0e94fb7af038ee94a795cdc7ea6a4a9 Mon Sep 17 00:00:00 2001 From: Eric Evans <194135482+ericevans-nv@users.noreply.github.com> Date: Fri, 12 Dec 2025 09:36:08 -0600 Subject: [PATCH 02/15] Pushing package structure for testing. Signed-off-by: Eric Evans <194135482+ericevans-nv@users.noreply.github.com> --- packages/nvidia_nat_rag_lib/pyproject.toml | 13 +++++++++++-- .../nvidia_rag-2.4.0.dev0-py3-none-any.whl | Bin 0 -> 215736 bytes 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 packages/nvidia_nat_rag_lib/vendor/nvidia_rag-2.4.0.dev0-py3-none-any.whl diff --git a/packages/nvidia_nat_rag_lib/pyproject.toml b/packages/nvidia_nat_rag_lib/pyproject.toml index d2c70af61d..a9c763912c 100644 --- a/packages/nvidia_nat_rag_lib/pyproject.toml +++ b/packages/nvidia_nat_rag_lib/pyproject.toml @@ -18,13 +18,15 @@ name = "nvidia-nat-rag-lib" dynamic = ["version"] dependencies = [ "nvidia-nat~=1.4", - "nvidia-rag>=2.2.2", + "nvidia-rag>=2.4.0", ] - requires-python = ">=3.11,<3.14" description = "Subpackage for NVIDIA RAG library integration in NeMo Agent Toolkit" readme = "src/nat/meta/pypi.md" keywords = ["ai", "rag", "agents", "retrieval"] +license = { text = "Apache-2.0" } +authors = [{ name = "NVIDIA Corporation" }] +maintainers = [{ name = "NVIDIA Corporation" }] classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3.11", @@ -32,12 +34,19 @@ classifiers = [ "Programming Language :: Python :: 3.13", ] +[project.urls] +documentation = "https://docs.nvidia.com/nemo/agent-toolkit/latest/" +source = "https://github.com/NVIDIA/NeMo-Agent-Toolkit" + + [tool.uv] +managed = true config-settings = { editable_mode = "compat" } [tool.uv.sources] nvidia-nat = { workspace = true } +nvidia-rag = { path = "vendor/nvidia_rag-2.4.0.dev0-py3-none-any.whl" } # TODO EE: Remove this local path override once nvidia-rag>=2.4.0 is published to PyPI [project.entry-points.'nat.components'] diff --git a/packages/nvidia_nat_rag_lib/vendor/nvidia_rag-2.4.0.dev0-py3-none-any.whl b/packages/nvidia_nat_rag_lib/vendor/nvidia_rag-2.4.0.dev0-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..2a8dd6579692575716cb6e7728b52a4f710e4bd5 GIT binary patch literal 215736 zcmaHxLv$`o)TQ6pwr$(CZQFKkY&$o$ZQHi38{1C$TdODi_uyYOtLfQipHt5+MHvuK zQ~&?~3E&Zm(m6DA%j*XQ09e5Q0QCQE?cJ?Rt&9wujVu@p46N*}Tn!B99ldIjo0`-`m_K%BD{&I<3wO&#w+HNA5%jA-tf(-q9Qf@(QqODu?PWZe( z5K-v~eKCIe^&pbTtNAV#u5W+t0S2MC`%Dn{5{0K&5gbtCPx6Bl?l2wWQDs1YUOK1uFWiMQ|R zl~RV)o>c&+pkn44%2SS(Y7g0H0wJIEPMBn*+PNBnt~y}^GG_8R(1tWk7PD%kwZQ@0 zjl9Uu0LD^PH;7=g-V9I7TFYYVHyslhe650FMRh?2H;SZ;z2yZ4Kzkg4`ZAKfK~+qsWlq8@ z`&@NG7d3X&C7u0~8%uiIXsMYjM`WeN*VZHuJ zCUo~^>y&!2LlOchkII}29*#4}*fkO`9edHuM0o8Z9y8!#-WpTIe7v`*m2c-CR)Y_c zJFRBT_o4g(Yi~x}4yR(zv@`HO?#@Y}u&q+ee|{J@)!XX~_9$D%`V*{Q11+WQ-wt{T|A2d7!x5YB3W$<)v zrZ!D75;tDuW%@vIxGx^<6%(a8%_@g3TArKY0km%DJniXY?Ip1rSZp=T>9gFwQ=X2^ z;fWKHz2u6C*Q)m|>oKNyjPfuoW;fG8ZMS;(O4s2Y!#}ID=lOD6p>1MkDJ1R_EeP@i zTX$wkx2D;zO#vDuu%pC*vZHTQ$QiO5M`ERYFA7xp!v}q6PryKOhfqW!{W&I@Ud&5>=cn*P%;`h^ed@3_u5wK6!tlxf$~9Flg&D?K*{H z@jQsequ9WmER7ISQ1DQgTjFEbXJPK@`Iy?6qAX3bYMm;2_GKy~hyq$@E5>;me~doe zOPa{KU-6WrcPMS+SUT|WVIlgJ4r4Hgs~a17w>(KQ!bQ;!Pt0b{6qujC=0%ayl>m~% z zP46*0C-}GDASk5aS8_4??J|3?iC9dF;ITX+$t|zmRgL$93yj`qIEq)>9%f2Wnt6VW z6rl1BTX^$NrYGu8lmc)>c;p69ic>LrLwihTG9%ic&# zcT$jh;eBL@tnxb;ePxx&YtA^9v0B)3YH7+Ngxd-Nk=MYL;8(M%eOJXuqgxjc6CzeA zt;7?HZ8Dd($5@(5I?{LHyNj3pYc8H(fv4`eb>z$tEo?Hd`we=in@3iuHrm~eoYSZy za_Pb+fI#;jJhCI4LeP(%%3NOBkYMGg`RMza%A2u< z#u*}d!Op!4`_p*(3FrFEf1rbAR{DfPI<)LqQ4(WrWn1q}e>tEq3L_N~v8`c%3d%wO z+eh4ZjxH%-9_}fXTbz1zLf~#$%+ECu?9EM+OK?=~SRWm<1ms=(9TW3i-U)P3vnX2lrHiMFYA?3_?Nk%2U6S8Tk` zG}}sXq%u%rLc((I$?Uq5M;))=X(pocAiK|hFXrHR&5+x3}O7zFAB zBuzU`Tg@G?_($WwOhJhw0t|spVVfVM8qr2@zQU|2ug};Tceca@q1+86-)P`B@4G#E zRN+79^zBWzV{L6kG+E3qFqxoTBVk?`FDRL(RySP9DP8h^%NTpD&MY%?8OaV`g|VgD zy~|Pn(zaMPZc-HVIfJh1*sCb5akk_4|ffVB4J7ak4Y8uATkr0*Ru=` z3~Ekk4eNi-$6{U*aFeblJRk4~rL-ZlAOS$9P;I=zztQG?sFz;7B|VJ_qMZBaN^0~( zG73>x#~tn(EYDAE_{+drZ!TLvq@=XFL(L|!wI2)KF=-@3lUSc@tyJ!%LP;;HIs zB>nKb4~>ACMj={O_7mHzU^f6&4%Un79TFjzF{)g;!)a1jfFHh&ul>u4j*jN{+O1-; zKFcIwGSE3Ce8Is}iU(><08~pc$Kn z6poYqsSNnKE5g-MsE*<3lf24~H;A{tIVl^F|FIgD86srQbnMz!Ea-ELV$76xC@rIc zdbdw|$!x*B7r;FCBk5eRAYHYNnAbDkHhvNM0#MjRzq(Su5Yh+zbP7(xSTC{hQ6WA8 zxiE>z!>fW*yAbZ!u1vD6&XBu%qR8)bM%|}PGd|Onu+20t4)#cK=%-3mSl7qEg6UWC?{O|Jd=u^NA5x%eGI<>*Z)yq_m zqXwwxSl8skTGF9AGwS-Q*6_jc8_Ouj+jUk^GVYB0p;v6d$^2)14n=1m;y}64ODdf4 zahF;>CrS-MD+&>?bKwhMkp~h)n!cQ#C@CFwTknrI`YR=1N;TA?-{M&;geK}&OnQgX zkxCdKMVG~&{;1AGrC*`tR8urZ(94B!HRqB(ca@DoZgI`RG-V3+WlE+?YAc`mE-QKx zwcDGq+4d~@-H$uso-mhr4C@+V+~)%l9xg&nYg~Pe=*#(M zuYa0lJ7gYCm>MyPOoXNN-xeuTn##;&^5~${(SAGAot2v+68ZDdKNaj_!?p;A)xGl{_r5&5eY$HUaZ^EYHEd7YgX1Y zNGS3TYVJx>G-+v^z)rR%snxux+tfa&!Qpq~T;AO#LQq+t0m+$`YDwv{=$S^rR_<`A z?>|T1;WMP&iTKj$FG}|C*!jKTYo^yHY3m0Ca13E1QNCXw)RWJc9Zu~NKYDm5aj0hW zU@~v=AWU^m#2!CCkuTN-m?Lz;P~Ox-iv{3C0s;9o7{rn)_}Dwi7TKkCNsZoFiffr>`pt>&S4oOHaST?O}&1M00+EqRTFaAlYVmngG#rLR?G9S0kc z<`SEJ*^8j4+WA$uMY$3f)*b>pnemDsHJ0vB-N$#Z0(=NcsFo5)I%h zOByc?SPk5}KKd8^9T_5JFQ&5#uHNF&)a>OVUfolW3O=f2LLKBwrLn}WPNhQ#z=;76 z%DUxJXC#5dZO4kzO`F!W#EqWQ=WAB@6C!^3hQ*7N#-#OORp82uqa#?Vtb$!z)a8IG zmH=SHK&M;SqLru<%*RyD5&GmZWYAQJU1$v@c2kgE=$}?U(;GdhZ2Dj5zXpsA(lj>9 ztG2PwQq75~a(|@3jY?gdr1ny>ah}RYjEfEw)UAg^wUjbtt~S!cJ>NkZChIJ^a#_1M zEl(?*gPXU5%8}C)9A`TdvXBw{y4A9Xf}))|N$hx^O==7%uoM&33ov(8_9y zzPHb1Fr7lhmaTSnl?F7j<6XU6FKcldgr>2KtRD$YMc9_PzXN=V72-M|+|s%g7RQFk zNAD}FnyaOKrkbCTIw`HMh_s z79JTfrAfvEi`$I1;o2$NQ?qkump7AZ3FQs0>q4+9B5F!lQnL7BO=?;(OsOi1@8Zzb zT2b;k@JL*j9BCHRy)@O(?7kMvrG^lB9P{o=2vw0-G(erE&)dDcMz=Db)w*gE6;?j7 zEs4<45mu9(AK<*yZl8)G&6AI{OX=;_Ll@X=mbkOl^vS_JyF-Xj|LceoMc)oD(k`&r z>u{ojv4i*X^{4@Cy%uM`EQb>6z>ifc)42uw0a(po!8+Xu+tFiV+B>n*8+H)gqFssb zG_K$r2Bo3`8oRD?r;gn#>PO-xUAs1)(X73+vo&#dFTJ~kgWQ{NPhiC10 z67!?qUK~wERd^LJz7@yu`AzntbQVy&g`Nl1I2xqxPSHvsHMpA`?e2?YM>IZrU#v(E zSyPggfmtWfU(hymBc-W8jk2!i!t1$kPN6hUxfNpW&YS~4h9v1e(WXXzpI)FpQ%^eS zn1n{l$`I`J$U+$q1u5wt#q3yF!pVlr!bf3ke~xhZ8)ZFkVCI0Q;!Hc-O}Q z*x8pcOm58-*M7V($Kr4=sZSaINs&gBc=0G0IeI*d-1um48bPeZ)yAsqv-q_ zLeSKm3Kptr*lbWLYGC#+Ds2&3Ol63C#-29(?25}WHmS_@Kk~++(ohw%^J)gX_V6V! z>qZw(h^%GA)s8le(D*z;-9;gIkUCFm`?PHinLzAl&unE;t$rQd{yv@NYEkI6uC#jt zJz=0bkq9rti+Gm6JNI>=#@~8Z{rw{cGi6PhLHE~&gut?%j&g625pJ@6DA+EJ<}M*3 z-;H3olB}-t5GmFH@#m07h0~la)Xh@cX0=Qo_of78lNSEUhW`-6>u3WyQhHr;w23}Y=%zAC(3!9!-3I zK5&EFeQk8-L;B0z!0Lx%u?CLPT}w10g*6>9R`~lgwJ+h{5O@KTh+trx)$rpqCr9d2PgcY3(cBBBK^HhMQ!`^dTh048hQQ=iY>NfiE5p6?r zTPelGsphgxs-1)5-z)gUPM^7o#&JVWHM*y{wZIQz%6`V0kYAFGsr6zDbF-fyq62Nvs)kPC4lPju&#$aC^b={r+ z^Kz||-7{uJK!&41`{=roj%@cZSIzBDTgUC27XOaF$+&#OUNa`!g3kWw`ji##;QWUU zPj`y~@3`ZjQ|QOs^B1)|Rr$&xa&b zGi88Gf%Ra{nk8~}=Z5PO*ivkPgt5!nkipg%OZEW<%GI!rKozuAK`e>H(;XRH9CVRr zpB*I$C_|7lkOm9Zosl5s+4~txEBjZr{CVNiP;e!ab5Y>Mb2o=oSqEq4XnPP!`K=Z2 zRVPt{*|Y&_#bjnE%9{0Dl3=tNk{06HH7+A?7+wU;*_m7_lC)TB;`dT^-J*1QBvahsa(t&LO(|MaZb|s^!wPc0_CwQxPTm zWE|7&IXU@*`Ag@ZCOT8f=4{86ra_o)M-f+!T77To#k$}s zWHOA=lTlXj#0{3Mmzfq*a-Ux&h@e&jly{y|{EhU#j@ZkDfDcp7P||~-EYkB%yw3$q z+0Uxf>!GNO41C4*O`ig4zFGwpE!1@F7twlT2-LJq&YG4)Gvz`fES74sanB3O5B2XO za4qts&SI`5Dd{uelRC}8J;oH^gT-%F-kv}V3CnapX-c*R*Z;0=!LVTmUqnRr_Hj7g zw2oO=u)c7%K5=ChNhR z+B~AA+79Z@zRl(=SUUgK^Bn)Tlgx+i83+d!US{Ab5hw$9<)A5*Dd4R=#(bmYb+&;C zG~-!FY~`W;)Z;7^U_qWgLsz)`#0S7DvDzJUeYKJp4o&QP!hVyq+MFdF8`{0jUzx5^ zY;e6${b<#AZ>hKtvO$2C-@j}E-pe(>Elt?r*LzxIf9WJ7DqB2ea3&J>J-(y#DQ$t-0kJ(VyL1i> zPAQVHSf!NUQWKZ+Oq+7~LNAblLpuPj<~XERotv^9hK}iKi8Ycc0z$UPxJhbL?%)v9_QPpBfvY zemzq8uE57@cQ*Eaqm$s^-rSqCZu`95{QU*wE%T4M+r3&4Lnk#T#vJ108RDM4_^Z*a zBJgrJK`URrR}3od+nTYFMSj38dqi*sUP1@_^FgF&Hkol z5qIs{(+XB34*1Lf11e3?p5f^WkD~RTKK2jaj3Mp5j^7>>ZUuD3{kb>=1UP$m`8y+a z^M(d<@$ruu2kY7v2F?wN1PJo*;fqlFL3fJx>R@mMV3|b_Cr9K|YygkCu;sdCDw-!$ z(>88Ra`ua72hWjK~q zA-}~(XL_9-GZqWfTBV;0e=CPOC!_aFhMG*1H{rG%9KmpKMybVzetF~Q0f;)5H(RbG zgi?J-U`A9`TJ}i~O-q@S?}rFSXL5;$ily(yKsh4!zR%Q=>gPT81tWxIsw}k^-ESmI zA{AZrJeDBfb>R;qeKAEtu8Ey`Z@1d40!7m5kH2=ZcP*Qv7*GlI_OW}#I912L^xEuq zgC84>BhVy2Y9ND_i@r~1l#BKY4ytscM zO=O7U8aUN2*4|mD5$UQRKzH;yczpy%VF9Cyz-K$m^bSBukDr*DZix}89dprhE}jr} z(ZY$esA{3aY7ylpyn(gfP_a%f5pLQ$Ql7dLIED`3-CK!DhJ%u~&!S`PJjRyl10C7| zV$H8bk2U>fAiS_vzZ$lX9?=uFZG+^xro)E3?qxO7?4)-GEW7qItrs zAOw={z~V?loAEMzfJ;cBk}`jFsaCC@p9b(wia*0c7EXHdM@HI3om{J|f#McA95R8$ zIWJIij@eQyWpFi!O*t?D{T}SIj1sAyUE0MX(ih!sZSu@DahE#5?0(r>y_e>Z_rxni z6~w)F*NpiE+0S=4#-VRZoC+d@de(iXAb%W=Sqx4muFSGZ>d{b98w7zo{ZY|xJFkGJ z%li+npNAv5rES*)x(O&pLa`5Y5`>RCPKdkUFmNJSuMYu1um9u0&ceBw&-+?aQKxud zb8PYkxKy_F+0lUwUxn>oGN;7t7_pV^2&unxqiC(fWpYPmjX}OT7AX}&R#cta9r_rs?#RyOZO@*600litwBR|YX3&)Xz0(m?xzqXowNxjFfGxwv05 zh3ivI5OdNb52pjESK(*@^W5P2NrXI@nFMTXEqy`h5aq}Qr|-ma>i0g0+bJu!Wm|=W znNZ`f`}(yVRFzIOCmS>BizZy%D%4;}2RI+t<=$E3(KB|iji#NH*P+wV0t;g<6Fk6l z=^J(Q0lV}lpF0u@ zAbp$8tmFE|1L2Q@V^p7iz^<>UKR?-HIoh_4pP?-+vbAgpLV~0^H5rPwPWXS|#rfoFNgXb4m53x6dei2xOdzoIB3groCAP(7Dpz>W9&BheU3V!&hd z9s!NMoabIC7w=ki{!_D;%6Ze-c0MMsBkQ+t7us3jE z{Hy?e&XJ9fWd7SocvI8sXfxkS# zWebgy;OvMX1A?dUqfz3_J&fLaemhlF>GVXmfysg?65`k6U?thfn|?NwV0HhVl0hMQ zB@!q%T(z`yuLEsnu7N)UUtG~qM>>PGhcp?|1hJY!pEtDfYH85yxdrVRWx&*9#yUf&aF42%qeh0go^-K zZ?{Vx&5S2n`^+nZq734Jw{F+h_nC6VJ81PhU`!BV6G)xfLI=j zA!`HW-JrE9k)kXryp#b9o$l|45!^$Z(^a}Ix=#2J>bbd2KYW)$W9Gus3P6+iWzZ@n z+|NH!_X(VGG}IEeZPT6a#_N&}0``H&5IzbCfhqeO$kVJbB6$Y9yjK*QS7|^>5UN;Zs? zmCLPS(Kc+F#9)TGAiYm6tP^)^;TNq4QE0Sog z7E!Ptv(3^=A1Nip6>dc!ich$^;5ae8561rUBH+TBLyfvr5{+zJBaqxlIYB%kF zZn>g;%f);+NY6ttKDi}=Ac%my|AiOJK#m&Gs=qPKM^F6cF9LChu)7V9@)*wfX{FAB zUN)ggxzILZe8zSxXn6Q#PQBh`e)#o5L#Tr~X}(R5(&O}%1plFsy|=xucd*M~;t(DA zy+l9j>^U-k*RSd9h-vakKRF1@g^1%?vVprPtl{snS(Bgz8PB`;LJblVev;$3!HFd~ zu0`B^Z!#U!h-{*p7l5d;FIg|-it+x$62mQO)8!bodR3;&&RgL8H)m#pPV1^RHCj4nP@Y6@SwKmWV?AD8 zax+`Q*a7WNS7K=P}8?v`VSoxtwOa~Zy<3|u`8(vZQO{An=#=q z$h!co4NNZW1G1%cVU22`xZ|kxkdo-y-diRmIPD$Q5m@HwioS*p@fo3}rT)W@VJhE8 zs711KRmS9%yJx4ixHp{?=p)ikA=;EqR4(~;L$LleO_%eJ;mi20BHT=Tz5hDon}VV1 zxpR*fA!}=wyNAWMb=6`bdlxHxeN&B|=aihOn$Z7A<&Y4kO$g5D-miIt&I z=dz!sg%y+XwP<4o^q-HW=4^BCeQhm?01ClICq3=SMG%WklQkV8of#Js?^dh>;uU-@BrOfl zP;L8(=qhCE8k3R{SIvK|(XxdMy_CB5>ASt5-`Zn5wNY;rvAH8D*qI3>SL*n5p%p`| z>slS5~ghLw3Dko{h0Pa-s1oq4VPo3(2I3U&sF-mNq#%>NX+yJl<|D3Ml7h0z+}99w_Q}dR zUq#13)YUW^H0Dgivf5s}F!7i4*AQ-Kx{tpfte-FZCDATY$(3rOXs%8YWggF>i4?M?@}TZb#X}ti+wytsEJn zzmxVKP*#P|q=-%&8X6Ph$c>Wb_8C<1zP|PteJnConp_L8W@KLbKblpn$QPhj3?(Ct zjf;$sLyd);rz8Xwj)_grSTeXmGn~(BTqKX3oI%zozf5_vQ8drZ|FZ27a zBSVF%CA+(lT5 zBVJ+cvMjlr7p#v~`S3s7T)VSqSG0|^4vZUs(*q366sdh*$HBrcfkomn7T>In<)Th* zSgBt{-j#@!lhXpl-V;eQ{T(~5^BlY!{6S4`AF74tVIco3E$Q8pJgmEODG)x%FTNod zVOv5~F;yQ%#`@fB{AR-6hbca7heff8NCoFWlcck3x{Xwlwv$mFr^7hib})#WX3y>(@^VQ1ql?=MjO^VdaxTj88N?YZ6?_Uc78F?M=JMt@6(67Rr<(pJ@B?*|W7o9&}a zULxs>s+hs2pf8ltk)T|*_#)_sU;mN!bvKA<&Vd_4?5j5}+5Uf2vLcN=JB zA?)Me$`%_1{%_JNJA6Ib0ge(V*VfRW#vHSOnD z4ppT)LK^%5zkQAvv9zu?wJ&M%iw4ZabpZ(ph|MzX089|9vnqoautPFwf4>m)^P`CD z46vMB*xdGP`uBkq$5E$7aTm|^$uSzGm7-^YW*S8l5((tiT}3wS-gVXeZWGB$P&oOI zXzO&9?ef#a??J&{=U31Mnh2&Zhz?*--@j(%5RT*wV{g7O6nw;ThvXRcl;-!{>b2b* zF(6QT9E2W@Z_S~KA2kP;qonDvi)PROk*XN#MSVB(L>~7@i=h1EaJ!SPi%|&{%y6yaz#jN$Z2o1M;DJL5jlFQ9WP~ z&BJ%_2N!Y|$p!UK4VMf0wCF3o<5%5Tu}iC66=5yR{A6B>Zl(ob{ZAYs0zgn3vlF-qD`$uN6l_N zpg0>vzOeGs;Y0(?@d<%!b$GOC-i-?Vu%K^(+;6f7v57r4YOex<-kdWGY2%%g?86q? z*QdjkIy2(F!;JdUouJJ|q?T`u^iqx+*uS$dIAD?36FgyQ)DI_m=32GqP$fq&LsV`R z^#{UcVnIZXk>F!K@Rf^CQsG@;C1{3?1Qqx9rc7H#svg>h44DK>62jm)KbW~uc(OO? zTA3pX_u}pODIaH_PS>q%jIDI9XXDK%Y!)KnQ9A}KIWXBs;z_gNQf*r<7V1cJ*@@m8 z^s@aq!Jt|-MT=0N1@&(J1F#@6?LDJ!K6SlaTD{1=ebmath-8YPpJ>W)8o#_h81GJ? z{&DCV)}P;$TC+dL2qu_&Zzlu`l=1p@e{Hm+d%mCRihf7n!Fwf&Zhx6hsbhENw;Yd; zbF|e1v{6uBGIT;5qL_VIBzU^^Wx%Z znZyfJ$oW?AQF4(8IAVP7heyUw*TSCrtkPqVjX*zu{s6ot-9X%B5FVRR zOZu_~>z>qdXc$5?I)&IqgDye_i93veJ@2eWL4HbfjM5n{mR}U8rYO<|tURcKNuKyE z0f!=V66{~U|L9v@@1>IX&y>h@`SiPfn1~qH1+7P$j?Slj4LVI6lkt@ZZms?;M%^8aWrCGs?9RlC z>e2TGI;BFr!U`f@oh<@r!mCGsuOI=nSa?(tqe~jDBoZx57+SC|w-IMy!%V16WHF#c zQiH1*n(ZTQjR-qUcP~`YpHXZR(4Ndu?}8Q7hpQMI5huHG{1c40F!)Mvb-S_yVZ(PJ z{%FgA>E79sZ%^ON$IsXMhC-9y($TuFB#5TyDc~KtHQWL(P23!P%7>i}=}#CA`sXoPTx-n^C+{yfs5aaUeVw-;HI3vX zTsm+z5X>(v=T1pbzAYmy?TB_MqaQT@e=p9}NtNv$VQ&>2qKIZt|E!WdW%s2q%~iislgDp$Pz~STKo5&T zR_xuY@5&qDCtOOYU69ON*WJS;uK*i#u55q<}fa8&XHmSc7*t%ziPS-wL?p z;I4)l`+UiMU29oC6J^${*IFYFRhxafHvV7}7A|5dmpKs!4ZUvraeB=?#dqQaszysDo+;_m2H@_+SIZLV5R8!rpS&8A>Ab?)tJ(> z$gK5e6LZWr*u@h2YAt2aPldfE`;!U5L#D8ayXWs`)5qCso}%WE?&!14J! zi^c=`&?Cbk+6o&&--5-W5`e#u4*|H>_TC{8Ga?VgkPbr*5XWnj0O%VP2 zRG*KkEPsF_BV<-o#~{_ucsMhpZ7nlpgo;q2nj2+#d{-%Xb0qiiE#9YGA@CMlM+jD0 z6tf|sIrAGi2BY14XA04K1WjkYRG=!~8)X&Jfeaj(adEqDEse$-mGzj{j(7@YXahw) z8+G>q?w(5ibveqy!@syTc%aYb4eRRE3?de)ku@kugiD^OyQ(RlEBHJ!Mm(L&s2r-3 zZ=_;c70I@sm=E-} z`uD$#Y0;M)jW^XCJ2Ip2u;lq2Z*Jau49Cl}=JDgg{@*s7h0vVAZ0m(4%tI~~Z+3V$jVwfOWUD(n6Ta@urbd1$EUpIQ&t2IO3%!_Rjf%X{8dz#a^rkT^# zZNY?V;tgC;&lYTefvf`)$0Yu-dSM9$Kay+eD&ML#YPl-(Av{L%ReSv6KBVu3S#NvN z!|EK%^ciq~fde>g;~Q2Jdt?Eid_tfjxS+UcVM_BT(kpt4X1s1D#D4~^n#)G0g)sSI zPCK=)S?zBgsnU&}*e|{>%JRS;tbW$=k!wtj(gVL(cKuzYe1VZDa~eKO)vMRqQO|t_ zkphsCBU9ST|Nf)P8Zs~V(bTjYw=8aSho_D7oe2p@>vf)GnAY3d6E z!KGLGO)I<3dxs0lOa_1`=UM1$J)nm^Jq$^AUF$*LJBvzHVHatAl64HuaokKPkhn)b z6)I4VPMc};!|IwxppS+7U;+{M%L9=#x61 z=|VbKVjB;5F6C!Gf$wjBD3tJYx#2hV(5ixIsr?B1w_i_6u$uVFUv23Ra*n)9fA&sp zm5BYr$vxQ&rqyJ6`kzz(qNg}AB%&f&(%XYrV=CNua#?1UvJC5SA8Js6fJnbzoYgM{ zu`B!zF0VsDPGchWSK)<4D3F}!NG-HWP8{f3RDNp9v?3djU&MQhcwBqxjIvl=lWkME zOG$Vf%GVChp^CSR613Jp*q(k-#dRthF8vMMilxQ*n-PSaLlb~d|-F;Rki+D;~%FJpC6c6c4D$?AQn1Z zRTum;)*Lr=Ji+sjg+$6is$XG!C%TI4dj+4t+z9Js1YAh!bDqmA9X7MgsOwG;GzSwB z6d$HyLOnMy07(gOZr_!CA3V*K-Tpdbi^7(sh3tBF5*5Bs$S z@&pN{(MaG`XB6{wH=>25x;W@Onrpb3;k2+il)>tP7HI;ty!Ll$5!02KPIb)zCl$@1 zETo0{g+}*vM;A0wDKl)y0d+3Ica`zV&+i+79*yK-#(C69ElGDGRv3-QWW2h{rtCL2 zn_S$c4QBC37n0>uM#Z_}FC7>#Zd5htqiI)gwcI}sZ{|^_VaqN>*Ii*1VAG1J%~yiE zc1{*msPQ0!FI+6s2BBVTjI$2ZYnn+XI!aE~O7M_qI*;cB{=KffK)VPbltS9rIUw^tP2TMkPWo zVYud7tmM1S)V4}77|y4_`87`JV+|1JFwEo}n%iE*LT~Iu{Ktn%t6PVjsm?3U*O7Jf zt<@B4;$?i6H&@bU?1AfR>WomgAJ z)G23^4qW4ADsI}sOE8Icir{qUgD8(_G+>rZUAnRtaT;UE78VPFT5?0BcmVHv4?;@g z7~$Bq6zC06@wX%CY!3ks!0W|)eK2d8`h55M_KZ40dH%ZnWk`;}3y5i~>cySkY0HIM z0pDWi_I7TBXF}Njsd~y~OUgVruwK+p6Nporu}mMJ@Aa+C--)GHigQCE0lJza{^M$M zTnZ(^fy)OYAYk5OmAKRZeEMJNIYO=cgz-U@Ugi}Jy+_>070cgA&cCv%_!GMfTdDW; zeE4vt_jR-Xw7*p}^mcu}FmV8OyCbSESBd3T5Qbx?8I%}Y1?PbFl5%V1;LexfsMRe| zFg0i@5Xk|AL#W0ihFMigwzbTvSnR)NE*9m1V*Nm)yg;XjXp39EJ$+LxXAkjcYPDW4wLj zeJu}dmR9|p3oORhm^s+sd${RZ$D`Oj=~KH!tu|FLJgSLe1t||s1;|3)NY(L|wzsZ$ z3}00Nqk$c~mcrDauPmsEJON$dV4oW0G>!XkN7>ILwuNr|@KS5XaF=LEQAgCc{CRUp z!fg!^WNnKQlqC83KOjjTYL!EGf;}Nu7fiLUY{eN^-ZmG6EXg_&c1fcsBRXyrak8_?YBV(U^ zfXjF8!Vd)?-*&WTmYp?z%Z0;Uw9*`kDy>B899&sGY|a^20qk-W3F zYwtKHW84%&ROj0d{d<>nB3z%AfJMdp(7@373i@K=)K1qr3V4e=Fu*oZGJ3ZkQz}b1 z(y1Rjn@T{ApQjPCe(h0Bzb2tjwBsXH#SZO~M2)2h5sS$Pq{sn>Q)v719u%peEs`hV z2_Q3;&#yem)W8||Q@$Udwf&&9{V;uHqHTT+_8in5tAIBYIq4UEoTYWuJ=4LF5w#;u z!zrF~z|DN>7^d7UNMV}COR;;I24%vhM`gIKnSnj<@F0L4rUG_vEEcrYx3*nDw3i$GQOEn1X%>VivWs&sE(+gkg6MtAmwcJloW!oKjD&%5_I zoGco4Dq#pr?GnVWYhAnvPMq?K*nIN@U8_qvkJ z#6hb&Q=E=`&Z?ilTFeMhJR2$~vEh%8X;X`U-N4Dr%)p^Z;!9>R7q%tGvYi zW2A`|CXfd(SOhP%!_+whPw(>gc+@Zh@QbqeCJqm8%-krP`Zk=KC>?fF>Ii7xN96K7* z){AjW9=|>TEY=4Rs`mi4VFak7=A_ugWS&qE$fGK0fDwLO)~oiEA01?1v#Hh|=KmU+ z97)^RdPusTqn>45yLysO{e^s<;jBokjP?(~ffKqBf4wFY7>Y#K%V;d0Fy*P%GK=PZ ziF#ahAYztHlv%z8QMRO`Y=7$lfWjH(*Z}b@Qr8`G?Yg02WfZofXT@w4GD=2px(V6` zDaG1;<)m2p4k<;=05r6h(5YnA)>;Z};nmwyGlT>-gpOsKxBSdCgTJK|0~eb^PKjb; z;6;(J8Agwc6vhR}J&Ihg5=%igo)Nk#C-wL-PsyIqr!Q`&6l-8jj3uxO1BR_+HswlP zChSm4@wJgZlE zf);fWE!xl9)j|^8-@7F6t^MDokEW@7XE4BAmo&!NHIKa{dTQ>(q-wzs!*330w7FBS zQ}~)fpJgkICneXzK2K3-bJ21t5uG8Rh}e^$Hqle?>6WbMW26}LEHl=?8Z^wTfH0~xr1UfJ z(nZ5{c{%iht>(pcA7pDl$mdlJa>q(kQaPsOuynhO$1Zk>R5EP=q8WUH3pc66F{+vT<>F()sjRu4NP}r`r$5f(^5QVo zW>YC?My>hfADHCR!^O$PlLffpl6oMg-O>*-=tCebqP)+T3Q?ubOlIYm!->9G?$tdA zMwKEqi(7Wi&|IuwMIV1N4DI=MG|dn3)KWJ#kM%8KSZs%u-Zpcm&x_H`zd^byu!sMC zj0a*w%50gfArI56<3|*hVHBUh==tZKM`>s;?sNw@FJY>}NjXWMeZi$w@tp=m*bLsT zJ<+WTg_`ssMS|D*j^!l7$CB#pmzT$~e;p&P{Bg5!9@OWg)=1v`U}kTM|p< zb`t+}Gj)Q;n+kik#B4U0dbz%?;4?i`M)thOgu`~%UWtk7W)zc4KJ>h$bj}ac;PU-m zHg=y2+r|U(OQhC!E~eBmd#z|KJs#G%x9oU*@Kj6YW*UbV^2Doy@a6InTO9EJYBl8{ zCN2ENptm)zgYeB?aR$O43zR)a@>Q(vlOrB34kTw$+!3Az@rCw@mQ|L@JVRi0p)CBY zNUv()RdwUqyT1fwL`!a`#iXzrwQ#qt;rhj5HV(I{6?^{2+QRNtis~pT;pAYU?UBZv zC4!6F=Hv^a?H3JtvRNmM0G_w2IYE$|jUM0%HPg@(1P=w53(A z3j>|8U^k$57{fL#g>zRU#3br<6U_jZ=!*O}dBc(k*L_cNv@Ck0f^lD6R5Q9ucVqEE zfpZpcg!yK%J0a9d*+!sb2qR5qLdci;r6$^a;_`dND3s+G+~`_h0}U03g@rIF>ea;J z4jW3Kkv_#RfpEnQrVqcbJltO@Qbe6Vda?1GT2=(1a1Aw9!8L1^C?9I^ zvz!@)rR0DYI2$y^BDNOIa1!?d80?5L2_(nBeEW)Kaf5UMpXevLz0Fc_rWpul@@&#b z@GjO0 z!a%-&{2Lhy1vlYKe=)q`E`6jt1I$zSW&jB-v>8~SRF7J++-4o zCG?h05JlSr(>ey?+~4Gyj~@_8RZu230Wgg7k98oHT>9xa1Unv*N$j6Q{Y;3;lai^< z9#Fv3zYnfW8GB(NPmV=oKEuasttRE7?YGjZj6|88l%M9+up}OpH~M1J!<#?&L{bzf zlI)i=!+culW#J@=gz_6U@k}Hu{kbk zHDRoB9bNxcDo+A?#JVfU&db_SPkLdwMeS1!<|w7aP7Tc5C=htn#d3L&I6{P@>O&8n zzNm>~m|d~rUp=q%x;MSs)8@@QVKpFWZ^A#OrNwC>HCN+S{beA@LnLd$)POx~8k>xg zT{iq8??yJfkaQF9*II0*XA+5)gN+Vo4BU{oy?ET5R+3z-3a45^KS2NM{(lKR)z zm7E}PA{KH?SZ2cU&eB2_?JnK^xzTXH zwWksosaj{5rcj6uWT(#2ehk%YS|5%L3wl6O-W-U&q*E$6(7xAm`r9U)OD&oWBPFV+&2%Xy zQdr|wmosTC+NO*DBGR_=YEvZ|am&8W|+I4L$I z_1c2~=0>T~@tJFSvq{S47NB zR+@37g0C;fH%=_#@bh)}Jd=Oyeh(L4348y!Iru&}Ik_02_-IBi<=q`;2-KgTqQ}%*VYUA#++s>mEd}9b@94_QmjSw(HA5c?1aJ1#D=B8|>sE$iW zYs{J^r=QU^Y>keu%)8J%&^v}l3<2oRibOWGrzirYoN)`$Nao=h4=O|MYqum@1stSa z$-LFzHZjU)$FNn9HoS=p%HTVf!C@{-XRp>HvPj-&4&!&awtiVZ;W0Z-+>dtphX)lT zS3)7XuBvsT*W2=*C9V&UEkHQL;=ja}CDspXLzG3tWVeU`0o~u6PUQ7krMn8JmsH!X zogx$6L-W|4Lrq+}!Tj2folxa^VlUZYuhld@7y5EBwqi9hueyAhFx$)XOAtFhYVNwf3#6!pZNi=$Ch$xy zO|8~HzVGm{%U13p?M8T!8ip8&Ok-zc3n0W@*$*>Uvgl+H%W!`b88RaNnn(X*1RNgK zhi;fjm+X4GM*=o>Db#EIoE^6(!!8;KRnqBe=17HH%+1>pYrCCYF;ol?i*&1$^13nH zw&=;=U&?Kq{5fs>{Ml+UVK>nHu06v(#-(m3DGYH=VE4k}~y;35bwj@dT)-#8O~zh>{H@UR}hRY*1RmuYCos8;CsvRZ>hs zNd;G;^HT~#glF;t*mluQ5GmpTi8sM*k3Y3gE>F-r`5O-w9B)c7Sl+Dd1KuZ zD@)wb)~w&lcU_NDkGiB7p6PA8Ov z;Q_9tsfm;BS#+(-WSNtk@0;HPh0Fa4+;I_IB9|;z2J;X<(Wz6A-XQi0A6F`lNY_BV z)FkIs$sJDUgEh*}DH~}#U{nZsebD7c>|KD)NjCz0mEdZnVsRg(pT&02Hy6wV4zN3n zErTb@;=BY}v0}n{2G#9lf#+%+R%ecEYwk^BwXGEugRaIVm0CSty-REKYgiGsbaoUS zlDT+!_;~q#l}vP0rBofWxeQ{33tAWK^H0Qy!ExmPN7-|m)pEU^70kxR zlnZk7;jhP8Z-$SMb)EE-O~yY(d7+Ll;`iS^+4g}Sq?dV-f260|+NbY7@agto$2Bve zBN=`Na`3H%;j!wrS-xi7XR=E0$SNcKmpelXVVBl8Fk|BmX$FZH`2SwkN4+fhYE|Ao zZbIa8=e^-TGdcRRd1U+$f}`t^@?=(wmRT`m^8;~o0Os;>Ce1Qy=>9F%o4|n5j>(bD zPHuHuChbo1E@9FffS_4ds0LZ|{BQ1xQGC7L_yw_POk~XL0hLD=VcHY|`CW8^YXuOV zHjb~+Z?!%Y3fz`H((yKdx!MBT&6^~5-LTS-*W7Vs-tjV=G2}>%&!}xfzky0e=Q+Rp4NScs<5ml~v)tUCH5I;NYbnS5uWPBkizKJUn<9Dh{wh z)eNg)=*=cS>b|6GD2TnxZVHG@{<_=HfTH4(Z z;2a3jy>qP^S#iR-JlAaXG5&!LAy2`>r}k4aeYN(8Lgp>%hRn`8&R-qYeIM+eSUwbd z(}=1B1B==QU1AGh7Y%*|n$dym+JIP+tB zS(g@*KLoSuQ&uneYBa3cRn-~v=Y>P9Vql&Vbi!A3!d5{E*4U?8jcs__Bz8_@U0(6{ zZGD=zT$;CZ4|Pz47qLAr$d|UDumRe!f0&VtQ_uhG2hRzet3u2t7+DADebkQ?=I*h($x}ew@RQQx!9xUF+9b(!v8~PsH9@Qra)b2!uIB*=!3z|7hV$*oOOS& z96qmXk8f@NSU;#d1AdyXt!Bj)FMWfv!Lm!^9p7 zWo&EA{LnIB0JM`ukF)1zq(^sD6^&kEcw{dFlG&_EF?n!MW}E`vo}CU7&Y=>6mmV;2 z;t~0G>5S4$2Rxe?m(p31{TXQZ3SDGmGPk{B;GxDQ5s03t`0`l-3y~YHf?)jSx*E=s)Opw_}7t zL?P?HnDFE2;AlQ=x*ZBZi9Wk&7St$OnbYPB7_})dko`gWslq1Xw=e+X0ez8 zX*ElYY6`vZdR1$+(n}4jOlfwC6cQzu(J)rCg39PjFN%p(=6sSU$*@GaV*V5Bz?1?S zfC$gbjsjfOhjO?lRSbjHbXpM~dNE)+My!ex`pPL@pIKFHY$w)NU!w0!pkPpizqDXe z5lp#bf0YzJtxal(?MK88MiAANz52Eij@eH&EY7_WMXT}miu4Fsj^zF}7jTS#YDpMx z!-}GD>kDs;OW$IkfQgLDJx%W~q&=b%FVk5=kjQ1N{Ct zR_E5L&uOR(*@6Ei&5g!4qEa!T z=`!27z=>Mc5N(4Gsq!722gE^$f8@V=zLFZ||1_|C2bcr9k?Q8Q3DaZ!`M@KG8?}Y; z=gyFEa!@qhFW<5hr+bQ|e~E}c87`V{o7!SF#4QI_ALj245|?J`P+%7S$PB1r8LLm9 z!~S5lAYvAwQ!t+!C=ylAAVnVyJNI}HqG>0cptM0eYtNI7rn@Mx3Ofboqx|kuIC&gm zD&i3g3k=;=pmFi?j%^J7b4$9u>w%oq)CdufLxr?0knasDI%c60EvHd_!wk8nXg%V`67{}WFrkiJd6-$&Nxy<8dMK1dk>kKdv;WsXwgcHjE z01u@MNnE-po%(Bf`)ac9ZL^g044NSLF~wCA&^D;g!|hA`yjz9%OPf;k9R6D~r76K+ zR2W=xt2O~lc$3%mA(2}jay;3@p|ffncMw%P4XUGfg;Uk_W%r>+Avf%PkU-Rf#CuF4E@nL}N(H5T!k|44f>m5Uj`oLz(>oBC;&+_x%C%zKf8&uFuRAxV_0S-f+Z z)kA%#FC#Q$_PJ5N0J7SD52Y-DT`x#bTc=&ub7kAu_M4Xp$JsrYK zQSbW<{ZgBs5B$1HttlOPEzJ{)m8ZS&HH)Gd-wmX?#`*)5)4h$;Rf$5GoS<|G<{0;g zGLK$T#J0vitx()1Md`U{Q9>P9>F=3BUtX&ICBETD`hf;lU16Wf+uTxE#$mCc$WQ?q zZM^1t(K3u2xR{C_nn@$-v*bs4R!-bW?hgEwT90Xfcyeoj`Iz9+j0?m z+KzO7@39ho)oMM32ADhOT_ee4`HVEs?mTg!IiI|VVfRTTzT$r2ju-=a?< z`51stroav{SjaV|T&x>8nm@;+z08sPjl4uRPBR9(hE+}+5o+DI1=>UBL|JXe%v|&h zxg^OY{X|eN1}2!dJE-Ki@*E};n2Q^kVR}0`z-TV!X6fgpAnk9kvi5;Zj`VHKT}@EA z>MwhLW+^DdGvi*X95M$kO;UE#{mi%{!UZi7m;21py|PQ2Fh=!-Rz#CPV4C&1aJRX) zZgj^c`ZgsN=WTLI5A3KOAYpl`-#v`80!efp*SvK+2FGYL)6#nvIz|^3RHx2>M8mS>HSULaq3*XdrWwK5(yG2iflN zdA-gjdmpIQf-u^^!JYbw#R=KLUY}M$ZMvH~hs1kgKMH6bE@{4wZVJU^3BN8%r4s{h zd4DRxqCkK5|33TUB#+t^<$G}b)6J{Yq4WGyB)HQi$cbTHYZBnoPL8zsOyDpj~ipdE~%^D#)fRQm9~ ztT`B>z<@_ZIa|7!ev~+h_AoR^PezJN8LLiQehfhoQ2eC=XKa*keXdQj6|+Az8jb?4 zh?0e`a_dJr91o?H3~gaf%cuT(&y&crr;&8HXTyqx$;|8}sy`8?(qACLJgf03) zMfYuOi%@5B;sE0ra^m6zH}dt6R2CvNIm7#3Ahav~+A{$yblW(#yJ~mIl~1CPz+yn` zRtzeuv@zNLDD9#)$@0(SmxR!r%s3!`e}{A^ZbMjo7-`9Yn6L{z(qjmW^xrMtK6$d$ zQ%Qb=ItF)=?W1KsR!%}67j_LjI-sJzd*iFI?GO|8GSUm8&??QH3-S`!z@(NqJ|5oO z{Cu7+8h*choKWu$nIf~t>61Y#Tb+I5#~> zhZ!k_mb?~B`;w9!b4#=FmCF^~PML;mMKn2#swjULbL3s?uFqiTF8DlA03t>coGRob z@=fqgzf@@VJ{j&#fhuG)b1Mh=N zJ(v)WZM}L}@GUnZGZz_o`Om+RR^HHvwl%_%Ks|6NU-wq*2g+>;hk1V}5XueIyfEk_;BM%-9d$?}E72OmONf*SD(l;&!C#IBc!v@G90{;#pCh9=n? zP!kt}AyG~9<|Jfc0QK1O9$42vXN`U#uU*JnF?A*$8-#Lhd32_kfZ;*{S)JlZvG*F= zE5Uf0NwY|ZzxW>{X`lF_MJk`8BAt7729ZH{ERVFN<;a>L9u=)%5h<+s3DQ4 zqS8;7n~Gt#lmn{zsCTgMaqTS#p_1dEc;eFxQDL_xaD=b3fdn-ESE9DF6I$hptf#%o zJWv=Rv9Q6Fz0YI&=7q7S;rS#sSi5>@Z7<7e0O}2vaeINJm1lp%1QVbkf~ZT znWWN;=e6ISaksm*AqI=C#`PJ^N*y(z>2%K9-qc&O1xbyUJ1zC5QoYtG~*fp-Cx#Y!(53I?MVqdi84KbzEPUQ$Sv{*R{O>n)c%n!XS*8PQBgi) zqGFto?JjG(tQsq-Xq%Z*+SsycFfsp)l@n=IhgV?a<@`Y1KXhm>n@fo)$$WOz1j9^J zkfB9D3LOLl(m?y@l9;wI6KRwD!)#uiOBx#0U>m}3@fQPSBg4GiWMtTVVYMx{&7stTOEFtRPLQC z6}iaSKtj77`@ZGTZQF(0t|M!9%$_9=Cd@O4@Cs^DUa+->NRldG`4NS*B&MOPdhFKI zrgNrpNPO^a369Uns)A%V89!#ZlFd!(0cmN+mSX184`FSSZcRz<_vs^MMwIvVw2*<@O8Qe9$XjBetl|-<4r$4xw2aka=7;REiM^9faP+K~ zJuYuK#c1-0j2Q33s#Co37^YG@XbTc>SYaLj%J6bgXR)O#&bu$PAr|Rz^bh?1b(bOW z9huI<0swF_{D13H{%aHZ-$IrD{AH_H*7nC^kKKNwL1NgHiI)4Qwg9bE1P%eNY+u)cS&ie!+*WZ-P7Ywgw*lywFx;UPG)X<^iMH6zCFK}-()cxsPb?` zJ2uOt-csTvAIUF2fS%r;Z}ldA{M~u76RWpJ$G1jJIlFU{`I9GSW^aaUnsWC#Op!!I z4kU;&Opg;l?e1mq9|&JGX*WJ-4IHrRgvi89k`?`?=k~kPdzb^52>~XK10a&^GXf{> zVJCTzm`@o1P?y93#&(>+{lKBX^AmA+5;Mw4l3*l4l4}GwhVp#UN=RcP=iMYwmN(2z z@XNJaV+ev~{%j;4RHXp2fP8`dYpNVV%A*f#vdN31qkjGFl}0P4M$U+q1fGwEZmhZc zve2J=qln2BS$`&l0F3J3LdXZB<DoEI?N=b$4iaXDGQ>@DMx$t&$_l`mjQK`hkb~tGNoELWA!0#`2LX) zK{drPcHvP}A>V%3U)rb02&>#~f`pc1Hu5M#a;7@M&~t=gIinaLfr*OL060HX=D?>+ z91@5l(*wPGvi5f9%CiucSg50q_1nC8EEaYM4n;3PC*yVQmP783Mo% zhWH94r7oxBlZNPG@tHHaK)Iwc@TBum#HPxSa7K3BiWNbSnW?VZ@KPttPR#!-y?CcW zc?89R%T-z-@efyIs>jZS1}H;s3XB}!h2fNwWhy0FBNA?TL}!)33C`!~wJc-;LnSgQ zsDZ4qTM$2#gnU7b$bdm)z7py*yH^|6cB4udJHJ`0 z>_;=AdDCF z_x0dx)E)(aEO5$veiKb;r=17ESlc zGN0uWiy0LPLIR*m540?rA3M6ERQM0OD(wnZE~!BCZ1cHd)pFEbt6sULtAnyIHKycb zvSm}L&Spx>__kf6@=-q?g`?XF7a#EzOw(QzZVUWy6O31bsO4oi!vo3qDAnhfCL6Mq3xKSt=}->Yk{|6jYc0hK^%djM3Zs@q96B(xEag>13}V-BaRTjn==>) z8v@sys@UK;!#5>mLl2~>LeC6=ByrBL8pxoxSPY(lLrZdPeLr|(nb<~;<2hH4MBH?i z?kKXRRKa3l;Fx92v6D=-)9~<0)!QKJ*Taon%F5I(0MV``dLweKe#=K%4HgjAoO&=B z9h&%7z+iaYU#y*nK?n^?V(>Q(bp#2X8OSK^D`OE_GXbZ>jZ3JzT_95_p&DY}3w0_T zcj&d`U?gKJ!cs}9No{7HnnoW zhdum4u#69rZU+-YeS@h{#eNV>cKAR~)!$#8Omw!xQZ`>>z6S(h7MRYK$DL~{o^yH7 z7YdIfV9kf-IciWiDuR;iG8D_(L7_A{#t9Gw1$lpj_16A`{E$5llp8=s(Dan(h6Ff&mT9Ty4@VW+Fc^OPnkD#Ldn z;KfWvJwY7UQMf`1BQ;T;0v^DvRBu%beBs}7NI?)4~d!T;n~N@SCMncp7WAaNoXrOs(8uh%6wCgfD&)-7bREmQS<$YmnRLfZTYtVx4{iyI6gJh8UeD!myrkyt%15&v}>j~HwOK9#HQ4NP#x z6Mm|5R9z{Vb>pHMrtaNOH%83}mA_ma5$|iBcbjB9#(8#mfhGN8OXH%#KfzBOkmZHl zpbCbn1BTai_Osuh?xevGDpfr?lwZP3^M{{FqKr4}g<>ILifox?GC9WSq}MvOaZwR zr1AP?i^k7lEd?`}{7(Yj($VQ0d)PeOYf+^U93juNns-y7W-{$+&{Q)4$MGpV@ZPvg z*92)RDpn-H&`Hz zt0Ed6;Gbz(v+q~(Q)d+0bV(^syWDI`;TnLCSr>~o9OpS zl$;EApk`l|C!V=bHm^m`zx|DJe`X%h2SMZhJvCeyxxa1uTj$ym{kO1;XUV@^!hEd= zE8RC8o{6GdO6&~>sJ8jWmi9E=B&r3o@`R~4p1OR7_Z5Q;zce+n#zd=O# z)L0MdrrK}%x$$f4bL!7J&~x&97k~pPiMP6ZW)Lcxx)Uk>oyZ@GB1JTATH^C*V#*x^7zgPjB4Y9x>;SD#wTRs-fyuj9t!Qk{UJTcWMzoWrvh@4-E z#Mpn~3SW}rm3x;8wu}zxjstPWbc9rR!h*)P`p@p`dnU&--;ZC+WiaGs(^TKG@pj2d zV)dK$V|9mIIEGx>jRY0WRC8q0$*OF^7qsJZ;-H0l=`H4;Y0@!Wp?cQz`I>62U9<@# z)qC+MQUtZ`)KuVDx%>Cd49Z6!n{STQZd9X@q$wi8kQ!()v_5U)gVF}lD<^z>@<}Q3 zVhhR@eOSl*rdG7>A8*Xkc*I&R55^0^VYk#OG_$OR*oLciJ77l$K-gBY18E~pWw9Go zTCAS1oT_%dz^yui^^^y31ODo}YWZ7#P^tduqcv<1E{lh0=MjYP{8}e_TBiUD(x2uL zeQ3$C3hN;paa-x&2-GXUImVv+VyBo}r&fzWTAA)=&hE?Kr?o@h+jVv1)Ad|WIvwfG zqiHbE17$5mdu_V>*J)yags;uP7GSnq+rPe)v>gzS>uWI3aw0{86U*6J2SuS$lzF}F@Z)Iyo zM(~O8?B$Yc)Tv717i<sz$wH7p_D!SODK@_Pb+B{r;9h+CN+9bISpK#} z{P%SDNDFlyFhz<3946^8)SMfd&Kd4z64tSQ>WEpMrlW4M^h7?xDG+f~JbUy|)Cy6` z_)l96CQ!v-x_3v3o#%Bkdw2QJ0?puuo%iGQav1Z|=Izwv?ZJkV(^QPCu`&EZNJs7C zJ?#enH}uQHvvbb`XA)%iaqb!S0i48`q(Db^js~en@JbzR5bT zoB@~p?1P5hkpNnN4wyocH)^V}l)P0E6bn25Ja-^CQtpUl%-l+pL@w{P-)QJH3GF3b zlMaqOE?#bKLp?5gJ^3(Zuh;&w;Jc`{p@R+gRqwW{qu| zL<-3xDi!@013pU(MWyxg<`mbgy{SrQ?V5H`cLQVH*9e%Cc1j8zz3qZzes!%MO{1Eh zeDj2~ChR*4^;+4XmEzbWIFb>5s)`h!>EjBcKN|hy7KcNbe_z&)ee}r^0-8U571}>| zV4pwliG`~B^@OH%g9m?#p1}B|oHjpFu0KB)2TtN<5O=irN8{l~&6gK|0)U64526o6 zfD0WMP@W9)COjBZS=@s$i7sc&nrBBp{)s{6D1vR1aGtqrjV7)*q}T_NT!RG#@^cyJ zXX*|p$uO@lRXO(O>*eA>gfEq~FGd*g6>0TIM)Lt!kmC}l!xPzM6Sgn54#XOYGB6(~ zD1|pcOWsY)DbC;P$NS~r;p5c8&cVCu`|QVn74X6JFIuPdg)8pJAtgLLaN|F3c+!wT z*pg0=5KY%j0#@(|##9+A#soiT0!i*zdUQ@jxxmGo_e)#pH7b1ala(&or>~o|oQt@%ue^ zKDa$^-7{GDeLT-x14i)M19vVE&=&!HTZfy+yu8n1MXb3Gi^0EnoaWuVlFWg6?r12Kh2Gl+>RIn+ZCrDD@3-SYH1uo4veNgtmzM#ty z$|_+SfFp(?`@j(2ecR#UC8g`4CW1o&Gl7sIX0lQ|KsZpW|DvAyydUC(2Jibd|E()L zLBt(LT2WRLPmGK&qGMqdE|3ce6{@5KDJ9zh$>3{}eJI|9@=Q-5L|yi`yY>E(G4%CS-jwcD}``-5ft(H0S$w2vOe~4d&Cv!>_70n-G)ele+i7nF` zU||RQ>-KhlS@QAs0Lt(hObd6$FRBG!I=i=^%@D#MuL1c6%c!*vzc0upPX3u2I~-qp zc-TF7k@>3aCP}aE3+!iYIuD0bFF57xEk)OUbRuRATC}88<@{IF*sk zo>NQ?mSqdHb(#ge(gFe;6v~5k_KpUPN-NjpcjryI`&qm8#ge%{Ge|Tj(U;vHFmIzqZ7`t_mL%_H>3*bwxow< zA@h}&Q&s+{1cCRINWL~t#CG4fz}=neoEsg`E5^CM*|0@i%4%$1dq6Ccl6QaPs|a+t z3ZugZCe8^3(tK?P7`4CHI>#}(I;9Y8bQI(?mfF>N+N~>NcIewJl43u$&JkOzpb$2buB%Zxx%8*~dA zMKVeLQ-bX}{}g%;a^wov7y^W+2g#NFBZAQ5?!_oMH-tjn`{vsR{O$NnG(ER}@2-j8 zMqg*4!udk0A$K;{HtXd2c9!2}x-R^iiR3fW8e^emn$6urKf@N1ffVNN18qbuicb^k zabBVx?gB0iDWDu$LD>H8q7Tw4<*P!xhq^s12K`JkKJ109CaCr19T5JPyz?SV6A z7P+q6LdN#lJW_9Pv0&sw$L>g?jF#xB8Ki>1=8glm8N~xDUGumWO`af7UQC&&^MvIK zJY@&;XwUgfaMelQ1sfk5=8kP)XM0QeF@9Izq5K(!v(?L`&=&f;z5{8jBF8d5@HVIA z3U;|mcMuV{2pv=^>twWu;1DEjmh7>f#W8|2C-om}I*6$RdM!H&R_Nm6!*Lmd0W5+O zec*sPb>#;<$EAI=#}j2|Eu(}dsSRU;>YyAV4x8@4Uf$q{aYO9z2!zKMsa*X<(0zP2 zvdk%U3m@0W4gUN7ikYKUc_4!Zz)(yush(3tEX3&bTv36nU%R`{>XsMlH^BZYe+}uAC%V`)rHyxyLBiSsLEpuMn$Cw~j9rpO$t*PF64j zYlMRLrVUPc*>yb*WI6(>Myp+=BizZFmUbn6pZ>UnafMN);$pF@}r?)BMmp zvEg$tpp&Ma>(#N&sKAPk6X+00;9}fqRRhzx4>B-FqAcs#@vp!csuWc}%h9^)$1vsp z;p?2jGijqP9ox3;q~oMx+qUz@w$ZU|TOHfBZQJJbKXWqQ!Au?3RR^`Jo@;B}3;#^w zY`l;m=Nc`djL5IApMoQ4h@2UFbhr&6youq^j|*6056W{8Yn?BH`-3@-Q;sT30^PC2Q3Bav zPjMoOi`N5dS$#6kv^PA~$k5I^zYU^rcU_4rb%zPp&*-5CRgkqqJOTC3UW7ZN^@imRZg+G?Mjv8TlLv#0GC!mKFR`-o2KNV8z7HDKm}0Gl6Bs&i zP}11G(HCjaO#g6crMWx|tkZe3c`%Q|h{*;Ly*84G@XtuWL;Y^#_{=&L?TwO$fiXt` zZc$`2>Dt1@bOV}3l)>5te1%}LTzyIm)(@P(+EQ0`su6&X!>lJ92$^v}zPQ$xf7b^H zh@<`5d4r}M#?&y;_l|hG)>su3KE!DoRnlDDys@Hp!^cAM-TWPy@T> z4=XVrrv}##z57(aKMx3AFf<0@_3e^{W8K#0?|SB9Y< zS9LwZ427WNIeH)4*LQcP2L?Bnr(p8jkcEhpNgBirXo+wsZ#1;y+tY-8-viAnmR$d; zahi-9y!#8M``%xq{)r~QdJSR9z}i2}UV*@6m7E}J3K0kF9^>>g`#po;sfJncE(F%E zJJGNU`=P1o`3Q5-Drx*F4yMS#6k6icaPL>z^g*zPsLBd~=%8IoL2Djo&ABEx1n3dx0(Aw6*o+i{h7XWs zv+Y{=)EbzXZ&|z)Q)%3Jny~TX%?NJ<)uSnRCxx>FPeqKZWa`?V4lBGIBgQ`fMDA}k z#fq!&5y<_mVym!jQtE$oZGuvoID{HTF>A8;J=y}f)^k6Ui-lcX>d^OfF>GbF_KQH?BT4%%|&`iZZxeM7p z?g8ed$ToN0T?uTQ5MJMCi>G>6@FWEFV_?aC*C&pdngsF2cxw4Axjt_HAypn6@|O z>BnOP55F6{(89rMDmzSWFv)cj^zbgNJ2Z9)tYd;p6thbYflEN(CT3TBU{0r-DlVcZ z4l*dQDxPgmC4C=9Ia?-CDSb3lR(uDi~1t-15>jcmgE zzXFnj7mNpdz{>>X8fOoaklcw(dxb301iuI}DKOZz$c2$n^>Y1zidZCJrTQ!|9G_In zPFQk~f}0ibjukR+1HJ3|mWeA4}Dqqz9m-lYN$yks+Zt`|l=J*@jt} z*ONiEhmas`K>Q;_(+03EDr*(OIadph%A0o%w^BmKunTb(wE#0`cYx5X9Bk`!GwI=l zxBd4e#BS{B%t|YVKOKxyk;+Tc);ff4L24@Aix_M`E0|%|=Bl8)RKc>T9fR*_ythS) z7t61Y0z$UXO6tr3$q5C}nO%i1H^Aw8b9!4XghRdE;KeH(_3|B{J^Z{_ zcsctn-o5)z&+AS&H%Y9lq%Z1|Gp`np3Gs+j@m?@BgWI%rb*Rs}1k8;%GqHd-Au*)%vte6HpqHHT1R!{i;KC6HE zWRYo)N=?i&o>x&7u^_N_3oPxst_wYmLs`Lu7kt?rJ77C@3?xtQ4Kj|y#`<;=O}4iq zu$4_S2XkH|?ZQX)NQ4c96>R}{Ji1&)vJF7f^K@`k8OX?l?b9ti)MbU$2#-gEA-WLb zj5svn)-4HU=`cp1kGI<%!`6z)tDr30hJ^iKI77R(OJuk{8~0yH8LP!5Za@szp_fyO zfs1R}zA9yZL2b%_)Q${M1onr;KX`$Fnt>PNz)^`7q~vE(_P~YwLC)XW_CR??%nll@ z9s7|kp1-&9Z`P>5YZ@&GpFc*FF%Ju-w?Goo#1yl49by{@)%FGH81+7_c5$(Xrn}T! zIk|W#m774jd(Rtg7J5GCcJ2X%(w?)N$MqRq?)%?axv7Zg`6;n5;px}MUg#B`6FLOpApOav8Gl6imGBDYl?l#yN zV$IdlrY9cnWWMPzxgpRIr|Laz;*_{g>o*BQ^od8Pk^)HVwpjWM%Pfx47UQoa09oaPycQaKT{y*=_bn9`>14Td z4WdfcWNivnDL!9i`2n?~is5VM_49e0SA>2vTMC*fLh@H?)irq#)}phfd^el4Gb>X2 z%@}u&%5*UOjljZL=SgT|`ZQm1|Qg)aeFh|L<9{6EOo=HncF%6=Y#ptv6$I#nrBbALT7vg^cGHvHcmjxTsreJ%x zrdmZVDd&!fM$BRdD}FSCLbAe*gr-rh;l?=@g=sn@c? zNo%6A7TxfPlT)Ql9*f4KSfMLD%EEsxuyfV6@_el8Q(?WWEX(3W@ z>I$c7dE}sL(O*2h(79YB`E^*RY)qXLUyZ+&@_#!dJ8nxGp|yHJCvzx!%bzp3)mg(bE~6Sj_&ki zWl^)PDtuU5!FGm=7xV?ZBg;vJq(L}3NKV9-wqalrLt2{2cdJ$ zSjm3`#zE=c;7pQ=v5lXg>B5Q}>%3Hn`Jo>7HLsmHve*eKN# z{?4W<6qG{g_PJfTCVxd%Wduc&1vak>?}myo9A~VEDqW^^(`}^)O9yVAu8t*|LTT`; z#Tixth%0%yveQBM#-N*6^#QpXP4$zw^>=|7=*RB}I^|k+lR5U|t|@McVqf;R3hHZ; zpc=%M>y*QXH7RrmTt)7DtZ@!y*vi{}Zt!zl-7{*}Vx-!5xFC;kH%FqO{1hA%E}sdL zf@m8Xw$$QZ%W6R{L+%9@6u(6ZSLD)B23%z zGD!j<*-yQu#DME_hWF(C=v?I^=YBc|Z>899gqN6@+y7u8mGAw`lk*j$){x6^#C<&D zX7IvscL!SS1(&4tV%rZU4_{|Qm?Cn%e{`IQb-BY_KhFzIX$CXaaQ6L`c`e&n`JE%{ z_Vq%Vc3o-nfRZb(`dFQOcX+tqdl*FexEyy&ZRi4`(RrMd= zXu7;E$1&yduE2aJKDpWYRQS|>Kl4!hai8ycY=!aiOL+-4z9+uF7P&+0f{|a7#kOM=X6Nl~*eqnn`{`Zth`;ia+_$37af0=mW2J9u(z;ab z0(S4QfrALp1j+r^P9BRtivHU2j1WegiKe=(%gX@%(vX1%%@XJ%cNyHo=fD0YHm z;IQ^bsBzuvv`BUG+`9u#KvLk?HaFWRr}MzWh&MvMgY|yNqj-LCz($V;Lmt+SCyIDsH6;>h0iY z`RLk3=1+h7onvIaT1zqvDfP`-YH6;W;EGPn$*`P{qdKwDxdz>Ggyhu zRDO8g^#~e9)y+`JUs|AMSmDn#a)G5~mPryF8L`kn`UjbK_?Rv^lAD?$6syxp04t(+ z9rG1`(jJt4KgAO~#j#09dhghpF8?IK;X%v{ZnvP~WW{1p$FbbN!GBf`JwghfG*6s;0udkQ$P? zs*_jpKW-}p z=ZWs_DJig$E+nDxYJwd0yS3r%DAM?Jw6+|>MW{a|8|$`;DHr|t#+BL5$3^o4xwsw2 zhMM-xf?VsPyq!`ZpB`1+4^cUfLiQ4`xpcX)=g+aW@Y<5J|M#fecB*CT(*4)Vk`KJq z6M4proQNM^tDl-nN^j~{FO)=e{ zH|N)mQKX;m=|3(AZ;82xxZnyGL+H+uL7u=dwuRCPOIX|(vljnY#?s+D>#f=3O}EwK z+0`&oV6vhy@H&f#x+R874=G5mFC~8~fMH%(#eks7^TDS^PCb-f9{{dVZ ztye%>=Y+JLc~M%Uv}qoaC7IE?pGlw5%F&{JqH_hguthIM)rsmW!%;@oSsZ}F)GHnc zrJOQ(@+@(c*&agg;>?J6=#cN$ox{T*Suv70>vAi_nITeE49Z7@GzVz9bGFUnSK(VC zoox2P(Bqu2>8dklIIp?>`Qq7NhQuUqgdby$e&`=8!B2Mw%(2R4rj)6pjykmOqC>#z z1~Ns+G};)Iqk*=WR*rMDe&WmlmddfOLG$DXj9NMd~Nd9p)uF6w%kLl9$()rNA{B6s;3l4#7JRuhT8-Y2Z9H7_7w}@*Ku!E=$$OZ z&N-5r;(^)GNpz35qPh?O&*jQQx;EK)yPt_YHqu-B0L5WRCg91x|C)(_+5qeMq0OW5 zWnbS_j5r|)x>_AFI|nr%g)k_55!@i6hzy|p4m7Oe<#0i73}11L&(&khW~W-%F}1vv z-<7K^>+xxPo3LP8O#TEwffG9qtWi#% zz%^vVC(;>AP%$1$LQG8Xgq(g&M95smh;^G&_56)UCIW`Br73O_K3EIBXq+q_jsl3U zC;*)HsRd@O`ddc^X@WSroyy$w^LH`v75$kEKF~jQ^1xAthR7yK({npXRZaVb@lGm^ z>~{w#S#94i9n!C)hTz>!WWSenX?$ zPjw<$tS?H*DN@@ZQ(F1fV23rORP_*Ea0^(OxUs4BwBKI2=4RH`bz|G-1&I|TR~hLZ z+dCsV@oyb-J^GvX!CNL9UAvV{I(0LS0u|(EAqXB{=T9#t?}hyLza5?Uz1;X8FuuQ% zKGWS>TD}KgFb>+JJIRP)C@BMvWO(aFn`&4ip%FDR8ZdnamBx&x2mRf(acb5gSFUL< zV0PgoL?T#?0!P_?AstEPevXPvHzfVj9}xT7#8{u)2pHy~n+U&kx*UEgo-pLq2*Ec+8Ls+>_H_g@M4yVJAwrro_0RG~}Q@7eEFy87Lc>sD8GIR(97j9|8xIehzLwzJk8@ zX20y(Z%3RQ`9VkI#QECM)mjEKzh$=H7Zh`um{98sb6Y@;Ac?s$W;s*;h5|X1fRDG( zfpPrFQ*Wm^jfhu$?iGPKkE2J$%_$^xLSh7=f!?u~EMDhQ9&fVVX#V~;Yc4pe1*R4h zx*c&GBuO~VbzG^(IK7L8IHG}T5lCe02|JSxW^}aq=+p=lc!yh|f@fd_MnWVo87fjk zCu2;a7_F5kg29x1W@NaNR)bw_UwB3OAd{ieP_8LwRe5kK*UQbD2|&*jF0wg=4i6jR zHEqL?08SF*F-8^xPj;4^OWSD|>jFwAN{3IppqqDoM7Ah=vYFdz>UL6e3n8#Lk}vOd z&})GYD}Ziaeqs2>QiSgat|e}l_*6KkrQaU=IgLZXHXaKEnhqvo-6VtC_vI0 zt?c52*{7Tu=X;w?CxOz#76*;urWm#79g18%1rinGVt~b%YI0Mw7nGuM(Q8Sv~*^_1;sTa z;^dLyU7V*rXbbxgkKnHBr+dRTupU8*m^F5WakN8JsguJq zKTI51X5p(2$XkCNDd-`Q-x#{$7F#h#`q(`yqH2F}*gPva7?SugtdasH3Hj zd%B8HsnV!+w=zRd%FlO$gfJ`1{e8$^HC7;sy-X%37@CZi!38Sbl-a+07~MAMAK)5g zU8+lhWSUVdvw<%K4Gn^z1un>FE_n$DSeIj(rZuNYoshA3IY!4TXsf6qLc)L<^r?c8 zJ@eh*5QKRC$UNTp|N# zrJ}>}8_PCSHyV}C+01gbW~em<+lmgNR(j8dtx9Ep&qt5%SK}IxHqr_r*X!gHMm5`3 zt}0z-!pX`8FK#HNWl9PCE(KOatXv(Q*W=pFJ8>*2S;t$bss>+`^~^}WciPSp2l40l zzQ#t-i1{q%@4+>;WYObO6v7~36JH8%*O9pYXkN$f$Z=TL#sm0`w0!@Xbg0~J8*vN zrML_rSJ?e=z*7`ExOlO9$5cT|8Ym9PrqH$#V1steQJO0HC0a^4bL^SZ!@8J~ce1u9 zgFd) z+fo;er?fKO8(t3^%?o7E{oSZ`d_|?XhMx7J_}sbf$d`N!IOMN7o~3DoOa3M>kCR*L zQ8?PIB@3R$oxaWk`JL66j=nkWu!yPV6XzkHh84A#xwTUCN<`M6UvCV~&F#@VOPr({ zzZ6St%_U?_D~**!;nNS0W?Px-Q<~S}+wA}?)@nVY$v`vKe%08PH8hdjJ&YxX%ErqY zB|Of3BBo(yA=SZkGf^K&Ko%&R)wS{#$~Hl=9@^@W&yiU<&ts8(Ig6!YT-}eygJ_nm z6sK*F@)x8(H!QD75p+0=SnGz;zcHg?St~k1q-eYu}5$*d~nKwk&*1Z<}ytojm2FMSgQceZQ{0 z`SCWwk!iihI64>nt&mk+^SGb9kLG(Fwp6|0kQYcEOmPvlT~vh>g1PC1PP7j=pjPPB z2aR^xU>Npa%dDmTwh$MNKl1SKBQ~U{7ph1-I$sfzO+`b*lT)gn7^I}YgM~P~!}HUE z#n}F|u|JU&PrSQql*P8475;|(Z$=>ov^iD`3s#7d zI_vBIOC(I=r))Qvkb-Y~BLd*jpbaJh0`xXfF9?&$h#^xFSjDxpB*9FS$je84KWePW zC1wvC|1|7e%Ej^4{7g2tmn7RT$fc@EoZ+RsQaPk<8Q;~t#=L6Q6!!xB+&2kDXkM7R z1+7~gDMi~QNy6HPpJM-z%3nv{Td=l8ubxFkm8MUQf$3T}N{5M=qEBJW4OsNI3nmn@ zad8DlKn!`AN0>Y6X@h2xbv9nUkSK zn~2PfUvZJvSMGN{x1zLkB(AMPUy{CuElH$MF(y!%Ghfv`>*k$0gJbjiY%N5chdYVM)I@W|JN4Rlo zs(53P_0r(4UJN%(5+F1DN{x~(vvwd_((^1&tv9$ww69`|a*61@>Q4XB;Mf!rrhlhB zd_f(yGH86d*_x9}?9$V}*D9h9^p62_4{z#y9OaZ=uS%ZZXm?q$g`T@pf z3@Wq>wl!Y`JX=vlwBa=|mx+gm-04xF7{uvpqV-^w;)xG_J-VI9yOCr(ZO`iqtl?mE z7#)#Y=9omQ<|eKl&$Wf>h9dHb&Y2+>nci~zM?M99C~j-yjBjw<5Q;!bsByf&N z_EFVAVQU6B2K#Ewvr-z$q>NES3-RkIIT9Xoc;k$Lg?vfYOVl_Kmigq#@2!j%k@`-9 zcaJ?Z`gwc%`Q5zfC@_vKkHixhjhq}T%$cyyzv~Vmn_6V(M=*i1)wT@E3#JtN!J~+M z|CVIgul)t%;}KZDZEi>~d_iD1M$_~13q~g}?nu*J_b??t$t15<5~$?fcpuK(gx6_w zV$YiQ0yy^N;4L$#Bhrn1KB*cw(Ux^EZBz?l5u$lYO;I8_>SNq+7; z+`r^em^EKVM~WU;EgUJV*cp1F6Rd0BWv`Z6lhTbfQV>63-WrufM*?CWyq6q?!|?i~ zv$!N_#R1PeMjCro6-#-T$F7X9?>=BDwdJU4*O@77@7xL0*!Z|Mww1$7?}SG_N<(5i z#NfcS?|_Rkh>fnjmJu&9zahHi?78>2UXEtWU0Y%h+2_paJ6MNJMrU>*S zCfjB#2|3yZCHkOdVaa^eKCS8Nfs1YJ5kO9q8z3@_u~wL|K5SM9m|ub>oe1Pr5; z(ouxMkSGHSVo?1HIY>M>@cm{4)Tduk%T~99PrnS$wFzu3%#+D+6YP{9nb!ECS_;}m z3#dAumG}p+kh5op{Zbo3pv4-o1W9v`_7W8NwYSTNNgEVri#KGkw$Nqb=@L0hq_4+* zD;*l59dE=CMc?!jJ!0<05V$fSTBz@}kG3kJ2SJx+An51bZ>Qut%5q2wkp6DjAim{# zaQay{e}t#R9;qxE*%-Q^Hl-dF(+3H$$c@^W9{h)x^+$RpnxiW>N2&u)l=T3q*Mw`Q zPl#la1Oolk5rB^P#?WRoF`gLh*v0vwQ{ZB|-ddX|@40rKWdmY^7#Htko0{>+?9dqe z4rRdDk}Dm+hcPU|25)89#q#JsnpE11$(U?j2K!bkE2R*PLe7tguNtOy>Q0|wJVG>y z;o1b>cpg0(g8MFco4+_0TwhckElWD80RBZ;h@=C&(C7)wpajRxkD@$gCToVJkNRy4 z5h8bPS2&2+`bq^mv(H3{I-D`zIchYZCwoD14nI5qVFIRxOYpYrDCm~LsDP?r@i{Wk zqm$3}L#q@14xuI@pL+L(Ywp4p|5pIiDc=(V{1(5LutO2*pH6uv^NX{)6EAx$DL!|> zy-nA8b$9SOUo*u7)I7m{MX~{GKNBLRmE_}~2csnrPOrQ@HDt+7@Q}vidu&0mhd*k|{IzWs@rF2i< z>>O2Y-rxknMOaA^{u&iUF_3yv_)DhVU&c_Zrq}@)Fij7_+mMzbRg$d=QOBM`xy*K1SU9vy#&MY~d z5M%+Od+f+SAk(O5ykp;zzxczRcxf$KbWr0FT16M*l&P!_yu~~`&H*K$U)8XM0vOwN zJpRGKL-Z==R?bT^Y8<|J(28(QfBgeKL!VwxAT#gtjlV?1 zPtwlOsf#pPq#<_8po|gp`%s<$4{O{$_Hm1EQ0-^T&nls!#GoG@2(FmT=hWD}b_$_2 z|LkQ9|9C${|1K3Biq0Ja6fxMqN_1kw&z#u#3h|Wv%^~~3efvHHi})?Us%j8l2uNH7 zQZ>{lWVk~LM_t`)tpUC`92NLvcnkG*b99_1Wo%~?gO_$9I<~sF9P%|LKIiIXzxT&J z((jlLy0`jOZa5Wi*Cj?nobvR(r&qUqk<5N76FO0u8Ji#@1`OwHD;F?-ILzMu*CxMuXtnl1`i8ThspAYk(14 zAKY`-UIG*`6GTiN%D}Oahx7vq^N}6We9F4EqxG%*tPlz|jjuz&JN%bO$+;T8rL@pI zOi(B#P=RDVmq(=Exo} zPtNk_AJBmbW-jzB8u$4|{UOIr<8_U*l3c9INnokt~@iXvh=td4=%DQJF1Vypc zt?zk2M9%DT5WKW0G;0?jfz)VuN~PI+EWYUpO(#e8y`vHDs?>Zp@9a6U1N&+%0p$80 z0gwx;5oa$1|8=?8fLM?54QIHbVlG@iy*yZM8H&YA` zF}b!$vX?db13v8FO7tmv;i|#0mhw$a*b3k`B0Q1w*e9+8zp~s-U%K$bi`>3b1p=p{ zf4j`B#&BT+(M5oYNV5Dzp(M~_QuJ+NlrXsxP)kII(2H`{pvFmC)%|+NSTjtPovk;; z{QIh11&V%}KlP>2p!~DU||JNU4S5%jMTO6 z3V@`>erDG;kTmt=O@^lhVbYxYbA5N9TKd42yn`eDm9LyUL^CB_LC&r-qEx{C>DEE{ zur8O;O&|8&+Cp|5{E|X!!297ZAN$GsW18;h#E(^q;kr3iEj99P({<0?MYmpVNNFp> zQ-#uQ>LKULU5`XhZqOm)>CW(PR8^gy2rnZK44-YLkdH}1MyUECcXi9*$ z%mp4CGkOc<_prk-7o{`ZAeT95aY-VVeggkDT~n5M@hSgA^Phu`o0yR45&y2fX1c38 ztkUcm1c*@D?m+PHgkt^U(H!q#x~nL$#~}Y(al22foq9Rae>P^n@*^b;J(o!n13+P| zL?8rO?_UyHM$GkgyTDo`h?!1z8wNJAy4&ai;nw)d1ez}&qB?u>|I+NAQcShDr;Ju| z(H#`&_g;tmH4?5a7 z=7s?OFuVn!|Fg`UB@Lqq9Ou2Ovq$=;B+x^C^+##e8dlD~MM%IJ=U~rUVYp!HCj2{O zYh4j)ZgTQ5#*YMC5e?4~pg@|=2760&_*Z3Tq_!~%|GLM7o%_n12wh-paCgKwnau%O zy}%MuYp*m$1Ksw@x=K+$RNHY{IA1zUMU-KWLD|x3gs+#KNI>2hDn=Ef4#9Ft?32Z! zvMxbj1;!5j3sCv?3i@6_pD900e`v*~@&Wv_ooCZ0!f;k-kIdikH~!*5!h2A@B`~d? z7ssZ?5{W1kG=i)mYz-CJ3X{+Q$$+;&fCRT_eMFM$&9VyT#oe2wlx|P5g|8x~-s8&T zJ{ds6Jd{6M1YS{)_zbnIqnEyb2O=|E@HKZ%H=yD*UM)s?S=F9leD$K>JqDmDOefsA z&ooma|BS|*rU63cf?ew(dVH;f4SA%iO-Z5m&>cf$7UbZukrvLiOgTp95_I@y;xFS%oQA1b0 zQu}=KrQvIRNt$aXF@GqfO>I#_&zGoiad2JpVm0RD6vTXn;7Xld-`e=9!pORK1Ne5g z%<9k}c)#YKhHh%+riB;Br(lAl^wTH=dJ3*#tbX*CMWFF074&*BQHu{v`%o9$p5B`~ zQeE>^`QIC;HW<*e3Zfcu`eevGU9Zvc@?5{|MZ3{`SfqPOP#hsnt*WzD&`D%`4$E-UjJ39KT?E}-ov`}7CRgeE zVND|Nn`-@53v!1!0gX!8+MV*|fZxYG zKahOL&(xRI(c~`K!8qYt$N7Rl+7?wiBY=K>*LL1Kf9C5xtXuN106Xo0H{uwJ>t^iH zPpj)l!Yw+ojVRph(f|wrFpWfWF|iBoZ1qp7j(mNnj1qkN3?^STk3ZqGaV4MZgDNz) zB^|{=TBh`S)*p%HD^)<34C(&W89gkST7?l;=10=QhkOGYtCuU9?jkx0B`bB=t3X>&WZ2=tpN zqnA2eW_^yi0{Mk;K+fyp#+SOqUbmik!zW+nN~QG{+N{W$i87Xsnl}z+q^jCJ6P!IDP?etym8qJ(U39w}6=E7U zDMuAq`ZuPszje2cCP1{(lcoTgde!O}1!N{ndJRB%G`Z$o1Cf`91*txM?hcNBA#Fb2 z4(~@AT2j|?5ocx)jv$MKaCtw6not3?{=#YJZ z>P65Z#^_;S{Vk_}*dmSK7I;G)rfOH-T;g?CT811E2F5Uk9M#x4s=v%5YyuvV+GJ(qO4J$xhTU^pIP(MSK<3c7^6!?ge&?kRIf zx_}r7xH2j|zy&LopSncI@#XYosl@FI9^0?cnhY8Cu zdIqEtjT9#xTu@MepTuhAx9KLd`vd+|D5_401p`~ZzA7{8q1ND#p`;C{*Mj@9PTNB- zJK=E1pOQ_~`g7Bv#iYEsl<7v_%HpViwo=m&@q2-RD~vgUE&irgJIGW-xF{VpU!?af zzy9?W?U|V(oi=zk<@wE=$#8+qz&s4AZgPt`;n@atBKKfIa<9?+g3tb+{>Y%*> zMk{8Ds0YWqpqJQ4sjAJhEwwzie%ZTIj{&&8&$&zMKtXh90$1SsP*zu6hOT}XkRBUZ ziFc}tc9=_)s{=Wt<@RLAYDOs?_*>)Dz8m;hO( zs){gJI`ZTj$(DTbV9>(@x#-Su%EoaYhOt0gMy(@IL^`$r7XqV;s0xiGz2jJklTe*q zeBg@fBcBB)I)lc{*hxub5yxN>VDHuOA#zW?1ov2_s;!7WR3~NFW&5j_tV#-qH5j3y z`CT$V=v(x1@{ca_4%wUfQYa~SSwd|&6yZ9KjzZ0JqnM{qA!|Tqgg#pGF3;jVz4TG& z34f}6J!eJGC{T6`do1228C{dgc3q6R%9Pnf*ra@{NhDng8YA+n4M0 zD17Vt9VChhrs)h~Uic>dm-y~HoRtxJ`Vw@f5uOlALJivxpRCLinK9~zQb^_SasT5% zXHwcH*O~$=S1uHUYXPDaj=H&&p+}SQ%FaI6JE8TN@jQvc>=n+ z;55)uJ;(*yAX6CxGquLd5rFlaLzHa`6g%np78DKl#s5cUmr^Q3IJO6&o8Oz6oB#Wz zWdt*|BV*~73UX1khcaO5oLN#LG3c@~bf|@pk#R?WQ9u#CGwV@hXY;BCpcpRZ^Mw$A zA9fFv`D*GnthS5$UC3V*fdAwM30N#Rt`Vx$3V`H`sCs@`)bC6{+#G`SA=O_wN?Sz$ zgV)T32kXDg5^y7*n5&Q#%NSyXkXI=lIBM#F~%kdj2F2^|;xwCtXYU~S_fi@4|%Qb-_>SP3i=31b1 z2Zn+3kzH)Hv#dWJh6oazIlmTVo?2O=SXGxwgGtji4jN5lT)GXm(UMrrQp9W7xgvDjyVKJ{4LygiElFVTu%_5^y+o{=@bV+ z59K8{L-kjpL{ee`$+AxOin!)ff9^wFc7~uT93A(R>K#?p=ogacpK#4epPtY>ve%^> zUR}fYb56P7`ZReAw#&Wp_rHREUoNGFxL{8SB4aYrt?y+fim6CkOX7dJt+oD!PvMCR z3HeUekJ_;SV(E^(G7CG)hEHHPnR1gE_f4C}XjuyM)C@D*xrFl927RWuW z?^!Hygj!rJdO9&2MEmG3fV6GgzkHBnS}>#V~xj#fn@#&9&WJIzSA#lY`JWd zVFCxwozdj(L>kMf9nQ)qi7nUR)oBeIpwE^l)ftzzXFq^oJZ@T!SH`r7I~c!JX=3~25uG^$qud9!a~XE&!7Jp}iRtbFA8PJ$R(@15qKSK3j@+PG zcL+Atro6bi1wSE!@=V@$bn0M31)-|&iM#BKl96VDk+nrFZiP@9)fPQ|)6Z5}zhtlsSTpwK;xdBQ>Eu3*zH(}yn) z!%aaU5Cx-#vINo?h}9}K{~5bg4d|Q0@%inrT-NG!kjPI9I11UoYF!s}Rb4@s`JU(m zz$oc?R#;lW|FOM2^eLL1OecdZtr92=D5Ufk(6J*^{rkC|#%bIsQ?4lLtxvEI7h1XD zvCn!Otxf|M+68NiiMi#Of1II_3u-1}ieqk%OSCpk0Jky7bpUH=;QP907!FGjz z2K<}T#@98Jb2R3p%AIWvh)x>Nzb14#wKin>9IYr+P0M!`ywtHkK)~0O40+vk0e*g) z9hzJmWxYJ7F-GZ9ez0;2_D|P|f$R`mN0vHUgtx%GE*zPHNa)Pt?^rZ|fW{(f2F*%7QHf5vJB> zcDs%OAXwKE36*wGbrPbEzbLxS$?AoU!<(FneG28{@geRl4LH@srfccN<+2;gKzSDf%^L8RdgPIDTY zX5%o}JJHkjfTV|g=!gA}MGKI1K?c23Fc^l^vm*pBoEBH>1vb*S$lEW=;)o#4IYU1^ zo~SNA@m;kgJs}sEx89m^-uHEItdY`!W*PF^x)(=kqD+-Vhr_G43VH*OcX?HhzheG6 zuiDAB7xU}!pBFm(peeK8fCz$(G%>Jfk>|K;^qP9=pwxqg=Dw{WTS>z-RyLy`#c`4r zc|QQs`2stmm!2FNhKI&m~4t zA2Gc1x#M}=nrAb$pFL$({JJ)9!KB&^2tSi;nU8b+xvj;|fg!;O8`sH zrM+R!f>8!p2xc>MOAjM?YkHv0Ph5Y^4OX{o1%nP0tDQi3J9Y+iBCYt4dGmax%)4eD z@TG24VEWEGi&tmhjDVAtO93;Sej|9zU_$wI7#m27RxmhCH9&aaRT?z^HLGf-RSKsp zR&;1q88s@QJGy$K7s;s`3^y~MBSd8rclA16LAK`|o31mjG~}HWqwrfFy%@%~ZnkS% zZ9>0L0Aqzlcar?u;MgUYLE`(w2vC^LCA-akk9cKkZ7>V{HW8?{IGU|j(oiinjqqw& zg=~Bq88c~t!Ub5i0jfs=VBn9Vg5x-L(s_c{<^eD+xS_GPVq@7+RCA3};bgENc47-H zInz6J{pBVJjhnn`S^x*BxI9M0hN z+1v5NJi(iCixD#B-U&GGjFa2G^pPFwE%quk z$jnGhuyfFB<&Oe^5+6|M#K%+^xj>rG?1oZKouYKgpXIcen4IF& zI3qZ@Ta$d|-8D;iU?Izc6egwAu+TDM<0$yX`!!yTb_OOZ&l~a=e|dhYlxDcD1!!)v zl)xg^K zmOhf00I}+j!{*J+qS}Lc(t`{Vht6St;x{X-XTIatJCw(>wm57DjYCxz`~@3V#~%2L zKvPv`xHMN2NQGKVVlQA0MN#}RrWNm*2_^JUE;9@wkYUw;GVz^Z;)YW_Mk(QgnZVBc zb32u7&W3rPedxpdQkwS`s<)U78yZu4ilWwkFbnRr=VEu0CvmFOME_T;irx8J z7CP}C7lp(EWi>1}&3y@AVi6Elh%P9+mF$;|=xQz6d`}s z$NqnR)?J${21Y;V@0KF-?32aew&sDM4{`(tPw~b=^HKow?c2+hKdsQ{ z6B$(^#5}4Xi-TWE2xOCF#!0B_&a*37&Y#8&>|(k+sk9d6uD%ggH65d^Cv%$BQD~eb zeJwLo5I_}B>SE7~C_4xdJe0 zuA0~YM8K63O2C@_s`AGb)`k(wz7@!U;_`$6)V6Q-PkM@!)sdm$@NUwPvO2r>zJZfF zlD;%&rCy$hxWXPkhaV)gFO(wZFkEhHKtRQFnXbB1S5nuu-Lndf;_B}&U!>2{4}E0D z&ZNpbKIs+L--du#{I3BGf!wRq#E!s~^z#e8!Kzcf1cN{?zMbyb9|hYtm?(FKB=2z! z{?4y9-2ugrg|R|E7gka>2LuGa2Z&coUM`@x7o41JrIMPGbp1(m%)qGm)X}gi26WIh zEqw~xj47oE1o^`~fc)!>*@!TtPJRr=R%d2_{8ZTN&tf9Jmw&jAtKEX4c64XRc<2RC zwknfby}(np&m)%{RUnYayXGy;SQBlWJK-{Z86bk1HR>k*ZL*zC^f9aGHnmq({XowA z0_3ev%?udR$M%@+;7zYIEF<$f8rP z#i>Wm6Q64X_5U!0HXphP?w+zrhmUa(&x>3YHcUDTGw~z8ZJ&GOi%#iIJl7=RVFtHW zDNn_qd2pc=@^aQlCTMX<$-Og81CG26#WK@Fz0uQlL=ex6Ju!OU;p?_TB)Xzw)SLFw zh(}9C=9ay{WaVKhqKLP{CA#E`zS3VCo`!n_A_1`^GvGBINsm8^4&#HNSFMH-&U-t_ ztD~S!yuv%-zH`a955>g_UMPuBh7I-7Tnui`vL?&g7hg{kv{J5;v@!s2SDVP7CFGEM zuYn>x+pEhFn*Pv89{}JO!=XOr_<-MmTLmENPK$>24B-AS&gh2S*F!1r%?3AUoHoyY zv@%@@l;7|s#i<-1m35^CewfylZC%3c$t;bSKh$Ycclvk28S;)F#qx}}JJteKV^Pat zO>#%c)$D`a$lNDq0Wb--l6rwdqXILg^&JR%FpP41oI$k0sYB%q=zm7i;%*WAi{lXT$e5! zPPJskF_z=q+iO9BiKgS{fOLp~Pvv{ol~1PwUqJLD=hO^$xB_d1FSI`wX;TKzr-a&RSS@JC_C zfT9b_ZDi!+wq64lwV4zY9hZl9>li#3MOFD$cIzJ;6{kmDpm1jE9YlguWn)8*^m$!@ z=3q(rhzHAg`WtYLt!Xm4W@DfM3E8_jldO#qF6(c)JJ-2gKRjkB`0+Qyc{QAibHe&H z;A(T!c#o)(veQS{B&MQh(2VTkuZ}8`$xJP>+O=~LI{ou0+fikuBiBBhSl5g7Q)^$` ztv&@7YxHqX66+Ua2OdY!RfRm#v&PvA5P{dpUnlSB;whp3r0?X_=ETQ%hHL$`iE=V0O}Y;${sKyF$* z4Bb#x=yXlj?Jn2agMX4h2=qh@`MjwRDfg_JZk|m7bNSi~gINtrannmX6%JB>oYN3We zeI9?R+;F(g4 z8@?TWVtQMz!O{G;HBaZ4gz*5yQ02%Q-*i;>)rodW&&mX=>kJi!i=m6* zU98>^FfV>jr-2f@Q_q_z-QP}0o$#*-V;lk17_`<<$IOCPnhP~`R3GASm%nU9Jv)}D|Iz@SPK_E(& z(#FAbHFofZ7!bmyRH$t9rgC0o%k(MF?fZkb>2DVNtt|{0?23wB=1A)Lkot@hCM zXfCdvu4xX0Iu4IbSWCId$cp|8z~`5oZX{RVjRJ{@Ep667ZqlzoXVVhc zLrfJ++-YT=`;eRg?5l|M>3+F*U?U{8$nG?^VJbZ-r28=X9U3$DZ=ktPr9kI!c;jz| zUGJC0oN+TT(mS}?>mCJ@)<&6#=ldK&PD{F}f5t6hr-m?$YOi`Gy#jld33?zO!gB_c z54yk(69XN($<>%SzP4t-(77_%P57fR0O$rtpA+FVrvr(kn~}F`Zo?;2q~s%rKR+)E zE#upFNMq`lo8Qw@Y4*3GFKAlR3;7eAk&^mT!{}pZJNVI+=JtL6nH*YGAj~`_l|m#< zlY=3oL}3RPSUsy$6)YL%6UV(rDPfQoQ6Q3xdYQ`dJlsX$O^Vg<2A|Ulf@#+S{}y>B z&C$bf=^vB53{3{R)la5~!m$=Kv6=-KaDlnN0Fx^GLn}9#hWZR)=b?rc-5ih=tV43G zOz66(y=dM)E#td>%HQB?mtlBJybvg`s;|)?bc(bzYulWECs;(5X_MeGoGl%U+txZ_ zPIHAKe6}kFk@q*ll((WXE5j<$PkJ-PoGl{4oSqHBXC{RHiMP1n3+X*dp9inte%TUw zst0+=CPKCz2Uen|GX;@by)w0*vBN?-qxCkEd9CRP?2~}{n3hc~!QpFi`0Q6^S5}Pr zD0Zm!RUvfSo0fJFbu5x^YMHrRoKA;A3z6ZYD;GvCc&cE#>(fPIO!@%lvre9TVjgaE z&jZlsYm7*8qAdcUr;zu>+-eKhq|i|AX*gYFPRC1wM`VP~9cm~yQ%~o)I|S-<69+kZTs59{H-Zq>m%jrnb@l#7wjk?y=*i@9Nxexb;=*kFANt0>8-4^ohu|_ z$t+C5=grf?oLK#hr1EjA+e4LpY1a%Tg0oK2-+%j9+ZT~7e|zODK0mH(%*_LSN32?8 z_I3S(1pPE>`<6b5LP0BTy{Z0VrQb5AAt38nl0`BbM^8KeO7Mhq&qHaXLPdu%pf_uq zh>Z<&4lSwLG16MSfCCk!Ga*x&QDRajrb=)3O08xGOE4S*6+(&SsWtSK zW1KguYwkauX>1s_4r*7tJS6q-f!*2;R+7g@#J`k^Tk~yw$pr;(y>C|NQyyDg81+}o zjx;SJC*b{GBw;<7o{u}-<#%VpFk5Ep9|F&)wRLBXJwLkv8v+~wa$wrK4T zQf27(gz1WJ*6xnX*GvbWfGkCZ{qP_K4{F z``X?yIWAb^Vw6>B!et``S)ylT)$u0$KXPnD43q^5#aWv?1IwA}?u0M-KSV-vs+y6# zlp#-PDjm8gOXi*Lf2<>YBh1Ck!6}dxU~^oVP1<#E>IAwW+_B2-CSk=5?5lNgf5*NG zfua31m!9(P)c7>r;z4o+2k8^S%PoY7SPTs2t;739d$lA-Z7o6R{+O?-;cpTwgj#f9 z1Wt-v%=)5Kj|<2Vg_OKnW5+IHG<8+L6zCbkG~oq%Tb}CQVwzUD@SIh`5QSeOk) zcKti**OHIV7=CLzRnHZs*`HhLLJ%hG(khb_(u5wX{}%>AnS;L^T&_dgKVmUHHPMe~ zTB{*f(tEAJR=wQTxV>L%#T2)$bQ(sa7zEeBUNJg~MG0W4NBcfD+DGSjS%<-ewB|lK zLF!kmYIRw?f(pvHurGbX42G~LTQaON!atwlyHq;sbjaAGJ72V?twL$x^(Bc3oiW!s z+M2wtOj6y51RXM$)9g1=GC&jGD5ci%LYRSdeCA5G_v~8V0u}Mj@Dlh{$DC_dT7BKX ze@g_DmTep%%Tf}lGSSaF{4f|(^ocmV;*=jN6nj*SR1#TBSMTkaZ!*~uJ5x={v0;z> zt9K%Rgp5w#-6%5NhA(S+;ju_v{irpgC%IDyfajatMMAqVQoD;A^H!q1W6cS002osn zMqxtWeV-D?E#G?(Hz&!) z#^e3F^&&nbt5!5U42i>FFM{g%dc~<$?70GGqU17m+#)^P2_3z2g`ySA^T2SUwyzms zLx-oOQvB-NsuK^En)g{3t&rdi6lpgz$XY%4Ly3k{N_h!yO)GUTPdidUkfqV`hjUt_ zMgEB30~5eb)O64Md>*xu(>*B${GY_HNFC~%EdLDYGJdLMv$#Fg;p{4kgt6=I7?066gam%(2X1FZK@PRqjAn3mEATOuK7d+9*4)_xq(IOrBaxwW8gOLX_!nb5c zu;XnF7W=O~QA~zDl$ua!lg87lpBcBODh?NYWngjg5Q)X|J$~Qnl!B z_h2xRkSOU;JYvdm`?C&oID^-sd=GAkt^>f8J#n;_4+0r_sK?Y#HLyRHbSv%_-C8nKIk1~q)SR3>Ecabjw4*dU5ZTuFMCfunSx#%VQrdph}n zZs%}hqWOcPmvNh@;v$E0&@F=j^aNYFKG)HxwLrhmjjQh|e=6(LM1zosp$V; zl3_|hwV3r;rNBAhw3s0%;=G8a{pSiJdCb7_y?Z9Qljlo_=i9s^{=Ls4RQhi6lVmV4 z-8&WK-pMyVe{Vm0G~Dq94-DIWwtl+SdwguuE&S|hBH+JhFCby}32|z{Z)r*s6>3L# zzw;sea_0eOA+;Z1LRCag>fc1Mms-JiO3Rq4g*E4tYAz(|0;0jX*h-z?_Os25cZcyf zczHg*m+npbGwb?2(-n^@rDCtPb7 zm6(pxE^DS7D*m-PH=Z?o0)Tn!zaTC)>RzPiwuK}xikrp-rrMBE?D9^}gy(I~jz*SS z>Rk@Rvhr)1y)y<9ywZ%7UFhgu{vL3pH4G?Ov)C1*^pJZrW(O!53Qz&f+`9alpt<>_ zmjZkzG0^)Z{P>$iWVUJj46xYn4rR}gYrQq+lQALRp?XOfq8a9fkbv5wDexsLjL_$^}#SaU|{_cD82`1rBAqRKxY zXXf)``6nV&2c@|1X4LNG1IS?lL|Z$<I5JroHaQ64G(xl41kIkBQH|e^&Dw zPw0aL(izaDWE_=fHWEW2_OL^ngA9>%ne}QP2S|d11Z?c8N@51;gMRcET43Zc{EDoG z8fVR2LdA1cY0n5tD8pouqx%a5{(r21JPDU40-43k#hmPU1Pj_D<*ze_5M2`#!t&oE zP*MY1((N~Is!QL`SC)HIBoJEu4x9> zr3E_=CXOh6&?a@T4#jh(PXcZX9+}k`L^a`~!Lz9Hic{ zdLf>SIi_G0#IB4#?RgT)ml=8O4NcJ^730s54*|~P2-kTFvi>nzMHc!RBm6vUMg_&I zri$B6*S~ZME1x+cdH^}K6S_a+M-}uFp>zh2l8P)uCowU!QBZIY8L7_h;P4OZV+=dQ zZ7hY$f1;B$@;loc@P(+8Fx_}{dP^oeugpnN{3tDKTfo_4U_=OmT_@y^l>9qdG5$Ae zKe^L}sMe z7hL1KNd$(Fov(d={OMwPd_RO#`!KepOsVlf=l0b(MZ5bQzkcs=LKrmm{4lTm7!#rZ z_B(EFS6x5XYd3tm7W?Iy(vVO-&PpT;_FX*QRrdPG>(g&dZmpo4zoaY!)G;|U+Jwd! zAPsmDnBy2xA3XR`M1jslJd7pD^~Dn-myg^1{$VV#E?(q^EQLUay@WUGZsM!+e)4Oh znDOgToiQ&2+ea&mY?Raf!Z^>}+B$*06m7(eXeP72AF-H^hwJy(SR~p=iH&TA2ubph z8{TXTeDbj;uS7L_?7&=C3foA$qu|L}hg!``iipVHH(^R$mV|<5-GuT)!HID05$sgt zcU)sTKMLTJj-|p7;zCKnHUuA<`;YSJy#BjR0V}VFjS7^y%iab0EYQ-KJ!~bwL?B_Z zZ>#FSxE^1Bk}q9>+r1W(+95`Eo(}9TZZEI*d>@RUYDnL3jAN7MjYnIiE}eMPg(gGo z0+E(TMHzQLw7<>`pbh(*c^DBaWy;2pqw%&n?A?6z%zO=s>x)vZK9ev_V$E1`E1<>j;8aOyYiWny2zhd-6c_{Ye zt<=V9mSo=VN=oM7NbfetR>yb?uJZ#a5Y<_zO+VPkn>ijvsoYn#9hg0TfmNQ`8l_lX z7WVu49dmyjD^CI^K#Qgyv(~|b7=T#Gwz{{ zb_j&@&~^xI%@{(2X*y)M)ES-_pWR^KIBFLXbG2W@V$A^7d1L)QZ?co5c|SPkBc^IY z#)8d;)-G=s@%f?b2vEAtLh%foz~KyAM>r33W?jz9bv;Q zo5rcmQ4(hBd03l+)>>8cC_mI#a_&P0cQ9;3gdm%Lh$&ziW^IWR7{oSu3$o^<*FDzw zqc(x2#u?12R&UqQ5&BA+C7t}*iw&rzd_(R9umV+B7Kn;6Ucj%oC_3Y>M(gWWx+a~j z@~V$rr0m>RVaAAHF*m$o_>R64#R-VFl;eSswe`UGGwlKmz=-|9?RI!z%UUH7x{Z1V zy9g3x500@GE^}huMhN;bs1Uc;I4JVN$m!l8So6#j)+X|rbl|07w^AA25G9IMU~~?q zH+=3`2t)P>n!WV5 z3GJRw`D&}y+Tz$NSkF>KnEIz`d_*K<0|xka1|2AwfYJ6Mf(Ns>`=r~EVs~NE|!JyzIEkLf9_S?R$6uxT#d)HWeRW)iimnYURtV0rjFG6Fqt13nl`liO+ZJ+7lYR}PpfB1UFL+28Z9423-&+>RZcQ- zF<6|sQO351%V6)tzz;f3R+p!Xw{SJxFY5Y_t{LmIQA|`6E)TV;5r|nECj>kOl>@B& zLy!pW)(eAlI0})8kPZTl!t0kiYdk3S4oo^K6apju2$L{r<)6QB3Nt8zW^OQ#qeW&J z7?vulGYWM7y$&i-ynFWzgi>Q4HTo!o)*?uP8vdbhj3}v>ZaJ~JQ)3G)S=Nx%H~xzz zL98@*1dtr63-s(4qY94(G}VV0$J+ntPgmy!rooM+LWe#NUhW=ruF`VGU_Ay8q*@iT ze5sFm{F!tRA6%cwW*iN>iFvXR_|PT14dyG~&o0WL;E{CDSLms#vj{IWAw;)tp8}Jr zWn}h43K<>-IxCF)3yg3W8Z8mjE|K%E(}(B5u?rgLsv;HMIVs&rsb{k$R)?2)8%->?mMID8Th)j5>hlf!ejVVwxS`s%{V_eNkt0N?s?t>d`k3NH ztAQy=-&x;!2LQVE8zbx9UU|F$S$HsfOk3Br?-*SYfQMi)=qxI$aAqluK2A{urO?a1 zk5(bx)E_*si{;=qDH(Sv?T+#GRQZp|ET_tY!@ZVu9wtx5<_)evI%LIT@%qr)KfY0HLvZ_m@AD=zGh*1*8Hr9st3Czh z-0UL6J$4Asxe=X`spYf`6(Q~LQHC0D>-B>=O!v5#_tT~3+c*4{{M-Yhx9qFdi$5MY zE|!Cz`P<)DDIrJRWREHOzJn11nm)h7)>K!!p}>Vh)iF&9J$L9tEmT5m){K6Y#XRVi z!q^0FY}TLQ+B}6b&iC!Y4i$`x=E_8{k^=C-8I_J z`8!-DIL@DPF+l-nXQKGX4O2e^4)VptUxlp}2fbFOgE+G1O&|EJojS0ws z_X-*Zjw@kdbW&m4Dao;xw#M~^9hkCsN{sCqdlX=tJ+xao?qpuP@B^&ZY8t_6CuwVew-`6GJai1NrF}y>Bk>YyTx7OG&f{*IoDRVsd}1CILkq}&K8<3e^8%~1`)rZMWSa}@ zXa+8XcnxWnGlvfBh}bX<3H!$2j%)zMY^X|!<&tf zva+&uOfQp3=)$mcm%~BFT)Z{RAZ7P|@x(GLuju9uF5+@%Iy|(_%goalc|>VqI0t z6{-)K+mRqONJprqi8Qfc_36jOb(hvLnk0dZttaHFvPQNy>Mu@Zglu1(diMQXAC(wmoBR2GUPHUdvUbaGUkY4Vc)AsvjX)!J zF;46h76ll2z1gek{@3?IVC^h2IM6fO0N!#fDa>KOU99?jS2U?~ovbLvTW)Jzh-`T^4YyLJ}X#zxF4yZoWiZojRZ z!f=URX9=!85NMM7ohEp}>rsDH`3#$z!wE+k%}fhE5l9BGW_i%oC#J^FQLAbc2PD!z z_mSFxmu~wfqgSLB`&9%nEKjt&W$>X9IYTXO!mw)N^9TH?Il5;8F9kIL-P0$g%fr}} zE?xODg`@Z1TAlh;WoflS1bP6pz^Jx`{RR6Sk!hzz$pjy`heP_#X;lDnWUP@ju?9*a zT?*fXn@t|#)Z@^ytJn%{MS^8FarM=-=|pI!SSpuoW%h)A&6xYxoEwH6hg3OYB`zud zn8XIAq!~6xAPTrItEZuOcn&Pn(bA4n6n9qI`e4ttj+9iGLN-n2OEFKkp*3_4jvCWI z8WZ-SKweVtza7u#d_T&E%?g0?y^Fi>jLiV8T4{%w;Ak6h5}jTfiJ)@m3;~aLX$Q4x zj;dC!L#$B#z@Qx)Y^rlXyR!*Vznrm3WngeNH6_a?S@UYee_G(=K3R3aYLDPR?!kb2KN3?^0^MAecHp0^&>_93G^P*^spq zMxS*9kmA|6kmbOg;n)ZQ=A+?|eyvtjlGBK!#VsBM3N?haKPh`3LQshg9CkcF3elwVeqG^MZ586K9YZw&cT~|@d zj_t$fnhlifDRas(I{0OVH=>gRhD_p-?s2IQ!aOZNloNdBdwK`?K7)O2ikh}|3|PeD zU43L8pgOd}*dGWq1EsONT9c&)i;R|a1vl$Nfa+6hcEJ4(29g;806O53PqZo6z3kq= z%Kfhfda(fxbX;hXQH{$VaZ z_uMmu8W2nOs5wVCMkW~0VG;H)k&5yzr0E|D3X-US%4ocSZ?}NZSs9}jI`2&}qHiO| zeXnUYR-nb<^lgb*Nfr;Cql5Unl0O7454vqlYi1lksESKFVfHbDkg@XZKzX);keq|P zucW%~v{qN#gb+CER{m!fHKqs@hwnOhlsULJvfGp=3yAK}i=aul@QfNv^}~|zw@P+Q z#Onhy7>)F^HKdPEZK728eRqzepXkfzjNB*SjKKc<>rUt%Kl}>$sDZCvqrmsZ$a%g# zqwL#XnEJEN4Ce45I|8k2iPG-<4(W#8O--h@_Y{8#cU-P0k)hFV{Yqt#Y!{+J#5DG) zX-ptJX&*qgyC9-@W^YunnR}?(K6)Gim$)U>%CoWo){^p+X?g<>`)4IQ!8tN1#=Olf zR^{!|GQPoJ-5mh7aPtSm3I2B^;OU@{C5~Z(`QSzpK~5nOX*^i`|Q? zOPM-hFG5IMkRWvoR$}3p(;*vOkkBCmbP;^)rt2dhBTv%?Lc3+Dfe#qYKB}@ebXdIu z6PVCO);6sV|1uepvlNzM=IGHq2f`Bv{JA6O+=$UgPc z-!LHYU}+XMpc`ZMF3S+Ah}-cV@RDz|2|LSc-F(?`>Iw^*V*}37#u@(a_vKoH7`Wnq zfvZN1qti@(vOpNRcciKJntkb7Qixpw%)ArDw)w_d*u%LzD(MMIJq&~H$z&Vg0m?@M zJ0_RWx&D%MnDFYNt|@7SYw!Bv@hXTJQ^i5hjGH$%s|)EdJM`!Q1=CJpTAuow@+%(QH_lih04fB7{qaL$(8-T47{8zR;A&^8Wa9MO#&C-IqyN@$JLak zEZO*ECPJ27bQD2~`$^A2#Jq-Go{kV|s`C<_aO;AMXXX5IEr#KwHvLzw8|KpCbUCPb zc@5Bw4@JbeS%>PHAbDBM2v^pGe~R+gg4hKgA&s3NzoT7!8P>l7>OYp9HyD%k(oONG zD%w|wXgmjAmo&&5bGRTG6eNY1+uPX$3q#3dDiSue;#;$QI=qHTG5nJJ$%tx#9*bKN zvL+`~#sY%U(#6h)0u6fa>LB5+_~!fuSAq=~%RsfLpU*h7vd8tlPwR2$AGsgWEQ?@*ui-$KKcf*0I8EroDXE(>LEO-pSEHy0kc`LMC zNEi%E7}$a$6^|?W$^OvoQa4}@wTea8^u~h>c+3Mw`Figr&1bUS>Ovn{GP6keaPL35 zVh!Q9nYFV#>!mbVzR>9n^HY6c+fK{uQAl%@$rlXS9b|dx13I(kCJs(H6((`MkCLsT z!VGcm#^ncQKoqoxM)=~2{Bb5adzM9!aUB86 zmbxlWIC=wYCPQpGO3J!Q`|Pf}rW|_34N^4@ha~)Z=F|*HXD7@g8V*mM{`EYv#b})c z-U?Cv83Y0VmxQiZA(%shDFoN8IXJ3r=vU#rD;8!luMOSWm6Y1OxFE)8s_@4k>ZhnM z@2S#~tYD?rFRfI5X3ie0K$FqBxZR#lUwd;olMQpas`~CZj_U|T+=~dXC~V=U5ImDX z0U|u8iT7Sl&H3sS5dhaS78|HxaOu*k1KyJ|dkBsGV2YDrzZr+gE6SuhJlG?NgN*q> zfFu|w%#9n8W*EJ^&V-a?G?NU}uzMT8ZpNuVJgf`Xn#bJY63rcYIwXk^fL5Qnl@v5y zA8SjhqJPxU{yt^Dq`|xB!I?|NT^+^3ZFS!Hi}^$yHdDo#UxuvazY>FlK2|)yY->UU zdf*&@$IKb1hB#XY9!>gO1XSzXf9QU56o=zdi6W39nCfXQho8(Uw?rB};h#KKi0(0{cy!$dATS!Kfr|XO00@ zFqbqytOVxLs91Mj`F&p67|Nam+RJ%LETJ433UH8MNMYd1N9+qFsrU5Pi*8?0%Q-b} zd+9J>1b9O_C9#^o%By>#>c=>D1i(aRX(9HY-$S>qRo!QK;=RTXog6QVzw>0 zA-c_f`ih0x{r<)6Xc{^kNX|q;ktRN`bBl?LPna}5F1$@Kd&SC$_2Ml)=MHYM`egkO zd!M(VzkUK9-|@4k1G$*eP*_gB%@_#H>@i!0&ci^;R$ET;7lrt$w$u(4m9M;YFh`kO9BK`8tHn?$G3@S%eD93+#8TH&U6?FH7_dg^i30zxrs4(3ci* zc8{JXvGg75vpm8a)Iaw16!;Ai43G2ZI-NY6oZYRf&S0qTS>J0z3@2iqp=7AWGiH_bf~((z>es;3;tFay^#W>Dkqf`9h2BQ#F3`!5h53m<5;}ws$>pHcWQvc2z=R zPVrP2#PA9Mqm&t@S+U>Z_xr+HQ!GV?kcR4Br!e(wD*hOmYiU=wuKMSpA~P&vM+k1H zFzcXsdGt9}zy8To!xFlc2anYOVYnL+9^dxy>j@xXH03O#QPvno6>>3Q0H?SG*|BZL z1Mld&jGF5*?40L)ES=EKe*)AtsdY-c9oF}l@?853SqUa{u%0(pov%Tb%Ic)j1W3*C zE+=m-B*>bAR?%HmstZ~jygr+z3{|(ajxx(nKZ%jUU$Kl694e3p7G2p+)DL0J8YX#t z+(tp34h@hecP@RTL)g7O9C<>BqeE-RutI@iN?aSRj43W~gnTEg^k`5wl@jw&2gTwqndTk(%{Y;93I{UP5Kxfo!`G~RwA zM4_>h?P|84(1dWwnO=u#;JOZ%!?DTBcC!8s-cjHsGi5gHd5A~+foa0oi)X>IL^P>+ zJ`nm9xZ=z+++-b#1k?l$KY|)iHS~&% zeJfFHXE$SLpm8f7C`WuoY&Qvzc~qCj^CwEe2B=m5$3t~vBi~8uhE;yzf%s@1QOnbH z6ErqCi>Ocibo5C1k{FEp6aITmW#sbWF&1^hWe1pK6H(G*Bn7Pg`$(=-dy4ur*uQ7z zH5*d|%Ym|QN#g>48G00tE^ObDDYYxcRHlol49e(6KBPP{9J#ibO|dM;Uq8TC^nbc! zJSapQ&89@jqKB8$qr}YV4VzB}Rznw&xllcXZe;Qu|JtaZMW-8bKk5xtB+TtWcVMv< zI0Q3uBm0VyQ+@t+x@FKxPI2P!Cgz1MN}G9&%zS8&cMm;B<=%8sBRc5Uh=uBPIP{%1 zPp$=Ju?ad7j8+{}%*&N3huM0&Qef^+o-&OMQXvkdjPC_U5=oI(a65C?^y1JmSDt~K zgsbScZp3qC?tSwXhqA@$7sc}>jOgtzo|*0t;1+k>N9;Nag565h1ZhA5zOjq15dlW9 zjk|5jZT{VaquN^y?+uK>uD5kiFnsCB{$=nWH6&ix@1!`5rKAfsv4qk3Z1_H}_hwb+ zJbuQ?DtSCQVh!Zw`JMkXzKJm5=o0&l%8kCDvwxC=vuD9Y=4pb%k9-C_@?7fdnr2(H z53#DEdifKm1kDRXQ^;!m&1~ylb1WWLLo&FW&QT^>P4UZdgmyioKuVk5j7&_hwG(?* zyxdMsYxbG@Q7y@&;>$Q{@h1poj6JfV?y%E$%?^R7q5NC&d4K|M8*5s$CDn)3E7j1u zzkTJ(zdRQKuSSz=A0TSq?M!N^hQhucBsbF@_YRpxNhy4 zRCV$GzCJv4e|&zt-P#+g7`V~Haa(IvY@zUfr0CS0waq5o=mgDeWCk(utZ(`$Zq7-$ z$<yY(`|JL?&`(I$3qO<7WCxao_6kFYEJV8$pS%D_lH{4Y4LS2U>+s|j)X z;6aWpap;Db*9fc641y(-Sy&qsILPNo;KNjV9>AVVn`m|q8t7yOuye>etsmN z(Z$GZsDK-4{Cxpr^6s10o*^4UA#0i??n;Ikt8`Cm>Y*;z))50k1&~m&*H&3B?A4k( z=PC53#)@i`>O+%}TJN$}j4^71AK)Rj+3Rvu?v<{HmY=YodY?@VsE#pK*mJ|g&1<77 zn&;UAAr^XjB$$0YDIwe=kanT0QJO(5DjWi9k@l~K_KJ0UT4e9U2W*oDD5PHOu}w5t zkL0ugzz?;L4V(XcFvO8#;#32d<%P+B}6L8jQN#2 z=y|iM5cBIf#hTBvgoqed2tOa+nsq+Iyaq6R(_WNsNLi8Y{IIRd!@z)oyU_N7!*V6U z9D7aypP~RzIWyz1da)1^l&HuNFX-2;b6iv^uOkez9rzzl`dxAu{fdYJIylbw`tw*?ExxkSLudPlHY4^6_n`tbREctfxqs&_BCf!}e(7$+#(D`DrdUV3ITpfg>Zm;>e6m@m|NThI}5k>A~G zj`(#pP`R_k8`0T8w~w;{QfpCV!LG4qEW#I%?g>qaK`P1CB6lc+wTR@=T7$EsAlFbf zR{Om=Ae-?KeEcUWk8r3sP1JyY)YN14&O#P$9TN}*f1m05 z>;3J}-Vwq5Vyyy|No2PWlC-F6L6RDxD6rn}y}N%W0!C5i0WztksiR3`w5-=o21uY% z$`#e0fJz7wYKjH+@9_O!Nol?Ar0oRu-<{+MPgx{~3_7WaN%70oa@tBz&TN6AzjRT2 z%XqovZ( zzYtEbrk$NQfvtqgONl()OysSnM@wwK2NmQp?+ZX~8E(WUb|%NI8U6MOTyby!ebLqd z^TLYM%0(gr--sf0BwfaQo2Gt%l~zd46luFSNW>@An|- zw}xrm{()OMO#ExC)F(x!7`-X{si7Xhs*<~=%`-v( zR~_5O6tkcIcNpkf$GoW*6#xK7>HpLC+dJCX*gMmD7}!`}{Yu`k*&M&8_53R<2o;(% zyVjO*jNT&h#WU)}C5e-`lB96cfqy z;yH^FnK%M1m@&8HdUE@Wi!U11C(+WXrB~@8wtq}bfgs&_WQBbki(d#Q3 zYdDmhrb%cE$AzoNb39+u@_NsI3(^LZ}4v`U;T5RaiV-aW?)mzGnHj@Bd-G`|qXIeW62 z5BhP|rPlKiQ!bP-UTkA$l}ih*GR(R8UWtldfp`rUMUp^(QsR zP~98drpJ2X0laDCej1+PCxXu^hPl%$619ie(buzxX<*E>VExjE-&Fwr$(CZQHhOpRsM*HqO}gOir>^9_~ZZ zKcc((tE#;(vr&$7+9YTv4}n?}7K_etU+Tgd!h8g||I8 zJ7+5heuj#b>xGxoO)$$yR5|COr>0AkkzuA{mo2HXvaQWQRhe^z29DE>44vO8TU3F* z^MrrbMF^%kTdIe4e%E=`QuQiFs+UWaGI`05zv>wMq*WIX`}cl7_JqCIDWf_0bnY)83Iszd%pdAGgCF$v0nC=-#5*7KrW{YMl z)ZD(}+pfps)%s99kf4#9fSMSn`@+QXosidtz<$Ppv>sh?$dv1Ym_livEqNB*RdYN*{>p$N192c%l*y z%sJXP-;qX~w;nrB`&c=N4r@l*HVboy=T6OphUMXuHY4Nwjez|;jHX}qGeN}WjrY(@ z(ZF)hHzH6ToR={zFPRoVo6ckHd>K7xS5#Hnzcc~@nr+RfCe@JQE|A@OXB8tflQl5m z9F%}naf~xqG%hS2PXqj_9iN~ZKOye(-T{G&ks?MU4IVCHL2nhv+31X2)$7bhu4o!N zRY2iMY5-X7iGICGylEHL1@cz+@OZf{LI{}tmbCd{a0dWsz*Uo_xFONgZ+cN%fwaPl z^U0Yn?wdEYe5ia~30eH_SKENDAknH(&8+K7_ z0GycHaLh$rqQ1-h#2ZYkIy60jWE8a((Vo^fKi$CA4L3!!8t+}bRcXB07IwHA1{y^% zk-4QWvTN%S&D);J966Hw8hNqZwCkld4t-<5DJcKS@LP{1VtxfO_iD z8F&*&NZCr-ru+n=d>R}^{b_U%60fUW+5==?;oSm_)p+`TC|kl{kPW4hNmc^hNaA5v zu7?l@!$k-SESnKHcEho3Z@yT*&cjHpE? zoMuC|3wvT&^0aW-Eimp6n_)SmuYov{lOqXg*)Wvu-8F?Xq5-s| z21+Dk%W@5gNci|~dDyWyfQnmN%vUGzdk#ULUS(JM-7VSG^GVv&jEFd=SfxdL*zV%k z=-3zfZOMxfGAs%gO0Yckz@+a6swQ+sqS&h868RM$`I2!tvTp8EW#ajw zd5*G?oHGSmwrveMWF+`%sW7HtF>vUi*+x7)HQD;T@AD!;Ip;b>2`ng_&N9`aA)xsRs2vA{d1rf*}ohxnYLDI{6#i$6P_b z@gYvINKIj9N)#X`P{_h80BRXbJdNmD{%|t6IHjPC`v(_@iI7lnQ5}RvzpWf9FC!$- z3lI=v0TzHBbOJ;L@vC~dyOu5hU)nH*_@KW!+kUFNw@zYY5<0TG*vqblGB?3cY6qO9 zU7eL?k*C0eqkAjR13(r&$|=fY&Qw)p9ELc-qo2_$G7a7(SfjxvQdkT=AyM$8tQi#a z2fd`ou-`3*8l0}i1aUQ`Rz-NDwMt{CbSQd#lS~(|m6{0oRKw zhMt%UJV99!3$Qhjc1gZj8vAFPuwZexFzKZNrDlvqgM-sVqtS(efHieacxb!;lMq_w z4-C1g^wmnqFr4}V+T-+zxy(%Vt+y)Kom)HUFTbE8RpluBoszJ@K>Ue_1JpwWwcuyWRteGm_qV{c)Gi;89a&lA~$PLSI@ z4o&WU$o^%4jLOPF3cxv~P*)~&Cz3G);%8hX)}Lgo4yAC4Pd~4IHN08oAgT^<&vKb_ zJ~%>)Sc$b)R*8&Ei8J|A*h5919HwxFy1VFfo$`HyJTc{ubHPz?%@h+9XDayi)nI`1 zT%&~3A30K5&{<}Q-izget}xd7pz#iT%H~r6KP2}P#2_?u0##;NVt!i~XX(6(r^&(x z+IA?_B%Tzp?pg@NPiRqDC#SNlWQe)u6i$jC88cbG*|Y&jx5L9~9oFa+`s!L>xYM9-kYJxAJQbkNjw#rIvkoxA-ro-29%h%Y#($ ztYi-Uj}_pAaGvyXJTCyJApi;UuTOn0xPf3K;8pO$#Hp#Z{rtIpPoaQ;QBk%&(<*fQ ze@sRd_pY8GrD$76%mQUPiK2(XTnPGvu~uHR#1(m~_XB zY`VFBlyKQGRO*6+=AJ8fQ3^h`T_*K)9q==#6$ni9Y1+922(-2gELHCOk1IEov`XRc zRrL0{*8X^x8-`TdeIDbt?d1)Rw4knvzuqY7#~(W@;wxo0(nxVWpG55(ENtxlOewdU znv?CW5kMd2rZyY9ki%)=6u0(`VJ|M=+2Dq=n+=PSN+Jy^}d} z$r2<;N*`G!yM7Arq}ZvI$erLTXSETGma9;=6=iHc4Vv;!9Pdu5>e7+K;)+xy68;*S zXx@yOnTeyB=DOdGZe)Eyzbfn4;o=ZHU zmUF_fSaf(mSCGs534oJcRw{@yx5M`?k{eSy45J1qLmcje{a_3bo`<*rW*0Ocv`8E} zurZ1v35f-oN0I_=!Xi4Ni$TN|zZavj_=s?%`!J*MZ$^ZWsDy7aNBPAE5mzPf& z!`wtGxLrh+9}bF+Y90WcH-uYVk`v+8OpjLkd-&gXu;1-%V`Y7;viEQRZ2jAk949;+&zkc_5LtDJA;I zk}~{Ro)=i}zWjkQ^?cJz|FldWtvRAFxdP03A3Cwu$UR+RXbB^&`8ea(wpZ|M#6Q4$ z{6|zH*A(`FICsM16}`U*^R8qCL39lR=loH*-lcB z&}Ib!dpe6a1H&)(x@nqNg>d_=5Z-RzvN@{*#?pT&V6)ggRMGeqi{8rxhpAs;g1N99 zuN2ToiUyP*t4+W^=#m(6z7iq z*jKpZO3O8^bUS-F_k%OgB#BxR zALLx?nZm7IEzCQt3ay(N2UWDJ3w%%dQ*}nbZLUtRFt4fO7FYPp<#&)6!42y0g?$iq zlj!Xn{RzRN)3k*4$Q$XN1xd;?t^{^So3~LUMVI*nRMhux({aCgg!FRl`|k7kbibc^ z#!nB3^@hztuyOA4l{m=*hU27-Ivor9?EA}s>6FgFf02L|$C=$%=%apxyn{Kq-Fn+{ zsa;=haJxAoZ zB+ob+Vp~?{u&V|uw>6_Rb}Vn%iUfd}ZQ>KT*&@7O5H#3J@0BMUDaD~RkV|FIqS9U_y z<3~2z)n48~ODS$ySkuv!4jRD1hVu4j@}!-8#|uUJWZ|3Kg(rZ7yHHO&S@wqeON%_4 zyx8@7P1Qog^k7m_K5RxZU2j@g-}a1+Gu&vaR?f_?v$QW_xW=e{_cIImGoOXdQ3M-pZN5|v6W^Hcie zmh@Hi1-w37cM$VOv#+arr5#EDcT=q=)c2?@FXb=n`p)H5SN5$}5 z$@^5e`UUs<3mw~ty2<-T7B&Emhr16#vJf*)fJ=7)Ka?W~nBUFK7XEzXu zI@k#kZ`(ApUu*dcbdlWqJspnMdulpu-K(f)71dO&3!`zxsasZxBYUOaRpxyxanCY! z1(H=Zv`Fo2*^1_o%4-vdfuzn|^Q|~;a1CB$*SHU{j@BcH@N0RhaKJisO^+1Y=elX< z8#XI4XPY?e!)P}jV_f;$E2rc)>j6g9Eyv~xZrQvxZ4tjvkxz*2&Si1+JU}6K0raM6 za5ArC_lc?24DgAlo~pg^#E=VVozCTJ_>MJ_{@;Gdeq0ZZu68$75pDAEGHW>vwY%=x zoaV>i-hv^11)bP-H}L9p*LxjJo~XSGF4sGGTRJVixA+Mi5A&>xd3+VI>ECz*Ruf}r zi=-HroZy{R=mLWyt=0j@&e$uz@obg9kJs|Q1UVl5E|2nW<=UP7FI!)N)pPzJm&|Ft zc97q9X$l|}>ffyCKeTJqzj#s+rnRD*bN=#X?sxUdJjz)NSzxaHZy+EHuQbU?O zg{T*|l@*2gGL!HvRm(-?3Lr6!Lj`!C$bMl_*hHmQ|9#d)g*UT+BAdyiF>ngyp|#jZ zW+sX>(lxP8p{4MaPV!udBM6%OQAwh@$ig-mm+24I7OTY%!x(+`SdOorFUQ8l7|deA zL|eRa@1>qEU(LtLPu0tdH#!(q@cx6bq6{T$M}RZH*zNoWPD)sRJqwb3nw^|6WlM_;Yk}b!7p?=Fist zdfcC9eYv=Jdb;@ex_ODx(HR=e$jnsKN~qlGdzR#v*G1dqXNrEV^zE!&0BaP~pS| z5n$*Znbby?fEv<9;gpw~tuz{eejAMSH8*c2gdF0ayGA_;t-68WpsNO}s`n3AXrfv# z+DQEsp}|i*JrP^h%~LE0!$3Y?W-{ z3i+cKoTkkiIKn=!G$ZWNHh@T@e8&sTB?5g>y%|MWMB+JX1(-c{WhuWzqdf&iy(x0% zJlP8Ydtv#Uuzm2mpc_**mHq4S5I*I>3RKL2cEtdzABc3vZ$h`i5rVhd)aZzEe4iz@ z0D}GF5>IC@OI<`|3&q@~QP zDJ}s0WXg=@Z{DxGv&~|gxqy9Vwhxef@!}=~=O4dkW|Ov1dw6B?n<#aZ*EXxun)181NVv^wc=m z)ykn?iRO&-m2>1T_{Ls;(m>?Z1oK0)o{?xivG8*G7101wo#Jqu|P)Jqk+JCMpgXYC!RObAZTk2n~2nu%sXl$oa zyj1lLkjfPGp62)9c9FrT(oP)(Gwh<@KyJ}vTv)Eb*YLiipIm9u!K!`_*p9C$kF|9Cjog| zp)1|yV5PPKFb(1LqWEuDN!wsr&$8r2ok1-CR+Va_av5k6?e5_FY%d$Pu>TZ)Ttfy%NjUHHTAe^OkWo)oeMNd*>AKE zo9u3hRv8vol3f<=LSWLQ-JV^)P%smrnCLxtQjq0FC>$t*GGjVe?nUo@TnxWbD+vLbIw&$p*|;GuJ$R4wy_6Nz}ThQ_mx;{oF$i-j)RFg>+CR4 z6vdR94QgdTLNO6(_RC<)Bt7n&P`VtzY`}t&^{KH4aI(*E~Re(6{`6vkIdbrZTd9q1TF;gN+y3 z;-^QnZu}BRSWeLoq^2h@l*2ds)o|}$YUof*NsQ~3vE!mo>u9jf9OL?7q~DT0wa_7D z{RwF!FHi00K@kvQ30|!yp<-o61T$oh1t{Au^yojrVK;=vjLuMb1J+aj8}ub^G6^;A zI#7pDidG}oW)RcTVAcX-inE-JwH`E2m3c%Hc^HXoKQ`do756CTs%N#DMy-X-mZZ!@ zo<%$tvE1a(;3NF&l+boR>|sWZOVs3%#Yt=?g;XSzawMq{j1Tf63`Fe3-QhVE1o0yh z{sjH{QRx;Ar@d@ELoRt{&TN{FPw>ZrfgR~I`<_VV5+nu1hpKqU=Y-zPICn!%#!?Cg za0G*Y)N(1&9;5tS_&(Drkq5j{mElON0rh>#>IX-~c9ww311&%gABrhFh^}?h4Ka#E zA1$Hrr$8pCh7QHOzSALgoo&Bg{91~Ykswv;xNT7I4|tk;C56$0N{EP#wG=&DD+qbc z3PawCqZu*(pz8#8s7&Z57OH};gKYjBl1-*vG^#6Ly)pcXy`xQzFGQiPx@Gk+qlh%x zM|v|-auog{i?D<})k!oxQBH#^dYjd9aGnbJOk&2J6wl63-D+Z6>Hdh{3zOf<)?S{R zTfsG4{AIHVHZlXAdBAj!*G}a)I;an|f>qxkrGX;QNPE~OEO8YdpIUPab+2OTNq@*@ znUT(;NWN|LU3o%H6(e{rUVpT!$D)xF4xzZo#!`(7;~;A;EtBd1(hcx7Gv#FFIyEBn#E)r-&XXCYDFt;hGV!Ulxlz`4CcOc#yeIpDLuoot zHht)@h6KITU)+b2Scw~03BVQ3-Q1=J%rMrv9H{J&${Q#>&{#AXbq~j@q({S(9w#$A zfkn|!H$hOb8s3JC3TQ^WkzkLap-3eU_menPzyTl}L}RHIgeB)$_eLEKy|`UN%FT@^ zTAj4B)lTAHRs*{Zk^~rXpU8-9GB}lApp4dGQ&0-q+Z)Qwe?TIx25O3mY-^EWmBId= zn$1RZHbmXcZVkm0r331txyA<0OkJ$iB|W9>@A}-^6G%oqV`Q9Q<7SI0C=-6QoA^0+ z{js@FOo>JV39Cy#o2?R!(AP-&o3Fgh#*8L^*TTOZbbT`3$`{h5N;IXd;s!-89|oa- z0MOE?9yJS#sLepMG9B5HnY3+w(}Pm_?PoUhrbL)*y5^Bb$)OCC^$)m<4rq{a;(9p3 zImQ5tPGUXKp+7bAylW6pml4h#uT#N6ToJ_HDBAIxOhlA*%NtpqIm=knt#EHL&(-2NW@I9`9;12=pUgU4VFm3CZl+o%VY@`L{K2 z5NO;l6yFop1+u5j(`ric=dSQpV+r28)|Z?9>yPopQf{jpM|`;LpBZliTmw37TNjUM zUw#yWc#wr02S$@?tm^BeIv4QUPT^Cyz?cDnELd<<7|7!kGM3D%TY4@i1mCV_?LG|h zMO>G>t0#HiFHq#wGU55F+Q)fE&RXz`2U?xQU&O8MHv0X|L8?w&sq%I1v2VLYX$8*d zyjq@{r9&SKGmJ1!8_W#74ar0Rn@&yiIiz{0Ua$~p9=L42q4v`9WPl7^psV7xLKL(E} zDQ^PdUoMWhww&;7`k@atjtM*m{?%3YZY0+9+`yL+-0dy#nff`XFYo{_Nr1_ezjgkj zE`Q*syPBWFqPYT$W%;-_O5{&UlyQ#<`ieL$u*Fo2=UVCCpnk(4yNT{|lJ*kXw$^T1 z>pao)!W3%-o$6A8m#-M=yQc@6|5Tp|Yc;oMyjzB^{Ywz*kE+(XCz<+DUy5Z`*(hKJ zc4QsJjRJnzcKHDct~ZU(F>78>Vz}Bzs1vchbfy}HU%hYzvcmRKN^(X*|@6VF$$v5Fw@P+xLGgA_FXp| zts}7}#2(HEEl_F{h!L3FmA@)kY?wsOn$a?qlt zEVD1;Ms$HdbVnwTp|u4K?Vy})UH&d_|oNg*`Z14AzRsnP#x|8cxQ!#Gd+ zfx;`Xw9ZHhnccZuKUgV0I$Elt4&B5o1e7Np@vvcg$msjMn&ksLBAqA0rYnk0>mdN* zpvnH!8$Poxp5h1f167K)+AN4Z+Z}%gdzy^PBsYqp^acfMH~%16Rk>P}iLDDLNQG9c zd74eRZfl3e;>v!4^q%%P`o%v5 z0;2MG2wbVp06)R&y(~tTIu`$kRfz3>u=N$DMkF(=oSD5pM>)ZX?erk9T@#U`yr$e3 zR~%A{veKy)PGM#7!Np-UG-EzxI9WGqwJAD_$zYv%9-=Ho)MQJizryr?&DFFes+I@@w}b-%UHiK|xzuHW`!v}zKrv4#_!iTn zTwRp#gu;nIB_-7ykoES#Bq6s5T6>)#U+PHYcHPYhGT9FlKkg&9`(iJ>r8VeYQhsPd z;FeipFoysTV|nsgg332b936NAOR)OUWAIhmf}7flH_E2js4nq)HUC^Spguvf0;;EC z7SmsO;Fr;GV*LhDtN!&#F#a>T^Xz(x=P-xlgWula{(LjpL2PY0!c(V&k-6-ojH*dx zt=$NZl!~9oJY#QHzP?1s?M;RpoYG^SfG1F)Z)qZal<*xBvEayIC^61HFxLF*|Ahlr!GES@PRxHmVH{GC)WFJx`POY`bm>m6L=gCDa65*VKu=ny@!I2H( zI1n+h>L_cRYqhXaOu6G9@I~409|0o{ej>i8ZK==AtR-{0v`uC3Xg<{@p<+*LlI;gzFMlM*RwVz)T5OO6dbN-IB?8Cg-_u3R2}NVf}` z{?0cR>O$tn2o2eUuXc;Dwe9RprZo(AZS57#g7$0&|Ejm{y~$JGf5w|3pdCGl2mk;v zLI427|7X1EWNKz(YW%;cCXTiJ5!*wTKWHI;Yv zX6s5SscJk&2g~obSy+jPQsdE8u{w@b0k$?t_~Qvr#Z7&5Tb=(lZ=8{1hS{qpi+)P_ zVJ(lrv;0Z8?~BvzvHr$+*Zt*v1a4k#A1^n)Zrq&QM*jNQ+duz2o-Up)hz-hE%qj{E zdg!?Q2+P^^IIUcf0!ipPuDqy{Sd+wGaGR8S2SPV}Fz+7RjPf!W%~ToD*~w4^=BM$1 z9%N6%QpjM^=`qMWtcQv|<)BfjUk=jS=LNSi1GML1X=Rx>0oCfcv4hhwP?X`VIKv$H#^9buK~SUG>Ay*PP&_zVEH0k%WBDMWt(CLuJc11Lj625C|uQ8FO}X9yS4 z$EZ2b?D94u`2d`>NFPj8igBR%Y@ielEiqF1bIIt6f>TeuTO`vCnR3)SDjDPyIl(3; z#%R$9Yt5WlL-E+b_cK^x+Ua1u+wM$I2M%h{c8MZ=s=z3s_nv7dfyA^QrF#x4>y9~5 zBBwYQP&gCHMaV(m!0_9^yu4u|w6+peD_J@LC?J`DdeCekCDLXdQLJ)EN9P?lKm{6> zfdFC%wUqI8kiXC>XgHw(+kZLr4-puX1MjNps}%+U@=U=-BVYhYpfn&x+0TxF#L84L z3U@KDf@`YUX#%vv>ltJXoE+!3;5CN2g#~&@woDDDp;G|pH8UUYdQ!xcXzc}j1kwOG zs1i9SZyDqbF&X&!!YL~bgoyN-*wCY}>j}TB; zRWJyw?L;8oSeN}v-IpAo7co(rj~rcJ+bS2F6~KiDaDD3(MXDe2I2~6+a0&bX~QSGKL9M0Rj27!qF$2I5Ke$5qm}B zX)qT7;~B=ynbiXq;i!4SS>aAo86hbz(V=(jFSS`>;qOG&Q!VM#Qrt3|q-3#5-FQE6 zaW2 znk{rIO_m$vEYjRHhqH8=S>n(?2X#=8-#H8Dqik5K?a0~R^-aKvD^4BX>iPT5p7!8P zqS}~LfZ_nOBYU05lR>@>O_b~{S+TOPKYsS;*;AQaUd`Pyz99sI7Z?z*GrU`(q^T3E z0$%h#0}zFDMK)Vu#3UiLzFGB?gl}&!xIy(cubFq%Iotr+0!V#0y2S~Iy>}<<1Z97Ur4d@_$0WgcY=NZD$@j5nI0{D3}68t@1p7KfcYekAnvC^293 zOspLG1NK7X6P)DNgiyx9f2aI;Q}|Z|fwXOZ+Gm2D_OS44sV(iMzL=W_LfRx@{62zA zNwR}g#~QSV@)RIToj#J-dyP#f%4wb~zw9NF1hj@%buj=bA>k5|3V^SOze3gEdDhEP z8E`lgqx7BmHb>))fb*Sn%irhTp#DBlh&22!=mG)9+Cy&{ zh6EtBSZpn2t!^VAP`%LJ-+GTW;NV`@{^|+Civ+s40`WwIemIIbbNz5qrFSDs!yJVd!FgM7r`4+%6?G-&Fu9U(KWwh{T| zcmnf(zF4Y|EEfRyYdNy)s$Q0M9kRkuvm^@_?d;T@a2OkslW&38(o74%LMW2>(-EK} z7B?D0mRmGR?oou28~LpNG4oO+H|V%1NmM+mEma}hQW7Xe+a$27CmN1#qOYm6riGQY(d4moW_kY&oXu zSRny&``u9tn4>2&0h$na;L%~JCedb7uvUoFh*=+?5PhOzR#;QQNY*$q_n2n+#EhYE z3WQo(XO~1f56IjEoz3`gVXvC`()Sw*6R9didYSOQP{6<7`ropVmmC46#gl;}_$k2a zIOtN9lHm(CX>-O@*6sR>=FZ$Dhn}&=p0w3-BykfGov@f12?bEySSkhqkJnKv;x*n*{ff;K*7M6s5ht!2Z`qeqe5+VE5?BWg^y>HY^)YM+ z^vAUf1`gw-3A_Q(;pjQ^%5MbOdlAVwmx>?h;z>;5rqsSejs1gFC!WAgplspkHFkF&rg( z0tuCfIp5Zo4mE|PiOazbsONGH3}qV4T*;$fUd~>10IRg5q6tdKSnp2I6J7eA=s{ZLd01Dd+@s=%Mi z)^h7NSwqM2cyCgCccXCN3Q3=>33Vc4@3Mn1XzX?-J=z2OMHKhsBOc0f`K&|5R@nqV zxDKhY%^}qUZDS{a*P&vMMIp&&Y!GdNU3cuRWOl#!EN>qkQcV?#@>A^ zMgHj9s<`miiEd*7fs;K*T>BSQYn$t^X;2@avwaRaS*#ITk)F}@P};E0tzJ}?u~SrL zo2u02N8pdVYgJ+Wz_ZmRgBiZp%s}JB!RW5qjW!zY&cF5kt5=zXKinThQY8vpVb)I2 zD{`_mHw2FZFhi?q<^7q>Hof7W)1Q~N@=8A`OGQc~EBd9%J5k|wgaCSBT4d#5OxAC@ zUdnMIsO-4mx@(==zFQB95JBpzUTVN!3g6btbU+p0_4Fj|hOp*#ww=*7VI&_d@CN>H zDuN)4P@G{k0Jeh>*Tn90dK?u(14fXU4r}vaAy;FTAeSEhVJB+brPN4-N2w5-O!H#wE7QPa~x2Z86B9|FgF!6k!@g7puf(m&c<- zrj`(m$60evzmRobhw{QK^UXb6q&SXdDP8nh{hEQLpKcTsWq@2go>H0M%((K!u%W^ za$!QIBuul*_Te@c>WSlQ6&7oIWh41iZ1kkA<$_T4Z;nC0du3R&G`&pf#Jl#PJqy_Vp*roPCDEUTKx;gJU`d z{oVdLSS|A6lr+HV;_x7b;~GD#gt{Y}yUz<NjA{oma!hJ-`lVos4)T4fVggKIqI zTG?BP*QR@e^vTv?9;3%DDBGjYD_nY( zmvwuc_3C1_mV5V{8wo7?_yb%T+<{Flg2YNg5HX`+=26HaFlZGYcr!fELgRK;pgS_$(c#F07#rvocb7 zU2_ZN(-f>u`HuK1wkrdcHdh0d`FBf^x(gIljKQ~Rs~grpHaCg$RO+8tR+#h|JIr7Q zy}-c-?S<`BmfBW0m(^y~#gchHJyR3w*B#IFRlt@SSp2es8K zBF&V-J57CKWpoSE2>`jSW+rDha2SpPqM9B&(k%{Z1Y)?e1AdzGtmIt zA=PLm0szQs1OTA^e-aI62YWkbQ+;z&J5wh^7kekVfBW{lmhM~QiF?n~UgzxAPC^Tf zV|gYS4A@#lHg+zwlAcVL_re5?l7=8aFaT1k93QvZs=fu-7Iz6Y=4|pF_M=4ey8n)< zt~kE@`rXyOeOI{P`0LS#Ez&ey6XI`1OXknS#Sxfy!|Nd`x91xFnfu1i`Tmv5Jnpl+ z{JMYR&4J->#UAc+80{*ou$Ze-+Fc6yn$O~Mn2+T zx@M~dzqgyS^U?$4f?CdWUTBfR$5ln&9yjKT%kOM)Lb*hiKTR-5r>Eo87wA6I3q8Ql ze4nA?;Fsp$fi@a(ExTj_=tA|xqDR)w8zeYIGQmvJzEP%Th*E~2{|t}-6~9r-&A)e> z^`(>h3O*a)Xzb_g`usiOdmR_1lf2l^eq0Wmp`IMv7f0>Qb-a_DzoG9%9xf{eII{rr z$1#!MC%`0tBqeyNBeb6d4Wbm9FU$~6xUXnqG2f9by3P|x(0x6K{E=WJPzJCEQUEg8 zGvOVxhtjpOrAfJJMT{fu(W4PWEA|LNoV-*iV3}yN%$(;iG9M5KDpK$jmY7dwea4ib zS4GsLkW2yFA9#x73ST;Jn@H;p^K87%^uUEP{}ZwyXpZdOgA-iL?wZHmk|9s5VArCF zy^)flW=ped-8XdO4e4Wk({sT{U>!837Cp=vej~UXGl&DZj&4oF$vzSyz2<*^ZD|K(Kq^@wH=&AEWOo88~aS3#Av~QO9O;|;P zE;y1Z&cAON7hncFN;ySHR*>l+_9&!HC0<$eQJ@#06uk`@-wbIdc+GDz{s=NW4IUUz zF6(L&J{a;@QY{1g;F-xbvLoP4a+S9Xp7*(zQa?xhrv*QxBJa5k7mx->4$g=E-n_;X zEyU6qPr$v$gOD`=L}$&)YnZ4hJ%z>|2T_-V?`Tx7Xy5C3D?I3M)_bLYO9< zF>XEm%fEuqr(TTOzA{<8H5wzJAmLuxy&?Rt4c%j6Dmsk{rqSCr=8f#*J9$j!pSc|6jVIz+G-M#&LVo&qgD%n^2EO-T{^1^(+i-=LH~!N-^)iDg(N z%RgRX*(tesurki$&1LaF8G5kQ6Y{FBpz(LDgn}CX(^9qfsyOGpU>vC9Yy5}8$`(Ht z?#ncw@B0uu7<9}K#=B!R;$jTN6;8CM9wpj&evDUe@=YT973}Uan4UcYKyclV9wZuP ze1AKz0{{nWzl2{HmBEA;!`p4=H}Mtm%2%yGhA7UdOqfBx7qGEh)jV#FR+y8VHUKhF zoiKmdfDL6#dhUM#jX-k0;ra1#ckq*Uc6xN=5zg-jXS7U7ps>k)l%cW+b$vz24tYM8Wp;j)!lw6xB$7Fw(K0 z>URZkTI>vFEtt*0;vZibAFwT9qs(CfgV6;e6{dz7sOg>K{z?DTJ3Z@x=R~AGi6bNl zs><^G0J;-;0zOK;p;gwA|LZM7of?qdB7J;}wNCYg+Z3s~IJep;fvhVq)g`a_1iaJT{{m<#o z!w=t9_5Dcs!!o9z@Uuo~f_~N*JPPQ@SH^E2TSa}2IIKhx#s)M(Ji%zr*T(V)+Ge8d zagMGnkw9cG0?QdgOr<6COAgdm#8!ABmZ7K%5)B)k0D82*TjZirwQ3ubMzFi5la(=hh457oH7|IoDC zdW{w?nmZTqM-5E1tp?4}o`(!G0mC(-pqp$Sy0uN?$NtA)zMQihPDwjh^9*5wekf_? zHq;wu?}5de7%^DU@Ta?C!&mIBCb|yiezYKe`TVbK$EMd zR)rjT@@*BkdS46Xpt5UCR?GOujQ&NZ1^*ad`U$=9my;l_tzcbM*EV*b9f}JUwX1;l{BNNN48OsM^M=sY6-Ter~1fi=Sk=y zJAGs4gQHy7C=i>Xqm)y4nS_;+Ws0uI-S`Ah4jFMelV&%Dx4wVLc!VK?iTs94(qc6Y zvNl{UFpQXr;P&~TpDL?H3<$k%Q{`vBEcon?0&Me7;@@5T{AahMzu5cX_s?Iw02T+| z{>2_Ueoa9ljLJ~YLqD0h(KI7d!q+y$8LFemUV`}`L^oS4xm2KnpN1lrB@;oSDd5g) zE@BXZW_bY5qTEQNAfZv{bRg4K;IT*DKFJQcJs^W3bjvPaep3lde>w$GGXsMLSfIX4 z#}2x0p5kbrdIYY=P~IX~@GyvYiybb(HRtqomdG3m!pE4kh#@HKS|zUw^T7Lp1)c6Y zd|u6>1qDmG@k2?wxpgMcIg@%*sY@}?8D*!}Gd@8uP=prD>E*lUd%u6tbWAHQNnJq& zA+6hxfw;v3@m%fDn|>DYDz0VlbKJI5Y=+J??>!HrV&*t z0=aiLj>22j(X?r50LsBIKS}&sgBRpT$|xjsF#snS)Pe$`Qs=`xa`_*g>_^|VJK6fQMi%V`!&MX?+@KJ*I4QKTYUWB4bk z)5|6e$D%Vz0HtD`U_w}@Q1jDDzP__+DjYE2idr6do|0+wwuQNfq~4%UpyQ2pY@|uH z{2=9+URZL9Q`d!X%$Hz<05c(AAcv5OrYRFmLnxls?4HSBu7yo`U~Qf39NW}KCGjiw zWLY`9n-8r5AeLn;;>9wgxE_sdDqN7aqvNB(2nVT-onO-cHrlQZ+N13rX%NmLJk$pT zfKS*Vnki`d$*wkY+|4P7W_M`s+R(h1xPMvzcxI6z>}(@Su>{8WV#|0xtq5t;yC7(_4ke)!&H4w)7KtsZdE{0QVS z8sF;GBIO{wjJ3$biD4f2GcU`gnKw_B_glcYG#?S_mdDuItFYT~krk$Ece8kR8YF49 zODPW&nt2d{#Bv}ZsFHfFT;P*hFeX+)H0%IgIRAq9;+n1v@GaXbA2S&A#E-6c;vs-9 zgq|E`Gv4?SeO%GWJsRz~RxAWMjZmEjHqA} zIAf%!cLk^?n%QoE8fe^1FgP{##TwT!XMdOBslQ9pFQAoAA;+73uXIq3)exTmHk>@4(wFt~mTWK$VUz-v)_gCYVoj0Le)91pv( z9Ao4_gqp-0M~VEZ2}NZ28%1Z;Nb%#!3}6J9A5>RW@Ufl8A=ipn9%?Wn9UlTuavJm`)ctQdr3gQjY^(x7z#A6}9V_tiiV>g2PVO>j5pVo8!hdzKT-wr6It-sZ&n$tHv;SX1t_g6Id@FUjpeww@BmTq z7u@>=Ojb2mULLrpm#Q?>FrA6OD0PG>M#BLT5ZQkZPfvEk;L1>Eg1KZw6}^rEkABF9 zrm4pq#)wB?M4+0q$H*F%0Eu6qRMAl!UIhzAA)N~L0?x{wrtA{B5z=BjkW*+jytM!! znSyA^cXG(IYJ;5WASVTB0V&0g3cXpjVQRXxTI6dbVby0IVRF+mrr--)z1d;t z*<0grG8@d@HOgBhIjluJV~IA3BvFROW2%p6Io3`J9{2m|}#@)Hc0ZS*gSUW_4o? zxQ3xeFOPqfxa7x6PN8<7n)qngSbA}!AcBVC9z3~Cgi>K6j)r=c$? zQH;pw^x(A3C|w)i@}Nt9e1&|DA<#sNFfzjp8pfD85v$!Du=Mzgy+V8*&z62NNpLy1 zD_AQ>5%hEa>71o;lH4{~AG0k_GYM=WG?kyVYVPPQ?hsgmX<0VSM^`LGo9QcU%+ej} z6haT>Xt3TlC*lCb8Y+bAjzpvJWG)3UA34cx5L+d(Hr!As+`1~Nx~j4jXf1?&)KjL2 zO~Mu_9I1NNm6fp+W&0Fp+n~V4Z5W_todhW==1%YV^S$Tm;EHLaBc_veg{6Ax(2A(6 zl`q(C5v^)#J0W5l=sti$X6T|asepG4{pMzPL)qYl4FewdPPy`Gux}ZLs@yPy0>|wK zZcfCfhW`WMB=v{t5G3&sv11~>QAeHFXtQrxFQyEU>D1h8Q^49?s7wrep3shAQof{9 zWRO~U@6=sZ52o6Xaf5*ZWy8x%MY>6ikD;#1Q{CZxwlk)H_gQ1TagQeDtZ zzamF#-K{XjW zL`85*?DT-i*EV%T$g%{43~5#qixz9_cVv(ih>qSR=nVK+`#A#BYNudYspyunCD8-` zg$=eQb$0`^!olAqn@IC_!6<`v^xoT!PMVcx$D9W{EBVpT7addMVW7qM$zcGQdm^fG z99SFIT{H4`9dr+SRcQOID!JC^Fyqf>xv0UQLDPq8`g} zF;H_jPU}^*Y|C5tw_qAiN?{%IsTrJCYj36(YX;Xrowd^$)v4kh08sU51+)@_AnwI? zYkE)~a@m>k2REd<2UGv5&8|r~T{Yp4ASzaFQ4$Q)4=x!p2Yw5DC>(1+t)Ac=?A9XZ zn|lg|{8}%-v?l|3)Q&ze0i&7@X0?6iLvW46u{XUw>tN~aGGmk zTmsO@GJkw&O$+1DPlBuvT;0aeDVoz-@mmu?5zF@nrzgD+=;h$%HO_F}jHd;!QCN5d zpZmF=$7KnX2sMLo8nSCx*+mvB-xc2FMToZQbb+1wu5#EhXHd6VUQ;gu{J1qWxH z_Wil@p=gLuu@L9_7H6b13L>u$T=6j1*D0j*%uV*aGl#Jp%N`u0#;|eRT2~Mv(#g-7&JOIKmF3&`)5(^cP~g z-~554%O}^Gub&!q+NIJYgdWlexBvPx-^u!Eo5XI`@lIIg}F!{PgAq?mUYHv`LlgI4HRyv zlPbm5K+)S;!*zTQK2KPGY79_dq|k(7rRga@v{JXE~#AVZbH5$6>V>wH&vu?e@2QapsWQ|?c(2w2sh?i^9? za(_#MOb7YdI<{oKtfjcS=as`?{sQZ?Rz|bnA_P+hG&d2%H#!=lf%5Ra1Y4e6Sbi@~ zinTv{C{tn!mMK#EKAos)-(7zR)cNv5x&16!Ak0=a=?Z3d6R}!b8Yq+<6Z3i482pCmvtf81 zpXykQ`AlVCX7M16+-9Hn-KMjRL8$6cy#EN^Z}dK+wu+fyR=F+m z%8T*`c?HJ39i1nRyL{2*b1yrxq8)nJ1}o-Ky=H6G7+ECiSija(*kmH5n>ZXpo~XMz zJSstcPu03M$u|YY-DqiFCfIWa?X^sV=YRqGOTL1k9mr{xhjFGDo z2eIp}>(cf*a(v3H8!q zooha~SnDazwzli9U*+knW+zOoU#=X$=gx!PH?_HaT2rb7yP}Y`!U=_R>}XU-^fcM4 zMn#&>IK13?|2|-Cfcrc4>O(aHI;30;n6r`MmYs)XUN=F$Y0o zMlwRR>VAnKnQn@RBVpD|aax zmnEuFx}rgGvkJb(^@F(KNfB*o?9LJc)1Qjv3e-imjItm!uAZh_@x);1dV28G1*q!U z2xtpo(yj~qLtcwQ6-$!{4t1O98H2`8&`+N<+!WhaCF%%OudHfVtRXg)T?doE_Y!~B z!rR=^maOOrsGc}O7{e|&2yA$MeB2%UJ6ORCx@fn8GDX%94?#6 zxkU~)Eva;&Clni$_VfpI-klA%%u>cwCy|%&8Zs61H(DBQfw$*qyKY6hs8dA2dVv8A zQ8uptDh3i)rzmQIT+4h>9IoED3Kk0#9Z5lw!=|{)0Pg|7t;mxg!O4aME?>-S2eyoe zkQ}8KZT6PNSq~1f2H{|m&7h2nWMoe05;CPuFKVR)wz9di-pN7#gpHx8nk)2gp4LgF>C!n_KI@pTmt znCWI)2>2R^KCge9}XBSHT`R zYbb0g%8?b|qL26h%HTY@isBo&X0nP}+c4ODnd`xfRg+>>3BvmB0CHDS?%$~?SWF5u z180 z3zeDlB^cpxCT`RQ`}Owbk|*3GX?ZrM<(q1LqKzH;-WCEQY;)HT+-Ex9b{e;gEHFTK zZA^C+E+?lW@9^~eBv;o?K5=T^f}YL0-eRLTU)%00d2h1ig2cgcjmU@4M+?cKG~XB5 z%@|jWcCp9?Y_$zb-F?Qz{%?9C_Dre1fBAy{b;B$%kH_O>f@|t-F3~ZC*61i3*^SZ& z<#T0H=fxXVR!q2eR^Vd2Xlxe5Fas9rLlxR6&!;EtPzRAM>R&Asu(AKX(qTWOedE&p zDuu@hh0pAabH$UML>xE21yTLG@A0xd`A%?S5_~c%f3P{`StH!kth9+wsaWTVTmF-loX~_Aau8gg-)1xD=e=_O~-gS?>VQ>HRvZIlla;~ zpbHg=AT{a&f$cS%Q3v$S>6mPdyf|W-FjO7M*%a4IWqgjM6lscHh|xY4ZBl^8aku~$ zh3};upR{K}UI4B?(>@9`=~RbTP7S4#nsef~TBPk8-ZBX*5B%+82@qi1R{=Z}OgIJ{ zqd3|X16{S5yb34zEEjvo7Na`zuV%OiLhfUrguUt1&}rg6O5YLHV%Al~|Q= zapSQe0Ljv&(x&{XSCtZ+i!I8FoKT8lI|f4BvWvtY+v1wV%)gF<3CT6My$McSz@q7r zr%E~t(?h}+A%LmZKHlf=SM04 z0FloC03QGV0B&|^WNBeva$#pLa$#p*b7gXNWpXcbWpZ|9axQRr?S1WX+enh&e?3JV z6n7?PDEMRD9W%OS$I=qz=BzAfMcK0*9tQ#_JTG+z`~{8Mqr^iS5AblHn% zFMeQW??%U?0XsUsyg0ucT#e4p*#BaKlatZuXmB+g`)m-$jOyep<2lc6_|)Iq7fqZ- z6Q1P!U^L}P5zQi=9kRhHoGkdk3;)^PK9rM1rfi)|dB%zbmnGDf9(%{LJW3M=RagfL zc=D6i{oerLE?u)_c*l~oVC$R%h$v?>=p5spCVW+}C}ERyxr(DOnQ(R+6$@%c0zqVd zl_1jVB81W*lwZNGnOTm71tGjBiq+xMr?R6?AQLphKdmyvACQcYgmQ2>w-A`60-=1xCos$t=vOjdhjY&6 z=?%{k=nh-)Y#HS^kU5|?1t@W}jEbardwZVeUBQbfpG8T8#ngyMp1d5qX2ftA zyIn++1tOcm7*H9WOjl`?09PvH%HsasP<=hzd+xKR3m(SB0{DNyCm*S>c%vFLS_0Hr z1W>1Z1x@f|67k&MdjUWuX)=rE>x_DI$X@e8(1hwe!5Xu09T&_h$8N%SjS&6?Ak2Bf z;Wfv~;xAxDApa^wBCm5Kqe5r008aJyUP85;huH+MAJf0Ozsoc)pmxk}!lYo+bh2J@ zlOFTvh9^u^W~(p@m%MIDwyoi&2kV!c66PVHNUW zoKs-XoEezn1PkfKZJJ&3 zIj$^sJ*EaZh~a;VJ~dGO5?DNf*ArYdB-}Un?n1oNkR^b-DR5_`2k%vw^EW`zxW`T+ z9#4BXT>ya|urs_3ZUhBYbs$3q99r-?DeOL%1>vt^txNrKb#)QEeDNZ9dvNf{f$4w#rM~W)w?ajEEb0 z?2O97>~5>7IvUIspDRF}JqFJaE)1Fn#4EMTil5Zl5s>o@>_oWJ(A&`x*6X7*DPR=E zn>R3A@J{?d0m1sp5lT?UrnlFvp}#JoIQQ*^Pa>`Rd_-Rxs!GBV59D@#kHaFo2Cn&q zij0z3D)2iSy%__=U^l6LfyTrun@VFq9=dCm?(Xf$b;yS%qKUw<14}B8`)Ll#!cCN= zi9hE>$2&cLeL8$MJoS3a8=ak;dtHCMf?coE?GhB&Jo;)`!y2dbt`h_ath_-0ZSAuo zn6R*p3QB=60L+Rq%K_92r?BcGab%;on8WUoMYs;pma2A`d=EDDlhJEvu0q|8>=3X~ z1Rd;nN7j&FBOpKI>j}=(**cEzJcYYI<=5+ZXJ%J9{8RE-G$% z?K(80ODx5VrZPF;D&Y8WyhQ2*##e#>xXYRWENMZJegrWAs+myUd*vtZgDxMZte=Q+v& z6F%?S9ohy^y&M4g-SgkSC>clafp;vWnbXrZ#xNh&`Gv7^Ob*%S`WW83>XhSR)G1#& z>y%?-wxeG<>lnjhR5ISWYvDkd%n-)= zJuP#e>Hqk6xbS#~%o9rx8SCs{;-43@&rG>s=HC8JD*YtAwS**wkgr<9Om zkSpUT!Tlc#z=}4dU%rHW-)q9-l7kKd;%dA#Dw(K(tt$Z+&|4=T#pjTL*p=N?TP_;} zL0UAmu?awG2BJ%PD|9%y-9cgB1F#=(?VE_j8)a)LO-hpN!9EN0moLS(m8EF`bFWzR zy{9+NC4xm%#Jpd&Z74cpf+Wisxb{Iu0y&!+gMnFw^5Zq`380!IHnIz-&S|Yk=t|RS zGja@Hfl@*ujco8nsNj9?x&O?6rm#pSd9co6=mGvI--42xPULI)TfQevU=;<(m3@mf zX`kIkLqdAX!ikU^6Ly`Yw}6h=^bohVBVs7rr|+)Q^dsv`aU)JwIPBd$U_#hhabbJ! z{RUQ1Ai5t;r$H%KO99Ces0DH%wjD?aefh8X3C5Oe7-5|EPvC!6Jrse{Y2K%H%?}{9 zpw^&1m|MKwnaf?CEE8hu-xEU;;CEve?})E`u~h=NC64R_`+Sxrd@q`*T@-g=lruP~ zDbqtjiYTc*DJcq@&uHfCQK1rK`K1H2>@nRc?kh}6;yfUnP!r~`3V}ShNaJZR!>zvK z`gF)Ojck~w`j^~82M78&tv!DA24Ah{sV4qzj?|tT30Lge!mSiPN&=1g5hSQJ~ z^(cv!>!s4wp=b^YATMo3NPtp*2aOx>iIPdY25HSN8|}6d<7f8#(TMR=AAx;?#HhAP z{mY@V$(#%Je}+cHBt-)u2rMuUx(MSrzD<+(4$V|Ejsk30z;c>*YxIT1be$zm8q1jx za3KT-H`D7NT!Y?1n{p=;Mu0UC1SErRu zqwV{CDYsbr^nj{ja|#%l#j;4-^?;{vqZOl@1CNH8$ZUn1!p+GWV=rzpFMR+6HBrsORfi9~}>SXV{%Rzh= zb;tmSy$R!?KeMF^RO$Pl7uv}Y*|w1+jeWU3Mk ze1QXyhiF_!HZW94i3!S5R+8c;IU5>b{YH9Q~7%^Zmci#;sUz*ma>Xdnsc za8}64TvmZmQXQGvc$;RAc1r4kZIlc@J%ExHPo1Tt1&4*?VYCEFCJRyJ61x|?p?TFa zs}g0(`6yU7N6+IO2ovdwNuV|Cq~{UkVVetW9(?gTy>;LNO904ueGka?<4do`o>hxW z8+H3FHk}4?zo|2=tY^?170C(GT-YMQJSA_cu!LJ=ACjt9!oVW9v`9~`%a{ltU8X_W zfKyO9+sN5MJD0U;JKEK2HqoxmrvDJM11UI6KJtvVwU3Qt+c;@04M~c|a=}(E8xuV> z5Yl>}4Mskp(K4f6{IE^0P!Za?Luba^ULm&VERfwGtb{HkL`d``lDv*G+EzW%xU}@@ z<#vn2J~|mG;)OVDwx%5tgbVo^Hn$jG2$pp za61P@henzrFyQOr?mCOq-k<;rBBZ3g@#F?Hy^D+B-SFt@{BjKc4TJG;aC!90)vp&r zVFabH8VRBtJY_d@bfMM(6CxksB9S{KyJ2e@IOO=CY>2orIkTTjZre=8LIxS4tN!vCFaSzSO81?{>Spe zDlAY@>u28kKjnWKfB20B2(%wuwx&W&F?wF)P0ccv#Y(g+Bj_lnKM4xcP+B<1Y!6t?fd$VTXsQMH zf%u8)q<#w#UO*FjPTTqi5K9G~|p(X~h>lPxx_YV!`%ZsKL2C+`E%dw2S#eF3o1v6^(CB37`tx!_~`>r$uxHYI0$$gY;TDA+algm5BNc9k3)pp zT@SZLXe|eoj3k6O#W2$Ab-}QsD4Jqq8jRx&HyIJct?3j+*qw)q0xeZ_*)cs_VpEW| z;0N6>AUR=b`Q*SFZ|b{lI~2Oew4Dt+2GVyXu_4wtD9!CLYl z^1st-<4&vM%}%Rgh2A%{h40~{F7e;20qnm1Rgm7nOm8;&I2L>CY1O*Cll7Z=u#E-W z!u?B(;oG3xYSSgQ-qIRrvF4*0CZ*5~J$WQMVA-W2jDu(?e1N-ktgN}gm_j&Umsa% zm*GeBnq4D~d^V#{{Z#rKY_AjU#HN*kVZ`Y|Sd25i!tj`>>TX^-7s=6pXp)iG2rZSa zN@?ZNMa5R+Yiq`J2pv4F>C3p3RXRFgw`n#-@mhIo@wc#Giz)6iWJ&QBC5(wMY9x7t z?ibZ2TCAmtE~3~w_A1+TYV8&xxa^^thOul48SxG%v*6ORfKF>+Do2x<;ZT^ZGwF!H zsp40C$wV>q_h{cAb`cpXRNfr~nB!Pfc9_x~tG5et61@LlTWoSVicnE*99~%DF<$gY z?~7nED9k|vYGOY*pbz|p%%h$0c-X~K(Aztx zHA#VW!W+bZqMLa>NpeZPn?Y>z(hIr{`mZC z_(63N*@hZ4v@W&b5))0M2{CUEkFk7cG5L62!%A~fQw|Fi3S*uSLSedU(=pBE(gGPe z*%Prump>X61RZ1pz*o~q9M`ed*p?`F$Ew5F;C3;hkwl158IwfTTooWB14=Cd zoFg~MKvU{sC0r@Tcrh|emOvF-x`LIwON>xDr066Oj^r~LVSVTbS*p3)L`+eH4Oc8k zWJ9#}$SSJaizW2Wzg7<7bntiIBcG*Q_cn$Hb|!J&lSki-kRQ+Uj@Gc^ESZMh82pD_ zl8zRPbRn+BI5r&NKSA(MY^>$r z$sj0IQH3vI1ruq7&ijMs9|YF_Lt(uMK^qU~ba>^e;`ZX{S* z8;)`43tpKg#5PGTGTf7D!eD;ncS2Y~k+3|bic2SqvEF%FHyIVLBersBweHd0lfXWl zg?GYhBOAT`Gi+cYjGoRU!Yl)MLRM2Uj!@t}AtO$o(n&h8bU55PR~&_0n9OwIkpifp zC0HO((E36j?pe)mt2P(I?die_KzH0e+ir_e@K5cu{RZ8c{|sK_zq!*^kkC9S^xb>A zh4*&rz~`#wSHt3a{PWj}fBx=ae^d|qKeS8sySK0I?fdTL`|jpz?!m*HP+w}u+tO%f z9`Bu2O?Ypj_-36OTmB}AXTMqI+20Tz@!g2`-H0c;@^@*(+max8h=!HFC@Du88=5IG zmgS{XrHm7Kw2b1A%!N{*)}PAL&sN{%VQpZFaIz@PR|2fI_t8(&TxA^0q;KL#rfsP* z>jEgzgg=3Q5~EVIA|@<}$YUpbT;D1U?6VW)N|U|pX+jF?i|RaXYS1o-++>Im+g_L- zM7ae}Ri3H=z%&0JtRfT{fpR$LIKr-FeXf%S3-`<{N8Nf2ss-hILb^gK{lQJHM%?{y zg_-22ez$F4YBk&V`5(q8z0XGYxr8G;ZsLSR4|crdNg<7`ARf``VcTKrv=LyxTdP|U z{9ta+$O*%OnAz1_rWfSQ<=e2bam+@ccZ< zZq@VKjdHM-g(_l(c0W^26=kSU7>MS~qru|M=4RYd%~+r>g>c!*AsfFv!pwB6BNH<6 zX}8BNhG)m4v)2?4vZgnqv*6f)0*qPdgHn!v-$N37re)B90rvZI7xNUjM*^kGIP|p4wxqFI$j5k#`u38#U)O6T*K=Q}dmjH@tNs$KF{yJgsUv|d|t zBBd>h_$$$>XBl*xP9!-28^@0_z7cfeX)Gt~vh-HK+YS287An^JZK%_BD9YJ}bpmRe z7B(2vosCHQmXYMPp&iAU#C}_Gq+u{FSul2o+fkPTpM)>|j6 zigD8b)o|9p3FTe2V~V&sU-~xm<%adOLSeVvy^+W!q#BubfK^i30kOf*ydAk6@apF! zHqzV$r7=vw38fm5P=Tn%(GnBd6)q`g7=bdFPEzKFY|Uk_M;xoJI!IkzO`km_P#|j8@HMh(%4~T4cZ`tl&g?lE!Cw2_4&4ezs(s(!~L(A z{Zi+l4;EoEjd|8_=b`WJQ6he_@JQ`1DD99z9Sorx=tOos^gtapCAp_GcV=SxLpklp zUSNJGhe3$N0p`~4x29+>6=|tp_CI?r!wm~4Pw6`p*UqaeQ#yg{b1M6`(+l~AD%rsG?L;{tCgIzqlBUx zVwnt9Byu-w2~eQR<>cemZ}b#%a`|5CiL|v9lT2Kt7VH$*T_36uFnbQ^Ni2wtcpqojM^LB;&E`B zW+?aM&?0c+BujIVXIi{39d_1;=&Z*fOZY9|DJ~Tu&xEk4l`WyVYydRu%d%3xemhC0 zYc%y1%_D@Xx`t$;%2QG()w|72I`1v%C`8IO6-{NH^~y~tjtZ3o@9MUnDf=Arq+^2X z-h;MNh=@1GxL}dG4PNi3YELvF_MkFeu7RPUeGeU0PggNWc#2iqr-XAl7zfT|OE3fY zeG6jN6jNBvynWUR@K1@?h`omB+Pp21(Y=Q|CFAe=xO#Tm$MOCSi{_omW{sC)$<8Yo z)*1kNA1uTzV2v7wh)=tc#RSvPbw->hP_DtGC(>aIC0 zeHW?di5-ggs7m?jfDg_OmeceX2e6{!0dH6`N)k+Vh93`*aAwSTiF^QYmYkNg@~M%++vS` z0w@Fo*W!<(UB3`UT;WC8?p4v9Z-l8XFmC#Z^U|wVL=$(l zin#5iUbwVnS8AA-&(y1FqxDwDCrZMZmgtQM0M+BNGVd17g-_T1S<%}24aVjgGSz6Q zZ6bah^|j@mkSNPhw_@boJ-FHd(=zF9PDtf|P&L_parAM+kPFL6zhb!w*J^;Cr&(o59|SOv0u zP;-rjRZv?-+nQvog4iNsjIM52SKH5MIq3gzL3yS&(u!5IqAm(TwV227aoD@IZsy+Z zn`v$P!8}E$Wob|hwJ@!HrIvK8uTrXr#){eNw-UO9=a;|#{U+i0NL*QY6w~!Z(&qSA z6`tvAtH5G{v4b%hFhn4>KxP;5x%~5=O>yYCH3z9Xhe?jtgjDZL>Cu&=D|jL9>mcKD z?VeMuY?Sxtg5;}GaTqy}(I6m``BOllgs!a^8XXFY2{3pBk8!P6t8vd&xI0ArojMtTgoDsW=Fb@k@#YDKShh_k;D=%fn_**>hijkQ_ z7Tg9rcNj08B?1j?ViAx+2d^L|3t z-;XcNE~U$;DERc}7C7}ei%&grnMQmk`Zv=47xE*qCsgW*IHio)ePAj58_@ZiqH)ff zi1u%suk3L75Cg{^iC{E*80XgMI_4REfN@6jsN@#ik?Y3BsCD&P$^^puC@rbR9g!-O z8OGqf)y`)R=Y_L2`i)gF zGg(;{e;FTbt43MOes5IJ7Lu~8!)W4}dV^h3y)_C~0*=e_l@HBT^2RzQCESLDva0)g z9e^TEt!CYF;??XMwMs72hgY?z)M`0wEuPD8QLETs*6@;=fD3i8zqx;DUNN69HO9|XfFk`fSZwp&hZOp>c zp^*th2b45RNs}UrNi7*xt)7ykh~zazPSIACQc9hHQNzK?Qs^}6Y9+&6OV%`Nlu8J8 zra=dBb!MZCte($Wf1nn6s-+4V@XjswtEVpitB1zZ-KkBQ?U(gzV+nkAR#SCFQZaOOP}-E$08M<~4&eRfVh>?}W+f1xeGD#((VHu-LWb4j0I!H_Txite zVhEE6b!7Hgj#qNa%t#@fy@VxAuWgU9rkPmP7Bv?gD*aEzp$)l)A!F88tS??_jm_s_ zmot0z<;;ei-@YRn*3%8QM_b6CW4%l=jJr*FUV?BkBJ0ocuhN&$1)ZOZEow9xVXT=Mt_20g!wpJuTd`bmam2|1Z1he$w zx2WT~bz9H1&}dSDbt?^Z+tg=#P-j+!ZT*?p!wwACpg=2vTZJ3K4lL(es|t-zMe*%x zdALD`cGSrmDplyw;E>-^i#8zWK!iZ0_>^G+%^I~T zSDQCYD04eOpub*|TQsnz%xO0=rv)T{AvMk{Spto0be zMp^p*f}BtK4!>Z9%Y_oGB5y61y85#eCdyq~_$eutAKmd{7l++gc{UB}x%97>b^F$q z?&}Y_j^XF87B)Va2wqAfzLqbvz(Cp}h*DX)$|5u)7+6K`e0y@-==Mr_=M8J>3Q%slCGHk|x2?q3tZygB~MVmPEU zg-Vrb_BPCore$=ih&%AxA~>jTNATC1kWLo(N2jCV*;R0KdOjW=2baTte>)ss)i_d; zs>}gXA&>Ujl_$f5_g^MYlUKt`Sj|&XA*}bV0szee4s#UypE(S)d0GPttIR`~5QzxB zBeMx0b(ZT`(5q2u?cBKiP`ny!9Du>asFnP9XsDI$46SUThzL;0=75gH#IGGd&_kku zKGIn1%jjrcU(DpeC(*}fR^GMJ6S~|jW+K8{v?f5}3|Qqac-}$KuVE#dLx0h3BviO5 z2UOcA|4p-u@plu4Y!z~5HK5|ckpA`2^_i84Mhv!1fy&QM6O<6szHAZbKb*}_u)g7^ z(9gKjWk2YNWA3+R9;SO%PyU5`$UHmtiSV9IJU9AMC0Y=g&l6J0g?hmgWd4vY-mkY$GM|vGn4@uU!1^hOlTes1{ct=n|0N*T=HTH6yDb0! zQ@e1FW(sEd?gN))SV-6HK)PZHkJo}c+8P=QwU~^#Cv4XWg^fCWg~qaxuAoW>>dLlp z{WzRTXhijV9+cSWTUAgPe80;k|MYz}z4y$(fu{}2PvPtI1NStL^fzkUh7 zYfN(J>iqfP5Y48g)n!}D8H*uGWei5TVn(BiWY!C_--tIOI@}6nT9jBT&n8xmnX-*z zZaNg95EG+d_&mnav@_DqB6gLk(M#%BMOZBQ@|XZ@d4%-qh9;{7ozRf(=S5zDDKGS| zNja4nDw3fhaKp{dm!LV;RXLqN(@i#G86Fr>oC+?el9$3&0LB{^?BI4W z7d}NX@ux<=Cz-5+bSH;8Lm)bQ@?;f(?+_}7647S+)HOWQ!+a>3I#@qUKfTmGj6Tw3 zopZ*-X=^;_Kno9H^FiE!HV$pEkubM}Gu5Q+`WNt?x^uX82_3lUhaGF05Hvy)BeY^h z9rsGDLxWY+CU$X+P_PgQc8G`tHguUIT^BQ)=mqO-HnA^Lj z(oKF>oWS&Ex5aEOe*w*x@_ze867wfd5oK%J=a400^-Durx7LBtFm&|rz}s5|c{WED zUe%3UQ|Jf_m9ssD?tHrmbWQXxxXV!PwcfH<6EQqIZ4-WG5o30gQ7Ed3Y&fmYq!(83#f}d2`mM)FRq(>%YNhjx;rok7!K3+CO!Dev#Vv{m!6d>s8`sn)LaS`-xvJhAzD`(HYlZ6S&fRWU7bm?wm8SDO7#x1!9r}6q}5BRy| zwLE{@9r0~OE@yL7S=$Y{C7!lwxorP)ZL0HNb#gk2NYY??yFRVBcqP3V>LHTuRIv8X zV9>I=Gx|($EBt^e;jtQ1s_oQCx#UPuZdVPfg0f<+Rzp1>Oi02>NMyLvEl%G7n@nbR z+%i9|Lu{%WoE+eID$rENCg4wQgx9_kbw!MwrxQbhQkkIBLdbL{& z$R0}JyARHy=}qb7LkUUU|KD8^G$PZ8B~WRCtI_Z+&yjh z=SF_NWL=h<-M&YFA5A`}?Kgi^wjGqr=FVmS#8`Sy_ z*C*CtkM4pgtmC)YhgYAH54ucUEt;C+1Is^0s_Q*emP~J#e^kwox}^Blb9B`)BsTDSqw_l;`p}<_F2Xgoo93RM0JMMJ4hRbNDAuD z8{jhA_vQPEucPJ~dSZV?Hd$vj5iz$@LC0dSX`XL+JxD}w>BdH zuAB^Hwcens#Gy3(6F=6}%>(0(-<} z7T=3zD|@H{9qMxRvc3rdzed(bcxf~pS~IEnx~^C#|8=26m$Qep2w!fMyho_^}^2A)55`3;tgPY;7-A(B)tN z0JF#d00jTz4yv=Gfsuu+*+1)Aj@oYA1_y%gtls{lWEnxjx^9WrelmE+WUGRPI^d?R zib{fnbUlQ_jH|xF^6&SEY<3LcoQ;Qi&GkL|?$_Pa5!5vP?N4`48RlP;;ebVFTJ$3g zIQb@F-7O)z-L7wJ8NLv=eS~dUv$(}|dqi3CGdY&7Uii^{d&bO9^K=vOUBqx?fwx$a z9w(IwjK%b$Ki8-VB;^yxeeqfeS_9dSGvxiiMx1`s zBTB$FK1Z)#fPsJ%^C)-rJK)i?`v-$h76E+lW=;FXh_NT;TtR)ZH-f0D>f1&8N6;S_vNs>$ z{#pF69(3&o@FW$E2DsXSG(gm$^@WuQUIhy zT2u{7+~lI~v4HgsUu-k7>Ib z51zm%Y}7Ruc+i~QFe?0&&FN?)dhyZL5l7&9i1s)PSq>3QsipaYG#U1)*)|D0D4I?! z+*9zfExkbI+VW2FR+Tdt<_V%v3G&cbQGAK|Fy{zQgeU~!?X$+JZ=hz0@09e0y&YH? zPC*kHOuP{0rQYJUa+oD9cw zN4`mAl~Ur!5kJtQ#>Ddc)B zsATX zRwl0GD{ho*!%$MfHq%IeoeMrLb}+2bB`=AzC}H$ySnYytH`fhbO{WRGt%YKIqSG_+ z=jNvyt@tcj-RcdLu!w^ftPnJruY?jukFio>MM_6D9Gez$Qdv%eXcc&doUPp`GR~Tm ze$(nAtS99G2D$#m1Pcwf2?V8=n6+r|4)pacHMIEMFc#4$H*db7UwQ)#X#fc;z&^A| zBnK^;b9;@p6JEBVCTS5^f&gKb*L=Q4b4k3VFnd>fbzt+spyi9sJ((X|bVnTcVC#T3 zyJ5eJY^eHcaZxZaS;Is~Wr^Lz-0HzQ3H?YVejSWl+=n9sg{wTk0v6E1IH|@08k7W9 zKwY977Na~Gn>pzR5`X@Z)gjoH?T?i%7&8(nY9P}8_Pp-c`;n1aqY~gGdkS!SgorKJ zPc8ns+(gWnUX?UDaG^o%lXeLO%tPl&JZrcOxTuc5hI_w+_9zehDA)UMh2JuK@{qa9-;R-3{N^;evpV zGKKK>-}QLTG~CpUruBJ~m3yAw$&!A>?~CTX(M2_W0VFU)P&2-bm^$b$&;G z;8YS%Rgay2{@Yj1;WGqI3={xB3-SLKPH{D`wlFsMKjD;VH5vS$0pH5oOSAcs`*Q-pWfZth{1rmxq8GIXibpmqU18hk^k(X=pl50Y;v zvb>oeu8w%hp4Ih{vE!Jm12;FQXX2+rz%EXYN9yRhq9%&q*EF>}y1D@iXUB&7nf^<42J`tX1Ez~^DsAs%Mye<=g}{mN5Md`M1F{2b9#3Yj^KPQQiX;Kr zq~?9w`jiSeF#A|BDVl&4{CS{(4-^<*qG)VfVJU0=3viB6#)6va^7j6_MpHQApbfev z6)04q>QZip+sUFBSE~!bx!eK^fk(`qnk>is9Pn==f3Ke2+`Dz-g2A~KOg*yE1Yh2a zo_@S}IO`5Ss}-B2r`xo#pqzRD6A)9?ryvceR`OxPO9Z%_G8<}RoE2%q^q&W6LIfHU z#V@0?PisY#L!u9Cpb%691rTHjGC}UdI3mb|8t+Lne>~njp!5c+s_c1UX2|V5L?UqW zM6y*qJN)gxQ2B%pn+OT!g>CP=OfmihBug7)Xb++JhtL76!x%|ANjp@S(4|61`Gkoo zaKsmANK8gFgU}OFS7=L+XZtNeS}tAEM(YM_8duBAv^XGC%G9a(lK+JN=+>p}0aeQn zZNO(wx0JqC`jIc-$Y~ge5Bpm%DH)2;hnE84bOWbyUju%F3NTQ%U}7BWBASY+?Ah3> zN)`o2pb9oMhfw{d#Fm0|r?z=rEknt$7=7q1XZXQ89odg3N`sI#-MNR*u~+Ud6}|FN zt4V-1@+(|GO0fP_={zZNGGf^tEWgv#j}1AvWFCdGQHUvI(nMUYA`Wdc0TOLE&2p#Z zz=z2&%IMC)^1OqZv2MkPjsv{`=7Xnz_`ndfCsdq_`+3~Av%A2rQ{Fj%Z@(WCQ11)n zglj-l_e0#W`!|$UvGvg|oQ8i|zioJBg8@X|AJ!8i1#)7HDE?wa?YT%_WzAw~RGy!%JqP<|++TKL=UNP=@|CR+%%? zK|Y)Sa0l)KJA-ZG!-L?X**#fdUnnSPP$c=%sy)BoGpumvF%PjP*paj0yAOw6#?g{U zfzA|F)EI4Q-5V>jnUiSwgZ)1D2?!GOaCy1slCUwTq~;F^X3|>eZVDD7-)3$qYu2>s zYtZt1lgzXqYM(ZRJ$pwV-7+An>cv+>B4760@g$tQ;9`+=QgB87Cx;R*uNfdbxk=e; zGiz1HU)uXQwwbwo`=m3{Sqo*8-`ha{YK%D|{O{g$dCiOzVhwt;55&w`6dLTJGO8J2 z`{eBYc8L^Cjg;{M%GQd&Et;WEAJtY0N6nA!wYv8z!Oi`OLY{kUV9rH{-Jd@&7eGFR zQ2Bm(etsCyd&n-lj?z&k-jkg*fKjpNg1u}Gt29V#P?r41A(S^N*rPW&`Jf)IRZm>Q z#uAfA9+oVPH-^rl+*VgqE(5b!x~rlJSVfVaNaH^_(WXAD{I3sO+Vdk6WAfkzUOP7- zYjeIpT1G?2jiEbu;=<-HI1JUVP7LZr(0dt+X7uGZ1P`WHbOWPidW-jm-LI{F?vvLn zYu#gRbho;?_*sD0qhK&Z-99TOy{Xr2{eKwIGpe?CpyoaB@;uqN+-uKGk?gEb)g$?NREur z>>M&8U@Uo6uFbMhhp>`?|Z1`&A#?lF#}5*V9HkK<<-Q zo2S^m`{Qy z6E*aS(97{7z2B^CZ(et<%zBlZ+3jwQ487^C?0Rp*c@}$9()!cE8;|iKi^)={F(~~% zXEOSJf5F)SNycOKkysPxY^8C=o#9mJrhCtcm|o~CJX}F;b)&vZ>mF#;iuQ^;De((0 z{RGJ@28#ipD#lo0{~aj*+A-_q5O?Y^YLyfmL1H4UfqnEZ&*^3)DQ4DbmQCi+dUjK1 zv8EC`5(Lp_5JT&p`PJ*&TD^>%Oo~oNe zKi~urWfjri3hFRr}nRB4C3CTDsPWNaT$mPiO$7GHep4YELDs<#&7f3>Y}td&ag8BCtc6 zcP5GViIzTo52>KLO&+-__3?uK zkGPJ~BeMCp(pLS4NM*-lb<`ohx90uUP6xCAO#mCT`G=%ZRI0U)T%fl5Yxr|t`e5+^ zqR?2#jhy-Pp5caw=VVdVY4#G(Mt zUoB;#;mx}?AFR2!8>#l!%5KfXThuJHfif_7?p&sJ(BEkZV3mfdTzU{t$QJ-<2#g_{ z+_Hw~y7P~ji=zmj=GcoX_yE8rqawG5NPh>_E@;P>>$;SC^(APcyn%rwlw^_BL+_`v z#}6$2PM$cvc@S2XzFUScQ&v^Gy8!96)6}?XRoQ1=!x>ZOPvKA^Pd^5Rd7?BPv^|r} z>_o;9v*n!UoeEpNZ-Ar#2OJ9b0}QAI9u@9+6HypL2=PFNdrri6~5zjO)N#Y&CvNcZL$@BS_|tOYI`X43x=Hg$%Fd zFj-wC!EC(vz(QlE5p1}tt@{@Q%aUI62SZ*K;cD-}lXQ1f8PrU5455ASh?(pZ=B0?x z0J0k;!LXls$jZ|fsYg142M9ClLnp9*bBvHWR+VIPRigL5DCa_O51@`nUVMHg`{As7 zpbE7p?AO(cgC`MPT`lt%*$1Z3QijbR;a6SON_PM&goS{-a|aXDr^`>E(|tst3Pu8h zV#;>$AMP=oMO@>e(~rLYhvlF~$>qv_$>ow!;Ka}PRGbk&4CUEt{Syg%?Bwxg7O~Fb zAqeuR{U0bgL6pKgVV20tTeJ#~lF0836Ch~yJxL6A><{8XN^J{Fg^BV{4?!zJiW+wk zW0mC(P>i<>!*WvOWeQA-3B{QJEugDo(fT^ZE|;;E2*)C=MH2O<3GOM7I8t-AY6Nf% zI;UY7nJnaJCsoLa(delWoFT|2qhQZEdK2m={R8g9h_fc)$5V|Bz^ugWBe6&%B@}ZY z&THadUm^>38|3p4AKZ!wzyW~n7%;{?qVIYUf!F1UzRz7aQc(=9Omh68qH8#fN#_V+ zpp`4i=Opqcm;(H+DsKrybr?{b;L(uc*z-VJoPb2qRo=nt)x_@ua+$6000PdHpfb8- zE*0wx15hA%D+)+Fi-9Am-V#L+#}=p}fwoP?7mvfYOS8;_kZhs{5pVU;tshCMj>rpO zM`S?zerD|N1Dl8cfUID}Bh``|=AVMB2U{sc4W(3>#&0;P@)z1Hwl7Qt%r>j1fbsBw zxtumNlVYGUCjS^zW#ARGk#gedS58P?bv7ZR2N#Q1IV1^6sH&b32V1w2PysR;1#E$@E3u5@LrH&a3P>VB|>iUNZ zRMFFog7wn{(2_(T5%V$9YI+H?LTt(Cs=h(GWN4cZNZ9t(rnYmVHHtVNr59E#hEjlN z4RlhrE}Q307QBV9304}a$HH3Jj9Y-`=)`4@2=E9M?If(w981yE9(b5I0C`Yq@>zkp zw~5j}feoi8&N>di1vB%t`lq_2v*6;9=H7EgB_*=2EA-+8xu_jsw(`(LcG;tW*Y~#6 z>+=@;nxRp^A$4CTTBVG@w$D*3_tj)fs;Dk(V0oodK#{x2fDHnR0flxHzU`xtt<#Dd zu@vP8(sFEBvLG?Bn6Q52U*a^bdSeEWpzG${7wLDMWk_B2A%@Y9k$^CRd~Xv0CJ_q7 zq4$(lY;r*}STE$qGrgUO5D7{Jz-MVkNedq?&B7uJK4%+P4%nxE)R!=a?ma>Yo8n^&xBr9j(#*38 ztV#>3(m=}kU56w@{jbzw)f5;FbwOTV6CMhRXqR*bygQ#|#TL!~;M*Q0&<4Lgznc2^ zkxxn(;-`ZFXs06_-`@E$(MfE*&aReI^Gvt>y+tFF5XmGXEv#zkky z?ZX7dn67G4gsF}$V!Cww<|6W~yt0k5x7d)o6sW5#i0W=LqEb6X9xEnZqz>nVTSeXf zu6>dDSulZHUT+M3?2-z}p#f3qurYFF%_o`07taD*LKT6I-+We|%MQ-g?#h5yM{}Y7 zyo1oJ<*#fPseQz0{u3l|gE+Y~C%h#ae`e4_I6{@UCISog!z)IGT=T0;By}(&kw#^U z1u)?S{HALQ$@B3j;08$$BQ`;4P{J3tDO&VrmCvW3FmH}Ut=1dnR7tZZg)C&UaJPO^ ziAqC^vfx%=w|jj<7C*0cTtNOn`~goOK+g0QULj;_5wobuHzQl8NGOl!gK&l5H#m1$Iaqyla#JkZ8~ zV^07YvNFE`iT4xLLJAB^4Ge zxcC-hzXw>d2cbCM2StVRpEc}@Z(sM_KkAczsCj5T*vWAl+4UoKhaTaLLL0Ioc__4p zmv8012+M-F*}$%!9yxj%Qr*=c)Hv34k2OJKTZD7?O z9`!k_E79lK$9M`Pd4X=@WfZzsj5m^SD+EglAsfb^8Mq=;?}3WwXuz86u+i6qW4l0C z8;QjY!u|wIAE%*T|5by3QeYMFH8aUZuqJ5jiy-R8nxx>gCQZ>cgN_8ia>kOh-8EHT z-4)ZaYP$qnLDTrY>@{H~ylnZyX9E28n#_5U8;2+nW61`zN+3}9zQkppNVR-rB_B40` z^Je^Auoj>coC=6ZO%exWgl}z4txH@i{4Wd=`wWMi@<3DH7nm{K!=Q*%T0B31jkt+k zXG`tH(B5mA|0%9qLe@ytyj{E^$drX~_s?cW1*AI2FoC0wu{sA?hCymHDmOCKax6o; z)sk@1qm`f2>RYGesaBfPeQh+#D-CN#J*IaY;Ga=p&DkBR#bH^D!ZTCW^#E=##FnY7 z(|)lHgO&h5NXf^<25Hh{)* z=d%LseGrQ(^AdW$Sv8sAEL(t+<|TRBMHAgJx3Cl?$85-zNOl9pjH1Mv-nSsFI6-u# zWR^kr`vfzdS`xc^txod%Gl+M32P90xV3%z`S>)hTp>gUvZar>YjFg`yO`=|E7(LJp zFn?2sf~uXZ5QNLZkw#&xWkn?|sV_kn)^OvVm$^)^^$erKrZPJGRe_^rsi)J^fIl{T zL2|OxF)M-PS2)s-$m{Rn0E4E_)RAzdsw~v#CP`Me5nw$eL{$OxO@C{?$}%qdg>p?y zL}za!_c1U$n!L%_*-$mc;SF;$n73a3Z?Gys)IKOccx{e@nib1w8?^9F>wWjjd0qSb zN%5m;4FVh6d(MasL`gb~lM;P$?FGSsB~U#W9wXIsa*b(t?|AOgQ8t93owT+Mzt&PM zUUmX@zGg2T<0fk@HKKiS5>=)g=&{6*uq!GF)oXWli`!&smCab)BNxU~77<#E!fLk_ z<3|wkT_BUTFu}T2OBFRF!I9DpL2`+|Sf;w6o9&um7!5;m0XHFB3=l4Wn+H55{F)OA zHV>I97Y2V>@}NzYY|A6Tgy{beu7XTU6tOt*vh8Zye7%%-;0nA2C9!w(%`r?c-h(vnf^Ye1wUv3#2%)Lvn!$;o>LKBBMFC zh=w^ySez2w3zFnLPo{o*x_PRM)GGle702Xzj!C80_i*Iuvy8qYFoyed{iDAq$~f6f zx3;cC5q!DWg5}OZsvJf5o?=}^?Dv9XW%hd3)8{@{o<>DJ5}dAsir)gEqo#uDhz+Bx z&V@Qk62KWRbtjSD%IWn|0^!v5$n-MKK%?P#)?qYH#b?@lo=AD_5>CskQAC8=B<2ca zY2u(ov!+ZYBdHP?jYM(1;css|jDI#UBULj}{MKN+FK~pcBm=l+c&5$0xj?0leC9b; zMRJc_Br3FID=~7mk5Jjmj)hSMWQH3WkF-}sog)mPxlxkQ%=o7n$nk5^J`*rVy@&q1 z5?i?^#htXFj$&%!C>b7{1rNJ9D2_kXsZUuF_ey~uTMJk3LcxCQ3JfIGb6P)C^9yrc zawRSki(0RI?oK*;*>Bd3>GqNtA&vwV_yYTS!kj*3`F;@FWV)9G{(MUU3?wxXL4|5` z2Ta@w2&t8Rg>zyVzQG^|2=mCIX`UC|9s!sMO4?;k8Z=Uts$aF zvNi17#Y-T7P__rn9u5x|GVNXlj6|2Q*PWBmsm26Tlvx9%FT5PI(OMUmMm( zzbzA-0ZHOoK!-N&r<)UHybRh`sV=bJgLl-%y{l2b?BD#fs>8@epOjOH$h5=!!&yAo zluXE}qzmv@!5X^ioozsO4g&xg_=%h9kW`bx5nV#~_2`?#Q||>X z6_m8vLSL{-{9?heEm@sX7RT1HqhUA<+OIDRWQ$Xua=Bk11UMIF%ioEsv+itM3paom9A}Gv;3(vZ6hN)MLhVv9mkGqG zq}uMIsVAymuY)GzZoHQVCF@CykbX8+Pfkw<;}5-|4)a}jEQJ^H;;dF< zA4^I0C8Gq7c{v;Wh${OlEk4R5jmXA?2a;q{LgDJ|Yef~Fv20Wim4JF(?%~$n$wkwg zp-sAtwgZypV9)-e`WAHRp^Q{M(Xk0ORd1y-q-A@|zQ`%`#e^*K0NRhH$%I^=nm5jpYW_ z?M}7pO9l7cf)Rt(@-dr?Ii$$O2$KX&o8ReeXnAB6PE%}@L$ zK*4!)=KzM2*vS&D1kHuE?ea#lPPi3LM{CYEiu!K9k{JqLURCe+V3X2cgE3y7CwtBo4s|rObcs}KIR5$LP-EH}cs?_52@G5;LhBw3fWwRL)v zwsm#5;sQty_Q4E}9fUU@8@k5_RJdKw(p2DI^5a!3lLl*=l9pfXsoFqY=O^G$s0A@4 zOzk4(Io*!2l^k=1CZG9s*LVgA3FKU@=|NcPqxs=~3c1k!*88o3J@~wxBc4Og(x#M% zS_!YFn!=>Iwk(zpaO*D3djP|b?rQXOaw-e*G58NXQk*bM6sO>>hLgg|ItiIfznaI- zZzQ<^vlZ1yLcwos|1!zhT}M4UxzGo6kSlNB1bSqCv_d7Xx@$$d)C1@Xr!E0|81(`Y zW~<#Q0PVsufS`$x02TK_NPgV`JJbgCpNM)!f+#)<-a9^?gq*CCF%i^!EkasdNwZR) z&0N{>>UiEHH)LmSIv35*tFLCU*eYYP*5%E&WlMY7PS<-nge{3f*iYa|!r4=M&03@z zR3==zB}d9z?Z_UwiuWXO^VaA>GJfLmlnpfiigq*e-rtK|av^$G6PsF&T{oxBW=aLk z5^|5LF_{SOOpzK_?Ma|2+XEFo2om@hm(s^t=T$QcwY(&r^Wm5M=Fh*{PyMujXf0iI z&?jM&>rPswgy=W*us01^ZpY@VmHldlyy!X!U3gF}{xqc;UPG%RIONq28c@++2$F*D>F5XcM(f0G;+SVo5fAw}DlB!W zsbRwRdu1K}azpfH1nuoG(UynCaJ{%3#S~n$%5yoNyC5F19@^Xv+yb1z`HH)D@CSM7 z%BvEEqd*l*ue!Vh+_d@sxb6|40Q}|+3nEKPue0&B-Xc2Zt?Cb+sxfV_oXN3op@h#` zcm37(g5lV8e8&;R;fpzYvNM|6U&J%aQwWMq1d&2Mmd2tBEkLgM%y@O{2xc8_=6o2}~2_=Btw1cty`(|NUfRU=qxlucx*{8}b zi|s%@BOt+?sYUV)2Qs-Wd%Bsh871bMxcXa?@v z@v7=pKE!>ixIT!aGE3-Xfm=Fv^#p_|yGsHCBJJ=BHw}3JOHV-_P>%fA|92ynnLj2p ztqb{ZeG>E+(;fY-&7E*SBrt`W@Gt}}C&bW|E6PhYl#}hXC#=gp;0cfX$N03b`b}?- z4Cf2}zoIEofUWToKmdR;p#KL=xj0)`JJIXuS=d@Q>*@VNQzi-1wwnwH!MDDV0k9~L z`jY_xx?9MX_{rsj;Hil$Vw#!~AjXQM6=S}iwbrEKbBB&$O}kgJ@jSJ^Q>`7PDK>Pn zX)5C9IH_+G4r$v)_w{eFZyL2FeU^UiTlm6MuZ%qc)@_dD|Dp>-;TLfPVrzbs6w9 z4?pp*>>FrhFMy1IHRY@ec%k6Hxy+V8%!8(KGc~D`5V>$GFVp*r!+daQt{5p)saM#w zQS)5n_o1{y=4ehIYcGjhKx3+@PoHIuPPzYX42_=wB%toaf7MhO~+NLx!&7E%37Vn|CG(x5nA8P5x>nFe5|((j#w_NEy=W zha#o_+MtxWL;C~=%C<;X@ZM|gw4Y6mEg|80_gW*Dl;NvGMrT{?xp{;xy#xF0!g>Hu zUce8qCf+B}P8p3Vqy^1(M=+xTLN~=ee&Vz4oSjP_#X(T3X}^LZMgNUFHUZkl{lNqP zsIme8_y-^XY+WskEe!M=4b15O_v5J9X(^C33oaea_~#)V-`$*_vu0 zmzJZ1qoWoTDilc=iXp-tP`=eXvfC&--o| zxktaAa#a|Y-d3yHmk_t<2YFPP(93j_-Ot2~FOQFh=ag=?UKejKM-Lb8AEV!aSUFi) zy*xaO)sgFXqBczzDF)eYNSDQG$UXM)sdmUa0qyntXih<)zzAnMI9Ogkrk2_5Y}|uwJjuK40%IbFr_lnWTeJqkKwIanvP`A5 zgmbY8Y2R7($V!Vsw5s&*%3H?CpL!^*c^vO>!6;z>_Phq`tE@JjzbEDHj$U&cO)EHoQWx;hSb&_{+D{8MfD z&#yU^5aQ^_>4uQ52K4Wqav8(4Qht?obrYyr)J5znY5bsSm+lFytTFJ}G=O9R{Ue)I zMX``FQ+VN-P8PbyXWz-CBh_X>x$$C$W&Fu|_9}sfwZ`;HpDC*m`sgls&?zL8z~rT~ zs?4gC`ATt8n6qZeBhHY+#OPJ=rn?H#<7#A6L1LwD~xz{&nW)iJ@OY|O}i@5$TS8ha9A%r)mPHPV{4V; zzCPo-)nHopEIN;@=|(o>E8S|V%oE1X+%^z}W#+QnBZ|ex1el*xfOBt9u6Ws187*2^ z{V(RaCQ|7Ut@+>xGO$+Y7z&|nARh|{$9&0t0_1;tOK~P(hd)|@+A{^YWs2eQh$B~G z8j`}{SS3VIe#|^7$a3iVDp&}MF7qeU2w@f~DB9xAOmAor)ujLfxR*5)SC~*I4K+=T z0?8JY;bhj(CndG~h6vLq1=daQntxE+4A5@%y(vIrmjzeBfaUL?jyS9|pPx@lsg_?0 zHEHw-o|`PeNCMa>*NrEPpQEZN>7Zx7bs`{1U%5P$>yb~V{PtbG%3Dr%D z`t2F|*1-XJ6bpOr5bvVK3=bBd&4osby8>Q_lW$x0a zqo}L^1I+f+!>;abb7GAGqZ6&gcSc*49*yePfc(RDtS~^oBLP7y$;6kY2O)aL`;qF> zUdmx36EU-f`C$gUa(S?!c<4CF#ViT}m8s-FEv*V1ps&;&E#!rYEqv$D?FkqhGyt6% z=n@h@>5X7z;o=^{UOG=edk5_)Ns}A4=k`Dt{PsXOoUhXp!pX9Nnt-rDT`j1lY8+#* zviJNmiN7FHaR?1GV-?yD#s&$o_Lro1?jM7{0gU`vd4X@Kx(jPTW=RQxb_YCzczi7n zm?9EEZz_H3zd2`^C$`2cW9>z{>t8X;^FrrV;QI|AB&E+_T)LxW4)GCh)m(IziCv%T zk@Ujoa0dN~Qu^Kq%;Cz?{Vm4b{#;Kue-3qI*9FPB8i}#tto$u!c(W^S<*15=Fj8_V#-l%M#_zit`){#D)2W>d?fiRWd{8c zPm3KwH(>NJ%iX_FUvL}hdOZE&c4_9lf%Y$biO=cOvo0!$S854y96>cv1O^EsrFZm? zf*%4!h?&B!)FQq1^#(Ty4@HCqBi-SD4x!*i=H+8}ppmRW@NybnCJCFVqGS;i@oh>mDj4K*o^jRZfi zRw9X`Bm34PQG@kl1y9f{6h3QKXb5|$6S@lH+ra3o-ryrPEr~Q)af*FTSfD@WjZ`1J zs&dAF#qp-?6XAw&YM3xM9DK?wMZ1K}MBK^JNyEd+A<}F@0Vx7m#)0Q0HD$yQ79iI} z`ynWyz*OQn&8ZgAOtn|ZAviF(9kF$UX0glmk@tp?RQF8-a#8bh_Bb635$YfaXbSqC zfdI$lmx|FLF_m zR1r}8xIyc`5J8Ja?{;Hiexrx{i{uvm%)S|S*W)$|!-&P32e5-$vKyMjGz$a}dJ19U z6TQsK_c;6mJe2zVo*S0g@Cg9^&@(vINHf|-C~NV51Y`RqJPDowy$G-=7S+)qT`cRB zaK@YUS++I`3&=#H5e^U+bJkatA6TPucX-(^Fks9;My(qont3UZ&Vj}?m?z{LFE751 zIfJn|mWN(CPdS>zVWr9E3o*P1(Hjj-^HGC;`%1e@1`e#^l{9jVt9TGEb?RHC_UH(X zSlK6$w&S>DPF>{)kzx~AtBu4MAiX*FhqA|19xW1_pg4&IMyZ!;BWMoiLfAtSx9zl!bR+O48Yeyz5V%$Z-rW1E#qRwE_1CNCAgcUXhUH0N?fpcVQ}+WV0yIk1 zm)bR?GOq1`tFXffEg-#we}%=uZyB-27FAYuDrq2{A=y^iS(EG6{ZIrZF^B=8%8#O# zP7dC@G#O}(XT{xcJ8q7MlR5(0>jwLnr6(q=dn#D2eOd<|%J{UGQ3>G4#8R?M!bW9M zmI`!;s1{|oyLj{zhSV`0gkk4Pa1T2AlhN~(7&z&ftlkUKubfmwYMLxbdwmdybYc*&uylF*vc!1>abe%BtW%zP9@p%7DqjRgQL0jsTP&Bs%) zMtm7rl};%4-E49nj2~~MFB!_EP!w=%5}zqt+;o*K17O{#yB}&&?bY9i{~?Zzbpc@2 zw00-*c~Qj`RI>JM!`H2Hv@dMD=n5;(puj@AQcmSh2PREV=Z8W?I0>5}xo1P9L~Bpj znGfkbK(~8Jom#VbA#Z!1&jvBpKszl)t}*Y=aF~#ni4KnzzRI9=yiG{K6b2xDjRH6W z+jb2@xLX{%gUcx?%5`)=Y@SF4dF@BOGAEK zE1&F|zGn_}5^IQv9LAwTqXP+rn^;XHdgWkb6C&Z+O*#Brlv--LB1JMjrSC;Dp^Oer z?uRvZ=IU@s$8l(Hs6!%e?H7vxnc4U!wL>Eblh6?3T1{K+)DRH z0U&TwbQ%|R`UIn6CNa&O!e6ivulPuJo(GUJWJWjoIyf?5ZEjF;B&@)Qy}^?m5FAd5`vo{?x7 zM*7z|;Q?MIM=(dva3bSP=@HW^(WE+C#Y)XC=4{+Ip9dl=1>)$Tq63dChody&CJ-j) zxbb9$MkBpWX0M%b2B^OE4qiS4$HOfcvMP%;wd!cpdAitP;I?Pm@g1>KHH)7Pehp(d zj*habK4ffwOnclH1cPpxI}%XRQ1GBVGH8GsF1K&-s{7S2Ig%lMfK9)CQ*<{ z_Z5tISrYN|+?!FkTIc>e+M8k*BCL~Zn5nw0>ALr&fb(H;sN2#~nb`-69e-Py`-*c< z=rah69o%YsJ}i4BxNYlcV2G{o@t5MFnJJ{uqr$K88`}1qs27n>M+kN{zL(;J2}s}m znGMfEKF&C;U@;9rd8@yu&aF(R z(qxy0DQ&s{Jge8u7weF#5p5Xnx@tVu)xSA+iof|zLU5aqjeqVHt|X1K@t@o6eb z;`QmCXG;Aln#3)7wDRax-+U7=?%za(2Ut*bGA~sgNPFJ_%kGY_5{c~-jf6~ntq`!2 zCkr_#bZvQ>t>A!V7(}x5`X+{p!)mX)YEYU282P*tfBah<9u<80AsG|T{y1+7%v*S7 z2*G4TQ3!hs(61}!)3*p1Af33mTd4|a2Rku`Rd%yt%iUe=D=-`R6HeH|1uhEQ03DkgFlh@Rv`cSjY_% z#}aL?^|{U&0D%tON;t4gYMep$YU)GrO+t=hg4CYkSV|0dwRgIIK3HyUCN-v2ctHBR z#-7Bz2ZmsoY?&EHyToAOa?oXsBukucbBBctBfn6mkf`nGPOzuxM^^Va)%+7pS|dMs zzsj3_&NosD>)*mr05KU7s_ zWn?_jfkS?Ob_897I57#Z&_Cqgz-SQR5hX=`A@3s~P-k>lA}QW$<7z+x>L#gi5Ny8y z9-9J(tCr$B1h=BlTeYx%0$^a%2@M-j-KpQ4>YsoZaB7ec_(L7a=1J!sxtcFIbLYcs zqo+Xx!8L2wE2edF6Rw4|V}Njd@^k7v-O24cAOrP7FdB*AW@ba(0MHi8J}T${I%~JD zwsCHn3}W?HogX^X&cHrot-S+reX3CFn2y<=2Cz?2CTwpY=?qBHYKTLWzJ zRf5bp6-(wu8Fj%r*=KaidE#5*lKPyX2#$fH?Gy2vZv5LDMLL&N@>5FC4eIVfJUSEj zFU=f#{6`A>jC^_o+j0L2O3w%Xsn4>(Z+}3=vwOL5Xh7Xf!yt`XdjwCVkj>@4<0XsQ z7pr_{qfvfYKwU7*M3g%@zAK+{-?&)xbQHT}io4Iy{3}?p6XDuwG7QWmQxlJdRl~`5 zWrgY4ZX1N^jZwZ-ZKfH4udo^PmYcM;lkudS`I2vSo7#USEAs0qp|aU^bGNR>{V9O# z49*p|+tQAGbfd2fGTw>2vRZriE9 zu~h!9PWl380B%QMJkzOPQVpcp2@N71qY;vNf&t6aWY1CN>`Cz$Z}aC+XRH7IN|5^& zbLYtj&-@PyW3~uy)+z^eR3tbH_DNi@I|`^L_GQ1K%U?i{{_I1xCnThR_Bsu51|SFO z88MCJOUOTh6D~&i@&>MWNF7j&I1q_>K8s9(2JE+X@R5jqUV9gcgqUp`g4K1^wvU-dFoAYv0;N+6`E!83knw87~tMcfgU@cr&&-& zM)ymR-bIaJ1=*!RM>pka#+B1OJyf%{G{WoM7#G))I8Ll3a6%S~Ei6kFqrKSds>Q#* zZ;G?0vm8H1*O%VmqF&SxpENQ(S43QBqrF{&R1*na8~3oX3@@2UIvTIKe|;#M<&AP{ zdJ$-u-N0M1nE#w!IO@H^wY3hsWibJDGK(OH6uX=$saOzq6m6_|y&W-uWD^m3ViUhD z0fXfF+U+sH!GN4JtQzQOkjjs6H`3z;t0s#q;yDuCUa1><1NDsJUePXdA%4oPaOw7! zHOdn55M=C#a>;1j#Vp#d!N8OGZ?Hy z^Gs7tJWu$vfm^NyilXD7HdtqKw@#wvo)}SZvp(S}AY?w?^c_8PPNu@Kxww1w*WW%L zm}J_gYSxp_vsU=aRbrW7&>hwhr6txhVgtE9${Ak4uzd|aD78r3K-5od2on3cMMHah zPv}K-R-3n|EqUp88~*(P`$JW6zN7Khfl>w4+OqCWcTvqhrWGt|HuYDGnqBsE#ZiZ& z25Ab|hDbPhd)g;pM!V4>@qzrI&CMP8c42I?7jPl;z`A#hWMvB}#kPrp-{gUX`V3$X zI8VGkFEQN!TZq{;l!>OAoPQ_x*3+}rRNJJmc*lM)IoX^b9)dQlh$mS#^_oI1#^$-v z-=;CfgB)E|dvK#lT^ztJ0s@X^h zyG392we_4fgAaN-o7oIV`!b&7{U7)*)(iMDHEs1Cw{8&nw*g~uS`J>8Zg#!Z*YicI zm^S)gQH|-{#SMs-+Q;?TAniz+BxB^__yS{W4o8;kfHJ$yJO+MgK~weMTh-lH35z{? zRq|reNcSEj&g-8t!>$1bMb7JD0!`R4ozXPh%EK^u3rma5w0z(=Civp|Vs+$_y>l>V z=B-`cQft`*;<~b#Y|&j~;V3XPbBWY!s_IOgpjo|C|9o{SKuO)eqWi~o)2^@r zcU|=jOE3YFoXAOqg*4|@f0U$W914fNGw`|ObhJz%R6XWq4j2|7Gqov$aW;{;l5kYu zaL<$Esc7)-<9+)1@gDXF_NKvN|Qi^>G3qq9?^Yb678YX?`^F0m#0C5}u0Kxw( zyV#hRn>suEx9#>?+tYDFtmSt%w;)n!o4=;N-juO%a9fH7aD5&Q46Tl7y`17-KJ}c4 zm=F^CPW$`KYt~}*EAo}mtIe=7B>wZ^>UQaDrK~HTVdMDT>PxZteNUse-SqNC?xUxB&kCL_*`c{@O^oPOuiEW{0!~GDJL6?tjxoc;Aw#O?*7l?D z=}~%_ff?r3i<_pl-6mTqscUCd>vA-Ut*(2p@*mCDWowNaMOji49iPcIHn; zMSL*++G_*E>C7rYGu(OWoh63ss8cRLt+($c4_9Yz@9h~#(_vdBlO*xOOo)Xp#&lAh zH(Sdv&s9b&*J7~A4iCLIv>MDe9D>FNkE2#etrpy^7kK?nXNm1brQ!*P^q-#cusw;MMy$<=jwn_E^X-puzJAim+q>YG}cM_M;F|J;M%-#X#G2lrVY zh)Lf$wv?%_x;g)%X#8cY?ZefX0L@bRaD?kV<}bxF^1)+XB~K`n@#^4~RT{_qPdM&1 zy@xXapZ3(W$aCDxG4v<=^cF#3&v5FBZYLQT88j~nAGZC>p2n|LqR|kFXCSb(9ajtl zSkLL9M$lK7)hz=>kn>d%!WfW<7>CK3_nWu;Ej)vMiV)EDiysdlE5?YnX}L48{};H z()}d?j|mE5Gw0N3bJC)Xu4V1AZSWQ1j3Tdvc$GpGg0?)oM8UbWv=yII7Bd(fHjl*c zf9BHD!*iG)ij6YYYG$Ot`wsg_SbRkCX?x*DtZF^#vqdchw&3K!TQC{0X<)GCd6vDK znjL&JNNf>SYN=@7gLPYucArqNulx5Rk%3)IfPjU;Q*i)lUd(g_13C)Bt5$M=lG&N& z#3Q_yqOYiU^0_Yk^S(M}23UI?*1>TMd3s)nd(`+?Gxgo~8IjTc84{DpdGtO4VuoF} zzO#aqaDy5=q3`KGF)9tC;wl{*GtE$A#Q1TSuNzY7JCusoCY(Y3r3$1q=YDx^XF!*1 zUra|*20Lz7rc)ti_m$Ms(BsEKG8`r5CDtDqD^G3BL$n~QMWgfqRv|86!4f(d*RXWF z@+|3`0u1c^Ag_WGu+=&RL?<>% zNI>G;elHw4qA%WPVADw7I!-)$VGBQKbSJM{QL}QS8Jc^uHJ5$}BK@!Sr*HHje$d8z zG0u*#d3t5*eGI$+bXS3#$9*6LNr2%-nkm(ia5j@JsAxF1gLj^x5>_Xz(k-4wx`=K?!3oBjo+V}(9sb9KMx1OLHK(*xrLMh z`+GKx%aH9`k?8e6gd7Zq9hOz6@R~bG8~Vh=eNFZ^{yjVrf&ZdBF9vvNWB6{`EFEf* z8CL=S>F$U@SeO(~{~AU%MQ13D?5Opbki=OUNlk(@S;(FXBFz3QT z=(%**ZrGFlfo*&VBtWcPuBrb%_qlArCf&={5I{GnN)HC`0x4s|6rfrDM&%;9_s_)y zz(^VCEl>kSvK`IYuS9W6)g8&?jU4DI=v@Wpi8aucBi{g@Q1iuCPSiq*{2@+lGW9Rt zGF~eW4tyVcRW@Q%GJX8pv1xt7F9vjsfhI;lk7)L`v?*EU+|NfsKDiS>eTeEd<6Abs zlpeE-A{#`FzYE#6VQ>nSKf4_Y!Z=Vtuvkg>T{#}Ta;q)vsl0^ikU&^87K&I(>==L` zwOE&;cqMV|u$DFQQy8V~a>}m4aMKR$A;zI+yyVHGlxOl+CrBed0ug@^ zHc*rMq#QxIZ8IJ1LPgkZSdE)RuRlIkfcku3eIir`sL73LoDeBzIIoZy2Pp~_HjG#t zH5x==U0@XnV7Kn%FFQ2#hAw_%QGuVBspjO4I-_wf0_e(CxL^ zi_D9g0(}5X#bRu-0|NC{18!IRv(YkL-SX<0Hfxv~@?H!6@(qsbeDs&ymG5}Da#WI* z&aoO<_xjbg8z=byXZ-TYhy>m&f4Ras_)nZfGEH&&V-S(LI61XaL=OuK`C+&w%$Se- z4tg_NpZ2`gN*8eSOXw}e^z!LR!6o9z>3BG$W?uiN=t2^Ib$Ku_DggNK7eWIR0@Rys zQKdiZOQ}SD2qFMF#KmGdGlz)hvX1>MT_c-Ft93lNu~iV~SBU4?+XBpFPzAG+`z3vV z9N!F9_k1;Hhk}XYwMP21hpL2c8D-)7=Dn592-eO-#qk8cpC*tIke≧@UiucE5q| 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 literal 0 HcmV?d00001 From b3a6508aa554bb72e393d93320dd46a3f7518eca Mon Sep 17 00:00:00 2001 From: Eric Evans <194135482+ericevans-nv@users.noreply.github.com> Date: Thu, 15 Jan 2026 12:36:16 -0600 Subject: [PATCH 03/15] Add rag_lib subpackage to support NVIDIA RAG Blueprint library with proper testing Signed-off-by: Eric Evans <194135482+ericevans-nv@users.noreply.github.com> --- packages/nvidia_nat_all/pyproject.toml | 2 + .../nvidia_nat_rag_lib/LICENSE-3rd-party.txt | 1 + packages/nvidia_nat_rag_lib/LICENSE.md | 1 + packages/nvidia_nat_rag_lib/pyproject.toml | 12 +- .../nvidia_nat_rag_lib/src/nat/meta/pypi.md | 42 +- .../src/nat/plugins/rag_lib/__init__.py | 14 + .../src/nat/plugins/rag_lib/client.py | 403 ++++------- .../src/nat/plugins/rag_lib/config.py | 74 ++ .../src/nat/plugins/rag_lib/register.py | 2 +- .../nvidia_nat_rag_lib/tests/test_config.yml | 56 -- .../tests/test_rag_lib_function.py | 663 +++++++++++++++--- 11 files changed, 825 insertions(+), 445 deletions(-) create mode 120000 packages/nvidia_nat_rag_lib/LICENSE-3rd-party.txt create mode 120000 packages/nvidia_nat_rag_lib/LICENSE.md create mode 100644 packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/config.py delete mode 100644 packages/nvidia_nat_rag_lib/tests/test_config.yml diff --git a/packages/nvidia_nat_all/pyproject.toml b/packages/nvidia_nat_all/pyproject.toml index 13f78c3dc9..cccb2b8a47 100644 --- a/packages/nvidia_nat_all/pyproject.toml +++ b/packages/nvidia_nat_all/pyproject.toml @@ -36,6 +36,7 @@ dependencies = [ "nvidia-nat-opentelemetry", "nvidia-nat-phoenix", "nvidia-nat-profiling", + "nvidia-nat-rag-lib", # nvidia-nat-ragaai cannot be part of all due to conflicts with nvidia-nat-strands # "nvidia-nat-ragaai", "nvidia-nat-redis", @@ -86,6 +87,7 @@ nvidia-nat-openpipe-art = { workspace = true } nvidia-nat-opentelemetry = { workspace = true } nvidia-nat-phoenix = { workspace = true } nvidia-nat-profiling = { workspace = true } +nvidia-nat-rag-lib = { workspace = true } nvidia-nat-ragaai = { workspace = true } nvidia-nat-redis = { workspace = true } nvidia-nat-s3 = { workspace = true } diff --git a/packages/nvidia_nat_rag_lib/LICENSE-3rd-party.txt b/packages/nvidia_nat_rag_lib/LICENSE-3rd-party.txt new file mode 120000 index 0000000000..bab0d1f8a7 --- /dev/null +++ b/packages/nvidia_nat_rag_lib/LICENSE-3rd-party.txt @@ -0,0 +1 @@ +../../LICENSE-3rd-party.txt \ No newline at end of file diff --git a/packages/nvidia_nat_rag_lib/LICENSE.md b/packages/nvidia_nat_rag_lib/LICENSE.md new file mode 120000 index 0000000000..f0608a63ae --- /dev/null +++ b/packages/nvidia_nat_rag_lib/LICENSE.md @@ -0,0 +1 @@ +../../LICENSE.md \ No newline at end of file diff --git a/packages/nvidia_nat_rag_lib/pyproject.toml b/packages/nvidia_nat_rag_lib/pyproject.toml index a9c763912c..1c6e2bc885 100644 --- a/packages/nvidia_nat_rag_lib/pyproject.toml +++ b/packages/nvidia_nat_rag_lib/pyproject.toml @@ -9,7 +9,7 @@ include = ["nat.*"] [tool.setuptools_scm] -git_describe_command = ["git", "describe", "--long", "--first-parent"] +git_describe_command = "git describe --long --first-parent" root = "../.." @@ -17,11 +17,14 @@ root = "../.." name = "nvidia-nat-rag-lib" dynamic = ["version"] dependencies = [ - "nvidia-nat~=1.4", - "nvidia-rag>=2.4.0", + # Keep package version constraints as open as possible to avoid conflicts with other packages. Always define a minimum + # version when adding a new package. If unsure, default to using `~=` instead of `==`. Does not apply to nvidia-nat packages. + # Keep sorted!!! + "nvidia-nat~=1.5", + "nvidia-rag>=2.4.0", # TODO: Update version constraint when nvidia-rag is published to PyPI ] requires-python = ">=3.11,<3.14" -description = "Subpackage for NVIDIA RAG library integration in NeMo Agent Toolkit" +description = "Subpackage for NVIDIA RAG library in NeMo Agent toolkit" readme = "src/nat/meta/pypi.md" keywords = ["ai", "rag", "agents", "retrieval"] license = { text = "Apache-2.0" } @@ -46,7 +49,6 @@ config-settings = { editable_mode = "compat" } [tool.uv.sources] nvidia-nat = { workspace = true } -nvidia-rag = { path = "vendor/nvidia_rag-2.4.0.dev0-py3-none-any.whl" } # TODO EE: Remove this local path override once nvidia-rag>=2.4.0 is published to PyPI [project.entry-points.'nat.components'] diff --git a/packages/nvidia_nat_rag_lib/src/nat/meta/pypi.md b/packages/nvidia_nat_rag_lib/src/nat/meta/pypi.md index bea33ba21f..13074c85c1 100644 --- a/packages/nvidia_nat_rag_lib/src/nat/meta/pypi.md +++ b/packages/nvidia_nat_rag_lib/src/nat/meta/pypi.md @@ -1,23 +1,35 @@ -# NVIDIA RAG Library Integration + -- Direct access to NVIDIA RAG Library functionality -- Configurable environment setup and prerequisites verification -- Retry logic for robust operation -- Support for various deployment modes (on-premises, hosted, mixed) -- Integration with NAT's component reference system +![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") -## Installation +# NVIDIA NeMo Agent Toolkit RAG Library Subpackage -Install as part of the NeMo Agent Toolkit: +Subpackage for NVIDIA RAG library integration in NeMo Agent toolkit. -```bash -pip install "nvidia-nat[rag-lib]" -``` +This package provides integration with the NVIDIA RAG Blueprint library, allowing NeMo Agent toolkit workflows to use retrieval-augmented generation capabilities with flexible configuration. + +## Features -## Usage +- RAG generation and semantic search over vector stores +- Query rewriting and query decomposition for improved retrieval +- Reranking for higher quality results +- Filter expression generation for metadata filtering +- Multimodal support with VLM inference +- Citation generation and guardrails -Configure the RAG library in your workflow YAML file and use it as a function in your agent setup. +For more information about the NVIDIA NeMo Agent toolkit, please visit the [NeMo Agent toolkit GitHub Repo](https://github.com/NVIDIA/NeMo-Agent-Toolkit). diff --git a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/__init__.py b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/__init__.py index e69de29bb2..bcd923c929 100644 --- a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/__init__.py +++ b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/__init__.py @@ -0,0 +1,14 @@ +# 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. diff --git a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/client.py b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/client.py index 58e397f871..09c31e5484 100644 --- a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/client.py +++ b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/client.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# 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"); @@ -13,294 +13,179 @@ # See the License for the specific language governing permissions and # limitations under the License. -import asyncio import logging -import os -from pathlib import Path -from typing import Literal - +from logging import Logger + +from nvidia_rag.utils.configuration import EmbeddingConfig as NvidiaRAGEmbeddingConfig +from nvidia_rag.utils.configuration import FilterExpressionGeneratorConfig as NvidiaRAGFilterGeneratorConfig +from nvidia_rag.utils.configuration import LLMConfig as NvidiaRAGLLMConfig +from nvidia_rag.utils.configuration import NvidiaRAGConfig +from nvidia_rag.utils.configuration import QueryDecompositionConfig as NvidiaRAGQueryDecompositionConfig +from nvidia_rag.utils.configuration import QueryRewriterConfig as NvidiaRAGQueryRewriterConfig +from nvidia_rag.utils.configuration import ReflectionConfig as NvidiaRAGReflectionConfig +from nvidia_rag.utils.configuration import VectorStoreConfig as NvidiaRAGVectorStoreConfig +from nvidia_rag.utils.configuration import VLMConfig as NvidiaRAGVLMConfig from pydantic import Field -from pydantic import HttpUrl +from pydantic import SecretStr from nat.builder.builder import Builder from nat.cli.register_workflow import register_function from nat.data_models.component_ref import EmbedderRef from nat.data_models.component_ref import LLMRef +from nat.data_models.component_ref import RetrieverRef from nat.data_models.function import FunctionBaseConfig -from nat.data_models.retry_mixin import RetryMixin +from nat.embedder.nim_embedder import NIMEmbedderModelConfig +from nat.llm.nim_llm import NIMModelConfig +from nat.plugins.rag_lib.config import EmbedderConfigType +from nat.plugins.rag_lib.config import LLMConfigType +from nat.plugins.rag_lib.config import RAGPipelineConfig +from nat.plugins.rag_lib.config import RetrieverConfigType +from nat.retriever.milvus.register import MilvusRetrieverConfig +from nat.retriever.nemo_retriever.register import NemoRetrieverConfig -logger = logging.getLogger(__name__) +logger: Logger = logging.getLogger(__name__) -class NvidiaRAGLibConfig(FunctionBaseConfig, RetryMixin, name="nvidia_rag_lib"): - """Configuration for NVIDIA RAG Library integration. +class NvidiaRAGLibConfig(FunctionBaseConfig, name="nvidia_rag_lib"): + """Configuration for NVIDIA RAG Library. - This configuration manages the setup and instantiation of the NVIDIA RAG library, - providing retrieval-augmented generation capabilities including document ingestion, - vector search, reranking, and response generation. The configuration handles - environment setup, model references, deployment modes, and operational parameters. + All component configs are optional - NvidiaRAGConfig provides defaults. """ - # Core RAG configuration - vdb_endpoint: HttpUrl = Field(default=HttpUrl("http://localhost:19530"), description="Vector database endpoint URL") - reranker_top_k: int = Field(default=10, description="Number of top results to rerank") - vdb_top_k: int = Field(default=100, description="Number of top results from vector database") - collection_names: list[str] | None = Field(default=None, description="List of collection names to use for queries") - - # Document processing - chunk_size: int = Field(default=512, description="Size of document chunks for processing") - chunk_overlap: int = Field(default=150, description="Overlap between document chunks") - generate_summary: bool = Field(default=False, description="Whether to generate document summaries") - blocking_upload: bool = Field(default=False, description="Whether to use blocking upload for documents") - - # Infrastructure - vectorstore_gpu_device_id: str | None = Field(default="0", description="GPU device ID for vector store") - model_directory: str | None = Field(default="~/.cache/model-cache", description="Directory for model cache") - - # Model configuration (NAT component references) - llm_name: LLMRef | None = Field(default=None, description="Reference to the LLM to use for responses") - embedder_name: EmbedderRef | None = Field(default=None, - description="Reference to the embedder to use for embeddings") - ranking_modelname: str | None = Field(default=None, description="Name of the ranking model") - - # Service endpoints - app_embeddings_serverurl: str | None = Field(default="", description="Embeddings service URL") - app_llm_serverurl: str | None = Field(default="", description="LLM service URL") - app_ranking_serverurl: str | None = Field(default=None, description="Ranking service URL") - - # Deployment and operational - deployment_mode: Literal["on_prem", "hosted", "mixed"] = Field(default="hosted", - description="Deployment mode for the RAG system") - timeout: float | None = Field(default=60.0, description="Timeout for operations in seconds") - - # Setup and management options - env_library_path: str | None = Field(default=None, description="Path to .env_library file for environment setup") - use_accuracy_profile: bool = Field(default=False, description="Load accuracy profile settings") - use_perf_profile: bool = Field(default=False, description="Load performance profile settings") - verify_prerequisites: bool = Field(default=True, description="Verify prerequisites before initialization") - health_check_dependencies: bool = Field(default=True, description="Perform health check on dependent services") - health_check_timeout: float = Field(default=30.0, description="Timeout in seconds for health check operations") - - -async def _load_env_library(config: NvidiaRAGLibConfig) -> None: - """Load environment variables from a specified .env_library file. - - This function loads environment variables from an external environment - library file if specified in the configuration. This allows for - centralized environment management across multiple RAG deployments. - - Args: - config: Configuration containing the path to the environment library file - """ - if not config.env_library_path: + llm: LLMConfigType = Field(default=None, description="LLM configuration") + embedder: EmbedderConfigType = Field(default=None, description="Embedder configuration") + retriever: RetrieverConfigType = Field(default=None, description="Vector store configuration") + + rag_pipeline: RAGPipelineConfig = Field(default_factory=RAGPipelineConfig) + + +@register_function(config_type=NvidiaRAGLibConfig) # type: ignore[arg-type] +async def nvidia_rag_lib(config: NvidiaRAGLibConfig, builder: Builder): + """Initialize NVIDIA RAG with flexible config resolution.""" + try: + from nvidia_rag import NvidiaRAG + except ImportError as e: + raise ImportError("nvidia-rag package is not installed.") from e + + rag_config: NvidiaRAGConfig = await build_nvidia_rag_config(config, builder) + logger.info("NVIDIA RAG initialized") + yield NvidiaRAG(config=rag_config) + + +async def build_nvidia_rag_config(config: NvidiaRAGLibConfig, builder: Builder) -> NvidiaRAGConfig: + """Build NvidiaRAGConfig by resolving NAT refs/components to nvidia_rag configs.""" + + pipeline: RAGPipelineConfig = config.rag_pipeline + + # Create base config with pipeline settings and defaults + rag_config: NvidiaRAGConfig = NvidiaRAGConfig( + ranking=pipeline.ranking, + retriever=pipeline.search_settings, + vlm=pipeline.vlm or NvidiaRAGVLMConfig(), + query_rewriter=pipeline.query_rewriter or NvidiaRAGQueryRewriterConfig(), + filter_expression_generator=pipeline.filter_generator or NvidiaRAGFilterGeneratorConfig(), + query_decomposition=pipeline.query_decomposition or NvidiaRAGQueryDecompositionConfig(), + reflection=pipeline.reflection or NvidiaRAGReflectionConfig(), + enable_citations=pipeline.enable_citations, + enable_guardrails=pipeline.enable_guardrails, + enable_vlm_inference=pipeline.enable_vlm_inference, + vlm_to_llm_fallback=pipeline.vlm_to_llm_fallback, + default_confidence_threshold=pipeline.default_confidence_threshold, + ) + + # Resolve and map each component's fields (mutates rag_config) + await _resolve_llm_config(config.llm, builder, rag_config) + await _resolve_embedder_config(config.embedder, builder, rag_config) + await _resolve_retriever_config(config.retriever, builder, rag_config) + + return rag_config + + +async def _resolve_llm_config(llm: LLMConfigType, builder: Builder, rag_config: NvidiaRAGConfig) -> None: + """Resolve LLM config and map all fields to NvidiaRAGConfig.llm.""" + + if llm is None: return - env_path = Path(config.env_library_path).expanduser() - if not env_path.exists(): - logger.warning("Environment library file not found: %s", env_path) + if isinstance(llm, NvidiaRAGLLMConfig): + rag_config.llm = llm return - try: - from dotenv import load_dotenv - load_dotenv(env_path) - logger.info("Loaded environment library: %s", env_path) - except ImportError: - logger.warning("python-dotenv not available, skipping .env_library loading") + if isinstance(llm, LLMRef): + llm = builder.get_llm_config(llm) + + if isinstance(llm, NIMModelConfig): + rag_config.llm.model_name = llm.model_name + if llm.base_url: + rag_config.llm.server_url = llm.base_url + if llm.api_key: + rag_config.llm.api_key = llm.api_key + if llm.temperature is not None: + rag_config.llm.parameters.temperature = llm.temperature + if llm.top_p is not None: + rag_config.llm.parameters.top_p = llm.top_p + if llm.max_tokens is not None: + rag_config.llm.parameters.max_tokens = llm.max_tokens + return + raise ValueError(f"Unsupported LLM config type: {type(llm)}") -async def _setup_environment_variables(config: NvidiaRAGLibConfig, builder: Builder) -> None: - """Configure environment variables required by the NVIDIA RAG library. - This function sets up all necessary environment variables that the NVIDIA RAG - library expects, including vector database endpoints, model configurations, - document processing parameters, and infrastructure settings. Model names are - dynamically extracted from NAT component references when available. +async def _resolve_embedder_config(embedder: EmbedderConfigType, builder: Builder, rag_config: NvidiaRAGConfig) -> None: + """Resolve embedder config and map all fields to NvidiaRAGConfig.embeddings.""" - Args: - config: Configuration containing RAG parameters and component references - builder: NAT builder instance for accessing LLM and embedder configurations - """ - # Core configuration - if config.vdb_endpoint: - os.environ["VDB_ENDPOINT"] = str(config.vdb_endpoint) - - os.environ["RERANKER_TOP_K"] = str(config.reranker_top_k) - os.environ["VDB_TOP_K"] = str(config.vdb_top_k) - - if config.collection_names: - os.environ["COLLECTION_NAMES"] = ",".join(config.collection_names) - - # Document processing - os.environ["CHUNK_SIZE"] = str(config.chunk_size) - os.environ["CHUNK_OVERLAP"] = str(config.chunk_overlap) - os.environ["GENERATE_SUMMARY"] = str(config.generate_summary).lower() - os.environ["BLOCKING_UPLOAD"] = str(config.blocking_upload).lower() - - # Infrastructure - if config.vectorstore_gpu_device_id is not None: - os.environ["VECTORSTORE_GPU_DEVICE_ID"] = config.vectorstore_gpu_device_id - - if config.model_directory: - model_dir = Path(config.model_directory).expanduser() - os.environ["MODEL_DIRECTORY"] = str(model_dir) - - # Model names from NAT component references - if config.llm_name: - try: - llm_config = builder.get_llm_config(config.llm_name) - model_name = getattr(llm_config, 'model_name', None) - if model_name: - os.environ["APP_LLM_MODELNAME"] = str(model_name) - logger.debug("Set APP_LLM_MODELNAME from LLM reference: %s", model_name) - except Exception as e: - logger.warning("Failed to get LLM config for %s: %s", config.llm_name, e) - - if config.embedder_name: - try: - embedder_config = builder.get_embedder_config(config.embedder_name) - model_name = getattr(embedder_config, 'model_name', None) - if model_name: - os.environ["APP_EMBEDDINGS_MODELNAME"] = str(model_name) - logger.debug("Set APP_EMBEDDINGS_MODELNAME from embedder reference: %s", model_name) - except Exception as e: - logger.warning("Failed to get embedder config for %s: %s", config.embedder_name, e) - - if config.ranking_modelname: - os.environ["APP_RANKING_MODELNAME"] = config.ranking_modelname - - # Service URLs - if config.app_embeddings_serverurl is not None: - os.environ["APP_EMBEDDINGS_SERVERURL"] = config.app_embeddings_serverurl - if config.app_llm_serverurl is not None: - os.environ["APP_LLM_SERVERURL"] = config.app_llm_serverurl - if config.app_ranking_serverurl is not None: - os.environ["APP_RANKING_SERVERURL"] = config.app_ranking_serverurl - - # Deployment mode - os.environ["DEPLOYMENT_MODE"] = config.deployment_mode - - -async def _load_profiles(config: NvidiaRAGLibConfig) -> None: - """Load accuracy and performance optimization profiles. - - This function loads predefined environment configurations for accuracy - or performance optimization. These profiles contain environment variable - settings that optimize the RAG system for specific use cases. - - Args: - config: Configuration specifying which profiles to load - """ - if config.use_accuracy_profile: - accuracy_profile_path = Path("accuracy_profile.env") - if accuracy_profile_path.exists(): - try: - from dotenv import load_dotenv - load_dotenv(accuracy_profile_path) - logger.info("Loaded accuracy profile") - except ImportError: - logger.warning("python-dotenv not available, skipping accuracy profile") - else: - logger.warning("Accuracy profile file not found: %s", accuracy_profile_path) - - if config.use_perf_profile: - perf_profile_path = Path("perf_profile.env") - if perf_profile_path.exists(): - try: - from dotenv import load_dotenv - load_dotenv(perf_profile_path) - logger.info("Loaded performance profile") - except ImportError: - logger.warning("python-dotenv not available, skipping performance profile") - else: - logger.warning("Performance profile file not found: %s", perf_profile_path) - - -def _verify_prerequisites(config: NvidiaRAGLibConfig) -> None: - """Verify that all required prerequisites are met before initialization. - - This function checks that necessary directories exist, dependencies are - available, and the system is properly configured for RAG operations. - - Args: - config: Configuration containing prerequisite specifications - """ - # Check model directory - if config.model_directory: - model_dir = Path(config.model_directory).expanduser() - if not model_dir.exists(): - logger.warning("Model directory does not exist: %s", model_dir) + if embedder is None: + return + if isinstance(embedder, NvidiaRAGEmbeddingConfig): + rag_config.embeddings = embedder + return -@register_function(config_type=NvidiaRAGLibConfig) -async def nvidia_rag_lib(config: NvidiaRAGLibConfig, builder: Builder): - """ - Initialize and configure the NVIDIA RAG library client. + if isinstance(embedder, EmbedderRef): + embedder = builder.get_embedder_config(embedder) - This function orchestrates the complete setup process for the NVIDIA RAG library, - including environment configuration, profile loading, prerequisite verification, - and service health checks. It yields a query function that provides access to - the fully configured RAG system for retrieval-augmented generation operations. + if isinstance(embedder, NIMEmbedderModelConfig): + rag_config.embeddings.model_name = embedder.model_name + if embedder.base_url: + rag_config.embeddings.server_url = embedder.base_url + if embedder.api_key: + rag_config.embeddings.api_key = embedder.api_key + return - Args: - config: Configuration parameters for the NVIDIA RAG library - builder: NAT builder instance for accessing other components + raise ValueError(f"Unsupported embedder config type: {type(embedder)}") - Yields: - NvidiaRAG: Fully configured NVIDIA RAG library client instance - Raises: - ImportError: If the nvidia-rag library is not installed - RuntimeError: If required prerequisites are not met - """ - logger.info("Starting NVIDIA RAG setup...") +async def _resolve_retriever_config(retriever: RetrieverConfigType, builder: Builder, + rag_config: NvidiaRAGConfig) -> None: + """Resolve retriever config and map all fields to NvidiaRAGConfig.vector_store.""" - try: - # Step 1: Load .env_library if available - if config.env_library_path: - logger.debug("Loading environment library: %s", config.env_library_path) - await _load_env_library(config) - - # Step 2: Set up environment variables - logger.debug("Setting up environment variables...") - await _setup_environment_variables(config, builder) - - # Step 3: Load profiles if requested - if config.use_accuracy_profile or config.use_perf_profile: - logger.debug("Loading performance profiles...") - await _load_profiles(config) - - # Step 4: Verify prerequisites - if config.verify_prerequisites: - logger.debug("Verifying prerequisites...") - _verify_prerequisites(config) - - # Step 5: Import and instantiate the NVIDIA RAG library - logger.debug("Importing NVIDIA RAG library...") - from nvidia_rag import NvidiaRAG + if retriever is None: + return - rag = NvidiaRAG() + if isinstance(retriever, NvidiaRAGVectorStoreConfig): + rag_config.vector_store = retriever + return - # Step 6: Health check if requested - if config.health_check_dependencies: - logger.debug("Performing health check...") - try: - health_status = await asyncio.wait_for(rag.health(check_dependencies=True), - timeout=config.health_check_timeout) - logger.info("Health check completed: %s", health_status) - except TimeoutError: - logger.warning("Health check timed out after %ss, but continuing", config.health_check_timeout) - except Exception as e: - logger.warning("Health check failed, but continuing: %s", e) + if isinstance(retriever, RetrieverRef): + retriever = await builder.get_retriever_config(retriever) + + if isinstance(retriever, MilvusRetrieverConfig): + rag_config.vector_store.url = str(retriever.uri) + if retriever.collection_name: + rag_config.vector_store.default_collection_name = retriever.collection_name + if retriever.connection_args: + if "user" in retriever.connection_args: + rag_config.vector_store.username = retriever.connection_args["user"] + if "password" in retriever.connection_args: + rag_config.vector_store.password = SecretStr(retriever.connection_args["password"]) + return - # Yield the RAG instance - yield rag + if isinstance(retriever, NemoRetrieverConfig): + rag_config.vector_store.url = str(retriever.uri) + if retriever.collection_name: + rag_config.vector_store.default_collection_name = retriever.collection_name + if retriever.nvidia_api_key: + rag_config.vector_store.api_key = retriever.nvidia_api_key + return - except ImportError as e: - logger.error("nvidia_rag library not available. Install with: pip install nvidia-rag. Error: %s", e) - raise ImportError("nvidia-rag is required for this function. " - "Follow installation steps: pip install nvidia-rag --force-reinstall") from e - except Exception as e: - logger.error("Failed to set up NVIDIA RAG: %s", e) - raise - finally: - pass + raise ValueError(f"Unsupported retriever config type: {type(retriever)}") diff --git a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/config.py b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/config.py new file mode 100644 index 0000000000..954f7718a7 --- /dev/null +++ b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/config.py @@ -0,0 +1,74 @@ +# 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. +"""Configuration models and type aliases for NVIDIA RAG integration.""" + +from nvidia_rag.utils.configuration import EmbeddingConfig as NvidiaRAGEmbeddingConfig +from nvidia_rag.utils.configuration import FilterExpressionGeneratorConfig as NvidiaRAGFilterGeneratorConfig +from nvidia_rag.utils.configuration import LLMConfig as NvidiaRAGLLMConfig +from nvidia_rag.utils.configuration import QueryDecompositionConfig as NvidiaRAGQueryDecompositionConfig +from nvidia_rag.utils.configuration import QueryRewriterConfig as NvidiaRAGQueryRewriterConfig +from nvidia_rag.utils.configuration import RankingConfig as NvidiaRAGRankingConfig +from nvidia_rag.utils.configuration import ReflectionConfig as NvidiaRAGReflectionConfig +from nvidia_rag.utils.configuration import RetrieverConfig as NvidiaRAGRetrieverConfig +from nvidia_rag.utils.configuration import VectorStoreConfig as NvidiaRAGVectorStoreConfig +from nvidia_rag.utils.configuration import VLMConfig as NvidiaRAGVLMConfig +from pydantic import BaseModel +from pydantic import Field + +from nat.data_models.component_ref import EmbedderRef +from nat.data_models.component_ref import LLMRef +from nat.data_models.component_ref import RetrieverRef +from nat.embedder.nim_embedder import NIMEmbedderModelConfig +from nat.llm.nim_llm import NIMModelConfig +from nat.retriever.milvus.register import MilvusRetrieverConfig +from nat.retriever.nemo_retriever.register import NemoRetrieverConfig + +# Type aliases for component configuration +LLMConfigType = NIMModelConfig | NvidiaRAGLLMConfig | LLMRef | None + +EmbedderConfigType = NIMEmbedderModelConfig | NvidiaRAGEmbeddingConfig | EmbedderRef | None + +RetrieverConfigType = MilvusRetrieverConfig | NemoRetrieverConfig | NvidiaRAGVectorStoreConfig | RetrieverRef | None + + +class RAGPipelineConfig(BaseModel): + """Native nvidia_rag pipeline settings. + + Groups all RAG-specific settings that control search behavior, + query preprocessing, and response quality. + """ + + # Search behavior + search_settings: NvidiaRAGRetrieverConfig = Field( + default_factory=NvidiaRAGRetrieverConfig) # type: ignore[arg-type] + ranking: NvidiaRAGRankingConfig = Field(default_factory=NvidiaRAGRankingConfig) # type: ignore[arg-type] + + # Query preprocessing (optional) + query_rewriter: NvidiaRAGQueryRewriterConfig | None = None + filter_generator: NvidiaRAGFilterGeneratorConfig | None = None + query_decomposition: NvidiaRAGQueryDecompositionConfig | None = None + + # Response quality (optional) + reflection: NvidiaRAGReflectionConfig | None = None + + # Multimodal (optional) + vlm: NvidiaRAGVLMConfig | None = None + + # Pipeline flags + enable_citations: bool = True + enable_guardrails: bool = False + enable_vlm_inference: bool = False + vlm_to_llm_fallback: bool = True + default_confidence_threshold: float = 0.0 diff --git a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/register.py b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/register.py index 2be2d28d29..0a8992b620 100644 --- a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/register.py +++ b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/register.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# 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"); diff --git a/packages/nvidia_nat_rag_lib/tests/test_config.yml b/packages/nvidia_nat_rag_lib/tests/test_config.yml deleted file mode 100644 index dd87f3ca00..0000000000 --- a/packages/nvidia_nat_rag_lib/tests/test_config.yml +++ /dev/null @@ -1,56 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -# Test configuration for NVIDIA RAG Library integration -# This configuration is used as a pytest fixture for testing - -llms: - test_llm: - _type: nim - model_name: meta/llama-3.1-8b-instruct - temperature: 0.0 - max_tokens: 512 - -embedders: - test_embedder: - _type: nim - model_name: nvidia/nv-embedqa-e5-v5 - truncate: "END" - -functions: - rag_client: - _type: nvidia_rag_lib - # Core configuration - vdb_endpoint: "http://localhost:19530" - reranker_top_k: 5 - vdb_top_k: 50 - collection_names: ["test_collection"] - - # Document processing - chunk_size: 256 - chunk_overlap: 50 - generate_summary: false - blocking_upload: false - - # Model references - llm_name: test_llm - embedder_name: test_embedder - - # Infrastructure - vectorstore_gpu_device_id: "0" - model_directory: "/tmp/test-model-cache" - - # Service configuration - deployment_mode: "hosted" - timeout: 30.0 - - # Setup options (disabled for testing) - verify_prerequisites: false - health_check_dependencies: false - health_check_timeout: 10.0 - -workflow: - _type: react_agent - llm_name: test_llm - tool_names: [rag_client] - verbose: false diff --git a/packages/nvidia_nat_rag_lib/tests/test_rag_lib_function.py b/packages/nvidia_nat_rag_lib/tests/test_rag_lib_function.py index 760550067d..1a251abc9a 100644 --- a/packages/nvidia_nat_rag_lib/tests/test_rag_lib_function.py +++ b/packages/nvidia_nat_rag_lib/tests/test_rag_lib_function.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# 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"); @@ -12,125 +12,570 @@ # 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. -""" -Test suite for the NVIDIA RAG Library integration. -This module contains tests for the NVIDIA RAG Library function configuration, -registration, and basic functionality verification. -""" +from __future__ import annotations -from pathlib import Path +from unittest.mock import AsyncMock +from unittest.mock import MagicMock import pytest -import yaml +from pydantic import HttpUrl -from nat.data_models.config import Config +from nat.data_models.component_ref import EmbedderRef +from nat.data_models.component_ref import LLMRef +from nat.data_models.component_ref import RetrieverRef +from nat.embedder.nim_embedder import NIMEmbedderModelConfig +from nat.llm.nim_llm import NIMModelConfig +from nat.retriever.milvus.register import MilvusRetrieverConfig + +# ============================================================================= +# Fixtures +# ============================================================================= @pytest.fixture -def test_config(): - """Pytest fixture that loads the test configuration.""" - config_path = Path(__file__).parent / "test_config.yml" +def mock_builder() -> MagicMock: + """Create mock NAT builder with component resolution.""" + builder: MagicMock = MagicMock() + + def get_llm_config(_ref: LLMRef) -> NIMModelConfig: + return NIMModelConfig( + model_name="meta/llama-3.1-8b-instruct", + temperature=0.2, + top_p=0.95, + max_tokens=4096, + ) + + builder.get_llm_config = MagicMock(side_effect=get_llm_config) + + def get_embedder_config(ref: EmbedderRef) -> NIMEmbedderModelConfig: + return NIMEmbedderModelConfig(model_name="nvidia/nv-embedqa-e5-v5") + + builder.get_embedder_config = MagicMock(side_effect=get_embedder_config) + + async def get_retriever_config(_ref: RetrieverRef) -> MilvusRetrieverConfig: + return MilvusRetrieverConfig( + uri=HttpUrl("http://localhost:19530"), + collection_name="test_collection", + embedding_model="nvidia/nv-embedqa-e5-v5", + ) + + builder.get_retriever_config = AsyncMock(side_effect=get_retriever_config) + + return builder + + +# ============================================================================= +# Config Resolution Tests +# ============================================================================= + + +class TestConfigResolution: + """Test NvidiaRAGLibConfig translation to NvidiaRAGConfig.""" + + async def test_resolve_llm_from_ref(self, mock_builder: MagicMock) -> None: + """Test LLMRef from NvidiaRAGLibConfig resolves to NvidiaRAGConfig.llm.""" + + # NOTE: First nvidia_rag import takes ~20s due to module-level initialization. + from nvidia_rag.utils.configuration import NvidiaRAGConfig + + from nat.plugins.rag_lib.client import _resolve_llm_config + + rag_config: NvidiaRAGConfig = NvidiaRAGConfig() + await _resolve_llm_config(LLMRef("llm_nim"), mock_builder, rag_config) + + assert rag_config.llm.model_name == "meta/llama-3.1-8b-instruct" + assert rag_config.llm.parameters.temperature == 0.2 + assert rag_config.llm.parameters.top_p == 0.95 + assert rag_config.llm.parameters.max_tokens == 4096 + + async def test_resolve_llm_from_nim_config(self, mock_builder: MagicMock) -> None: + """Test NIMModelConfig from NvidiaRAGLibConfig resolves to NvidiaRAGConfig.llm.""" + from nvidia_rag.utils.configuration import NvidiaRAGConfig + + from nat.plugins.rag_lib.client import _resolve_llm_config + + nim_config: NIMModelConfig = NIMModelConfig( + model_name="meta/llama-3.1-8b-instruct", + base_url="http://nim:8000/v1", + api_key="direct-api-key", + temperature=0.7, + top_p=0.9, + max_tokens=2048, + ) + + rag_config: NvidiaRAGConfig = NvidiaRAGConfig() + await _resolve_llm_config(nim_config, mock_builder, rag_config) + + assert rag_config.llm.model_name == "meta/llama-3.1-8b-instruct" + assert rag_config.llm.server_url == "http://nim:8000/v1" + assert rag_config.llm.api_key.get_secret_value() == "direct-api-key" + assert rag_config.llm.parameters.temperature == 0.7 + assert rag_config.llm.parameters.top_p == 0.9 + assert rag_config.llm.parameters.max_tokens == 2048 + + async def test_resolve_llm_none_uses_defaults(self, mock_builder: MagicMock) -> None: + """Test None llm in NvidiaRAGLibConfig preserves NvidiaRAGConfig defaults.""" + from nvidia_rag.utils.configuration import LLMConfig + from nvidia_rag.utils.configuration import NvidiaRAGConfig + + from nat.plugins.rag_lib.client import _resolve_llm_config + + rag_config: NvidiaRAGConfig = NvidiaRAGConfig() + original_llm: LLMConfig = rag_config.llm + + await _resolve_llm_config(None, mock_builder, rag_config) + + assert rag_config.llm is original_llm + + async def test_resolve_llm_native_config_passthrough(self, mock_builder: MagicMock) -> None: + """Test NvidiaRAGLLMConfig passes through to NvidiaRAGConfig unchanged.""" + from nvidia_rag.utils.configuration import LLMConfig as NvidiaRAGLLMConfig + from nvidia_rag.utils.configuration import NvidiaRAGConfig + + from nat.plugins.rag_lib.client import _resolve_llm_config + + native_config: NvidiaRAGLLMConfig = NvidiaRAGLLMConfig( + model_name="custom/model", + server_url="http://custom:8000", + model_engine="custom-engine", + ) + + rag_config: NvidiaRAGConfig = NvidiaRAGConfig() + await _resolve_llm_config(native_config, mock_builder, rag_config) + + assert rag_config.llm is native_config + + async def test_resolve_llm_unsupported_type_raises(self, mock_builder: MagicMock) -> None: + """Test unsupported llm type in NvidiaRAGLibConfig raises ValueError.""" + from nvidia_rag.utils.configuration import NvidiaRAGConfig + + from nat.plugins.rag_lib.client import _resolve_llm_config + + rag_config: NvidiaRAGConfig = NvidiaRAGConfig() + + with pytest.raises(ValueError, match="Unsupported LLM config type"): + await _resolve_llm_config({"invalid": "config"}, mock_builder, rag_config) + + async def test_resolve_embedder_from_ref(self, mock_builder: MagicMock) -> None: + """Test EmbedderRef from NvidiaRAGLibConfig resolves to NvidiaRAGConfig.embeddings.""" + from nvidia_rag.utils.configuration import NvidiaRAGConfig + + from nat.plugins.rag_lib.client import _resolve_embedder_config + + rag_config: NvidiaRAGConfig = NvidiaRAGConfig() + await _resolve_embedder_config(EmbedderRef("embedder_nim"), mock_builder, rag_config) + + assert rag_config.embeddings.model_name == "nvidia/nv-embedqa-e5-v5" + + async def test_resolve_embedder_from_nim_config(self, mock_builder: MagicMock) -> None: + """Test NIMEmbedderModelConfig from NvidiaRAGLibConfig resolves to NvidiaRAGConfig.embeddings.""" + from nvidia_rag.utils.configuration import NvidiaRAGConfig + + from nat.plugins.rag_lib.client import _resolve_embedder_config + + nim_config: NIMEmbedderModelConfig = NIMEmbedderModelConfig( + model_name="nvidia/nv-embedqa-e5-v5", + base_url="http://embedder:8000/v1", + api_key="direct-embedder-key", + ) + + rag_config: NvidiaRAGConfig = NvidiaRAGConfig() + await _resolve_embedder_config(nim_config, mock_builder, rag_config) + + assert rag_config.embeddings.model_name == "nvidia/nv-embedqa-e5-v5" + assert rag_config.embeddings.server_url == "http://embedder:8000/v1" + assert rag_config.embeddings.api_key.get_secret_value() == "direct-embedder-key" + + async def test_resolve_embedder_none_uses_defaults(self, mock_builder: MagicMock) -> None: + """Test None embedder in NvidiaRAGLibConfig preserves NvidiaRAGConfig defaults.""" + from nvidia_rag.utils.configuration import EmbeddingConfig + from nvidia_rag.utils.configuration import NvidiaRAGConfig + + from nat.plugins.rag_lib.client import _resolve_embedder_config + + rag_config: NvidiaRAGConfig = NvidiaRAGConfig() + original_embeddings: EmbeddingConfig = rag_config.embeddings + + await _resolve_embedder_config(None, mock_builder, rag_config) + + assert rag_config.embeddings is original_embeddings + + async def test_resolve_embedder_native_config_passthrough(self, mock_builder: MagicMock) -> None: + """Test NvidiaRAGEmbeddingConfig passes through to NvidiaRAGConfig unchanged.""" + from nvidia_rag.utils.configuration import EmbeddingConfig as NvidiaRAGEmbeddingConfig + from nvidia_rag.utils.configuration import NvidiaRAGConfig + + from nat.plugins.rag_lib.client import _resolve_embedder_config + + native_config: NvidiaRAGEmbeddingConfig = NvidiaRAGEmbeddingConfig( + model_name="custom/embedder", + server_url="http://custom:8000", + ) + + rag_config: NvidiaRAGConfig = NvidiaRAGConfig() + await _resolve_embedder_config(native_config, mock_builder, rag_config) + + assert rag_config.embeddings is native_config + + async def test_resolve_embedder_unsupported_type_raises(self, mock_builder: MagicMock) -> None: + """Test unsupported embedder type in NvidiaRAGLibConfig raises ValueError.""" + from nvidia_rag.utils.configuration import NvidiaRAGConfig + + from nat.plugins.rag_lib.client import _resolve_embedder_config + + rag_config: NvidiaRAGConfig = NvidiaRAGConfig() + + with pytest.raises(ValueError, match="Unsupported embedder config type"): + await _resolve_embedder_config({"invalid": "config"}, mock_builder, rag_config) + + async def test_resolve_retriever_from_ref(self, mock_builder: MagicMock) -> None: + """Test RetrieverRef from NvidiaRAGLibConfig resolves to NvidiaRAGConfig.vector_store.""" + from nvidia_rag.utils.configuration import NvidiaRAGConfig + + from nat.plugins.rag_lib.client import _resolve_retriever_config + + rag_config: NvidiaRAGConfig = NvidiaRAGConfig() + await _resolve_retriever_config(RetrieverRef("retriever_milvus"), mock_builder, rag_config) + + assert rag_config.vector_store.name == "milvus" + assert rag_config.vector_store.url == "http://localhost:19530/" + assert rag_config.vector_store.default_collection_name == "test_collection" + + async def test_resolve_retriever_from_milvus_config(self, mock_builder: MagicMock) -> None: + """Test MilvusRetrieverConfig from NvidiaRAGLibConfig resolves to NvidiaRAGConfig.vector_store.""" + from nvidia_rag.utils.configuration import NvidiaRAGConfig + + from nat.plugins.rag_lib.client import _resolve_retriever_config + + milvus_config: MilvusRetrieverConfig = MilvusRetrieverConfig( + uri=HttpUrl("http://milvus:19530"), + collection_name="my_collection", + embedding_model="nvidia/nv-embedqa-e5-v5", + connection_args={ + "user": "admin", "password": "secret123" + }, + ) + + rag_config: NvidiaRAGConfig = NvidiaRAGConfig() + await _resolve_retriever_config(milvus_config, mock_builder, rag_config) + + assert rag_config.vector_store.url == "http://milvus:19530/" + assert rag_config.vector_store.default_collection_name == "my_collection" + assert rag_config.vector_store.username == "admin" + assert rag_config.vector_store.password.get_secret_value() == "secret123" + + async def test_resolve_retriever_from_nemo_config(self, mock_builder: MagicMock) -> None: + """Test NemoRetrieverConfig from NvidiaRAGLibConfig resolves to NvidiaRAGConfig.vector_store.""" + from nvidia_rag.utils.configuration import NvidiaRAGConfig + + from nat.plugins.rag_lib.client import _resolve_retriever_config + from nat.retriever.nemo_retriever.register import NemoRetrieverConfig + + nemo_config: NemoRetrieverConfig = NemoRetrieverConfig( + uri=HttpUrl("http://nemo-retriever:8000"), + collection_name="nemo_collection", + nvidia_api_key="nemo-api-key", + ) + + rag_config: NvidiaRAGConfig = NvidiaRAGConfig() + await _resolve_retriever_config(nemo_config, mock_builder, rag_config) + + assert rag_config.vector_store.url == "http://nemo-retriever:8000/" + assert rag_config.vector_store.default_collection_name == "nemo_collection" + assert rag_config.vector_store.api_key.get_secret_value() == "nemo-api-key" - with open(config_path, encoding='utf-8') as f: - config_dict = yaml.safe_load(f) + async def test_resolve_retriever_none_uses_defaults(self, mock_builder: MagicMock) -> None: + """Test None retriever in NvidiaRAGLibConfig preserves NvidiaRAGConfig defaults.""" + from nvidia_rag.utils.configuration import NvidiaRAGConfig + from nvidia_rag.utils.configuration import VectorStoreConfig - return Config.model_validate(config_dict) + from nat.plugins.rag_lib.client import _resolve_retriever_config + rag_config: NvidiaRAGConfig = NvidiaRAGConfig() + original_vector_store: VectorStoreConfig = rag_config.vector_store -class TestNvidiaRAGLib: - """Test suite for the NVIDIA RAG Library function.""" + await _resolve_retriever_config(None, mock_builder, rag_config) + + assert rag_config.vector_store is original_vector_store + + async def test_resolve_retriever_native_config_passthrough(self, mock_builder: MagicMock) -> None: + """Test NvidiaRAGVectorStoreConfig passes through to NvidiaRAGConfig unchanged.""" + from nvidia_rag.utils.configuration import NvidiaRAGConfig + from nvidia_rag.utils.configuration import VectorStoreConfig as NvidiaRAGVectorStoreConfig + + from nat.plugins.rag_lib.client import _resolve_retriever_config + + native_config: NvidiaRAGVectorStoreConfig = NvidiaRAGVectorStoreConfig( + name="custom", + url="http://custom:19530", + ) + + rag_config: NvidiaRAGConfig = NvidiaRAGConfig() + await _resolve_retriever_config(native_config, mock_builder, rag_config) + + assert rag_config.vector_store is native_config + + async def test_resolve_retriever_unsupported_type_raises(self, mock_builder: MagicMock) -> None: + """Test unsupported retriever type in NvidiaRAGLibConfig raises ValueError.""" + from nvidia_rag.utils.configuration import NvidiaRAGConfig + + from nat.plugins.rag_lib.client import _resolve_retriever_config + + rag_config: NvidiaRAGConfig = NvidiaRAGConfig() + + with pytest.raises(ValueError, match="Unsupported retriever config type"): + await _resolve_retriever_config({"invalid": "config"}, mock_builder, rag_config) # type: ignore[arg-type] + + +# ============================================================================= +# RAGPipelineConfig Mapping Tests +# ============================================================================= + + +class TestRAGPipelineConfigMapping: + """Test RAGPipelineConfig fields are correctly mapped to NvidiaRAGConfig.""" + + async def test_pipeline_fields_with_defaults(self, mock_builder: MagicMock) -> None: + """Test that default RAGPipelineConfig creates valid NvidiaRAGConfig with all required fields.""" + from nvidia_rag.utils.configuration import NvidiaRAGConfig + + from nat.plugins.rag_lib.client import NvidiaRAGLibConfig + from nat.plugins.rag_lib.client import build_nvidia_rag_config + + config = NvidiaRAGLibConfig() + rag_config: NvidiaRAGConfig = await build_nvidia_rag_config(config, mock_builder) + + # Fields that always have values (from default_factory) + assert rag_config.ranking is not None + assert rag_config.retriever is not None + + # Fields that get defaults when None + assert rag_config.vlm is not None + assert rag_config.query_rewriter is not None + assert rag_config.filter_expression_generator is not None + assert rag_config.query_decomposition is not None + assert rag_config.reflection is not None + + # Boolean flags have correct defaults + assert rag_config.enable_citations is True + assert rag_config.enable_guardrails is False + assert rag_config.enable_vlm_inference is False + assert rag_config.vlm_to_llm_fallback is True + assert rag_config.default_confidence_threshold == 0.0 + + async def test_pipeline_fields_passthrough(self, mock_builder: MagicMock) -> None: + """Test that explicit RAGPipelineConfig values are passed through to NvidiaRAGConfig.""" + from nvidia_rag.utils.configuration import NvidiaRAGConfig + from nvidia_rag.utils.configuration import QueryRewriterConfig + from nvidia_rag.utils.configuration import RankingConfig + from nvidia_rag.utils.configuration import RetrieverConfig + from nvidia_rag.utils.configuration import VLMConfig + + from nat.plugins.rag_lib.client import NvidiaRAGLibConfig + from nat.plugins.rag_lib.client import build_nvidia_rag_config + from nat.plugins.rag_lib.config import RAGPipelineConfig + + custom_ranking = RankingConfig(enable_reranker=False) + custom_retriever = RetrieverConfig(top_k=20, vdb_top_k=200) + custom_vlm = VLMConfig(model_name="custom/vlm-model", temperature=0.5) + custom_query_rewriter = QueryRewriterConfig(enable_query_rewriter=True) + + pipeline = RAGPipelineConfig( + ranking=custom_ranking, + search_settings=custom_retriever, + vlm=custom_vlm, + query_rewriter=custom_query_rewriter, + enable_citations=False, + enable_guardrails=True, + enable_vlm_inference=True, + vlm_to_llm_fallback=False, + default_confidence_threshold=0.5, + ) + + config = NvidiaRAGLibConfig(rag_pipeline=pipeline) + rag_config: NvidiaRAGConfig = await build_nvidia_rag_config(config, mock_builder) + + # Explicit values passed through + assert rag_config.ranking.enable_reranker is False + assert rag_config.retriever.top_k == 20 + assert rag_config.retriever.vdb_top_k == 200 + assert rag_config.vlm.model_name == "custom/vlm-model" + assert rag_config.vlm.temperature == 0.5 + assert rag_config.query_rewriter.enable_query_rewriter is True + + # Boolean flags passed through + assert rag_config.enable_citations is False + assert rag_config.enable_guardrails is True + assert rag_config.enable_vlm_inference is True + assert rag_config.vlm_to_llm_fallback is False + assert rag_config.default_confidence_threshold == 0.5 + + async def test_none_optional_fields_get_defaults(self, mock_builder: MagicMock) -> None: + """Test that None optional fields receive proper default config objects.""" + from nvidia_rag.utils.configuration import FilterExpressionGeneratorConfig + from nvidia_rag.utils.configuration import NvidiaRAGConfig + from nvidia_rag.utils.configuration import QueryDecompositionConfig + from nvidia_rag.utils.configuration import QueryRewriterConfig + from nvidia_rag.utils.configuration import ReflectionConfig + from nvidia_rag.utils.configuration import VLMConfig + + from nat.plugins.rag_lib.client import NvidiaRAGLibConfig + from nat.plugins.rag_lib.client import build_nvidia_rag_config + from nat.plugins.rag_lib.config import RAGPipelineConfig + + # Explicitly set optional fields to None + pipeline = RAGPipelineConfig( + vlm=None, + query_rewriter=None, + filter_generator=None, + query_decomposition=None, + reflection=None, + ) + + config = NvidiaRAGLibConfig(rag_pipeline=pipeline) + rag_config: NvidiaRAGConfig = await build_nvidia_rag_config(config, mock_builder) + + # All should be valid config objects, not None + assert isinstance(rag_config.vlm, VLMConfig) + assert isinstance(rag_config.query_rewriter, QueryRewriterConfig) + assert isinstance(rag_config.filter_expression_generator, FilterExpressionGeneratorConfig) + assert isinstance(rag_config.query_decomposition, QueryDecompositionConfig) + assert isinstance(rag_config.reflection, ReflectionConfig) + + +# ============================================================================= +# NvidiaRAG Functional Tests +# ============================================================================= + + +class TestNvidiaRAGMethods: + """Test NvidiaRAG class can be imported and has expected methods.""" + + def test_import_and_instantiate_nvidia_rag(self) -> None: + """Verify nvidia_rag can be imported and instantiated.""" + from nvidia_rag import NvidiaRAG + + rag = NvidiaRAG() + assert rag is not None + assert isinstance(rag, NvidiaRAG) + + def test_generate_method_exists(self) -> None: + """NvidiaRAG should have a generate method.""" + from nvidia_rag import NvidiaRAG + + assert hasattr(NvidiaRAG, "generate") + assert callable(getattr(NvidiaRAG, "generate")) + + def test_search_method_exists(self) -> None: + """NvidiaRAG should have a search method.""" + from nvidia_rag import NvidiaRAG + + assert hasattr(NvidiaRAG, "search") + assert callable(getattr(NvidiaRAG, "search")) + + def test_health_method_exists(self) -> None: + """NvidiaRAG should have a health method.""" + from nvidia_rag import NvidiaRAG + + assert hasattr(NvidiaRAG, "health") + assert callable(getattr(NvidiaRAG, "health")) + + +@pytest.mark.integration +class TestNvidiaRAGIntegration: + """Test NvidiaRAG generate(), search(), and health() methods with running services. + + Requires: Milvus, NVIDIA API key. + """ + + @pytest.fixture + def milvus_test_collection(self): + """Create test collection with proper schema for nvidia-rag.""" + from langchain_core.documents import Document + from langchain_milvus import Milvus + from langchain_nvidia_ai_endpoints import NVIDIAEmbeddings + from pymilvus import MilvusClient + + collection_name = "test_collection" + client = MilvusClient(uri="http://localhost:19530") + + if client.has_collection(collection_name): + client.drop_collection(collection_name) + + embeddings = NVIDIAEmbeddings(model="nvidia/nv-embedqa-e5-v5") + Milvus.from_documents( + documents=[ + Document( + page_content="Test document about machine learning", + metadata={"source": "test_doc.txt"}, + ) + ], + embedding=embeddings, + collection_name=collection_name, + connection_args={"uri": "http://localhost:19530"}, + ) + + yield collection_name + + client.drop_collection(collection_name) + + async def test_generate_method(self, mock_builder: MagicMock) -> None: + """Test NvidiaRAG generate() returns a result.""" + from nvidia_rag import NvidiaRAG + + from nat.plugins.rag_lib.client import NvidiaRAGLibConfig + from nat.plugins.rag_lib.client import build_nvidia_rag_config + + config = NvidiaRAGLibConfig( + llm=LLMRef("llm"), + embedder=EmbedderRef("embedder"), + retriever=RetrieverRef("retriever"), + ) + rag_config = await build_nvidia_rag_config(config, mock_builder) + rag = NvidiaRAG(config=rag_config) + + messages = [{"role": "user", "content": "What is RAG?"}] + result = await rag.generate(messages=messages, use_knowledge_base=False) + + assert result is not None + + async def test_search_method(self, mock_builder: MagicMock, milvus_test_collection: str) -> None: + """Test NvidiaRAG search() returns a result.""" + from nvidia_rag import NvidiaRAG - def test_function_registration(self): - """Test that the RAG function is properly registered with NAT.""" - from nat.cli.type_registry import GlobalTypeRegistry from nat.plugins.rag_lib.client import NvidiaRAGLibConfig - registry = GlobalTypeRegistry.get() - - # Check if our function is registered - try: - function_info = registry.get_function(NvidiaRAGLibConfig) - assert function_info is not None - assert function_info.config_type == NvidiaRAGLibConfig - except KeyError: - pytest.fail("NvidiaRAGLibConfig function not properly registered") - - @pytest.mark.asyncio - async def test_rag_library_acquisition(self, test_config): - """Test acquiring the RAG library. - - Simple test that tries to acquire the RAG library. - """ - from nat.builder.workflow_builder import WorkflowBuilder - - try: - async with WorkflowBuilder.from_config(test_config) as builder: - # Simply acquire the RAG library function - rag_function = await builder.get_function("rag_client") - - # Verify we got the function - assert rag_function is not None - print("RAG library acquired successfully") - - except ImportError as e: - if "nvidia-rag" in str(e): - pytest.fail(f"nvidia-rag library not available: {e}") - else: - raise - - @pytest.mark.asyncio - async def test_rag_search_functionality(self, test_config): - """Test RAG library search functionality after successful acquisition. - - This test demonstrates how to use the RAG library's search capabilities - including citation parsing. It doesn't need to work (no vector DB running) - but shows the proper setup for RAG search operations. - """ - from nat.builder.workflow_builder import WorkflowBuilder - - def parse_search_citations(citations): - """Parse search citations into formatted document strings.""" - parsed_docs = [] - - for idx, citation in enumerate(citations.results): - # If using pydantic models, citation fields may be attributes, not dict keys - content = getattr(citation, 'content', '') - doc_name = getattr(citation, 'document_name', f'Citation {idx+1}') - parsed_document = f'\n{content}\n' - parsed_docs.append(parsed_document) - - # combine parsed documents into a single string - internal_search_docs = "\n\n---\n\n".join(parsed_docs) - return internal_search_docs - - try: - async with WorkflowBuilder.from_config(test_config) as builder: - # Acquire the RAG library function - rag_function = await builder.get_function("rag_client") - assert rag_function is not None - - try: - # Demonstrate search configuration matching our config - collection_names = test_config.functions["rag_client"].collection_names - reranker_top_k = test_config.functions["rag_client"].reranker_top_k - vdb_top_k = test_config.functions["rag_client"].vdb_top_k - - search_results = rag_function.search( - query="test query", - collection_names=collection_names, - reranker_top_k=reranker_top_k, - vdb_top_k=vdb_top_k, - ) - parsed_docs = parse_search_citations(search_results) - - # Assert if data was returned from parsed_docs - assert parsed_docs is not None - - except Exception as e: - print(f"RAG search failed as expected (no vector DB): {e}") - - except ImportError as e: - if "nvidia-rag" in str(e): - pytest.fail(f"nvidia-rag library not available: {e}") - else: - raise + from nat.plugins.rag_lib.client import build_nvidia_rag_config + + config = NvidiaRAGLibConfig( + llm=LLMRef("llm"), + embedder=EmbedderRef("embedder"), + retriever=RetrieverRef("retriever"), + ) + rag_config = await build_nvidia_rag_config(config, mock_builder) + rag_config.vector_store.default_collection_name = milvus_test_collection + rag_config.embeddings.dimensions = None + rag = NvidiaRAG(config=rag_config) + + result = await rag.search(query="What is machine learning?") + + assert result is not None + + async def test_health_method(self, mock_builder: MagicMock) -> None: + """Test NvidiaRAG health() returns a result.""" + from nvidia_rag import NvidiaRAG + + from nat.plugins.rag_lib.client import NvidiaRAGLibConfig + from nat.plugins.rag_lib.client import build_nvidia_rag_config + + config = NvidiaRAGLibConfig( + llm=LLMRef("llm"), + embedder=EmbedderRef("embedder"), + retriever=RetrieverRef("retriever"), + ) + rag_config = await build_nvidia_rag_config(config, mock_builder) + rag = NvidiaRAG(config=rag_config) + + result = await rag.health() + + assert result is not None From 1978f2fe964649e07314d4b4f2fe658459f91436 Mon Sep 17 00:00:00 2001 From: Eric Evans <194135482+ericevans-nv@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:32:35 -0600 Subject: [PATCH 04/15] Add rag library mode search and generate tools. Signed-off-by: Eric Evans <194135482+ericevans-nv@users.noreply.github.com> --- .../configs/rag_library_mode_config.yml | 90 +++++++ examples/deploy/docker-compose.milvus.yml | 5 +- .../src/nat/plugins/rag_lib/client.py | 61 ++++- .../src/nat/plugins/rag_lib/models.py | 46 ++++ .../src/nat/plugins/rag_lib/register.py | 1 + .../src/nat/plugins/rag_lib/tools/__init__.py | 14 + .../src/nat/plugins/rag_lib/tools/generate.py | 132 +++++++++ .../src/nat/plugins/rag_lib/tools/register.py | 20 ++ .../src/nat/plugins/rag_lib/tools/search.py | 93 +++++++ .../nvidia_nat_rag_lib/tests/test_models.py | 87 ++++++ .../tests/test_rag_lib_function.py | 251 ++++++++++++------ .../nvidia_nat_rag_lib/tests/test_tools.py | 156 +++++++++++ scripts/langchain_web_ingest.py | 2 + src/nat/embedder/nim_embedder.py | 1 + 14 files changed, 868 insertions(+), 91 deletions(-) create mode 100644 examples/RAG/simple_rag/configs/rag_library_mode_config.yml create mode 100644 packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/models.py create mode 100644 packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/__init__.py create mode 100644 packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/generate.py create mode 100644 packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/register.py create mode 100644 packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/search.py create mode 100644 packages/nvidia_nat_rag_lib/tests/test_models.py create mode 100644 packages/nvidia_nat_rag_lib/tests/test_tools.py diff --git a/examples/RAG/simple_rag/configs/rag_library_mode_config.yml b/examples/RAG/simple_rag/configs/rag_library_mode_config.yml new file mode 100644 index 0000000000..3bd0d8e097 --- /dev/null +++ b/examples/RAG/simple_rag/configs/rag_library_mode_config.yml @@ -0,0 +1,90 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-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. + +llms: + nim_llm: + _type: nim + model_name: meta/llama-3.3-70b-instruct + temperature: 0 + max_tokens: 4096 + top_p: 1 + +embedders: + nim_embedder: + _type: nim + model_name: nvidia/llama-3.2-nv-embedqa-1b-v2 + truncate: "END" + +retrievers: + milvus_retriever: + _type: milvus_retriever + uri: http://localhost:19530 + collection_name: cuda_docs + embedding_model: nim_embedder + top_k: 100 + +function_groups: + rag_client: + _type: nvidia_rag_lib + llm: nim_llm + embedder: nim_embedder + retriever: milvus_retriever + rag_pipeline: + enable_citations: true + default_confidence_threshold: 0.25 + + ranking: + top_k: 10 + enable_reranker: true + model_name: nvidia/llama-3.2-nv-rerankqa-1b-v2 + + # TODO: Re-enable once library bug is fixed. + # BUG: enable_reflection breaks search() in nvidia_rag. + # Missing 'await' on check_context_relevance() at nvidia_rag/rag_server/main.py:1112 + # reflection: + # enable_reflection: true + # max_loops: 2 + # context_relevance_threshold: 1 + # response_groundedness_threshold: 1 + +functions: + search: + _type: rag_lib_search + rag_client: rag_client + topic: NVIDIA technologies + description: "Search NVIDIA documentation (CUDA, MCP) with AI-enhanced query understanding. Use for any technical question." + collection_names: + - cuda_docs + - mcp_docs + reranker_top_k: 10 + + generate: + _type: rag_lib_generate + rag_client: rag_client + topic: NVIDIA technologies + description: "Generate a high-quality, self-verified answer with citations. Uses reflection to reduce errors." + collection_names: + - cuda_docs + - mcp_docs + use_knowledge_base: true + enable_citations: true + +workflow: + _type: react_agent + tool_names: + - search + - generate + verbose: true + llm_name: nim_llm diff --git a/examples/deploy/docker-compose.milvus.yml b/examples/deploy/docker-compose.milvus.yml index d398e2f98f..1cacbb1453 100644 --- a/examples/deploy/docker-compose.milvus.yml +++ b/examples/deploy/docker-compose.milvus.yml @@ -21,8 +21,8 @@ services: MINIO_ACCESS_KEY: minioadmin MINIO_SECRET_KEY: minioadmin ports: - - "9001:9001" - - "9000:9000" + - "19001:9001" + - "19000:9000" volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data command: minio server /minio_data --console-address ":9001" @@ -100,4 +100,3 @@ services: networks: default: name: nvidia-rag-test - diff --git a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/client.py b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/client.py index 09c31e5484..f30ccb6817 100644 --- a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/client.py +++ b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/client.py @@ -15,6 +15,7 @@ import logging from logging import Logger +from typing import TYPE_CHECKING from nvidia_rag.utils.configuration import EmbeddingConfig as NvidiaRAGEmbeddingConfig from nvidia_rag.utils.configuration import FilterExpressionGeneratorConfig as NvidiaRAGFilterGeneratorConfig @@ -29,11 +30,12 @@ from pydantic import SecretStr from nat.builder.builder import Builder -from nat.cli.register_workflow import register_function +from nat.builder.function import FunctionGroup +from nat.cli.register_workflow import register_function_group from nat.data_models.component_ref import EmbedderRef from nat.data_models.component_ref import LLMRef from nat.data_models.component_ref import RetrieverRef -from nat.data_models.function import FunctionBaseConfig +from nat.data_models.function import FunctionGroupBaseConfig from nat.embedder.nim_embedder import NIMEmbedderModelConfig from nat.llm.nim_llm import NIMModelConfig from nat.plugins.rag_lib.config import EmbedderConfigType @@ -43,10 +45,21 @@ from nat.retriever.milvus.register import MilvusRetrieverConfig from nat.retriever.nemo_retriever.register import NemoRetrieverConfig +if TYPE_CHECKING: + from nvidia_rag import NvidiaRAG + logger: Logger = logging.getLogger(__name__) -class NvidiaRAGLibConfig(FunctionBaseConfig, name="nvidia_rag_lib"): +class NvidiaRAGFunctionGroup(FunctionGroup): + """FunctionGroup wrapper that exposes the NvidiaRAG client.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.rag_client: NvidiaRAG | None = None + + +class NvidiaRAGLibConfig(FunctionGroupBaseConfig, name="nvidia_rag_lib"): """Configuration for NVIDIA RAG Library. All component configs are optional - NvidiaRAGConfig provides defaults. @@ -59,20 +72,24 @@ class NvidiaRAGLibConfig(FunctionBaseConfig, name="nvidia_rag_lib"): rag_pipeline: RAGPipelineConfig = Field(default_factory=RAGPipelineConfig) -@register_function(config_type=NvidiaRAGLibConfig) # type: ignore[arg-type] +@register_function_group(config_type=NvidiaRAGLibConfig) async def nvidia_rag_lib(config: NvidiaRAGLibConfig, builder: Builder): - """Initialize NVIDIA RAG with flexible config resolution.""" + """Initialize NVIDIA RAG client and expose via FunctionGroup.""" try: from nvidia_rag import NvidiaRAG except ImportError as e: raise ImportError("nvidia-rag package is not installed.") from e - rag_config: NvidiaRAGConfig = await build_nvidia_rag_config(config, builder) - logger.info("NVIDIA RAG initialized") - yield NvidiaRAG(config=rag_config) + group = NvidiaRAGFunctionGroup(config=config) + rag_config: NvidiaRAGConfig = await _build_nvidia_rag_config(config, builder) + group.rag_client = NvidiaRAG(config=rag_config) + logger.info("NVIDIA RAG client initialized") -async def build_nvidia_rag_config(config: NvidiaRAGLibConfig, builder: Builder) -> NvidiaRAGConfig: + yield group + + +async def _build_nvidia_rag_config(config: NvidiaRAGLibConfig, builder: Builder) -> NvidiaRAGConfig: """Build NvidiaRAGConfig by resolving NAT refs/components to nvidia_rag configs.""" pipeline: RAGPipelineConfig = config.rag_pipeline @@ -126,6 +143,30 @@ async def _resolve_llm_config(llm: LLMConfigType, builder: Builder, rag_config: rag_config.llm.parameters.top_p = llm.top_p if llm.max_tokens is not None: rag_config.llm.parameters.max_tokens = llm.max_tokens + + # Propagate LLM values to query_rewriter when not set, instead of using library defaults. + if "model_name" not in rag_config.query_rewriter.model_fields_set: + rag_config.query_rewriter.model_name = llm.model_name + if "server_url" not in rag_config.query_rewriter.model_fields_set and llm.base_url: + rag_config.query_rewriter.server_url = llm.base_url + if "api_key" not in rag_config.query_rewriter.model_fields_set and llm.api_key: + rag_config.query_rewriter.api_key = llm.api_key + + # Propagate LLM values to reflection when not set, instead of using library defaults. + if "model_name" not in rag_config.reflection.model_fields_set: + rag_config.reflection.model_name = llm.model_name + if "server_url" not in rag_config.reflection.model_fields_set and llm.base_url: + rag_config.reflection.server_url = llm.base_url + if "api_key" not in rag_config.reflection.model_fields_set and llm.api_key: + rag_config.reflection.api_key = llm.api_key + + # Propagate LLM values to filter_expression_generator when not set, instead of using library defaults. + if "model_name" not in rag_config.filter_expression_generator.model_fields_set: + rag_config.filter_expression_generator.model_name = llm.model_name + if "server_url" not in rag_config.filter_expression_generator.model_fields_set and llm.base_url: + rag_config.filter_expression_generator.server_url = llm.base_url + if "api_key" not in rag_config.filter_expression_generator.model_fields_set and llm.api_key: + rag_config.filter_expression_generator.api_key = llm.api_key return raise ValueError(f"Unsupported LLM config type: {type(llm)}") @@ -150,6 +191,8 @@ async def _resolve_embedder_config(embedder: EmbedderConfigType, builder: Builde rag_config.embeddings.server_url = embedder.base_url if embedder.api_key: rag_config.embeddings.api_key = embedder.api_key + if embedder.dimensions is not None: + rag_config.embeddings.dimensions = embedder.dimensions return raise ValueError(f"Unsupported embedder config type: {type(embedder)}") diff --git a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/models.py b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/models.py new file mode 100644 index 0000000000..bc6d0c4933 --- /dev/null +++ b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/models.py @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-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. + +from typing import TYPE_CHECKING + +from pydantic import BaseModel + +if TYPE_CHECKING: + from nvidia_rag.rag_server.response_generator import Citations + + +class RAGResultBase(BaseModel): + """Base model for RAG tool results.""" + + model_config = {"arbitrary_types_allowed": True} + + +class RAGSearchResult(RAGResultBase): + """RAG search result.""" + + citations: "Citations" + + def __str__(self) -> str: + return self.citations.model_dump_json() + + +class RAGGenerateResult(RAGResultBase): + """RAG generation result.""" + + answer: str + citations: "Citations | None" = None + + def __str__(self) -> str: + return self.model_dump_json(exclude_none=True) diff --git a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/register.py b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/register.py index 0a8992b620..6198011e03 100644 --- a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/register.py +++ b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/register.py @@ -19,3 +19,4 @@ # Import any providers which need to be automatically registered here from . import client +from .tools import register diff --git a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/__init__.py b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/__init__.py new file mode 100644 index 0000000000..3bcc1c39bb --- /dev/null +++ b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/__init__.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-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. diff --git a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/generate.py b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/generate.py new file mode 100644 index 0000000000..a5b782e7d7 --- /dev/null +++ b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/generate.py @@ -0,0 +1,132 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-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. + +import logging +from collections.abc import AsyncGenerator +from typing import TYPE_CHECKING + +from pydantic import Field + +from nat.builder.builder import Builder +from nat.builder.function_info import FunctionInfo +from nat.cli.register_workflow import register_function +from nat.data_models.component_ref import FunctionGroupRef +from nat.data_models.finetuning import OpenAIMessage +from nat.data_models.function import FunctionBaseConfig +from nat.plugins.rag_lib.models import RAGGenerateResult + +if TYPE_CHECKING: + from nvidia_rag.rag_server.main import NvidiaRAG + from nvidia_rag.rag_server.response_generator import RAGResponse + + from nat.plugins.rag_lib.client import NvidiaRAGFunctionGroup + +logger: logging.Logger = logging.getLogger(__name__) + + +class RAGLibGenerateConfig(FunctionBaseConfig, name="rag_lib_generate"): + """Generate tool configuration.""" + + rag_client: FunctionGroupRef = Field(description="Reference to nvidia_rag_lib function group.") + topic: str | None = Field(default=None, description="Topic injected into description via {topic} placeholder.") + description: str = Field(default="Generate an answer{topic} with citations from the knowledge base.", + description="Tool description. Use {topic} placeholder to include topic.") + collection_names: list[str] | None = Field(default=None, + description="Collections for context. None uses client defaults.") + use_knowledge_base: bool = Field(default=True, description="Whether to use RAG (True) or pure LLM (False).") + confidence_threshold: float | None = Field( + default=None, ge=0.0, le=1.0, description="Minimum relevance score for context. None uses client default.") + enable_citations: bool | None = Field(default=None, + description="Whether to include citations. None uses client default.") + enable_guardrails: bool | None = Field(default=None, + description="Whether to enable guardrails. None uses client default.") + temperature: float | None = Field(default=None, + ge=0.0, + le=2.0, + description="Sampling temperature. None uses client default.") + max_tokens: int | None = Field(default=None, + ge=1, + description="Maximum tokens to generate. None uses client default.") + top_p: float | None = Field(default=None, + ge=0.0, + le=1.0, + description="Nucleus sampling probability. None uses client default.") + filter_expr: str | None = Field( + default=None, description="Static metadata filter expression, e.g., 'year >= 2023'. None applies no filter.") + + +@register_function(config_type=RAGLibGenerateConfig) +async def rag_lib_generate(config: RAGLibGenerateConfig, builder: Builder): + """RAG Library Generate Tool.""" + + rag_group: NvidiaRAGFunctionGroup = await builder.get_function_group(config.rag_client) # type: ignore[assignment] + rag_client: NvidiaRAG = rag_group.rag_client + topic_str: str = f" about {config.topic}" if config.topic else "" + description: str = config.description.format(topic=topic_str) + + async def _generate(query: str) -> RAGGenerateResult: + """Generate an answer using the knowledge base.""" + from nvidia_rag.rag_server.response_generator import ChainResponse + from nvidia_rag.rag_server.response_generator import Citations + + chunks: list[str] = [] + final_citations: Citations | None = None + + try: + response: RAGResponse = await rag_client.generate( + messages=[OpenAIMessage(role="user", content=query).model_dump()], + use_knowledge_base=config.use_knowledge_base, + filter_expr=config.filter_expr or "", + collection_names=config.collection_names, + confidence_threshold=config.confidence_threshold, + enable_citations=config.enable_citations, + enable_guardrails=config.enable_guardrails, + temperature=config.temperature, + max_tokens=config.max_tokens, + top_p=config.top_p, + ) + + stream: AsyncGenerator[str, None] = (response.generator if hasattr(response, "generator") else response) + + async for raw_chunk in stream: + if raw_chunk.startswith("data: "): + raw_chunk = raw_chunk[len("data: "):].strip() + if not raw_chunk or raw_chunk == "[DONE]": + continue + + try: + parsed: ChainResponse = ChainResponse.model_validate_json(raw_chunk) + + if parsed.choices: + choice = parsed.choices[0] + if choice.delta and choice.delta.content: + content = choice.delta.content + if isinstance(content, str): + chunks.append(content) + + if parsed.citations and parsed.citations.results: + final_citations = parsed.citations + + except Exception: + continue + + answer: str = "".join(chunks) if chunks else "No response generated." + return RAGGenerateResult(answer=answer, citations=final_citations) + + except Exception: + logger.exception("RAG generate failed") + return RAGGenerateResult(answer="Error generating answer. Please try again.") + + yield FunctionInfo.from_fn(fn=_generate, description=description) diff --git a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/register.py b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/register.py new file mode 100644 index 0000000000..578b6f24c0 --- /dev/null +++ b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/register.py @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-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. + +# flake8: noqa +# isort:skip_file + +from . import generate +from . import search diff --git a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/search.py b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/search.py new file mode 100644 index 0000000000..58c3c9274e --- /dev/null +++ b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/search.py @@ -0,0 +1,93 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-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. + +import logging +from typing import TYPE_CHECKING + +from pydantic import Field + +from nat.builder.builder import Builder +from nat.builder.function_info import FunctionInfo +from nat.cli.register_workflow import register_function +from nat.data_models.component_ref import FunctionGroupRef +from nat.data_models.function import FunctionBaseConfig +from nat.plugins.rag_lib.models import RAGSearchResult + +if TYPE_CHECKING: + from nat.plugins.rag_lib.client import NvidiaRAGFunctionGroup + +logger: logging.Logger = logging.getLogger(__name__) + + +class RAGLibSearchConfig(FunctionBaseConfig, name="rag_lib_search"): + """Search tool configuration.""" + + rag_client: FunctionGroupRef = Field(description="Reference to nvidia_rag_lib function group.") + topic: str | None = Field(default=None, description="Topic injected into description via {topic} placeholder.") + description: str = Field( + default="Retrieve document chunks{topic} which can be used to answer the provided question.", + description="Tool description. Use {topic} placeholder to include topic.") + collection_names: list[str] | None = Field(default=None, description="Collections to search.") + reranker_top_k: int | None = Field(default=None, ge=1, description="Number of results after reranking.") + vdb_top_k: int | None = Field( + default=None, + ge=1, + description="Number of candidates from vector DB before reranking. None uses client default.") + confidence_threshold: float | None = Field(default=None, + ge=0.0, + le=1.0, + description="Minimum relevance score. None uses client default.") + enable_query_rewriting: bool | None = Field(default=None, + description="Whether to rewrite queries. None uses client default.") + enable_reranker: bool | None = Field(default=None, + description="Whether to use reranking. None uses client default.") + enable_filter_generator: bool | None = Field( + default=None, description="Whether to auto-generate filters. None uses client default.") + filter_expr: str | None = Field( + default=None, description="Static metadata filter expression, e.g., 'year >= 2023'. None applies no filter.") + + +@register_function(config_type=RAGLibSearchConfig) +async def rag_lib_search(config: RAGLibSearchConfig, builder: Builder): + """RAG Library Search Tool.""" + + rag_group: NvidiaRAGFunctionGroup = await builder.get_function_group(config.rag_client) # type: ignore[assignment] + rag_client = rag_group.rag_client + topic_str = f" related to {config.topic}" if config.topic else "" + description = config.description.format(topic=topic_str) + + async def _search(query: str) -> RAGSearchResult: + """Search for relevant documents.""" + from nvidia_rag.rag_server.response_generator import Citations + + try: + citations: Citations = await rag_client.search( + query=query, + filter_expr=config.filter_expr or "", + collection_names=config.collection_names, + reranker_top_k=config.reranker_top_k, + vdb_top_k=config.vdb_top_k, + confidence_threshold=config.confidence_threshold, + enable_query_rewriting=config.enable_query_rewriting, + enable_reranker=config.enable_reranker, + enable_filter_generator=config.enable_filter_generator, + ) + logger.info("Search returned %d results for query: %s", citations.total_results, query[:50]) + return RAGSearchResult(citations=citations) + except Exception: + logger.exception("RAG search failed") + return RAGSearchResult(citations=Citations(total_results=0, results=[])) + + yield FunctionInfo.from_fn(fn=_search, description=description) diff --git a/packages/nvidia_nat_rag_lib/tests/test_models.py b/packages/nvidia_nat_rag_lib/tests/test_models.py new file mode 100644 index 0000000000..67ecae39cf --- /dev/null +++ b/packages/nvidia_nat_rag_lib/tests/test_models.py @@ -0,0 +1,87 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-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. + +import json + +import pytest +from nvidia_rag.rag_server.response_generator import Citations + +from nat.plugins.rag_lib.models import RAGGenerateResult +from nat.plugins.rag_lib.models import RAGSearchResult + +# Rebuild models to resolve forward references to Citations +RAGSearchResult.model_rebuild() +RAGGenerateResult.model_rebuild() + + +class TestRAGSearchResult: + """Tests for RAGSearchResult model.""" + + @pytest.fixture + def citations(self) -> Citations: + """Create Citations object.""" + return Citations(total_results=2, results=[]) + + def test_creation(self, citations: Citations) -> None: + """Test RAGSearchResult can be created with citations.""" + result = RAGSearchResult(citations=citations) + assert result.citations is citations + + def test_str_returns_json(self, citations: Citations) -> None: + """Test __str__ returns JSON from citations.model_dump_json().""" + result = RAGSearchResult(citations=citations) + output = str(result) + + parsed = json.loads(output) + assert parsed["total_results"] == 2 + + +class TestRAGGenerateResult: + """Tests for RAGGenerateResult model.""" + + @pytest.fixture + def citations(self) -> Citations: + """Create Citations object.""" + return Citations(total_results=1, results=[]) + + def test_creation_with_answer_only(self) -> None: + """Test RAGGenerateResult can be created with just an answer.""" + result = RAGGenerateResult(answer="This is the answer.") + assert result.answer == "This is the answer." + assert result.citations is None + + def test_creation_with_citations(self, citations: Citations) -> None: + """Test RAGGenerateResult can be created with answer and citations.""" + result = RAGGenerateResult(answer="Answer with sources.", citations=citations) + assert result.answer == "Answer with sources." + assert result.citations is citations + + def test_str_without_citations(self) -> None: + """Test __str__ excludes citations when None.""" + result = RAGGenerateResult(answer="Just an answer.") + output = str(result) + + parsed = json.loads(output) + assert parsed["answer"] == "Just an answer." + assert "citations" not in parsed + + def test_str_with_citations(self, citations: Citations) -> None: + """Test __str__ includes citations when present.""" + result = RAGGenerateResult(answer="Answer.", citations=citations) + output = str(result) + + parsed = json.loads(output) + assert parsed["answer"] == "Answer." + assert "citations" in parsed diff --git a/packages/nvidia_nat_rag_lib/tests/test_rag_lib_function.py b/packages/nvidia_nat_rag_lib/tests/test_rag_lib_function.py index 1a251abc9a..6fc630f076 100644 --- a/packages/nvidia_nat_rag_lib/tests/test_rag_lib_function.py +++ b/packages/nvidia_nat_rag_lib/tests/test_rag_lib_function.py @@ -12,6 +12,24 @@ # 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. +"""Tests for NVIDIA RAG library integration. + +KNOWN ISSUE - Embedding Model Compatibility: + nvidia_rag.EmbeddingConfig always passes a `dimensions` parameter to the embedding API. + Some models (e.g., nvidia/nv-embedqa-e5-v5) reject this parameter entirely, causing errors: + "This model does not support 'dimensions', but a value of '2048' was provided." + + Compatible models: nvidia/llama-3.2-nv-embedqa-1b-v2 (supports dimensions param) + Incompatible models: nvidia/nv-embedqa-e5-v5 (fixed 1024-dim, rejects dimensions param) + + Upstream fix needed: nvidia_rag should allow dimensions=None to not pass the parameter. + +TODO: Add integration tests to catch config compatibility issues: + - Test search/generate with different embedding models + - Test with different LLM providers + - Test with different retriever configs (Milvus, NeMo) + - Parametrize tests across model combinations to catch API rejections early +""" from __future__ import annotations @@ -28,37 +46,63 @@ from nat.llm.nim_llm import NIMModelConfig from nat.retriever.milvus.register import MilvusRetrieverConfig +# NOTE: First nvidia_rag import takes ~20s due to module-level initialization. + # ============================================================================= # Fixtures # ============================================================================= - -@pytest.fixture -def mock_builder() -> MagicMock: - """Create mock NAT builder with component resolution.""" - builder: MagicMock = MagicMock() - - def get_llm_config(_ref: LLMRef) -> NIMModelConfig: - return NIMModelConfig( +LLM_CONFIGS: dict[str, NIMModelConfig] = { + "nim_llm_llama8b": + NIMModelConfig( model_name="meta/llama-3.1-8b-instruct", temperature=0.2, top_p=0.95, max_tokens=4096, - ) + ), + "nim_llm_llama70b": + NIMModelConfig( + model_name="meta/llama-3.1-70b-instruct", + temperature=0.1, + top_p=0.9, + max_tokens=4096, + ), +} + +EMBEDDER_CONFIGS: dict[str, NIMEmbedderModelConfig] = { + # nvidia/llama-3.2-nv-embedqa-1b-v2: supports dimensions parameter + "nim_embedder": NIMEmbedderModelConfig(model_name="nvidia/llama-3.2-nv-embedqa-1b-v2"), + # nvidia/nv-embedqa-e5-v5: REJECTS dimensions param + "nim_embedder_e5": NIMEmbedderModelConfig(model_name="nvidia/nv-embedqa-e5-v5"), +} + +RETRIEVER_CONFIGS: dict[str, MilvusRetrieverConfig] = { + "milvus_retriever": + MilvusRetrieverConfig( + uri=HttpUrl("http://localhost:19530"), + collection_name="test_collection", + embedding_model="nim_embedder", + ), +} + + +@pytest.fixture(name="mock_builder") +def fixture_mock_builder() -> MagicMock: + """Create mock NAT builder with component resolution.""" + builder: MagicMock = MagicMock() + + def get_llm_config(ref: LLMRef) -> NIMModelConfig: + return LLM_CONFIGS[str(ref)] builder.get_llm_config = MagicMock(side_effect=get_llm_config) def get_embedder_config(ref: EmbedderRef) -> NIMEmbedderModelConfig: - return NIMEmbedderModelConfig(model_name="nvidia/nv-embedqa-e5-v5") + return EMBEDDER_CONFIGS[str(ref)] builder.get_embedder_config = MagicMock(side_effect=get_embedder_config) - async def get_retriever_config(_ref: RetrieverRef) -> MilvusRetrieverConfig: - return MilvusRetrieverConfig( - uri=HttpUrl("http://localhost:19530"), - collection_name="test_collection", - embedding_model="nvidia/nv-embedqa-e5-v5", - ) + async def get_retriever_config(ref: RetrieverRef) -> MilvusRetrieverConfig: + return RETRIEVER_CONFIGS[str(ref)] builder.get_retriever_config = AsyncMock(side_effect=get_retriever_config) @@ -76,13 +120,12 @@ class TestConfigResolution: async def test_resolve_llm_from_ref(self, mock_builder: MagicMock) -> None: """Test LLMRef from NvidiaRAGLibConfig resolves to NvidiaRAGConfig.llm.""" - # NOTE: First nvidia_rag import takes ~20s due to module-level initialization. from nvidia_rag.utils.configuration import NvidiaRAGConfig from nat.plugins.rag_lib.client import _resolve_llm_config rag_config: NvidiaRAGConfig = NvidiaRAGConfig() - await _resolve_llm_config(LLMRef("llm_nim"), mock_builder, rag_config) + await _resolve_llm_config(LLMRef("nim_llm_llama8b"), mock_builder, rag_config) assert rag_config.llm.model_name == "meta/llama-3.1-8b-instruct" assert rag_config.llm.parameters.temperature == 0.2 @@ -164,9 +207,9 @@ async def test_resolve_embedder_from_ref(self, mock_builder: MagicMock) -> None: from nat.plugins.rag_lib.client import _resolve_embedder_config rag_config: NvidiaRAGConfig = NvidiaRAGConfig() - await _resolve_embedder_config(EmbedderRef("embedder_nim"), mock_builder, rag_config) + await _resolve_embedder_config(EmbedderRef("nim_embedder"), mock_builder, rag_config) - assert rag_config.embeddings.model_name == "nvidia/nv-embedqa-e5-v5" + assert rag_config.embeddings.model_name == "nvidia/llama-3.2-nv-embedqa-1b-v2" async def test_resolve_embedder_from_nim_config(self, mock_builder: MagicMock) -> None: """Test NIMEmbedderModelConfig from NvidiaRAGLibConfig resolves to NvidiaRAGConfig.embeddings.""" @@ -236,7 +279,7 @@ async def test_resolve_retriever_from_ref(self, mock_builder: MagicMock) -> None from nat.plugins.rag_lib.client import _resolve_retriever_config rag_config: NvidiaRAGConfig = NvidiaRAGConfig() - await _resolve_retriever_config(RetrieverRef("retriever_milvus"), mock_builder, rag_config) + await _resolve_retriever_config(RetrieverRef("milvus_retriever"), mock_builder, rag_config) assert rag_config.vector_store.name == "milvus" assert rag_config.vector_store.url == "http://localhost:19530/" @@ -341,10 +384,10 @@ async def test_pipeline_fields_with_defaults(self, mock_builder: MagicMock) -> N from nvidia_rag.utils.configuration import NvidiaRAGConfig from nat.plugins.rag_lib.client import NvidiaRAGLibConfig - from nat.plugins.rag_lib.client import build_nvidia_rag_config + from nat.plugins.rag_lib.client import _build_nvidia_rag_config config = NvidiaRAGLibConfig() - rag_config: NvidiaRAGConfig = await build_nvidia_rag_config(config, mock_builder) + rag_config: NvidiaRAGConfig = await _build_nvidia_rag_config(config, mock_builder) # Fields that always have values (from default_factory) assert rag_config.ranking is not None @@ -373,7 +416,7 @@ async def test_pipeline_fields_passthrough(self, mock_builder: MagicMock) -> Non from nvidia_rag.utils.configuration import VLMConfig from nat.plugins.rag_lib.client import NvidiaRAGLibConfig - from nat.plugins.rag_lib.client import build_nvidia_rag_config + from nat.plugins.rag_lib.client import _build_nvidia_rag_config from nat.plugins.rag_lib.config import RAGPipelineConfig custom_ranking = RankingConfig(enable_reranker=False) @@ -394,7 +437,7 @@ async def test_pipeline_fields_passthrough(self, mock_builder: MagicMock) -> Non ) config = NvidiaRAGLibConfig(rag_pipeline=pipeline) - rag_config: NvidiaRAGConfig = await build_nvidia_rag_config(config, mock_builder) + rag_config: NvidiaRAGConfig = await _build_nvidia_rag_config(config, mock_builder) # Explicit values passed through assert rag_config.ranking.enable_reranker is False @@ -421,7 +464,7 @@ async def test_none_optional_fields_get_defaults(self, mock_builder: MagicMock) from nvidia_rag.utils.configuration import VLMConfig from nat.plugins.rag_lib.client import NvidiaRAGLibConfig - from nat.plugins.rag_lib.client import build_nvidia_rag_config + from nat.plugins.rag_lib.client import _build_nvidia_rag_config from nat.plugins.rag_lib.config import RAGPipelineConfig # Explicitly set optional fields to None @@ -434,7 +477,7 @@ async def test_none_optional_fields_get_defaults(self, mock_builder: MagicMock) ) config = NvidiaRAGLibConfig(rag_pipeline=pipeline) - rag_config: NvidiaRAGConfig = await build_nvidia_rag_config(config, mock_builder) + rag_config: NvidiaRAGConfig = await _build_nvidia_rag_config(config, mock_builder) # All should be valid config objects, not None assert isinstance(rag_config.vlm, VLMConfig) @@ -484,96 +527,146 @@ def test_health_method_exists(self) -> None: @pytest.mark.integration class TestNvidiaRAGIntegration: - """Test NvidiaRAG generate(), search(), and health() methods with running services. + """Parameterized tests for NvidiaRAG generate(), search(), and health() methods.""" - Requires: Milvus, NVIDIA API key. - """ - - @pytest.fixture - def milvus_test_collection(self): - """Create test collection with proper schema for nvidia-rag.""" + @pytest.fixture(name="create_collection") + def fixture_create_collection(self): + """Factory to create Milvus collections with specific embedding models.""" from langchain_core.documents import Document from langchain_milvus import Milvus from langchain_nvidia_ai_endpoints import NVIDIAEmbeddings from pymilvus import MilvusClient - collection_name = "test_collection" - client = MilvusClient(uri="http://localhost:19530") + created: list[str] = [] - if client.has_collection(collection_name): - client.drop_collection(collection_name) - - embeddings = NVIDIAEmbeddings(model="nvidia/nv-embedqa-e5-v5") - Milvus.from_documents( - documents=[ - Document( - page_content="Test document about machine learning", - metadata={"source": "test_doc.txt"}, - ) - ], - embedding=embeddings, - collection_name=collection_name, - connection_args={"uri": "http://localhost:19530"}, - ) + def _create(embedder_ref: str) -> str: + import re + + model_name = EMBEDDER_CONFIGS[embedder_ref].model_name + sanitized = re.sub(r"[^a-zA-Z0-9_]", "_", model_name) + collection_name = f"test_{sanitized}" + client = MilvusClient(uri="http://localhost:19530") + if client.has_collection(collection_name): + client.drop_collection(collection_name) - yield collection_name + embeddings = NVIDIAEmbeddings(model=model_name) + Milvus.from_documents( + documents=[Document(page_content="Test document", metadata={"source": "test"})], + embedding=embeddings, + collection_name=collection_name, + connection_args={"uri": "http://localhost:19530"}, + ) + created.append(collection_name) + return collection_name - client.drop_collection(collection_name) + yield _create - async def test_generate_method(self, mock_builder: MagicMock) -> None: - """Test NvidiaRAG generate() returns a result.""" + client = MilvusClient(uri="http://localhost:19530") + for name in created: + if client.has_collection(name): + client.drop_collection(name) + + @pytest.mark.parametrize("llm_ref", list(LLM_CONFIGS.keys())) + @pytest.mark.parametrize( + "embedder_ref", + [ + "nim_embedder", + # TODO: nvidia_rag always passes dimensions param which nv-embedqa-e5-v5 rejects + # "nim_embedder_e5", + ]) + @pytest.mark.parametrize("retriever_ref", list(RETRIEVER_CONFIGS.keys())) + async def test_search( + self, + mock_builder: MagicMock, + create_collection, + llm_ref: str, + embedder_ref: str, + retriever_ref: str, + ) -> None: + """Test NvidiaRAG search() with different component configs.""" from nvidia_rag import NvidiaRAG from nat.plugins.rag_lib.client import NvidiaRAGLibConfig - from nat.plugins.rag_lib.client import build_nvidia_rag_config + from nat.plugins.rag_lib.client import _build_nvidia_rag_config + + collection_name = create_collection(embedder_ref) config = NvidiaRAGLibConfig( - llm=LLMRef("llm"), - embedder=EmbedderRef("embedder"), - retriever=RetrieverRef("retriever"), + llm=LLMRef(llm_ref), + embedder=EmbedderRef(embedder_ref), + retriever=RetrieverRef(retriever_ref), ) - rag_config = await build_nvidia_rag_config(config, mock_builder) + rag_config = await _build_nvidia_rag_config(config, mock_builder) + rag_config.vector_store.default_collection_name = collection_name rag = NvidiaRAG(config=rag_config) - messages = [{"role": "user", "content": "What is RAG?"}] - result = await rag.generate(messages=messages, use_knowledge_base=False) + result = await rag.search(query="test query") assert result is not None - async def test_search_method(self, mock_builder: MagicMock, milvus_test_collection: str) -> None: - """Test NvidiaRAG search() returns a result.""" + @pytest.mark.parametrize("llm_ref", list(LLM_CONFIGS.keys())) + @pytest.mark.parametrize( + "embedder_ref", + [ + "nim_embedder", + # TODO: nvidia_rag always passes dimensions param which nv-embedqa-e5-v5 rejects + # "nim_embedder_e5", + ]) + @pytest.mark.parametrize("retriever_ref", list(RETRIEVER_CONFIGS.keys())) + async def test_generate( + self, + mock_builder: MagicMock, + llm_ref: str, + embedder_ref: str, + retriever_ref: str, + ) -> None: + """Test NvidiaRAG generate() with different component configs.""" from nvidia_rag import NvidiaRAG from nat.plugins.rag_lib.client import NvidiaRAGLibConfig - from nat.plugins.rag_lib.client import build_nvidia_rag_config + from nat.plugins.rag_lib.client import _build_nvidia_rag_config config = NvidiaRAGLibConfig( - llm=LLMRef("llm"), - embedder=EmbedderRef("embedder"), - retriever=RetrieverRef("retriever"), + llm=LLMRef(llm_ref), + embedder=EmbedderRef(embedder_ref), + retriever=RetrieverRef(retriever_ref), ) - rag_config = await build_nvidia_rag_config(config, mock_builder) - rag_config.vector_store.default_collection_name = milvus_test_collection - rag_config.embeddings.dimensions = None + rag_config = await _build_nvidia_rag_config(config, mock_builder) rag = NvidiaRAG(config=rag_config) - result = await rag.search(query="What is machine learning?") + messages = [{"role": "user", "content": "What is RAG?"}] + result = await rag.generate(messages=messages, use_knowledge_base=False) assert result is not None - async def test_health_method(self, mock_builder: MagicMock) -> None: - """Test NvidiaRAG health() returns a result.""" + @pytest.mark.parametrize("llm_ref", list(LLM_CONFIGS.keys())) + @pytest.mark.parametrize( + "embedder_ref", + [ + "nim_embedder", + # TODO: nvidia_rag always passes dimensions param which nv-embedqa-e5-v5 rejects + # "nim_embedder_e5", + ]) + @pytest.mark.parametrize("retriever_ref", list(RETRIEVER_CONFIGS.keys())) + async def test_health( + self, + mock_builder: MagicMock, + llm_ref: str, + embedder_ref: str, + retriever_ref: str, + ) -> None: + """Test NvidiaRAG health() with different component configs.""" from nvidia_rag import NvidiaRAG from nat.plugins.rag_lib.client import NvidiaRAGLibConfig - from nat.plugins.rag_lib.client import build_nvidia_rag_config + from nat.plugins.rag_lib.client import _build_nvidia_rag_config config = NvidiaRAGLibConfig( - llm=LLMRef("llm"), - embedder=EmbedderRef("embedder"), - retriever=RetrieverRef("retriever"), + llm=LLMRef(llm_ref), + embedder=EmbedderRef(embedder_ref), + retriever=RetrieverRef(retriever_ref), ) - rag_config = await build_nvidia_rag_config(config, mock_builder) + rag_config = await _build_nvidia_rag_config(config, mock_builder) rag = NvidiaRAG(config=rag_config) result = await rag.health() diff --git a/packages/nvidia_nat_rag_lib/tests/test_tools.py b/packages/nvidia_nat_rag_lib/tests/test_tools.py new file mode 100644 index 0000000000..39f662dea3 --- /dev/null +++ b/packages/nvidia_nat_rag_lib/tests/test_tools.py @@ -0,0 +1,156 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-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. + +from unittest.mock import AsyncMock +from unittest.mock import MagicMock + +import pytest +from nvidia_rag.rag_server.response_generator import Citations + +from nat.builder.builder import Builder +from nat.builder.function import LambdaFunction +from nat.data_models.component_ref import FunctionGroupRef +from nat.plugins.rag_lib.client import NvidiaRAGFunctionGroup +from nat.plugins.rag_lib.client import NvidiaRAGLibConfig +from nat.plugins.rag_lib.models import RAGGenerateResult +from nat.plugins.rag_lib.models import RAGSearchResult +from nat.plugins.rag_lib.tools.generate import RAGLibGenerateConfig +from nat.plugins.rag_lib.tools.generate import rag_lib_generate +from nat.plugins.rag_lib.tools.search import RAGLibSearchConfig +from nat.plugins.rag_lib.tools.search import rag_lib_search + + +class TestNvidiaRAGSearchTool: + + @pytest.fixture + def mock_builder(self) -> MagicMock: + return MagicMock(spec=Builder) + + @pytest.fixture + def tool_config(self) -> RAGLibSearchConfig: + return RAGLibSearchConfig( + rag_client=FunctionGroupRef("rag_client"), + collection_names=["test_collection"], + reranker_top_k=5, + ) + + @pytest.fixture + def function_group(self) -> NvidiaRAGFunctionGroup: + config = NvidiaRAGLibConfig() + group = NvidiaRAGFunctionGroup(config=config) + group.rag_client = MagicMock() + return group + + async def test_search_returns_results(self, + tool_config: RAGLibSearchConfig, + mock_builder: MagicMock, + function_group: NvidiaRAGFunctionGroup) -> None: + function_group.rag_client.search = AsyncMock(return_value=Citations(total_results=3, results=[])) + mock_builder.get_function_group = AsyncMock(return_value=function_group) + + async with rag_lib_search(tool_config, mock_builder) as fn_info: + tool = LambdaFunction.from_info(config=tool_config, info=fn_info, instance_name="search") + result = await tool.acall_invoke(query="test query") + + assert isinstance(result, RAGSearchResult) + assert result.citations.total_results == 3 + + async def test_search_handles_error(self, + tool_config: RAGLibSearchConfig, + mock_builder: MagicMock, + function_group: NvidiaRAGFunctionGroup) -> None: + function_group.rag_client.search = AsyncMock(side_effect=Exception("Search failed")) + mock_builder.get_function_group = AsyncMock(return_value=function_group) + + async with rag_lib_search(tool_config, mock_builder) as fn_info: + tool = LambdaFunction.from_info(config=tool_config, info=fn_info, instance_name="search") + result = await tool.acall_invoke(query="test query") + + assert isinstance(result, RAGSearchResult) + assert result.citations.total_results == 0 + + +class TestNvidiaRAGGenerateTool: + + @pytest.fixture + def mock_builder(self) -> MagicMock: + return MagicMock(spec=Builder) + + @pytest.fixture + def tool_config(self) -> RAGLibGenerateConfig: + return RAGLibGenerateConfig( + rag_client=FunctionGroupRef("rag_client"), + use_knowledge_base=True, + enable_citations=True, + ) + + @pytest.fixture + def function_group(self) -> NvidiaRAGFunctionGroup: + config = NvidiaRAGLibConfig() + group = NvidiaRAGFunctionGroup(config=config) + group.rag_client = MagicMock() + return group + + async def test_generate_returns_answer(self, + tool_config: RAGLibGenerateConfig, + mock_builder: MagicMock, + function_group: NvidiaRAGFunctionGroup) -> None: + + async def mock_stream(): + yield 'data: {"id": "1", "model": "m", "choices": [{"delta": {"content": "Hello"}}]}' + yield 'data: {"id": "1", "model": "m", "choices": [{"delta": {"content": " world"}}]}' + yield 'data: [DONE]' + + function_group.rag_client.generate = AsyncMock(return_value=mock_stream()) + mock_builder.get_function_group = AsyncMock(return_value=function_group) + + async with rag_lib_generate(tool_config, mock_builder) as fn_info: + tool = LambdaFunction.from_info(config=tool_config, info=fn_info, instance_name="generate") + result = await tool.acall_invoke(query="test") + + assert isinstance(result, RAGGenerateResult) + assert result.answer == "Hello world" + + async def test_generate_handles_error(self, + tool_config: RAGLibGenerateConfig, + mock_builder: MagicMock, + function_group: NvidiaRAGFunctionGroup) -> None: + function_group.rag_client.generate = AsyncMock(side_effect=Exception("Generate failed")) + mock_builder.get_function_group = AsyncMock(return_value=function_group) + + async with rag_lib_generate(tool_config, mock_builder) as fn_info: + tool = LambdaFunction.from_info(config=tool_config, info=fn_info, instance_name="generate") + result = await tool.acall_invoke(query="test") + + assert isinstance(result, RAGGenerateResult) + assert "Error generating answer" in result.answer + + async def test_generate_handles_empty_stream(self, + tool_config: RAGLibGenerateConfig, + mock_builder: MagicMock, + function_group: NvidiaRAGFunctionGroup) -> None: + + async def mock_empty_stream(): + yield 'data: [DONE]' + + function_group.rag_client.generate = AsyncMock(return_value=mock_empty_stream()) + mock_builder.get_function_group = AsyncMock(return_value=function_group) + + async with rag_lib_generate(tool_config, mock_builder) as fn_info: + tool = LambdaFunction.from_info(config=tool_config, info=fn_info, instance_name="generate") + result = await tool.acall_invoke(query="test") + + assert isinstance(result, RAGGenerateResult) + assert result.answer == "No response generated." diff --git a/scripts/langchain_web_ingest.py b/scripts/langchain_web_ingest.py index 604b26099e..4c76da348d 100644 --- a/scripts/langchain_web_ingest.py +++ b/scripts/langchain_web_ingest.py @@ -131,6 +131,7 @@ async def main(*, parser.add_argument("--collection_name", "-n", default=CUDA_COLLECTION_NAME, help="Collection name for the data.") parser.add_argument("--milvus_uri", "-u", default=DEFAULT_URI, help="Milvus host URI") parser.add_argument("--clean_cache", default=False, help="If true, deletes local files", action="store_true") + parser.add_argument("--embedding_model", "-e", default="nvidia/nv-embedqa-e5-v5", help="Embedding model to use") args = parser.parse_args() if len(args.urls) == 0: @@ -142,4 +143,5 @@ async def main(*, milvus_uri=args.milvus_uri, collection_name=args.collection_name, clean_cache=args.clean_cache, + embedding_model=args.embedding_model, )) diff --git a/src/nat/embedder/nim_embedder.py b/src/nat/embedder/nim_embedder.py index 85c27c0fa3..859ecd58ca 100644 --- a/src/nat/embedder/nim_embedder.py +++ b/src/nat/embedder/nim_embedder.py @@ -50,6 +50,7 @@ class NIMEmbedderModelConfig(EmbedderBaseConfig, RetryMixin, name="nim"): truncate: TruncationOption = Field(default="NONE", description=("The truncation strategy if the input on the " "server side if it's too large.")) + dimensions: int | None = Field(default=None, description="Embedding output dimensions.") model_config = ConfigDict(protected_namespaces=(), extra="allow") From 5676a6d57687df997759b9bb4eba610d7db497ba Mon Sep 17 00:00:00 2001 From: Eric Evans <194135482+ericevans-nv@users.noreply.github.com> Date: Wed, 21 Jan 2026 19:18:59 -0600 Subject: [PATCH 05/15] Add Advanced RAG example using nvidia_rag_lib package Signed-off-by: Eric Evans <194135482+ericevans-nv@users.noreply.github.com> --- examples/RAG/simple_rag/README.md | 234 ++++++++++++++++-- .../configs/rag_library_mode_config.yml | 51 +--- .../src/nat/plugins/rag_lib/client.py | 114 +++++++-- .../src/nat/plugins/rag_lib/register.py | 1 - .../src/nat/plugins/rag_lib/tools/__init__.py | 14 -- .../src/nat/plugins/rag_lib/tools/generate.py | 132 ---------- .../src/nat/plugins/rag_lib/tools/register.py | 20 -- .../src/nat/plugins/rag_lib/tools/search.py | 93 ------- .../nvidia_nat_rag_lib/tests/test_tools.py | 168 ++++++------- scripts/langchain_web_ingest.py | 15 ++ 10 files changed, 420 insertions(+), 422 deletions(-) delete mode 100644 packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/__init__.py delete mode 100644 packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/generate.py delete mode 100644 packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/register.py delete mode 100644 packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/search.py diff --git a/examples/RAG/simple_rag/README.md b/examples/RAG/simple_rag/README.md index b6fe016880..513d9351f2 100644 --- a/examples/RAG/simple_rag/README.md +++ b/examples/RAG/simple_rag/README.md @@ -26,25 +26,41 @@ This is a simple example RAG application to showcase how one can configure and u ## Table of Contents -- [Key Features](#key-features) -- [Quickstart: RAG with Milvus](#quickstart-rag-with-milvus) - - [Installation and Setup](#installation-and-setup) - - [Install this Workflow](#install-this-workflow) - - [Set Up Milvus](#set-up-milvus) - - [Set Up API Keys](#set-up-api-keys) - - [Bootstrap Data](#bootstrap-data) - - [Configure Your Agent](#configure-your-agent) - - [Run the Workflow](#run-the-workflow) -- [Adding Long-Term Agent Memory](#adding-long-term-agent-memory) - - [Prerequisites](#prerequisites) - - [Adding Memory to the Agent](#adding-memory-to-the-agent) -- [Adding Additional Tools](#adding-additional-tools) -- [Using Test Time Compute](#using-test-time-compute) +- [Simple RAG Example](#simple-rag-example) + - [Table of Contents](#table-of-contents) + - [Key Features](#key-features) + - [Quickstart: RAG with Milvus](#quickstart-rag-with-milvus) + - [Installation and Setup](#installation-and-setup) + - [Install this Workflow](#install-this-workflow) + - [Set Up Milvus](#set-up-milvus) + - [Set Up API Keys](#set-up-api-keys) + - [Bootstrap Data](#bootstrap-data) + - [Configure Your Agent](#configure-your-agent) + - [Run the Workflow](#run-the-workflow) + - [Adding Long-Term Agent Memory](#adding-long-term-agent-memory) + - [Prerequisites](#prerequisites) + - [Adding Memory to the Agent](#adding-memory-to-the-agent) + - [Adding Additional Tools](#adding-additional-tools) + - [Using Test Time Compute](#using-test-time-compute) + - [Advanced RAG with NVIDIA RAG Library](#advanced-rag-with-nvidia-rag-library) + - [What the Library Provides](#what-the-library-provides) + - [Prerequisites](#prerequisites-1) + - [Bootstrap Data](#bootstrap-data-1) + - [How the Pipeline Works](#how-the-pipeline-works) + - [Two-Stage Retrieval](#two-stage-retrieval) + - [Query Rewriting](#query-rewriting) + - [Confidence Filtering](#confidence-filtering) + - [Structured Citations](#structured-citations) + - [Integration with NAT Components](#integration-with-nat-components) + - [RAG-Specific Configuration](#rag-specific-configuration) + - [Example Configuration](#example-configuration) + - [Run the Workflow](#run-the-workflow-1) ## Key Features - **Milvus Vector Database Integration:** Demonstrates the `milvus_retriever` component for storing and retrieving document embeddings from CUDA and MCP documentation. - **ReAct Agent with RAG:** Shows how a `react_agent` can use retriever tools to answer questions by searching through indexed documentation. +- **Advanced RAG Pipeline with NVIDIA RAG Library:** Showcases enhanced retrieval with semantic reranking, query rewriting, confidence filtering, and structured citations. - **Long-term Memory with Mem0:** Includes integration with Mem0 platform for persistent memory, allowing the agent to remember user preferences across sessions. - **Multi-Collection Retrieval:** Demonstrates multiple retriever tools (`cuda_retriever_tool` and `mcp_retriever_tool`) for searching different knowledge bases. - **Additional Tool Integration:** Shows how to extend the RAG system with complementary tools like `tavily_internet_search` and `code_generation` for comprehensive question answering. @@ -353,3 +369,193 @@ The final workflow result should look similar to the following: ```console ['CUDA and MCP are two distinct technologies with different purposes and cannot be directly compared. CUDA is a parallel computing platform and programming model, primarily used for compute-intensive tasks such as scientific simulations, data analytics, and machine learning, whereas MCP is an open protocol designed for providing context to Large Language Models (LLMs), particularly for natural language processing and other AI-related tasks. While they serve different purposes, CUDA and MCP share a common goal of enabling developers to create powerful and efficient applications. They are complementary technologies that can be utilized together in certain applications to achieve innovative outcomes, although their differences in design and functionality set them apart. In essence, CUDA focuses on parallel computing and is developed by NVIDIA, whereas MCP is focused on context provision for LLMs, making them unique in their respective fields but potentially synergistic in specific use cases.'] ``` + +## Advanced RAG with NVIDIA RAG Library + +The NVIDIA RAG Library plugin (`nvidia_rag_lib`) integrates the [NVIDIA RAG Blueprint](https://github.com/NVIDIA-AI-Blueprints/rag) pipeline into NeMo Agent Toolkit. The NVIDIA RAG Blueprint is NVIDIA's reference solution for building production RAG systems that ground AI responses in enterprise knowledge, reducing hallucinations and ensuring accuracy. + +The library handles the complexity of multi-stage retrieval, semantic reranking, and query optimization, allowing you to focus on building your application rather than implementing RAG infrastructure. + +### What the Library Provides + +The `nvidia_rag_lib` plugin provides agent tools powered by the NVIDIA RAG pipeline. + +- **Multi-stage retrieval** with configurable candidate pools and reranking +- **Semantic reranking** using NeMo Retriever models +- **Query rewriting** via LLM-based query optimization +- **Confidence filtering** to ensure result quality +- **Structured citations** for source attribution +- **Multi-collection search** across multiple knowledge bases + +All of these features are managed by the library and configured declaratively in YAML, with no custom code required. + +### Prerequisites + +Install the NVIDIA RAG Library plugin: +```bash +uv pip install -e packages/nvidia_nat_rag_lib +``` + +### Bootstrap Data + +The NVIDIA RAG Library example uses a different embedding model (`nvidia/llama-3.2-nv-embedqa-1b-v2`) than the basic quickstart. If you have an existing `cuda_docs` collection from the quickstart, drop and re-ingest with the correct embedding model: + +```bash +python scripts/langchain_web_ingest.py \ + -n cuda_docs \ + -e nvidia/llama-3.2-nv-embedqa-1b-v2 \ + --drop_collection +``` + +### How the Pipeline Works + +The `nvidia_rag_lib` plugin orchestrates a multi-stage retrieval pipeline based on the NVIDIA RAG Blueprint. Each stage is handled automatically based on your configuration. + +#### Two-Stage Retrieval + +A recall-then-precision approach balances thoroughness with relevance: + +1. **Stage 1 - Vector Search (Recall):** A large candidate pool is retrieved using embedding similarity, casting a wide net to ensure relevant documents are not missed. + +2. **Stage 2 - Reranking (Precision):** Candidates pass through a semantic reranker that scores relevance to the query, narrowing down to the most relevant results. + +``` +Query → Embed → Retrieve candidates → Rerank → Final results +``` + +#### Query Rewriting + +When enabled, an LLM reformulates user queries before searching. This helps when: +- User queries are conversational or ambiguous +- Technical terminology varies across documents +- Queries benefit from expansion or clarification + +#### Confidence Filtering + +Results below a confidence threshold are automatically filtered out, preventing low-quality matches from reaching the agent. + +#### Structured Citations + +Search results include document metadata (document name, relevance score) in a structured format, enabling source attribution and traceability in responses. + +### Integration with NAT Components + +The `nvidia_rag_lib` plugin integrates with standard NeMo Agent Toolkit components. You configure `llms`, `embedders`, and `retrievers` sections as usual. The plugin references these components by name: + +```yaml +function_groups: + cuda_qa: + _type: nvidia_rag_lib + llm: nim_llm # References llms.nim_llm + embedder: nim_embedder # References embedders.nim_embedder + retriever: cuda_retriever # References retrievers.cuda_retriever +``` + +This means you can reuse existing NAT infrastructure definitions and swap in the RAG library without changing your LLM, embedder, or retriever configurations. + +### RAG-Specific Configuration + +The plugin adds configuration specific to the RAG pipeline. These fields differ from a standard NAT retriever setup: + +| Field | Purpose | +|-------|---------| +| `topic` | Description for agent tool selection | +| `collection_names` | Milvus collections to search | +| `reranker_top_k` | Number of results after reranking | +| `rag_pipeline.enable_citations` | Include document metadata in results | +| `rag_pipeline.default_confidence_threshold` | Filter low-confidence results | +| `rag_pipeline.ranking.enable_reranker` | Enable semantic reranking | +| `rag_pipeline.ranking.model_name` | Reranker model to use | +| `rag_pipeline.query_rewriter.enabled` | Enable LLM query rewriting | + +### Example Configuration + +```yaml +function_groups: + cuda_qa: + _type: nvidia_rag_lib + include: + - search + llm: nim_llm + embedder: nim_embedder + retriever: cuda_retriever + topic: NVIDIA CUDA library + collection_names: + - cuda_docs + reranker_top_k: 10 + rag_pipeline: + enable_citations: true + default_confidence_threshold: 0.25 + ranking: + enable_reranker: true + model_name: nvidia/llama-3.2-nv-rerankqa-1b-v2 + query_rewriter: + enabled: true +``` + +### Run the Workflow + +```bash +nat run --config_file examples/RAG/simple_rag/configs/rag_library_mode_config.yml \ + --input "How do I install CUDA" +``` + +The logs show the pipeline stages in action: + +```console +INFO:nvidia_rag.rag_server.main:Setting top k as: 100. +INFO:nvidia_rag.rag_server.main:Narrowing the collection from 100 results and further narrowing it to 10 with the reranker for search +INFO:nvidia_rag.rag_server.main:Setting ranker top n as: 10. +INFO:nvidia_rag.utils.vdb.milvus.milvus_vdb: Milvus Retrieval latency: 0.8911 seconds +INFO:nvidia_rag.rag_server.main: == Context reranker time: 5631.08 ms == +INFO:nvidia_rag.utils.common:Confidence threshold filtering: 10 -> 10 documents (threshold: 0.25) +``` + +The agent decides to search the knowledge base and retrieves grounded document excerpts: + +```console +[AGENT] +Agent input: How do I install CUDA +Agent's thoughts: +Thought: To answer the user's question about installing CUDA, I need to provide them with the correct steps and requirements. + +Action: cuda_search__search +Action Input: {'query': 'CUDA installation steps'} +``` + +The search tool returns structured citations in JSON format: + +```console +[AGENT] +Calling tools: cuda_search__search +Tool's input: {'query': 'CUDA installation steps'} +Tool's response: +{"total_results":10,"results":[{"document_id":"","content":"Note\nFor both native as well as cross development, +the toolkit must be installed using the distribution-specific installer... +Download the NVIDIA CUDA Toolkit from https://developer.nvidia.com/cuda-downloads. +Choose the platform you are using and download the NVIDIA CUDA Toolkit... +...(truncated)"},...]} +``` + +The agent synthesizes a comprehensive, grounded response with specific commands for multiple platforms: + +```console +['To install CUDA, you can follow these steps: + +1. Verify that you have a CUDA-capable GPU. +2. Download the NVIDIA CUDA Toolkit from https://developer.nvidia.com/cuda-downloads. +3. Install the NVIDIA CUDA Toolkit. The installation steps may vary depending on your operating system. +4. Test that the installed software runs correctly and communicates with the hardware. + +For example, on Ubuntu, you can install CUDA using the following commands: +# apt update +# apt install cuda-toolkit + +On Windows, you can use the network installer or full installer. + +Additionally, you can use Conda to install CUDA: +$ conda install cuda -c nvidia + +You can also use pip wheels: +$ python3 -m pip install nvidia-cuda-runtime-cu12'] +``` diff --git a/examples/RAG/simple_rag/configs/rag_library_mode_config.yml b/examples/RAG/simple_rag/configs/rag_library_mode_config.yml index 3bd0d8e097..1d8a24aa54 100644 --- a/examples/RAG/simple_rag/configs/rag_library_mode_config.yml +++ b/examples/RAG/simple_rag/configs/rag_library_mode_config.yml @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. + llms: nim_llm: _type: nim @@ -28,7 +29,7 @@ embedders: truncate: "END" retrievers: - milvus_retriever: + cuda_retriever: _type: milvus_retriever uri: http://localhost:19530 collection_name: cuda_docs @@ -36,55 +37,29 @@ retrievers: top_k: 100 function_groups: - rag_client: + cuda_search: _type: nvidia_rag_lib + include: + - search llm: nim_llm embedder: nim_embedder - retriever: milvus_retriever + retriever: cuda_retriever + topic: NVIDIA CUDA library + collection_names: + - cuda_docs + reranker_top_k: 10 rag_pipeline: enable_citations: true default_confidence_threshold: 0.25 - ranking: - top_k: 10 enable_reranker: true model_name: nvidia/llama-3.2-nv-rerankqa-1b-v2 - - # TODO: Re-enable once library bug is fixed. - # BUG: enable_reflection breaks search() in nvidia_rag. - # Missing 'await' on check_context_relevance() at nvidia_rag/rag_server/main.py:1112 - # reflection: - # enable_reflection: true - # max_loops: 2 - # context_relevance_threshold: 1 - # response_groundedness_threshold: 1 - -functions: - search: - _type: rag_lib_search - rag_client: rag_client - topic: NVIDIA technologies - description: "Search NVIDIA documentation (CUDA, MCP) with AI-enhanced query understanding. Use for any technical question." - collection_names: - - cuda_docs - - mcp_docs - reranker_top_k: 10 - - generate: - _type: rag_lib_generate - rag_client: rag_client - topic: NVIDIA technologies - description: "Generate a high-quality, self-verified answer with citations. Uses reflection to reduce errors." - collection_names: - - cuda_docs - - mcp_docs - use_knowledge_base: true - enable_citations: true + query_rewriter: + enabled: true workflow: _type: react_agent tool_names: - - search - - generate + - cuda_search verbose: true llm_name: nim_llm diff --git a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/client.py b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/client.py index f30ccb6817..158403ade8 100644 --- a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/client.py +++ b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/client.py @@ -14,6 +14,7 @@ # limitations under the License. import logging +from collections.abc import AsyncGenerator from logging import Logger from typing import TYPE_CHECKING @@ -35,6 +36,7 @@ from nat.data_models.component_ref import EmbedderRef from nat.data_models.component_ref import LLMRef from nat.data_models.component_ref import RetrieverRef +from nat.data_models.finetuning import OpenAIMessage from nat.data_models.function import FunctionGroupBaseConfig from nat.embedder.nim_embedder import NIMEmbedderModelConfig from nat.llm.nim_llm import NIMModelConfig @@ -42,50 +44,121 @@ from nat.plugins.rag_lib.config import LLMConfigType from nat.plugins.rag_lib.config import RAGPipelineConfig from nat.plugins.rag_lib.config import RetrieverConfigType +from nat.plugins.rag_lib.models import RAGGenerateResult +from nat.plugins.rag_lib.models import RAGSearchResult from nat.retriever.milvus.register import MilvusRetrieverConfig from nat.retriever.nemo_retriever.register import NemoRetrieverConfig if TYPE_CHECKING: - from nvidia_rag import NvidiaRAG + from nvidia_rag.rag_server.response_generator import RAGResponse logger: Logger = logging.getLogger(__name__) -class NvidiaRAGFunctionGroup(FunctionGroup): - """FunctionGroup wrapper that exposes the NvidiaRAG client.""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.rag_client: NvidiaRAG | None = None - - class NvidiaRAGLibConfig(FunctionGroupBaseConfig, name="nvidia_rag_lib"): """Configuration for NVIDIA RAG Library. - All component configs are optional - NvidiaRAGConfig provides defaults. + Exposes search and generate tools that share a single RAG client. """ llm: LLMConfigType = Field(default=None, description="LLM configuration") embedder: EmbedderConfigType = Field(default=None, description="Embedder configuration") retriever: RetrieverConfigType = Field(default=None, description="Vector store configuration") - rag_pipeline: RAGPipelineConfig = Field(default_factory=RAGPipelineConfig) + topic: str | None = Field(default=None, description="Topic for tool descriptions.") + collection_names: list[str] | None = Field(default=None, description="Collections to query.") + reranker_top_k: int = Field(default=10, ge=1, description="Number of results after reranking.") + @register_function_group(config_type=NvidiaRAGLibConfig) async def nvidia_rag_lib(config: NvidiaRAGLibConfig, builder: Builder): - """Initialize NVIDIA RAG client and expose via FunctionGroup.""" + """NVIDIA RAG Library - exposes search and generate tools.""" try: from nvidia_rag import NvidiaRAG except ImportError as e: raise ImportError("nvidia-rag package is not installed.") from e - group = NvidiaRAGFunctionGroup(config=config) - rag_config: NvidiaRAGConfig = await _build_nvidia_rag_config(config, builder) - group.rag_client = NvidiaRAG(config=rag_config) + rag_client: NvidiaRAG = NvidiaRAG(config=rag_config) logger.info("NVIDIA RAG client initialized") + topic_str: str = f" about {config.topic}" if config.topic else "" + + async def search(query: str) -> RAGSearchResult: + """Search for relevant documents.""" + from nvidia_rag.rag_server.response_generator import Citations + + try: + citations: Citations = await rag_client.search( + query=query, + collection_names=config.collection_names, + reranker_top_k=config.reranker_top_k, + ) + return RAGSearchResult(citations=citations) + except Exception: + logger.exception("RAG search failed") + return RAGSearchResult(citations=Citations(total_results=0, results=[])) + + async def generate(query: str) -> RAGGenerateResult: + """Generate an answer using the knowledge base.""" + from nvidia_rag.rag_server.response_generator import ChainResponse + from nvidia_rag.rag_server.response_generator import Citations + + chunks: list[str] = [] + final_citations: Citations | None = None + + try: + response: RAGResponse = await rag_client.generate( + messages=[OpenAIMessage(role="user", content=query).model_dump()], + collection_names=config.collection_names, + reranker_top_k=config.reranker_top_k, + ) + + stream: AsyncGenerator[str, None] = (response.generator if hasattr(response, "generator") else response) + + async for raw_chunk in stream: + if raw_chunk.startswith("data: "): + raw_chunk = raw_chunk[len("data: "):].strip() + if not raw_chunk or raw_chunk == "[DONE]": + continue + try: + parsed: ChainResponse = ChainResponse.model_validate_json(raw_chunk) + if parsed.choices: + choice = parsed.choices[0] + if choice.delta and choice.delta.content: + content = choice.delta.content + if isinstance(content, str): + chunks.append(content) + if parsed.citations and parsed.citations.results: + final_citations = parsed.citations + except Exception: + continue + + answer: str = "".join(chunks) if chunks else "No response generated." + return RAGGenerateResult(answer=answer, citations=final_citations) + + except Exception: + logger.exception("RAG generate failed") + return RAGGenerateResult(answer="Error generating answer. Please try again.") + + group = FunctionGroup(config=config) + + group.add_function( + "search", + search, + description=( + f"Retrieve grounded excerpts{topic_str}. " + "Returns document chunks from indexed sources - use this to ground your response in cited source material " + "rather than general knowledge."), + ) + group.add_function( + "generate", + generate, + description=(f"Generate a grounded, cited answer{topic_str}. " + "Synthesizes an answer from retrieved documents, ensuring the response is grounded in cited " + "source material rather than general knowledge."), + ) yield group @@ -94,7 +167,6 @@ async def _build_nvidia_rag_config(config: NvidiaRAGLibConfig, builder: Builder) pipeline: RAGPipelineConfig = config.rag_pipeline - # Create base config with pipeline settings and defaults rag_config: NvidiaRAGConfig = NvidiaRAGConfig( ranking=pipeline.ranking, retriever=pipeline.search_settings, @@ -110,7 +182,6 @@ async def _build_nvidia_rag_config(config: NvidiaRAGLibConfig, builder: Builder) default_confidence_threshold=pipeline.default_confidence_threshold, ) - # Resolve and map each component's fields (mutates rag_config) await _resolve_llm_config(config.llm, builder, rag_config) await _resolve_embedder_config(config.embedder, builder, rag_config) await _resolve_retriever_config(config.retriever, builder, rag_config) @@ -144,7 +215,6 @@ async def _resolve_llm_config(llm: LLMConfigType, builder: Builder, rag_config: if llm.max_tokens is not None: rag_config.llm.parameters.max_tokens = llm.max_tokens - # Propagate LLM values to query_rewriter when not set, instead of using library defaults. if "model_name" not in rag_config.query_rewriter.model_fields_set: rag_config.query_rewriter.model_name = llm.model_name if "server_url" not in rag_config.query_rewriter.model_fields_set and llm.base_url: @@ -152,7 +222,6 @@ async def _resolve_llm_config(llm: LLMConfigType, builder: Builder, rag_config: if "api_key" not in rag_config.query_rewriter.model_fields_set and llm.api_key: rag_config.query_rewriter.api_key = llm.api_key - # Propagate LLM values to reflection when not set, instead of using library defaults. if "model_name" not in rag_config.reflection.model_fields_set: rag_config.reflection.model_name = llm.model_name if "server_url" not in rag_config.reflection.model_fields_set and llm.base_url: @@ -160,7 +229,6 @@ async def _resolve_llm_config(llm: LLMConfigType, builder: Builder, rag_config: if "api_key" not in rag_config.reflection.model_fields_set and llm.api_key: rag_config.reflection.api_key = llm.api_key - # Propagate LLM values to filter_expression_generator when not set, instead of using library defaults. if "model_name" not in rag_config.filter_expression_generator.model_fields_set: rag_config.filter_expression_generator.model_name = llm.model_name if "server_url" not in rag_config.filter_expression_generator.model_fields_set and llm.base_url: @@ -200,7 +268,7 @@ async def _resolve_embedder_config(embedder: EmbedderConfigType, builder: Builde async def _resolve_retriever_config(retriever: RetrieverConfigType, builder: Builder, rag_config: NvidiaRAGConfig) -> None: - """Resolve retriever config and map all fields to NvidiaRAGConfig.vector_store.""" + """Resolve retriever config and map fields to NvidiaRAGConfig.vector_store and retriever.""" if retriever is None: return @@ -221,6 +289,8 @@ async def _resolve_retriever_config(retriever: RetrieverConfigType, builder: Bui rag_config.vector_store.username = retriever.connection_args["user"] if "password" in retriever.connection_args: rag_config.vector_store.password = SecretStr(retriever.connection_args["password"]) + if retriever.top_k: + rag_config.retriever.top_k = retriever.top_k return if isinstance(retriever, NemoRetrieverConfig): @@ -229,6 +299,8 @@ async def _resolve_retriever_config(retriever: RetrieverConfigType, builder: Bui rag_config.vector_store.default_collection_name = retriever.collection_name if retriever.nvidia_api_key: rag_config.vector_store.api_key = retriever.nvidia_api_key + if retriever.top_k: + rag_config.retriever.top_k = retriever.top_k return raise ValueError(f"Unsupported retriever config type: {type(retriever)}") diff --git a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/register.py b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/register.py index 6198011e03..0a8992b620 100644 --- a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/register.py +++ b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/register.py @@ -19,4 +19,3 @@ # Import any providers which need to be automatically registered here from . import client -from .tools import register diff --git a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/__init__.py b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/__init__.py deleted file mode 100644 index 3bcc1c39bb..0000000000 --- a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-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. diff --git a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/generate.py b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/generate.py deleted file mode 100644 index a5b782e7d7..0000000000 --- a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/generate.py +++ /dev/null @@ -1,132 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-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. - -import logging -from collections.abc import AsyncGenerator -from typing import TYPE_CHECKING - -from pydantic import Field - -from nat.builder.builder import Builder -from nat.builder.function_info import FunctionInfo -from nat.cli.register_workflow import register_function -from nat.data_models.component_ref import FunctionGroupRef -from nat.data_models.finetuning import OpenAIMessage -from nat.data_models.function import FunctionBaseConfig -from nat.plugins.rag_lib.models import RAGGenerateResult - -if TYPE_CHECKING: - from nvidia_rag.rag_server.main import NvidiaRAG - from nvidia_rag.rag_server.response_generator import RAGResponse - - from nat.plugins.rag_lib.client import NvidiaRAGFunctionGroup - -logger: logging.Logger = logging.getLogger(__name__) - - -class RAGLibGenerateConfig(FunctionBaseConfig, name="rag_lib_generate"): - """Generate tool configuration.""" - - rag_client: FunctionGroupRef = Field(description="Reference to nvidia_rag_lib function group.") - topic: str | None = Field(default=None, description="Topic injected into description via {topic} placeholder.") - description: str = Field(default="Generate an answer{topic} with citations from the knowledge base.", - description="Tool description. Use {topic} placeholder to include topic.") - collection_names: list[str] | None = Field(default=None, - description="Collections for context. None uses client defaults.") - use_knowledge_base: bool = Field(default=True, description="Whether to use RAG (True) or pure LLM (False).") - confidence_threshold: float | None = Field( - default=None, ge=0.0, le=1.0, description="Minimum relevance score for context. None uses client default.") - enable_citations: bool | None = Field(default=None, - description="Whether to include citations. None uses client default.") - enable_guardrails: bool | None = Field(default=None, - description="Whether to enable guardrails. None uses client default.") - temperature: float | None = Field(default=None, - ge=0.0, - le=2.0, - description="Sampling temperature. None uses client default.") - max_tokens: int | None = Field(default=None, - ge=1, - description="Maximum tokens to generate. None uses client default.") - top_p: float | None = Field(default=None, - ge=0.0, - le=1.0, - description="Nucleus sampling probability. None uses client default.") - filter_expr: str | None = Field( - default=None, description="Static metadata filter expression, e.g., 'year >= 2023'. None applies no filter.") - - -@register_function(config_type=RAGLibGenerateConfig) -async def rag_lib_generate(config: RAGLibGenerateConfig, builder: Builder): - """RAG Library Generate Tool.""" - - rag_group: NvidiaRAGFunctionGroup = await builder.get_function_group(config.rag_client) # type: ignore[assignment] - rag_client: NvidiaRAG = rag_group.rag_client - topic_str: str = f" about {config.topic}" if config.topic else "" - description: str = config.description.format(topic=topic_str) - - async def _generate(query: str) -> RAGGenerateResult: - """Generate an answer using the knowledge base.""" - from nvidia_rag.rag_server.response_generator import ChainResponse - from nvidia_rag.rag_server.response_generator import Citations - - chunks: list[str] = [] - final_citations: Citations | None = None - - try: - response: RAGResponse = await rag_client.generate( - messages=[OpenAIMessage(role="user", content=query).model_dump()], - use_knowledge_base=config.use_knowledge_base, - filter_expr=config.filter_expr or "", - collection_names=config.collection_names, - confidence_threshold=config.confidence_threshold, - enable_citations=config.enable_citations, - enable_guardrails=config.enable_guardrails, - temperature=config.temperature, - max_tokens=config.max_tokens, - top_p=config.top_p, - ) - - stream: AsyncGenerator[str, None] = (response.generator if hasattr(response, "generator") else response) - - async for raw_chunk in stream: - if raw_chunk.startswith("data: "): - raw_chunk = raw_chunk[len("data: "):].strip() - if not raw_chunk or raw_chunk == "[DONE]": - continue - - try: - parsed: ChainResponse = ChainResponse.model_validate_json(raw_chunk) - - if parsed.choices: - choice = parsed.choices[0] - if choice.delta and choice.delta.content: - content = choice.delta.content - if isinstance(content, str): - chunks.append(content) - - if parsed.citations and parsed.citations.results: - final_citations = parsed.citations - - except Exception: - continue - - answer: str = "".join(chunks) if chunks else "No response generated." - return RAGGenerateResult(answer=answer, citations=final_citations) - - except Exception: - logger.exception("RAG generate failed") - return RAGGenerateResult(answer="Error generating answer. Please try again.") - - yield FunctionInfo.from_fn(fn=_generate, description=description) diff --git a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/register.py b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/register.py deleted file mode 100644 index 578b6f24c0..0000000000 --- a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/register.py +++ /dev/null @@ -1,20 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-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. - -# flake8: noqa -# isort:skip_file - -from . import generate -from . import search diff --git a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/search.py b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/search.py deleted file mode 100644 index 58c3c9274e..0000000000 --- a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/tools/search.py +++ /dev/null @@ -1,93 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-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. - -import logging -from typing import TYPE_CHECKING - -from pydantic import Field - -from nat.builder.builder import Builder -from nat.builder.function_info import FunctionInfo -from nat.cli.register_workflow import register_function -from nat.data_models.component_ref import FunctionGroupRef -from nat.data_models.function import FunctionBaseConfig -from nat.plugins.rag_lib.models import RAGSearchResult - -if TYPE_CHECKING: - from nat.plugins.rag_lib.client import NvidiaRAGFunctionGroup - -logger: logging.Logger = logging.getLogger(__name__) - - -class RAGLibSearchConfig(FunctionBaseConfig, name="rag_lib_search"): - """Search tool configuration.""" - - rag_client: FunctionGroupRef = Field(description="Reference to nvidia_rag_lib function group.") - topic: str | None = Field(default=None, description="Topic injected into description via {topic} placeholder.") - description: str = Field( - default="Retrieve document chunks{topic} which can be used to answer the provided question.", - description="Tool description. Use {topic} placeholder to include topic.") - collection_names: list[str] | None = Field(default=None, description="Collections to search.") - reranker_top_k: int | None = Field(default=None, ge=1, description="Number of results after reranking.") - vdb_top_k: int | None = Field( - default=None, - ge=1, - description="Number of candidates from vector DB before reranking. None uses client default.") - confidence_threshold: float | None = Field(default=None, - ge=0.0, - le=1.0, - description="Minimum relevance score. None uses client default.") - enable_query_rewriting: bool | None = Field(default=None, - description="Whether to rewrite queries. None uses client default.") - enable_reranker: bool | None = Field(default=None, - description="Whether to use reranking. None uses client default.") - enable_filter_generator: bool | None = Field( - default=None, description="Whether to auto-generate filters. None uses client default.") - filter_expr: str | None = Field( - default=None, description="Static metadata filter expression, e.g., 'year >= 2023'. None applies no filter.") - - -@register_function(config_type=RAGLibSearchConfig) -async def rag_lib_search(config: RAGLibSearchConfig, builder: Builder): - """RAG Library Search Tool.""" - - rag_group: NvidiaRAGFunctionGroup = await builder.get_function_group(config.rag_client) # type: ignore[assignment] - rag_client = rag_group.rag_client - topic_str = f" related to {config.topic}" if config.topic else "" - description = config.description.format(topic=topic_str) - - async def _search(query: str) -> RAGSearchResult: - """Search for relevant documents.""" - from nvidia_rag.rag_server.response_generator import Citations - - try: - citations: Citations = await rag_client.search( - query=query, - filter_expr=config.filter_expr or "", - collection_names=config.collection_names, - reranker_top_k=config.reranker_top_k, - vdb_top_k=config.vdb_top_k, - confidence_threshold=config.confidence_threshold, - enable_query_rewriting=config.enable_query_rewriting, - enable_reranker=config.enable_reranker, - enable_filter_generator=config.enable_filter_generator, - ) - logger.info("Search returned %d results for query: %s", citations.total_results, query[:50]) - return RAGSearchResult(citations=citations) - except Exception: - logger.exception("RAG search failed") - return RAGSearchResult(citations=Citations(total_results=0, results=[])) - - yield FunctionInfo.from_fn(fn=_search, description=description) diff --git a/packages/nvidia_nat_rag_lib/tests/test_tools.py b/packages/nvidia_nat_rag_lib/tests/test_tools.py index 39f662dea3..562161b2b7 100644 --- a/packages/nvidia_nat_rag_lib/tests/test_tools.py +++ b/packages/nvidia_nat_rag_lib/tests/test_tools.py @@ -15,142 +15,132 @@ from unittest.mock import AsyncMock from unittest.mock import MagicMock +from unittest.mock import patch import pytest from nvidia_rag.rag_server.response_generator import Citations from nat.builder.builder import Builder -from nat.builder.function import LambdaFunction -from nat.data_models.component_ref import FunctionGroupRef -from nat.plugins.rag_lib.client import NvidiaRAGFunctionGroup from nat.plugins.rag_lib.client import NvidiaRAGLibConfig +from nat.plugins.rag_lib.client import nvidia_rag_lib from nat.plugins.rag_lib.models import RAGGenerateResult from nat.plugins.rag_lib.models import RAGSearchResult -from nat.plugins.rag_lib.tools.generate import RAGLibGenerateConfig -from nat.plugins.rag_lib.tools.generate import rag_lib_generate -from nat.plugins.rag_lib.tools.search import RAGLibSearchConfig -from nat.plugins.rag_lib.tools.search import rag_lib_search -class TestNvidiaRAGSearchTool: +class TestNvidiaRAGLib: @pytest.fixture def mock_builder(self) -> MagicMock: - return MagicMock(spec=Builder) + builder = MagicMock(spec=Builder) + builder.get_llm_config = MagicMock(return_value=None) + builder.get_embedder_config = MagicMock(return_value=None) + builder.get_retriever_config = AsyncMock(return_value=None) + return builder @pytest.fixture - def tool_config(self) -> RAGLibSearchConfig: - return RAGLibSearchConfig( - rag_client=FunctionGroupRef("rag_client"), - collection_names=["test_collection"], - reranker_top_k=5, - ) + def config(self) -> NvidiaRAGLibConfig: + return NvidiaRAGLibConfig(collection_names=["test_collection"], ) @pytest.fixture - def function_group(self) -> NvidiaRAGFunctionGroup: - config = NvidiaRAGLibConfig() - group = NvidiaRAGFunctionGroup(config=config) - group.rag_client = MagicMock() - return group + def mock_rag_client(self) -> MagicMock: + client = MagicMock() + client.search = AsyncMock(return_value=Citations(total_results=3, results=[])) + return client async def test_search_returns_results(self, - tool_config: RAGLibSearchConfig, + config: NvidiaRAGLibConfig, mock_builder: MagicMock, - function_group: NvidiaRAGFunctionGroup) -> None: - function_group.rag_client.search = AsyncMock(return_value=Citations(total_results=3, results=[])) - mock_builder.get_function_group = AsyncMock(return_value=function_group) + mock_rag_client: MagicMock) -> None: + with patch("nvidia_rag.NvidiaRAG", return_value=mock_rag_client): + async with nvidia_rag_lib(config, mock_builder) as group: + functions = await group.get_all_functions() + search_fn = next((f for name, f in functions.items() if name.endswith("search")), None) + assert search_fn is not None - async with rag_lib_search(tool_config, mock_builder) as fn_info: - tool = LambdaFunction.from_info(config=tool_config, info=fn_info, instance_name="search") - result = await tool.acall_invoke(query="test query") + result = await search_fn.acall_invoke(query="test query") - assert isinstance(result, RAGSearchResult) - assert result.citations.total_results == 3 + assert isinstance(result, RAGSearchResult) + assert result.citations.total_results == 3 async def test_search_handles_error(self, - tool_config: RAGLibSearchConfig, + config: NvidiaRAGLibConfig, mock_builder: MagicMock, - function_group: NvidiaRAGFunctionGroup) -> None: - function_group.rag_client.search = AsyncMock(side_effect=Exception("Search failed")) - mock_builder.get_function_group = AsyncMock(return_value=function_group) + mock_rag_client: MagicMock) -> None: + mock_rag_client.search = AsyncMock(side_effect=Exception("Search failed")) - async with rag_lib_search(tool_config, mock_builder) as fn_info: - tool = LambdaFunction.from_info(config=tool_config, info=fn_info, instance_name="search") - result = await tool.acall_invoke(query="test query") + with patch("nvidia_rag.NvidiaRAG", return_value=mock_rag_client): + async with nvidia_rag_lib(config, mock_builder) as group: + functions = await group.get_all_functions() + search_fn = next((f for name, f in functions.items() if name.endswith("search")), None) + result = await search_fn.acall_invoke(query="test query") - assert isinstance(result, RAGSearchResult) - assert result.citations.total_results == 0 - - -class TestNvidiaRAGGenerateTool: - - @pytest.fixture - def mock_builder(self) -> MagicMock: - return MagicMock(spec=Builder) - - @pytest.fixture - def tool_config(self) -> RAGLibGenerateConfig: - return RAGLibGenerateConfig( - rag_client=FunctionGroupRef("rag_client"), - use_knowledge_base=True, - enable_citations=True, - ) - - @pytest.fixture - def function_group(self) -> NvidiaRAGFunctionGroup: - config = NvidiaRAGLibConfig() - group = NvidiaRAGFunctionGroup(config=config) - group.rag_client = MagicMock() - return group + assert isinstance(result, RAGSearchResult) + assert result.citations.total_results == 0 async def test_generate_returns_answer(self, - tool_config: RAGLibGenerateConfig, + config: NvidiaRAGLibConfig, mock_builder: MagicMock, - function_group: NvidiaRAGFunctionGroup) -> None: + mock_rag_client: MagicMock) -> None: async def mock_stream(): yield 'data: {"id": "1", "model": "m", "choices": [{"delta": {"content": "Hello"}}]}' yield 'data: {"id": "1", "model": "m", "choices": [{"delta": {"content": " world"}}]}' yield 'data: [DONE]' - function_group.rag_client.generate = AsyncMock(return_value=mock_stream()) - mock_builder.get_function_group = AsyncMock(return_value=function_group) + mock_rag_client.generate = AsyncMock(return_value=mock_stream()) - async with rag_lib_generate(tool_config, mock_builder) as fn_info: - tool = LambdaFunction.from_info(config=tool_config, info=fn_info, instance_name="generate") - result = await tool.acall_invoke(query="test") + with patch("nvidia_rag.NvidiaRAG", return_value=mock_rag_client): + async with nvidia_rag_lib(config, mock_builder) as group: + functions = await group.get_all_functions() + generate_fn = next((f for name, f in functions.items() if name.endswith("generate")), None) + assert generate_fn is not None - assert isinstance(result, RAGGenerateResult) - assert result.answer == "Hello world" + result = await generate_fn.acall_invoke(query="test") + + assert isinstance(result, RAGGenerateResult) + assert result.answer == "Hello world" async def test_generate_handles_error(self, - tool_config: RAGLibGenerateConfig, + config: NvidiaRAGLibConfig, mock_builder: MagicMock, - function_group: NvidiaRAGFunctionGroup) -> None: - function_group.rag_client.generate = AsyncMock(side_effect=Exception("Generate failed")) - mock_builder.get_function_group = AsyncMock(return_value=function_group) + mock_rag_client: MagicMock) -> None: + mock_rag_client.generate = AsyncMock(side_effect=Exception("Generate failed")) - async with rag_lib_generate(tool_config, mock_builder) as fn_info: - tool = LambdaFunction.from_info(config=tool_config, info=fn_info, instance_name="generate") - result = await tool.acall_invoke(query="test") + with patch("nvidia_rag.NvidiaRAG", return_value=mock_rag_client): + async with nvidia_rag_lib(config, mock_builder) as group: + functions = await group.get_all_functions() + generate_fn = next((f for name, f in functions.items() if name.endswith("generate")), None) + result = await generate_fn.acall_invoke(query="test") - assert isinstance(result, RAGGenerateResult) - assert "Error generating answer" in result.answer + assert isinstance(result, RAGGenerateResult) + assert "Error generating answer" in result.answer async def test_generate_handles_empty_stream(self, - tool_config: RAGLibGenerateConfig, + config: NvidiaRAGLibConfig, mock_builder: MagicMock, - function_group: NvidiaRAGFunctionGroup) -> None: + mock_rag_client: MagicMock) -> None: async def mock_empty_stream(): yield 'data: [DONE]' - function_group.rag_client.generate = AsyncMock(return_value=mock_empty_stream()) - mock_builder.get_function_group = AsyncMock(return_value=function_group) - - async with rag_lib_generate(tool_config, mock_builder) as fn_info: - tool = LambdaFunction.from_info(config=tool_config, info=fn_info, instance_name="generate") - result = await tool.acall_invoke(query="test") - - assert isinstance(result, RAGGenerateResult) - assert result.answer == "No response generated." + mock_rag_client.generate = AsyncMock(return_value=mock_empty_stream()) + + with patch("nvidia_rag.NvidiaRAG", return_value=mock_rag_client): + async with nvidia_rag_lib(config, mock_builder) as group: + functions = await group.get_all_functions() + generate_fn = next((f for name, f in functions.items() if name.endswith("generate")), None) + result = await generate_fn.acall_invoke(query="test") + + assert isinstance(result, RAGGenerateResult) + assert result.answer == "No response generated." + + async def test_group_exposes_both_tools(self, + config: NvidiaRAGLibConfig, + mock_builder: MagicMock, + mock_rag_client: MagicMock) -> None: + with patch("nvidia_rag.NvidiaRAG", return_value=mock_rag_client): + async with nvidia_rag_lib(config, mock_builder) as group: + functions = await group.get_all_functions() + function_names = list(functions.keys()) + assert any(name.endswith("search") for name in function_names) + assert any(name.endswith("generate") for name in function_names) diff --git a/scripts/langchain_web_ingest.py b/scripts/langchain_web_ingest.py index 4c76da348d..5942f1e55a 100644 --- a/scripts/langchain_web_ingest.py +++ b/scripts/langchain_web_ingest.py @@ -21,6 +21,7 @@ from langchain_milvus import Milvus from langchain_nvidia_ai_endpoints import NVIDIAEmbeddings from langchain_text_splitters import RecursiveCharacterTextSplitter +from pymilvus import MilvusClient from web_utils import cache_html from web_utils import get_file_path_from_url from web_utils import scrape @@ -38,9 +39,18 @@ async def main(*, milvus_uri: str, collection_name: str, clean_cache: bool = True, + drop_collection: bool = False, embedding_model: str = "nvidia/nv-embedqa-e5-v5", base_path: str = "./.tmp/data"): + if drop_collection: + client = MilvusClient(uri=milvus_uri) + if client.has_collection(collection_name): + logger.info("Dropping existing collection: %s", collection_name) + client.drop_collection(collection_name) + else: + logger.info("Collection '%s' does not exist, nothing to drop", collection_name) + embedder = NVIDIAEmbeddings(model=embedding_model, truncate="END") # Create the Milvus vector store @@ -131,6 +141,10 @@ async def main(*, parser.add_argument("--collection_name", "-n", default=CUDA_COLLECTION_NAME, help="Collection name for the data.") parser.add_argument("--milvus_uri", "-u", default=DEFAULT_URI, help="Milvus host URI") parser.add_argument("--clean_cache", default=False, help="If true, deletes local files", action="store_true") + parser.add_argument("--drop_collection", + default=False, + help="Drop existing collection before ingesting", + action="store_true") parser.add_argument("--embedding_model", "-e", default="nvidia/nv-embedqa-e5-v5", help="Embedding model to use") args = parser.parse_args() @@ -143,5 +157,6 @@ async def main(*, milvus_uri=args.milvus_uri, collection_name=args.collection_name, clean_cache=args.clean_cache, + drop_collection=args.drop_collection, embedding_model=args.embedding_model, )) From ddf8d34fd344ebfda150cca9ca6fa50e58619d97 Mon Sep 17 00:00:00 2001 From: Eric Evans <194135482+ericevans-nv@users.noreply.github.com> Date: Wed, 21 Jan 2026 19:52:42 -0600 Subject: [PATCH 06/15] Improve nvidia_rag_lib optional dependency handling and test quality Signed-off-by: Eric Evans <194135482+ericevans-nv@users.noreply.github.com> --- examples/RAG/simple_rag/README.md | 12 +++--- .../src/nat/plugins/rag_lib/client.py | 40 ++++++++++--------- .../tests/test_rag_lib_function.py | 40 ++++++------------- 3 files changed, 40 insertions(+), 52 deletions(-) diff --git a/examples/RAG/simple_rag/README.md b/examples/RAG/simple_rag/README.md index 513d9351f2..90d567fc3b 100644 --- a/examples/RAG/simple_rag/README.md +++ b/examples/RAG/simple_rag/README.md @@ -51,7 +51,7 @@ This is a simple example RAG application to showcase how one can configure and u - [Query Rewriting](#query-rewriting) - [Confidence Filtering](#confidence-filtering) - [Structured Citations](#structured-citations) - - [Integration with NAT Components](#integration-with-nat-components) + - [Integration with NeMo Agent Toolkit Components](#integration-with-nemo-agent-toolkit-components) - [RAG-Specific Configuration](#rag-specific-configuration) - [Example Configuration](#example-configuration) - [Run the Workflow](#run-the-workflow-1) @@ -419,7 +419,7 @@ A recall-then-precision approach balances thoroughness with relevance: 2. **Stage 2 - Reranking (Precision):** Candidates pass through a semantic reranker that scores relevance to the query, narrowing down to the most relevant results. -``` +```text Query → Embed → Retrieve candidates → Rerank → Final results ``` @@ -438,9 +438,9 @@ Results below a confidence threshold are automatically filtered out, preventing Search results include document metadata (document name, relevance score) in a structured format, enabling source attribution and traceability in responses. -### Integration with NAT Components +### Integration with NeMo Agent Toolkit Components -The `nvidia_rag_lib` plugin integrates with standard NeMo Agent Toolkit components. You configure `llms`, `embedders`, and `retrievers` sections as usual. The plugin references these components by name: +The `nvidia_rag_lib` plugin integrates with standard NeMo Agent toolkit components. You configure `llms`, `embedders`, and `retrievers` sections as usual. The plugin references these components by name: ```yaml function_groups: @@ -451,11 +451,11 @@ function_groups: retriever: cuda_retriever # References retrievers.cuda_retriever ``` -This means you can reuse existing NAT infrastructure definitions and swap in the RAG library without changing your LLM, embedder, or retriever configurations. +This means you can reuse existing NeMo Agent toolkit infrastructure definitions and swap in the RAG library without changing your LLM, embedder, or retriever configurations. ### RAG-Specific Configuration -The plugin adds configuration specific to the RAG pipeline. These fields differ from a standard NAT retriever setup: +The plugin adds configuration specific to the RAG pipeline. These fields differ from a standard NeMo Agent toolkit retriever setup: | Field | Purpose | |-------|---------| diff --git a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/client.py b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/client.py index 158403ade8..2943eff31f 100644 --- a/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/client.py +++ b/packages/nvidia_nat_rag_lib/src/nat/plugins/rag_lib/client.py @@ -18,15 +18,6 @@ from logging import Logger from typing import TYPE_CHECKING -from nvidia_rag.utils.configuration import EmbeddingConfig as NvidiaRAGEmbeddingConfig -from nvidia_rag.utils.configuration import FilterExpressionGeneratorConfig as NvidiaRAGFilterGeneratorConfig -from nvidia_rag.utils.configuration import LLMConfig as NvidiaRAGLLMConfig -from nvidia_rag.utils.configuration import NvidiaRAGConfig -from nvidia_rag.utils.configuration import QueryDecompositionConfig as NvidiaRAGQueryDecompositionConfig -from nvidia_rag.utils.configuration import QueryRewriterConfig as NvidiaRAGQueryRewriterConfig -from nvidia_rag.utils.configuration import ReflectionConfig as NvidiaRAGReflectionConfig -from nvidia_rag.utils.configuration import VectorStoreConfig as NvidiaRAGVectorStoreConfig -from nvidia_rag.utils.configuration import VLMConfig as NvidiaRAGVLMConfig from pydantic import Field from pydantic import SecretStr @@ -51,6 +42,7 @@ if TYPE_CHECKING: from nvidia_rag.rag_server.response_generator import RAGResponse + from nvidia_rag.utils.configuration import NvidiaRAGConfig logger: Logger = logging.getLogger(__name__) @@ -72,7 +64,7 @@ class NvidiaRAGLibConfig(FunctionGroupBaseConfig, name="nvidia_rag_lib"): @register_function_group(config_type=NvidiaRAGLibConfig) -async def nvidia_rag_lib(config: NvidiaRAGLibConfig, builder: Builder): +async def nvidia_rag_lib(config: NvidiaRAGLibConfig, builder: Builder) -> AsyncGenerator[FunctionGroup, None]: """NVIDIA RAG Library - exposes search and generate tools.""" try: from nvidia_rag import NvidiaRAG @@ -162,19 +154,25 @@ async def generate(query: str) -> RAGGenerateResult: yield group -async def _build_nvidia_rag_config(config: NvidiaRAGLibConfig, builder: Builder) -> NvidiaRAGConfig: +async def _build_nvidia_rag_config(config: NvidiaRAGLibConfig, builder: Builder) -> "NvidiaRAGConfig": """Build NvidiaRAGConfig by resolving NAT refs/components to nvidia_rag configs.""" + from nvidia_rag.utils.configuration import FilterExpressionGeneratorConfig + from nvidia_rag.utils.configuration import NvidiaRAGConfig + from nvidia_rag.utils.configuration import QueryDecompositionConfig + from nvidia_rag.utils.configuration import QueryRewriterConfig + from nvidia_rag.utils.configuration import ReflectionConfig + from nvidia_rag.utils.configuration import VLMConfig pipeline: RAGPipelineConfig = config.rag_pipeline rag_config: NvidiaRAGConfig = NvidiaRAGConfig( ranking=pipeline.ranking, retriever=pipeline.search_settings, - vlm=pipeline.vlm or NvidiaRAGVLMConfig(), - query_rewriter=pipeline.query_rewriter or NvidiaRAGQueryRewriterConfig(), - filter_expression_generator=pipeline.filter_generator or NvidiaRAGFilterGeneratorConfig(), - query_decomposition=pipeline.query_decomposition or NvidiaRAGQueryDecompositionConfig(), - reflection=pipeline.reflection or NvidiaRAGReflectionConfig(), + vlm=pipeline.vlm or VLMConfig(), + query_rewriter=pipeline.query_rewriter or QueryRewriterConfig(), + filter_expression_generator=pipeline.filter_generator or FilterExpressionGeneratorConfig(), + query_decomposition=pipeline.query_decomposition or QueryDecompositionConfig(), + reflection=pipeline.reflection or ReflectionConfig(), enable_citations=pipeline.enable_citations, enable_guardrails=pipeline.enable_guardrails, enable_vlm_inference=pipeline.enable_vlm_inference, @@ -189,8 +187,9 @@ async def _build_nvidia_rag_config(config: NvidiaRAGLibConfig, builder: Builder) return rag_config -async def _resolve_llm_config(llm: LLMConfigType, builder: Builder, rag_config: NvidiaRAGConfig) -> None: +async def _resolve_llm_config(llm: LLMConfigType, builder: Builder, rag_config: "NvidiaRAGConfig") -> None: """Resolve LLM config and map all fields to NvidiaRAGConfig.llm.""" + from nvidia_rag.utils.configuration import LLMConfig as NvidiaRAGLLMConfig if llm is None: return @@ -240,8 +239,10 @@ async def _resolve_llm_config(llm: LLMConfigType, builder: Builder, rag_config: raise ValueError(f"Unsupported LLM config type: {type(llm)}") -async def _resolve_embedder_config(embedder: EmbedderConfigType, builder: Builder, rag_config: NvidiaRAGConfig) -> None: +async def _resolve_embedder_config(embedder: EmbedderConfigType, builder: Builder, + rag_config: "NvidiaRAGConfig") -> None: """Resolve embedder config and map all fields to NvidiaRAGConfig.embeddings.""" + from nvidia_rag.utils.configuration import EmbeddingConfig as NvidiaRAGEmbeddingConfig if embedder is None: return @@ -267,8 +268,9 @@ async def _resolve_embedder_config(embedder: EmbedderConfigType, builder: Builde async def _resolve_retriever_config(retriever: RetrieverConfigType, builder: Builder, - rag_config: NvidiaRAGConfig) -> None: + rag_config: "NvidiaRAGConfig") -> None: """Resolve retriever config and map fields to NvidiaRAGConfig.vector_store and retriever.""" + from nvidia_rag.utils.configuration import VectorStoreConfig as NvidiaRAGVectorStoreConfig if retriever is None: return diff --git a/packages/nvidia_nat_rag_lib/tests/test_rag_lib_function.py b/packages/nvidia_nat_rag_lib/tests/test_rag_lib_function.py index 6fc630f076..e07ab4e602 100644 --- a/packages/nvidia_nat_rag_lib/tests/test_rag_lib_function.py +++ b/packages/nvidia_nat_rag_lib/tests/test_rag_lib_function.py @@ -12,24 +12,7 @@ # 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. -"""Tests for NVIDIA RAG library integration. - -KNOWN ISSUE - Embedding Model Compatibility: - nvidia_rag.EmbeddingConfig always passes a `dimensions` parameter to the embedding API. - Some models (e.g., nvidia/nv-embedqa-e5-v5) reject this parameter entirely, causing errors: - "This model does not support 'dimensions', but a value of '2048' was provided." - - Compatible models: nvidia/llama-3.2-nv-embedqa-1b-v2 (supports dimensions param) - Incompatible models: nvidia/nv-embedqa-e5-v5 (fixed 1024-dim, rejects dimensions param) - - Upstream fix needed: nvidia_rag should allow dimensions=None to not pass the parameter. - -TODO: Add integration tests to catch config compatibility issues: - - Test search/generate with different embedding models - - Test with different LLM providers - - Test with different retriever configs (Milvus, NeMo) - - Parametrize tests across model combinations to catch API rejections early -""" +"""Tests for NVIDIA RAG library integration.""" from __future__ import annotations @@ -508,21 +491,21 @@ def test_generate_method_exists(self) -> None: from nvidia_rag import NvidiaRAG assert hasattr(NvidiaRAG, "generate") - assert callable(getattr(NvidiaRAG, "generate")) + assert callable(NvidiaRAG.generate) def test_search_method_exists(self) -> None: """NvidiaRAG should have a search method.""" from nvidia_rag import NvidiaRAG assert hasattr(NvidiaRAG, "search") - assert callable(getattr(NvidiaRAG, "search")) + assert callable(NvidiaRAG.search) def test_health_method_exists(self) -> None: """NvidiaRAG should have a health method.""" from nvidia_rag import NvidiaRAG assert hasattr(NvidiaRAG, "health") - assert callable(getattr(NvidiaRAG, "health")) + assert callable(NvidiaRAG.health) @pytest.mark.integration @@ -571,8 +554,9 @@ def _create(embedder_ref: str) -> str: "embedder_ref", [ "nim_embedder", - # TODO: nvidia_rag always passes dimensions param which nv-embedqa-e5-v5 rejects - # "nim_embedder_e5", + pytest.param( + "nim_embedder_e5", + marks=pytest.mark.xfail(reason="nvidia_rag passes dimensions param which nv-embedqa-e5-v5 rejects")), ]) @pytest.mark.parametrize("retriever_ref", list(RETRIEVER_CONFIGS.keys())) async def test_search( @@ -609,8 +593,9 @@ async def test_search( "embedder_ref", [ "nim_embedder", - # TODO: nvidia_rag always passes dimensions param which nv-embedqa-e5-v5 rejects - # "nim_embedder_e5", + pytest.param( + "nim_embedder_e5", + marks=pytest.mark.xfail(reason="nvidia_rag passes dimensions param which nv-embedqa-e5-v5 rejects")), ]) @pytest.mark.parametrize("retriever_ref", list(RETRIEVER_CONFIGS.keys())) async def test_generate( @@ -644,8 +629,9 @@ async def test_generate( "embedder_ref", [ "nim_embedder", - # TODO: nvidia_rag always passes dimensions param which nv-embedqa-e5-v5 rejects - # "nim_embedder_e5", + pytest.param( + "nim_embedder_e5", + marks=pytest.mark.xfail(reason="nvidia_rag passes dimensions param which nv-embedqa-e5-v5 rejects")), ]) @pytest.mark.parametrize("retriever_ref", list(RETRIEVER_CONFIGS.keys())) async def test_health( From a6edc40ce7e89c443c234680d3c7d30c83be015e Mon Sep 17 00:00:00 2001 From: Eric Evans <194135482+ericevans-nv@users.noreply.github.com> Date: Wed, 21 Jan 2026 20:10:54 -0600 Subject: [PATCH 07/15] Refine README and update copyright dates Signed-off-by: Eric Evans <194135482+ericevans-nv@users.noreply.github.com> --- examples/RAG/simple_rag/README.md | 50 +++++-------------- examples/deploy/docker-compose.milvus.yml | 4 +- .../nvidia_nat_rag_lib/src/nat/meta/pypi.md | 2 +- .../src/nat/plugins/rag_lib/__init__.py | 2 +- .../src/nat/plugins/rag_lib/client.py | 2 +- .../src/nat/plugins/rag_lib/config.py | 2 +- .../src/nat/plugins/rag_lib/register.py | 2 +- .../tests/test_rag_lib_function.py | 2 +- 8 files changed, 20 insertions(+), 46 deletions(-) diff --git a/examples/RAG/simple_rag/README.md b/examples/RAG/simple_rag/README.md index 90d567fc3b..c9937cc0a0 100644 --- a/examples/RAG/simple_rag/README.md +++ b/examples/RAG/simple_rag/README.md @@ -46,11 +46,7 @@ This is a simple example RAG application to showcase how one can configure and u - [What the Library Provides](#what-the-library-provides) - [Prerequisites](#prerequisites-1) - [Bootstrap Data](#bootstrap-data-1) - - [How the Pipeline Works](#how-the-pipeline-works) - - [Two-Stage Retrieval](#two-stage-retrieval) - - [Query Rewriting](#query-rewriting) - - [Confidence Filtering](#confidence-filtering) - - [Structured Citations](#structured-citations) + - [Key Capabilities](#key-capabilities) - [Integration with NeMo Agent Toolkit Components](#integration-with-nemo-agent-toolkit-components) - [RAG-Specific Configuration](#rag-specific-configuration) - [Example Configuration](#example-configuration) @@ -372,13 +368,13 @@ The final workflow result should look similar to the following: ## Advanced RAG with NVIDIA RAG Library -The NVIDIA RAG Library plugin (`nvidia_rag_lib`) integrates the [NVIDIA RAG Blueprint](https://github.com/NVIDIA-AI-Blueprints/rag) pipeline into NeMo Agent Toolkit. The NVIDIA RAG Blueprint is NVIDIA's reference solution for building production RAG systems that ground AI responses in enterprise knowledge, reducing hallucinations and ensuring accuracy. +The NVIDIA RAG Library (`nvidia_rag_lib`) integrates the [NVIDIA RAG Blueprint](https://github.com/NVIDIA-AI-Blueprints/rag) pipeline into NeMo Agent Toolkit. The library handles the complexity of multi-stage retrieval, semantic reranking, and query optimization, allowing you to focus on building your application rather than implementing RAG infrastructure. ### What the Library Provides -The `nvidia_rag_lib` plugin provides agent tools powered by the NVIDIA RAG pipeline. +The `nvidia_rag_lib` library provides agent tools powered by the NVIDIA RAG pipeline. - **Multi-stage retrieval** with configurable candidate pools and reranking - **Semantic reranking** using NeMo Retriever models @@ -391,7 +387,7 @@ All of these features are managed by the library and configured declaratively in ### Prerequisites -Install the NVIDIA RAG Library plugin: +Install the NVIDIA RAG Library: ```bash uv pip install -e packages/nvidia_nat_rag_lib ``` @@ -407,40 +403,18 @@ python scripts/langchain_web_ingest.py \ --drop_collection ``` -### How the Pipeline Works +### Key Capabilities -The `nvidia_rag_lib` plugin orchestrates a multi-stage retrieval pipeline based on the NVIDIA RAG Blueprint. Each stage is handled automatically based on your configuration. +The `nvidia_rag_lib` library orchestrates a multi-stage retrieval pipeline with the following capabilities: -#### Two-Stage Retrieval - -A recall-then-precision approach balances thoroughness with relevance: - -1. **Stage 1 - Vector Search (Recall):** A large candidate pool is retrieved using embedding similarity, casting a wide net to ensure relevant documents are not missed. - -2. **Stage 2 - Reranking (Precision):** Candidates pass through a semantic reranker that scores relevance to the query, narrowing down to the most relevant results. - -```text -Query → Embed → Retrieve candidates → Rerank → Final results -``` - -#### Query Rewriting - -When enabled, an LLM reformulates user queries before searching. This helps when: -- User queries are conversational or ambiguous -- Technical terminology varies across documents -- Queries benefit from expansion or clarification - -#### Confidence Filtering - -Results below a confidence threshold are automatically filtered out, preventing low-quality matches from reaching the agent. - -#### Structured Citations - -Search results include document metadata (document name, relevance score) in a structured format, enabling source attribution and traceability in responses. +- **Two-stage retrieval:** Combines broad vector search (recall) with semantic reranking (precision) to surface the most relevant results +- **Query rewriting:** LLM reformulates ambiguous or conversational queries before searching +- **Confidence filtering:** Automatically filters out low-quality matches below a configurable threshold +- **Structured citations:** Returns document metadata (name, relevance score) for source attribution ### Integration with NeMo Agent Toolkit Components -The `nvidia_rag_lib` plugin integrates with standard NeMo Agent toolkit components. You configure `llms`, `embedders`, and `retrievers` sections as usual. The plugin references these components by name: +The `nvidia_rag_lib` library integrates with standard NeMo Agent toolkit components. You configure `llms`, `embedders`, and `retrievers` sections as usual. The library references these components by name: ```yaml function_groups: @@ -455,7 +429,7 @@ This means you can reuse existing NeMo Agent toolkit infrastructure definitions ### RAG-Specific Configuration -The plugin adds configuration specific to the RAG pipeline. These fields differ from a standard NeMo Agent toolkit retriever setup: +The library adds configuration specific to the RAG pipeline. These fields differ from a standard NeMo Agent toolkit retriever setup: | Field | Purpose | |-------|---------| diff --git a/examples/deploy/docker-compose.milvus.yml b/examples/deploy/docker-compose.milvus.yml index 1cacbb1453..307453dab3 100644 --- a/examples/deploy/docker-compose.milvus.yml +++ b/examples/deploy/docker-compose.milvus.yml @@ -21,8 +21,8 @@ services: MINIO_ACCESS_KEY: minioadmin MINIO_SECRET_KEY: minioadmin ports: - - "19001:9001" - - "19000:9000" + - "9001:9001" + - "9000:9000" volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data command: minio server /minio_data --console-address ":9001" diff --git a/packages/nvidia_nat_rag_lib/src/nat/meta/pypi.md b/packages/nvidia_nat_rag_lib/src/nat/meta/pypi.md index 13074c85c1..79e9497893 100644 --- a/packages/nvidia_nat_rag_lib/src/nat/meta/pypi.md +++ b/packages/nvidia_nat_rag_lib/src/nat/meta/pypi.md @@ -1,5 +1,5 @@