Spaces:
Running
Running
File size: 13,176 Bytes
31add3b |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 |
import pytest
from unittest.mock import patch, AsyncMock, MagicMock
from app import synthesize_responses # Removed generate_visualization from here as it's the SUT for one test
from insight_state import InsightFlowState
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
from langchain_openai import ChatOpenAI # For spec
from openai import AsyncOpenAI # For spec of the client if we were to patch app.openai_async_client
# Import app to allow patching its global openai_async_client
import app as application_module
# Define a plausible system prompt for the synthesizer for use in assertion
SYNTHESIZER_SYSTEM_PROMPT_TEMPLATE = """You are a master synthesizer AI. Your task is to integrate the following diverse perspectives into a single, coherent, and insightful response. Ensure that the final synthesis is well-structured, easy to understand, and accurately reflects the nuances of each provided viewpoint. Do not simply list the perspectives; weave them together.
Perspectives:
{formatted_perspectives}
Synthesized Response:"""
@pytest.mark.asyncio
async def test_synthesize_responses_node():
"""
Test the synthesize_responses node to ensure it calls the llm_synthesizer
with the correct prompt and updates the state.
"""
initial_state = InsightFlowState(
query="Test Query",
selected_personas=["analytical", "scientific"],
persona_responses={
"analytical": "Analytical perspective text.",
"scientific": "Scientific perspective text."
},
synthesized_response=None,
current_step_name="execute_persona_tasks" # Previous step
)
mock_synthesized_text = "This is the beautifully synthesized response from the LLM."
# Expected formatted perspectives string
expected_perspectives_text = ("- Perspective from analytical: Analytical perspective text.\n"
"- Perspective from scientific: Scientific perspective text.")
expected_final_prompt_content = SYNTHESIZER_SYSTEM_PROMPT_TEMPLATE.format(formatted_perspectives=expected_perspectives_text)
# Create a mock for the entire llm_synthesizer object in app.py
mock_llm_synthesizer_replacement = MagicMock(spec=ChatOpenAI)
# Mock its ainvoke method
mock_llm_synthesizer_replacement.ainvoke = AsyncMock(return_value=AIMessage(content=mock_synthesized_text))
# Patch app.llm_synthesizer to be our mock_llm_synthesizer_replacement
with patch('app.llm_synthesizer', new=mock_llm_synthesizer_replacement) as mock_synthesizer_in_app:
# Call the actual node function
# synthesize_responses (from app) will now use the patched llm_synthesizer (mock_synthesizer_in_app)
output_state = await synthesize_responses(initial_state.copy())
# Assertions
mock_synthesizer_in_app.ainvoke.assert_called_once()
args, kwargs = mock_synthesizer_in_app.ainvoke.call_args
called_messages = args[0] # The 'messages' argument
assert len(called_messages) == 1 # Expecting a single system message for simplicity here, or system + human
# For now, let's assume the prompt is constructed as a single SystemMessage containing everything
# This might evolve when we implement the actual prompt construction in app.py
assert isinstance(called_messages[0], SystemMessage)
assert called_messages[0].content == expected_final_prompt_content
assert output_state["synthesized_response"] == mock_synthesized_text
assert output_state["current_step_name"] == "generate_visualization"
assert output_state["persona_responses"] == initial_state["persona_responses"] # Should not change
@pytest.mark.asyncio
@patch('app.generate_dalle_image') # Corrected: Patch where it's used by app.generate_visualization
@patch('app.openai_async_client')
@patch('app.generate_mermaid_code') # <--- ADDED PATCH for mermaid generation utility
async def test_generate_visualization_node_creates_dalle_and_mermaid(
mock_app_generate_mermaid_code, # <--- ADDED mock argument
mock_app_openai_client,
mock_app_level_generate_dalle_image_func # Renamed to reflect it's app's view of the function
):
"""
Test the generate_visualization node to ensure it calls generate_dalle_image
and generate_mermaid_code, and updates the state with image URL and mermaid code.
"""
initial_synthesized_response = "This is a detailed synthesized response about complex topics."
initial_state = InsightFlowState(
query="Test Query for Visuals",
synthesized_response=initial_synthesized_response,
persona_responses={}, # Explicitly initialize for completeness
visualization_image_url=None,
visualization_code=None,
current_step_name="synthesize_responses"
)
expected_dalle_url = "https://dalle.example.com/generated_image.png"
mock_app_level_generate_dalle_image_func.return_value = expected_dalle_url
expected_mermaid_output = "graph LR; A-->B; C-->D;" # Mocked mermaid output
mock_app_generate_mermaid_code.return_value = expected_mermaid_output
# Ensure llm_mermaid_generator is available in the application_module for the test context
# If app.llm_mermaid_generator is None during the test, the call might be skipped.
# We assume it's initialized by initialize_configurations(), which should run before/during app import or be called by on_chat_start.
# For node tests, if direct LLM calls are made, ensure they are properly mocked or the LLMs are available.
# Here, generate_mermaid_code (the util) takes the LLM as an arg, which app.generate_visualization supplies.
# So, we need to make sure app.llm_mermaid_generator is a mock or a real object for the call.
# Let's patch app.llm_mermaid_generator to be a MagicMock for this test to ensure it's passed correctly.
with patch.object(application_module, 'llm_mermaid_generator', new_callable=MagicMock) as mock_llm_mermaid_in_app:
output_state = await application_module.generate_visualization(initial_state.copy())
# DALL-E Assertions
mock_app_level_generate_dalle_image_func.assert_called_once()
called_dalle_kwargs = mock_app_level_generate_dalle_image_func.call_args.kwargs
assert called_dalle_kwargs.get('prompt') == f"A hand-drawn style visual note or sketch representing the key concepts of: {initial_synthesized_response}"
assert called_dalle_kwargs.get('client') is mock_app_openai_client
assert output_state["visualization_image_url"] == expected_dalle_url
# Mermaid Assertions
mock_app_generate_mermaid_code.assert_called_once_with(
initial_synthesized_response,
mock_llm_mermaid_in_app # Assert it was called with the (mocked) app.llm_mermaid_generator
)
assert output_state["visualization_code"] == expected_mermaid_output
# General State Assertions
assert output_state["current_step_name"] == "present_results"
assert output_state["synthesized_response"] == initial_synthesized_response
assert output_state["persona_responses"] == initial_state["persona_responses"] # Should not change
@pytest.mark.asyncio
@patch('app.cl.user_session.get')
@patch('app.cl.Image') # Mock for cl.Image class
@patch('app.cl.Text') # <--- ADDED PATCH for cl.Text class
@patch('app.cl.Message') # Mock for cl.Message class
async def test_present_results_node_sends_all_content(
mock_message_class,
mock_text_class, # <--- ADDED mock argument
mock_image_class,
mock_user_session_get,
):
"""Test present_results node sends synthesized response, visuals, and perspectives."""
# Configure app.cl.user_session.get mock
def user_session_get_side_effect(key, default=None):
if key == "show_visualization":
return True
if key == "show_perspectives":
return True
# Fallback for other keys used by the node if any (e.g. in future)
# Currently, present_results from app.py doesn't use default values from cl.user_session.get for these booleans directly in its logic
# It implies they should exist. The main app's on_chat_start sets them.
# For this test, we only care about show_visualization and show_perspectives.
return default
mock_user_session_get.side_effect = user_session_get_side_effect
# Configure cl.Message mock (the class itself)
# Each time cl.Message() is called, it returns a new mock_message_instance.
# This instance needs an async 'send' method.
# To allow multiple calls to cl.Message and check them, we need Message instances to be distinct if their send methods are checked individually.
# A simpler way for now is to check the call_args_list of mock_message_class.
# The send method is on the instance, so we make the class return an instance that has an async send.
async def mock_send(*args, **kwargs):
pass # Mock async send
# Store all created message instances to check their send calls individually if needed
created_message_mocks = []
def message_constructor_side_effect(*args, **kwargs):
instance = MagicMock(name=f"MockClMessageInstance_{len(created_message_mocks)}")
instance.send = AsyncMock(wraps=mock_send) # each instance gets its own send mock
instance.content = kwargs.get("content")
instance.elements = kwargs.get("elements")
created_message_mocks.append(instance)
return instance
mock_message_class.side_effect = message_constructor_side_effect
# Configure cl.Image mock (the class itself)
mock_image_instance_returned_by_constructor = MagicMock(name="MockClImageInstance")
mock_image_class.return_value = mock_image_instance_returned_by_constructor
# Configure cl.Text mock (the class itself)
mock_text_instance_returned_by_constructor = MagicMock(name="MockClTextInstance")
mock_text_class.return_value = mock_text_instance_returned_by_constructor
initial_state = application_module.InsightFlowState(
query="Test Query for Presentation",
synthesized_response="Final synthesized answer.",
persona_responses={"analytical": "Analytical point.", "creative": "Creative idea."},
visualization_image_url="http://example.com/dalle.png",
visualization_code="graph TD; X-->Y;",
current_step_name="generate_visualization" # Previous step
)
output_state = await application_module.present_results(initial_state.copy())
# Assertions for user_session.get calls
mock_user_session_get.assert_any_call("show_visualization")
mock_user_session_get.assert_any_call("show_perspectives")
# Assertions for cl.Message calls
# Expected calls: 1 for synthesized, 1 for DALL-E, 1 for Mermaid, 2 for perspectives = 5 calls
assert mock_message_class.call_count == 5
# Ensure each created message mock had its send method called once
for msg_mock_instance in created_message_mocks:
msg_mock_instance.send.assert_called_once()
all_message_constructor_calls = mock_message_class.call_args_list
# 1. Synthesized response
call_synthesized_kwargs = all_message_constructor_calls[0].kwargs
assert call_synthesized_kwargs.get('content') == "Final synthesized answer."
assert not call_synthesized_kwargs.get('elements')
# 2. DALL-E Image
call_dalle_kwargs = all_message_constructor_calls[1].kwargs
mock_image_class.assert_called_once_with(
url="http://example.com/dalle.png",
name="dalle_visualization",
display="inline",
size="large" # Default in app.py
)
assert call_dalle_kwargs.get('elements') == [mock_image_instance_returned_by_constructor]
# Content for image message could be None or specific text. Let's assume it's empty if not specified.
assert call_dalle_kwargs.get('content') == "" # Or check for a specific title if app.py adds one
# 3. Mermaid Diagram
call_mermaid_kwargs = all_message_constructor_calls[2].kwargs
mock_text_class.assert_called_once_with(
content="graph TD; X-->Y;", # The raw mermaid code from initial_state
mime_type="text/mermaid",
name="generated_diagram",
display="inline"
)
assert call_mermaid_kwargs.get('elements') == [mock_text_instance_returned_by_constructor]
assert call_mermaid_kwargs.get('content') == "" # Expecting empty content for message with element
# 4. Persona Perspectives (order within perspectives might not be guaranteed due to dict iteration)
perspective_contents_sent = set()
perspective_contents_sent.add(all_message_constructor_calls[3].kwargs.get('content'))
perspective_contents_sent.add(all_message_constructor_calls[4].kwargs.get('content'))
expected_perspective1_content = "**Perspective from analytical:**\nAnalytical point."
expected_perspective2_content = "**Perspective from creative:**\nCreative idea."
assert expected_perspective1_content in perspective_contents_sent
assert expected_perspective2_content in perspective_contents_sent
# Check state update
assert output_state["current_step_name"] == "results_presented"
|