Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ jobs:
run: uv sync --locked --group dev

- name: Run pre-commit hooks
run: uv run make pre-commit
run: make pre-commit
9 changes: 5 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ repos:
- --use-current-year
- repo: local
hooks:
- id: pyright
name: pyright
entry: uv run pyright
- id: ty
name: ty
entry: uv run --locked --group=dev ty check
language: system
types: [python]
pass_filenames: false
always_run: true
require_serial: true
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,15 @@ as tox and package coverage.
| Focused test | `make test TEST=path/to/test_file.py::test_name` (extra flags via `ARGS="-k ... -q"`) |
| Serial, deterministic run | `make test WORKERS=1` (no parallelism, still unsets live keys) |
| Coverage | `make test-coverage` |
| Pre-commit hooks | `uv run pre-commit run --all-files` |
| Pre-commit hooks | `make pre-commit` |
| Docs check | `make docs-fern` |
| Ruff diagnosis | `uv run ruff check path/to/file.py` |
| Ruff formatting diagnosis | `uv run ruff format path/to/file.py` |
| Pyright diagnosis | `uv run pyright` |
| ty diagnosis | `uv run --locked --group=dev ty check` |

| Change type | Minimum validation |
| --- | --- |
| Docs or repository metadata only | `uv run pre-commit run --files <changed files>`; build docs when rendering, links, examples, or docs configuration may be affected |
| Docs or repository metadata only | `uv run --locked --group=dev pre-commit run --files <changed files>`; build docs when rendering, links, examples, or docs configuration may be affected |
| Runtime bug fix | Focused regression test plus pre-commit on changed files; broaden when shared behavior is touched |
| Public API, config, or Colang behavior | Focused tests plus related docs/examples; add broader package tests when compatibility risk is meaningful |
| Server, streaming, tracing, actions, or generation | Targeted tests for the changed path and fallback/unsupported path |
Expand Down
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ Run Python commands through uv.
| Focused tests | `make test TEST=path/to/test_file.py::test_name` |
| Full test suite | `make test` |
| Supported Python versions | `uv run tox` |
| Pre-commit hooks | `uv run pre-commit run --all-files` |
| Pre-commit hooks | `make pre-commit` |
| Docs check | `make docs-fern` |
| Package coverage | `make test-coverage` |

Expand All @@ -193,11 +193,11 @@ tracing, or docs.
Set up local pre-commit hooks if you want checks to run before every commit:

```bash
uv run pre-commit install
make pre-commit-install
```

The pre-commit configuration runs Ruff, Ruff format, license-header insertion,
and Pyright.
and ty.

## Documentation and Notebooks

Expand Down
11 changes: 7 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.PHONY: help
.PHONY: test test-parallel test-serial test-benchmark test-watch test-coverage test-profile record-cassettes rewrite-cassettes replay-cassettes snapshot-cassettes check-record-test-env warm-fastembed-cache
.PHONY: docs-fern docs-fern-strict docs-fern-live docs-fern-preview-watch docs-fern-generate-sdk docs-fern-fix-empty-links docs-fern-publish-staging docs-fern-publish-public
.PHONY: pre-commit
.PHONY: pre-commit pre-commit-install

.DEFAULT_GOAL := help

Expand Down Expand Up @@ -103,8 +103,10 @@ docs-fern-fix-empty-links:
node scripts/fix-empty-fern-links.mjs

pre-commit:
uv run pre-commit install
uv run pre-commit run --all-files
uv run --locked --group=dev pre-commit run --all-files

pre-commit-install:
uv run --locked --group=dev pre-commit install

help:
@printf '%s\n' \
Expand Down Expand Up @@ -145,4 +147,5 @@ help:
' docs-fern-fix-empty-links Replace empty Markdown links with titles from Fern navigation' \
'' \
'Maintenance:' \
' pre-commit Install and run pre-commit hooks'
' pre-commit Run pre-commit hooks against all files' \
' pre-commit-install Install the Git pre-commit hook'
39 changes: 29 additions & 10 deletions nemoguardrails/actions/action_dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,30 @@
import os
from importlib.machinery import ModuleSpec
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Tuple, Union, cast
from typing import Any, Awaitable, Callable, Dict, List, Optional, Protocol, Tuple, Type, TypeAlias, Union, cast

from nemoguardrails import utils
from nemoguardrails.exceptions import LLMCallException

log = logging.getLogger(__name__)


class AsyncInvokableAction(Protocol):
def ainvoke(self, *args: Any, **kwargs: Any) -> Awaitable[Any]: ...


class RunnableAction(Protocol):
def run(self, *args: Any, **kwargs: Any) -> Any: ...


RegisteredAction: TypeAlias = Union[
Callable[..., Any],
Type[Any],
AsyncInvokableAction,
RunnableAction,
]


class ActionDispatcher:
def __init__(
self,
Expand All @@ -48,7 +64,7 @@ def __init__(
"""
log.info("Initializing action dispatcher")

self._registered_actions: Dict[str, Any] = {}
self._registered_actions: Dict[str, RegisteredAction] = {}

if load_all_actions:
# TODO: check for better way to find actions dir path or use constants.py
Expand Down Expand Up @@ -117,17 +133,19 @@ def load_actions_from_path(self, path: Path):
if os.path.exists(actions_py_path):
self._registered_actions.update(self._load_actions_from_module(actions_py_path))

def register_action(self, action: Callable, name: Optional[str] = None, override: bool = True):
def register_action(self, action: RegisteredAction, name: Optional[str] = None, override: bool = True):
"""Registers an action with the given name.

Args:
action (Callable): The action function.
action (RegisteredAction): The action function or runnable object.
name (Optional[str]): The name of the action. Defaults to None.
override (bool): If an action already exists, whether it should be overridden or not.
"""
if name is None:
action_meta = getattr(action, "action_meta", None)
action_name = action_meta["name"] if action_meta else action.__name__
action_name = action_meta["name"] if action_meta else getattr(action, "__name__", None)
if not isinstance(action_name, str) or not action_name:
raise ValueError("An explicit name is required for actions without __name__.")
else:
action_name = name

Expand Down Expand Up @@ -165,7 +183,7 @@ def has_registered(self, name: str) -> bool:
name = self._normalize_action_name(name)
return name in self.registered_actions

def get_action(self, name: str) -> Optional[Callable]:
def get_action(self, name: str) -> Optional[RegisteredAction]:
"""Get the registered action by name.

Args:
Expand Down Expand Up @@ -194,11 +212,11 @@ async def execute_action(

if action_name in self._registered_actions:
log.info("Executing registered action: %s", action_name)
maybe_fn: Optional[Callable] = self._registered_actions.get(action_name, None)
maybe_fn: Optional[RegisteredAction] = self._registered_actions.get(action_name, None)
if not maybe_fn:
raise Exception(f"Action '{action_name}' is not registered.")

fn = cast(Callable, maybe_fn)
fn: Any = maybe_fn
# Actions that are registered as classes are initialized lazy, when
# they are first used.
if inspect.isclass(fn):
Expand All @@ -216,10 +234,11 @@ async def execute_action(
else:
log.warning(f"Synchronous action `{action_name}` has been called.")

elif hasattr(fn, "ainvoke") and callable(fn.ainvoke): # type: ignore[union-attr]
elif callable(ainvoke := getattr(fn, "ainvoke", None)):
# Duck-type check for LangChain Runnables (or any object
# with ainvoke) to avoid importing langchain in core.
result = await fn.ainvoke(input=params) # type: ignore[union-attr]
async_invoke = cast(Callable[..., Awaitable[Any]], ainvoke)
result = await async_invoke(input=params)
else:
# TODO: there should be a common base class here
fn_run_func = getattr(fn, "run", None)
Expand Down
9 changes: 5 additions & 4 deletions nemoguardrails/actions/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
TypedDict,
TypeVar,
Union,
cast,
)


Expand Down Expand Up @@ -58,7 +57,7 @@ def action(
callable: The decorated function or class.
"""

def decorator(fn_or_cls: Union[Callable, Type]) -> Union[Callable, Type]:
def decorator(fn_or_cls: T) -> T:
"""Inner decorator function to add metadata to the action.

Args:
Expand All @@ -67,7 +66,9 @@ def decorator(fn_or_cls: Union[Callable, Type]) -> Union[Callable, Type]:
fn_or_cls_target = getattr(fn_or_cls, "__func__", fn_or_cls)

# Action name is optional for the decorator, but mandatory for ActionMeta TypedDict
action_name: str = cast(str, name or fn_or_cls.__name__)
action_name = name or getattr(fn_or_cls, "__name__", None)
if not isinstance(action_name, str) or not action_name:
raise ValueError("An explicit name is required for actions without __name__.")

action_meta: ActionMeta = {
"name": action_name,
Expand All @@ -79,7 +80,7 @@ def decorator(fn_or_cls: Union[Callable, Type]) -> Union[Callable, Type]:
setattr(fn_or_cls_target, "action_meta", action_meta)
return fn_or_cls

return decorator # pyright: ignore (TODO - resolve how the Actionable Protocol doesn't resolve the issue)
return decorator


@dataclass
Expand Down
8 changes: 5 additions & 3 deletions nemoguardrails/actions/llm/generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def _extract_user_message_example(self, flow: Flow) -> None:

el = elements[1]
if isinstance(el, SpecOp):
spec_op: SpecOp = cast(SpecOp, el)
spec_op: SpecOp = el

if spec_op.op == "match":
# The SpecOp.spec type is Union[Spec, dict]. Convert Dict to Spec if it's provided
Expand Down Expand Up @@ -170,7 +170,7 @@ def _extract_bot_message_example(self, flow: Flow):
if not isinstance(el, SpecOp):
return

spec_op: SpecOp = cast(SpecOp, el)
spec_op: SpecOp = el
spec: Dict[str, Any] = (
asdict(spec_op.spec) # TODO! Refactor this function as it's duplicated in many places
if isinstance(spec_op.spec, Spec)
Expand Down Expand Up @@ -1286,7 +1286,9 @@ async def generate_intent_steps_message(
llm_call_info_var.set(LLMCallInfo(task=Task.GENERATE_INTENT_STEPS_MESSAGE.value))

gen_options: Optional[GenerationOptions] = generation_options_var.get()
llm_params = (gen_options and gen_options.llm_params) or {}
llm_params = (
gen_options.llm_params if gen_options is not None and gen_options.llm_params is not None else {}
)
additional_params = {
**llm_params,
"temperature": self.config.lowest_temperature,
Expand Down
9 changes: 5 additions & 4 deletions nemoguardrails/cli/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ async def _run_chat_v1_0(
# If we have streaming from a locally loaded config, we initialize the handler.
if streaming and not server_url and rails_app:
try:
bot_message_list = []
async for chunk in rails_app.stream_async(messages=history):
bot_message_list: list[str] = []
async for chunk in rails_app.stream_async(messages=history, include_metadata=False):
if isinstance(chunk, str) and chunk.startswith('{"error"'):
try:
error_data = json.loads(chunk)
Expand All @@ -93,7 +93,7 @@ async def _run_chat_v1_0(
break

console.print("[green]" + f"{chunk}" + "[/]", end="")
bot_message_list.append(chunk)
bot_message_list.append(cast(str, chunk))

bot_message_text = "".join(bot_message_list)
bot_message = {"role": "assistant", "content": bot_message_text}
Expand Down Expand Up @@ -474,7 +474,8 @@ async def _check_local_async_actions():
and chat_state.output_events[0]["counter"] == 0
):
# If there are no pending actions, we stop
check_task.cancel()
if check_task is not None:
check_task.cancel()
check_task = None
if chat_state.output_state is not None:
debugger.set_output_state(chat_state.output_state)
Expand Down
6 changes: 3 additions & 3 deletions nemoguardrails/cli/debugger.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ def tree(
main_flow = state.flow_id_states["main"][0]

root = Tree("main")
queue = [[main_flow, root]]
queue: list[tuple[FlowState, Tree]] = [(main_flow, root)]

while queue:
flow_state: FlowState
Expand Down Expand Up @@ -237,7 +237,7 @@ def tree(
head_element = elements[head.position]

if isinstance(head_element, SpecOp):
head_element_spec_op = cast(SpecOp, head_element)
head_element_spec_op = head_element
if head_element_spec_op.op == "match":
# Convert Spec to Spec object if it's a Dict
spec: Spec = (
Expand Down Expand Up @@ -270,7 +270,7 @@ def tree(
child_flow_label = "[" + child_flow_label + "]"

child_node = node.add(child_flow_label)
queue.append([child_flow_state, child_node])
queue.append((child_flow_state, child_node))

console.print(root)

Expand Down
2 changes: 1 addition & 1 deletion nemoguardrails/cli/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def select_provider_type() -> Optional[ProviderType]:

# exact match only
if result in provider_types:
return cast(ProviderType, result) # type: ignore
return cast(ProviderType, result)

# fuzzy match
matches = [t for t in provider_types if result.lower() in t.lower()]
Expand Down
2 changes: 1 addition & 1 deletion nemoguardrails/embeddings/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
try:
import redis # type: ignore
except ImportError:
redis = None # type: ignore
redis = None

from nemoguardrails.rails.llm.config import EmbeddingsCacheConfig

Expand Down
2 changes: 1 addition & 1 deletion nemoguardrails/embeddings/providers/azureopenai.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class AzureEmbeddingModel(EmbeddingModel):

def __init__(self, embedding_model: str):
try:
from openai import AzureOpenAI # type: ignore
from openai import AzureOpenAI
except ImportError:
raise ImportError("Could not import openai, please install it with `pip install openai`.")
# Set Azure OpenAI API credentials
Expand Down
2 changes: 1 addition & 1 deletion nemoguardrails/embeddings/providers/fastembed.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class FastEmbedEmbeddingModel(EmbeddingModel):
engine_name = "FastEmbed"

def __init__(self, embedding_model: str, **kwargs):
from fastembed import TextEmbedding as Embedding # type: ignore
from fastembed import TextEmbedding as Embedding

# Enabling a short form model name for all-MiniLM-L6-v2.
if embedding_model == "all-MiniLM-L6-v2":
Expand Down
2 changes: 1 addition & 1 deletion nemoguardrails/embeddings/providers/nim.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class NIMEmbeddingModel(EmbeddingModel):

def __init__(self, embedding_model: str, **kwargs):
try:
from langchain_nvidia_ai_endpoints import NVIDIAEmbeddings # type: ignore
from langchain_nvidia_ai_endpoints import NVIDIAEmbeddings

self.model = embedding_model
self.document_embedder = NVIDIAEmbeddings(model=embedding_model, **kwargs)
Expand Down
11 changes: 5 additions & 6 deletions nemoguardrails/embeddings/providers/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,16 @@ def __init__(
**kwargs,
):
try:
import openai # type: ignore
from openai import OpenAI # type: ignore
except ImportError:
raise ImportError("Could not import openai, please install it with `pip install openai`.")
if openai.__version__ < "1.0.0": # type: ignore
import openai
except ImportError as err:
raise ImportError("Could not import openai, please install it with `pip install openai`.") from err
if openai.__version__ < "1.0.0":
Comment thread
coderabbitai[bot] marked this conversation as resolved.
raise RuntimeError(
"`openai<1.0.0` is no longer supported. Please upgrade using `pip install openai>=1.0.0`."
)

self.model = embedding_model
self.client = OpenAI(**kwargs)
self.client = openai.OpenAI(**kwargs)

self.embedding_size_dict = {
"text-embedding-ada-002": 1536,
Expand Down
Loading