Skip to content

Conversation

tgasser-nv
Copy link
Collaborator

@tgasser-nv tgasser-nv commented Aug 29, 2025

Description

Nemo Guardrails doesn't have type-checking enforced in the project today. This PR will be used to compile type-cleaning related to the nemoguardrails/actions module prior to merging into the top-level type-cleaning PR1367.

Progress

Prior to this work the actions module had 189 errors, 0 warnings, and 0 informations.

  • init.py
  • pycache
  • action_dispatcher.py
  • actions.py
  • core.py
  • langchain
  • llm
  • math.py
  • output_mapping.py
  • retrieve_relevant_chunks.py
  • summarize_document.py
  • v2_x
  • validation

Related Issue(s)

Checklist

  • I've read the CONTRIBUTING guidelines.
  • I've updated the documentation if applicable.
  • I've added tests if applicable.
  • @mentions of the person or team responsible for reviewing proposed changes.

@codecov-commenter
Copy link

codecov-commenter commented Aug 29, 2025

Codecov Report

❌ Patch coverage is 84.09091% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 71.62%. Comparing base (9a1d178) to head (74026bc).
⚠️ Report is 4 commits behind head on develop.

Files with missing lines Patch % Lines
nemoguardrails/actions/llm/generation.py 82.71% 14 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #1361      +/-   ##
===========================================
- Coverage    71.68%   71.62%   -0.07%     
===========================================
  Files          168      171       +3     
  Lines        16862    17053     +191     
===========================================
+ Hits         12088    12214     +126     
- Misses        4774     4839      +65     
Flag Coverage Δ
python 71.62% <84.09%> (-0.07%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
nemoguardrails/actions/llm/utils.py 80.00% <100.00%> (ø)
nemoguardrails/context.py 100.00% <100.00%> (ø)
nemoguardrails/actions/llm/generation.py 85.00% <82.71%> (-0.96%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@tgasser-nv tgasser-nv self-assigned this Aug 29, 2025
@tgasser-nv tgasser-nv changed the title chore: Type-fixes for generation.py chore: Guardrails Type-fixes Sep 2, 2025
@tgasser-nv tgasser-nv changed the base branch from develop to chore/type-clean-guardrails September 2, 2025 16:19
@tgasser-nv tgasser-nv changed the title chore: Guardrails Type-fixes chore: Guardrails actions type-fixes Sep 2, 2025
@tgasser-nv tgasser-nv marked this pull request as draft September 2, 2025 16:22
@cparisien
Copy link
Collaborator

@tgasser-nv, any guidance on how to review this? I'm struggling a bit. If we're more strongly enforcing types, do we mainly rely on test coverage to ensure we haven't broken anything with the new constraints?

@tgasser-nv tgasser-nv changed the title chore: Guardrails actions type-fixes chore(types): Guardrails actions module type-fixes Sep 5, 2025
@tgasser-nv
Copy link
Collaborator Author

@tgasser-nv, any guidance on how to review this? I'm struggling a bit. If we're more strongly enforcing types, do we mainly rely on test coverage to ensure we haven't broken anything with the new constraints?

Yes, I run the unit-tests continuously as I'm cleaning Pyright errors. We have far too many optional types in our Pydantic models and function/method signatures, the biggest bucket of fixes are asserting these aren't None before going on to use them.

The lines which are either too complex to fix (i.e. generation.py prompt assignments with 10 levels of nested indentation) or too hard to understand (what is this line doing?!) are marked with # pyright: ignore and a TODO relating to them.

@tgasser-nv
Copy link
Collaborator Author

Status: Pyright errors: 0, waived: 7.
Debugging test_passthrough_llm_action_invoked_via_logs

@tgasser-nv tgasser-nv marked this pull request as ready for review September 6, 2025 15:05
@tgasser-nv
Copy link
Collaborator Author

Fixing the types exposed a bug in an untested function.

The test_passthrough_llm_action_invoked_via_logs test fails because ther serialization.py state_to_json() function doesn't correctly serialize ParsedTaskOutput. I added a skip decorator and created Github issue #1378 to track the work to fix this.

>       raise TypeError(f'Object of type {o.__class__.__name__} '
                        f'is not JSON serializable')
E       TypeError: Object of type ParsedTaskOutput is not JSON serializable

@tgasser-nv tgasser-nv changed the title chore(types): Guardrails actions module type-fixes chore(types): Type-clean /actions Sep 8, 2025
@tgasser-nv tgasser-nv changed the title chore(types): Type-clean /actions chore(types): Type-clean /actions (189 errors) Sep 10, 2025
@tgasser-nv tgasser-nv changed the base branch from chore/type-clean-guardrails to develop September 22, 2025 21:31
@tgasser-nv tgasser-nv force-pushed the chore/type-clean-actions-llm-generation branch from 5fb3780 to b751596 Compare September 24, 2025 19:14
@codecov-commenter
Copy link

codecov-commenter commented Sep 25, 2025

Comment on lines 399 to 400
assert event
assert event["type"] == "UserMessage"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not related to your changes, but should not use assert

  if not event:
      raise ValueError(
          "No user message found in event stream. Unable to generate user intent."
      )
  if event["type"] != "UserMessage":
      raise ValueError(
          f"Expected UserMessage event, but found {event['type']}. "
          "Cannot generate user intent from this event type."
      )

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I agree. I wasn't sure if there was a good reason to use assert here rather than raising exceptions and wanted to be consistent with existing code. Will make this change.

Comment on lines 1243 to 1244
assert event
assert event["type"] == "UserMessage"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above we should not use assert in our non-test codes

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep agree here, added your sample code in from the above comment for this one too. Not worrying about DRYing the code too much since this file has three functions with untestable CC before refactoring:

/Users/tgasser/projects/nemo_guardrails/nemoguardrails/actions/llm/generation.py
    M 1235:4 LLMGenerationActions.generate_intent_steps_message - F (50)
    M 864:4 LLMGenerationActions.generate_bot_message - F (43)
    M 381:4 LLMGenerationActions.generate_user_intent - F (42)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll DRY this up when refactoring to address the Cyclomatic Complexity

Comment on lines 831 to 837
results = (
await self.flows_index.search(
text=f"${var_name} = ", max_results=5, threshold=None
)
if var_name
else None
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: To me in line if-else in python is less readable

           if var_name:
               results = await self.flows_index.search(
                   text=f"${var_name} = ", max_results=5, threshold=None
               )
           else:
               results = None

but no need to make this change, it is just a preference 👍🏻

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, I reworked this to use the same structure as above

)

if not results:
raise Exception("No results found while generating value")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure about this Exception

I think None value should be OK, the system should generate a value based on instructions alone, without needing similar examples from the flows index.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, I added a guard statement before the examples string append loop:

            # We add these in reverse order so the most relevant is towards the end.
            if results:
                for result in reversed(results):
                    # If the flow includes "GenerateValueAction", we ignore it as we don't want the LLM
                    # to learn to predict it.
                    if "GenerateValueAction" not in result.text:
                        examples += f"{result.text}\n\n"

triggering_flow_id = flow_id
if not triggering_flow_id:
raise Exception(
f"No flow_id provided to generate flow."
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure

Suggested change
f"No flow_id provided to generate flow."
"No flow_id provided to generate flow."

relative_filepath = Path(module.__file__).relative_to(Path.cwd())
except ValueError:
relative_filepath = Path(module.__file__).resolve()
# todo! What are we trying to do here?
Copy link
Collaborator

@Pouyanpi Pouyanpi Sep 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😆

I think the intent was to make it easier to traceback the problematic file.
So if there are multiple identical filenames, which one is problematic

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, I added this back in again with some extra checking to make it type-clean

@Pouyanpi Pouyanpi requested a review from Copilot September 26, 2025 09:54
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR performs type-cleaning for the NeMo Guardrails actions module to address 189 type errors identified prior to enforcing type-checking. The changes include adding type annotations, import fixes, handling optional types, and adding necessary tests to cover edge cases.

  • Adds comprehensive type annotations throughout the actions module
  • Updates imports to use proper langchain-core modules and adds type checking compatibility
  • Improves error handling and null checks in action dispatcher and generation functions

Reviewed Changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
nemoguardrails/actions/actions.py Adds Protocol definitions and improved type annotations for action decorators
nemoguardrails/actions/action_dispatcher.py Enhances type safety with better annotations and error handling for action execution
nemoguardrails/actions/llm/generation.py Major type improvements for LLM generation actions with proper optional handling
nemoguardrails/actions/llm/utils.py Updates LLM utility functions with enhanced type annotations and null checks
nemoguardrails/actions/v2_x/generation.py Type-safe improvements for v2.x generation actions
tests/ Adds comprehensive test coverage for previously untested edge cases
pyproject.toml Enables type checking for the actions module

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

)

result = await llm_call(
llm,
Copy link
Preview

Copilot AI Sep 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable llm is used here but should be generation_llm to match the pattern used elsewhere in the function and maintain type consistency.

Suggested change
llm,
generation_llm,

Copilot uses AI. Check for mistakes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good catch, applied it locally and checked it with unit-tests

from langchain_core.language_models import BaseChatModel
from langchain_core.language_models.llms import BaseLLM
from langchain_text_splitters import ElementType
from pytest_asyncio.plugin import event_loop
Copy link
Preview

Copilot AI Sep 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import appears to be a development/testing dependency that shouldn't be in production code. This import should be removed as it's not used in the file and pytest_asyncio is a test-only dependency.

Suggested change
from pytest_asyncio.plugin import event_loop

Copilot uses AI. Check for mistakes.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tgasser-nv

There are some unsused imports including the pytest_asynci

from langchain_text_splitters import ElementType
from pytest_asyncio.plugin import event_loop

and

    get_initial_actions,

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed these unused imports

triggering_flow_id = flow_id
if not triggering_flow_id:
raise Exception(
f"No flow_id provided to generate flow."
Copy link
Preview

Copilot AI Sep 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message uses an f-string but doesn't include any variables. The message should be a plain string: \"No flow_id provided to generate flow.\"

Suggested change
f"No flow_id provided to generate flow."
"No flow_id provided to generate flow."

Copilot uses AI. Check for mistakes.


event = get_last_user_utterance_event_v2_x(events)
if not event:
raise Exception("Passthrough LLM Action couldn't find last user utterance")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: better to avoid general Exception

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, replaced with RuntimeError

log.info("Generating flow for name: {name}")

if not self.instruction_flows_index:
raise Exception("No instruction flows index has been created.")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RuntimeError maybe

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep updated this.

Comment on lines 38 to 39
class Actionable(Protocol):
"""Protocol for any object with ActionMeta metadata (i.e. decorated with @action)"""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Protocol is not used and adds unnecessary complexity. We're already doing runtime attribute checking with hasattr(obj, 'action_meta') throughout the codebase. maybe a util function like is_action. duck typing

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got rid of the Protocol, I think I tried to solve the type issues with this and couldn't get it working. That's when I went back to checking attributes dynamically. I removed this

Copy link
Collaborator

@Pouyanpi Pouyanpi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tgasser-nv, thanks, looks good! Please merge after addressing the comments :+1

@tgasser-nv
Copy link
Collaborator Author

tgasser-nv commented Sep 26, 2025

I addressed all feedback, and unit-tests are passing locally. For some reason the Github action Python 3.10 unit-tests are crashing with Fatal Python error: Illegal instruction. I can't debug any further because I can't get to the machine the actions run on.

I ran locally and all the unit-tests passed on Python 3.10 locally. Full logs here:
20250926_pytest_py3.10.18.log

============================= test session starts ==============================
platform darwin -- Python 3.10.18, pytest-8.4.1, pluggy-1.6.0 -- /Users/tgasser/Library/Caches/pypoetry/virtualenvs/nemoguardrails-qkVbfMSD-py3.10/bin/python
cachedir: .pytest_cache
rootdir: /Users/tgasser/projects/nemo_guardrails
configfile: pytest.ini
testpaths: tests, docs/colang-2/examples
plugins: httpx-0.35.0, anyio-4.10.0, langsmith-0.4.20, cov-6.2.1, asyncio-0.26.0, profiling-1.8.1
asyncio: mode=strict, asyncio_default_fixture_loop_scope=function, asyncio_default_test_loop_scope=function
collecting ... collected 1681 items

tests/colang/parser/test_basic.py::test_1 PASSED                         [  0%]
tests/colang/parser/test_basic.py::test_2 PASSED                         [  0%
tests/colang/parser/test_basic.py::test_3 PASSED                         [  0%]
tests/colang/parser/v2_x/test_ast.py::test_basic PASSED                  [  0%]
....

tests/v2_x/test_various_mechanics.py::test_expr_func_search PASSED       [ 99%]
tests/v2_x/test_various_mechanics.py::test_generate_value_with_NLD PASSED [ 99%]
tests/v2_x/test_various_mechanics.py::test_flow_states_info PASSED       [100%]

================ 1573 passed, 108 skipped in 137.96s (0:02:17) =================

With this I'm going to bypass the Python 3.10 unit-test result and merge.

@tgasser-nv tgasser-nv merged commit 67de947 into develop Sep 26, 2025
27 of 32 checks passed
@tgasser-nv tgasser-nv deleted the chore/type-clean-actions-llm-generation branch September 26, 2025 21:47
Pouyanpi pushed a commit that referenced this pull request Sep 29, 2025
Type-cleaned all files under `nemoguard/actions` and added them to pyright pre-commit hooks so type-coverage doesn't regress.
Pouyanpi pushed a commit that referenced this pull request Oct 1, 2025
Type-cleaned all files under `nemoguard/actions` and added them to pyright pre-commit hooks so type-coverage doesn't regress.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants