@@ -590,3 +590,252 @@ def test_config_mixed_alias_usage():
590590 assert config .parse_agent_response_max_retries == 12
591591 assert config .max_tool_calls == 28
592592 assert config .tool_call_max_retries == 1 # default value
593+
594+
595+ # Tests for quote normalization in tool input parsing
596+ async def test_tool_node_json_input_with_double_quotes (mock_react_agent ):
597+ """Test that valid JSON with double quotes is parsed correctly."""
598+ tool_input = '{"query": "search term", "limit": 5}'
599+ mock_state = ReActGraphState (agent_scratchpad = [AgentAction (tool = 'Tool A' , tool_input = tool_input , log = 'test' )])
600+
601+ response = await mock_react_agent .tool_node (mock_state )
602+ response = response .tool_responses [- 1 ]
603+
604+ assert isinstance (response , ToolMessage )
605+ assert response .name == "Tool A"
606+ # When JSON is successfully parsed, the mock tool receives a dict and LangChain extracts the "query" value
607+ assert response .content == "search term" # The mock tool extracts the query field value
608+
609+
610+ async def test_tool_node_json_input_with_single_quotes_normalization_enabled (mock_react_agent ):
611+ """Test that JSON with single quotes is normalized to double quotes when normalization is enabled."""
612+ # Agent should have normalization enabled by default
613+ assert mock_react_agent .replace_single_quotes_with_double_quotes_in_tool_input is True
614+
615+ tool_input_single_quotes = "{'query': 'search term', 'limit': 5}"
616+ mock_state = ReActGraphState (
617+ agent_scratchpad = [AgentAction (tool = 'Tool A' , tool_input = tool_input_single_quotes , log = 'test' )])
618+
619+ response = await mock_react_agent .tool_node (mock_state )
620+ response = response .tool_responses [- 1 ]
621+
622+ assert isinstance (response , ToolMessage )
623+ assert response .name == "Tool A"
624+ # With quote normalization enabled, single quotes get normalized and JSON is parsed successfully
625+ # The mock tool then receives a dict and LangChain extracts the "query" value
626+ assert response .content == "search term"
627+
628+
629+ async def test_tool_node_json_input_with_single_quotes_normalization_disabled (mock_config_react_agent ,
630+ mock_llm ,
631+ mock_tool ):
632+ """Test that JSON with single quotes is NOT normalized when normalization is disabled."""
633+ tools = [mock_tool ('Tool A' ), mock_tool ('Tool B' )]
634+ prompt = create_react_agent_prompt (mock_config_react_agent )
635+
636+ # Create agent with quote normalization disabled
637+ agent = ReActAgentGraph (llm = mock_llm ,
638+ prompt = prompt ,
639+ tools = tools ,
640+ detailed_logs = mock_config_react_agent .verbose ,
641+ replace_single_quotes_with_double_quotes_in_tool_input = False )
642+
643+ assert agent .replace_single_quotes_with_double_quotes_in_tool_input is False
644+
645+ tool_input_single_quotes = "{'query': 'search term', 'limit': 5}"
646+ mock_state = ReActGraphState (
647+ agent_scratchpad = [AgentAction (tool = 'Tool A' , tool_input = tool_input_single_quotes , log = 'test' )])
648+
649+ response = await agent .tool_node (mock_state )
650+ response = response .tool_responses [- 1 ]
651+
652+ assert isinstance (response , ToolMessage )
653+ assert response .name == "Tool A"
654+ # Should use the raw string input since JSON parsing fails and normalization is disabled
655+ assert response .content == tool_input_single_quotes
656+
657+
658+ async def test_tool_node_invalid_json_fallback_to_string (mock_react_agent ):
659+ """Test that invalid JSON falls back to using the raw string input."""
660+ # Invalid JSON that cannot be fixed by quote normalization
661+ tool_input_invalid = "{'query': 'search term', 'limit': }"
662+ mock_state = ReActGraphState (
663+ agent_scratchpad = [AgentAction (tool = 'Tool A' , tool_input = tool_input_invalid , log = 'test' )])
664+
665+ response = await mock_react_agent .tool_node (mock_state )
666+ response = response .tool_responses [- 1 ]
667+
668+ assert isinstance (response , ToolMessage )
669+ assert response .name == "Tool A"
670+ # Should fall back to using the raw string
671+ assert response .content == tool_input_invalid
672+
673+
674+ async def test_tool_node_string_input_no_json_parsing (mock_react_agent ):
675+ """Test that plain string input is used as-is without attempting JSON parsing."""
676+ tool_input_string = "simple string input"
677+ mock_state = ReActGraphState (
678+ agent_scratchpad = [AgentAction (tool = 'Tool A' , tool_input = tool_input_string , log = 'test' )])
679+
680+ response = await mock_react_agent .tool_node (mock_state )
681+ response = response .tool_responses [- 1 ]
682+
683+ assert isinstance (response , ToolMessage )
684+ assert response .name == "Tool A"
685+ assert response .content == tool_input_string
686+
687+
688+ async def test_tool_node_none_input (mock_react_agent ):
689+ """Test that 'None' input is handled correctly."""
690+ tool_input_none = "None"
691+ mock_state = ReActGraphState (agent_scratchpad = [AgentAction (tool = 'Tool A' , tool_input = tool_input_none , log = 'test' )])
692+
693+ response = await mock_react_agent .tool_node (mock_state )
694+ response = response .tool_responses [- 1 ]
695+
696+ assert isinstance (response , ToolMessage )
697+ assert response .name == "Tool A"
698+ assert response .content == tool_input_none
699+
700+
701+ async def test_tool_node_nested_json_with_single_quotes (mock_react_agent ):
702+ """Test that complex nested JSON with single quotes is normalized correctly."""
703+ # Complex nested JSON with single quotes - doesn't have a "query" field so would return the full dict
704+ tool_input_nested = \
705+ "{'user': {'name': 'John', 'preferences': {'theme': 'dark', 'notifications': True}}, 'action': 'update'}"
706+ mock_state = ReActGraphState (
707+ agent_scratchpad = [AgentAction (tool = 'Tool A' , tool_input = tool_input_nested , log = 'test' )])
708+
709+ response = await mock_react_agent .tool_node (mock_state )
710+ response = response .tool_responses [- 1 ]
711+
712+ assert isinstance (response , ToolMessage )
713+ assert response .name == "Tool A"
714+ # Since this JSON doesn't have a "query" field, the mock tool receives the full dict
715+ # and LangChain can't extract a "query" parameter, so it falls back to default behavior
716+ assert "John" in str (response .content ) or isinstance (response .content , dict )
717+
718+
719+ async def test_tool_node_mixed_quotes_in_json (mock_config_react_agent , mock_llm , mock_tool ):
720+ """Test that JSON with mixed quotes is handled appropriately."""
721+ # This creates a scenario with mixed quotes that might be challenging to normalize
722+ tools = [mock_tool ('Tool A' )]
723+ prompt = create_react_agent_prompt (mock_config_react_agent )
724+
725+ agent = ReActAgentGraph (llm = mock_llm , prompt = prompt , tools = tools , detailed_logs = False )
726+
727+ # Mixed quotes - this is challenging JSON to normalize
728+ tool_input_mixed = '''{'outer': "inner string with 'nested quotes'", 'number': 42}'''
729+ mock_state = ReActGraphState (agent_scratchpad = [AgentAction (tool = 'Tool A' , tool_input = tool_input_mixed , log = 'test' )])
730+
731+ response = await agent .tool_node (mock_state )
732+ response = response .tool_responses [- 1 ]
733+
734+ assert isinstance (response , ToolMessage )
735+ assert response .name == "Tool A"
736+ # Mixed quotes are complex to normalize, so it likely falls back to raw string input
737+ assert response .content == tool_input_mixed
738+
739+
740+ async def test_tool_node_whitespace_handling (mock_react_agent ):
741+ """Test that whitespace in tool input is handled correctly."""
742+ # Tool input with leading/trailing whitespace
743+ tool_input_whitespace = " {'query': 'search term'} "
744+ mock_state = ReActGraphState (
745+ agent_scratchpad = [AgentAction (tool = 'Tool A' , tool_input = tool_input_whitespace , log = 'test' )])
746+
747+ response = await mock_react_agent .tool_node (mock_state )
748+ response = response .tool_responses [- 1 ]
749+
750+ assert isinstance (response , ToolMessage )
751+ assert response .name == "Tool A"
752+ # With whitespace trimmed and quote normalization, JSON is parsed and "query" value is extracted
753+ assert response .content == "search term"
754+
755+
756+ def test_config_replace_single_quotes_default ():
757+ """Test that replace_single_quotes_with_double_quotes_in_tool_input defaults to True."""
758+ config = ReActAgentWorkflowConfig (tool_names = ['test' ], llm_name = 'test' )
759+ assert config .replace_single_quotes_with_double_quotes_in_tool_input is True
760+
761+
762+ def test_config_replace_single_quotes_explicit_false ():
763+ """Test that replace_single_quotes_with_double_quotes_in_tool_input can be set to False."""
764+ config = ReActAgentWorkflowConfig (tool_names = ['test' ],
765+ llm_name = 'test' ,
766+ replace_single_quotes_with_double_quotes_in_tool_input = False )
767+ assert config .replace_single_quotes_with_double_quotes_in_tool_input is False
768+
769+
770+ def test_react_agent_init_with_quote_normalization_param (mock_config_react_agent , mock_llm , mock_tool ):
771+ """Test that ReActAgentGraph initialization respects the quote normalization parameter."""
772+ tools = [mock_tool ('Tool A' ), mock_tool ('Tool B' )]
773+ prompt = create_react_agent_prompt (mock_config_react_agent )
774+
775+ # Test with normalization enabled
776+ agent_enabled = ReActAgentGraph (llm = mock_llm ,
777+ prompt = prompt ,
778+ tools = tools ,
779+ detailed_logs = False ,
780+ replace_single_quotes_with_double_quotes_in_tool_input = True )
781+ assert agent_enabled .replace_single_quotes_with_double_quotes_in_tool_input is True
782+
783+ # Test with normalization disabled
784+ agent_disabled = ReActAgentGraph (llm = mock_llm ,
785+ prompt = prompt ,
786+ tools = tools ,
787+ detailed_logs = False ,
788+ replace_single_quotes_with_double_quotes_in_tool_input = False )
789+ assert agent_disabled .replace_single_quotes_with_double_quotes_in_tool_input is False
790+
791+
792+ # Additional test to specifically verify the JSON parsing logic with quote normalization
793+ async def test_quote_normalization_json_parsing_logic (mock_config_react_agent , mock_llm ):
794+ """Test the specific quote normalization logic in JSON parsing."""
795+ from langchain_core .tools import BaseTool
796+
797+ # Create a custom tool that returns the exact input it receives
798+ class ExactInputTool (BaseTool ):
799+ name : str = "ExactInputTool"
800+ description : str = "Returns exactly what it receives"
801+
802+ async def _arun (self , query , ** kwargs ):
803+ return f"Received: { query } (type: { type (query ).__name__ } )"
804+
805+ def _run (self , query , ** kwargs ):
806+ return f"Received: { query } (type: { type (query ).__name__ } )"
807+
808+ tools = [ExactInputTool ()]
809+ prompt = create_react_agent_prompt (mock_config_react_agent )
810+
811+ # Test with quote normalization enabled
812+ agent_enabled = ReActAgentGraph (llm = mock_llm ,
813+ prompt = prompt ,
814+ tools = tools ,
815+ detailed_logs = False ,
816+ replace_single_quotes_with_double_quotes_in_tool_input = True )
817+
818+ # Test with single quotes - should be normalized and parsed as JSON
819+ tool_input_single = "{'query': 'test', 'count': 42}"
820+ mock_state = ReActGraphState (
821+ agent_scratchpad = [AgentAction (tool = 'ExactInputTool' , tool_input = tool_input_single , log = 'test' )])
822+ response = await agent_enabled .tool_node (mock_state )
823+ response_content = response .tool_responses [- 1 ].content
824+
825+ # Should receive the "query" field value from the parsed JSON dict
826+ # This proves that quote normalization worked and JSON was successfully parsed
827+ assert "Received: test (type: str)" in response_content
828+
829+ # Test with quote normalization disabled
830+ agent_disabled = ReActAgentGraph (llm = mock_llm ,
831+ prompt = prompt ,
832+ tools = tools ,
833+ detailed_logs = False ,
834+ replace_single_quotes_with_double_quotes_in_tool_input = False )
835+
836+ response = await agent_disabled .tool_node (mock_state )
837+ response_content = response .tool_responses [- 1 ].content
838+
839+ # Should receive the raw string (JSON parsing failed due to no normalization)
840+ # The full JSON string should be passed as the query parameter
841+ assert tool_input_single in response_content and "type: str" in response_content
0 commit comments