|  | 
|  | 1 | +# SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | 
|  | 2 | +# SPDX-License-Identifier: Apache-2.0 | 
|  | 3 | +# | 
|  | 4 | +# Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | 5 | +# you may not use this file except in compliance with the License. | 
|  | 6 | +# You may obtain a copy of the License at | 
|  | 7 | +# | 
|  | 8 | +# http://www.apache.org/licenses/LICENSE-2.0 | 
|  | 9 | +# | 
|  | 10 | +# Unless required by applicable law or agreed to in writing, software | 
|  | 11 | +# distributed under the License is distributed on an "AS IS" BASIS, | 
|  | 12 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | 13 | +# See the License for the specific language governing permissions and | 
|  | 14 | +# limitations under the License. | 
|  | 15 | + | 
|  | 16 | +import logging | 
|  | 17 | +from unittest.mock import patch | 
|  | 18 | + | 
|  | 19 | +import pytest | 
|  | 20 | + | 
|  | 21 | +from nemoguardrails import RailsConfig | 
|  | 22 | +from tests.utils import TestChat | 
|  | 23 | + | 
|  | 24 | + | 
|  | 25 | +@pytest.mark.asyncio | 
|  | 26 | +async def test_bot_thinking_event_logged_in_runtime(caplog): | 
|  | 27 | +    test_reasoning_trace = "Let me think about this step by step..." | 
|  | 28 | + | 
|  | 29 | +    caplog.set_level(logging.INFO) | 
|  | 30 | + | 
|  | 31 | +    with patch( | 
|  | 32 | +        "nemoguardrails.actions.llm.generation.get_and_clear_reasoning_trace_contextvar" | 
|  | 33 | +    ) as mock_get_reasoning: | 
|  | 34 | +        mock_get_reasoning.return_value = test_reasoning_trace | 
|  | 35 | + | 
|  | 36 | +        config = RailsConfig.from_content(config={"models": [], "passthrough": True}) | 
|  | 37 | +        chat = TestChat(config, llm_completions=["The answer is 42"]) | 
|  | 38 | + | 
|  | 39 | +        await chat.app.generate_events_async( | 
|  | 40 | +            [{"type": "UserMessage", "text": "What is the answer?"}] | 
|  | 41 | +        ) | 
|  | 42 | + | 
|  | 43 | +        bot_thinking_logs = [ | 
|  | 44 | +            record | 
|  | 45 | +            for record in caplog.records | 
|  | 46 | +            if "Event :: BotThinking" in record.message | 
|  | 47 | +        ] | 
|  | 48 | +        assert len(bot_thinking_logs) >= 1 | 
|  | 49 | + | 
|  | 50 | + | 
|  | 51 | +@pytest.mark.asyncio | 
|  | 52 | +async def test_bot_message_event_logged_in_runtime(caplog): | 
|  | 53 | +    caplog.set_level(logging.INFO) | 
|  | 54 | + | 
|  | 55 | +    config = RailsConfig.from_content(config={"models": [], "passthrough": True}) | 
|  | 56 | +    chat = TestChat(config, llm_completions=["The answer is 42"]) | 
|  | 57 | + | 
|  | 58 | +    await chat.app.generate_events_async( | 
|  | 59 | +        [{"type": "UserMessage", "text": "What is the answer?"}] | 
|  | 60 | +    ) | 
|  | 61 | + | 
|  | 62 | +    bot_message_logs = [ | 
|  | 63 | +        record for record in caplog.records if "Event :: BotMessage" in record.message | 
|  | 64 | +    ] | 
|  | 65 | +    assert len(bot_message_logs) >= 1 | 
|  | 66 | + | 
|  | 67 | + | 
|  | 68 | +@pytest.mark.asyncio | 
|  | 69 | +async def test_context_update_event_logged_in_runtime(caplog): | 
|  | 70 | +    caplog.set_level(logging.INFO) | 
|  | 71 | + | 
|  | 72 | +    config = RailsConfig.from_content(config={"models": [], "passthrough": True}) | 
|  | 73 | +    chat = TestChat(config, llm_completions=["Response"]) | 
|  | 74 | + | 
|  | 75 | +    await chat.app.generate_events_async([{"type": "UserMessage", "text": "Hello"}]) | 
|  | 76 | + | 
|  | 77 | +    context_update_logs = [ | 
|  | 78 | +        record | 
|  | 79 | +        for record in caplog.records | 
|  | 80 | +        if "Event :: ContextUpdate" in record.message | 
|  | 81 | +        or "Event ContextUpdate" in record.message | 
|  | 82 | +    ] | 
|  | 83 | +    assert len(context_update_logs) >= 1 | 
|  | 84 | + | 
|  | 85 | + | 
|  | 86 | +@pytest.mark.asyncio | 
|  | 87 | +async def test_all_events_logged_when_multiple_events_generated(caplog): | 
|  | 88 | +    test_reasoning_trace = "Analyzing..." | 
|  | 89 | + | 
|  | 90 | +    caplog.set_level(logging.INFO) | 
|  | 91 | + | 
|  | 92 | +    with patch( | 
|  | 93 | +        "nemoguardrails.actions.llm.generation.get_and_clear_reasoning_trace_contextvar" | 
|  | 94 | +    ) as mock_get_reasoning: | 
|  | 95 | +        mock_get_reasoning.return_value = test_reasoning_trace | 
|  | 96 | + | 
|  | 97 | +        config = RailsConfig.from_content(config={"models": [], "passthrough": True}) | 
|  | 98 | +        chat = TestChat(config, llm_completions=["Response"]) | 
|  | 99 | + | 
|  | 100 | +        await chat.app.generate_events_async([{"type": "UserMessage", "text": "Test"}]) | 
|  | 101 | + | 
|  | 102 | +        bot_thinking_found = any( | 
|  | 103 | +            "Event :: BotThinking" in record.message for record in caplog.records | 
|  | 104 | +        ) | 
|  | 105 | +        bot_message_found = any( | 
|  | 106 | +            "Event :: BotMessage" in record.message for record in caplog.records | 
|  | 107 | +        ) | 
|  | 108 | + | 
|  | 109 | +        assert bot_thinking_found | 
|  | 110 | +        assert bot_message_found | 
|  | 111 | + | 
|  | 112 | + | 
|  | 113 | +@pytest.mark.asyncio | 
|  | 114 | +async def test_bot_thinking_event_logged_before_bot_message(caplog): | 
|  | 115 | +    test_reasoning_trace = "Step 1: Understand\nStep 2: Respond" | 
|  | 116 | + | 
|  | 117 | +    caplog.set_level(logging.INFO) | 
|  | 118 | + | 
|  | 119 | +    with patch( | 
|  | 120 | +        "nemoguardrails.actions.llm.generation.get_and_clear_reasoning_trace_contextvar" | 
|  | 121 | +    ) as mock_get_reasoning: | 
|  | 122 | +        mock_get_reasoning.return_value = test_reasoning_trace | 
|  | 123 | + | 
|  | 124 | +        config = RailsConfig.from_content(config={"models": [], "passthrough": True}) | 
|  | 125 | +        chat = TestChat(config, llm_completions=["Answer"]) | 
|  | 126 | + | 
|  | 127 | +        await chat.app.generate_events_async( | 
|  | 128 | +            [{"type": "UserMessage", "text": "Question"}] | 
|  | 129 | +        ) | 
|  | 130 | + | 
|  | 131 | +        bot_thinking_idx = None | 
|  | 132 | +        bot_message_idx = None | 
|  | 133 | + | 
|  | 134 | +        for idx, record in enumerate(caplog.records): | 
|  | 135 | +            if "Event :: BotThinking" in record.message and bot_thinking_idx is None: | 
|  | 136 | +                bot_thinking_idx = idx | 
|  | 137 | +            if "Event :: BotMessage" in record.message and bot_message_idx is None: | 
|  | 138 | +                bot_message_idx = idx | 
|  | 139 | + | 
|  | 140 | +        assert bot_thinking_idx is not None | 
|  | 141 | +        assert bot_message_idx is not None | 
|  | 142 | +        assert bot_thinking_idx < bot_message_idx | 
|  | 143 | + | 
|  | 144 | + | 
|  | 145 | +@pytest.mark.asyncio | 
|  | 146 | +async def test_event_history_update_not_logged(caplog): | 
|  | 147 | +    caplog.set_level(logging.INFO) | 
|  | 148 | + | 
|  | 149 | +    config = RailsConfig.from_content(config={"models": [], "passthrough": True}) | 
|  | 150 | +    chat = TestChat(config, llm_completions=["Response"]) | 
|  | 151 | + | 
|  | 152 | +    await chat.app.generate_events_async([{"type": "UserMessage", "text": "Test"}]) | 
|  | 153 | + | 
|  | 154 | +    event_history_update_logs = [ | 
|  | 155 | +        record | 
|  | 156 | +        for record in caplog.records | 
|  | 157 | +        if "Event :: EventHistoryUpdate" in record.message | 
|  | 158 | +        or "Event EventHistoryUpdate" in record.message | 
|  | 159 | +    ] | 
|  | 160 | +    assert len(event_history_update_logs) == 0 | 
0 commit comments