Skip to content

Commit e03244c

Browse files
authored
Merge pull request #550 from NVIDIA/feat/issue-335-visible-import-errors
Feat/issue 335 visible import errors
2 parents b6ad06a + 014db8a commit e03244c

File tree

8 files changed

+128
-50
lines changed

8 files changed

+128
-50
lines changed

nemoguardrails/actions/action_dispatcher.py

+51-23
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515

16-
"""Module for the calling proper action endpoints based on events received at action server endpoint """
16+
"""Module for the calling proper action endpoints based on events received at action server endpoint"""
1717

1818
import importlib.util
1919
import inspect
2020
import logging
2121
import os
22+
from pathlib import Path
2223
from typing import Any, Dict, List, Optional, Tuple, Union
2324

2425
from langchain.chains.base import Chain
@@ -53,36 +54,39 @@ def __init__(
5354

5455
if load_all_actions:
5556
# TODO: check for better way to find actions dir path or use constants.py
57+
current_file_path = Path(__file__).resolve()
58+
parent_directory_path = current_file_path.parents[1]
5659

5760
# First, we load all actions from the actions folder
58-
self.load_actions_from_path(os.path.join(os.path.dirname(__file__), ".."))
61+
self.load_actions_from_path(parent_directory_path)
62+
# self.load_actions_from_path(os.path.join(os.path.dirname(__file__), ".."))
5963

6064
# Next, we load all actions from the library folder
61-
library_path = os.path.join(os.path.dirname(__file__), "../library")
65+
library_path = parent_directory_path / "library"
6266

6367
for root, dirs, files in os.walk(library_path):
6468
# We only load the actions if there is an `actions` sub-folder or
6569
# an `actions.py` file.
6670
if "actions" in dirs or "actions.py" in files:
67-
self.load_actions_from_path(root)
71+
self.load_actions_from_path(Path(root))
6872

6973
# Next, we load all actions from the current working directory
7074
# TODO: add support for an explicit ACTIONS_PATH
71-
self.load_actions_from_path(os.getcwd())
75+
self.load_actions_from_path(Path.cwd())
7276

7377
# Last, but not least, if there was a config path, we try to load actions
7478
# from there as well.
7579
if config_path:
7680
config_path = config_path.split(",")
7781
for path in config_path:
78-
self.load_actions_from_path(path)
82+
self.load_actions_from_path(Path(path.strip()))
7983

8084
# If there are any imported paths, we load the actions from there as well.
8185
if import_paths:
8286
for import_path in import_paths:
83-
self.load_actions_from_path(import_path)
87+
self.load_actions_from_path(Path(import_path.strip()))
8488

85-
log.info(f"Registered Actions: {self._registered_actions}")
89+
log.info(f"Registered Actions :: {sorted(self._registered_actions.keys())}")
8690
log.info("Action dispatcher initialized")
8791

8892
@property
@@ -94,16 +98,17 @@ def registered_actions(self):
9498
"""
9599
return self._registered_actions
96100

97-
def load_actions_from_path(self, path: str):
101+
def load_actions_from_path(self, path: Path):
98102
"""Loads all actions from the specified path.
99103
100104
This method loads all actions from the `actions.py` file if it exists and
101105
all actions inside the `actions` folder if it exists.
102106
103107
Args:
104108
path (str): A string representing the path from which to load actions.
109+
105110
"""
106-
actions_path = os.path.join(path, "actions")
111+
actions_path = path / "actions"
107112
if os.path.exists(actions_path):
108113
self._registered_actions.update(self._find_actions(actions_path))
109114

@@ -257,38 +262,45 @@ def _load_actions_from_module(filepath: str):
257262
action_objects = {}
258263
filename = os.path.basename(filepath)
259264

265+
if not os.path.isfile(filepath):
266+
log.error(f"{filepath} does not exist or is not a file.")
267+
log.error(f"Failed to load actions from {filename}.")
268+
return action_objects
269+
260270
try:
261271
log.debug(f"Analyzing file {filename}")
262272
# Import the module from the file
263273

264274
spec = importlib.util.spec_from_file_location(filename, filepath)
275+
if spec is None:
276+
log.error(f"Failed to create a module spec from {filepath}.")
277+
return action_objects
278+
265279
module = importlib.util.module_from_spec(spec)
266280
spec.loader.exec_module(module)
267281

268282
# Loop through all members in the module and check for the `@action` decorator
269283
# If class has action decorator is_action class member is true
270284
for name, obj in inspect.getmembers(module):
271-
if inspect.isfunction(obj) and hasattr(obj, "action_meta"):
272-
log.info(f"Adding {obj.__name__} to actions")
273-
action_objects[obj.action_meta["name"]] = obj
274-
275-
if inspect.isclass(obj) and hasattr(obj, "action_meta"):
285+
if (inspect.isfunction(obj) or inspect.isclass(obj)) and hasattr(
286+
obj, "action_meta"
287+
):
276288
try:
277289
action_objects[obj.action_meta["name"]] = obj
278290
log.info(f"Added {obj.action_meta['name']} to actions")
279291
except Exception as e:
280-
log.debug(
292+
log.error(
281293
f"Failed to register {obj.action_meta['name']} in action dispatcher due to exception {e}"
282294
)
283295
except Exception as e:
284-
log.debug(
285-
f"Failed to register {filename} in action dispatcher due to exception {e}"
296+
relative_filepath = Path(module.__file__).relative_to(Path.cwd())
297+
log.error(
298+
f"Failed to register {filename} from {relative_filepath} in action dispatcher due to exception: {e}"
286299
)
287300

288301
return action_objects
289302

290-
@staticmethod
291-
def _find_actions(directory) -> Dict:
303+
def _find_actions(self, directory) -> Dict:
292304
"""Loop through all the subdirectories and check for the class with @action
293305
decorator and add in action_classes dict.
294306
@@ -301,15 +313,31 @@ def _find_actions(directory) -> Dict:
301313
action_objects = {}
302314

303315
if not os.path.exists(directory):
316+
log.debug(f"_find_actions: {directory} does not exist.")
304317
return action_objects
305318

306319
# Loop through all files in the directory and its subdirectories
307320
for root, dirs, files in os.walk(directory):
308321
for filename in files:
309322
if filename.endswith(".py"):
310323
filepath = os.path.join(root, filename)
311-
action_objects.update(
312-
ActionDispatcher._load_actions_from_module(filepath)
313-
)
324+
if is_action_file(filepath):
325+
action_objects.update(
326+
ActionDispatcher._load_actions_from_module(filepath)
327+
)
328+
if not action_objects:
329+
log.debug(f"No actions found in {directory}")
330+
log.exception(f"No actions found in the directory {directory}.")
314331

315332
return action_objects
333+
334+
335+
def is_action_file(filepath):
336+
"""Heuristics for determining if a Python file can have actions or not.
337+
338+
Currently, it only excludes the `__init__.py files.
339+
"""
340+
if "__init__.py" in filepath:
341+
return False
342+
343+
return True

nemoguardrails/actions/langchain/actions.py

+14-10
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
# limitations under the License.
1515

1616
"""This module wraps LangChain tools as actions."""
17+
import os
18+
1719
from nemoguardrails.actions import action
1820
from nemoguardrails.actions.langchain.safetools import (
1921
ApifyWrapperSafe,
@@ -28,13 +30,15 @@
2830
ZapierNLAWrapperSafe,
2931
)
3032

31-
apify = action(name="apify")(ApifyWrapperSafe)
32-
bing_search = action(name="bing_search")(BingSearchAPIWrapperSafe)
33-
google_search = action(name="google_search")(GoogleSearchAPIWrapperSafe)
34-
searx_search = action(name="searx_search")(SearxSearchWrapperSafe)
35-
google_serper = action(name="google_serper")(GoogleSerperAPIWrapperSafe)
36-
openweather_query = action(name="openweather_query")(OpenWeatherMapAPIWrapperSafe)
37-
serp_api_query = action(name="serp_api_query")(SerpAPIWrapperSafe)
38-
wikipedia_query = action(name="wikipedia_query")(WikipediaAPIWrapperSafe)
39-
wolframalpha_query = action(name="wolframalpha_query")(WolframAlphaAPIWrapperSafe)
40-
zapier_nla_query = action(name="zapier_nla_query")(ZapierNLAWrapperSafe)
33+
# TODO: Document this env variable.
34+
if os.environ.get("NEMO_GUARDRAILS_DEMO_ACTIONS"):
35+
apify = action(name="apify")(ApifyWrapperSafe)
36+
bing_search = action(name="bing_search")(BingSearchAPIWrapperSafe)
37+
google_search = action(name="google_search")(GoogleSearchAPIWrapperSafe)
38+
searx_search = action(name="searx_search")(SearxSearchWrapperSafe)
39+
google_serper = action(name="google_serper")(GoogleSerperAPIWrapperSafe)
40+
openweather_query = action(name="openweather_query")(OpenWeatherMapAPIWrapperSafe)
41+
serp_api_query = action(name="serp_api_query")(SerpAPIWrapperSafe)
42+
wikipedia_query = action(name="wikipedia_query")(WikipediaAPIWrapperSafe)
43+
wolframalpha_query = action(name="wolframalpha_query")(WolframAlphaAPIWrapperSafe)
44+
zapier_nla_query = action(name="zapier_nla_query")(ZapierNLAWrapperSafe)

nemoguardrails/actions/langchain/safetools.py

+22-12
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,31 @@
1818
The same validation logic can be applied to others as well.
1919
"""
2020

21-
from langchain_community.utilities import (
22-
ApifyWrapper,
23-
BingSearchAPIWrapper,
24-
GoogleSearchAPIWrapper,
25-
GoogleSerperAPIWrapper,
26-
OpenWeatherMapAPIWrapper,
27-
SearxSearchWrapper,
28-
SerpAPIWrapper,
29-
WikipediaAPIWrapper,
30-
WolframAlphaAPIWrapper,
31-
ZapierNLAWrapper,
32-
)
21+
import logging
3322

3423
from nemoguardrails.actions.validation import validate_input, validate_response
3524

25+
log = logging.getLogger(__name__)
26+
27+
try:
28+
from langchain_community.utilities import (
29+
ApifyWrapper,
30+
BingSearchAPIWrapper,
31+
GoogleSearchAPIWrapper,
32+
GoogleSerperAPIWrapper,
33+
OpenWeatherMapAPIWrapper,
34+
SearxSearchWrapper,
35+
SerpAPIWrapper,
36+
WikipediaAPIWrapper,
37+
WolframAlphaAPIWrapper,
38+
ZapierNLAWrapper,
39+
)
40+
except ImportError:
41+
log.warning(
42+
"The langchain_community module is not installed. Please install it using pip: pip install langchain_community"
43+
)
44+
45+
3646
MAX_QUERY_LEN = 50
3747
MAX_LOCATION_LEN = 50
3848

nemoguardrails/actions/validation/base.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from typing import List
1818
from urllib.parse import quote
1919

20-
from .filter_secrets import contains_secrets
20+
from nemoguardrails.actions.validation.filter_secrets import contains_secrets
2121

2222
MAX_LEN = 50
2323

nemoguardrails/cli/chat.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,6 @@ def run_chat(
586586
config_id (Optional[str]): The configuration ID. Defaults to None.
587587
"""
588588
rails_config = RailsConfig.from_path(config_path)
589-
rails_app = LLMRails(rails_config, verbose=verbose)
590589

591590
if verbose and verbose_llm_calls:
592591
console.print(
@@ -607,6 +606,7 @@ def run_chat(
607606
)
608607
)
609608
elif rails_config.colang_version == "2.x":
609+
rails_app = LLMRails(rails_config, verbose=verbose)
610610
asyncio.run(_run_chat_v2_x(rails_app))
611611
else:
612612
raise Exception(f"Invalid colang version: {rails_config.colang_version}")

nemoguardrails/library/hallucination/actions.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
from langchain.chains import LLMChain
2020
from langchain.llms.base import BaseLLM
2121
from langchain.prompts import PromptTemplate
22-
from langchain_openai import OpenAI
2322

2423
from nemoguardrails import RailsConfig
2524
from nemoguardrails.actions import action
@@ -37,6 +36,13 @@
3736

3837
log = logging.getLogger(__name__)
3938

39+
try:
40+
from langchain_openai import OpenAI
41+
except ImportError:
42+
log.warning(
43+
"The langchain_openai module is not installed. Please install it using pip: pip install langchain_openai"
44+
)
45+
4046
HALLUCINATION_NUM_EXTRA_RESPONSES = 2
4147

4248

nemoguardrails/library/sensitive_data_detection/actions.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616
import logging
1717
from functools import lru_cache
1818

19-
import spacy
20-
2119
try:
2220
from presidio_analyzer import PatternRecognizer
2321
from presidio_analyzer.nlp_engine import NlpEngineProvider
@@ -48,6 +46,13 @@ def _get_analyzer():
4846
"`pip install presidio-analyzer presidio-anonymizer`."
4947
)
5048

49+
try:
50+
import spacy
51+
except ImportError:
52+
raise RuntimeError(
53+
"The spacy module is not installed. Please install it using pip: pip install spacy."
54+
)
55+
5156
if not spacy.util.is_package("en_core_web_lg"):
5257
raise RuntimeError(
5358
"The en_core_web_lg Spacy model was not found. "

tests/test_action_error.py

+25
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,28 @@ def fetch_user_profile():
7070

7171
chat >> "hello there!"
7272
chat << "I'm sorry, an internal error has occurred."
73+
74+
75+
def test_action_not_registered():
76+
"""Test that an error is raised when an action is not registered."""
77+
config = RailsConfig.from_content(
78+
"""
79+
define user express greeting
80+
"hello"
81+
82+
define flow
83+
user express greeting
84+
execute unregistered_action
85+
bot express greeting
86+
"""
87+
)
88+
chat = TestChat(
89+
config,
90+
llm_completions=[
91+
" express greeting",
92+
' "Hello John!"',
93+
],
94+
)
95+
96+
chat >> "hello there!"
97+
chat << "Action 'unregistered_action' not found."

0 commit comments

Comments
 (0)