Skip to content

Feat/issue 335 visible import errors #550

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jun 11, 2024
74 changes: 51 additions & 23 deletions nemoguardrails/actions/action_dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""Module for the calling proper action endpoints based on events received at action server endpoint """
"""Module for the calling proper action endpoints based on events received at action server endpoint"""

import importlib.util
import inspect
import logging
import os
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Union

from langchain.chains.base import Chain
Expand Down Expand Up @@ -53,36 +54,39 @@ def __init__(

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

# First, we load all actions from the actions folder
self.load_actions_from_path(os.path.join(os.path.dirname(__file__), ".."))
self.load_actions_from_path(parent_directory_path)
# self.load_actions_from_path(os.path.join(os.path.dirname(__file__), ".."))

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

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

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

# Last, but not least, if there was a config path, we try to load actions
# from there as well.
if config_path:
config_path = config_path.split(",")
for path in config_path:
self.load_actions_from_path(path)
self.load_actions_from_path(Path(path.strip()))

# If there are any imported paths, we load the actions from there as well.
if import_paths:
for import_path in import_paths:
self.load_actions_from_path(import_path)
self.load_actions_from_path(Path(import_path.strip()))

log.info(f"Registered Actions: {self._registered_actions}")
log.info(f"Registered Actions :: {sorted(self._registered_actions.keys())}")
log.info("Action dispatcher initialized")

@property
Expand All @@ -94,16 +98,17 @@ def registered_actions(self):
"""
return self._registered_actions

def load_actions_from_path(self, path: str):
def load_actions_from_path(self, path: Path):
"""Loads all actions from the specified path.

This method loads all actions from the `actions.py` file if it exists and
all actions inside the `actions` folder if it exists.

Args:
path (str): A string representing the path from which to load actions.

"""
actions_path = os.path.join(path, "actions")
actions_path = path / "actions"
if os.path.exists(actions_path):
self._registered_actions.update(self._find_actions(actions_path))

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

if not os.path.isfile(filepath):
log.error(f"{filepath} does not exist or is not a file.")
log.error(f"Failed to load actions from {filename}.")
return action_objects

try:
log.debug(f"Analyzing file {filename}")
# Import the module from the file

spec = importlib.util.spec_from_file_location(filename, filepath)
if spec is None:
log.error(f"Failed to create a module spec from {filepath}.")
return action_objects

module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)

# Loop through all members in the module and check for the `@action` decorator
# If class has action decorator is_action class member is true
for name, obj in inspect.getmembers(module):
if inspect.isfunction(obj) and hasattr(obj, "action_meta"):
log.info(f"Adding {obj.__name__} to actions")
action_objects[obj.action_meta["name"]] = obj

if inspect.isclass(obj) and hasattr(obj, "action_meta"):
if (inspect.isfunction(obj) or inspect.isclass(obj)) and hasattr(
obj, "action_meta"
):
try:
action_objects[obj.action_meta["name"]] = obj
log.info(f"Added {obj.action_meta['name']} to actions")
except Exception as e:
log.debug(
log.error(
f"Failed to register {obj.action_meta['name']} in action dispatcher due to exception {e}"
)
except Exception as e:
log.debug(
f"Failed to register {filename} in action dispatcher due to exception {e}"
relative_filepath = Path(module.__file__).relative_to(Path.cwd())
log.error(
f"Failed to register {filename} from {relative_filepath} in action dispatcher due to exception: {e}"
)

return action_objects

@staticmethod
def _find_actions(directory) -> Dict:
def _find_actions(self, directory) -> Dict:
"""Loop through all the subdirectories and check for the class with @action
decorator and add in action_classes dict.

Expand All @@ -301,15 +313,31 @@ def _find_actions(directory) -> Dict:
action_objects = {}

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

# Loop through all files in the directory and its subdirectories
for root, dirs, files in os.walk(directory):
for filename in files:
if filename.endswith(".py"):
filepath = os.path.join(root, filename)
action_objects.update(
ActionDispatcher._load_actions_from_module(filepath)
)
if is_action_file(filepath):
action_objects.update(
ActionDispatcher._load_actions_from_module(filepath)
)
if not action_objects:
log.debug(f"No actions found in {directory}")
log.exception(f"No actions found in the directory {directory}.")

return action_objects


def is_action_file(filepath):
"""Heuristics for determining if a Python file can have actions or not.

Currently, it only excludes the `__init__.py files.
"""
if "__init__.py" in filepath:
return False

return True
24 changes: 14 additions & 10 deletions nemoguardrails/actions/langchain/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
# limitations under the License.

"""This module wraps LangChain tools as actions."""
import os

from nemoguardrails.actions import action
from nemoguardrails.actions.langchain.safetools import (
ApifyWrapperSafe,
Expand All @@ -28,13 +30,15 @@
ZapierNLAWrapperSafe,
)

apify = action(name="apify")(ApifyWrapperSafe)
bing_search = action(name="bing_search")(BingSearchAPIWrapperSafe)
google_search = action(name="google_search")(GoogleSearchAPIWrapperSafe)
searx_search = action(name="searx_search")(SearxSearchWrapperSafe)
google_serper = action(name="google_serper")(GoogleSerperAPIWrapperSafe)
openweather_query = action(name="openweather_query")(OpenWeatherMapAPIWrapperSafe)
serp_api_query = action(name="serp_api_query")(SerpAPIWrapperSafe)
wikipedia_query = action(name="wikipedia_query")(WikipediaAPIWrapperSafe)
wolframalpha_query = action(name="wolframalpha_query")(WolframAlphaAPIWrapperSafe)
zapier_nla_query = action(name="zapier_nla_query")(ZapierNLAWrapperSafe)
# TODO: Document this env variable.
if os.environ.get("NEMO_GUARDRAILS_DEMO_ACTIONS"):
apify = action(name="apify")(ApifyWrapperSafe)
bing_search = action(name="bing_search")(BingSearchAPIWrapperSafe)
google_search = action(name="google_search")(GoogleSearchAPIWrapperSafe)
searx_search = action(name="searx_search")(SearxSearchWrapperSafe)
google_serper = action(name="google_serper")(GoogleSerperAPIWrapperSafe)
openweather_query = action(name="openweather_query")(OpenWeatherMapAPIWrapperSafe)
serp_api_query = action(name="serp_api_query")(SerpAPIWrapperSafe)
wikipedia_query = action(name="wikipedia_query")(WikipediaAPIWrapperSafe)
wolframalpha_query = action(name="wolframalpha_query")(WolframAlphaAPIWrapperSafe)
zapier_nla_query = action(name="zapier_nla_query")(ZapierNLAWrapperSafe)
34 changes: 22 additions & 12 deletions nemoguardrails/actions/langchain/safetools.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,31 @@
The same validation logic can be applied to others as well.
"""

from langchain_community.utilities import (
ApifyWrapper,
BingSearchAPIWrapper,
GoogleSearchAPIWrapper,
GoogleSerperAPIWrapper,
OpenWeatherMapAPIWrapper,
SearxSearchWrapper,
SerpAPIWrapper,
WikipediaAPIWrapper,
WolframAlphaAPIWrapper,
ZapierNLAWrapper,
)
import logging

from nemoguardrails.actions.validation import validate_input, validate_response

log = logging.getLogger(__name__)

try:
from langchain_community.utilities import (
ApifyWrapper,
BingSearchAPIWrapper,
GoogleSearchAPIWrapper,
GoogleSerperAPIWrapper,
OpenWeatherMapAPIWrapper,
SearxSearchWrapper,
SerpAPIWrapper,
WikipediaAPIWrapper,
WolframAlphaAPIWrapper,
ZapierNLAWrapper,
)
except ImportError:
log.warning(
"The langchain_community module is not installed. Please install it using pip: pip install langchain_community"
)


MAX_QUERY_LEN = 50
MAX_LOCATION_LEN = 50

Expand Down
2 changes: 1 addition & 1 deletion nemoguardrails/actions/validation/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from typing import List
from urllib.parse import quote

from .filter_secrets import contains_secrets
from nemoguardrails.actions.validation.filter_secrets import contains_secrets

MAX_LEN = 50

Expand Down
2 changes: 1 addition & 1 deletion nemoguardrails/cli/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,6 @@ def run_chat(
config_id (Optional[str]): The configuration ID. Defaults to None.
"""
rails_config = RailsConfig.from_path(config_path)
rails_app = LLMRails(rails_config, verbose=verbose)

if verbose and verbose_llm_calls:
console.print(
Expand All @@ -607,6 +606,7 @@ def run_chat(
)
)
elif rails_config.colang_version == "2.x":
rails_app = LLMRails(rails_config, verbose=verbose)
asyncio.run(_run_chat_v2_x(rails_app))
else:
raise Exception(f"Invalid colang version: {rails_config.colang_version}")
8 changes: 7 additions & 1 deletion nemoguardrails/library/hallucination/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
from langchain.chains import LLMChain
from langchain.llms.base import BaseLLM
from langchain.prompts import PromptTemplate
from langchain_openai import OpenAI

from nemoguardrails import RailsConfig
from nemoguardrails.actions import action
Expand All @@ -37,6 +36,13 @@

log = logging.getLogger(__name__)

try:
from langchain_openai import OpenAI
except ImportError:
log.warning(
"The langchain_openai module is not installed. Please install it using pip: pip install langchain_openai"
)

HALLUCINATION_NUM_EXTRA_RESPONSES = 2


Expand Down
9 changes: 7 additions & 2 deletions nemoguardrails/library/sensitive_data_detection/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
import logging
from functools import lru_cache

import spacy

try:
from presidio_analyzer import PatternRecognizer
from presidio_analyzer.nlp_engine import NlpEngineProvider
Expand Down Expand Up @@ -48,6 +46,13 @@ def _get_analyzer():
"`pip install presidio-analyzer presidio-anonymizer`."
)

try:
import spacy
except ImportError:
raise RuntimeError(
"The spacy module is not installed. Please install it using pip: pip install spacy."
)

if not spacy.util.is_package("en_core_web_lg"):
raise RuntimeError(
"The en_core_web_lg Spacy model was not found. "
Expand Down
25 changes: 25 additions & 0 deletions tests/test_action_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,28 @@ def fetch_user_profile():

chat >> "hello there!"
chat << "I'm sorry, an internal error has occurred."


def test_action_not_registered():
"""Test that an error is raised when an action is not registered."""
config = RailsConfig.from_content(
"""
define user express greeting
"hello"

define flow
user express greeting
execute unregistered_action
bot express greeting
"""
)
chat = TestChat(
config,
llm_completions=[
" express greeting",
' "Hello John!"',
],
)

chat >> "hello there!"
chat << "Action 'unregistered_action' not found."
Loading